pax_global_header00006660000000000000000000000064151160062150014507gustar00rootroot0000000000000052 comment=3cb22eefd72ae6e00d50dbc8ea9bbc5c91c75b76 transparency-dev-tessera-3cb22ee/000077500000000000000000000000001511600621500171275ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/.github/000077500000000000000000000000001511600621500204675ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/.github/dependabot.yml000066400000000000000000000004471511600621500233240ustar00rootroot00000000000000version: 2 updates: - package-ecosystem: gomod directory: / groups: all-go-deps: patterns: - "*" schedule: interval: weekly - package-ecosystem: github-actions directory: / groups: all-gha-deps: patterns: - "*" schedule: interval: weekly transparency-dev-tessera-3cb22ee/.github/workflows/000077500000000000000000000000001511600621500225245ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/.github/workflows/aws_integration_test.yml000066400000000000000000000154061511600621500275110ustar00rootroot00000000000000name: AWS Conformance Test on: push: branches: - main # This prevents two workflows from running at the same time. # This workflows calls terragrunt, which does not allow concurrent runs. concurrency: group: aws-conformance cancel-in-progress: false permissions: id-token: write contents: read env: TOFU_VERSION: "1.10.0" TG_VERSION: "0.77.22" TG_DIR: "deployment/live/aws/conformance/ci/" TESSERA_PREFIX_NAME: trillian-tessera ECR_REGISTRY: 864981736166.dkr.ecr.us-east-1.amazonaws.com ECR_REPOSITORY_CONFORMANCE: trillian-tessera/conformance:latest ECR_REPOSITORY_HAMMER: trillian-tessera/hammer:latest AWS_REGION: us-east-1 jobs: aws-integration: runs-on: ubuntu-latest steps: - name: Configure AWS credentials uses: aws-actions/configure-aws-credentials@61815dcd50bd041e203e49132bacad1fd04d2708 # v5.1.1 with: role-to-assume: ${{ vars.AWS_IAMROLE_GITHUB_CI }} aws-region: ${{ env.AWS_REGION }} - name: Checkout code uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false ## Authenticate with ECR to push the conformance and hammer images. - name: Login to Amazon ECR id: login-ecr uses: aws-actions/amazon-ecr-login@062b18b96a7aff071d4dc91bc00c4c1a7945b076 # v2.0.1 ## Build the conformance image and push it to ECR. This will be used ## later on by Terragrunt. - name: Build, tag, and push Conformance image to Amazon ECR id: build-publish-conformance shell: bash env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: ${{ env.ECR_REPOSITORY_CONFORMANCE }} run: | docker build -f ./cmd/conformance/aws/Dockerfile . -t "$ECR_REGISTRY/$ECR_REPOSITORY" docker push "$ECR_REGISTRY/$ECR_REPOSITORY" echo "Pushed image to $ECR_REGISTRY/$ECR_REPOSITORY" ## Build the hammer image and push it to ECR. This will be used ## later on by Terragrunt. - name: Build, tag, and push Hammer image to Amazon ECR id: build-publish-hammer shell: bash env: ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} ECR_REPOSITORY: ${{ env.ECR_REPOSITORY_HAMMER }} run: | docker build -f ./internal/hammer/Dockerfile . -t "$ECR_REGISTRY/$ECR_REPOSITORY" docker push "$ECR_REGISTRY/$ECR_REPOSITORY" echo "Pushed image to $ECR_REGISTRY/$ECR_REPOSITORY" ## Destroy any pre-existing deployment/live/aws/conformance/ci env. ## This might happen if a previous integration test workflow has failed. - name: Terragrunt destroy pre conformance test id: terragrunt-destroy-pre uses: gruntwork-io/terragrunt-action@95fc057922e3c3d4cc021a81a213f088f333ddef # v3.0.2 with: tofu_version: ${{ env.TOFU_VERSION }} tg_version: ${{ env.TG_VERSION }} tg_dir: ${{ env.TG_DIR }} tg_command: "destroy" env: ECS_EXECUTION_ROLE: ${{ vars.AWS_IAMROLE_ECS_EXECUTION }} ECS_CONFORMANCE_TASK_ROLE: ${{ vars.AWS_IAMROLE_ECS_CONFORMANCE_TASK }} TESSERA_SIGNER: unused TESSERA_VERIFIER: unused ## Generate a new keys for the log to use, and export them to environment ## variables for Terragrunt to use. - name: Generate Tessera keys id: generate-keys shell: bash run: | go run github.com/transparency-dev/serverless-log/cmd/generate_keys@80334bc9dc573e8f6c5b3694efad6358da50abd4 \ --key_name=tessera/test/conformance \ --out_priv=${{ runner.temp }}/key.sec \ --out_pub=${{ runner.temp }}/key.pub cat ${{ runner.temp }}/key.pub echo "TESSERA_SIGNER=$(cat ${{ runner.temp }}/key.sec)" >> "$GITHUB_ENV" echo "TESSERA_VERIFIER=$(cat ${{ runner.temp }}/key.pub)" >> "$GITHUB_ENV" ## Apply the deployment/live/aws/conformance/ci terragrunt config. ## This will bring up the conformance infrastructure which consists of: ## - the storage module ## - a private S3 <--> ECS network link for the hammer to read the log ## - an ECS cluster to run Fargate tasks ## - a conformance service, with multiple conformance binary instances ## - a hammer task definition (but no execution) # TODO(phboneff): AuroraDB takes a long time to be brought up and down # consider keeping it around between tests / using Aurora Serveless - name: Terragrunt apply id: terragrunt-apply uses: gruntwork-io/terragrunt-action@95fc057922e3c3d4cc021a81a213f088f333ddef # v3.0.2 with: tofu_version: ${{ env.TOFU_VERSION }} tg_version: ${{ env.TG_VERSION }} tg_dir: ${{ env.TG_DIR }} tg_command: "apply" env: ECS_EXECUTION_ROLE: ${{ vars.AWS_IAMROLE_ECS_EXECUTION }} ECS_CONFORMANCE_TASK_ROLE: ${{ vars.AWS_IAMROLE_ECS_CONFORMANCE_TASK }} INPUT_POST_EXEC_1: | echo "ECS_CLUSTER=$(terragrunt output -raw ecs_cluster)" >> "$GITHUB_ENV" INPUT_POST_EXEC_2: | echo "VPC_SUBNETS=$(terragrunt output -json vpc_subnets)" >> "$GITHUB_ENV" ## Now we can run the hammer using the task definition, against the ## conformance service. This step returns the hammer task's exit code. - name: Run Hammer id: hammer shell: bash run: | cat ${{ runner.temp }}/key.pub echo "Will launch a hammer ECS task." HAMMER_ARN=$(aws ecs run-task \ --cluster="$ECS_CLUSTER" \ --task-definition=hammer \ --count=1 \ --launch-type=FARGATE \ --network-configuration='{"awsvpcConfiguration": {"assignPublicIp":"ENABLED","subnets": '$VPC_SUBNETS'}}' \ --query 'tasks[0].taskArn') echo "Hammer task running, ARN: $HAMMER_ARN." echo "Waiting for task to stop..." aws ecs wait tasks-stopped --cluster="$ECS_CLUSTER" --tasks=[$HAMMER_ARN] echo "The task has stopped. Fetching exit code and returning this action with it." exit $(aws ecs describe-tasks --cluster="$ECS_CLUSTER" --tasks=[$HAMMER_ARN] --query 'tasks[0].containers[0].exitCode') - name: Terragrunt destroy post conformance test id: terragrunt-destroy-post uses: gruntwork-io/terragrunt-action@95fc057922e3c3d4cc021a81a213f088f333ddef # v3.0.2 with: tofu_version: ${{ env.TOFU_VERSION }} tg_version: ${{ env.TG_VERSION }} tg_dir: ${{ env.TG_DIR }} tg_command: "destroy" env: ECS_EXECUTION_ROLE: ${{ vars.AWS_IAMROLE_ECS_EXECUTION }} ECS_CONFORMANCE_TASK_ROLE: ${{ vars.AWS_IAMROLE_ECS_CONFORMANCE_TASK }} transparency-dev-tessera-3cb22ee/.github/workflows/benchmark-go-main.yml000066400000000000000000000022421511600621500265260ustar00rootroot00000000000000name: Benchmark Go (main) on: push: branches: - main permissions: contents: read jobs: benchmark: name: Performance regression check runs-on: ubuntu-latest permissions: # deployments permission to deploy GitHub pages website deployments: write # contents permission to update benchmark contents in gh-pages branch contents: write steps: - name: Fetch Repo uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Install Go uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version-file: go.mod - name: Run benchmark run: set -o pipefail; go test ./... -benchmem -run=^$ -bench . | tee output.txt - name: Store benchmark result uses: benchmark-action/github-action-benchmark@4bdcce38c94cec68da58d012ac24b7b1155efe8b # v1.20.7 with: tool: 'go' output-file-path: output.txt github-token: ${{ secrets.GITHUB_TOKEN }} auto-push: true alert-threshold: "150%" fail-on-alert: true transparency-dev-tessera-3cb22ee/.github/workflows/benchmark-go-pr.yml000066400000000000000000000024361511600621500262300ustar00rootroot00000000000000name: Benchmark Go (PR) on: pull_request: branches: - main permissions: contents: read jobs: benchmark: name: Performance regression check runs-on: ubuntu-latest permissions: # allow posting comments to pull request pull-requests: write steps: - name: Fetch Repo uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Install Go uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version-file: go.mod - name: Run benchmark run: set -o pipefail; go test ./... -benchmem -run=^$ -bench . | tee output.txt - name: Store benchmark result uses: benchmark-action/github-action-benchmark@4bdcce38c94cec68da58d012ac24b7b1155efe8b # v1.20.7 with: tool: 'go' output-file-path: output.txt github-token: ${{ secrets.GITHUB_TOKEN }} auto-push: false alert-threshold: "150%" fail-on-alert: false # Don't make red crosses on the PR, it's almost certainly a false positive currently comment-on-alert: true # notify on PR if alert triggers summary-always: true # always comment on PRs to leave job summary transparency-dev-tessera-3cb22ee/.github/workflows/benchmark.yml000066400000000000000000000031731511600621500252050ustar00rootroot00000000000000name: Benchmark on: [push, pull_request] permissions: contents: read jobs: benchmark-mysql: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Start Tessera run: docker compose -f ./cmd/conformance/mysql/docker/compose.yaml up --build --detach - name: Run benchmark run: go run ./internal/hammer --log_public_key=transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW --log_url=http://localhost:2024 --max_read_ops=0 --num_writers=512 --max_write_ops=512 --max_runtime=1m --leaf_write_goal=2500 --show_ui=false - name: Stop Tessera if: ${{ always() }} run: docker compose -f ./cmd/conformance/mysql/docker/compose.yaml down benchmark-posix: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Start Tessera run: docker compose -f ./cmd/conformance/posix/docker/compose.yaml up --build --detach - name: Run benchmark run: go run ./internal/hammer --log_public_key=example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx --log_url=http://localhost:2025 --max_read_ops=0 --num_writers=512 --max_write_ops=512 --max_runtime=1m --leaf_write_goal=2500 --show_ui=false - name: Stop Tessera if: ${{ always() }} run: docker compose -f ./cmd/conformance/posix/docker/compose.yaml down transparency-dev-tessera-3cb22ee/.github/workflows/codeql.yml000066400000000000000000000106541511600621500245240ustar00rootroot00000000000000# For most projects, this workflow file will not need changing; you simply need # to commit it to your repository. # # You may wish to alter this file to override the set of languages analyzed, # or to provide custom queries or build logic. # # ******** NOTE ******** # We have attempted to detect the languages in your repository. Please check # the `language` matrix defined below to confirm you have the correct set of # supported CodeQL languages. # name: "CodeQL Advanced" on: push: branches: [ "main" ] pull_request: branches: [ "main" ] schedule: - cron: '15 10 * * 1' permissions: contents: read jobs: analyze: name: Analyze (${{ matrix.language }}) # Runner size impacts CodeQL analysis time. To learn more, please see: # - https://gh.io/recommended-hardware-resources-for-running-codeql # - https://gh.io/supported-runners-and-hardware-resources # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} permissions: # required for all workflows security-events: write # required to fetch internal or private CodeQL packs packages: read # only required for workflows in private repositories actions: read contents: read strategy: fail-fast: false matrix: include: - language: go build-mode: autobuild # CodeQL supports the following values keywords for 'language': 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' # Use `c-cpp` to analyze code written in C, C++ or both # Use 'java-kotlin' to analyze code written in Java, Kotlin or both # Use 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both # To learn more about changing the languages that are analyzed or customizing the build mode for your analysis, # see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/customizing-your-advanced-setup-for-code-scanning. # If you are analyzing a compiled language, you can modify the 'build-mode' for that language to customize how # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages steps: - name: Checkout repository uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL uses: github/codeql-action/init@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} # If you wish to specify custom queries, you can do so here or in a config file. # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs # queries: security-extended,security-and-quality # If the analyze step fails for one of the languages you are analyzing with # "We were unable to automatically build your code", modify the matrix above # to set the build mode to "manual" for that language. Then modify this step # to build your code. # ℹ️ Command-line programs to run using the OS shell. # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun - if: matrix.build-mode == 'manual' shell: bash run: | echo 'If you are using a "manual" build mode for one or more of the' \ 'languages you are analyzing, replace this with the commands to build' \ 'your code, for example:' echo ' make bootstrap' echo ' make release' exit 1 - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 with: category: "/language:${{matrix.language}}" transparency-dev-tessera-3cb22ee/.github/workflows/generated_files.yml000066400000000000000000000011011511600621500263600ustar00rootroot00000000000000name: generatedfiles on: push: branches: - main pull_request: branches: - main permissions: contents: read jobs: generatedfiles_job: runs-on: ubuntu-latest name: Regenerate derived files steps: - name: Check out the repository to the runner uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Regenerate the log test data run: ./testdata/build_log.sh - name: Confirm there are no diffs run: git diff --exit-code transparency-dev-tessera-3cb22ee/.github/workflows/go_test.yml000066400000000000000000000036761511600621500247270ustar00rootroot00000000000000name: Test Go on: [push, pull_request] permissions: contents: read jobs: test: strategy: matrix: go-version: [ '1.24.x', '1.25.x' ] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Fetch repo uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Install Go uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version: ${{ matrix.go-version }} - name: Run tests run: go test -v -race ./... test-mysql: env: DB_DATABASE: test_tessera DB_USER: root DB_PASSWORD: root runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Start MySQL run: | sudo /etc/init.d/mysql start mysql -e "CREATE DATABASE IF NOT EXISTS $DB_DATABASE;" -u$DB_USER -p$DB_PASSWORD - name: Test with Go # Parallel tests are disabled for the MySQL test database to always be in a known state. run: go test -p=1 -v -race ./storage/mysql/... -is_mysql_test_optional=true test-aws-mysql: env: DB_DATABASE: test_tessera DB_USER: root DB_PASSWORD: root runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Start MySQL run: | sudo /etc/init.d/mysql start mysql -e "CREATE DATABASE IF NOT EXISTS $DB_DATABASE;" -u$DB_USER -p$DB_PASSWORD - name: Test with Go # Parallel tests are disabled for the MySQL test database to always be in a known state. run: go test -p=1 -v -race ./storage/aws/... -is_mysql_test_optional=false transparency-dev-tessera-3cb22ee/.github/workflows/golangci-lint.yml000066400000000000000000000012161511600621500257760ustar00rootroot00000000000000name: golangci-lint on: push: pull_request: permissions: contents: read jobs: golangci: name: lint runs-on: ubuntu-latest steps: - name: Fetch repo uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Install Go uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 with: go-version-file: go.mod - name: golangci-lint uses: golangci/golangci-lint-action@e7fa5ac41e1cf5b7d48e45e42232ce7ada589601 # v9.1.0 with: version: v2.1.0 args: --timeout=8m transparency-dev-tessera-3cb22ee/.github/workflows/govulncheck.yml000066400000000000000000000006341511600621500255620ustar00rootroot00000000000000name: govulncheck on: push: branches: - main pull_request: branches: - main permissions: contents: read jobs: govulncheck_job: runs-on: ubuntu-latest name: Run govulncheck steps: - id: govulncheck uses: golang/govulncheck-action@b625fbe08f3bccbe446d94fbf87fcc875a4f50ee # v1.0.4 with: go-version-file: go.mod go-package: ./... transparency-dev-tessera-3cb22ee/.github/workflows/integration_test.yml000066400000000000000000000043401511600621500266320ustar00rootroot00000000000000name: Integration Test on: [push, pull_request] permissions: contents: read jobs: mysql-tlog-tiles-api: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Start Docker services (tessera-conformance-mysql-db and tessera-conformance-mysql) run: docker compose -f ./cmd/conformance/mysql/docker/compose.yaml up --build --detach - name: Run integration test run: go test -v -race ./integration/. --run_integration_test=true --log_url="http://localhost:2024" --write_log_url="http://localhost:2024" --log_public_key="transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW" - name: Stop Docker services (tessera-conformance-mysql-db and tessera-conformance-mysql) if: ${{ always() }} run: docker compose -f ./cmd/conformance/mysql/docker/compose.yaml down posix-tlog-tiles-api: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Start Docker services (tessera-conformance-posix) run: docker compose -f ./cmd/conformance/posix/docker/compose.yaml up --build --detach - name: Run integration test run: go test -v -race ./integration/. --run_integration_test=true --log_url="file:///tmp/tessera-posix-log" --write_log_url="http://localhost:2025" --log_public_key="example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx" - name: What's in the box? if: ${{ always() }} run: tree /tmp/tessera-posix-log - name: Stop Docker services (tessera-conformance-posix) if: ${{ always() }} run: docker compose -f ./cmd/conformance/posix/docker/compose.yaml down posix-fault-injection: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Run fault-injection test run: go test -v -race ./integration/fault/posix/... transparency-dev-tessera-3cb22ee/.github/workflows/scorecard.yml000066400000000000000000000057111511600621500252200ustar00rootroot00000000000000# This workflow uses actions that are not certified by GitHub. They are provided # by a third-party and are governed by separate terms of service, privacy # policy, and support documentation. name: Scorecard supply-chain security on: # For Branch-Protection check. Only the default branch is supported. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection branch_protection_rule: # To guarantee Maintained check is occasionally updated. See # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained schedule: - cron: '41 3 * * 3' push: branches: [ "main" ] permissions: contents: read jobs: analysis: name: Scorecard analysis runs-on: ubuntu-latest permissions: # Needed to upload the results to code-scanning dashboard. security-events: write # Needed to publish results and get a badge (see publish_results below). id-token: write # Needed to check out code contents: read steps: - name: "Checkout code" uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: "Run analysis" uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3 with: results_file: results.sarif results_format: sarif # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: # - you want to enable the Branch-Protection check on a *public* repository, or # - you are installing Scorecard on a *private* repository # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. # repo_token: ${{ secrets.SCORECARD_TOKEN }} # Public repositories: # - Publish results to OpenSSF REST API for easy access by consumers # - Allows the repository to include the Scorecard badge. # - See https://github.com/ossf/scorecard-action#publishing-results. # For private repositories: # - `publish_results` will always be set to `false`, regardless # of the value entered here. publish_results: true # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF # format to the repository Actions tab. - name: "Upload artifact" uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 with: name: SARIF file path: results.sarif retention-days: 5 # Upload the results to GitHub's code scanning dashboard (optional). # Commenting out will disable upload of results to your repo's Code Scanning dashboard - name: "Upload to code-scanning" uses: github/codeql-action/upload-sarif@fe4161a26a8629af62121b670040955b330f9af2 # v4.31.6 with: sarif_file: results.sarif transparency-dev-tessera-3cb22ee/.github/workflows/terragrunt_test.yml000066400000000000000000000013251511600621500265040ustar00rootroot00000000000000name: 'Terragrunt format check' on: - pull_request permissions: contents: read env: tofu_version: '1.10.0' tg_version: '0.77.22' jobs: checks: runs-on: ubuntu-latest permissions: contents: read steps: - name: 'Checkout' uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6.0.0 with: persist-credentials: false - name: Check terragrunt HCL uses: gruntwork-io/terragrunt-action@95fc057922e3c3d4cc021a81a213f088f333ddef # v3.0.2 with: tofu_version: ${{ env.tofu_version }} tg_version: ${{ env.tg_version }} tg_dir: 'deployment' tg_command: 'hclfmt --terragrunt-check --terragrunt-diff' transparency-dev-tessera-3cb22ee/.gitignore000066400000000000000000000012411511600621500211150ustar00rootroot00000000000000# If you prefer the allow list template instead of the deny list, see community template: # https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore # # Binaries for programs and plugins *.exe *.exe~ *.dll *.so *.dylib # Test binary, built with `go test -c` *.test # Output of the go coverage tool, specifically when used with LiteIDE *.out # Dependency directories (remove the comment below to include it) # vendor/ # Go workspace file go.work go.work.sum # Project IDX .idx/ # Terraform and terragrunt files /deployment/.terraform /deployment/*.tfstate.backup /deployment/**/.terragrunt-cache # lockfiles /testdata/log/checkpoint.lock transparency-dev-tessera-3cb22ee/AUTHORS000066400000000000000000000006001511600621500201730ustar00rootroot00000000000000# This is the official list of benchmark authors for copyright purposes. # This file is distinct from the CONTRIBUTORS files. # See the latter for an explanation. # # Names should be added to this file as: # Name or Organization # The email address is not required for organizations. # # Please keep the list sorted. Google LLC Internet Security Research Group transparency-dev-tessera-3cb22ee/CODEOWNERS000066400000000000000000000006671511600621500205330ustar00rootroot00000000000000# See https://help.github.com/articles/about-codeowners/ # for more info about CODEOWNERS file # It uses the same pattern rule for gitignore file # https://git-scm.com/docs/gitignore#_pattern_format # # These owners will be the default owners for everything in # the repo. Unless a later match takes precedence, # @transparency-dev/core-team will be requested for # review when someone opens a pull request. * @transparency-dev/core-team transparency-dev-tessera-3cb22ee/CONTRIBUTING.md000066400000000000000000000063261511600621500213670ustar00rootroot00000000000000# How to contribute We'd love to accept your patches and contributions to this project. There are a just a few small guidelines you need to follow. ## Getting Started If you're new to the project, a good place to start is the [main `README.md`](/README.md) file. This will give you a high-level overview of the project and its goals. Before you start coding, you'll need to set up your development environment. This project uses Go, so you'll need to have a recent version of Go installed. You can find instructions on how to do that [here](https://golang.org/doc/install). Once you have Go installed, you can clone the repository: ```bash git clone https://github.com/transparency-dev/tessera.git ``` ## Contributor License Agreement Contributions to any Google project must be accompanied by a Contributor License Agreement. This is not a copyright **assignment**, it simply gives Google permission to use and redistribute your contributions as part of the project. * If you are an individual writing original source code and you're sure you own the intellectual property, then you'll need to sign an [individual CLA][]. * If you work for a company that wants to allow you to contribute your work, then you'll need to sign a [corporate CLA][]. You generally only need to submit a CLA once, so if you've already submitted one (even if it was for a different project), you probably don't need to do it again. [individual CLA]: https://developers.google.com/open-source/cla/individual [corporate CLA]: https://developers.google.com/open-source/cla/corporate Once your CLA is submitted (or if you already submitted one for another Google project), make a commit adding yourself to the [AUTHORS][] and [CONTRIBUTORS][] files. This commit can be part of your first [pull request][]. [AUTHORS]: AUTHORS [CONTRIBUTORS]: CONTRIBUTORS ## Submitting a patch 1. It's generally best to start by opening a new issue describing the bug or feature you're intending to fix. Even if you think it's relatively minor, it's helpful to know what people are working on. Mention in the initial issue that you are planning to work on that bug or feature so that it can be assigned to you. 1. Follow the normal process of [forking][] the project, and setup a new branch to work in. It's important that each group of changes be done in separate branches in order to ensure that a pull request only includes the commits related to that bug or feature. 1. Do your best to have [well-formed commit messages][] for each change. This provides consistency throughout the project, and ensures that commit messages are able to be formatted properly by various git tools. 1. Finally, push the commits to your fork and submit a [pull request][]. ## Code Style This project follows the standard Go code style. You can use `gofmt` to format your code before submitting a pull request. We also use `golangci-lint` to check for common issues. You can run it locally with: ```bash golangci-lint run ``` [forking]: https://help.github.com/articles/fork-a-repo [well-formed commit messages]: http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html [pull request]: https://help.github.com/articles/creating-a-pull-request transparency-dev-tessera-3cb22ee/CONTRIBUTORS000066400000000000000000000021051511600621500210050ustar00rootroot00000000000000# People who have agreed to one of the CLAs and can contribute patches. # The AUTHORS file lists the copyright holders; this file # lists people. For example, Google employees are listed here # but not in AUTHORS, because Google holds the copyright. # # Names should be added to this file only after verifying that # the individual or the individual's organization has agreed to # the appropriate Contributor License Agreement, found here: # # https://developers.google.com/open-source/cla/individual # https://developers.google.com/open-source/cla/corporate # # The agreement for individuals can be filled out on the web. # # When adding J Random Contributor's name to this file, # either J's name or J's organization's name should be # added to the AUTHORS file, depending on whether the # individual or corporate CLA was used. # # Names should be added to this file as: # Name # # Please keep the list sorted. Al Cutter Martin Hutchinson Roger Ng transparency-dev-tessera-3cb22ee/LICENSE000066400000000000000000000261351511600621500201430ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. transparency-dev-tessera-3cb22ee/README.md000066400000000000000000000636521511600621500204220ustar00rootroot00000000000000# Tessera [![Go Report Card](https://goreportcard.com/badge/github.com/transparency-dev/tessera)](https://goreportcard.com/report/github.com/transparency-dev/tessera) [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/transparency-dev/tessera/badge)](https://scorecard.dev/viewer/?uri=github.com/transparency-dev/tessera) [![Benchmarks](https://img.shields.io/badge/Benchmarks-blue.svg)](https://transparency-dev.github.io/tessera/dev/bench/) [![Slack Status](https://img.shields.io/badge/Slack-Chat-blue.svg)](https://transparency-dev.slack.com/) Tessera is a Go library for building [tile-based transparency logs (tlogs)](https://c2sp.org/tlog-tiles). It is the logical successor to the approach [Trillian v1][] takes in building and operating logs. The implementation and its APIs bake-in [current best-practices based on the lessons learned](https://transparency.dev/articles/tile-based-logs/) over the past decade of building and operating transparency logs in production environments and at scale. Tessera was introduced at the Transparency.Dev summit in October 2024. Watch [Introducing Tessera](https://www.youtube.com/watch?v=9j_8FbQ9qSc) for all the details, but here's a summary of the high level goals: * [tlog-tiles API][] and storage * Support for both cloud and on-premises infrastructure * [GCP](./storage/gcp/) * [AWS](./storage/aws/) * [MySQL](./storage/mysql/) * [POSIX](./storage/posix/) * Make it easy to build and deploy new transparency logs on supported infrastructure * Library instead of microservice architecture * No additional services to manage * Lower TCO for operators compared with Trillian v1 * Fast sequencing and integration of entries * Optional functionality which can be enabled for those ecosystems/logs which need it (only pay the cost for what you need): * "Best-effort" de-duplication of entries * Synchronous integration * Broadly similar write-throughput and write-availability, and potentially _far_ higher read-throughput and read-availability compared to Trillian v1 (dependent on underlying infrastructure) * Enable building of arbitrary log personalities, including support for the peculiarities of a [Static CT API][] compliant log. The main non-goal is to support transparency logs using anything other than the [tlog-tiles API][]. While it is possible to deploy a custom personality in front of Tessera that adapts the tlog-tiles API into any other API, this strategy will lose a lot of the read scaling that Tessera is designed for. ## Table of Contents - [Status](#status) - [Roadmap](#roadmap) - [Concepts](#concepts) - [Usage](#usage) - [Getting Started](#getting-started) - [Writing Personalities](#writing-personalities) - [Features](#features) - [Lifecycles](#lifecycles) - [Contributing](#contributing) - [License](#license) - [Contact](#contact) ## Status Tessera is under active development, and is considered production ready since the [Beta release](https://github.com/transparency-dev/tessera/releases/tag/v0.2.0). See the table below for details. ### Storage drivers | Driver | Appender | Migration | Antispam | Garbage Collection | Notes | | ----------------------- | :------: | :-------: | :------: | :----------------: | --------------------------------------------- | | Amazon Web Services | ✅ | ⚠️ | ✅ | ✅ | | | Google Cloud Platform | ✅ | ⚠️ | ✅ | ✅ | | | POSIX filesystem | ✅ | ⚠️ | ✅ | ✅ | | | MySQL | ⚠️ | ⚠️ | ❌ | N/A | MySQL will remain in BETA for the time being. | > [!Note] > Please get in touch if you are interested in using any of the features or drivers held back in BETA above. Users of GCP, AWS, MySQL, and POSIX are welcome to try the relevant [Getting Started](#getting-started) guide. ## Roadmap Production ready around mid 2025. | # | Step | Status | | :-: | --------------------------------------------------------- | :----: | | 1 | Drivers for GCP, AWS, MySQL, and POSIX | ✅ | | 2 | [tlog-tiles API][] support | ✅ | | 3 | Example code and terraform scripts for easy onboarding | ✅ | | 4 | Stable API | ✅ | | 5 | Data migration between releases | ✅ | | 6 | Data migration between drivers | ✅ | | 7 | Witness support | ✅ | | 8 | Monitoring and metrics | ✅ | | 9 | Production ready | ✅ | | 10 | Mirrored logs (#576) | ⚠️ | | 11 | Preordered logs (#575) | ❌ | | 12 | Trillian v1 to Tessera migration (#577) | ❌ | | N | Fancy features (to be expanded upon later) | ❌ | The current API is unlikely to change in any significant way, however the API is subject to minor breaking changes until we tag 1.0. ### What’s happening to Trillian v1? [Trillian v1][] is still in use in production environments by multiple organisations in multiple ecosystems, and is likely to remain so for the mid-term. New ecosystems, or existing ecosystems looking to evolve, should strongly consider planning a migration to Tessera and adopting the patterns it encourages. > [!Tip] > To achieve the full benefits of Tessera, logs must use the [tlog-tiles API][]. ## Concepts This section introduces concepts and terms that will be used throughout the user guide. ### Sequencing When data is added to a log, it is first stored in memory for some period (this can be controlled via the [batching options](https://pkg.go.dev/github.com/transparency-dev/tessera#WithBatching)). If the process dies in this state, the entry will be lost. Once a batch of entries is processed by the sequencer, the new data will transition from a volatile state to one where it is durably assigned an index. If the process dies in this state, the entry will be safe, though it will not be available through the read API of the log until the leaf has been [Integrated](#integration). Once an index number has been issued to a leaf, no other data will ever be issued the same index number. All index numbers are contiguous and start from 0. > [!IMPORTANT] > Within a batch, there is no guarantee about which order index numbers will be assigned. > The only way to ensure that sequential calls to `Add` are given sequential indices is by blocking until a sequencing batch is completed. > This can be achieved by configuring a batch size of 1, though this will make sequencing expensive! ### Integration Integration is a background process that happens when a Tessera lifecycle object has been created. This process takes sequenced entries and merges them into the log. Once this process has been completed, a new entry will: - Be available via the read API at the index that was returned from sequencing - Have Merkle tree hashes that commit to this data being included in the tree ### Publishing Publishing is a background process that creates a new Checkpoint for the latest tree. This background process runs periodically (configurable via [WithCheckpointInterval](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithCheckpointInterval) and [WithCheckpointRepublishInterval](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithCheckpointRepublishInterval)) and performs the following steps: 1. Create a new Checkpoint and sign it with the signer provided by [WithCheckpointSigner](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithCheckpointSigner) 2. Contact witnesses and collect enough countersignatures to satisfy any witness policy configured by [WithWitnesses](https://pkg.go.dev/github.com/transparency-dev/tessera#AppendOptions.WithWitnesses) 3. If the witness policy is satisfied, make this new Checkpoint public available An entry is considered published once it is committed to by a published Checkpoint (i.e. a published Checkpoint's size is larger than the entry's assigned index). Due to the nature of append-only logs, all Checkpoints issued after this point will also commit to inclusion of this entry. ## Usage ### Getting Started The best place to start is the [codelab](./cmd/conformance#codelab). This will walk you through setting up your first log, writing some entries to it via HTTP, and inspecting the contents. Take a look at the example personalities in the `/cmd/` directory: - [posix](./cmd/conformance/posix/): example of operating a log backed by a local filesystem - This example runs an HTTP web server that takes arbitrary data and adds it to a file-based log. - [mysql](./cmd/conformance/mysql/): example of operating a log that uses MySQL - This example is easiest deployed via `docker compose`, which allows for easy setup and teardown. - [gcp](./cmd/conformance/gcp/): example of operating a log running in GCP. - This example can be deployed via terraform, see the [deployment instructions](./deployment/live/gcp/conformance#manual-deployment). - [aws](./cmd/conformance/aws/): example of operating a log running on AWS. - This example can be deployed via terraform, see the [deployment instructions](./deployment/live/aws/codelab#aws-codelab-deployment). - [posix-oneshot](./cmd/examples/posix-oneshot/): example of a command line tool to add entries to a log stored on the local filesystem - This example is not a long-lived process; running the command integrates entries into the log which lives only as files. The `main.go` files for each of these example personalities try to strike a balance when demonstrating features of Tessera between simplicity, and demonstrating best practices. Please raise issues against the repo, or chat to us in [Slack](#contact) if you have ideas for making the examples more accessible! ### Writing Personalities #### Introduction Tessera is a library written in Go. It is designed to efficiently serve logs that allow read access via the [tlog-tiles API][]. The code you write that calls Tessera is referred to as a personality, because it tailors the generic library to your ecosystem. Before starting to write your own personality, it is strongly recommended that you have familiarized yourself with the provided personalities referenced in [Getting Started](#getting-started). When writing your Tessera personality, the first decision you need to make is which of the native drivers to use: * [GCP](./storage/gcp/) * [AWS](./storage/aws/) * [MySQL](./storage/mysql/) * [POSIX](./storage/posix/) The easiest drivers to operate and to scale are the cloud implementations: GCP and AWS. These are the recommended choice for the majority of users running in production. If you aren't using a cloud provider, then your options are MySQL and POSIX: - POSIX is the simplest to get started with as it needs little in the way of extra infrastructure, and if you already serve static files as part of your business/project this could be a good fit. - Alternatively, if you are used to operating user-facing applications backed by a RDBMS, then MySQL could be a natural fit. To get a sense of the rough performance you can expect from the different backends, take a look at [docs/performance.md](/docs/performance.md). #### Setup Once you've picked a storage driver, you can start writing your personality! You'll need to import the Tessera library: ```shell # This imports the library at main. # This should be set to the latest release version to get a stable release. go get github.com/transparency-dev/tessera@main ``` #### Constructing the Appender Import the main `tessera` package, and the driver for the storage backend you want to use: ```go file=README_test.go region=common_imports "github.com/transparency-dev/tessera" // Choose one! "github.com/transparency-dev/tessera/storage/posix" // "github.com/transparency-dev/tessera/storage/aws" // "github.com/transparency-dev/tessera/storage/gcp" // "github.com/transparency-dev/tessera/storage/mysql" ``` Now you'll need to instantiate the lifecycle object for the native driver you are using. By far the most common way to operate logs is in an append-only manner, and the rest of this guide will discuss this mode. For lifecycle states other than Appender mode, take a look at [Lifecycles](#lifecycles) below. Here's an example of creating an `Appender` for the POSIX driver: ```go file=README_test.go region=construct_example driver, _ := posix.New(ctx, "/tmp/mylog") signer := createSigner() appender, shutdown, reader, err := tessera.NewAppender( ctx, driver, tessera.NewAppendOptions().WithCheckpointSigner(signer)) ``` See the documentation for each driver implementation to understand the parameters that each takes. The final part of configuring Tessera is to set up the addition features that you want to use. These optional libraries can be used to provide common log behaviours. See [Features](#features) after reading the rest of this section for more details. #### Writing to the Log Now you should have a Tessera instance configured for your environment with the correct features set up. Now the fun part - writing to the log! ```go file=README_test.go region=use_appender_example appender, shutdown, reader, err := tessera.NewAppender( ctx, driver, tessera.NewAppendOptions().WithCheckpointSigner(signer)) if err != nil { panic(err) } index, err := appender.Add(ctx, tessera.NewEntry(data))() ``` The `AppendOptions` allow Tessera behaviour to be tuned. Take a look at the methods named `With*` on the `AppendOptions` struct in the root package, e.g. [`WithBatching`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#AppendOptions.WithBatching) to see the available options are how they should be used. > [!Tip] > If you know ahead of time how many entries you want to add (e.g. if you're writing an "off-line" or batch tool, > which will process entries and then exit), you can pass that value as a performance hint to Tessera via the > _size_ parameter of `WithBatching`. Writing to the log follows this flow: 1. Call `Add` with a new entry created with the data to be added as a leaf in the log. - This method returns a _future_ of the form `func() (Index, error)`. 2. Call this future function, which will block until the data passed into `Add` has been sequenced - On success, an index number is _durably_ assigned and returned - On failure, the error is returned Once an index has been returned, the new data is sequenced, but not necessarily integrated into the log. As discussed above in [Integration](#integration), sequenced entries will be _asynchronously_ integrated into the log and be made available via the read API. Some personalities may need to block until this has been performed, e.g. because they will provide the requester with an inclusion proof, which requires integration. Such personalities are recommended to use [Synchronous Publication](#synchronous-publication) to perform this blocking. #### Reading from the Log Data that has been written to the log needs to be made available for clients and verifiers. Tessera makes the log readable via the [tlog-tiles API][]. In the case of AWS and GCP, the data to be served is written to object storage and served directly by the cloud provider. The log operator only needs to ensure that these object storage instances are publicly readable, and set up a URL to point to them. In the case of MySQL and POSIX, the log operator will need to take more steps to make the data available. POSIX writes out the files exactly as per the API spec, so the log operator can serve these via an HTTP File Server. MySQL is the odd implementation in that it requires personality code to handle read traffic. See the example personalities written for MySQL to see how this Go web server should be configured. ## Features ### Antispam In some scenarios, particularly where logs are publicly writable such as Certificate Transparency, it's possible for logs to be asked, whether maliciously or accidentally, to add entries they already contain. Generally, this is undesirable, and so Tessera provides an optional mechanism to try to detect and ignore duplicate entries on a best-effort basis. Logs that do not allow public submissions directly to the log may want to operate without this optional antispam measure, instead relying on the personality to never generate duplicates. This can allow for significantly cheaper operation and faster write throughput. The antispam mechanism consists of two layers which sit in front of the underlying `Add` implementation of the storage: 1. The first layer is an `InMemory` cache which keeps track of a configurable number of recently-added entries. If a recently-seen entry is spotted by the same application instance, this layer will short-circuit the addition of the duplicate, and instead return and index previously assigned to this entry. Otherwise the requested entry is passed on to the second layer. 2. The second layer is a `Persistent` index of a hash of the entry to its assigned position in the log. Similarly to the first layer, this second layer will look for a record in its stored data which matches the incoming entry, and if such a record exists, it will short-circuit the addition of the duplicate entry and return a previous version's assigned position in the log. These layes are configured by the `WithAntispam` method of the [AppendOptions](https://pkg.go.dev/github.com/transparency-dev/tessera@main#AppendOptions.WithAntispam) and [MigrateOptions](https://pkg.go.dev/github.com/transparency-dev/tessera@main#AppendOptions.WithAntispam). > [!Tip] > Persistent antispam is fairly expensive in terms of storage-compute, so should only be used where it is actually necessary. > [!Note] > Tessera's antispam mechanism is _best effort_; there is no guarantee that all duplicate entries will be suppressed. > This is a trade-off; fully-atomic "strong" de-duplication is _extremely_ expensive in terms of throughput and compute costs, and > would limit Tessera to only being able to use transactional type storage backends. ### Witnessing Logs are required to be append-only data structures. This property can be verified by witnesses, and signatures from witnesses can be provided in the published checkpoint to increase confidence for users of the log. Personalities can configure Tessera with options that specify witnesses compatible with the [C2SP Witness Protocol](https://github.com/C2SP/C2SP/blob/main/tlog-witness.md). Configuring the witnesses is done by creating a top-level [`WitnessGroup`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#WitnessGroup) that contains either sub `WitnessGroup`s or [`Witness`es](https://pkg.go.dev/github.com/transparency-dev/tessera@main#Witness). Each `Witness` is configured with a URL at which the witness can be requested to make witnessing operations via the C2SP Witness Protocol, and a Verifier for the key that it must sign with. `WitnessGroup`s are configured with their sub-components, and a number of these components that must be satisfied in order for the group to be satisfied. These primitives allow arbitrarily complex witness policies to be specified. Once a top-level `WitnessGroup` is configured, it is passed in to the `Appender` lifecycle options using [AppendOptions#WithWitnesses](https://pkg.go.dev/github.com/transparency-dev/tessera@main#AppendOptions.WithWitnesses). If this method is not called then no witnessing will be configured. > [!Note] > If the policy cannot be satisfied then no checkpoint will be published. > It is up to the log operator to ensure that a satisfiable policy is configured, and that the requested publishing rate is acceptable to the configured witnesses. ### Synchronous Publication Synchronous Publication is provided by [`tessera.PublicationAwaiter`](https://pkg.go.dev/github.com/transparency-dev/tessera#PublicationAwaiter). This allows applications built with Tessera to block until leaves passed via calls to `Add()` are committed to via a public checkpoint. > [!Tip] > This is useful if e.g. your application needs to return an inclusion proof in response to a request to add an entry to the log. ## Lifecycles ### Appender This is the most common lifecycle mode. Appender allows the application to add leaves, which will be assigned positions in the log contiguous to any entries the log has already committed to. This mode is instantiated via [`tessera.NewAppender`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#NewAppender), and configured using the [`tessera.NewAppendOptions`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#NewAppendOptions) struct. This is described above in [Constructing the Appender](#constructing-the-appender). See more details in the [Lifecycle Design: Appender](https://github.com/transparency-dev/tessera/blob/main/docs/design/lifecycle.md#appender). ### Migration Target This mode is used to migrate a log from one location to another. This is instantiated via [`tessera.NewMigrationTarget`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#NewMigrationTarget), and configured using the [`tessera.NewMigratonOptions`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#NewMigrationOptions) struct. > [!Tip] > This mode enables the migration of logs between different Tessera storage backends, e.g. you may wish to switch > serving infrastructure because: > * You're migrating between/to/from cloud providers for some reason. > * You're "freezing" your log, and want to move it to a cheap read-only location. > > You can also use this mode to migrate a [tlog-tiles][] compliant log _into_ Tessera. Binaries for migrating _into_ each of the storage implementations can be found at [./cmd/experimental/migrate/](./cmd/experimental/migrate/). These binaries take the URL of a remote tiled log, and copy it into the target location. These binaries ought to be sufficient for most use-cases. Users that need to write their own migration binary should use the provided binaries as a reference codelab. See more details in the [Lifecycle Design: Migration](https://github.com/transparency-dev/tessera/blob/main/docs/design/lifecycle.md#migration). ### Freezing a Log Freezing a log prevents new writes to the log, but still allows read access. We recommend that operators allow all pending [sequenced](#sequencing) entries to be [integrated](#integration), and all integrated entries to be [published](#publishing) via a Checkpoint before proceeding. Once all pending entries are published, the log is now _quiescent_, as described in [Lifecycle Design: Quiescent](https://github.com/transparency-dev/tessera/blob/main/docs/design/lifecycle.md#quiescent). To ensure all pending entries are published, keep an instance object for the current lifecycle state in a running process, but disable writes to this at the personality level. For example, a personality that takes HTTP requests from the Internet and calls `Appender.Add` should keep a process running with an `Appender`, but disable any code paths that lead to `Add` being invoked (e.g. by flipping a flag that changes this behaviour). The instantiated `Appender` allows its background processes to keep running, ensuring all entries are sequenced, integrated, and published. Determining when this is complete can be done by inspecting the databases or via the OpenTelemetry metrics which instrument this code; once the next-available sequence number and published checkpoint size have converged and remain stable, the log is in a quiescent state. A quiescent log using GCP, AWS, or POSIX that is now permanently read-only can be made cheaper to operate. The implementations no longer need any running binaries running Tessera code. Any databases created for this log (i.e. the sequencing tables, or antispam) can be deleted. The read-path can be served directly from the storage buckets (for GCP, AWS) or via a standard HTTP file server (for POSIX). A log using MySQL must continue to run a personality in order to serve the read path, and thus cannot benefit from the same degree of cost savings when frozen. ### Deleting a Log Deleting a log is generally performed after [Freezing a Log](#freezing-a-log). Deleting a GCP, AWS, or POSIX log that has already been frozen just requires deleting the storage bucket or files from disk. Deleting a MySQL log can be done by turning down the personality binaries, and then deleting the database. ### Sharding a Log A common way to deploy logs is to run multiple logs in parallel, each of which accepts a distinct subset of entries. For example, CT shards logs temporally, based on the expiry date of the certificate. Tessera currently has no special support for sharding logs. The recommended way to instantiate a new shard of a log is simply to create a new log as described above. This requires the full stack to be instantiated, including: - any DB instances - a personality binary for each log #589 tracks adding more elegant support for sharing resources for sharded logs. Please upvote that issue if you would like us to prioritize it. ## Contributing See [CONTRIBUTING.md](/CONTRIBUTING.md) for details. ## License This repo is licensed under the Apache 2.0 license, see [LICENSE](/LICENSE) for details. ## Contact - Slack: https://transparency-dev.slack.com/ ([invitation](https://transparency.dev/slack/)) - Mailing list: https://groups.google.com/forum/#!forum/trillian-transparency ## Acknowledgements Tessera builds upon the hard work, experience, and lessons from many _many_ folks involved in transparency ecosystems over the years. [tlog-tiles API]: https://c2sp.org/tlog-tiles [Static CT API]: https://c2sp.org/static-ct-api [Trillian v1]: https://github.com/google/trillian transparency-dev-tessera-3cb22ee/README_test.go000066400000000000000000000053531511600621500214600ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // This file contains code snippets used in the README.md file in this // directory. // // Having this code here, in a go test file, helps ensure that our examples // and snippets actually work with the current state of Tessera. // // To update the markdown files with changes from in here, run: // go run github.com/szkiba/mdcode@latest update // // TODO(al): We should probably add a presubmit test to check docs have // actually been updated. package tessera_test import ( "context" "crypto/rand" "testing" // #region common_imports "github.com/transparency-dev/tessera" // Choose one! "github.com/transparency-dev/tessera/storage/posix" // "github.com/transparency-dev/tessera/storage/aws" // "github.com/transparency-dev/tessera/storage/gcp" // "github.com/transparency-dev/tessera/storage/mysql" // #endregion "golang.org/x/mod/sumdb/note" ) func constructStorage() { ctx := context.Background() // #region construct_example driver, _ := posix.New(ctx, posix.Config{Path: "/tmp/mylog"}) signer := createSigner() appender, shutdown, reader, err := tessera.NewAppender( ctx, driver, tessera.NewAppendOptions().WithCheckpointSigner(signer)) // #endregion // use the vars so the compiler/linter doesn't complain. _, _, _, _ = appender, shutdown, reader, err } func TestConstructStorage(t *testing.T) { constructStorage() } func constructAndUseAppender() { ctx := context.Background() data := []byte("hello") driver, _ := posix.New(ctx, posix.Config{Path: "/tmp/mylog"}) signer := createSigner() // #region use_appender_example appender, shutdown, reader, err := tessera.NewAppender( ctx, driver, tessera.NewAppendOptions().WithCheckpointSigner(signer)) if err != nil { panic(err) } index, err := appender.Add(ctx, tessera.NewEntry(data))() // #endregion // use the vars so the compiler/linter doesn't complain. _, _, _, _ = appender, shutdown, reader, err _, _ = index, err } func TestConstructAndUseAppender(t *testing.T) { constructAndUseAppender() } func createSigner() note.Signer { s, _, _ := note.GenerateKey(rand.Reader, "TestKey") r, err := note.NewSigner(s) if err != nil { panic(err) } return r } transparency-dev-tessera-3cb22ee/SECURITY.md000066400000000000000000000013331511600621500207200ustar00rootroot00000000000000# Security Policy ## Reporting a Vulnerability **Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** To report a security issue, please use the GitHub Security Advisory ["Report a Vulnerability"](https://github.com/transparency-dev/tessera/security/advisories) tab. We ask you to submit high-quality reports, including as many details as possible, a buildable proof of concept against a recent build, a crash dump if available, and instructions on reproducing the issue. Please also include information about the affected software version, a description of the issue’s impact, and an attack scenario, as that helps us assess the vulnerability quickly and effectively. transparency-dev-tessera-3cb22ee/antispam.go000066400000000000000000000063361511600621500213020ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "context" "fmt" "sync" lru "github.com/hashicorp/golang-lru/v2" ) // newInMemoryDedup wraps an Add function to prevent duplicate entries being written to the underlying // storage by keeping an in-memory cache of recently seen entries. // Where an existing entry has already been `Add`ed, the previous `IndexFuture` will be returned. // When no entry is found in the cache, the delegate method will be called to store the entry, and // the result will be registered in the cache. // // Internally this uses a cache with a max size configured by the size parameter. // If the entry being `Add`ed is not found in the cache, then it calls the delegate. // // This object can be used in isolation, or in conjunction with a persistent dedup implementation. // When using this with a persistent dedup, the persistent layer should be the delegate of this // InMemoryDedup. This allows recent duplicates to be deduplicated in memory, reducing the need to // make calls to a persistent storage. func newInMemoryDedup(size uint) func(AddFn) AddFn { return func(af AddFn) AddFn { c, err := lru.New[string, func() IndexFuture](int(size)) if err != nil { panic(fmt.Errorf("lru.New(%d): %v", size, err)) } dedup := &inMemoryDedup{ delegate: af, cache: c, } return dedup.add } } type inMemoryDedup struct { delegate func(ctx context.Context, e *Entry) IndexFuture cache *lru.Cache[string, func() IndexFuture] } // Add adds the entry to the underlying delegate only if e hasn't been recently seen. In either case, // an IndexFuture will be returned that the client can use to get the sequence number of this entry. func (d *inMemoryDedup) add(ctx context.Context, e *Entry) IndexFuture { ctx, span := tracer.Start(ctx, "tessera.Appender.inmemoryDedup.Add") defer span.End() id := string(e.Identity()) f := sync.OnceValue(func() IndexFuture { // However many calls with the same entry come in and are deduplicated, we should only call delegate // once for each unique entry: df := d.delegate(ctx, e) return func() (Index, error) { idx, err := df() // If things went wrong we shouldn't cache the error, but rather let the request be retried as the error // may be transient (including ErrPushback). if err != nil { d.cache.Remove(id) } return idx, err } }) // if we've seen this entry before, discard our f and replace // with the one we created last time, otherwise store f against id. if prev, ok, _ := d.cache.PeekOrAdd(id, f); ok { f = func() IndexFuture { return func() (Index, error) { i, err := prev()() i.IsDup = true return i, err } } } return f() } transparency-dev-tessera-3cb22ee/antispam_test.go000066400000000000000000000101221511600621500223250ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "context" "crypto/sha256" "errors" "fmt" "sync" "testing" ) func TestDedup(t *testing.T) { ctx := context.Background() testCases := []struct { desc string newValue string wantIdx uint64 wantDup bool }{ { desc: "first element", newValue: "foo", wantIdx: 1, wantDup: true, }, { desc: "third element", newValue: "baz", wantIdx: 3, wantDup: true, }, { desc: "new element", newValue: "omega", wantIdx: 4, }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { idx := uint64(1) delegate := func(ctx context.Context, e *Entry) IndexFuture { thisIdx := idx idx++ return func() (Index, error) { return Index{Index: thisIdx}, nil } } dedupAdd := newInMemoryDedup(256)(delegate) // Add foo, bar, baz to prime the cache to make things interesting for _, s := range []string{"foo", "bar", "baz"} { if _, err := dedupAdd(ctx, NewEntry([]byte(s)))(); err != nil { t.Fatalf("dedupAdd(%q): %v", s, err) } } i, err := dedupAdd(ctx, NewEntry([]byte(tC.newValue)))() if err != nil { t.Fatalf("dedupAdd(%q): %v", tC.newValue, err) } if i.Index != tC.wantIdx { t.Errorf("got Index != want Index (%d != %d)", i.Index, tC.wantIdx) } if i.IsDup != tC.wantDup { t.Errorf("got IsDup != want IsDup(%t != %t)", i.IsDup, tC.wantDup) } }) } } func TestDedupDoesNotCacheError(t *testing.T) { idx := uint64(0) rErr := true // This delegate will return an error the first time it's called, but all further // calls will succeed. // When an error is returned, no entry will be "added" to the tree. delegate := func(ctx context.Context, e *Entry) IndexFuture { thisIdx := idx // Don't add an entry if we're returning an error if !rErr { idx++ } return func() (Index, error) { var err error // Return an error just the first time we're called if rErr { err = errors.New("bad thing happened") rErr = false } return Index{Index: thisIdx}, err } } dedupAdd := newInMemoryDedup(256)(delegate) k := "foo" for i := range 10 { idx, err := dedupAdd(t.Context(), NewEntry([]byte(k)))() // We expect an error from the delegate the first time. if i == 0 && err == nil { t.Errorf("dedupAdd(%q)@%d: was successful, want error", k, i) continue } // But the 2nd call should work. if i > 0 && err != nil { t.Errorf("dedupAdd(%q)@%d: got %v, want no error", k, i, err) continue } // After which, all subsequent adds should dedup to that successful add. if i > 1 && !idx.IsDup { t.Errorf("got IsDup=false, want isDup=true") continue } if idx.Index != 0 { t.Errorf("got index=%d, want %d", idx.Index, 1) } } } func BenchmarkDedup(b *testing.B) { ctx := context.Background() // Outer loop is for benchmark calibration, inside here is each individual run of the benchmark for b.Loop() { idx := uint64(1) delegate := func(ctx context.Context, e *Entry) IndexFuture { thisIdx := idx idx++ return func() (Index, error) { return Index{Index: thisIdx}, nil } } dedupAdd := newInMemoryDedup(256)(delegate) wg := &sync.WaitGroup{} // Loop to create a bunch of leaves in parallel to test lock contention for leafIndex := range 1024 { wg.Add(1) go func(index int) { _, err := dedupAdd(ctx, NewEntry(fmt.Appendf(nil, "leaf with value %d", index%sha256.Size)))() if err != nil { b.Error(err) } wg.Done() }(leafIndex) } wg.Wait() } } transparency-dev-tessera-3cb22ee/api/000077500000000000000000000000001511600621500177005ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/api/layout/000077500000000000000000000000001511600621500212155ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/api/layout/example_test.go000066400000000000000000000032151511600621500242370ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package layout_test import ( "fmt" "github.com/transparency-dev/tessera/api/layout" ) func ExampleNodeCoordsToTileAddress() { var treeLevel, treeIndex uint64 = 8, 123456789 tileLevel, tileIndex, nodeLevel, nodeIndex := layout.NodeCoordsToTileAddress(treeLevel, treeIndex) fmt.Printf("tile level: %d, tile index: %d, node level: %d, node index: %d", tileLevel, tileIndex, nodeLevel, nodeIndex) // Output: tile level: 1, tile index: 482253, node level: 0, node index: 21 } func ExampleTilePath() { tilePath := layout.TilePath(0, 1234067, 8) fmt.Printf("tile path: %s", tilePath) // Output: tile path: tile/0/x001/x234/067.p/8 } func ExampleEntriesPath() { entriesPath := layout.EntriesPath(1234067, 8) fmt.Printf("entries path: %s", entriesPath) // Output: entries path: tile/entries/x001/x234/067.p/8 } func ExampleParseTileLevelIndexPartial() { level, index, width, _ := layout.ParseTileLevelIndexPartial("0", "x001/x234/067.p/8") fmt.Printf("level: %d, index: %d, width: %d", level, index, width) // Output: level: 0, index: 1234067, width: 8 } transparency-dev-tessera-3cb22ee/api/layout/paths.go000066400000000000000000000157561511600621500227010ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package layout contains routines for specifying the path layout of Tessera logs, // which is really to say that it provides functions to calculate paths used by the // [tlog-tiles API]. // // [tlog-tiles API]: https://c2sp.org/tlog-tiles package layout import ( "fmt" "iter" "math" "strconv" "strings" ) const ( // CheckpointPath is the location of the file containing the log checkpoint. CheckpointPath = "checkpoint" ) // EntriesPathForLogIndex builds the local path at which the leaf with the given index lives in. // Note that this will be an entry bundle containing up to 256 entries and thus multiple // indices can map to the same output path. // The logSize is required so that a partial qualifier can be appended to tiles that // would contain fewer than 256 entries. func EntriesPathForLogIndex(seq, logSize uint64) string { tileIndex := seq / EntryBundleWidth return EntriesPath(tileIndex, PartialTileSize(0, tileIndex, logSize)) } // Range returns an iterator over a list of RangeInfo structs which describe the bundles/tiles // necessary to cover the specified range of individual entries/hashes `[from, min(from+N, treeSize) )`. // // If from >= treeSize or N == 0, the returned iterator will yield no elements. func Range(from, N, treeSize uint64) iter.Seq[RangeInfo] { return func(yield func(RangeInfo) bool) { // Range is empty if we're entirely beyond the extent of the tree, or we've been asked for zero items. if from >= treeSize || N == 0 { return } // Truncate range at size of tree if necessary. if from+N > treeSize { N = treeSize - from } endInc := from + N - 1 sIndex := from / EntryBundleWidth eIndex := endInc / EntryBundleWidth for idx := sIndex; idx <= eIndex; idx++ { ri := RangeInfo{ Index: idx, N: EntryBundleWidth, } switch ri.Index { case sIndex: ri.Partial = PartialTileSize(0, sIndex, treeSize) ri.First = uint(from % EntryBundleWidth) ri.N = uint(EntryBundleWidth) - ri.First // Handle corner-case where the range is entirely contained in first bundle, if applicable: if ri.Index == eIndex { ri.N = uint((endInc)%EntryBundleWidth) - ri.First + 1 } case eIndex: ri.Partial = PartialTileSize(0, eIndex, treeSize) ri.N = uint((endInc)%EntryBundleWidth) + 1 } if !yield(ri) { return } } } } // RangeInfo describes a specific range of elements within a particular bundle/tile. // // Usage: // // bundleRaw, ... := fetchBundle(..., ri.Index, ri.Partial") // bundle, ... := parseBundle(bundleRaw) // elements := bundle.Entries[ri.First : ri.First+ri.N] type RangeInfo struct { // Index is the index of the entry bundle/tile in the tree. Index uint64 // Partial is the partial size of the bundle/tile, or zero if a full bundle/tile is expected. Partial uint8 // First is the offset into the entries contained by the bundle/tile at which the range starts. First uint // N is the number of entries, starting at First, which are covered by the range. N uint } // NWithSuffix returns a tiles-spec "N" path, with a partial suffix if p > 0. func NWithSuffix(l, n uint64, p uint8) string { suffix := "" if p > 0 { suffix = fmt.Sprintf(".p/%d", p) } return fmt.Sprintf("%s%s", fmtN(n), suffix) } // EntriesPath returns the local path for the nth entry bundle. p denotes the partial // tile size, or 0 if the tile is complete. func EntriesPath(n uint64, p uint8) string { return fmt.Sprintf("tile/entries/%s", NWithSuffix(0, n, p)) } // TilePath builds the path to the subtree tile with the given level and index in tile space. // If p > 0 the path represents a partial tile. func TilePath(tileLevel, tileIndex uint64, p uint8) string { return fmt.Sprintf("tile/%d/%s", tileLevel, NWithSuffix(tileLevel, tileIndex, p)) } // fmtN returns the "N" part of a Tiles-spec path. // // N is grouped into chunks of 3 decimal digits, starting with the most significant digit, and // padding with zeroes as necessary. // Digit groups are prefixed with "x", except for the least-significant group which has no prefix, // and separated with slashes. // // See https://github.com/C2SP/C2SP/blob/main/tlog-tiles.md#:~:text=index%201234067%20will%20be%20encoded%20as%20x001/x234/067 func fmtN(N uint64) string { n := fmt.Sprintf("%03d", N%1000) N /= 1000 for N > 0 { n = fmt.Sprintf("x%03d/%s", N%1000, n) N /= 1000 } return n } // ParseTileLevelIndexPartial takes level and index in string, validates and returns the level, index and width in uint64. // // Examples: // "/tile/0/x001/x234/067" means level 0 and index 1234067 of a full tile. // "/tile/0/x001/x234/067.p/8" means level 0, index 1234067 and width 8 of a partial tile. func ParseTileLevelIndexPartial(level, index string) (uint64, uint64, uint8, error) { l, err := ParseTileLevel(level) if err != nil { return 0, 0, 0, err } i, w, err := ParseTileIndexPartial(index) if err != nil { return 0, 0, 0, err } return l, i, w, err } // ParseTileLevel takes level in string, validates and returns the level in uint64. func ParseTileLevel(level string) (uint64, error) { l, err := strconv.ParseUint(level, 10, 64) // Verify that level is an integer between 0 and 63 as specified in the tlog-tiles specification. if l > 63 || err != nil { return 0, fmt.Errorf("failed to parse tile level") } return l, err } // ParseTileIndexPartial takes index in string, validates and returns the index and width in uint64. func ParseTileIndexPartial(index string) (uint64, uint8, error) { w := uint8(0) indexPaths := strings.Split(index, "/") if strings.Contains(index, ".p") { var err error w64, err := strconv.ParseUint(indexPaths[len(indexPaths)-1], 10, 64) if err != nil || w64 < 1 || w64 >= TileWidth { return 0, 0, fmt.Errorf("failed to parse tile width") } w = uint8(w64) indexPaths[len(indexPaths)-2] = strings.TrimSuffix(indexPaths[len(indexPaths)-2], ".p") indexPaths = indexPaths[:len(indexPaths)-1] } if strings.Count(index, "x") != len(indexPaths)-1 || strings.HasPrefix(indexPaths[len(indexPaths)-1], "x") { return 0, 0, fmt.Errorf("failed to parse tile index") } i := uint64(0) for _, indexPath := range indexPaths { indexPath = strings.TrimPrefix(indexPath, "x") n, err := strconv.ParseUint(indexPath, 10, 64) if err != nil || n >= 1000 || len(indexPath) != 3 { return 0, 0, fmt.Errorf("failed to parse tile index") } if i > (math.MaxUint64-n)/1000 { return 0, 0, fmt.Errorf("failed to parse tile index") } i = i*1000 + n } return i, w, nil } transparency-dev-tessera-3cb22ee/api/layout/paths_test.go000066400000000000000000000210201511600621500237150ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package layout import ( "fmt" "testing" "github.com/google/go-cmp/cmp" ) func TestEntriesPathForLogIndex(t *testing.T) { for _, test := range []struct { seq uint64 logSize uint64 wantPath string }{ { seq: 0, logSize: 256, wantPath: "tile/entries/000", }, { seq: 255, logSize: 256, wantPath: "tile/entries/000", }, { seq: 251, logSize: 255, wantPath: "tile/entries/000.p/255", }, { seq: 256, logSize: 512, wantPath: "tile/entries/001", }, { seq: 3, logSize: 257, wantPath: "tile/entries/000", }, { seq: 256, logSize: 257, wantPath: "tile/entries/001.p/1", }, { seq: 123456789 * 256, logSize: 123456790 * 256, wantPath: "tile/entries/x123/x456/789", }, } { desc := fmt.Sprintf("seq %d", test.seq) t.Run(desc, func(t *testing.T) { gotPath := EntriesPathForLogIndex(test.seq, test.logSize) if gotPath != test.wantPath { t.Errorf("got file %q want %q", gotPath, test.wantPath) } }) } } func TestEntriesPath(t *testing.T) { for _, test := range []struct { N uint64 p uint8 wantPath string wantErr bool }{ { N: 0, wantPath: "tile/entries/000", }, { N: 0, p: 8, wantPath: "tile/entries/000.p/8", }, { N: 255, wantPath: "tile/entries/255", }, { N: 255, p: 253, wantPath: "tile/entries/255.p/253", }, } { desc := fmt.Sprintf("N %d", test.N) t.Run(desc, func(t *testing.T) { gotPath := EntriesPath(test.N, test.p) if gotPath != test.wantPath { t.Errorf("got file %q want %q", gotPath, test.wantPath) } }) } } func TestTilePath(t *testing.T) { for _, test := range []struct { level uint64 index uint64 p uint8 wantPath string }{ { level: 0, index: 0, wantPath: "tile/0/000", }, { level: 0, index: 0, p: 255, wantPath: "tile/0/000.p/255", }, { level: 1, index: 0, wantPath: "tile/1/000", }, { level: 15, index: 455667, p: 0, wantPath: "tile/15/x455/667", }, { level: 15, index: 123456789, p: 41, wantPath: "tile/15/x123/x456/789.p/41", }, } { desc := fmt.Sprintf("level %x index %x", test.level, test.index) t.Run(desc, func(t *testing.T) { gotPath := TilePath(test.level, test.index, test.p) if gotPath != test.wantPath { t.Errorf("Got path %q want %q", gotPath, test.wantPath) } }) } } func TestNWithSuffix(t *testing.T) { for _, test := range []struct { level uint64 index uint64 p uint8 wantPath string }{ { level: 0, index: 0, wantPath: "000", }, { level: 0, index: 0, p: 255, wantPath: "000.p/255", }, { level: 15, index: 455667, wantPath: "x455/667", }, { level: 15, index: 123456789, p: 65, wantPath: "x123/x456/789.p/65", }, } { desc := fmt.Sprintf("level %x index %x", test.level, test.index) t.Run(desc, func(t *testing.T) { gotPath := NWithSuffix(test.level, test.index, test.p) if gotPath != test.wantPath { t.Errorf("Got path %q want %q", gotPath, test.wantPath) } }) } } func TestParseTileLevelIndexPartial(t *testing.T) { for _, test := range []struct { pathLevel string pathIndex string wantLevel uint64 wantIndex uint64 wantP uint8 wantErr bool }{ { pathLevel: "0", pathIndex: "x001/x234/067", wantLevel: 0, wantIndex: 1234067, wantP: 0, }, { pathLevel: "0", pathIndex: "x001/x234/067.p/89", wantLevel: 0, wantIndex: 1234067, wantP: 89, }, { pathLevel: "63", pathIndex: "x999/x999/x999/x999/x999/999.p/255", wantLevel: 63, wantIndex: 999999999999999999, wantP: 255, }, { pathLevel: "0", pathIndex: "001", wantLevel: 0, wantIndex: 1, wantP: 0, }, { pathLevel: "0", pathIndex: "x001/x234/067.p/", wantErr: true, }, { pathLevel: "0", pathIndex: "x001/x234/067.p", wantErr: true, }, { pathLevel: "0", pathIndex: "x001/x234/", wantErr: true, }, { pathLevel: "0", pathIndex: "x001/x234", wantErr: true, }, { pathLevel: "0", pathIndex: "x001/", wantErr: true, }, { pathLevel: "0", pathIndex: "x001", wantErr: true, }, { pathLevel: "1", pathIndex: "x001/.p/abc", wantErr: true, }, { pathLevel: "64", pathIndex: "x001/002", wantErr: true, }, { pathLevel: "-1", pathIndex: "x001/002", wantErr: true, }, { pathLevel: "abc", pathIndex: "x001/002", wantErr: true, }, { pathLevel: "8", pathIndex: "001/002", wantErr: true, }, { pathLevel: "8", pathIndex: "x001/0002", wantErr: true, }, { pathLevel: "8", pathIndex: "x001/-002", wantErr: true, }, { pathLevel: "8", pathIndex: "x001/002.p/256", wantErr: true, }, { pathLevel: "63", pathIndex: "x999/x999/x999/x999/x999/x999/999.p/255", wantErr: true, }, } { desc := fmt.Sprintf("pathLevel: %q, pathIndex: %q", test.pathLevel, test.pathIndex) t.Run(desc, func(t *testing.T) { gotLevel, gotIndex, gotWidth, err := ParseTileLevelIndexPartial(test.pathLevel, test.pathIndex) if gotLevel != test.wantLevel { t.Errorf("got level %d want %d", gotLevel, test.wantLevel) } if gotIndex != test.wantIndex { t.Errorf("got index %d want %d", gotIndex, test.wantIndex) } if gotWidth != test.wantP { t.Errorf("got width %d want %d", gotWidth, test.wantP) } gotErr := err != nil if gotErr != test.wantErr { t.Errorf("got err %v want %v", gotErr, test.wantErr) } }) } } func TestRange(t *testing.T) { for _, test := range []struct { from, N, treeSize uint64 desc string want []RangeInfo }{ { desc: "from beyond extent", from: 10, N: 1, treeSize: 5, want: []RangeInfo{}, }, { desc: "range end beyond extent", from: 3, N: 100, treeSize: 5, want: []RangeInfo{{Index: 0, First: 3, N: 5 - 3, Partial: 5}}, }, { desc: "empty range", from: 1, N: 0, treeSize: 2, want: []RangeInfo{}, }, { desc: "ok: full first bundle", from: 0, N: 256, treeSize: 257, want: []RangeInfo{{N: 256}}, }, { desc: "ok: entire single (partial) bundle", from: 20, N: 90, treeSize: 111, want: []RangeInfo{{Index: 0, Partial: 111, First: 20, N: 90}}, }, { desc: "ok: slice from single bundle with initial offset", from: 20, N: 90, treeSize: 1 << 20, want: []RangeInfo{{Index: 0, Partial: 0, First: 20, N: 90}}, }, { desc: "ok: multiple bundles, first is full, last is truncated", from: 0, N: 4*256 + 42, treeSize: 1 << 20, want: []RangeInfo{ {Index: 0, Partial: 0, First: 0, N: 256}, {Index: 1, Partial: 0, First: 0, N: 256}, {Index: 2, Partial: 0, First: 0, N: 256}, {Index: 3, Partial: 0, First: 0, N: 256}, {Index: 4, Partial: 0, First: 0, N: 42}, }, }, { desc: "ok: multiple bundles, first is offset, last is truncated", from: 2, N: 4*256 + 4, treeSize: 1 << 20, want: []RangeInfo{ {Index: 0, Partial: 0, First: 2, N: 256 - 2}, {Index: 1, Partial: 0, First: 0, N: 256}, {Index: 2, Partial: 0, First: 0, N: 256}, {Index: 3, Partial: 0, First: 0, N: 256}, {Index: 4, Partial: 0, First: 0, N: 6}, }, }, { desc: "ok: offset and trucated from single bundle in middle of tree", from: 8*256 + 66, N: 4, treeSize: 1 << 20, want: []RangeInfo{{Index: 8, Partial: 0, First: 66, N: 4}}, }, } { t.Run(test.desc, func(t *testing.T) { i := 0 for gotInfo := range Range(test.from, test.N, test.treeSize) { if d := cmp.Diff(test.want[i], gotInfo); d != "" { t.Fatalf("got results[%d] with diff:\n%s", i, d) } i++ } }) } } transparency-dev-tessera-3cb22ee/api/layout/tile.go000066400000000000000000000040511511600621500225010ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package layout const ( // TileHeight is the maximum number of levels Merkle tree levels a tile represents. // This is fixed at 8 by tlog-tile spec. TileHeight = 8 // TileWidth is the maximum number of hashes which can be present in the bottom row of a tile. TileWidth = 1 << TileHeight // EntryBundleWidth is the maximum number of entries which can be present in an EntryBundle. // This is defined to be the same as the width of the node tiles by tlog-tile spec. EntryBundleWidth = TileWidth ) // PartialTileSize returns the expected number of leaves in a tile at the given tile level and index // within a tree of the specified logSize, or 0 if the tile is expected to be fully populated. func PartialTileSize(level, index, logSize uint64) uint8 { sizeAtLevel := logSize >> (level * TileHeight) fullTiles := sizeAtLevel / TileWidth if index < fullTiles { return 0 } return uint8(sizeAtLevel % TileWidth) } // NodeCoordsToTileAddress returns the (TileLevel, TileIndex) in tile-space, and the // (NodeLevel, NodeIndex) address within that tile of the specified tree node co-ordinates. func NodeCoordsToTileAddress(treeLevel, treeIndex uint64) (uint64, uint64, uint, uint64) { tileRowWidth := uint64(1 << (TileHeight - treeLevel%TileHeight)) tileLevel := treeLevel / TileHeight tileIndex := treeIndex / tileRowWidth nodeLevel := uint(treeLevel % TileHeight) nodeIndex := uint64(treeIndex % tileRowWidth) return tileLevel, tileIndex, nodeLevel, nodeIndex } transparency-dev-tessera-3cb22ee/api/layout/tile_test.go000066400000000000000000000042061511600621500235420ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package layout import ( "fmt" "testing" ) func TestNodeCoordsToTileAddress(t *testing.T) { for _, test := range []struct { treeLevel uint64 treeIndex uint64 wantTileLevel uint64 wantTileIndex uint64 wantNodeLevel uint wantNodeIndex uint64 }{ { treeLevel: 0, treeIndex: 0, wantTileLevel: 0, wantTileIndex: 0, wantNodeLevel: 0, wantNodeIndex: 0, }, { treeLevel: 0, treeIndex: 255, wantTileLevel: 0, wantTileIndex: 0, wantNodeLevel: 0, wantNodeIndex: 255, }, { treeLevel: 0, treeIndex: 256, wantTileLevel: 0, wantTileIndex: 1, wantNodeLevel: 0, wantNodeIndex: 0, }, { treeLevel: 1, treeIndex: 0, wantTileLevel: 0, wantTileIndex: 0, wantNodeLevel: 1, wantNodeIndex: 0, }, { treeLevel: 8, treeIndex: 0, wantTileLevel: 1, wantTileIndex: 0, wantNodeLevel: 0, wantNodeIndex: 0, }, } { t.Run(fmt.Sprintf("%d-%d", test.treeLevel, test.treeIndex), func(t *testing.T) { tl, ti, nl, ni := NodeCoordsToTileAddress(test.treeLevel, test.treeIndex) if got, want := tl, test.wantTileLevel; got != want { t.Errorf("Got TileLevel %d want %d", got, want) } if got, want := ti, test.wantTileIndex; got != want { t.Errorf("Got TileIndex %d want %d", got, want) } if got, want := nl, test.wantNodeLevel; got != want { t.Errorf("Got NodeLevel %d want %d", got, want) } if got, want := ni, test.wantNodeIndex; got != want { t.Errorf("Got NodeIndex %d want %d", got, want) } }) } } transparency-dev-tessera-3cb22ee/api/state.go000066400000000000000000000060041511600621500213470ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package api contains the tiles definitions from the [tlog-tiles API]. // // [tlog-tiles API]: https://c2sp.org/tlog-tiles package api import ( "bytes" "crypto/sha256" "encoding/binary" "fmt" "github.com/transparency-dev/tessera/api/layout" ) // HashTile represents a tile within the Merkle hash tree. // Leaf HashTiles will have a corresponding EntryBundle, where each // entry in the EntryBundle slice hashes to the value at the same // index in the Nodes slice. type HashTile struct { // Nodes stores the leaf hash nodes in this tile. // Note that only non-ephemeral nodes are stored. Nodes [][]byte } // MarshalText implements encoding/TextMarshaller and writes out an HashTile // instance as sequences of concatenated hashes as specified by the tlog-tiles spec. func (t HashTile) MarshalText() ([]byte, error) { r := &bytes.Buffer{} for _, n := range t.Nodes { if _, err := r.Write(n); err != nil { return nil, err } } return r.Bytes(), nil } // UnmarshalText implements encoding/TextUnmarshaler and reads HashTiles // which are encoded using the tlog-tiles spec. func (t *HashTile) UnmarshalText(raw []byte) error { if len(raw)%sha256.Size != 0 { return fmt.Errorf("%d is not a multiple of %d", len(raw), sha256.Size) } nodes := make([][]byte, 0, len(raw)/sha256.Size) for index := 0; index < len(raw); index += sha256.Size { data := raw[index : index+sha256.Size] nodes = append(nodes, data) } t.Nodes = nodes return nil } // EntryBundle represents a sequence of entries in the log. // These entries correspond to a leaf tile in the hash tree. type EntryBundle struct { // Entries stores the leaf entries of the log, in order. Entries [][]byte } // UnmarshalText implements encoding/TextUnmarshaler and reads EntryBundles // which are encoded using the tlog-tiles spec. func (t *EntryBundle) UnmarshalText(raw []byte) error { nodes := make([][]byte, 0, layout.EntryBundleWidth) for index := 0; index < len(raw); { dataIndex := index + 2 if dataIndex > len(raw) { return fmt.Errorf("dangling bytes at byte index %d in data of %d bytes", index, len(raw)) } size := int(binary.BigEndian.Uint16(raw[index:dataIndex])) dataEnd := dataIndex + size if dataEnd > len(raw) { return fmt.Errorf("require %d bytes from byte index %d, but size is %d", size, dataIndex, len(raw)) } data := raw[dataIndex:dataEnd] nodes = append(nodes, data) index = dataIndex + size } t.Entries = nodes return nil } transparency-dev-tessera-3cb22ee/api/state_test.go000066400000000000000000000074611511600621500224160ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // package api_test contains tests for the api package. package api_test import ( "bytes" "crypto/rand" "crypto/sha256" "fmt" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" ) func TestHashTile_MarshalTileRoundtrip(t *testing.T) { for _, test := range []struct { size int }{ { size: 1, }, { size: 255, }, { size: 11, }, { size: 42, }, } { t.Run(fmt.Sprintf("tile size %d", test.size), func(t *testing.T) { tile := api.HashTile{Nodes: make([][]byte, 0, test.size)} for i := range test.size { // Fill in the leaf index tile.Nodes = append(tile.Nodes, make([]byte, sha256.Size)) if _, err := rand.Read(tile.Nodes[i]); err != nil { t.Error(err) } } raw, err := tile.MarshalText() if err != nil { t.Fatalf("MarshalText() = %v", err) } tile2 := api.HashTile{} if err := tile2.UnmarshalText(raw); err != nil { t.Fatalf("UnmarshalText() = %v", err) } if diff := cmp.Diff(tile, tile2); len(diff) != 0 { t.Fatalf("Got tile with diff: %s", diff) } }) } } func TestLeafBundle_MarshalTileRoundtrip(t *testing.T) { for _, test := range []struct { size int }{ { size: 1, }, { size: 255, }, { size: 11, }, { size: 42, }, } { t.Run(fmt.Sprintf("tile size %d", test.size), func(t *testing.T) { bundleRaw := &bytes.Buffer{} want := make([][]byte, test.size) for i := range test.size { // Fill in the leaf index want[i] = make([]byte, i*100) if _, err := rand.Read(want[i]); err != nil { t.Error(err) } _, _ = bundleRaw.Write(tessera.NewEntry(want[i]).MarshalBundleData(uint64(i))) } tile2 := api.EntryBundle{} if err := tile2.UnmarshalText(bundleRaw.Bytes()); err != nil { t.Fatalf("UnmarshalText() = %v", err) } for i := range test.size { if got, want := tile2.Entries[i], want[i]; !bytes.Equal(got, want) { t.Errorf("%d: want %x, got %x", i, got, want) } } }) } } func TestLeafBundle_UnmarshalText(t *testing.T) { for _, test := range []struct { desc string input []byte wantErr bool }{ { desc: "no data", input: []byte{}, wantErr: false, }, { desc: "insufficient data", input: []byte{0x0, 0x02, 'a'}, wantErr: true, }, { desc: "empty nodes", input: []byte{0x0, 0x0, 0x0, 0x1, 'a', 0x0, 0x0}, wantErr: false, }, { desc: "insufficient integer bytes", input: []byte{0x1}, wantErr: true, }, } { t.Run(test.desc, func(t *testing.T) { tile := api.EntryBundle{} err := tile.UnmarshalText(test.input) if gotErr := err != nil; gotErr != test.wantErr { t.Errorf("wantErr: %t, got %v", test.wantErr, err) } }) } } func BenchmarkLeafBundle_UnmarshalText(b *testing.B) { bs := bytes.Buffer{} for i := range 222 { // Create leaves of different lengths for interest in the parsing / memory allocation leafStr := strings.Repeat(fmt.Sprintf("Leaf %d", i), i%20) _, _ = bs.Write(tessera.NewEntry([]byte(leafStr)).MarshalBundleData(uint64(i))) } rawBundle := bs.Bytes() for b.Loop() { tile := api.EntryBundle{} if err := tile.UnmarshalText(rawBundle); err != nil { b.Fatal(err) } } } transparency-dev-tessera-3cb22ee/append_lifecycle.go000066400000000000000000001000451511600621500227440ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "context" "errors" "fmt" "net/http" "os" "sync" "sync/atomic" "time" f_log "github.com/transparency-dev/formats/log" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/internal/otel" "github.com/transparency-dev/tessera/internal/parse" "github.com/transparency-dev/tessera/internal/witness" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "golang.org/x/mod/sumdb/note" "k8s.io/klog/v2" ) const ( // DefaultBatchMaxSize is used by storage implementations if no WithBatching option is provided when instantiating it. DefaultBatchMaxSize = 256 // DefaultBatchMaxAge is used by storage implementations if no WithBatching option is provided when instantiating it. DefaultBatchMaxAge = 250 * time.Millisecond // DefaultCheckpointInterval is used by storage implementations if no WithCheckpointInterval option is provided when instantiating it. DefaultCheckpointInterval = 10 * time.Second // DefaultCheckpointRepublishInterval is used by storage implementations if no WithCheckpointRepublishInterval option is provided when instantiating it. DefaultCheckpointRepublishInterval = 10 * time.Minute // DefaultPushbackMaxOutstanding is used by storage implementations if no WithPushback option is provided when instantiating it. DefaultPushbackMaxOutstanding = 4096 // DefaultGarbageCollectionInterval is the default value used if no WithGarbageCollectionInterval option is provided. DefaultGarbageCollectionInterval = time.Minute // DefaultAntispamInMemorySize is the recommended default limit on the number of entries in the in-memory antispam cache. // The amount of data stored for each entry is small (32 bytes of hash + 8 bytes of index), so in the general case it should be fine // to have a very large cache. DefaultAntispamInMemorySize = 256 << 10 // DefaultWitnessTimeout is the default maximum time to wait for responses from configured witnesses. DefaultWitnessTimeout = 5 * time.Second ) var ( appenderAddsTotal metric.Int64Counter appenderAddHistogram metric.Int64Histogram appenderHighestIndex metric.Int64Gauge appenderIntegratedSize metric.Int64Gauge appenderIntegrateLatency metric.Int64Histogram appenderDeadlineRemaining metric.Int64Histogram appenderNextIndex metric.Int64Gauge appenderSignedSize metric.Int64Gauge appenderWitnessedSize metric.Int64Gauge appenderWitnessRequests metric.Int64Counter appenderWitnessHistogram metric.Int64Histogram followerEntriesProcessed metric.Int64Gauge followerLag metric.Int64Gauge // Custom histogram buckets as we're still interested in details in the 1-2s area. histogramBuckets = []float64{0, 10, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000, 4000, 5000, 6000, 8000, 10000} ) func init() { var err error appenderAddsTotal, err = meter.Int64Counter( "tessera.appender.add.calls", metric.WithDescription("Number of calls to the appender lifecycle Add function"), metric.WithUnit("{call}")) if err != nil { klog.Exitf("Failed to create appenderAddsTotal metric: %v", err) } appenderAddHistogram, err = meter.Int64Histogram( "tessera.appender.add.duration", metric.WithDescription("Duration of calls to the appender lifecycle Add function"), metric.WithUnit("ms"), metric.WithExplicitBucketBoundaries(histogramBuckets...)) if err != nil { klog.Exitf("Failed to create appenderAddDuration metric: %v", err) } appenderHighestIndex, err = meter.Int64Gauge( "tessera.appender.index", metric.WithDescription("Highest index assigned by appender lifecycle Add function")) if err != nil { klog.Exitf("Failed to create appenderHighestIndex metric: %v", err) } appenderIntegratedSize, err = meter.Int64Gauge( "tessera.appender.integrated.size", metric.WithDescription("Size of the integrated (but not necessarily published) tree"), metric.WithUnit("{entry}")) if err != nil { klog.Exitf("Failed to create appenderIntegratedSize metric: %v", err) } appenderIntegrateLatency, err = meter.Int64Histogram( "tessera.appender.integrate.latency", metric.WithDescription("Duration between an index being assigned by Add, and that index being integrated in the tree"), metric.WithUnit("ms"), metric.WithExplicitBucketBoundaries(histogramBuckets...)) if err != nil { klog.Exitf("Failed to create appenderIntegrateLatency metric: %v", err) } appenderDeadlineRemaining, err = meter.Int64Histogram( "tessera.appender.deadline.remaining", metric.WithDescription("Duration remaining before context cancellation when appender is invoked (only set for contexts with deadline)"), metric.WithUnit("ms"), metric.WithExplicitBucketBoundaries(histogramBuckets...)) if err != nil { klog.Exitf("Failed to create appenderDeadlineRemaining metric: %v", err) } appenderNextIndex, err = meter.Int64Gauge( "tessera.appender.next_index", metric.WithDescription("The next available index to be assigned to entries")) if err != nil { klog.Exitf("Failed to create appenderNextIndex metric: %v", err) } appenderSignedSize, err = meter.Int64Gauge( "tessera.appender.signed.size", metric.WithDescription("Size of the latest signed checkpoint"), metric.WithUnit("{entry}")) if err != nil { klog.Exitf("Failed to create appenderSignedSize metric: %v", err) } appenderWitnessedSize, err = meter.Int64Gauge( "tessera.appender.witnessed.size", metric.WithDescription("Size of the latest successfully witnessed checkpoint"), metric.WithUnit("{entry}")) if err != nil { klog.Exitf("Failed to create appenderWitnessedSize metric: %v", err) } followerEntriesProcessed, err = meter.Int64Gauge( "tessera.follower.processed", metric.WithDescription("Number of entries processed"), metric.WithUnit("{entry}")) if err != nil { klog.Exitf("Failed to create followerEntriesProcessed metric: %v", err) } followerLag, err = meter.Int64Gauge( "tessera.follower.lag", metric.WithDescription("Number of unprocessed entries in the current integrated tree"), metric.WithUnit("{entry}")) if err != nil { klog.Exitf("Failed to create followerLag metric: %v", err) } appenderWitnessRequests, err = meter.Int64Counter( "tessera.appender.witness.requests", metric.WithDescription("Number of attempts to witness a log checkpoint"), metric.WithUnit("{call}")) if err != nil { klog.Exitf("Failed to create appenderWitnessRequests metric: %v", err) } appenderWitnessHistogram, err = meter.Int64Histogram( "tessera.appender.witness.duration", metric.WithDescription("Duration of calls to the configured witness group"), metric.WithUnit("ms"), metric.WithExplicitBucketBoundaries(histogramBuckets...)) if err != nil { klog.Exitf("Failed to create appenderWitnessHistogram metric: %v", err) } } // AddFn adds a new entry to be sequenced by the storage implementation. // // This method should quickly return an IndexFuture, which can be called to resolve to the // index **durably** assigned to the new entry (or an error). // // Implementations MUST NOT allow the future to resolve to an index value unless/until it has // been durably committed by the storage. // // Callers MUST NOT assume that an entry has been accepted or durably stored until they have // successfully resolved the future. // // Once the future resolves and returns an index, the entry can be considered to have been // durably sequenced and will be preserved even in the event that the process terminates. // // Once an entry is sequenced, the storage implementation MUST integrate it into the tree soon // (how long this is expected to take is left unspecified, but as a guideline it should happen // within single digit seconds). Until the entry is integrated and published, clients of the log // will not be able to verifiably access this value. // // Personalities which require blocking until the entry is integrated (e.g. because they wish // to return an inclusion proof) may use the PublicationAwaiter to wrap the call to this method. type AddFn func(ctx context.Context, entry *Entry) IndexFuture // IndexFuture is the signature of a function which can return an assigned index or error. // // Implementations of this func are likely to be "futures", or a promise to return this data at // some point in the future, and as such will block when called if the data isn't yet available. type IndexFuture func() (Index, error) // Index represents a durably assigned index for some entry. type Index struct { // Index is the location in the log to which a particular entry has been assigned. Index uint64 // IsDup is true if Index represents a previously assigned index for an identical entry. IsDup bool } // Appender allows personalities access to the lifecycle methods associated with logs // in sequencing mode. This only has a single method, but other methods are likely to be added // such as a Shutdown method for #341. type Appender struct { Add AddFn } // NewAppender returns an Appender, which allows a personality to incrementally append new // leaves to the log and to read from it. // // The return values are the Appender for adding new entries, a shutdown function, a log reader, // and an error if any of the objects couldn't be constructed. // // Shutdown ensures that all calls to Add that have returned a value will be resolved. Any // futures returned by _this appender_ which resolve to an index will be integrated and have // a checkpoint that commits to them published if this returns successfully. After this returns, // any calls to Add will fail. // // The context passed into this function will be referenced by any background tasks that are started // in the Appender. The correct process for shutting down an Appender cleanly is to first call the // shutdown function that is returned, and then cancel the context. Cancelling the context without calling // shutdown first may mean that some entries added by this appender aren't in the log when the process // exits. func NewAppender(ctx context.Context, d Driver, opts *AppendOptions) (*Appender, func(ctx context.Context) error, LogReader, error) { type appendLifecycle interface { Appender(context.Context, *AppendOptions) (*Appender, LogReader, error) } lc, ok := d.(appendLifecycle) if !ok { return nil, nil, nil, fmt.Errorf("driver %T does not implement Appender lifecycle", d) } if opts == nil { return nil, nil, nil, errors.New("opts cannot be nil") } if err := opts.valid(); err != nil { return nil, nil, nil, err } a, r, err := lc.Appender(ctx, opts) if err != nil { return nil, nil, nil, fmt.Errorf("failed to init appender lifecycle: %v", err) } for i := len(opts.addDecorators) - 1; i >= 0; i-- { a.Add = opts.addDecorators[i](a.Add) } sd := &integrationStats{} a.Add = sd.statsDecorator(a.Add) for _, f := range opts.followers { go f.Follow(ctx, r) go followerStats(ctx, f, r.IntegratedSize) } go sd.updateStats(ctx, r) t := terminator{ delegate: a.Add, readCheckpoint: r.ReadCheckpoint, } // TODO(mhutchinson): move this into the decorators a.Add = func(ctx context.Context, entry *Entry) IndexFuture { if deadline, ok := ctx.Deadline(); ok { appenderDeadlineRemaining.Record(ctx, time.Until(deadline).Milliseconds()) } ctx, span := tracer.Start(ctx, "tessera.Appender.Add") defer span.End() // NOTE: We memoize the returned value here so that repeated calls to the returned // future don't result in unexpected side-effects from inner AddFn functions // being called multiple times. // Currently this is the outermost wrapping of Add so we do the memoization // here, if this changes, ensure that we move the memoization call so that // this remains true. return memoizeFuture(t.Add(ctx, entry)) } return a, t.Shutdown, r, nil } // memoizeFuture wraps an AddFn delegate with logic to ensure that the delegate is called at most // once. func memoizeFuture(delegate IndexFuture) IndexFuture { f := sync.OnceValues(func() (Index, error) { return delegate() }) return f } func followerStats(ctx context.Context, f Follower, size func(context.Context) (uint64, error)) { name := f.Name() t := time.NewTicker(200 * time.Millisecond) for { select { case <-ctx.Done(): return case <-t.C: } n, err := f.EntriesProcessed(ctx) if err != nil { klog.Errorf("followerStats: follower %q EntriesProcessed(): %v", name, err) continue } s, err := size(ctx) if err != nil { klog.Errorf("followerStats: follower %q size(): %v", name, err) } attrs := metric.WithAttributes(followerNameKey.String(name)) followerEntriesProcessed.Record(ctx, otel.Clamp64(n), attrs) followerLag.Record(ctx, otel.Clamp64(s-n), attrs) } } // idxAt represents an index first seen at a particular time. type idxAt struct { idx uint64 at time.Time } // integrationStats knows how to track and populate metrics related to integration performance. // // Currently, this tracks integration latency only. // The integration latency tracking works via a "sample & consume" mechanism, whereby an Add decorator // will record an assigned index along with the time it was assigned. An asynchronous process will // periodically compare the sample with the current integrated tree size, and if the sampled index is // found to be covered by the tree the elapsed period is recorded and the sample "consumed". // // Only one sample may be held at a time. type integrationStats struct { // indexSample points to a sampled indexAt, or nil if there has been no sample made _or_ the sample was consumed. indexSample atomic.Pointer[idxAt] } // sample creates a new sample with the provided index if no sample is already held. func (i *integrationStats) sample(idx uint64) { i.indexSample.CompareAndSwap(nil, &idxAt{idx: idx, at: time.Now()}) } // latency will check whether the provided tree size is larger than the currently sampled index (if one exists), // and, if so, "consume" the sample and return the elapsed interval since the sample was taken. // // The returned bool is true if a sample exists and whose index is lower than the provided tree size, and // false otherwise. func (i *integrationStats) latency(size uint64) (time.Duration, bool) { ia := i.indexSample.Load() // If there _is_ a sample... if ia != nil { // and the sampled index is lower than the tree size if ia.idx < size { // then reset the sample store here so that we're able to accept a future sample. i.indexSample.Store(nil) } return time.Since(ia.at), true } return 0, false } // updateStates periodically checks the current integrated tree size and attempts to // consume any held sample, updating the metric if possible. // // This is a long running function, exitingly only when the provided context is done. func (i *integrationStats) updateStats(ctx context.Context, r LogReader) { if r == nil { klog.Warning("updateStates: nil logreader provided, not updating stats") return } t := time.NewTicker(100 * time.Millisecond) for { select { case <-ctx.Done(): return case <-t.C: } s, err := r.IntegratedSize(ctx) if err != nil { klog.Errorf("IntegratedSize: %v", err) continue } appenderIntegratedSize.Record(ctx, otel.Clamp64(s)) if d, ok := i.latency(s); ok { appenderIntegrateLatency.Record(ctx, d.Milliseconds()) } i, err := r.NextIndex(ctx) if err != nil { klog.Errorf("NextIndex: %v", err) } appenderNextIndex.Record(ctx, otel.Clamp64(i)) } } // statsDecorator wraps a delegate AddFn with code to calculate/update // metric stats. func (i *integrationStats) statsDecorator(delegate AddFn) AddFn { return func(ctx context.Context, entry *Entry) IndexFuture { start := time.Now() f := delegate(ctx, entry) return func() (Index, error) { idx, err := f() attr := []attribute.KeyValue{} if err != nil { switch { // Record the fact there was pushback, if any. case errors.Is(err, ErrPushbackAntispam): attr = append(attr, attribute.String("tessera.pushback", "antispam")) case errors.Is(err, ErrPushbackIntegration): attr = append(attr, attribute.String("tessera.pushback", "integration")) case errors.Is(err, ErrPushback): attr = append(attr, attribute.String("tessera.pushback", "other")) default: // If it's not a pushback, just flag that it's an errored request to avoid high cardinality of attribute values. // TODO(al): We might want to bucket errors into OTel status codes in the future, though. attr = append(attr, attribute.String("tessera.error.type", "_OTHER")) } } attr = append(attr, attribute.Bool("tessera.duplicate", idx.IsDup)) appenderAddsTotal.Add(ctx, 1, metric.WithAttributes(attr...)) d := time.Since(start) appenderAddHistogram.Record(ctx, d.Milliseconds(), metric.WithAttributes(attr...)) if !idx.IsDup { i.sample(idx.Index) } return idx, err } } } type terminator struct { delegate AddFn readCheckpoint func(ctx context.Context) ([]byte, error) // This mutex guards the stopped state. We use this instead of an atomic.Boolean // to get the property that no readers of this state can have the lock when the // write gets it. This means that no in-flight Add operations will be occurring on // Shutdown. mu sync.RWMutex stopped bool // largestIssued tracks the largest index allocated by this appender. largestIssued atomic.Uint64 } func (t *terminator) Add(ctx context.Context, entry *Entry) IndexFuture { t.mu.RLock() defer t.mu.RUnlock() if t.stopped { return func() (Index, error) { return Index{}, errors.New("appender has been shut down") } } res := t.delegate(ctx, entry) return func() (Index, error) { i, err := res() if err != nil { return i, err } // https://github.com/golang/go/issues/63999 - atomically set largest issued index old := t.largestIssued.Load() for old < i.Index && !t.largestIssued.CompareAndSwap(old, i.Index) { old = t.largestIssued.Load() } appenderHighestIndex.Record(ctx, otel.Clamp64(t.largestIssued.Load())) return i, err } } // Shutdown ensures that all calls to Add that have returned a value will be resolved. Any // futures returned by _this appender_ which resolve to an index will be integrated and have // a checkpoint that commits to them published if this returns successfully. // // After this returns, any calls to Add will fail. func (t *terminator) Shutdown(ctx context.Context) error { t.mu.Lock() defer t.mu.Unlock() t.stopped = true maxIndex := t.largestIssued.Load() if maxIndex == 0 { // special case no work done return nil } sleepTime := 0 * time.Millisecond for { select { case <-ctx.Done(): return ctx.Err() default: time.Sleep(sleepTime) } sleepTime = 100 * time.Millisecond // after the first time, ensure we sleep in any other loops cp, err := t.readCheckpoint(ctx) if err != nil { if !errors.Is(err, os.ErrNotExist) { return err } continue } _, size, _, err := parse.CheckpointUnsafe(cp) if err != nil { return err } klog.V(1).Infof("Shutting down, waiting for checkpoint committing to size %d (current checkpoint is %d)", maxIndex, size) if size > maxIndex { return nil } } } // NewAppendOptions creates a new options struct for configuring appender lifecycle instances. // // These options are configured through the use of the various `With.*` function calls on the returned // instance. func NewAppendOptions() *AppendOptions { return &AppendOptions{ batchMaxSize: DefaultBatchMaxSize, batchMaxAge: DefaultBatchMaxAge, entriesPath: layout.EntriesPath, bundleIDHasher: defaultIDHasher, checkpointInterval: DefaultCheckpointInterval, checkpointRepublishInterval: DefaultCheckpointRepublishInterval, addDecorators: make([]func(AddFn) AddFn, 0), pushbackMaxOutstanding: DefaultPushbackMaxOutstanding, garbageCollectionInterval: DefaultGarbageCollectionInterval, } } // AppendOptions holds settings for all storage implementations. type AppendOptions struct { // newCP knows how to format and sign checkpoints. newCP func(ctx context.Context, size uint64, hash []byte) ([]byte, error) batchMaxAge time.Duration batchMaxSize uint pushbackMaxOutstanding uint // EntriesPath knows how to format entry bundle paths. entriesPath func(n uint64, p uint8) string // bundleIDHasher knows how to create antispam leaf identities for entries in a serialised bundle. bundleIDHasher func([]byte) ([][]byte, error) checkpointInterval time.Duration checkpointRepublishInterval time.Duration witnesses WitnessGroup witnessOpts WitnessOptions addDecorators []func(AddFn) AddFn followers []Follower // garbageCollectionInterval of zero should be interpreted as requesting garbage collection to be disabled. garbageCollectionInterval time.Duration } // valid returns an error if an invalid combination of options has been set, or nil otherwise. func (o AppendOptions) valid() error { if o.newCP == nil { return errors.New("invalid AppendOptions: WithCheckpointSigner must be set") } if o.checkpointRepublishInterval > 0 && o.checkpointRepublishInterval < o.checkpointInterval { return fmt.Errorf("invalid AppendOptions: WithCheckpointRepublishInterval (%d) is smaller than WithCheckpointInterval (%d)", o.checkpointRepublishInterval, o.checkpointInterval) } return nil } // WithAntispam configures the appender to use the antispam mechanism to reduce the number of duplicates which // can be added to the log. // // As a starting point, the minimum size of the of in-memory cache should be set to the configured PushbackThreshold // of the provided antispam implementation, multiplied by the number of concurrent front-end instances which // are accepting write-traffic. Data stored in the in-memory cache is relatively small (32 bytes hash, 8 bytes index), // so we recommend erring on the larger side as there is little downside to over-sizing the cache; consider using // the DefaultAntispamInMemorySize as the value here. // // For more details on how the antispam mechanism works, including tuning guidance, see docs/design/antispam.md. func (o *AppendOptions) WithAntispam(inMemEntries uint, as Antispam) *AppendOptions { o.addDecorators = append(o.addDecorators, newInMemoryDedup(inMemEntries)) if as != nil { o.addDecorators = append(o.addDecorators, as.Decorator()) o.followers = append(o.followers, as.Follower(o.bundleIDHasher)) } return o } // CheckpointPublisher returns a function which should be used to create, sign, and potentially witness a new checkpoint. func (o AppendOptions) CheckpointPublisher(lr LogReader, httpClient *http.Client) func(context.Context, uint64, []byte) ([]byte, error) { return func(ctx context.Context, size uint64, root []byte) ([]byte, error) { ctx, span := tracer.Start(ctx, "tessera.CheckpointPublisher") defer span.End() cp, err := o.newCP(ctx, size, root) if err != nil { return nil, fmt.Errorf("newCP: %v", err) } appenderSignedSize.Record(ctx, otel.Clamp64(size)) // Handle witnessing { // Figure out the likely size the witnesses are aware of, but don't fail hard if we're unable // to do so: // a) it could be that this is the first checkpoint we're publishing // b) the witnessing protocol has a fallback path in case we get it wrong, anyway. var oldSize uint64 oldCP, err := lr.ReadCheckpoint(ctx) if err != nil { klog.Infof("Failed to fetch old checkpoint: %v", err) } else { _, oldSize, _, err = parse.CheckpointUnsafe(oldCP) if err != nil { return nil, fmt.Errorf("failed to parse old checkpoint: %v", err) } } wg := witness.NewWitnessGateway(o.witnesses, httpClient, oldSize, lr.ReadTile) start := time.Now() ctx, cancel := context.WithTimeout(ctx, o.witnessOpts.Timeout) defer cancel() witAttr := []attribute.KeyValue{} cp, err = wg.Witness(ctx, cp) if err != nil { if !o.witnessOpts.FailOpen { appenderWitnessRequests.Add(ctx, 1, metric.WithAttributes(attribute.String("error.type", "failed"))) return nil, err } klog.Warningf("WitnessGateway: failing-open despite error: %v", err) witAttr = append(witAttr, attribute.String("error.type", "failed_open")) } appenderWitnessRequests.Add(ctx, 1, metric.WithAttributes(witAttr...)) appenderWitnessedSize.Record(ctx, otel.Clamp64(size)) d := time.Since(start) appenderWitnessHistogram.Record(ctx, d.Milliseconds(), metric.WithAttributes(witAttr...)) } return cp, nil } } func (o AppendOptions) BatchMaxAge() time.Duration { return o.batchMaxAge } func (o AppendOptions) BatchMaxSize() uint { return o.batchMaxSize } func (o AppendOptions) PushbackMaxOutstanding() uint { return o.pushbackMaxOutstanding } func (o AppendOptions) EntriesPath() func(uint64, uint8) string { return o.entriesPath } func (o AppendOptions) CheckpointInterval() time.Duration { return o.checkpointInterval } func (o AppendOptions) CheckpointRepublishInterval() time.Duration { return o.checkpointRepublishInterval } func (o AppendOptions) GarbageCollectionInterval() time.Duration { return o.garbageCollectionInterval } // WithCheckpointSigner is an option for setting the note signer and verifier to use when creating and parsing checkpoints. // This option is mandatory for creating logs where the checkpoint is signed locally, e.g. in // the Appender mode. This does not need to be provided where the storage will be used to mirror // other logs. // // A primary signer must be provided: // - the primary signer is the "canonical" signing identity which should be used when creating new checkpoints. // // Zero or more dditional signers may also be provided. // This enables cases like: // - a rolling key rotation, where checkpoints are signed by both the old and new keys for some period of time, // - using different signature schemes for different audiences, etc. // // When providing additional signers, their names MUST be identical to the primary signer name, and this name will be used // as the checkpoint Origin line. // // Checkpoints signed by these signer(s) will be standard checkpoints as defined by https://c2sp.org/tlog-checkpoint. func (o *AppendOptions) WithCheckpointSigner(s note.Signer, additionalSigners ...note.Signer) *AppendOptions { origin := s.Name() for _, signer := range additionalSigners { if origin != signer.Name() { klog.Exitf("WithCheckpointSigner: additional signer name (%q) does not match primary signer name (%q)", signer.Name(), origin) } } o.newCP = func(ctx context.Context, size uint64, hash []byte) ([]byte, error) { _, span := tracer.Start(ctx, "tessera.SignCheckpoint") defer span.End() // If we're signing a zero-sized tree, the tlog-checkpoint spec says (via RFC6962) that // the root must be SHA256 of the empty string, so we'll enforce that here: if size == 0 { emptyRoot := rfc6962.DefaultHasher.EmptyRoot() hash = emptyRoot[:] } cpRaw := f_log.Checkpoint{ Origin: origin, Size: size, Hash: hash, }.Marshal() n, err := note.Sign(¬e.Note{Text: string(cpRaw)}, append([]note.Signer{s}, additionalSigners...)...) if err != nil { return nil, fmt.Errorf("note.Sign: %w", err) } return n, nil } return o } // WithBatching configures the batching behaviour of leaves being sequenced. // A batch will be allowed to grow in memory until either: // - the number of entries in the batch reach maxSize // - the first entry in the batch has reached maxAge // // At this point the batch will be sent to the sequencer. // // Configuring these parameters allows the personality to tune to get the desired // balance of sequencing latency with cost. In general, larger batches allow for // lower cost of operation, where more frequent batches reduce the amount of time // required for entries to be included in the log. // // If this option isn't provided, storage implementations with use the DefaultBatchMaxSize and DefaultBatchMaxAge consts above. func (o *AppendOptions) WithBatching(maxSize uint, maxAge time.Duration) *AppendOptions { o.batchMaxSize = maxSize o.batchMaxAge = maxAge return o } // WithPushback allows configuration of when the storage should start pushing back on add requests. // // maxOutstanding is the number of "in-flight" add requests - i.e. the number of entries with sequence numbers // assigned, but which are not yet integrated into the log. func (o *AppendOptions) WithPushback(maxOutstanding uint) *AppendOptions { o.pushbackMaxOutstanding = maxOutstanding return o } // WithCheckpointInterval configures the frequency at which Tessera will attempt to create & publish // new checkpoints. // // Well behaved clients of the log will only "see" newly sequenced entries once a new checkpoint is published, // so it's important to set that value such that it works well with your ecosystem. // // Regularly publishing new checkpoints: // - helps show that the log is "live", even if no entries are being added. // - enables clients of the log to reason about how frequently they need to have their // view of the log refreshed, which in turn helps reduce work/load across the ecosystem. // // Note that this option probably only makes sense for long-lived applications (e.g. HTTP servers). // // If this option isn't provided, storage implementations will use the DefaultCheckpointInterval const above. func (o *AppendOptions) WithCheckpointInterval(interval time.Duration) *AppendOptions { o.checkpointInterval = interval return o } // WithCheckpointRepublishInterval configures the frequency at which Tessera will allow re-publishing // checkpoints where the log hasn't grown since the last checkpoint was published. // // Setting this less than or equal to zero will disable republication of unchanged checkpoints. func (o *AppendOptions) WithCheckpointRepublishInterval(interval time.Duration) *AppendOptions { o.checkpointRepublishInterval = interval return o } // WithWitnesses configures the set of witnesses that Tessera will contact in order to counter-sign // a checkpoint before publishing it. A request will be sent to every witness referenced by the group // using the URLs method. The checkpoint will be accepted for publishing when a sufficient number of // witnesses to Satisfy the group have responded. // // If this method is not called, then the default empty WitnessGroup will be used, which contacts zero // witnesses and requires zero witnesses in order to publish. func (o *AppendOptions) WithWitnesses(witnesses WitnessGroup, opts *WitnessOptions) *AppendOptions { if opts == nil { opts = &WitnessOptions{} } if opts.Timeout == 0 { opts.Timeout = DefaultWitnessTimeout } o.witnesses = witnesses o.witnessOpts = *opts return o } // WitnessOptions contains extra optional configuration for how Tessera should use/interact with // a user-provided WitnessGroup policy. type WitnessOptions struct { // Timeout is the maximum time to wait while attempting to satisfy the configured witness policy. // // If the policy has not already been satisfied at the point this duration has passed, Tessera // will stop waiting for more responses. The FailOpen option below controls whether or not the // checkpoint will be published in this case. // // If unset, uses DefaultWitnessTimeout. Timeout time.Duration // FailOpen controls whether a checkpoint, for which the witness policy was unable to be met, // should still be published. // // This setting is intended only for facilitating early "non-blocking" adoption of witnessing, // and will be disabled and/or removed in the future. FailOpen bool } // WithGarbageCollectionInterval allows the interval between scans to remove obsolete partial // tiles and entry bundles. // // Setting to zero disables garbage collection. func (o *AppendOptions) WithGarbageCollectionInterval(interval time.Duration) *AppendOptions { o.garbageCollectionInterval = interval return o } transparency-dev-tessera-3cb22ee/append_lifecycle_test.go000066400000000000000000000067031511600621500240110ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "context" "strings" "testing" "time" "golang.org/x/mod/sumdb/note" ) func TestMemoize(t *testing.T) { // Set up an AddFn which will increment a counter every time it's called, and return that in the Index. i := uint64(0) deleg := func() (Index, error) { i++ return Index{ Index: i, }, nil } add := func(_ context.Context, _ *Entry) IndexFuture { return deleg } // Create a single future (for a single Entry), and convince ourselves that the counter is being incremented // each time the future is being invoked. f1 := add(nil, nil) a, _ := f1() b, _ := f1() if a.Index == b.Index { t.Fatalf("a(=%d) == b(=%d)", a.Index, b.Index) } // Now create an AddFn which memoizes the result of the delegate, like we do in NewAppender, and assert that // repeated calls to the future work as expected; only incrementing the counter once. add = func(_ context.Context, _ *Entry) IndexFuture { return memoizeFuture(deleg) } f2 := add(nil, nil) c, _ := f2() d, _ := f2() if c.Index != d.Index { t.Fatalf("c(=%d) != d(=%d)", c.Index, d.Index) } } const testSignerKey = "PRIVATE+KEY+example.com/log/testdata+33d7b496+AeymY/SZAX0jZcJ8enZ5FY1Dz+wTML2yWSkK+9DSF3eg" func TestAppendOptionsValid(t *testing.T) { for _, test := range []struct { name string opts *AppendOptions wantErrContains string }{ { name: "Valid", opts: NewAppendOptions().WithCheckpointSigner(mustCreateSigner(t, testSignerKey)), }, { name: "Valid: CheckpointRepublishInterval == CheckpointInterval", opts: NewAppendOptions(). WithCheckpointSigner(mustCreateSigner(t, testSignerKey)). WithCheckpointInterval(10 * time.Second). WithCheckpointRepublishInterval(10 * time.Second), }, { name: "Error: CheckpointRepublishInterval < CheckpointInterval", opts: NewAppendOptions(). WithCheckpointSigner(mustCreateSigner(t, testSignerKey)). WithCheckpointInterval(10 * time.Second). WithCheckpointRepublishInterval(9 * time.Second), wantErrContains: "WithCheckpointRepublishInterval", }, { name: "Error: No CheckpointSigner", opts: NewAppendOptions(), wantErrContains: "WithCheckpointSigner", }, } { t.Run(test.name, func(t *testing.T) { err := test.opts.valid() switch gotErr, wantErr := err != nil, test.wantErrContains != ""; { case gotErr && !wantErr: t.Fatalf("Got unexpected error %q, want no error", err) case !gotErr && wantErr: t.Fatalf("Got no error, expected error") case gotErr: if !strings.Contains(err.Error(), test.wantErrContains) { t.Fatalf("Got err %q, want error containing %q", err.Error(), test.wantErrContains) } } }) } } func mustCreateSigner(t *testing.T, k string) note.Signer { t.Helper() s, err := note.NewSigner(k) if err != nil { t.Fatalf("Failed to create signer: %v", err) } return s } transparency-dev-tessera-3cb22ee/await.go000066400000000000000000000105631511600621500205700ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "context" "errors" "os" "sync" "time" "github.com/transparency-dev/tessera/internal/parse" "k8s.io/klog/v2" ) // NewPublicationAwaiter provides an PublicationAwaiter that can be cancelled // using the provided context. The PublicationAwaiter will poll every `pollPeriod` // to fetch checkpoints using the `readCheckpoint` function. func NewPublicationAwaiter(ctx context.Context, readCheckpoint func(ctx context.Context) ([]byte, error), pollPeriod time.Duration) *PublicationAwaiter { a := &PublicationAwaiter{ c: sync.NewCond(&sync.Mutex{}), } go a.pollLoop(ctx, readCheckpoint, pollPeriod) return a } // PublicationAwaiter allows client threads to block until a leaf is published. // This means it has a sequence number, and been integrated into the tree, and // a checkpoint has been published for it. // A single long-lived PublicationAwaiter instance // should be reused for all requests in the application code as there is some // overhead to each one; the core of an PublicationAwaiter is a poll loop that // will fetch checkpoints whenever it has clients waiting. // // The expected call pattern is: // // i, cp, err := awaiter.Await(ctx, storage.Add(myLeaf)) // // When used this way, it requires very little code at the point of use to // block until the new leaf is integrated into the tree. type PublicationAwaiter struct { c *sync.Cond // size, checkpoint, and err keep track of the latest size and checkpoint // (or error) seen by the poller. size uint64 checkpoint []byte err error } // Await blocks until the IndexFuture is resolved, and this new index has been // integrated into the log, i.e. the log has made a checkpoint available that // commits to this new index. When this happens, Await returns the index at // which the leaf has been added, and a checkpoint that commits to this index. // // This operation can be aborted early by cancelling the context. In this event, // or in the event that there is an error getting a valid checkpoint, an error // will be returned from this method. func (a *PublicationAwaiter) Await(ctx context.Context, future IndexFuture) (Index, []byte, error) { _, span := tracer.Start(ctx, "tessera.Await") defer span.End() i, err := future() if err != nil { return i, nil, err } a.c.L.Lock() defer a.c.L.Unlock() for (a.size <= i.Index && a.err == nil) && ctx.Err() == nil { a.c.Wait() } // Ensure we propogate context done error, if any. if err := ctx.Err(); err != nil { a.err = err } return i, a.checkpoint, a.err } // pollLoop MUST be called in a goroutine when constructing an PublicationAwaiter // and will run continually until its context is cancelled. It wakes up every // `pollPeriod` to check if there are clients blocking. If there are, it requests // the latest checkpoint from the log, parses the tree size, and releases all clients // that were blocked on an index smaller than this tree size. func (a *PublicationAwaiter) pollLoop(ctx context.Context, readCheckpoint func(ctx context.Context) ([]byte, error), pollPeriod time.Duration) { var ( cp []byte cpErr error cpSize uint64 ) for done := false; !done; { select { case <-ctx.Done(): klog.Info("PublicationAwaiter exiting due to context completion") cp, cpSize, cpErr = nil, 0, ctx.Err() done = true case <-time.After(pollPeriod): cp, cpErr = readCheckpoint(ctx) switch { case errors.Is(cpErr, os.ErrNotExist): continue case cpErr != nil: cpSize = 0 default: _, cpSize, _, cpErr = parse.CheckpointUnsafe(cp) } } a.c.L.Lock() // Note that for now, this releases all clients in the event of a single failure. // If this causes problems, this could be changed to attempt retries. a.checkpoint = cp a.size = cpSize a.err = cpErr a.c.Broadcast() a.c.L.Unlock() } } transparency-dev-tessera-3cb22ee/await_test.go000066400000000000000000000144311511600621500216250ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "bytes" "context" "crypto/sha256" "fmt" "sync" "sync/atomic" "time" "errors" "testing" "github.com/transparency-dev/formats/log" "golang.org/x/mod/sumdb/note" ) func TestAwait(t *testing.T) { t.Parallel() testTimeout := 100 * time.Millisecond testCases := []struct { desc string fIndex uint64 fErr error fDelay time.Duration cpBody []byte cpErr error cpDelay time.Duration wantErr bool }{ { desc: "future error", fIndex: 0, fErr: errors.New("you have no future"), fDelay: 0, wantErr: true, }, { desc: "future takes too long", fIndex: 2, fErr: nil, fDelay: testTimeout, wantErr: true, }, { desc: "checkpoint is big enough", fIndex: 2, fErr: nil, fDelay: 0, cpBody: []byte("origin\n3\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n"), cpErr: nil, wantErr: false, }, { desc: "checkpoint is too small", fIndex: 2, fErr: nil, fDelay: 0, cpBody: []byte("origin\n2\nthisisdefinitelyahash\n"), cpErr: nil, wantErr: true, }, { desc: "checkpoint takes too long", fIndex: 2, fErr: nil, fDelay: 0, cpBody: []byte("origin\n3\nthisisdefinitelyahash\n"), cpErr: nil, cpDelay: testTimeout, wantErr: true, }, { desc: "checkpoint takes a few polls then returns", fIndex: 2, fErr: nil, fDelay: 0, cpBody: []byte("origin\n3\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n"), cpErr: nil, cpDelay: 40 * time.Millisecond, wantErr: false, }, { desc: "checkpoint takes a few polls then fails", fIndex: 2, fErr: nil, fDelay: 0, cpBody: nil, cpErr: errors.New("sorry but the checkpoint is in another castle"), cpDelay: 40 * time.Millisecond, wantErr: true, }, { desc: "checkpoint is garbled - no newlines", fIndex: 2, fErr: nil, fDelay: 0, cpBody: []byte("origin22nonewlineshere"), cpErr: nil, wantErr: true, }, { desc: "checkpoint is garbled - size not parseable", fIndex: 2, fErr: nil, fDelay: 0, cpBody: []byte("origin\ntwo\nnonewlineshere"), cpErr: nil, wantErr: true, }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { t.Parallel() // Await will time out via this context, causing tests to fail // if the integration condition is never reached. ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() readCheckpoint := func(ctx context.Context) ([]byte, error) { <-time.After(tC.cpDelay) return tC.cpBody, tC.cpErr } awaiter := NewPublicationAwaiter(ctx, readCheckpoint, 10*time.Millisecond) future := func() (Index, error) { <-time.After(tC.fDelay) return Index{Index: tC.fIndex}, tC.fErr } i, cp, err := awaiter.Await(ctx, future) if gotErr := err != nil; gotErr != tC.wantErr { t.Fatalf("gotErr != wantErr (%t != %t): %v", gotErr, tC.wantErr, err) } if err != nil { // Everything after here tests successful Await return } if i.Index != tC.fIndex { t.Errorf("expected index %d but got %d", tC.fIndex, i.Index) } if !bytes.Equal(cp, tC.cpBody) { t.Errorf("expected checkpoint %q but got %q", tC.cpBody, cp) } }) } } func TestAwait_multiClient(t *testing.T) { s, err := note.NewSigner("PRIVATE+KEY+example.com/log/testdata+33d7b496+AeymY/SZAX0jZcJ8enZ5FY1Dz+wTML2yWSkK+9DSF3eg") if err != nil { t.Fatal(err) } v, err := note.NewVerifier("example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx") if err != nil { t.Fatal(err) } t.Parallel() testTimeout := 1 * time.Second // Await will time out via this context, causing tests to fail // if the integration condition is never reached. ctx, cancel := context.WithTimeout(context.Background(), testTimeout) defer cancel() size := uint64(0) readCheckpoint := func(ctx context.Context) ([]byte, error) { <-time.After(3 * time.Millisecond) // Grow the tree every time this is called size += 10 // This isn't generating a real log but can be changed if needed hash := sha256.Sum256(fmt.Append(nil, size)) cpRaw := log.Checkpoint{ Origin: "example.com/log/testdata", Size: size, Hash: hash[:], }.Marshal() n, err := note.Sign(¬e.Note{Text: string(cpRaw)}, s) if err != nil { return nil, fmt.Errorf("note.Sign: %w", err) } return n, nil } awaiter := NewPublicationAwaiter(ctx, readCheckpoint, 10*time.Millisecond) wg := sync.WaitGroup{} for i := range 300 { index := uint64(i) future := func() (Index, error) { <-time.After(15 * time.Millisecond) return Index{Index: index}, nil } wg.Add(1) go func() { i, cpRaw, err := awaiter.Await(ctx, future) if err != nil { t.Errorf("function for %d failed: %v", i.Index, err) } if i.Index != index { t.Errorf("got %d but expected %d", i.Index, index) } cp, _, _, err := log.ParseCheckpoint(cpRaw, "example.com/log/testdata", v) if err != nil { t.Error(err) } if cp.Size < i.Index { t.Errorf("got cp size of %d for index %d", cp.Size, i.Index) } wg.Done() }() } wg.Wait() } func BenchmarkAwait(b *testing.B) { cpFormat := "origin/\n%d\nhash\n\nsig" cpSize := atomic.Uint64{} readCP := func(_ context.Context) ([]byte, error) { return fmt.Appendf(nil, cpFormat, cpSize.Load()), nil } a := NewPublicationAwaiter(b.Context(), readCP, time.Millisecond) t := time.NewTicker(time.Millisecond) go func() { for { select { case <-b.Context().Done(): return case <-t.C: cpSize.Add(1) } } }() f := func() (Index, error) { return Index{Index: cpSize.Load()}, nil } for b.Loop() { _, _, _ = a.Await(b.Context(), f) } } transparency-dev-tessera-3cb22ee/client/000077500000000000000000000000001511600621500204055ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/client/client.go000066400000000000000000000372321511600621500222210ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package client provides client support for interacting with logs that // uses the [tlog-tiles API]. // // [tlog-tiles API]: https://c2sp.org/tlog-tiles package client import ( "context" "fmt" "sync" "github.com/transparency-dev/formats/log" "github.com/transparency-dev/merkle/compact" "github.com/transparency-dev/merkle/proof" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/internal/otel" "golang.org/x/mod/sumdb/note" ) var ( hasher = rfc6962.DefaultHasher ) // ErrInconsistency should be returned when there has been an error proving consistency // between log states. // The raw log state representations are included as-returned by the target log, this // ensures that evidence of inconsistent log updates are available to the caller of // the method(s) returning this error. type ErrInconsistency struct { SmallerRaw []byte LargerRaw []byte Proof [][]byte Wrapped error } func (e ErrInconsistency) Unwrap() error { return e.Wrapped } func (e ErrInconsistency) Error() string { return fmt.Sprintf("log consistency check failed: %s", e.Wrapped) } // CheckpointFetcherFunc is the signature of a function which can retrieve the latest // checkpoint from a log's data storage. // // Note that the implementation of this MUST return (either directly or wrapped) // an os.ErrIsNotExist when the file referenced by path does not exist, e.g. a HTTP // based implementation MUST return this error when it receives a 404 StatusCode. type CheckpointFetcherFunc func(ctx context.Context) ([]byte, error) // TileFetcherFunc is the signature of a function which can fetch the raw data // for a given tile. // // Note that the implementation of this MUST: // - when asked to fetch a partial tile (i.e. p != 0), fall-back to fetching the corresponding full // tile if the partial one does not exist. // - return (either directly or wrapped) an os.ErrIsNotExist when neither the requested tile nor any // fallback tile exists. type TileFetcherFunc func(ctx context.Context, level, index uint64, p uint8) ([]byte, error) // EntryBundleFetcherFunc is the signature of a function which can fetch the raw data // for a given entry bundle. // // Note that the implementation of this MUST: // - when asked to fetch a partial entry bundle (i.e. p != 0), fall-back to fetching the corresponding full // bundle if the partial one does not exist. // - return (either directly or wrapped) an os.ErrIsNotExist when neither the requested bundle nor any // fallback bundle exists. type EntryBundleFetcherFunc func(ctx context.Context, bundleIndex uint64, p uint8) ([]byte, error) // ConsensusCheckpointFunc is a function which returns the largest checkpoint known which is // signed by logSigV and satisfies some consensus algorithm. // // This is intended to provide a hook for adding a consensus view of a log, e.g. via witnessing. type ConsensusCheckpointFunc func(ctx context.Context, logSigV note.Verifier, origin string) (*log.Checkpoint, []byte, *note.Note, error) // UnilateralConsensus blindly trusts the source log, returning the checkpoint it provided. func UnilateralConsensus(f CheckpointFetcherFunc) ConsensusCheckpointFunc { return func(ctx context.Context, logSigV note.Verifier, origin string) (*log.Checkpoint, []byte, *note.Note, error) { return FetchCheckpoint(ctx, f, logSigV, origin) } } // FetchCheckpoint retrieves and opens a checkpoint from the log. // Returns both the parsed structure and the raw serialised checkpoint. func FetchCheckpoint(ctx context.Context, f CheckpointFetcherFunc, v note.Verifier, origin string) (*log.Checkpoint, []byte, *note.Note, error) { ctx, span := tracer.Start(ctx, "tessera.client.FetchCheckpoint") defer span.End() cpRaw, err := f(ctx) if err != nil { return nil, nil, nil, err } cp, _, n, err := log.ParseCheckpoint(cpRaw, origin, v) if err != nil { return nil, nil, nil, fmt.Errorf("failed to parse Checkpoint: %v", err) } return cp, cpRaw, n, nil } // FetchRangeNodes returns the set of nodes representing the compact range covering // a log of size s. func FetchRangeNodes(ctx context.Context, s uint64, f TileFetcherFunc) ([][]byte, error) { ctx, span := tracer.Start(ctx, "tessera.client.FetchRangeNodes") defer span.End() span.SetAttributes(logSizeKey.Int64(otel.Clamp64(s))) nc := newNodeCache(f, s) nIDs := make([]compact.NodeID, 0, compact.RangeSize(0, s)) nIDs = compact.RangeNodes(0, s, nIDs) hashes := make([][]byte, 0, len(nIDs)) for _, n := range nIDs { h, err := nc.GetNode(ctx, n) if err != nil { return nil, err } hashes = append(hashes, h) } return hashes, nil } // FetchLeafHashes fetches N consecutive leaf hashes starting with the leaf at index first. func FetchLeafHashes(ctx context.Context, f TileFetcherFunc, first, N, logSize uint64) ([][]byte, error) { ctx, span := tracer.Start(ctx, "tessera.client.FetchLeafHashes") defer span.End() span.SetAttributes(firstKey.Int64(otel.Clamp64(first)), NKey.Int64(otel.Clamp64(N)), logSizeKey.Int64(otel.Clamp64(logSize))) nc := newNodeCache(f, logSize) hashes := make([][]byte, 0, N) for i, end := first, first+N; i < end; i++ { nID := compact.NodeID{Level: 0, Index: i} h, err := nc.GetNode(ctx, nID) if err != nil { return nil, fmt.Errorf("failed to fetch node %v: %v", nID, err) } hashes = append(hashes, h) } return hashes, nil } // GetEntryBundle fetches the entry bundle at the given _tile index_. func GetEntryBundle(ctx context.Context, f EntryBundleFetcherFunc, i, logSize uint64) (api.EntryBundle, error) { ctx, span := tracer.Start(ctx, "tessera.client.GetEntryBundle") defer span.End() span.SetAttributes(indexKey.Int64(otel.Clamp64(i)), logSizeKey.Int64(otel.Clamp64(logSize))) bundle := api.EntryBundle{} p := layout.PartialTileSize(0, i, logSize) sRaw, err := f(ctx, i, p) if err != nil { return bundle, fmt.Errorf("failed to fetch bundle at index %d: %v", i, err) } if err := bundle.UnmarshalText(sRaw); err != nil { return bundle, fmt.Errorf("failed to parse EntryBundle at index %d: %v", i, err) } return bundle, nil } // ProofBuilder knows how to build inclusion and consistency proofs from tiles. // Since the tiles commit only to immutable nodes, the job of building proofs is slightly // more complex as proofs can touch "ephemeral" nodes, so these need to be synthesized. // This object constructs a cache internally to make it efficient for multiple operations // at a given tree size. type ProofBuilder struct { treeSize uint64 nodeCache nodeCache } // NewProofBuilder creates a new ProofBuilder object for a given tree size. // The returned ProofBuilder can be re-used for proofs related to a given tree size, but // it is not thread-safe and should not be accessed concurrently. func NewProofBuilder(ctx context.Context, treeSize uint64, f TileFetcherFunc) (*ProofBuilder, error) { pb := &ProofBuilder{ treeSize: treeSize, nodeCache: newNodeCache(f, treeSize), } return pb, nil } // InclusionProof constructs an inclusion proof for the leaf at index in a tree of // the given size. func (pb *ProofBuilder) InclusionProof(ctx context.Context, index uint64) ([][]byte, error) { ctx, span := tracer.Start(ctx, "tessera.client.InclusionProof") defer span.End() span.SetAttributes(indexKey.Int64(otel.Clamp64(index))) nodes, err := proof.Inclusion(index, pb.treeSize) if err != nil { return nil, fmt.Errorf("failed to calculate inclusion proof node list: %v", err) } return pb.fetchNodes(ctx, nodes) } // ConsistencyProof constructs a consistency proof between the provided tree sizes. func (pb *ProofBuilder) ConsistencyProof(ctx context.Context, smaller, larger uint64) ([][]byte, error) { ctx, span := tracer.Start(ctx, "tessera.client.ConsistencyProof") defer span.End() span.SetAttributes(smallerKey.Int64(otel.Clamp64(smaller)), largerKey.Int64(otel.Clamp64(larger))) if m := max(smaller, larger); m > pb.treeSize { return nil, fmt.Errorf("requested consistency proof to %d which is larger than tree size %d", m, pb.treeSize) } nodes, err := proof.Consistency(smaller, larger) if err != nil { return nil, fmt.Errorf("failed to calculate consistency proof node list: %v", err) } return pb.fetchNodes(ctx, nodes) } // fetchNodes retrieves the specified proof nodes via pb's nodeCache. func (pb *ProofBuilder) fetchNodes(ctx context.Context, nodes proof.Nodes) ([][]byte, error) { hashes := make([][]byte, 0, len(nodes.IDs)) // TODO(al) parallelise this. for _, id := range nodes.IDs { h, err := pb.nodeCache.GetNode(ctx, id) if err != nil { return nil, fmt.Errorf("failed to get node (%v): %v", id, err) } hashes = append(hashes, h) } var err error if hashes, err = nodes.Rehash(hashes, hasher.HashChildren); err != nil { return nil, fmt.Errorf("failed to rehash proof: %v", err) } return hashes, nil } // LogStateTracker represents a client-side view of a target log's state. // This tracker handles verification that updates to the tracked log state are // consistent with previously seen states. type LogStateTracker struct { origin string consensusCheckpoint ConsensusCheckpointFunc cpSigVerifier note.Verifier tileFetcher TileFetcherFunc // The fields under here will all be updated at the same time. // Access to any of these fields is guarded by mu. mu sync.RWMutex // latestConsistent is the deserialised form of LatestConsistentRaw latestConsistent log.Checkpoint // latestConsistentRaw holds the raw bytes of the latest proven-consistent // LogState seen by this tracker. latestConsistentRaw []byte // proofBuilder for building proofs at LatestConsistent checkpoint. proofBuilder *ProofBuilder } // NewLogStateTracker creates a newly initialised tracker. // If a serialised LogState representation is provided then this is used as the // initial tracked state, otherwise a log state is fetched from the target log. func NewLogStateTracker(ctx context.Context, tF TileFetcherFunc, checkpointRaw []byte, nV note.Verifier, origin string, cc ConsensusCheckpointFunc) (*LogStateTracker, error) { ret := &LogStateTracker{ origin: origin, consensusCheckpoint: cc, cpSigVerifier: nV, tileFetcher: tF, } if len(checkpointRaw) > 0 { ret.latestConsistentRaw = checkpointRaw cp, _, _, err := log.ParseCheckpoint(checkpointRaw, origin, nV) if err != nil { return ret, err } ret.latestConsistent = *cp ret.proofBuilder, err = NewProofBuilder(ctx, ret.latestConsistent.Size, ret.tileFetcher) if err != nil { return ret, fmt.Errorf("NewProofBuilder: %v", err) } return ret, nil } _, _, _, err := ret.Update(ctx) return ret, err } // Update attempts to update the local view of the target log's state. // If a more recent logstate is found, this method will attempt to prove // that it is consistent with the local state before updating the tracker's // view. // Returns the old checkpoint, consistency proof, and newer checkpoint used to update. // If the LatestConsistent checkpoint is 0 sized, no consistency proof will be returned // since it would be meaningless to do so. func (lst *LogStateTracker) Update(ctx context.Context) ([]byte, [][]byte, []byte, error) { ctx, span := tracer.Start(ctx, "tessera.client.logstatetracker.Update") defer span.End() c, cRaw, _, err := lst.consensusCheckpoint(ctx, lst.cpSigVerifier, lst.origin) if err != nil { return nil, nil, nil, err } builder, err := NewProofBuilder(ctx, c.Size, lst.tileFetcher) if err != nil { return nil, nil, nil, fmt.Errorf("failed to create proof builder: %v", err) } lst.mu.Lock() defer lst.mu.Unlock() var p [][]byte if lst.latestConsistent.Size > 0 { if c.Size <= lst.latestConsistent.Size { return lst.latestConsistentRaw, p, lst.latestConsistentRaw, nil } p, err = builder.ConsistencyProof(ctx, lst.latestConsistent.Size, c.Size) if err != nil { return nil, nil, nil, err } if err := proof.VerifyConsistency(hasher, lst.latestConsistent.Size, c.Size, p, lst.latestConsistent.Hash, c.Hash); err != nil { return nil, nil, nil, ErrInconsistency{ SmallerRaw: lst.latestConsistentRaw, LargerRaw: cRaw, Proof: p, Wrapped: err, } } // Update is consistent, } oldRaw := lst.latestConsistentRaw lst.latestConsistentRaw, lst.latestConsistent = cRaw, *c lst.proofBuilder = builder return oldRaw, p, lst.latestConsistentRaw, nil } func (lst *LogStateTracker) Latest() log.Checkpoint { lst.mu.RLock() defer lst.mu.RUnlock() return lst.latestConsistent } // tileKey is used as a key in nodeCache's tile map. type tileKey struct { tileLevel uint64 tileIndex uint64 } // nodeCache hides the tiles abstraction away, and improves // performance by caching tiles it's seen. // Not threadsafe, and intended to be only used throughout the course // of a single request. type nodeCache struct { logSize uint64 ephemeral map[compact.NodeID][]byte tiles map[tileKey]api.HashTile getTile TileFetcherFunc } // newNodeCache creates a new nodeCache instance for a given log size. func newNodeCache(f TileFetcherFunc, logSize uint64) nodeCache { return nodeCache{ logSize: logSize, ephemeral: make(map[compact.NodeID][]byte), tiles: make(map[tileKey]api.HashTile), getTile: f, } } // SetEphemeralNode stored a derived "ephemeral" tree node. func (n *nodeCache) SetEphemeralNode(id compact.NodeID, h []byte) { n.ephemeral[id] = h } // GetNode returns the internal log tree node hash for the specified node ID. // A previously set ephemeral node will be returned if id matches, otherwise // the tile containing the requested node will be fetched and cached, and the // node hash returned. func (n *nodeCache) GetNode(ctx context.Context, id compact.NodeID) ([]byte, error) { ctx, span := tracer.Start(ctx, "tessera.client.nodecache.GetNode") defer span.End() span.SetAttributes(indexKey.Int64(otel.Clamp64(id.Index)), levelKey.Int64(int64(id.Level))) // First check for ephemeral nodes: if e := n.ephemeral[id]; len(e) != 0 { return e, nil } // Otherwise look in fetched tiles: tileLevel, tileIndex, nodeLevel, nodeIndex := layout.NodeCoordsToTileAddress(uint64(id.Level), uint64(id.Index)) tKey := tileKey{tileLevel, tileIndex} t, ok := n.tiles[tKey] if !ok { span.AddEvent("cache miss") p := layout.PartialTileSize(tileLevel, tileIndex, n.logSize) tileRaw, err := n.getTile(ctx, tileLevel, tileIndex, p) if err != nil { return nil, fmt.Errorf("failed to fetch tile: %v", err) } var tile api.HashTile if err := tile.UnmarshalText(tileRaw); err != nil { return nil, fmt.Errorf("failed to parse tile: %v", err) } t = tile n.tiles[tKey] = tile } // We've got the tile, now we need to look up (or calculate) the node inside of it numLeaves := 1 << nodeLevel firstLeaf := int(nodeIndex) * numLeaves lastLeaf := firstLeaf + numLeaves if lastLeaf > len(t.Nodes) { return nil, fmt.Errorf("require leaf nodes [%d, %d) but only got %d leaves", firstLeaf, lastLeaf, len(t.Nodes)) } rf := compact.RangeFactory{Hash: hasher.HashChildren} r := rf.NewEmptyRange(0) for _, l := range t.Nodes[firstLeaf:lastLeaf] { if err := r.Append(l, nil); err != nil { return nil, fmt.Errorf("failed to Append: %v", err) } } return r.GetRootHash(nil) } transparency-dev-tessera-3cb22ee/client/client_test.go000066400000000000000000000234301511600621500232530ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package client import ( "bytes" "context" "crypto/sha256" "errors" "fmt" "os" "path/filepath" "testing" "github.com/transparency-dev/formats/log" "github.com/transparency-dev/merkle/compact" "github.com/transparency-dev/merkle/proof" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "golang.org/x/mod/sumdb/note" ) var ( testOrigin = "example.com/log/testdata" testLogVerifier = mustMakeVerifier("example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx") // Built using testdata/build_log.sh testRawCheckpoints, testCheckpoints = mustLoadTestCheckpoints() ) func mustMakeVerifier(vs string) note.Verifier { v, err := note.NewVerifier(vs) if err != nil { panic(fmt.Errorf("NewVerifier(%q): %v", vs, err)) } return v } func mustLoadTestCheckpoints() ([][]byte, []log.Checkpoint) { raws, cps := make([][]byte, 0), make([]log.Checkpoint, 0) for i := 0; ; i++ { cpName := fmt.Sprintf("checkpoint.%d", i) r, err := testLogFetcher(context.Background(), cpName) if err != nil { if errors.Is(err, os.ErrNotExist) { // Probably just no more checkpoints left break } panic(err) } cp, _, _, err := log.ParseCheckpoint(r, testOrigin, testLogVerifier) if err != nil { panic(fmt.Errorf("ParseCheckpoint(%s): %v", cpName, err)) } raws, cps = append(raws, r), append(cps, *cp) } if len(raws) == 0 { panic("no checkpoints loaded") } return raws, cps } // testLogFetcher is a fetcher which reads from the checked-in golden test log // data stored in ../testdata/log func testLogFetcher(_ context.Context, p string) ([]byte, error) { path := filepath.Join("../testdata/log", p) return os.ReadFile(path) } func testLogTileFetcher(ctx context.Context, l, i uint64, p uint8) ([]byte, error) { return testLogFetcher(ctx, layout.TilePath(l, i, p)) } // fetchCheckpointShim allows fetcher requests for checkpoints to be intercepted. type fetchCheckpointShim struct { // Checkpoints holds raw checkpoints to be returned when the fetcher is asked to retrieve a checkpoint path. // The zero-th entry will be returned until Advance is called. Checkpoints [][]byte } // Fetcher intercepts requests for the checkpoint file, returning the zero-th // entry in the Checkpoints field. All other requests are passed through // to the delegate fetcher. func (f *fetchCheckpointShim) FetchCheckpoint(ctx context.Context) ([]byte, error) { if len(f.Checkpoints) == 0 { return nil, os.ErrNotExist } r := f.Checkpoints[0] return r, nil } // Advance causes subsequent intercepted checkpoint requests to return // the next entry in the Checkpoints slice. func (f *fetchCheckpointShim) Advance() { f.Checkpoints = f.Checkpoints[1:] } func TestCheckLogStateTracker(t *testing.T) { ctx := context.Background() for _, test := range []struct { desc string cpRaws [][]byte wantCpRaws [][]byte }{ { desc: "Consistent", cpRaws: [][]byte{ testRawCheckpoints[0], testRawCheckpoints[2], testRawCheckpoints[3], testRawCheckpoints[5], testRawCheckpoints[6], testRawCheckpoints[10], }, wantCpRaws: [][]byte{ testRawCheckpoints[0], testRawCheckpoints[2], testRawCheckpoints[3], testRawCheckpoints[5], testRawCheckpoints[6], testRawCheckpoints[10], }, }, { desc: "Identical CP", cpRaws: [][]byte{ testRawCheckpoints[0], testRawCheckpoints[0], testRawCheckpoints[0], testRawCheckpoints[0], }, wantCpRaws: [][]byte{ testRawCheckpoints[0], testRawCheckpoints[0], testRawCheckpoints[0], testRawCheckpoints[0], }, }, { desc: "Identical CP pairs", cpRaws: [][]byte{ testRawCheckpoints[0], testRawCheckpoints[0], testRawCheckpoints[5], testRawCheckpoints[5], }, wantCpRaws: [][]byte{ testRawCheckpoints[0], testRawCheckpoints[0], testRawCheckpoints[5], testRawCheckpoints[5], }, }, { desc: "Out of order", cpRaws: [][]byte{ testRawCheckpoints[5], testRawCheckpoints[2], testRawCheckpoints[0], testRawCheckpoints[3], }, wantCpRaws: [][]byte{ testRawCheckpoints[5], testRawCheckpoints[5], testRawCheckpoints[5], testRawCheckpoints[5], }, }, } { t.Run(test.desc, func(t *testing.T) { shim := fetchCheckpointShim{Checkpoints: test.cpRaws} lst, err := NewLogStateTracker(ctx, testLogTileFetcher, testRawCheckpoints[0], testLogVerifier, testOrigin, UnilateralConsensus(shim.FetchCheckpoint)) if err != nil { t.Fatalf("NewLogStateTracker: %v", err) } for i := range test.cpRaws { _, _, newCP, err := lst.Update(ctx) if err != nil { t.Errorf("Update %d: %v", i, err) } if got, want := newCP, test.wantCpRaws[i]; !bytes.Equal(got, want) { t.Errorf("Update moved to:\n%s\nwant:\n%s", string(got), string(want)) } shim.Advance() } }) } } func TestNodeCacheHandlesInvalidRequest(t *testing.T) { ctx := context.Background() wantBytes := []byte("0123456789ABCDEF0123456789ABCDEF") f := func(_ context.Context, _, _ uint64, _ uint8) ([]byte, error) { h := &api.HashTile{ Nodes: [][]byte{wantBytes}, } return h.MarshalText() } // Large tree, but we're emulating skew since f, above, will return a tile which only knows about 1 // leaf. nc := newNodeCache(f, 10) if got, err := nc.GetNode(ctx, compact.NewNodeID(0, 0)); err != nil { t.Errorf("got %v, want no error", err) } else if !bytes.Equal(got, wantBytes) { t.Errorf("got %v, want %v", got, wantBytes) } if _, err := nc.GetNode(ctx, compact.NewNodeID(0, 1)); err == nil { t.Error("got no error, want error because ID is out of range") } } func TestHandleZeroRoot(t *testing.T) { zeroCP := testCheckpoints[0] if zeroCP.Size != 0 { t.Fatal("BadData: checkpoint has non-zero size") } if len(zeroCP.Hash) == 0 { t.Fatal("BadTestData: checkpoint.0 has empty root hash") } if _, err := NewProofBuilder(context.Background(), zeroCP.Size, testLogTileFetcher); err != nil { t.Fatalf("NewProofBuilder: %v", err) } } func TestGetEntryBundleAddressing(t *testing.T) { for _, test := range []struct { name string idx uint64 clientLogSize uint64 actualLogSize uint64 wantPartialTileSize uint8 }{ { name: "works - partial tile", idx: 0, clientLogSize: 34, actualLogSize: 34, wantPartialTileSize: 34, }, { name: "works - full tile", idx: 1, clientLogSize: layout.TileWidth*2 + 45, actualLogSize: layout.TileWidth*2 + 45, wantPartialTileSize: 0, }, } { t.Run(test.name, func(t *testing.T) { gotIdx := uint64(0) gotTileSize := uint8(0) f := func(_ context.Context, i uint64, sz uint8) ([]byte, error) { gotIdx = i gotTileSize = sz p := layout.PartialTileSize(0, i, test.actualLogSize) if p != sz { return nil, os.ErrNotExist } return []byte{}, nil } _, err := GetEntryBundle(context.Background(), f, test.idx, test.clientLogSize) if err != nil { t.Fatalf("GetEntryBundle: %v", err) } if gotIdx != test.idx { t.Errorf("f got idx %d, want %d", gotIdx, test.idx) } if gotTileSize != test.wantPartialTileSize { t.Errorf("f got tileSize %d, want %d", gotTileSize, test.wantPartialTileSize) } }) } } func TestNodeFetcherAddressing(t *testing.T) { for _, test := range []struct { name string nodeLevel uint nodeIdx uint64 clientLogSize uint64 actualLogSize uint64 wantPartialTileSize uint8 }{ { name: "works - partial tile", nodeIdx: 0, clientLogSize: 34, actualLogSize: 34, wantPartialTileSize: 34, }, { name: "works - full tile", nodeIdx: 56, clientLogSize: layout.TileWidth*2 + 45, actualLogSize: layout.TileWidth*2 + 45, wantPartialTileSize: 0, }, } { t.Run(test.name, func(t *testing.T) { gotLevel, gotIdx, gotTileSize := uint(0), uint64(0), uint8(0) f := func(_ context.Context, l, i uint64, sz uint8) ([]byte, error) { gotLevel = uint(l) gotIdx = i gotTileSize = sz p := layout.PartialTileSize(l, i, test.actualLogSize) if p != sz { return nil, os.ErrNotExist } r := api.HashTile{} s := int(sz) if s == 0 { s = layout.TileWidth } for x := range s { h := sha256.Sum256(fmt.Appendf(nil, "node at %d/%d", l, i+uint64(x))) r.Nodes = append(r.Nodes, h[:]) } return r.MarshalText() } pb, err := NewProofBuilder(t.Context(), test.clientLogSize, f) if err != nil { t.Fatalf("NewProofBuilder: %v", err) } _, err = pb.fetchNodes(t.Context(), proof.Nodes{IDs: []compact.NodeID{compact.NewNodeID(test.nodeLevel, test.nodeIdx)}}) if err != nil { t.Fatalf("fetchNodes: %v", err) } if wantLevel := test.nodeLevel >> layout.TileHeight; gotLevel != wantLevel { t.Errorf("f got level %d, want %d", gotLevel, wantLevel) } if wantIdx := test.nodeIdx >> layout.TileHeight; gotIdx != wantIdx { t.Errorf("f got idx %d, want %d", gotIdx, wantIdx) } if gotTileSize != test.wantPartialTileSize { t.Errorf("f got tileSize %d, want %d", gotTileSize, test.wantPartialTileSize) } }) } } transparency-dev-tessera-3cb22ee/client/fetcher.go000066400000000000000000000077621511600621500223700ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package client import ( "context" "fmt" "io" "net/http" "net/url" "os" "path" "strings" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/internal/fetcher" "k8s.io/klog/v2" ) // NewHTTPFetcher creates a new HTTPFetcher for the log rooted at the given URL, using // the provided HTTP client. // // rootURL should end in a trailing slash. // c may be nil, in which case http.DefaultClient will be used. func NewHTTPFetcher(rootURL *url.URL, c *http.Client) (*HTTPFetcher, error) { if !strings.HasSuffix(rootURL.String(), "/") { rootURL.Path += "/" } if c == nil { c = http.DefaultClient } return &HTTPFetcher{ c: c, rootURL: rootURL, }, nil } // HTTPFetcher knows how to fetch log artifacts from a log being served via HTTP. type HTTPFetcher struct { c *http.Client rootURL *url.URL authHeader string } // SetAuthorizationHeader sets the value to be used with an Authorization: header // for every request made by this fetcher. func (h *HTTPFetcher) SetAuthorizationHeader(v string) { h.authHeader = v } func (h HTTPFetcher) fetch(ctx context.Context, p string) ([]byte, error) { u, err := h.rootURL.Parse(p) if err != nil { return nil, fmt.Errorf("invalid URL: %v", err) } req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil) if err != nil { return nil, fmt.Errorf("NewRequestWithContext(%q): %v", u.String(), err) } if h.authHeader != "" { req.Header.Add("Authorization", h.authHeader) } r, err := h.c.Do(req) if err != nil { return nil, fmt.Errorf("get(%q): %v", u.String(), err) } switch r.StatusCode { case http.StatusOK: // All good, continue below case http.StatusNotFound: // Need to return ErrNotExist here, by contract. return nil, fmt.Errorf("get(%q): %w", u.String(), os.ErrNotExist) default: return nil, fmt.Errorf("get(%q): %v", u.String(), r.StatusCode) } defer func() { if err := r.Body.Close(); err != nil { klog.Errorf("resp.Body.Close(): %v", err) } }() return io.ReadAll(r.Body) } func (h HTTPFetcher) ReadCheckpoint(ctx context.Context) ([]byte, error) { return h.fetch(ctx, layout.CheckpointPath) } func (h HTTPFetcher) ReadTile(ctx context.Context, l, i uint64, p uint8) ([]byte, error) { return fetcher.PartialOrFullResource(ctx, p, func(ctx context.Context, p uint8) ([]byte, error) { return h.fetch(ctx, layout.TilePath(l, i, p)) }) } func (h HTTPFetcher) ReadEntryBundle(ctx context.Context, i uint64, p uint8) ([]byte, error) { return fetcher.PartialOrFullResource(ctx, p, func(ctx context.Context, p uint8) ([]byte, error) { return h.fetch(ctx, layout.EntriesPath(i, p)) }) } // FileFetcher knows how to fetch log artifacts from a filesystem rooted at Root. type FileFetcher struct { Root string } func (f FileFetcher) ReadCheckpoint(_ context.Context) ([]byte, error) { return os.ReadFile(path.Join(f.Root, layout.CheckpointPath)) } func (f FileFetcher) ReadTile(ctx context.Context, l, i uint64, p uint8) ([]byte, error) { return fetcher.PartialOrFullResource(ctx, p, func(ctx context.Context, p uint8) ([]byte, error) { return os.ReadFile(path.Join(f.Root, layout.TilePath(l, i, p))) }) } func (f FileFetcher) ReadEntryBundle(ctx context.Context, i uint64, p uint8) ([]byte, error) { return fetcher.PartialOrFullResource(ctx, p, func(ctx context.Context, p uint8) ([]byte, error) { return os.ReadFile(path.Join(f.Root, layout.EntriesPath(i, p))) }) } transparency-dev-tessera-3cb22ee/client/otel.go000066400000000000000000000020651511600621500217020ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package client import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) const name = "github.com/transparency-dev/tessera/client" var ( tracer = otel.Tracer(name) ) var ( firstKey = attribute.Key("first") NKey = attribute.Key("N") logSizeKey = attribute.Key("logSize") indexKey = attribute.Key("index") levelKey = attribute.Key("level") smallerKey = attribute.Key("smaller") largerKey = attribute.Key("larger") ) transparency-dev-tessera-3cb22ee/client/stream.go000066400000000000000000000140271511600621500222330ustar00rootroot00000000000000// Copyright 2025 The Tessera Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package stream provides support for streaming contiguous entries from logs. package client import ( "context" "fmt" "iter" "github.com/transparency-dev/tessera/api/layout" "k8s.io/klog/v2" ) // TreeSizeFunc is a function which knows how to return the current tree size of a log. type TreeSizeFunc func(ctx context.Context) (uint64, error) // Bundle represents an entry bundle in a log, along with some metadata about which parts of the bundle // are relevent. type Bundle struct { // RangeInfo decribes which of the entries in this bundle are relevent. RangeInfo layout.RangeInfo // Data is the raw serialised bundle, as fetched from the log. // // For a tlog-tiles compliant log, this can be unmarshaled using api.EntryBundle. Data []byte } // EntryBundles produces an iterator which returns a stream of Bundle structs which cover the requested range of entries in their natural order in the log. // // If the adaptor encounters an error while reading an entry bundle, the encountered error will be returned via the iterator. // // This adaptor is optimised for the case where calling getBundle has some appreciable latency, and works // around that by maintaining a read-ahead cache of subsequent bundles which is populated a number of parallel // requests to getBundle. The request parallelism is set by the value of the numWorkers paramemter, which can be tuned // to balance throughput against consumption of resources, but such balancing needs to be mindful of the nature of the // source infrastructure, and how concurrent requests affect performance (e.g. GCS buckets vs. files on a single disk). func EntryBundles(ctx context.Context, numWorkers uint, getSize TreeSizeFunc, getBundle EntryBundleFetcherFunc, fromEntry uint64, N uint64) iter.Seq2[Bundle, error] { ctx, span := tracer.Start(ctx, "tessera.storage.StreamAdaptor") defer span.End() // bundleOrErr represents a fetched entry bundle and its params, or an error if we couldn't fetch it for // some reason. type bundleOrErr struct { b Bundle err error } // bundles will be filled with futures for in-order entry bundles by the worker // go routines below. // This channel will be drained by the loop at the bottom of this func which // yields the bundles to the caller. bundles := make(chan func() bundleOrErr, numWorkers) exit := make(chan struct{}) // Fetch entry bundle resources in parallel. // We use a limited number of tokens here to prevent this from // consuming an unbounded amount of resources. go func() { ctx, span := tracer.Start(ctx, "tessera.storage.StreamAdaptorWorker") defer span.End() defer close(bundles) treeSize, err := getSize(ctx) if err != nil { bundles <- func() bundleOrErr { return bundleOrErr{err: err} } return } // We'll limit ourselves to numWorkers worth of on-going work using these tokens: tokens := make(chan struct{}, numWorkers) for range numWorkers { tokens <- struct{}{} } klog.V(1).Infof("stream.EntryBundles: streaming [%d, %d)", fromEntry, fromEntry+N) // For each bundle, pop a future into the bundles channel and kick off an async request // to resolve it. for ri := range layout.Range(fromEntry, fromEntry+N, treeSize) { select { case <-exit: return case <-tokens: // We'll return a token below, once the bundle is fetched _and_ is being yielded. } c := make(chan bundleOrErr, 1) go func(ri layout.RangeInfo) { b, err := getBundle(ctx, ri.Index, ri.Partial) c <- bundleOrErr{b: Bundle{RangeInfo: ri, Data: b}, err: err} }(ri) f := func() bundleOrErr { b := <-c // We're about to yield a value, so we can now return the token and unblock another fetch. tokens <- struct{}{} return b } bundles <- f } klog.V(1).Infof("stream.EntryBundles: exiting") }() return func(yield func(Bundle, error) bool) { defer close(exit) for f := range bundles { b := f() if !yield(b.b, b.err) { return } // For now, force the iterator to stop if we've just returned an error. // If there's a good reason to allow it to continue we can change this. if b.err != nil { return } } klog.V(1).Infof("stream.EntryBundles: iter done") } } // Entry represents a single leaf in a log. type Entry[T any] struct { // Index is the index of the entry in the log. Index uint64 // Entry is the entry from the log. Entry T } // Entries consumes an iterator of Bundle structs and transforms it using the provided unbundle function, and returns an iterator over the transformed data. // // Different unbundle implementations can be provided to return raw entry bytes, parsed entry structs, or derivations of entries (e.g. hashes) as needed. func Entries[T any](bundles iter.Seq2[Bundle, error], unbundle func([]byte) ([]T, error)) iter.Seq2[Entry[T], error] { return func(yield func(Entry[T], error) bool) { for b, err := range bundles { if err != nil { yield(Entry[T]{}, err) return } es, err := unbundle(b.Data) if err != nil { yield(Entry[T]{}, err) return } if len(es) <= int(b.RangeInfo.First) { yield(Entry[T]{}, fmt.Errorf("logic error: First is %d but only %d entries", b.RangeInfo.First, len(es))) return } es = es[b.RangeInfo.First:] if len(es) > int(b.RangeInfo.N) { es = es[:b.RangeInfo.N] } rIdx := b.RangeInfo.Index*layout.EntryBundleWidth + uint64(b.RangeInfo.First) for i, e := range es { if !yield(Entry[T]{Index: rIdx + uint64(i), Entry: e}, nil) { return } } } } } transparency-dev-tessera-3cb22ee/client/stream_test.go000066400000000000000000000112561511600621500232730ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package client_test import ( "context" "fmt" "sync/atomic" "testing" "time" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/testonly" ) func TestEntryBundles(t *testing.T) { ctx := t.Context() logSize1 := uint64(12345) logSize2 := uint64(100045) tl, done := testonly.NewTestLog(t, tessera.NewAppendOptions().WithBatching(30000, time.Second).WithCheckpointInterval(time.Second)) defer func() { if err := done(ctx); err != nil { t.Fatalf("done: %v", err) } }() if _, err := populateEntries(t, tl, logSize1, "first"); err != nil { t.Fatalf("populateEntries(first): %v", err) } if _, err := populateEntries(t, tl, logSize2-logSize1, "second"); err != nil { t.Fatalf("populateEntries(second): %v", err) } var logSize atomic.Uint64 logSize.Store(uint64(logSize1)) size := func(ctx context.Context) (uint64, error) { return logSize.Load(), nil } // Finally, try to stream all the bundles back. // We'll first try to stream up to logSize1, then when we reach it we'll // make the tree appear to grow to logSize2 to test resuming. seenEntries := uint64(0) for gotEntry, gotErr := range client.EntryBundles(ctx, 2, size, tl.LogReader.ReadEntryBundle, 0, uint64(logSize2)) { if gotErr != nil { t.Fatalf("gotErr after %d: %v", seenEntries, gotErr) } if e := gotEntry.RangeInfo.Index*layout.EntryBundleWidth + uint64(gotEntry.RangeInfo.First); e != seenEntries { t.Fatalf("got idx %d, want %d", e, seenEntries) } seenEntries += uint64(gotEntry.RangeInfo.N) t.Logf("got RI %d / %d", gotEntry.RangeInfo.Index, seenEntries) switch seenEntries { case uint64(logSize1): // We've fetched all the entries from the original tree size, now we'll make // the tree appear to have grown to the final size. // The stream should start returning bundles again until we've consumed them all. t.Log("Reached logSize, growing tree") logSize.Store(uint64(logSize2)) time.Sleep(time.Second) } } } func TestEntries(t *testing.T) { ctx := t.Context() logSize := uint64(1234) tl, done := testonly.NewTestLog(t, tessera.NewAppendOptions().WithBatching(uint(logSize), time.Second).WithCheckpointInterval(time.Second)) defer func() { if err := done(ctx); err != nil { t.Fatalf("done: %v", err) } }() // Put some entries into a log. es, err := populateEntries(t, tl, logSize, "first") if err != nil { t.Fatalf("populateEntries(): %v", err) } wantEntries := make(map[string]struct{}) for _, e := range es { wantEntries[string(e)] = struct{}{} } unbundle := func(bundle []byte) ([][]byte, error) { eb := &api.EntryBundle{} if err := eb.UnmarshalText(bundle); err != nil { return nil, err } return eb.Entries, nil } // Now stream back entries and check that we saw all the entries we added above. eCh := make(chan []byte) size := func(ctx context.Context) (uint64, error) { return logSize, nil } go func() { defer close(eCh) for gotEntry, gotErr := range client.Entries(client.EntryBundles(ctx, 2, size, tl.LogReader.ReadEntryBundle, 0, logSize), unbundle) { if gotErr != nil { t.Errorf("gotErr: %v", gotErr) } eCh <- gotEntry.Entry } }() for e := range eCh { k := string(e) if _, ok := wantEntries[k]; !ok { t.Errorf("Expected missing entry %q - already seen?", k) } delete(wantEntries, k) } if l := len(wantEntries); l > 0 { t.Fatalf("Did not see %d expected entries", l) } } func populateEntries(t *testing.T, tl *testonly.TestLog, N uint64, ep string) ([][]byte, error) { t.Helper() es := make([][]byte, 0, N) fs := make([]tessera.IndexFuture, 0, N) for i := range N { e := fmt.Appendf(nil, "%s-%d", ep, i) es = append(es, e) fs = append(fs, tl.Appender.Add(t.Context(), tessera.NewEntry(e))) } t.Logf("Added %d entries", N) a := tessera.NewPublicationAwaiter(t.Context(), tl.LogReader.ReadCheckpoint, time.Second) for _, f := range fs { if _, _, err := a.Await(t.Context(), f); err != nil { return nil, err } } return es, nil } transparency-dev-tessera-3cb22ee/cmd/000077500000000000000000000000001511600621500176725ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/conformance/000077500000000000000000000000001511600621500221645ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/conformance/README.md000066400000000000000000000054001511600621500234420ustar00rootroot00000000000000# Conformance Personalities This directory contains personalities that serve a dual purpose: - To provide a simple example of a personality that can be deployed with each backend - To function as conformance and performance harnesses for Tessera (using the [hammer](../../internal/hammer/)) Each subdirectory contains an implementation of the same personality built on top of Tessera. Implementations are provided that use: - [A local POSIX-compliant filesystem](./posix/) - [MySQL](./mysql/) - [GCP](./gcp/) - [AWS](./aws/) Each of these personalities exposes an endpoint that accepts `POST` requests at a `/add` URL. The contents of any request body will be appended to the log, and the decimal index assigned to this newly _sequenced_ entry will be returned. ## Codelab This codelab will help you add a few entries to a log, and inspect its contents. First, you need to bring up personality (a server built with Tessera which manages the log) on the infrastructure of your choice: - [A local POSIX-compliant filesystem](./posix#bring-up-a-log) - [MySQL](./mysql#bring-up-a-log) - [GCP](/deployment/live/gcp/conformance#manual-deployment) - [AWS](/deployment/live/aws/codelab#aws-codelab-deployment) Choose one of the implementations above and deploy it. In the shell you are going to run this codelab in, define the following environment variables (check the logging output from the implementation you deployed, as these may have been output): - The write URL: `${WRITE_URL}` - The read URL: `${READ_URL}` - The log public key: `${LOG_PUBLIC_KEY}` The commands below add entries to the log, and then show a few approaches to inspect the contents of the log. ```shell # Add 3 entries in parallel, and wait for all requests to complete curl -d 'one!' -H "Content-Type: application/data" -X POST ${WRITE_URL}add & curl -d 'two!' -H "Content-Type: application/data" -X POST ${WRITE_URL}add & curl -d 'three!' -H "Content-Type: application/data" -X POST ${WRITE_URL}add & wait # Check that the checkpoint is of the correct size (i.e. 3). # If the checkpoint size is zero, this is expected. It may take a second to integrate the entries and publish the checkpoint. curl -s ${READ_URL}checkpoint # Look at the leaves after confirming the checkpoint size. Piping into xxd to reveal the leaf sizes. curl -s ${READ_URL}tile/entries/000.p/3 | xxd ``` The tiles format is plain-text, but it's better to inspect the log via tooling made for this purpose: ```shell go run github.com/mhutchinson/woodpecker@main \ --custom_log_type=tiles \ --custom_log_url=${READ_URL} \ --custom_log_vkey=${LOG_PUBLIC_KEY} ``` Use arrow keys left and right to go backwards and forwards through the entries in the log. Use `q` to quit. Here's a demo of the codelab being followed: ![Codelab demo](./demo.gif) transparency-dev-tessera-3cb22ee/cmd/conformance/aws/000077500000000000000000000000001511600621500227565ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/conformance/aws/Dockerfile000066400000000000000000000012511511600621500247470ustar00rootroot00000000000000FROM golang:1.24.1-alpine3.21@sha256:43c094ad24b6ac0546c62193baeb3e6e49ce14d3250845d166c77c25f64b0386 AS builder ARG GOFLAGS="-trimpath -buildvcs=false -buildmode=exe" ENV GOFLAGS=$GOFLAGS # Move to working directory /build WORKDIR /build # Copy and download dependency using go mod COPY go.mod . COPY go.sum . RUN go mod download # Copy the code into the container COPY . . # Build the application RUN go build -o bin/conformance-aws ./cmd/conformance/aws # Build release image FROM alpine:3.20.2@sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5 COPY --from=builder /build/bin/conformance-aws /bin/conformance-aws ENTRYPOINT ["/bin/conformance-aws"] transparency-dev-tessera-3cb22ee/cmd/conformance/aws/README.md000066400000000000000000000004441511600621500242370ustar00rootroot00000000000000# Conformance testing binary for AWS This binary is primarily intended to be used for checking Tessera conformance on AWS. If you want to try running it yourself, please see the instructions in the [README file in the /deployment/live/aws/codelab directory](/deployment/live/aws/codelab). transparency-dev-tessera-3cb22ee/cmd/conformance/aws/main.go000066400000000000000000000175461511600621500242460ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // aws is a simple personality allowing to run conformance/compliance/performance tests and showing how to use the Tessera AWS storage implmentation. package main import ( "context" "errors" "flag" "fmt" "io" "net/http" "time" aaws "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/go-sql-driver/mysql" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/storage/aws" aws_as "github.com/transparency-dev/tessera/storage/aws/antispam" "golang.org/x/mod/sumdb/note" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "k8s.io/klog/v2" ) var ( bucket = flag.String("bucket", "", "Bucket to use for storing log") dbName = flag.String("db_name", "", "AuroraDB name for the log DB") dbHost = flag.String("db_host", "", "AuroraDB host") dbPort = flag.Int("db_port", 3306, "AuroraDB port") dbUser = flag.String("db_user", "", "AuroraDB user") dbPassword = flag.String("db_password", "", "AuroraDB user") dbMaxConns = flag.Int("db_max_conns", 0, "Maximum connections to the database, defaults to 0, i.e unlimited") dbMaxIdle = flag.Int("db_max_idle_conns", 2, "Maximum idle database connections in the connection pool, defaults to 2") s3Endpoint = flag.String("s3_endpoint", "", "Endpoint for custom non-AWS S3 service") s3AccessKeyID = flag.String("s3_access_key", "", "Access key ID for custom non-AWS S3 service") s3SecretAccessKey = flag.String("s3_secret", "", "Secret access key for custom non-AWS S3 service") listen = flag.String("listen", ":2024", "Address:port to listen on") signer = flag.String("signer", "", "Note signer to use to sign checkpoints") publishInterval = flag.Duration("publish_interval", 3*time.Second, "How frequently to publish updated checkpoints") traceFraction = flag.Float64("trace_fraction", 0, "Fraction of open-telemetry span traces to sample") additionalSigners = []string{} antispamEnable = flag.Bool("antispam", false, "EXPERIMENTAL: Set to true to enable persistent antispam storage") antispamDb = flag.String("antispam_db_name", "", "AuroraDB name for the antispam DB") ) func init() { flag.Func("additional_signer", "Additional note signer for checkpoints, may be specified multiple times", func(s string) error { additionalSigners = append(additionalSigners, s) return nil }) } func main() { klog.InitFlags(nil) flag.Parse() ctx := context.Background() shutdownOTel := initOTel(ctx, *traceFraction) defer shutdownOTel(ctx) s, a := signerFromFlags() // Create our Tessera storage backend: awsCfg := storageConfigFromFlags() driver, err := aws.New(ctx, awsCfg) if err != nil { klog.Exitf("Failed to create new AWS storage: %v", err) } var antispam tessera.Antispam // Persistent antispam is currently experimental, so there's no documentation yet! if *antispamEnable { asOpts := aws_as.AntispamOpts{} // Use defaults antispam, err = aws_as.NewAntispam(ctx, antispamMysqlConfig().FormatDSN(), asOpts) if err != nil { klog.Exitf("Failed to create new AWS antispam storage: %v", err) } } appender, shutdown, _, err := tessera.NewAppender(ctx, driver, tessera.NewAppendOptions(). WithCheckpointSigner(s, a...). WithCheckpointInterval(*publishInterval). WithBatching(512, 300*time.Millisecond). WithPushback(10*4096). WithAntispam(tessera.DefaultAntispamInMemorySize, antispam)) if err != nil { klog.Exit(err) } // Expose a HTTP handler for the conformance test writes. // This should accept arbitrary bytes POSTed to /add, and return an ascii // decimal representation of the index assigned to the entry. http.HandleFunc("POST /add", func(w http.ResponseWriter, r *http.Request) { b, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } idx, err := appender.Add(r.Context(), tessera.NewEntry(b))() if err != nil { if errors.Is(err, tessera.ErrPushback) { w.Header().Add("Retry-After", "1") w.WriteHeader(http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(err.Error())) return } // Write out the assigned index _, _ = fmt.Fprintf(w, "%d", idx.Index) }) h2s := &http2.Server{} h1s := &http.Server{ Addr: *listen, Handler: h2c.NewHandler(http.DefaultServeMux, h2s), ReadHeaderTimeout: 5 * time.Second, } if err := http2.ConfigureServer(h1s, h2s); err != nil { klog.Exitf("http2.ConfigureServer: %v", err) } if err := h1s.ListenAndServe(); err != nil { if err := shutdown(ctx); err != nil { klog.Exit(err) } klog.Exitf("ListenAndServe: %v", err) } } // storageConfigFromFlags returns an aws.Config struct populated with values // provided via flags. func storageConfigFromFlags() aws.Config { if *bucket == "" { klog.Exit("--bucket must be set") } if *dbName == "" { klog.Exit("--db_name must be set") } if *dbHost == "" { klog.Exit("--db_host must be set") } if *dbPort == 0 { klog.Exit("--db_port must be set") } if *dbUser == "" { klog.Exit("--db_user must be set") } // Empty passord isn't an option with AuroraDB MySQL. if *dbPassword == "" { klog.Exit("--db_password must be set") } c := mysql.Config{ User: *dbUser, Passwd: *dbPassword, Net: "tcp", Addr: fmt.Sprintf("%s:%d", *dbHost, *dbPort), DBName: *dbName, AllowCleartextPasswords: true, AllowNativePasswords: true, } // Configure to use MinIO Server var awsConfig *aaws.Config var s3Opts func(o *s3.Options) if *s3Endpoint != "" { const defaultRegion = "us-east-1" s3Opts = func(o *s3.Options) { o.BaseEndpoint = aaws.String(*s3Endpoint) o.Credentials = credentials.NewStaticCredentialsProvider(*s3AccessKeyID, *s3SecretAccessKey, "") o.Region = defaultRegion o.UsePathStyle = true } awsConfig = &aaws.Config{ Region: defaultRegion, } } return aws.Config{ Bucket: *bucket, SDKConfig: awsConfig, S3Options: s3Opts, DSN: c.FormatDSN(), MaxOpenConns: *dbMaxConns, MaxIdleConns: *dbMaxIdle, } } func antispamMysqlConfig() *mysql.Config { if *antispamDb == "" { klog.Exit("--antispam_db_name must be set") } if *dbHost == "" { klog.Exit("--db_host must be set") } if *dbPort == 0 { klog.Exit("--db_port must be set") } if *dbUser == "" { klog.Exit("--db_user must be set") } // Empty passord isn't an option with AuroraDB MySQL. if *dbPassword == "" { klog.Exit("--db_password must be set") } return &mysql.Config{ User: *dbUser, Passwd: *dbPassword, Net: "tcp", Addr: fmt.Sprintf("%s:%d", *dbHost, *dbPort), DBName: *antispamDb, AllowCleartextPasswords: true, AllowNativePasswords: true, } } func signerFromFlags() (note.Signer, []note.Signer) { s, err := note.NewSigner(*signer) if err != nil { klog.Exitf("Failed to create new signer: %v", err) } var a []note.Signer for _, as := range additionalSigners { s, err := note.NewSigner(as) if err != nil { klog.Exitf("Failed to create additional signer: %v", err) } a = append(a, s) } return s, a } transparency-dev-tessera-3cb22ee/cmd/conformance/aws/otel.go000066400000000000000000000076261511600621500242630ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "errors" ec2 "go.opentelemetry.io/contrib/detectors/aws/ec2/v2" "go.opentelemetry.io/contrib/detectors/aws/ecs" "go.opentelemetry.io/contrib/propagators/aws/xray" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc" "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" "k8s.io/klog/v2" ) // initOTel initialises the open telemetry support for metrics and tracing. // // Tracing is enabled with statistical sampling, with the probability passed in. // Returns a shutdown function which should be called just before exiting the process. // // AWS requires that the ADOT collector is running on the local machine, listening on port 4317. // See https://aws-otel.github.io/docs/getting-started/collector func initOTel(ctx context.Context, traceFraction float64) func(context.Context) { var shutdownFuncs []func(context.Context) error // shutdown combines shutdown functions from multiple OpenTelemetry // components into a single function. shutdown := func(ctx context.Context) { var err error for _, fn := range shutdownFuncs { err = errors.Join(err, fn(ctx)) } shutdownFuncs = nil if err != nil { klog.Errorf("OTel shutdown: %v", err) } } // Instantiate new AWS resource detectors ec2ResourceDetector := ec2.NewResourceDetector() ecsResourceDetector := ecs.NewResourceDetector() resources, err := resource.New(ctx, resource.WithTelemetrySDK(), resource.WithFromEnv(), // unpacks OTEL_RESOURCE_ATTRIBUTES // Add your own custom attributes to identify your application resource.WithAttributes( semconv.ServiceNameKey.String("conformance"), semconv.ServiceNamespaceKey.String("tessera"), ), resource.WithDetectors(ec2ResourceDetector, ecsResourceDetector), ) if err != nil { klog.Exitf("Failed to detect resources: %v", err) } // Code below is mostly taken from the OTEL AWS documentation: https://aws-otel.github.io/docs/getting-started/go-sdk/manual-instr // Create and start new OTLP metric exporter metricExporter, err := otlpmetricgrpc.New(ctx, otlpmetricgrpc.WithInsecure(), otlpmetricgrpc.WithEndpoint("localhost:4317")) if err != nil { klog.Exitf("Failed to create new OTLP metric exporter: %v", err) } mp := metric.NewMeterProvider( metric.WithReader(metric.NewPeriodicReader(metricExporter)), metric.WithResource(resources), ) shutdownFuncs = append(shutdownFuncs, mp.Shutdown) otel.SetMeterProvider(mp) // Create and start new OTLP trace exporter traceExporter, err := otlptracegrpc.New(ctx, otlptracegrpc.WithInsecure(), otlptracegrpc.WithEndpoint("localhost:4317")) if err != nil { klog.Exitf("Failed to create new OTLP trace exporter: %v", err) } idg := xray.NewIDGenerator() // attach traceIDRatioBasedSampler to tracer provider // Associate resource with TracerProvider tp := trace.NewTracerProvider( trace.WithBatcher(traceExporter), trace.WithIDGenerator(idg), trace.WithResource(resources), trace.WithSampler(trace.TraceIDRatioBased(traceFraction)), ) shutdownFuncs = append(shutdownFuncs, tp.Shutdown) otel.SetTracerProvider(tp) otel.SetTextMapPropagator(xray.Propagator{}) return shutdown } transparency-dev-tessera-3cb22ee/cmd/conformance/demo.cast000066400000000000000000003661211511600621500237750ustar00rootroot00000000000000{"version": 2, "width": 113, "height": 34, "timestamp": 1727258639, "env": {"SHELL": "/bin/zsh", "TERM": "screen-256color"}} [0.030351, "o", "\u001b[0m\u001b[49m\u001b[39m\u001b[23m\u001b[24m\r\u001b[K\r\n\r\n\u001b[2A\u001b7\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/git/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/cmd/conformance/mysql/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[49m\u001b[34m\u001b[0m\u001b[34m\u001b[49m\u001b[39m\u001b[38;5;244m─────────────\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[40m\u001b[33m mhutchinson@thinkcentre\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m \u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[37m\u001b[0m\u001b[37m\u001b[40m\u001b[47m\u001b[30m 10:03:59\u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[?2004h"] [0.177885, "o", "\u001b[?25l"] [0.178087, "o", "\u001b8\u001b[0m\u001b[49m\u001b[39m\u001b[23m\u001b[24m\u001b[J"] [0.179528, "o", "\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007\u001b]1337;ShellIntegrationVersion=14;shell=zsh\u0007\u001bk../mysql/docker\u001b\\\u001b]133;D;0\u0007\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007"] [0.17966, "o", "\u001b[0m\u001b[38;5;254m\u001b[49m\u001b[39m\u001b[23m\u001b[24m\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r\u001b[0m\u001b[49m\u001b[39m\u001b[23m\u001b[24m\u001b[K"] [0.184357, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\r\n\u001bM\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mcm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mc\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/mysql/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[43m\u001b[34m\u001b[0m\u001b[34m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m codelab-demo \u001b[30m!3\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m\u001b[38;5;244m───\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[40m\u001b[33m mhutchinson@thinkcentre\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m \u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[37m\u001b[0m\u001b[37m\u001b[40m\u001b[47m\u001b[30m 10:03:59\u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47"] [0.184388, "o", "m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[K"] [0.184493, "o", "\u001b[?1h\u001b="] [0.185412, "o", "\u001b[?25h"] [0.185595, "o", "\u001b[?2004h"] [0.630991, "o", "\u001b[32md\u001b[39m"] [0.633328, "o", "\b\u001b[32md\u001b[39m\u001b[90mocker logs tessera-conformance-mysql\u001b[39m\r\r\n\u001b[K\u001bM\u001b[3C"] [0.739131, "o", "\b\u001b[33md\u001b[33mo\u001b[39m\u001b[1B\r\u001b[K\u001bM\u001b[4C"] [0.953913, "o", "\b\b\u001b[1m\u001b[31md\u001b[1m\u001b[31mo\u001b[1m\u001b[31mc\u001b[0m\u001b[39m"] [0.975142, "o", "\b\u001b[1m\u001b[31mc\u001b[1m\u001b[31mk\u001b[0m\u001b[39m"] [1.121351, "o", "\b\u001b[1m\u001b[31mk\u001b[1m\u001b[31me\u001b[0m\u001b[39m"] [1.162641, "o", "\b\b\b\b\b\u001b[0m\u001b[32md\u001b[0m\u001b[32mo\u001b[0m\u001b[32mc\u001b[0m\u001b[32mk\u001b[0m\u001b[32me\u001b[32mr\u001b[39m"] [1.340668, "o", "\u001b[39m "] [1.497056, "o", "\u001b[39m\u001b[4mc\u001b[24m\u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[39m \u001b[1B\r\u001b[K\u001bM\u001b[10C"] [1.499409, "o", "\u001b[90mompose up -d\u001b[39m\u001b[12D"] [1.516761, "o", "\b\u001b[4mc\u001b[39m\u001b[4mo\u001b[24m"] [1.678978, "o", "\b\u001b[4mo\u001b[39m\u001b[4mm\u001b[24m"] [1.771691, "o", "\b\u001b[4mm\u001b[39m\u001b[4mp\u001b[24m"] [1.933634, "o", "\b\u001b[4mp\u001b[39m\u001b[4mo\u001b[24m"] [2.091699, "o", "\b\u001b[4mo\u001b[39m\u001b[4ms\u001b[24m"] [2.133957, "o", "\b\u001b[4ms\u001b[39m\u001b[4me\u001b[24m"] [2.359232, "o", "\b\b\b\b\b\b\b\u001b[24mc\u001b[24mo\u001b[24mm\u001b[24mp\u001b[24mo\u001b[24ms\u001b[24me\u001b[39m "] [2.425181, "o", "\u001b[39mu"] [2.489702, "o", "\u001b[39mp"] [2.809283, "o", "\u001b[39m "] [2.831604, "o", "\u001b[39m-"] [2.980694, "o", "\u001b[39md"] [3.460627, "o", "\u001b[?1l\u001b>"] [3.463919, "o", "\u001b[?25l\u001b[?2004l\r\r\u001bM\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[0m\u001b[49m\u001b[23m\u001b[24m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[39m\u001b[23m\u001b[24m \u001b]133;B\u0007\u001b[32mdocker\u001b[39m compose up -d\u001b[K\u001b[?25h"] [3.464274, "o", "\r\r\n"] [3.464714, "o", "\u001bkdocker\u001b\\"] [3.464877, "o", "\u001b]133;C;\u0007"] [3.614592, "o", "\u001b[1A\u001b[1B\u001b[0G\u001b[?25l[+] Running 0/0\r\n"] [3.614686, "o", " \u001b[33m⠋\u001b[0m Network docker_default Creating \u001b[34m0.1s \u001b[0m\r\n\u001b[?25h"] [3.715086, "o", "\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/1\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠋\u001b[0m Container tessera-mysql-db Starting \u001b[34m0.1s \u001b[0m\r\n"] [3.715192, "o", " \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [3.814295, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠙\u001b[0m Container tessera-mysql-db Starting \u001b[34m0.2s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [3.914893, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠹\u001b[0m Container tessera-mysql-db Waiting \u001b[34m0.3s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [4.015037, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠸\u001b[0m Container tessera-mysql-db Waiting \u001b[34m0.4s \u001b[0m\r\n"] [4.015137, "o", " \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [4.114207, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠼\u001b[0m Container tessera-mysql-db Waiting \u001b[34m0.5s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [4.214456, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠴\u001b[0m Container tessera-mysql-db Waiting \u001b[34m0.6s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [4.314733, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠦\u001b[0m Container tessera-mysql-db Waiting \u001b[34m0.7s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [4.41456, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠧\u001b[0m Container tessera-mysql-db Waiting \u001b[34m0.8s \u001b[0m\r\n"] [4.414676, "o", " \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [4.514765, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠇\u001b[0m Container tessera-mysql-db Waiting \u001b[34m0.9s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [4.614992, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n"] [4.615118, "o", " \u001b[33m⠏\u001b[0m Container tessera-mysql-db Waiting \u001b[34m1.0s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [4.715169, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠋\u001b[0m Container tessera-mysql-db Waiting \u001b[34m1.1s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [4.814337, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠙\u001b[0m Container tessera-mysql-db Waiting \u001b[34m1.2s \u001b[0m\r\n"] [4.814453, "o", " \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [4.915057, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n"] [4.915093, "o", " \u001b[33m⠹\u001b[0m Container tessera-mysql-db Waiting \u001b[34m1.3s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [5.014154, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠸\u001b[0m Container tessera-mysql-db Waiting \u001b[34m1.4s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [5.1144, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠼\u001b[0m Container tessera-mysql-db Waiting \u001b[34m1.5s \u001b[0m\r\n"] [5.114503, "o", " \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [5.214638, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠴\u001b[0m Container tessera-mysql-db Waiting \u001b[34m1.6s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [5.314869, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠦\u001b[0m Container tessera-mysql-db Waiting \u001b[34m1.7s \u001b[0m\r\n"] [5.314948, "o", " \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [5.414779, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠧\u001b[0m Container tessera-mysql-db Waiting \u001b[34m1.8s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [5.515021, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠇\u001b[0m Container tessera-mysql-db Waiting \u001b[34m1.9s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [5.614192, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠏\u001b[0m Container tessera-mysql-db Waiting \u001b[34m2.0s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [5.714425, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠋\u001b[0m Container tessera-mysql-db Waiting \u001b[34m2.1s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [5.814367, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠙\u001b[0m Container tessera-mysql-db Waiting \u001b[34m10.2s \u001b[0m\r\n"] [5.814411, "o", " \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [5.915152, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠹\u001b[0m Container tessera-mysql-db Waiting \u001b[34m10.3s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [6.0144, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠸\u001b[0m Container tessera-mysql-db Waiting \u001b[34m10.4s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [6.114652, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠼\u001b[0m Container tessera-mysql-db Waiting \u001b[34m10.5s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [6.214941, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠴\u001b[0m Container tessera-mysql-db Waiting \u001b[34m10.6s \u001b[0m\r\n"] [6.214993, "o", " \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [6.315168, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[33m⠦\u001b[0m Container tessera-mysql-db Waiting \u001b[34m10.7s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mCreated\u001b[0m \u001b[34m0.0s \u001b[0m\r\n\u001b[?25h"] [6.414459, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-mysql-db \u001b[32mHealthy\u001b[0m \u001b[34m10.7s \u001b[0m\r\n \u001b[33m⠙\u001b[0m Container tessera-conformance-mysql Starting \u001b[34m10.8s \u001b[0m\r\n\u001b[?25h"] [6.514661, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l[+] Running 2/3\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-mysql-db \u001b[32mHealthy\u001b[0m \u001b[34m10.7s \u001b[0m\r\n \u001b[33m⠹\u001b[0m Container tessera-conformance-mysql Starting \u001b[34m10.9s \u001b[0m\r\n\u001b[?25h"] [6.539007, "o", "\u001b[1A\u001b[1A\u001b[1A\u001b[1A\u001b[0G\u001b[?25l\u001b[34m[+] Running 3/3\u001b[0m\r\n \u001b[32m✔\u001b[0m Network docker_default \u001b[32mCreated\u001b[0m \u001b[34m0.1s \u001b[0m\r\n \u001b[32m✔\u001b[0m Container tessera-mysql-db \u001b[32mHealthy\u001b[0m \u001b[34m10.7s \u001b[0m\r\n"] [6.539201, "o", " \u001b[32m✔\u001b[0m Container tessera-conformance-mysql \u001b[32mStarted\u001b[0m \u001b[34m10.9s \u001b[0m\r\n\u001b[?25h"] [6.542471, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"] [6.542665, "o", "\u001bk../mysql/docker\u001b\\"] [6.560902, "o", "\u001b]133;D;0\u0007\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007"] [6.570385, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\r\n\r\n\u001bM\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mcm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mc\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[43m\u001b[34m\u001b[0m\u001b[34m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m codelab-demo \u001b[30m!3\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m\u001b[38;5;244m\r\n\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[K"] [6.570488, "o", "\u001b[?1h\u001b="] [6.571588, "o", "\u001b[?2004h"] [7.098436, "o", "d"] [7.099245, "o", "\bdo"] [7.100096, "o", "c"] [7.100919, "o", "k"] [7.101619, "o", "e"] [7.102367, "o", "r"] [7.103204, "o", " l"] [7.104006, "o", "o"] [7.104779, "o", "g"] [7.105497, "o", "s"] [7.106273, "o", " t"] [7.107081, "o", "e"] [7.107933, "o", "s"] [7.108672, "o", "s"] [7.10941, "o", "e"] [7.11018, "o", "r"] [7.110969, "o", "a"] [7.111722, "o", "-"] [7.112413, "o", "c"] [7.113103, "o", "o"] [7.113791, "o", "n"] [7.114484, "o", "f"] [7.115247, "o", "o"] [7.116075, "o", "r"] [7.11679, "o", "m"] [7.117541, "o", "a"] [7.118292, "o", "n"] [7.119052, "o", "c"] [7.119725, "o", "e"] [7.120412, "o", "-"] [7.121133, "o", "m"] [7.12184, "o", "y"] [7.122564, "o", "s"] [7.123317, "o", "q"] [7.124006, "o", "l"] [7.124192, "o", "\u001b[37D\u001b[3md\u001b[3mo\u001b[3mc\u001b[3mk\u001b[3me\u001b[3mr\u001b[3m \u001b[3ml\u001b[3mo\u001b[3mg\u001b[3ms\u001b[3m \u001b[3mt\u001b[3me\u001b[3ms\u001b[3ms\u001b[3me\u001b[3mr\u001b[3ma\u001b[3m-\u001b[3mc\u001b[3mo\u001b[3mn\u001b[3mf\u001b[3mo\u001b[3mr\u001b[3mm\u001b[3ma\u001b[3mn\u001b[3mc\u001b[3me\u001b[3m-\u001b[3mm\u001b[3my\u001b[3ms\u001b[3mq\u001b[3ml\u001b[23m\r\r\n\u001b[K"] [7.92729, "o", "\u001bM\u001b[2C\u001b[23md\u001b[23mo\u001b[23mc\u001b[23mk\u001b[23me\u001b[23mr\u001b[23m \u001b[23ml\u001b[23mo\u001b[23mg\u001b[23ms\u001b[23m \u001b[23mt\u001b[23me\u001b[23ms\u001b[23ms\u001b[23me\u001b[23mr\u001b[23ma\u001b[23m-\u001b[23mc\u001b[23mo\u001b[23mn\u001b[23mf\u001b[23mo\u001b[23mr\u001b[23mm\u001b[23ma\u001b[23mn\u001b[23mc\u001b[23me\u001b[23m-\u001b[23mm\u001b[23my\u001b[23ms\u001b[23mq\u001b[23ml\u001b[1B\r\u001b[K"] [7.930473, "o", "\u001bM\u001b[2C\u001b[32md\u001b[32mo\u001b[32mc\u001b[32mk\u001b[32me\u001b[32mr\u001b[39m\u001b[1B\r"] [7.931681, "o", "\u001b[?1l\u001b>"] [7.934839, "o", "\u001b[?25l"] [7.934952, "o", "\u001b[?2004l\u001bM\r\u001bM\u001bM\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[0m\u001b[49m\u001b[23m\u001b[24m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[39m\u001b[23m\u001b[24m \u001b]133;B\u0007\u001b[32mdocker\u001b[39m logs tessera-conformance-mysql\u001b[K\r\r\n\u001b[K\u001b[?25h"] [7.935333, "o", "\u001b[K\r\r\n"] [7.935811, "o", "\u001bkdocker\u001b\\"] [7.935981, "o", "\u001b]133;C;\u0007"] [7.947078, "o", "I0925 10:04:13.992164 1 main.go:213] Initializing database schema\r\nI0925 10:04:14.015320 1 main.go:233] Database schema initialized\r\nI0925 10:04:14.022598 1 mysql.go:82] Initializing checkpoint\r\nI0925 10:04:14.024053 1 main.go:82] Environment variables useful for accessing this log:\r\nexport WRITE_URL=http://localhost:2024/ \r\nexport READ_URL=http://localhost:2024/ \r\nexport LOG_PUBLIC_KEY=transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW\r\n"] [7.948494, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"] [7.94874, "o", "\u001bk../mysql/docker\u001b\\"] [7.965147, "o", "\u001b]133;D;0\u0007\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007"] [7.973821, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\r\n\r\n\u001bM\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mcm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mc\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/mysql/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[43m\u001b[34m\u001b[0m\u001b[34m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m codelab-demo \u001b[30m!3\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m\u001b[38;5;244m───\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[40m\u001b[33m mhutchinson@thinkcentre\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m \u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[37m\u001b[0m\u001b[37m\u001b[40m\u001b[47m\u001b[30m 10:04:15\u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b["] [7.973846, "o", "47m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[K"] [7.973944, "o", "\u001b[?1h\u001b="] [7.975077, "o", "\u001b[?2004h"] [8.879161, "o", "e"] [8.879884, "o", "\bex"] [8.880689, "o", "p"] [8.881407, "o", "o"] [8.88214, "o", "r"] [8.882865, "o", "t"] [8.883677, "o", " W"] [8.884397, "o", "R"] [8.885112, "o", "I"] [8.885787, "o", "T"] [8.886472, "o", "E"] [8.887207, "o", "_"] [8.887888, "o", "U"] [8.888576, "o", "R"] [8.889264, "o", "L"] [8.890247, "o", "="] [8.890932, "o", "h"] [8.891624, "o", "t"] [8.892317, "o", "t"] [8.893011, "o", "p"] [8.893704, "o", ":"] [8.894379, "o", "/"] [8.895073, "o", "/"] [8.895771, "o", "l"] [8.89646, "o", "o"] [8.897153, "o", "c"] [8.897846, "o", "a"] [8.898523, "o", "l"] [8.899223, "o", "h"] [8.899915, "o", "o"] [8.900607, "o", "s"] [8.9013, "o", "t"] [8.90199, "o", ":"] [8.902664, "o", "2"] [8.903378, "o", "0"] [8.904067, "o", "2"] [8.904759, "o", "4"] [8.905462, "o", "/"] [8.906184, "o", "\r\r\ne\u001b[K"] [8.906873, "o", "\rex"] [8.90758, "o", "p"] [8.908271, "o", "o"] [8.908961, "o", "r"] [8.909653, "o", "t"] [8.910359, "o", " R"] [8.911097, "o", "E"] [8.911793, "o", "A"] [8.912486, "o", "D"] [8.913184, "o", "_"] [8.913899, "o", "U"] [8.914592, "o", "R"] [8.915322, "o", "L"] [8.91629, "o", "="] [8.916984, "o", "h"] [8.917679, "o", "t"] [8.91837, "o", "t"] [8.919108, "o", "p"] [8.919795, "o", ":"] [8.920487, "o", "/"] [8.921178, "o", "/"] [8.921872, "o", "l"] [8.922585, "o", "o"] [8.923284, "o", "c"] [8.923975, "o", "a"] [8.924662, "o", "l"] [8.92535, "o", "h"] [8.92604, "o", "o"] [8.926729, "o", "s"] [8.927439, "o", "t"] [8.928136, "o", ":"] [8.928828, "o", "2"] [8.929517, "o", "0"] [8.930207, "o", "2"] [8.930899, "o", "4"] [8.931603, "o", "/"] [8.932317, "o", "\r\r\ne\u001b[K"] [8.933026, "o", "\rex"] [8.933703, "o", "p"] [8.934402, "o", "o"] [8.935124, "o", "r"] [8.935821, "o", "t"] [8.936531, "o", " L"] [8.937224, "o", "O"] [8.937917, "o", "G"] [8.938608, "o", "_"] [8.93932, "o", "P"] [8.940015, "o", "U"] [8.940711, "o", "B"] [8.941405, "o", "L"] [8.9421, "o", "I"] [8.942798, "o", "C"] [8.943505, "o", "_"] [8.944198, "o", "K"] [8.944889, "o", "E"] [8.945585, "o", "Y"] [8.946565, "o", "="] [8.947287, "o", "t"] [8.94798, "o", "r"] [8.948675, "o", "a"] [8.949377, "o", "n"] [8.950077, "o", "s"] [8.950769, "o", "p"] [8.951483, "o", "a"] [8.952183, "o", "r"] [8.952884, "o", "e"] [8.953576, "o", "n"] [8.954282, "o", "c"] [8.955008, "o", "y"] [8.955723, "o", "."] [8.956413, "o", "d"] [8.957108, "o", "e"] [8.957802, "o", "v"] [8.958498, "o", "/"] [8.959217, "o", "t"] [8.959911, "o", "e"] [8.960603, "o", "s"] [8.961296, "o", "s"] [8.961991, "o", "e"] [8.962686, "o", "r"] [8.963398, "o", "a"] [8.964086, "o", "/"] [8.964785, "o", "e"] [8.965479, "o", "x"] [8.966167, "o", "a"] [8.966862, "o", "m"] [8.967566, "o", "p"] [8.968303, "o", "l"] [8.968999, "o", "e"] [8.96969, "o", "+"] [8.97039, "o", "a"] [8.971131, "o", "e"] [8.971826, "o", "3"] [8.972522, "o", "3"] [8.973241, "o", "0"] [8.973931, "o", "e"] [8.974643, "o", "1"] [8.975366, "o", "5"] [8.976057, "o", "+"] [8.976755, "o", "A"] [8.977453, "o", "S"] [8.97815, "o", "f"] [8.978848, "o", "4"] [8.97956, "o", "/"] [8.980255, "o", "L"] [8.980952, "o", "1"] [8.981649, "o", "z"] [8.982325, "o", "E"] [8.98306, "o", "8"] [8.98375, "o", "5"] [8.984442, "o", "9"] [8.985141, "o", "V"] [8.985833, "o", "q"] [8.986527, "o", "l"] [8.98725, "o", "f"] [8.987951, "o", "Q"] [8.988637, "o", "g"] [8.989334, "o", "G"] [8.990023, "o", "z"] [8.990714, "o", "K"] [8.991418, "o", "y"] [8.992112, "o", "3"] [8.992807, "o", "4"] [8.993508, "o", "l"] [8.994237, "o", "9"] [8.994921, "o", "1"] [8.995635, "o", "G"] [8.996338, "o", "l"] [8.997034, "o", "8"] [8.997737, "o", "W"] [8.998441, "o", "6"] [8.999165, "o", "w"] [8.999868, "o", "f"] [9.000572, "o", "w"] [9.001275, "o", "p"] [9.001972, "o", "+"] [9.002683, "o", "v"] [9.003419, "o", "K"] [9.004109, "o", "P"] [9.00482, "o", "6"] [9.005507, "o", "2"] [9.006209, "o", "D"] [9.006903, "o", "W"] [9.007169, "o", "\u001bM\u001bM\u001b[106D\u001b[3me\u001b[3mx\u001b[3mp\u001b[3mo\u001b[3mr\u001b[3mt\u001b[3m \u001b[3mW\u001b[3mR\u001b[3mI\u001b[3mT\u001b[3mE\u001b[3m_\u001b[3mU\u001b[3mR\u001b[3mL\u001b[3m=\u001b[3mh\u001b[3mt\u001b[3mt\u001b[3mp\u001b[3m:\u001b[3m/\u001b[3m/\u001b[3ml\u001b[3mo\u001b[3mc\u001b[3ma\u001b[3ml\u001b[3mh\u001b[3mo\u001b[3ms\u001b[3mt\u001b[3m:\u001b[3m2\u001b[3m0\u001b[3m2\u001b[3m4\u001b[3m/\u001b[1B\u001b[23m\r\u001b[3me\u001b[3mx\u001b[3mp\u001b[3mo\u001b[3mr\u001b[3mt\u001b[3m \u001b[3mR\u001b[3mE\u001b[3mA\u001b[3mD\u001b[3m_\u001b[3mU\u001b[3mR\u001b[3mL\u001b[3m=\u001b[3mh\u001b[3mt\u001b[3mt\u001b[3mp\u001b[3m:\u001b[3m/\u001b[3m/\u001b[3ml\u001b[3mo\u001b[3mc\u001b[3ma\u001b[3ml\u001b[3mh\u001b[3mo\u001b[3ms\u001b[3mt\u001b[3m:\u001b[3m2\u001b[3m0\u001b[3m2\u001b[3m4\u001b[3m/\u001b[1B\u001b[23m\r\u001b[3me\u001b[3mx\u001b[3mp\u001b[3mo\u001b[3mr\u001b[3mt\u001b[3m \u001b[3mL\u001b[3mO\u001b[3mG\u001b[3m_\u001b[3mP\u001b[3mU\u001b[3mB\u001b[3mL\u001b[3mI\u001b[3mC\u001b[3m_\u001b[3mK\u001b[3mE\u001b[3mY\u001b[3m=\u001b[3mt\u001b[3mr\u001b[3ma\u001b[3mn\u001b[3ms\u001b[3mp\u001b[3ma\u001b[3mr\u001b[3me\u001b[3mn\u001b[3mc\u001b[3my\u001b[3m.\u001b[3md\u001b[3me\u001b[3mv\u001b[3m/\u001b[3mt\u001b[3me\u001b[3ms\u001b[3ms\u001b[3me\u001b[3mr\u001b[3ma\u001b[3m/\u001b[3me\u001b[3mx\u001b[3ma\u001b[3mm\u001b[3mp\u001b[3ml\u001b[3me\u001b[3m+\u001b[3ma\u001b[3me\u001b[3m3\u001b[3m3\u001b[3m0\u001b[3me\u001b[3m1\u001b[3m5\u001b[3m+\u001b[3mA\u001b[3mS\u001b[3mf\u001b[3m4\u001b[3m/\u001b[3mL\u001b[3m1\u001b[3mz\u001b[3mE\u001b[3m8\u001b[3m5\u001b[3m9\u001b[3mV\u001b[3mq\u001b[3ml\u001b[3mf\u001b[3mQ\u001b[3mg\u001b[3mG\u001b[3mz\u001b[3mK\u001b[3my\u001b[3m3\u001b[3m4\u001b[3ml\u001b[3m9\u001b[3m1\u001b[3mG\u001b[3ml\u001b[3m8\u001b[3mW\u001b[3m6\u001b[3mw\u001b[3mf\u001b[3mw\u001b[3mp\u001b[3m+\u001b[3mv\u001b[3mK\u001b[3mP\u001b[3m6\u001b[3m2\u001b[3mD\u001b[3mW\u001b[23m\r\r\n\u001b[K"] [10.080321, "o", "\u001bM\u001bM\u001bM\u001b[2C\u001b[23me\u001b[23mx\u001b[23mp\u001b[23mo\u001b[23mr\u001b[23mt\u001b[23m \u001b[23mW\u001b[23mR\u001b[23mI\u001b[23mT\u001b[23mE\u001b[23m_\u001b[23mU\u001b[23mR\u001b[23mL\u001b[23m=\u001b[23mh\u001b[23mt\u001b[23mt\u001b[23mp\u001b[23m:\u001b[23m/\u001b[23m/\u001b[23ml\u001b[23mo\u001b[23mc\u001b[23ma\u001b[23ml\u001b[23mh\u001b[23mo\u001b[23ms\u001b[23mt\u001b[23m:\u001b[23m2\u001b[23m0\u001b[23m2\u001b[23m4\u001b[23m/\u001b[1B\r\u001b[23me\u001b[23mx\u001b[23mp\u001b[23mo\u001b[23mr\u001b[23mt\u001b[23m \u001b[23mR\u001b[23mE\u001b[23mA\u001b[23mD\u001b[23m_\u001b[23mU\u001b[23mR\u001b[23mL\u001b[23m=\u001b[23mh\u001b[23mt\u001b[23mt\u001b[23mp\u001b[23m:\u001b[23m/\u001b[23m/\u001b[23ml\u001b[23mo\u001b[23mc\u001b[23ma\u001b[23ml\u001b[23mh\u001b[23mo\u001b[23ms\u001b[23mt\u001b[23m:\u001b[23m2\u001b[23m0\u001b[23m2\u001b[23m4\u001b[23m/\u001b[1B\r\u001b[23me\u001b[23mx\u001b[23mp\u001b[23mo\u001b[23mr\u001b[23mt\u001b[23m \u001b[23mL\u001b[23mO\u001b[23mG\u001b[23m_\u001b[23mP\u001b[23mU\u001b[23mB\u001b[23mL\u001b[23mI\u001b[23mC\u001b[23m_\u001b[23mK\u001b[23mE\u001b[23mY\u001b[23m=\u001b[23mt\u001b[23mr\u001b[23ma\u001b[23mn\u001b[23ms\u001b[23mp\u001b[23ma\u001b[23mr\u001b[23me\u001b[23mn\u001b[23mc\u001b[23my\u001b[23m.\u001b[23md\u001b[23me\u001b[23mv\u001b[23m/\u001b[23mt\u001b[23me\u001b[23ms\u001b[23ms\u001b[23me\u001b[23mr\u001b[23ma\u001b[23m/\u001b[23me\u001b[23mx\u001b[23ma\u001b[23mm\u001b[23mp\u001b[23ml\u001b[23me\u001b[23m+\u001b[23ma\u001b[23me\u001b[23m3\u001b[23m3\u001b[23m0\u001b[23me\u001b[23m1\u001b[23m5\u001b[23m+\u001b[23mA\u001b[23mS\u001b[23mf\u001b[23m4\u001b[23m/\u001b[23mL\u001b[23m1\u001b[23mz\u001b[23mE\u001b[23m8\u001b[23m5\u001b[23m9\u001b[23mV\u001b[23mq\u001b[23ml\u001b[23mf\u001b[23mQ\u001b[23mg\u001b[23mG\u001b[23mz\u001b[23mK\u001b[23my\u001b[23m3\u001b[23m4\u001b[23ml\u001b[23m9\u001b["] [10.080453, "o", "23m1\u001b[23mG\u001b[23ml\u001b[23m8\u001b[23mW\u001b[23m6\u001b[23mw\u001b[23mf\u001b[23mw\u001b[23mp\u001b[23m+\u001b[23mv\u001b[23mK\u001b[23mP\u001b[23m6\u001b[23m2\u001b[23mD\u001b[23mW\u001b[1B\r\u001b[K"] [10.085905, "o", "\u001bM\u001bM\u001bM\u001b[2C\u001b[33me\u001b[33mx\u001b[33mp\u001b[33mo\u001b[33mr\u001b[33mt\u001b[39m\u001b[1B\r\u001b[33me\u001b[33mx\u001b[33mp\u001b[33mo\u001b[33mr\u001b[33mt\u001b[39m\u001b[1B\r\u001b[33me\u001b[33mx\u001b[33mp\u001b[33mo\u001b[33mr\u001b[33mt\u001b[39m\u001b[1B\r"] [10.087231, "o", "\u001b[?1l\u001b>"] [10.092721, "o", "\u001b[?25l"] [10.092756, "o", "\u001b[?2004l\u001bM\u001bM\u001bM\r\u001bM\u001bM\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[0m\u001b[49m\u001b[23m\u001b[24m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[39m\u001b[23m\u001b[24m \u001b]133;B\u0007"] [10.092864, "o", "\u001b[33mexport\u001b[39m WRITE_URL=http://localhost:2024/\u001b[K\r\r\n\u001b[33mexport\u001b[39m READ_URL=http://localhost:2024/\u001b[K\r\r\n\u001b[33mexport\u001b[39m LOG_PUBLIC_KEY=transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW\u001b[K\r\r\n\u001b[K\u001b[?25h"] [10.093029, "o", "\u001b[K\r\r\n"] [10.093535, "o", "\u001bkexport\u001b\\"] [10.093693, "o", "\u001b]133;C;\u0007"] [10.093904, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"] [10.094016, "o", "\u001bk../mysql/docker\u001b\\"] [10.10987, "o", "\u001b]133;D;0\u0007\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007"] [10.118122, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\r\n\r\n\u001bM\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mcm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mc\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/mysql/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[43m\u001b[34m\u001b[0m\u001b[34m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m codelab-demo \u001b[30m!3\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m\u001b[38;5;244m───\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[40m\u001b[33m mhutchinson@thinkcentre\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m \u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[37m\u001b[0m\u001b[37m\u001b[40m\u001b[47m\u001b[30m 10:04:22\u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b["] [10.118176, "o", "47m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[K"] [10.118282, "o", "\u001b[?1h\u001b="] [10.119428, "o", "\u001b[?2004h"] [13.777627, "o", "c"] [13.77844, "o", "\bcu"] [13.779306, "o", "r"] [13.780109, "o", "l"] [13.780967, "o", " -"] [13.781741, "o", "d"] [13.782806, "o", " '"] [13.783529, "o", "o"] [13.784245, "o", "n"] [13.785008, "o", "e"] [13.785847, "o", "!"] [13.786658, "o", "'"] [13.787436, "o", " -"] [13.788269, "o", "H"] [13.789353, "o", " \""] [13.790146, "o", "C"] [13.79091, "o", "o"] [13.791767, "o", "n"] [13.792445, "o", "t"] [13.793159, "o", "e"] [13.793893, "o", "n"] [13.794608, "o", "t"] [13.795369, "o", "-"] [13.796144, "o", "T"] [13.796851, "o", "y"] [13.797539, "o", "p"] [13.798254, "o", "e"] [13.798956, "o", ":"] [13.799742, "o", " a"] [13.800711, "o", "p"] [13.801431, "o", "p"] [13.80214, "o", "l"] [13.802842, "o", "i"] [13.803579, "o", "c"] [13.804297, "o", "a"] [13.804985, "o", "t"] [13.805677, "o", "i"] [13.806364, "o", "o"] [13.80709, "o", "n"] [13.807776, "o", "/"] [13.808459, "o", "d"] [13.809143, "o", "a"] [13.809866, "o", "t"] [13.810551, "o", "a"] [13.811301, "o", "\""] [13.811998, "o", " -"] [13.812681, "o", "X"] [13.813404, "o", " P"] [13.814121, "o", "O"] [13.814813, "o", "S"] [13.815542, "o", "T"] [13.816519, "o", " $"] [13.817488, "o", "{"] [13.818211, "o", "W"] [13.818902, "o", "R"] [13.819629, "o", "I"] [13.820317, "o", "T"] [13.821002, "o", "E"] [13.821686, "o", "_"] [13.822398, "o", "U"] [13.823146, "o", "R"] [13.823833, "o", "L"] [13.824824, "o", "}"] [13.825524, "o", "a"] [13.826226, "o", "d"] [13.826923, "o", "d"] [13.827194, "o", "\u001b[74D\u001b[3mc\u001b[3mu\u001b[3mr\u001b[3ml\u001b[3m \u001b[3m-\u001b[3md\u001b[3m \u001b[3m'\u001b[3mo\u001b[3mn\u001b[3me\u001b[3m!\u001b[3m'\u001b[3m \u001b[3m-\u001b[3mH\u001b[3m \u001b[3m\"\u001b[3mC\u001b[3mo\u001b[3mn\u001b[3mt\u001b[3me\u001b[3mn\u001b[3mt\u001b[3m-\u001b[3mT\u001b[3my\u001b[3mp\u001b[3me\u001b[3m:\u001b[3m \u001b[3ma\u001b[3mp\u001b[3mp\u001b[3ml\u001b[3mi\u001b[3mc\u001b[3ma\u001b[3mt\u001b[3mi\u001b[3mo\u001b[3mn\u001b[3m/\u001b[3md\u001b[3ma\u001b[3mt\u001b[3ma\u001b[3m\"\u001b[3m \u001b[3m-\u001b[3mX\u001b[3m \u001b[3mP\u001b[3mO\u001b[3mS\u001b[3mT\u001b[3m \u001b[3m$\u001b[3m{\u001b[3mW\u001b[3mR\u001b[3mI\u001b[3mT\u001b[3mE\u001b[3m_\u001b[3mU\u001b[3mR\u001b[3mL\u001b[3m}\u001b[3ma\u001b[3md\u001b[3md\u001b[23m\r\r\n\u001b[K"] [15.010094, "o", "\u001bM\u001b[2C\u001b[23mc\u001b[23mu\u001b[23mr\u001b[23ml\u001b[23m \u001b[23m-\u001b[23md\u001b[23m \u001b[23m'\u001b[23mo\u001b[23mn\u001b[23me\u001b[23m!\u001b[23m'\u001b[23m \u001b[23m-\u001b[23mH\u001b[23m \u001b[23m\"\u001b[23mC\u001b[23mo\u001b[23mn\u001b[23mt\u001b[23me\u001b[23mn\u001b[23mt\u001b[23m-\u001b[23mT\u001b[23my\u001b[23mp\u001b[23me\u001b[23m:\u001b[23m \u001b[23ma\u001b[23mp\u001b[23mp\u001b[23ml\u001b[23mi\u001b[23mc\u001b[23ma\u001b[23mt\u001b[23mi\u001b[23mo\u001b[23mn\u001b[23m/\u001b[23md\u001b[23ma\u001b[23mt\u001b[23ma\u001b[23m\"\u001b[23m \u001b[23m-\u001b[23mX\u001b[23m \u001b[23mP\u001b[23mO\u001b[23mS\u001b[23mT\u001b[23m \u001b[23m$\u001b[23m{\u001b[23mW\u001b[23mR\u001b[23mI\u001b[23mT\u001b[23mE\u001b[23m_\u001b[23mU\u001b[23mR\u001b[23mL\u001b[23m}\u001b[23ma\u001b[23md\u001b[23md\u001b[1B\r\u001b[K"] [15.016444, "o", "\u001bM\u001b[2C\u001b[32mc\u001b[32mu\u001b[32mr\u001b[32ml\u001b[39m\u001b[4C\u001b[33m'\u001b[33mo\u001b[33mn\u001b[33me\u001b[33m!\u001b[33m'\u001b[39m\u001b[4C\u001b[33m\"\u001b[33mC\u001b[33mo\u001b[33mn\u001b[33mt\u001b[33me\u001b[33mn\u001b[33mt\u001b[33m-\u001b[33mT\u001b[33my\u001b[33mp\u001b[33me\u001b[33m:\u001b[33m \u001b[33ma\u001b[33mp\u001b[33mp\u001b[33ml\u001b[33mi\u001b[33mc\u001b[33ma\u001b[33mt\u001b[33mi\u001b[33mo\u001b[33mn\u001b[33m/\u001b[33md\u001b[33ma\u001b[33mt\u001b[33ma\u001b[33m\"\u001b[39m\u001b[1B\r"] [15.017802, "o", "\u001b[?1l\u001b>"] [15.023987, "o", "\u001b[?25l"] [15.024007, "o", "\u001b[?2004l\u001bM\r\u001bM\u001bM\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[0m\u001b[49m\u001b[23m\u001b[24m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[39m\u001b[23m\u001b[24m \u001b]133;B\u0007"] [15.024126, "o", "\u001b[32mcurl\u001b[39m -d \u001b[33m'one!'\u001b[39m -H \u001b[33m\"Content-Type: application/data\"\u001b[39m -X POST ${WRITE_URL}add\u001b[K\r\r\n\u001b[K\u001b[?25h"] [15.024415, "o", "\u001b[K\r\r\n"] [15.024927, "o", "\u001bkcurl\u001b\\"] [15.025058, "o", "\u001b]133;C;\u0007"] [15.074474, "o", "0"] [15.075172, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"] [15.075413, "o", "\u001bk../mysql/docker\u001b\\"] [15.091904, "o", "\u001b]133;D;0\u0007\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007"] [15.100401, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\r\n\r\n\u001bM\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mcm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mc\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/mysql/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[43m\u001b[34m\u001b[0m\u001b[34m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m codelab-demo \u001b[30m!3\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m\u001b[38;5;244m───\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[40m\u001b[33m mhutchinson@thinkcentre\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m \u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[37m\u001b[0m\u001b[37m\u001b[40m\u001b[47m\u001b[30m 10:04:27\u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b["] [15.100465, "o", "47m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[K"] [15.100517, "o", "\u001b[?1h\u001b="] [15.101465, "o", "\u001b[?2004h"] [15.825976, "o", "\u001b[32mcurl\u001b[39m -d \u001b[33m'one!'\u001b[39m -H \u001b[33m\"Content-Type: application/data\"\u001b[39m -X POST ${WRITE_URL}add\r\r\n\u001b[K\u001bM\u001b[76C"] [15.827107, "o", "\u001b[1B\r\u001b[K\u001bM\u001b[76C"] [16.101733, "o", "\u001b[74D"] [16.269505, "o", "\u001b[1C"] [16.686137, "o", "\u001b[1C"] [16.733997, "o", "\u001b[1C"] [16.758323, "o", "\u001b[1C"] [16.778607, "o", "\u001b[1C"] [16.82567, "o", "\u001b[1C"] [16.84741, "o", "\u001b[1C"] [16.892956, "o", "\u001b[1C"] [16.917676, "o", "\u001b[1C"] [16.967936, "o", "\u001b[1C"] [16.990144, "o", "\u001b[1C"] [17.037268, "o", "\u001b[1C"] [17.638822, "o", "\b\b\b\u001b[3P\u001b[62C \u001b[65D"] [17.942266, "o", "\u001b[33mt\u001b[33m!\u001b[33m'\u001b[39m -H\u001b[39m \u001b[33m\"\u001b[33mC\u001b[33mo\u001b[33mn\u001b[33mt\u001b[33me\u001b[33mn\u001b[33mt\u001b[33m-\u001b[33mT\u001b[33my\u001b[33mp\u001b[33me\u001b[33m:\u001b[33m \u001b[33ma\u001b[33mp\u001b[33mp\u001b[33ml\u001b[33mi\u001b[33mc\u001b[33ma\u001b[33mt\u001b[33mi\u001b[33mo\u001b[33mn\u001b[33m/\u001b[33md\u001b[33ma\u001b[33mt\u001b[33ma\u001b[33m\"\u001b[39m -X POST ${WRITE_URL}add\u001b[62D"] [18.012312, "o", "\u001b[33mw\u001b[33m!\u001b[33m'\u001b[39m -H\u001b[39m \u001b[33m\"\u001b[33mC\u001b[33mo\u001b[33mn\u001b[33mt\u001b[33me\u001b[33mn\u001b[33mt\u001b[33m-\u001b[33mT\u001b[33my\u001b[33mp\u001b[33me\u001b[33m:\u001b[33m \u001b[33ma\u001b[33mp\u001b[33mp\u001b[33ml\u001b[33mi\u001b[33mc\u001b[33ma\u001b[33mt\u001b[33mi\u001b[33mo\u001b[33mn\u001b[33m/\u001b[33md\u001b[33ma\u001b[33mt\u001b[33ma\u001b[33m\"\u001b[39m -X POST ${WRITE_URL}add\u001b[62D"] [18.130925, "o", "\u001b[33mo\u001b[33m!\u001b[33m'\u001b[39m -H\u001b[39m \u001b[33m\"\u001b[33mC\u001b[33mo\u001b[33mn\u001b[33mt\u001b[33me\u001b[33mn\u001b[33mt\u001b[33m-\u001b[33mT\u001b[33my\u001b[33mp\u001b[33me\u001b[33m:\u001b[33m \u001b[33ma\u001b[33mp\u001b[33mp\u001b[33ml\u001b[33mi\u001b[33mc\u001b[33ma\u001b[33mt\u001b[33mi\u001b[33mo\u001b[33mn\u001b[33m/\u001b[33md\u001b[33ma\u001b[33mt\u001b[33ma\u001b[33m\"\u001b[39m -X POST ${WRITE_URL}add\u001b[62D"] [18.90712, "o", "\u001b[?1l\u001b>"] [18.913143, "o", "\u001b[?25l"] [18.913243, "o", "\u001b[?2004l\r\r\u001bM\u001bM\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[0m\u001b[49m\u001b[23m\u001b[24m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[39m\u001b[23m\u001b[24m \u001b]133;B\u0007\u001b[32mcurl\u001b[39m -d \u001b[33m'two!'\u001b[39m -H \u001b[33m\"Content-Type: application/data\"\u001b[39m -X POST ${WRITE_URL}add\u001b[K\r\r\n\u001b[K\u001bM\u001b[14C\u001b[?25h"] [18.913482, "o", "\u001b[1B\r\u001b[K\u001bM\u001b[14C\u001b[1B\r\r\n"] [18.913953, "o", "\u001bkcurl\u001b\\"] [18.914116, "o", "\u001b]133;C;\u0007"] [19.073722, "o", "1"] [19.074446, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"] [19.074589, "o", "\u001bk../mysql/docker\u001b\\"] [19.09114, "o", "\u001b]133;D;0\u0007\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007"] [19.091237, "o", "\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007"] [19.099773, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\r\n\r\n\u001bM\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mcm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mc\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/mysql/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[43m\u001b[34m\u001b[0m\u001b[34m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m codelab-demo \u001b[30m!3\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m\u001b[38;5;244m───\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[40m\u001b[33m mhutchinson@thinkcentre\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m \u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[37m\u001b[0m\u001b[37m\u001b[40m\u001b[47m\u001b[30m 10:04:31\u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b["] [19.099865, "o", "47m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[K"] [19.099905, "o", "\u001b[?1h\u001b="] [19.101085, "o", "\u001b[?2004h"] [19.511613, "o", "\u001b[32mcurl\u001b[39m -d \u001b[33m'two!'\u001b[39m -H \u001b[33m\"Content-Type: application/data\"\u001b[39m -X POST ${WRITE_URL}add\r\r\n\u001b[K\u001bM\u001b[76C"] [19.512695, "o", "\u001b[1B\r\u001b[K\u001bM\u001b[76C"] [19.959524, "o", "\u001b[1B\r"] [20.431282, "o", "\u001bM\u001b[2C"] [21.086465, "o", "\u001b[1C"] [21.517744, "o", "\u001b[1C"] [21.53582, "o", "\u001b[1C"] [21.558437, "o", "\u001b[1C"] [21.605752, "o", "\u001b[1C"] [21.627388, "o", "\u001b[1C"] [21.670054, "o", "\u001b[1C"] [21.694355, "o", "\u001b[1C"] [21.739916, "o", "\u001b[1C"] [21.760797, "o", "\u001b[1C"] [21.808907, "o", "\u001b[1C"] [22.170932, "o", "\u001b[1C"] [22.502518, "o", "\b\b\b\u001b[3P\u001b[62C \u001b[65D"] [22.776153, "o", "\u001b[33mt\u001b[33m!\u001b[33m'\u001b[39m -H\u001b[39m \u001b[33m\"\u001b[33mC\u001b[33mo\u001b[33mn\u001b[33mt\u001b[33me\u001b[33mn\u001b[33mt\u001b[33m-\u001b[33mT\u001b[33my\u001b[33mp\u001b[33me\u001b[33m:\u001b[33m \u001b[33ma\u001b[33mp\u001b[33mp\u001b[33ml\u001b[33mi\u001b[33mc\u001b[33ma\u001b[33mt\u001b[33mi\u001b[33mo\u001b[33mn\u001b[33m/\u001b[33md\u001b[33ma\u001b[33mt\u001b[33ma\u001b[33m\"\u001b[39m -X POST ${WRITE_URL}add\u001b[62D"] [22.872273, "o", "\u001b[33mh\u001b[33m!\u001b[33m'\u001b[39m -H\u001b[39m \u001b[33m\"\u001b[33mC\u001b[33mo\u001b[33mn\u001b[33mt\u001b[33me\u001b[33mn\u001b[33mt\u001b[33m-\u001b[33mT\u001b[33my\u001b[33mp\u001b[33me\u001b[33m:\u001b[33m \u001b[33ma\u001b[33mp\u001b[33mp\u001b[33ml\u001b[33mi\u001b[33mc\u001b[33ma\u001b[33mt\u001b[33mi\u001b[33mo\u001b[33mn\u001b[33m/\u001b[33md\u001b[33ma\u001b[33mt\u001b[33ma\u001b[33m\"\u001b[39m -X POST ${WRITE_URL}add\u001b[62D"] [22.988189, "o", "\u001b[33mr\u001b[33m!\u001b[33m'\u001b[39m -H\u001b[39m \u001b[33m\"\u001b[33mC\u001b[33mo\u001b[33mn\u001b[33mt\u001b[33me\u001b[33mn\u001b[33mt\u001b[33m-\u001b[33mT\u001b[33my\u001b[33mp\u001b[33me\u001b[33m:\u001b[33m \u001b[33ma\u001b[33mp\u001b[33mp\u001b[33ml\u001b[33mi\u001b[33mc\u001b[33ma\u001b[33mt\u001b[33mi\u001b[33mo\u001b[33mn\u001b[33m/\u001b[33md\u001b[33ma\u001b[33mt\u001b[33ma\u001b[33m\"\u001b[39m -X POST ${WRITE_URL}add\u001b[62D"] [23.030824, "o", "\u001b[33me\u001b[33m!\u001b[33m'\u001b[39m -H\u001b[39m \u001b[33m\"\u001b[33mC\u001b[33mo\u001b[33mn\u001b[33mt\u001b[33me\u001b[33mn\u001b[33mt\u001b[33m-\u001b[33mT\u001b[33my\u001b[33mp\u001b[33me\u001b[33m:\u001b[33m \u001b[33ma\u001b[33mp\u001b[33mp\u001b[33ml\u001b[33mi\u001b[33mc\u001b[33ma\u001b[33mt\u001b[33mi\u001b[33mo\u001b[33mn\u001b[33m/\u001b[33md\u001b[33ma\u001b[33mt\u001b[33ma\u001b[33m\"\u001b[39m -X POST ${WRITE_URL}add\u001b[62D"] [23.225097, "o", "\u001b[33me\u001b[33m!\u001b[33m'\u001b[39m -H\u001b[39m \u001b[33m\"\u001b[33mC\u001b[33mo\u001b[33mn\u001b[33mt\u001b[33me\u001b[33mn\u001b[33mt\u001b[33m-\u001b[33mT\u001b[33my\u001b[33mp\u001b[33me\u001b[33m:\u001b[33m \u001b[33ma\u001b[33mp\u001b[33mp\u001b[33ml\u001b[33mi\u001b[33mc\u001b[33ma\u001b[33mt\u001b[33mi\u001b[33mo\u001b[33mn\u001b[33m/\u001b[33md\u001b[33ma\u001b[33mt\u001b[33ma\u001b[33m\"\u001b[39m -X POST ${WRITE_URL}add\u001b[62D"] [24.443803, "o", "\u001b[?1l\u001b>"] [24.450122, "o", "\u001b[?25l"] [24.450158, "o", "\u001b[?2004l\r\r\u001bM\u001bM\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[0m\u001b[49m\u001b[23m\u001b[24m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[39m\u001b[23m\u001b[24m \u001b]133;B\u0007\u001b[32mcurl\u001b[39m -d \u001b[33m'three!'\u001b[39m -H \u001b[33m\"Content-Type: application/data\"\u001b[39m -X POST ${WRITE_URL}add\u001b[K\r\r\n\u001b[K\u001bM\u001b[16C\u001b[?25h"] [24.450436, "o", "\u001b[1B\r\u001b[K\u001bM\u001b[16C\u001b[1B\r\r\n"] [24.450884, "o", "\u001bkcurl\u001b\\"] [24.451089, "o", "\u001b]133;C;\u0007"] [24.574464, "o", "2"] [24.575167, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"] [24.575336, "o", "\u001bk../mysql/docker\u001b\\"] [24.591653, "o", "\u001b]133;D;0\u0007\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007"] [24.591675, "o", "\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007"] [24.600283, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\r\n\r\n\u001bM\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mcm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mc\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/mysql/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[43m\u001b[34m\u001b[0m\u001b[34m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m codelab-demo \u001b[30m!3\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m\u001b[38;5;244m───\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[40m\u001b[33m mhutchinson@thinkcentre\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m \u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[37m\u001b[0m\u001b[37m\u001b[40m\u001b[47m\u001b[30m 10:04:37\u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b["] [24.600325, "o", "47m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[K"] [24.600428, "o", "\u001b[?1h\u001b="] [24.601557, "o", "\u001b[?2004h"] [27.012252, "o", "c"] [27.013016, "o", "\bcu"] [27.013782, "o", "r"] [27.014564, "o", "l"] [27.015381, "o", " -"] [27.016157, "o", "s"] [27.017207, "o", " $"] [27.018247, "o", "{"] [27.018993, "o", "R"] [27.019689, "o", "E"] [27.020438, "o", "A"] [27.021122, "o", "D"] [27.021847, "o", "_"] [27.022574, "o", "U"] [27.023331, "o", "R"] [27.024039, "o", "L"] [27.024992, "o", "}"] [27.025686, "o", "c"] [27.026376, "o", "h"] [27.027074, "o", "e"] [27.027793, "o", "c"] [27.028477, "o", "k"] [27.029162, "o", "p"] [27.029882, "o", "o"] [27.030577, "o", "i"] [27.031291, "o", "n"] [27.031971, "o", "t"] [27.032154, "o", "\u001b[29D\u001b[3mc\u001b[3mu\u001b[3mr\u001b[3ml\u001b[3m \u001b[3m-\u001b[3ms\u001b[3m \u001b[3m$\u001b[3m{\u001b[3mR\u001b[3mE\u001b[3mA\u001b[3mD\u001b[3m_\u001b[3mU\u001b[3mR\u001b[3mL\u001b[3m}\u001b[3mc\u001b[3mh\u001b[3me\u001b[3mc\u001b[3mk\u001b[3mp\u001b[3mo\u001b[3mi\u001b[3mn\u001b[3mt\u001b[23m\r\r\n\u001b[K"] [27.5467, "o", "\u001bM\u001b[2C\u001b[23mc\u001b[23mu\u001b[23mr\u001b[23ml\u001b[23m \u001b[23m-\u001b[23ms\u001b[23m \u001b[23m$\u001b[23m{\u001b[23mR\u001b[23mE\u001b[23mA\u001b[23mD\u001b[23m_\u001b[23mU\u001b[23mR\u001b[23mL\u001b[23m}\u001b[23mc\u001b[23mh\u001b[23me\u001b[23mc\u001b[23mk\u001b[23mp\u001b[23mo\u001b[23mi\u001b[23mn\u001b[23mt\u001b[1B\r\u001b[K"] [27.549589, "o", "\u001bM\u001b[2C\u001b[32mc\u001b[32mu\u001b[32mr\u001b[32ml\u001b[39m\u001b[1B\r"] [27.550667, "o", "\u001b[?1l\u001b>"] [27.553527, "o", "\u001b[?25l\u001b[?2004l\u001bM\r\u001bM\u001bM\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[0m\u001b[49m\u001b[23m\u001b[24m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[39m\u001b[23m\u001b[24m \u001b]133;B\u0007\u001b[32mcurl\u001b[39m -s ${READ_URL}checkpoint\u001b[K\r\r\n\u001b[K\u001b[?25h"] [27.553875, "o", "\u001b[K\r\r\n"] [27.554295, "o", "\u001bkcurl\u001b\\"] [27.554464, "o", "\u001b]133;C;\u0007"] [27.559348, "o", "transparency.dev/tessera/example\r\n3\r\nBkYMlwZNxfP2K8yj8aMVgyfn9JggETENwpGvx6VUL4g=\r\n\r\n— transparency.dev/tessera/example rjMOFbMZCBvUgR11SqiWrtRuRuPCE8Y1zH7xRc2VCVQBy/E4JJDCa+1gxiH/SONzmxbNoPkTjH45Y5JWGfa/cq46BQA=\r\n"] [27.560075, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"] [27.560303, "o", "\u001bk../mysql/docker\u001b\\"] [27.576918, "o", "\u001b]133;D;0\u0007"] [27.577018, "o", "\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007"] [27.585479, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\r\n\r\n\u001bM\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mcm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mc\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/mysql/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[43m\u001b[34m\u001b[0m\u001b[34m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m codelab-demo \u001b[30m!3\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m\u001b[38;5;244m───\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[40m\u001b[33m mhutchinson@thinkcentre\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m \u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[37m\u001b[0m\u001b[37m\u001b[40m\u001b[47m\u001b[30m 10:04:40\u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b["] [27.585514, "o", "47m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[K"] [27.585582, "o", "\u001b[?1h\u001b="] [27.586684, "o", "\u001b[?2004h"] [29.553995, "o", "c"] [29.554771, "o", "\bcu"] [29.555543, "o", "r"] [29.556376, "o", "l"] [29.557109, "o", " -"] [29.557885, "o", "s"] [29.558893, "o", " $"] [29.560073, "o", "{"] [29.560795, "o", "R"] [29.561544, "o", "E"] [29.562231, "o", "A"] [29.562928, "o", "D"] [29.563677, "o", "_"] [29.564413, "o", "U"] [29.565119, "o", "R"] [29.565813, "o", "L"] [29.566787, "o", "}"] [29.567534, "o", "t"] [29.568236, "o", "i"] [29.568935, "o", "l"] [29.569642, "o", "e"] [29.570341, "o", "/"] [29.571093, "o", "e"] [29.57187, "o", "n"] [29.572573, "o", "t"] [29.573281, "o", "r"] [29.573972, "o", "i"] [29.574669, "o", "e"] [29.575423, "o", "s"] [29.576123, "o", "/"] [29.57685, "o", "0"] [29.577579, "o", "0"] [29.578311, "o", "0"] [29.579068, "o", "."] [29.579801, "o", "p"] [29.580531, "o", "/"] [29.581294, "o", "3"] [29.582316, "o", " |"] [29.583109, "o", " x"] [29.583851, "o", "x"] [29.584577, "o", "d"] [29.584797, "o", "\u001b[45D\u001b[3mc\u001b[3mu\u001b[3mr\u001b[3ml\u001b[3m \u001b[3m-\u001b[3ms\u001b[3m \u001b[3m$\u001b[3m{\u001b[3mR\u001b[3mE\u001b[3mA\u001b[3mD\u001b[3m_\u001b[3mU\u001b[3mR\u001b[3mL\u001b[3m}\u001b[3mt\u001b[3mi\u001b[3ml\u001b[3me\u001b[3m/\u001b[3me\u001b[3mn\u001b[3mt\u001b[3mr\u001b[3mi\u001b[3me\u001b[3ms\u001b[3m/\u001b[3m0\u001b[3m0\u001b[3m0\u001b[3m.\u001b[3mp\u001b[3m/\u001b[3m3\u001b[3m \u001b[3m|\u001b[3m \u001b[3mx\u001b[3mx\u001b[3md\u001b[23m\r\r\n\u001b[K"] [30.171165, "o", "\u001bM\u001b[2C\u001b[23mc\u001b[23mu\u001b[23mr\u001b[23ml\u001b[23m \u001b[23m-\u001b[23ms\u001b[23m \u001b[23m$\u001b[23m{\u001b[23mR\u001b[23mE\u001b[23mA\u001b[23mD\u001b[23m_\u001b[23mU\u001b[23mR\u001b[23mL\u001b[23m}\u001b[23mt\u001b[23mi\u001b[23ml\u001b[23me\u001b[23m/\u001b[23me\u001b[23mn\u001b[23mt\u001b[23mr\u001b[23mi\u001b[23me\u001b[23ms\u001b[23m/\u001b[23m0\u001b[23m0\u001b[23m0\u001b[23m.\u001b[23mp\u001b[23m/\u001b[23m3\u001b[23m \u001b[23m|\u001b[23m \u001b[23mx\u001b[23mx\u001b[23md\u001b[1B\r\u001b[K"] [30.175028, "o", "\u001bM\u001b[2C\u001b[32mc\u001b[32mu\u001b[32mr\u001b[32ml\u001b[39m\u001b[38C\u001b[32mx\u001b[32mx\u001b[32md\u001b[39m\u001b[1B\r"] [30.17626, "o", "\u001b[?1l\u001b>"] [30.180055, "o", "\u001b[?25l"] [30.180097, "o", "\u001b[?2004l\u001bM\r\u001bM\u001bM\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[0m\u001b[49m\u001b[23m\u001b[24m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[39m\u001b[23m\u001b[24m \u001b]133;B\u0007\u001b[32mcurl\u001b[39m -s ${READ_URL}tile/entries/000.p/3 | \u001b[32mxxd\u001b[39m\u001b[K\r\r\n\u001b[K\u001b[?25h"] [30.180516, "o", "\u001b[K\r\r\n"] [30.181077, "o", "\u001bkcurl\u001b\\"] [30.181311, "o", "\u001b]133;C;\u0007"] [30.186332, "o", "00000000: 0004 6f6e 6521 0004 7477 6f21 0006 7468 ..one!..two!..th\r\n"] [30.18671, "o", "00000010: 7265 6521 ree!\r\n"] [30.186916, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"] [30.187113, "o", "\u001bk../mysql/docker\u001b\\"] [30.203469, "o", "\u001b]133;D;0\u0007"] [30.203572, "o", "\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007"] [30.211997, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\r\n\r\n\u001bM\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mcm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mc\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/mysql/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[43m\u001b[34m\u001b[0m\u001b[34m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m codelab-demo \u001b[30m!3\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m\u001b[38;5;244m───\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[40m\u001b[33m mhutchinson@thinkcentre\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m \u001b[0m\u001b[33m\u001b[40m\u001b[40m\u001b[33m\u001b[37m\u001b[0m\u001b[37m\u001b[40m\u001b[47m\u001b[30m 10:04:42\u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b[47m\u001b[47m\u001b[30m \u001b[0m\u001b[30m\u001b["] [30.212101, "o", "47m\u001b[49m\u001b[39m\r\n\u001b[0m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[K"] [30.212181, "o", "\u001b[?1h\u001b="] [30.213213, "o", "\u001b[?2004h"] [33.712646, "o", "g"] [33.71347, "o", "\bgo"] [33.714303, "o", " r"] [33.715155, "o", "u"] [33.715952, "o", "n"] [33.716826, "o", " g"] [33.717577, "o", "i"] [33.718369, "o", "t"] [33.719191, "o", "h"] [33.720007, "o", "u"] [33.720856, "o", "b"] [33.721642, "o", "."] [33.722328, "o", "c"] [33.723115, "o", "o"] [33.72384, "o", "m"] [33.724597, "o", "/"] [33.725315, "o", "m"] [33.726008, "o", "h"] [33.726787, "o", "u"] [33.727526, "o", "t"] [33.728248, "o", "c"] [33.728947, "o", "h"] [33.72971, "o", "i"] [33.730438, "o", "n"] [33.731232, "o", "s"] [33.732003, "o", "o"] [33.732743, "o", "n"] [33.733478, "o", "/"] [33.734213, "o", "w"] [33.734951, "o", "o"] [33.735766, "o", "o"] [33.736537, "o", "d"] [33.737239, "o", "p"] [33.737975, "o", "e"] [33.738747, "o", "c"] [33.739507, "o", "k"] [33.740282, "o", "e"] [33.740983, "o", "r"] [33.741719, "o", "@"] [33.742452, "o", "m"] [33.74321, "o", "a"] [33.743946, "o", "i"] [33.744683, "o", "n"] [33.745723, "o", " \\"] [33.746509, "o", "\r\r\n -\u001b[K"] [33.747282, "o", "-"] [33.748019, "o", "c"] [33.748751, "o", "u"] [33.749488, "o", "s"] [33.750226, "o", "t"] [33.750965, "o", "o"] [33.751754, "o", "m"] [33.752488, "o", "_"] [33.753221, "o", "l"] [33.753953, "o", "o"] [33.754689, "o", "g"] [33.755444, "o", "_"] [33.756178, "o", "t"] [33.756915, "o", "y"] [33.757687, "o", "p"] [33.758455, "o", "e"] [33.759471, "o", "="] [33.760262, "o", "t"] [33.761029, "o", "i"] [33.761768, "o", "l"] [33.762503, "o", "e"] [33.76326, "o", "s"] [33.764294, "o", " \\"] [33.765058, "o", "\r\r\n -\u001b[K"] [33.765805, "o", "-"] [33.766541, "o", "c"] [33.767309, "o", "u"] [33.76804, "o", "s"] [33.768778, "o", "t"] [33.769497, "o", "o"] [33.770237, "o", "m"] [33.771, "o", "_"] [33.771721, "o", "l"] [33.772457, "o", "o"] [33.773195, "o", "g"] [33.773935, "o", "_"] [33.774665, "o", "u"] [33.775421, "o", "r"] [33.776161, "o", "l"] [33.777181, "o", "="] [33.778236, "o", "$"] [33.779303, "o", "{"] [33.780048, "o", "R"] [33.780792, "o", "E"] [33.781524, "o", "A"] [33.78226, "o", "D"] [33.782983, "o", "_"] [33.783719, "o", "U"] [33.784456, "o", "R"] [33.785191, "o", "L"] [33.786213, "o", "}"] [33.787296, "o", " \\"] [33.788079, "o", "\r\r\n -\u001b[K"] [33.788831, "o", "-"] [33.789563, "o", "c"] [33.790303, "o", "u"] [33.791084, "o", "s"] [33.791857, "o", "t"] [33.792563, "o", "o"] [33.793304, "o", "m"] [33.794047, "o", "_"] [33.794779, "o", "l"] [33.795544, "o", "o"] [33.796265, "o", "g"] [33.797004, "o", "_"] [33.797742, "o", "v"] [33.798481, "o", "k"] [33.799242, "o", "e"] [33.79998, "o", "y"] [33.801009, "o", "="] [33.802034, "o", "$"] [33.803111, "o", "{"] [33.803887, "o", "L"] [33.804594, "o", "O"] [33.80533, "o", "G"] [33.806067, "o", "_"] [33.806803, "o", "P"] [33.807587, "o", "U"] [33.80833, "o", "B"] [33.809036, "o", "L"] [33.80977, "o", "I"] [33.810506, "o", "C"] [33.811277, "o", "_"] [33.812016, "o", "K"] [33.812751, "o", "E"] [33.813492, "o", "Y"] [33.814519, "o", "}"] [33.814767, "o", "\u001bM\u001bM\u001bM\u001b[35D\u001b[3mg\u001b[3mo\u001b[3m \u001b[3mr\u001b[3mu\u001b[3mn\u001b[3m \u001b[3mg\u001b[3mi\u001b[3mt\u001b[3mh\u001b[3mu\u001b[3mb\u001b[3m.\u001b[3mc\u001b[3mo\u001b[3mm\u001b[3m/\u001b[3mm\u001b[3mh\u001b[3mu\u001b[3mt\u001b[3mc\u001b[3mh\u001b[3mi\u001b[3mn\u001b[3ms\u001b[3mo\u001b[3mn\u001b[3m/\u001b[3mw\u001b[3mo\u001b[3mo\u001b[3md\u001b[3mp\u001b[3me\u001b[3mc\u001b[3mk\u001b[3me\u001b[3mr\u001b[3m@\u001b[3mm\u001b[3ma\u001b[3mi\u001b[3mn\u001b[3m \u001b[3m\\\u001b[1B\u001b[23m\r\u001b[3m \u001b[3m \u001b[3m-\u001b[3m-\u001b[3mc\u001b[3mu\u001b[3ms\u001b[3mt\u001b[3mo\u001b[3mm\u001b[3m_\u001b[3ml\u001b[3mo\u001b[3mg\u001b[3m_\u001b[3mt\u001b[3my\u001b[3mp\u001b[3me\u001b[3m=\u001b[3mt\u001b[3mi\u001b[3ml\u001b[3me\u001b[3ms\u001b[3m \u001b[3m\\\u001b[1B\u001b[23m\r\u001b[3m \u001b[3m \u001b[3m-\u001b[3m-\u001b[3mc\u001b[3mu\u001b[3ms\u001b[3mt\u001b[3mo\u001b[3mm\u001b[3m_\u001b[3ml\u001b[3mo\u001b[3mg\u001b[3m_\u001b[3mu\u001b[3mr\u001b[3ml\u001b[3m=\u001b[3m$\u001b[3m{\u001b[3mR\u001b[3mE\u001b[3mA\u001b[3mD\u001b[3m_\u001b[3mU\u001b[3mR\u001b[3mL\u001b[3m}\u001b[3m \u001b[3m\\\u001b[1B\u001b[23m\r\u001b[3m \u001b[3m \u001b[3m-\u001b[3m-\u001b[3mc\u001b[3mu\u001b[3ms\u001b[3mt\u001b[3mo\u001b[3mm\u001b[3m_\u001b[3ml\u001b[3mo\u001b[3mg\u001b[3m_\u001b[3mv\u001b[3mk\u001b[3me\u001b[3my\u001b[3m=\u001b[3m$\u001b[3m{\u001b[3mL\u001b[3mO\u001b[3mG\u001b[3m_\u001b[3mP\u001b[3mU\u001b[3mB\u001b[3mL\u001b[3mI\u001b[3mC\u001b[3m_\u001b[3mK\u001b[3mE\u001b[3mY\u001b[3m}\u001b[23m\r\r\n\u001b[K"] [34.598406, "o", "\u001b[4A\u001b[2C\u001b[23mg\u001b[23mo\u001b[23m \u001b[23mr\u001b[23mu\u001b[23mn\u001b[23m \u001b[23mg\u001b[23mi\u001b[23mt\u001b[23mh\u001b[23mu\u001b[23mb\u001b[23m.\u001b[23mc\u001b[23mo\u001b[23mm\u001b[23m/\u001b[23mm\u001b[23mh\u001b[23mu\u001b[23mt\u001b[23mc\u001b[23mh\u001b[23mi\u001b[23mn\u001b[23ms\u001b[23mo\u001b[23mn\u001b[23m/\u001b[23mw\u001b[23mo\u001b[23mo\u001b[23md\u001b[23mp\u001b[23me\u001b[23mc\u001b[23mk\u001b[23me\u001b[23mr\u001b[23m@\u001b[23mm\u001b[23ma\u001b[23mi\u001b[23mn\u001b[23m \u001b[23m\\\u001b[1B\r\u001b[23m \u001b[23m \u001b[23m-\u001b[23m-\u001b[23mc\u001b[23mu\u001b[23ms\u001b[23mt\u001b[23mo\u001b[23mm\u001b[23m_\u001b[23ml\u001b[23mo\u001b[23mg\u001b[23m_\u001b[23mt\u001b[23my\u001b[23mp\u001b[23me\u001b[23m=\u001b[23mt\u001b[23mi\u001b[23ml\u001b[23me\u001b[23ms\u001b[23m \u001b[23m\\\u001b[1B\r\u001b[23m \u001b[23m \u001b[23m-\u001b[23m-\u001b[23mc\u001b[23mu\u001b[23ms\u001b[23mt\u001b[23mo\u001b[23mm\u001b[23m_\u001b[23ml\u001b[23mo\u001b[23mg\u001b[23m_\u001b[23mu\u001b[23mr\u001b[23ml\u001b[23m=\u001b[23m$\u001b[23m{\u001b[23mR\u001b[23mE\u001b[23mA\u001b[23mD\u001b[23m_\u001b[23mU\u001b[23mR\u001b[23mL\u001b[23m}\u001b[23m \u001b[23m\\\u001b[1B\r\u001b[23m \u001b[23m \u001b[23m-\u001b[23m-\u001b[23mc\u001b[23mu\u001b[23ms\u001b[23mt\u001b[23mo\u001b[23mm\u001b[23m_\u001b[23ml\u001b[23mo\u001b[23mg\u001b[23m_\u001b[23mv\u001b[23mk\u001b[23me\u001b[23my\u001b[23m=\u001b[23m$\u001b[23m{\u001b[23mL\u001b[23mO\u001b[23mG\u001b[23m_\u001b[23mP\u001b[23mU\u001b[23mB\u001b[23mL\u001b[23mI\u001b[23mC\u001b[23m_\u001b[23mK\u001b[23mE\u001b[23mY\u001b[23m}\u001b[1B\r\u001b[K"] [34.603233, "o", "\u001b[4A\u001b[2C\u001b[32mg\u001b[32mo\u001b[39m\u001b[4B\r"] [34.604486, "o", "\u001b[?1l\u001b>"] [34.609169, "o", "\u001b[?25l"] [34.609259, "o", "\u001b[?2004l\u001b[4A\r\u001bM\u001bM\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b]133;A\u0007\u001b[0m\u001b[49m\u001b[23m\u001b[24m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[39m\u001b[23m\u001b[24m \u001b]133;B\u0007\u001b[32mgo\u001b[39m run github.com/mhutchinson/woodpecker@main \\\u001b[K\r\r\n --custom_log_type=tiles \\\u001b[K\r\r\n --custom_log_url=${READ_URL} \\\u001b[K\r\r\n --custom_log_vkey=${LOG_PUBLIC_KEY}\u001b[K\r\r\n\u001b[K\u001b[?25h"] [34.609558, "o", "\u001b[K\r\r\n"] [34.610086, "o", "\u001bkgo\u001b\\"] [34.61027, "o", "\u001b]133;C;\u0007"] [35.152434, "o", "I0925 10:04:47.606770 981177 main.go:220] No origin provided; using verifier name: \"transparency.dev/tessera/example\"\r\n"] [35.478632, "o", "\u001b[?1000l\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?2004l\u001b[?1049h\u001b[?1h\u001b=\u001b[?25l\u001b(B\u001b)0\u001b[H\u001b[J\u001b[?1000l\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?2004l"] [35.480231, "o", "\u001b[?25l\u001b[1;1H\u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\┌────────────────────Log Checkpoint────────────────────┐┌──────────────Witnessed Checkpoint (N=2)───────────────┐\u001b[2;1H│transparency.dev/tessera/example\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[3;1H│3\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[4;1H│BkYMlwZNxfP2K8yj8aMVgyfn9JggETENwpGvx6VUL4g=\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[5;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ "] [35.480352, "o", " \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[6;1H│— transparency.dev/tessera/example \u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[7;1H│rjMOFbMZCBvUgR11SqiWrtRuRuPCE8Y1zH7xRc2VCVQBy/\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[8;1H│E4JJDCa+1gxiH/SONzmxbNoPkTjH45Y5JWGfa/cq46BQA=\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[9;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[10;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f"] [35.480431, "o", "\u001b[97;40m\u001b]8;;\u001b\\│\u001b[11;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[12;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[13;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[14;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\││\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[15;1H└──────────────────────────────────────────────────────┘└────────────"] [35.480476, "o", "───────────────────────────────────────────┘\u001b[16;1H┌────────────────────────────────────────────────────Leaf 2─────────────────────────────────────────────────────┐\u001b[17;1H│three!\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[18;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[19;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[20;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ "] [35.480516, "o", " \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[21;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[22;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[23;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[24;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[25;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[26;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]"] [35.480557, "o", "8;;\u001b\\│\u001b[27;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[28;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[29;1H└───────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[30;1H\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[31;1H \u001b[32;1H "] [35.480591, "o", " \u001b[33;1H \u001b[34;1H \u001b[?25l"] [35.481689, "o", "\u001b[?25l\u001b[1;1H\u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\┌────────────────────Log Checkpoint────────────────────┐┌──────────────Witnessed Checkpoint (N=2)───────────────┐\u001b[2;1H│transparency.dev/tessera/example\u001b[2;56H││\u001b[2;113H│\u001b[3;1H│3\u001b[3;56H││\u001b[3;113H│\u001b[4;1H│BkYMlwZNxfP2K8yj8aMVgyfn9JggETENwpGvx6VUL4g=\u001b[4;56H││\u001b[4;113H│\u001b[5;1H│\u001b[5;56H││\u001b[5;113H│\u001b[6;1H│—\u001b[6;4Htransparency.dev/tessera/example\u001b[6;56H││\u001b[6;113H│\u001b[7;1H│rjMOFbMZCBvUgR11SqiWrtRuRuPCE8Y1zH7xRc2VCVQBy/\u001b[7;56H││\u001b[7;113H│\u001b[8;1H│E4JJDCa+1gxiH/SONzmxbNoPkTjH45Y5JWGfa/cq46BQA=\u001b[8;56H││\u001b[8;113H│\u001b[9;1H│\u001b[9;56H││\u001b[9;113H│\u001b[10;1H│\u001b[10;56H││\u001b[10;113H│\u001b[11;1H│\u001b[11;56H││\u001b[11;113H│\u001b[12;1H│\u001b[12;56H││\u001b[12;113H│\u001b[13;1H│\u001b[13;56H││\u001b[13;113H│\u001b[14;1H│\u001b[14;56H││\u001b[14;113H│\u001b[15;1H└───────────────────────"] [35.481731, "o", "───────────────────────────────┘└───────────────────────────────────────────────────────┘\u001b[16;1H┌────────────────────────────────────────────────────Leaf 2─────────────────────────────────────────────────────┐\u001b[17;1H│three!\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[18;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[19;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ "] [35.481771, "o", " \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[20;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[21;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[22;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[23;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[24;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[25;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[26;1H│\u001b[m\u000f\u001b[4"] [35.48181, "o", "0m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[27;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[28;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[29;1H└───────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[30;1H\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[31;1H "] [35.481849, "o", " \u001b[32;1H \u001b[33;1H \u001b[34;1H \u001b[?25l"] [35.483388, "o", "\u001b[?25l\u001b[1;1H\u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\┌────────────────────Log Checkpoint────────────────────┐┌──────────────Witnessed Checkpoint (N=2)───────────────┐\u001b[2;1H│transparency.dev/tessera/example\u001b[2;56H││\u001b[2;113H│\u001b[3;1H│3\u001b[3;56H││\u001b[3;113H│\u001b[4;1H│BkYMlwZNxfP2K8yj8aMVgyfn9JggETENwpGvx6VUL4g=\u001b[4;56H││\u001b[4;113H│\u001b[5;1H│\u001b[5;56H││\u001b[5;113H│\u001b[6;1H│—\u001b[6;4Htransparency.dev/tessera/example\u001b[6;56H││\u001b[6;113H│\u001b[7;1H│rjMOFbMZCBvUgR11SqiWrtRuRuPCE8Y1zH7xRc2VCVQBy/\u001b[7;56H││\u001b[7;113H│\u001b[8;1H│E4JJDCa+1gxiH/SONzmxbNoPkTjH45Y5JWGfa/cq46BQA=\u001b[8;56H││\u001b[8;113H│\u001b[9;1H│\u001b[9;56H││\u001b[9;113H│\u001b[10;1H│\u001b[10;56H││\u001b[10;113H│\u001b[11;1H│\u001b[11;56H││\u001b[11;113H│\u001b[12;1H│\u001b[12;56H││\u001b[12;113H│\u001b[13;1H│\u001b[13;56H││\u001b[13;113H│\u001b[14;1H│\u001b[14;56H││\u001b[14;113H│\u001b[15;1H└───────────────────────"] [35.483465, "o", "───────────────────────────────┘└───────────────────────────────────────────────────────┘\u001b[16;1H┌────────────────────────────────────────────────────Leaf 2─────────────────────────────────────────────────────┐\u001b[17;1H│three!\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[18;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[19;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ "] [35.483521, "o", " \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[20;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[21;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[22;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[23;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[24;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[25;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[26;1H│\u001b[m\u000f\u001b[4"] [35.483566, "o", "0m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[27;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[28;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[29;1H└───────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[30;1H\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[31;1H "] [35.483606, "o", " \u001b[32;1H \u001b[33;1H \u001b[34;1H \u001b[?25l"] [36.855708, "o", "\u001b[?25l\u001b[1;1H\u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\┌────────────────────Log Checkpoint────────────────────┐┌──────────────Witnessed Checkpoint (N=2)───────────────┐\u001b[2;1H│transparency.dev/tessera/example\u001b[2;56H││\u001b[2;113H│\u001b[3;1H│3\u001b[3;56H││\u001b[3;113H│\u001b[4;1H│BkYMlwZNxfP2K8yj8aMVgyfn9JggETENwpGvx6VUL4g=\u001b[4;56H││\u001b[4;113H│\u001b[5;1H│\u001b[5;56H││\u001b[5;113H│\u001b[6;1H│—\u001b[6;4Htransparency.dev/tessera/example\u001b[6;56H││\u001b[6;113H│\u001b[7;1H│rjMOFbMZCBvUgR11SqiWrtRuRuPCE8Y1zH7xRc2VCVQBy/\u001b[7;56H││\u001b[7;113H│\u001b[8;1H│E4JJDCa+1gxiH/SONzmxbNoPkTjH45Y5JWGfa/cq46BQA=\u001b[8;56H││\u001b[8;113H│\u001b[9;1H│\u001b[9;56H││\u001b[9;113H│\u001b[10;1H│\u001b[10;56H││\u001b[10;113H│\u001b[11;1H│\u001b[11;56H││\u001b[11;113H│\u001b[12;1H│\u001b[12;56H││\u001b[12;113H│\u001b[13;1H│\u001b[13;56H││\u001b[13;113H│\u001b[14;1H│\u001b[14;56H││\u001b[14;113H│\u001b[15;1H└───────────────────────"] [36.855769, "o", "───────────────────────────────┘└───────────────────────────────────────────────────────┘\u001b[16;1H┌────────────────────────────────────────────────────Leaf 1─────────────────────────────────────────────────────┐\u001b[17;1H│two!\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[18;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[19;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ "] [36.855887, "o", " \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[20;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[21;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[22;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[23;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[24;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[25;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[26;1H│\u001b[m\u000f\u001b[4"] [36.855937, "o", "0m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[27;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[28;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[29;1H└───────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[30;1H\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[31;1H "] [36.855977, "o", " \u001b[32;1H \u001b[33;1H \u001b[34;1H \u001b[?25l"] [36.857473, "o", "\u001b[?25l\u001b[1;1H\u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\┌────────────────────Log Checkpoint────────────────────┐┌──────────────Witnessed Checkpoint (N=2)───────────────┐\u001b[2;1H│transparency.dev/tessera/example\u001b[2;56H││\u001b[2;113H│\u001b[3;1H│3\u001b[3;56H││\u001b[3;113H│\u001b[4;1H│BkYMlwZNxfP2K8yj8aMVgyfn9JggETENwpGvx6VUL4g=\u001b[4;56H││\u001b[4;113H│\u001b[5;1H│\u001b[5;56H││\u001b[5;113H│\u001b[6;1H│—\u001b[6;4Htransparency.dev/tessera/example\u001b[6;56H││\u001b[6;113H│\u001b[7;1H│rjMOFbMZCBvUgR11SqiWrtRuRuPCE8Y1zH7xRc2VCVQBy/\u001b[7;56H││\u001b[7;113H│\u001b[8;1H│E4JJDCa+1gxiH/SONzmxbNoPkTjH45Y5JWGfa/cq46BQA=\u001b[8;56H││\u001b[8;113H│\u001b[9;1H│\u001b[9;56H││\u001b[9;113H│\u001b[10;1H│\u001b[10;56H││\u001b[10;113H│\u001b[11;1H│\u001b[11;56H││\u001b[11;113H│\u001b[12;1H│\u001b[12;56H││\u001b[12;113H│\u001b[13;1H│\u001b[13;56H││\u001b[13;113H│\u001b[14;1H│\u001b[14;56H││\u001b[14;113H│\u001b[15;1H└───────────────────────"] [36.857489, "o", "───────────────────────────────┘└───────────────────────────────────────────────────────┘\u001b[16;1H┌────────────────────────────────────────────────────Leaf 1─────────────────────────────────────────────────────┐\u001b[17;1H│two!\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[18;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[19;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ "] [36.857502, "o", " \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[20;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[21;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[22;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[23;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[24;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[25;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[26;1H│\u001b[m\u000f\u001b[4"] [36.857514, "o", "0m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[27;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[28;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[29;1H└───────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[30;1H\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[31;1H "] [36.857673, "o", " \u001b[32;1H \u001b[33;1H \u001b[34;1H \u001b[?25l"] [37.626613, "o", "\u001b[?25l\u001b[1;1H\u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\┌────────────────────Log Checkpoint────────────────────┐┌──────────────Witnessed Checkpoint (N=2)───────────────┐\u001b[2;1H│transparency.dev/tessera/example\u001b[2;56H││\u001b[2;113H│\u001b[3;1H│3\u001b[3;56H││\u001b[3;113H│\u001b[4;1H│BkYMlwZNxfP2K8yj8aMVgyfn9JggETENwpGvx6VUL4g=\u001b[4;56H││\u001b[4;113H│\u001b[5;1H│\u001b[5;56H││\u001b[5;113H│\u001b[6;1H│—\u001b[6;4Htransparency.dev/tessera/example\u001b[6;56H││\u001b[6;113H│\u001b[7;1H│rjMOFbMZCBvUgR11SqiWrtRuRuPCE8Y1zH7xRc2VCVQBy/\u001b[7;56H││\u001b[7;113H│\u001b[8;1H│E4JJDCa+1gxiH/SONzmxbNoPkTjH45Y5JWGfa/cq46BQA=\u001b[8;56H││\u001b[8;113H│\u001b[9;1H│\u001b[9;56H││\u001b[9;113H│\u001b[10;1H│\u001b[10;56H││\u001b[10;113H│\u001b[11;1H│\u001b[11;56H││\u001b[11;113H│\u001b[12;1H│\u001b[12;56H││\u001b[12;113H│\u001b[13;1H│\u001b[13;56H││\u001b[13;113H│\u001b[14;1H│\u001b[14;56H││\u001b[14;113H│\u001b[15;1H└───────────────────────"] [37.626664, "o", "───────────────────────────────┘└───────────────────────────────────────────────────────┘\u001b[16;1H┌────────────────────────────────────────────────────Leaf 0─────────────────────────────────────────────────────┐\u001b[17;1H│one!\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[18;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[19;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ "] [37.626734, "o", " \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[20;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[21;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[22;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[23;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[24;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[25;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[26;1H│\u001b[m\u000f\u001b[4"] [37.62675, "o", "0m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[27;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[28;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[29;1H└───────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[30;1H\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[31;1H "] [37.626821, "o", " \u001b[32;1H \u001b[33;1H \u001b[34;1H \u001b[?25l"] [37.627984, "o", "\u001b[?25l\u001b[1;1H\u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\┌────────────────────Log Checkpoint────────────────────┐┌──────────────Witnessed Checkpoint (N=2)───────────────┐\u001b[2;1H│transparency.dev/tessera/example\u001b[2;56H││\u001b[2;113H│\u001b[3;1H│3\u001b[3;56H││\u001b[3;113H│\u001b[4;1H│BkYMlwZNxfP2K8yj8aMVgyfn9JggETENwpGvx6VUL4g=\u001b[4;56H││\u001b[4;113H│\u001b[5;1H│\u001b[5;56H││\u001b[5;113H│\u001b[6;1H│—\u001b[6;4Htransparency.dev/tessera/example\u001b[6;56H││\u001b[6;113H│\u001b[7;1H│rjMOFbMZCBvUgR11SqiWrtRuRuPCE8Y1zH7xRc2VCVQBy/\u001b[7;56H││\u001b[7;113H│\u001b[8;1H│E4JJDCa+1gxiH/SONzmxbNoPkTjH45Y5JWGfa/cq46BQA=\u001b[8;56H││\u001b[8;113H│\u001b[9;1H│\u001b[9;56H││\u001b[9;113H│\u001b[10;1H│\u001b[10;56H││\u001b[10;113H│\u001b[11;1H│\u001b[11;56H││\u001b[11;113H│\u001b[12;1H│\u001b[12;56H││\u001b[12;113H│\u001b[13;1H│\u001b[13;56H││\u001b[13;113H│\u001b[14;1H│\u001b[14;56H││\u001b[14;113H│\u001b[15;1H└───────────────────────"] [37.627999, "o", "───────────────────────────────┘└───────────────────────────────────────────────────────┘\u001b[16;1H┌────────────────────────────────────────────────────Leaf 0─────────────────────────────────────────────────────┐\u001b[17;1H│one!\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[18;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[19;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ "] [37.628009, "o", " \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[20;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[21;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[22;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[23;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[24;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[25;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[26;1H│\u001b[m\u000f\u001b[4"] [37.628017, "o", "0m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[27;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[28;1H│\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[m\u000f\u001b[97;40m\u001b]8;;\u001b\\│\u001b[29;1H└───────────────────────────────────────────────────────────────────────────────────────────────────────────────┘\u001b[30;1H\u001b[m\u000f\u001b[40m\u001b]8;;\u001b\\ \u001b[31;1H "] [37.628061, "o", " \u001b[32;1H \u001b[33;1H \u001b[34;1H \u001b[?25l"] [39.548631, "o", "\u001b[34h\u001b[?25h\u001b[39;49m\u001b[m\u000f\u001b[?1l\u001b>\u001b[H\u001b[J\u001b[?1049l\u001b[?1000l\u001b[?1002l\u001b[?1003l\u001b[?1006l\u001b[?2004l\u001b[?1004l"] [39.553952, "o", "\u001b[1m\u001b[3m%\u001b[23m\u001b[1m\u001b[0m \r \r"] [39.554125, "o", "\u001bk../mysql/docker\u001b\\"] [39.570812, "o", "\u001b]133;D;0\u0007\u001b]1337;RemoteHost=mhutchinson@thinkcentre\u0007\u001b]1337;CurrentDir=/home/mhutchinson/git/tessera/cmd/conformance/mysql/docker\u0007"] [39.579978, "o", "\r\u001b[0m\u001b[23m\u001b[24m\u001b[J\u001b[0m\u001b[49m\u001b[39m\u001b]133;A\u0007\r\n\r\n\u001bM\u001b[0m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[47m\u001b[38;5;232m \u001b[0m\u001b[38;5;232m\u001b[47m\u001b[44m\u001b[37m\u001b[0m\u001b[37m\u001b[44m\u001b[44m\u001b[38;5;254m  \u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255m~\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mgi\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mtessera\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mcm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mc\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[38;5;250mm\u001b[0m\u001b[38;5;250m\u001b[44m\u001b[44m\u001b[38;5;254m/\u001b[1m\u001b[38;5;254m\u001b[44m\u001b[38;5;255mdocker\u001b[0m\u001b[38;5;255m\u001b[44m\u001b[44m\u001b[38;5;254m\u001b[0m\u001b[38;5;254m\u001b[44m\u001b[44m\u001b[38;5;254m \u001b[0m\u001b[38;5;254m\u001b[44m\u001b[43m\u001b[34m\u001b[0m\u001b[34m\u001b[43m\u001b[43m\u001b[30m  \u001b[30m codelab-demo \u001b[30m!3\u001b[0m\u001b[30m\u001b[43m\u001b[43m\u001b[30m \u001b[0m\u001b[30m\u001b[43m\u001b[49m\u001b[33m\u001b[0m\u001b[33m\u001b[49m\u001b[39m\u001b[38;5;244m\r\n\u001b[0m\u001b[38;5;244m\u001b[49m\u001b[39m\u001b[0m\u001b[49m\u001b[38;5;76m❯\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[38;5;76m\u001b[0m\u001b[38;5;76m\u001b[49m\u001b[30m\u001b[0m\u001b[30m\u001b[49m\u001b[39m \u001b[0m\u001b[49m\u001b[39m\u001b]133;B\u0007\u001b[K"] [39.580161, "o", "\u001b[?1h\u001b="] [39.581605, "o", "\u001b[?2004h"] [41.113782, "o", "\u001b[?2004l\r\r\n"] transparency-dev-tessera-3cb22ee/cmd/conformance/demo.gif000066400000000000000000044545261511600621500236230ustar00rootroot00000000000000GIF89a! NETSCAPE2.0!gif.ski!,(*6!",kloKLSk022FH=Ƨʭá),8c')5ʰ코+-0Ѽ˯Z^FĢǨ#$-ȫĢptPӽ羖AD:cgJ&'.Ġθy̳Xз=?8ɩŸ=>FγIL>^ƥ7::ͶusjwzS|UͲ涼pѹ !+e{./1㘘TWC)*3䊏\x~Z^Ϻlmphttw걲HIPRSYkpN12;LO@VW\MNUyz|olbimLŤԾ򡢣QTBd486_bHefk톔a/D,T L[^_eŤ?uC3N*:e#VGԽH*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^;ǐ#C@˘3k̹ϠCMӨS^ͺװ3@۸sͻ Nݒ+_μg<سkν>Nӫ'O#O8}NH(V6z& G߃F|MiV3Tf$!"Pه!]!*h dg>FV v`p[Ȑ`=@FQA `2g@$yXܙi:yFsVhaj6 !4rBP Z Z.@pBa)+Vh1" 6JhvfVA6$p \e[C@8Kmn& rfrZi &P,!@J<ꑏ6Q84!%0l1JCc A8& ЛcnBf8٘A& @>dx6`#` ^p|R 'pP? *A;A3(E-JFlw~~( "@QӟX(GR4gJЂ@@F d"B" W+e` 0bȮxkeD`K``kY:(0^WC @RPoAeJVR<@Amz bbf:,NX h 1@SPՂcB| 2QhGݨr/M(@Ȟ9A q. , Pxi$K\O|+P0(ȀLLKTA:C, N6@8 fԞgy.xi f1Qb6M(C`]GN25cM ,@`k <27|* Ɯ9T*Dӛ847:F@hD@0L~@ /+ e &<:e@..0,WR,huL5,vu(S x@] Kঔo__Jؕ!X[ӖA[ Jv@2@IWخ]2 7ȑe oy/v6=n ;Ynء) n䎂f hpw^\   @B\$  Tm7_C6 mWs 0җnG0KWԕW}Rӱ>oYՙ>v1 !R>i@Feo]8mHpcrnO=!: A< u=s`S@e8@,;t1i3 3jnjQR:j @F@167;BjK@V# SWpT93P(b#(AjHjHg*Pݖe u5p65PA@joNPXփ?TH>%O@oSUoD m :CA`s7Svxp(ZQi$ #=P 7Us v1n W `IOi* *HI?CY 0o[Tx:8I" ݶ@-0Fx0jiRQy*٘7p1" 0% =SƆ.\lٗo_$ǘ'Ww(H5aGAIRpʲ4| B>y-ōؐ[YPyDp$ 7P֌#Zz\v&6ɒ6:!9', M@:Z~/70䳄C$8w71SjBإHj>00;ewYE9kzJc`)Yaӄqanl:VojpęٔٙjWqn5Pngi108,2C30pH9f&)~ D(*Yp%G ymWzݺ !b``> |:pa_ :v^7FZڏS 8aaQ ,*`'ǁ^6 5Z^DZ; ^ $.vZNV7zTFF;۳VS!PQR[ @ @]W[\SRK.0])lp;NKpa0`:M#@SqyK,SrK]Tsږflq Cpk`2i 6!tp+Ṡ{+JJZykȸBu𳍺#KvmCo 0n 69W @c1w3ԅ3GP $;uY^ƈ r fdbr&Dp'L@wK늡 L@Рd0J0HHyۿ۟8K,<LGw쿻W,,)`옮\pS`kU >I>.f, -,k1 =G$@&-:` ' A!+`VZ=ʡ52kgb}lp=`0rg|{-! A|ӤTkP-<&pٯQ&Dҳ&@]ڗڢ}ٙHBڛۗ;pۼ]X0 ۲8@+u`ZۿMmp9ّ܎f&pjڦKA8c8޷8LqICL?GV+gj(0~i}b݈K2 #Q➱+J/,#]㿑e =0m>V#AA 1`m eNP0`+;;Q4\N8 ]Nd^aёjlnn[.r>tf~w|~瀾q^^bNza^s~%Q#!>J~ꨞꪾIa>$~븞N f>^6Nk0 >^0 !>N0 s ߞ>͞ ` .@ ܮ_ێ E!Wa n0@1(a0@%*?4_{as` a6@ba` BLbNR?QTX@aZ^`b?d_fhjlnpr?t_vMax~?|?z?!Qqk_//?$NoOo_O(_؟ڿ?_?_@@ DPB >QD-^ĘQF=~RH%MDRJ-]SL5męSN=}TPEETRM>UTU^ŚUV]~VXe͞EVZmݾW\uśW^}X`… FXbƍ?Ydʕ-_ƜYfΝ=ZhҥMFZj֭][lڵmƝ[n޽}\pōG\r͝?]tխ_Ǟ]vݽ^x͟G^zݿ_|ǟ_~0@$@D0AdA0B 'B /0C 7C?1DG$DOD1EWdE_1FgFo1GwG2H!$H#D2I%dI'2J)J+2K-K/3L1$L3D3M5dM7߄3N9礳N;3O=O?4PA%PCE4QEeQG4RI'RK/4SM7SO?5TQG%TSOE5UUWeUW_5VYgV[o5W]wW_6Xa%XcE6YeeYg6ZiZk6[m[o7\q%\sE7]ue]w߅7^y祷^{7_}_8`&`F8afa8b'b/8c7c?9dG&dOF9eWfe_9fgfo9gwg:h&hF:ifi:jj:kk;l&lF;mfm߆;n离n;oox'xG>ygy矇>z駧z>{{?|'|G?}g}߇?~秿~?`8@ЀD`@6Ё`%8A VЂ`5AvЃaE8BЄ'Da UBЅ/a e8CІ7auCЇ?b8D"шGDbD&6щOb8E*VъWbE.vы_c8F2ьgDcոF6эoc8G:юwcG>яd 9HBҐDd"HF6ґd$%9IJVҒd&5INvғe(E9JRҔDe*UJVҕe,e9KZҖe.uK^җf09LbӘDf2Lf6әτf49MjVӚf6Mnvӛg89NrӜDg:չNvӝg<9OzӞg>O~ӟh@:PԠEhBP6ԡhD%:QVԢhF5QvԣiHE:RԤ'EiJURԥ/iLe:SԦ7iNuSԧ?J@!-, ;(*6!",kFH=훡cklpĢKLSĢӽĠ@C:bfJ‿sy[_GƧXJL>Y\E,.:=?8˰dƥhyz~ptPɩʬ24?tfRUCEFOABLγ{/1268BȫUXCˮCF;y|Shioj񇎔^694xTU]ͲлuMO@ѹ:<7')3txRտзb춽pvx}q鈍[8:Bȍ]bcj셌tu{>?H_cH{Uζ呖_PQY䰱$%-۬korwQZ[b,-1Zꊐ\_`gm~;=GeflHJSkoN}o\^e߼LNVehKmrOc̯{}y~V|ϵgkL̲𠡢notGJ=rsx_~ !+Ɣa滼i145zTjnM]ƨ\`G/D,LT [SSY;j!C3N*VGA{뻳żH*\ȰÇ#JHŋ3jȱǏIɓ(S\ɲ˗&AʜI͛8kϟ@ Jс;*]ʴiʣPJ:u'իXjʵE`ÊӫٳhZ) h.J 9n#r߿L0+ Ӣhr:%Ǒ !@D.YѴgӨ^zlװ#}8lk)^k僻nES5[K뷉IEgX*TNӐ&%@v]@p&qҝ`wBp1 AIB %иezr%P!xB~Ls'x1LpץPO(3m$d ܢ%|( Bge4b0?P1 .@q `(4?b D+ls 4p sS$6F褔DCR#"@nAS ŕY27P$0ت@`0CXh&P1Aט0RAf ItXbkCc |%aGrL"'*\ + T.AD g5b.Az > o:dӚ଍7-L:ϙ~ @JЂHBІ:D'JъZhE#Uz HGJҒ(MJWҖ#]LgJӚ8ͩMwSv@ PJԢHM?5Ԧ:PT )ZXͪVի` XwjΞShg 1i\JפNxͫ^Ъv `ձ%KzֺZ,X:pgR6se3[ Em>UKo#,diH2D!uF+\ ` A~]4A K+RTzm AHAv(G:?8F:&ZB%ЉhabӺ`nP68>҂!Q08&-P1aq>e\.0x0\\f'(NO葓,BD ZP 'F-=T#pb&) ގV ;Z9{4"]3JqPy3= :>tbD%aC'z *7æ4(Hq x$L2$1w&D,Y8j:>#rL`?g "v킗# e)#Vf*ҐP|ͮJO hsKe܁|n|sK]>'5? ~‡NSb@9@;$:m\-ЃE` lDHRPp VT90m0[=NO7  N;[j&O`zwHYIwk4Zt Pe Ap ON``)NAv4z*kڦzNfNN@a  lZ OZ4 .}P=Yf Ԓ> O: c p%p{/dUʨȦ}bQ0 P@ p\i PN 9m g!0 U:Eeʯ)-0u : WPQNuV*+/ (1RKfpdOP !IoQq `o>[@+uwִOQ~ ! b=HA[2p 0 wKknG;?%RP|a K R'@uOo  56`;+Up 3D PpDKQC_۹` 4P >0P.X(TuN@ PTaNc:OOOkds+Xkj V/`N!F&Mp/T T{0j۽NNcЪt hN ,O,#K' = d =*9*{ O*cFTAv[dkul|蚮P6 0aP^P!dP e  E}kg %PjvPYIapr9%rh,[3]; VOջӋJ@B-D}O=N1`[`B|<׵:Ϡ =N dzuJd?NUf BV-vyDPH`o\ة<{P Pn Z(ڐQÞP>Ǟ 5݅B0PU`.b=U\~=Hj[ 0 w`'>Xe0n{V%T4pNSPEsN=J*.NPрN1?D 7NPbXHSJT` J Oc 0 hdNK`<Cuqw 0ei`ob?tkM!0{O [_mTwg: VN(w^Xw/WOȫlrku .p.gN߀ M}bɆs/0Q=j S)d  đ?磍 P ܆ ͥKI we`i, С QDEK<@QD8 Hs'.$DȀM DVpTY3"EET)M6]piz`AS$sS ^j⧒6-jZC\:i]Ң)2Ma(CR|(m7dȰ|*V-۬[-)]Ϗq. \p 5MSNH9Z@.C6m;%\r;Sv`2EJrS,Zhr(~-~=W,rM ޕƅyThň!0RJ@ AdAlБ^ -n@EN@C -%|Ё 0  P` 1 D0XF1x1Fgxf@ (R4E)"(IPHS92H RA*RԒH# n" FrFX`sBߌs6EPЌQБT Ѕo$@L|z\ pH:\$0(`e0VaPm@1_-'XcEVZ$iJUE08M4Y*.(z)%p?䠄DpJ@lݨo$PR)i6,IJ c 8⥎j 9ၴ@C' 1)< +ls. la̱2 0/2 *AG.^yfMCKBXWn l0ΕIn7&s;)F$9*A"o^Ip&G`"I$e:7Pb8aG'v\-l_t&5v`\wq}ku,bC]ݘ>O^ OPE>BIDd"HF6ґ4b$CJVҒt/@ e(E).ҔD%XJ2E0 WҖe.HpvF u9=ҘDf2HI6(f4-I'Lf SMnvSd9NrӜDg:)eӝ'3lӞg>M~b+9PԠEP6>%:QVhFQiHE:RԤ'EiJURԥ/iLe:SԦ7%GqSԧ?jP:TըGEjRIS6թOjT:UVժWŪĘUvի_kX:VU)BVdVխok\:WCBĶJWկl`;أZ X6ֱlde}`Ud5Yvֳ]AEmjUZt}RLZֶmn`X6np;\wͫo;W\6׹υ.RaY.7nv]Hu;^׼=<Н׽o|QKw$o~_Fvp<`Fp,T/pM` WNp@! , 8(*6br[i_nP]S`E`WeHSu\kBKhAIf37KS^VdLW{`pDMl7GbNZ>Fa9?W5;QVc!",NZ15HCKj5:O+/=JUx\kJTw15G]lMX}KVz`o:@X@He8=TVbJSvDNm16IZh6;P]l^m`pTP\Xe9?X/4EYgMY~,B=P{FOn49N9>U7=T_o.1A03E-1AUcQ^*-:48MGQr=D`6;RP[;C\CLj,.>DLk.2D27I/4FO\MW}?HdKVxTb^m,0?*9:(.74oL,E>)*0')5NY~CLiR_-I?.NADgLu),;7~P2dG7zOJq?_MvOy3iKKsIo/TCGl@a;W&'.#$-SVC]^eKLS}nefmACLҩz~T9;6-/:SU\a9:CGHQgkL68Bz|ρ=?GNOWWlpN^796OR@575KT ZH*<li#JHŋ3jȱǏ CIɓ(S\ɲ˗!$(s&Ag6sɳϟ@ JѣH*]ʴӧPJJ*7F#(Kլͽ8NaCbh۷pʝK]["%0l!@hF4|¶ޝL˘3k6 xZ "6X"_̺װc˞=)i8g N8ƓTP ni8;ct؂+νwȿ8oM< N_P{ϿA d&ـ IVPWXVhB|֙@0gTVA.E8W(4FD7nF7_`UBLDiU56PF1J$$UJ\v`)dihlp)tix|矀*蠄jPu~MDhDsQfґ1:Eqj[n▪ /RUʔ9TPt:A.BRjjP3jW+UA˔iS.؂I-)h$@KnBЭEdIFPH*R髉A jBS K KEpqA*SC#$NB T@ :P-*r69(d*&+ 9==IC2KDs2(9mQ]"Il*1fvAlD63x M3:wAA+R)M=Q)9BqΟ5@ DC ^xF'OD).<%/38/ o~)^uJc?KmD mD|-n<-<($| !c #R|IL dC8h3<&ث}IS K .G#aa8D'>SX 9OԊ 9} %Dp*HQHuEVvUlS"-FԈFcfHVxDOBe !u)ׄ } `KMb:)d%+Zͬf7zhGKڷxvliWųk0Z" I P-g f޺bJY70a mB,8J^;h@0f-lsN%vUr@3*Pq6(^b@$lk!@ +5LA$) ^(@ 0hT  $ +PZ -9F6s, w!qT* B'y-Ѐ*D y@ np @ )3A@,b@4@`$PXֲlf4 19{Z TJF@:`"d?ȉK>GD*(@ l@DAP WW I@.@|`0@ ]Ȩt @* pp' A@ 66 q b;n @"Bpk^ ζ@8H@ 7`:[ǶlgC;^p"O@(nq5oz*g &7m (@|KM<XHc Ae+غ;P@ 70la"6BB X@ :;nT} A?!jv D|;8@ &@@2'f(BA2yқNt}Ŭpdʖ|AzC]Uȑ, .XV Y5 t_- )Q]PF@(2m A%7Wm (glW& AsN~8z* _ 6@G.(odږb#X05Ȃ؁p)8{A8/k._89yQS}h'eU`YHcU$$@f7uy 20`t? b cPx`M`m рᇀ((8؉(Xac s[0vXш0(qH|`VsXs3HXЌ(p{x3X0miprje PYQ`kp{W]c@*(Qq ))`|@o8@ i% iI60 11Qh74)6Ɋވ-)0yhxY#@Ȍ5i`g x )^'lp+yQpia|]4 !4Fl$@4 &΁~$_ r)y1I)| A1EPo7AFøYI|dO E9)E6PєIp陪WG')_tK0~^)E=fk6; ,0\u "0r@~?0&a&@Q:n2XsPx1@O]0v$ٟ)Z,pJ ʈb !Iu Ix:I8 ڛٌ 1*h4:9)!YCIg# 0gН0*b&UEY0 5Sybz!uNp1wrJ{ڧzW`ojv Q-:|z#cprZh^E1O_\Wj ZdJZFFi >="05`Wgʚ&Ҭz'*]ڭZ&!, 8(*6br!",@B@*-<69:-.7XgfjPP],0@27KWe]lv]haqx|WuzWzr|koRBE?Tz:@XS`]k49Np``dLNZ.2DZILB`pj|X9<=bt)+9ꅊ_QTF~~^aL~EH?z~X>A>u`dNotTinR_bMjJTw㬲kĢͱFJA/12JKSCLj8:?c6;Qз,-0ʬklpEOoZ^EǨӽ@AJ[i02:&'.>E`puPLX|#$-]^eHSuxy}Ӽ<@715HdflݘbwhVXCǥtuzTV]354jnMqMO@PQY]{|sLT 2L*?uCGH*\Ȱ>#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ)! ѣH*]ʴӧP7~XHdUYʵׯ`ÊKk&^5aٷpʝK]^fp)S LÈ )Y.ѴWQ]"CWԴoO? O5`nL ?dP E&&œdխ'H ZH6 aGHޤ M$( CI zpġHD":cED",C P`YPD@p#Fǃ | M 'BDž D'B@x#hA Pȑ"cGPĔ4*RD@ H@4@F"`Q@8đ,q@.a 1&ęf@ HT|@ RH')'Fysfĝ;#JSlM $ `P+@㼧qԓ! !|Z4? 2TpE`+&VCJT5$YATRДiAjT@UO+OdB XUj ,u\IEl!_])SSN$AMkxt ܼUX"İAdZT*V"UddJd ,@T JCZbZN["ĵEYdg++bnc[&%Oow;Eq$w͍t !, 8(*6brnn!",`c~ELQ6@P]@1;`bL4>[i-+715G),9VcXg(,6ijh=FmmacRWb;DhiTXRWIOjkhi]:C16I8.9@HeU/3C_n;AZMY~S^]l?Gc`o.1A\jaq,0?FHO]`P6?SXghvBJdeiip@HllY\QV^;DkEGn[-.1𩪪\]dKVzuKLS淾qST\'(/ghnsX89BҼelpNRUC:<=+,/|UyƥstyJTwy|TEFO !+agσbcjMNVfjK12qYkZ Ai* D.GZt@$6NBL5&rcP:d 6PQѿ/&LŮp?YQ;YTH% +,2 %yLqF$EHF欔3TJ\oTt!ܑҬ++VAР+2S6m0ȐL34 w=)[Bp-TIN2M'H^m0Vj)F^ڨ+emE|i;RV^IQO ˢ1l…ZANT}F4=zrK>& `8@b@hRyt `0p@(E(PNJR$,,@8@d "*xCL'R@%h5lj 8"NU\BC@g8# fH(fP2y DaHIV^Ţ( W@P3 r!L H 8@"`$75HNQT&;ըP4ȅ<H"P$# ! C)DQXGNĬA+KȪ'qkLIVDB2[@`c!2A H#IJx ` P|8fIgh!lTҀ.x#xtD \7ĸ @ .Wu@@0@TAK-H jh)I7)@AIG,kW%s$5H~_r_07%n/J_7^O0QP>m)r^)@L1&>7ۘE16Ȍoo({DLd ;.!,+(*6brnnac7G;/:zCK2,8C2=D2E`WeBKhENoAIf*-:\jaqWd15H39LDMl5Fa8?VCKj5;QVc`cJTwNZ@He~ELKVz:@X5:OJTx]l\k`o/4E8=T-1A28KJSv6;PVc]lDNmT9?X,/>;B[@GcGQq,0?.1BP\TaXe)*0FOnMY~9>U@1;49N7=T.1AYg48M.2D),;5-8Uc_o/4F-+7;C\8.9GQr]:C-1B6;RQ^P[TXhihiRW.+7h>F?Hd,.=B10;l>F,.6S7@T7@c`ګ»l,[!,1(*6br,B=P{6yPLt]KsJqJrOzMv>\1[E/QB-K@2bH5qM,E>1\F5tN4jK?^EjLtHnNyDgKrBdIo@a8R?_Ip:V*:;'*5,C>8}Q+<<)282_G/TC,A<*69.NA.OB7~Q/SC8R)592`G2aHHlNw=[CfAcJq9SBc@`=[˷߿xd|n)aA@HȬ "& @Gr̝S^ͺ:oETtELDAdPc*(Qb\]KNjOw8o#"\O,;`0WgϿ?=3h = @т Q95z `i (b/x P7` =AU( A w_.#&䅒L6i7%1<N5= ,gQC"(a$H:)ty(`H=C\M&JF*館9!fY'*x ꪼ *!,L1}(*6br-1BKVzaq8=T]l+.=28K;C[')6.2D7=T:@XQ^R_R_Yg[jJUx,B=P{6yPLt]KsJqJrOzMv>\1[E/QB-K@2bH5qM,E>*:;1\F5tN4jK?^EjLtHnNyDgKrBdIo@a8R?_Ip:V,C>8}Q+<<)282_G/TC,A<*69*7:(,6.NA.OB7~Q/SC8R)592`G2aHHlNw=[CfAcJq9SBc@`=[GbJTw16HDMl68C+.=/3CZiVc]lS`7>T>FaGQr>E`bcjEFPAIf15GLW{:AZ:BZ,/>\jCKj?HdMW|aq_n38LNY~5:PYf抋{}hipJKT_`gXe`p/3E_oP\49NAJh,.99?W),;),76{PKsHn@`*9:Dh4mK.OA1]F2dI2bHH aC"Jŋ3jȱǏ CIɓ(S\ɲ˗0cʜIA8!Nhϟ@ JѣH*]4$N:wFlJիXjʵׯ.><("؂$BH@ ,ɶnʝKݻ>2*ؤ8F+^̸zwMZ~|q3ϠCM:!NxA6#C[ t|pq@'(" >] s/,}lmɣ,{o_(y=ϿG<$QHa:F2ԐE9p! EԀ `P@*T0$o,zu@ #[f ved|%}LwXf\ s1,@!@yfZ}(Ќ5@@i埒Y@MY)-{i^iqd_v꫰vtI4 7 DEi @ ȐwQb=n.4&aCf]dCJW`nꦤ gZتj'%zT֛z l81@CP*!@$p <ֶYc2ıep C>H:+j꽨j%mI_4TWmK  O@Cp,p'@o4V6CF@ P(@DۢҟJ4=И2no>.DK*֜wXRp 0AQ0–1?AșZ- %d+k!Э04 tϠ.*yynn/W@Xp8hzEoB @NYkXH@y|@2=}<` * ^BCR/U醥@ "¡0>gҗ@FZ bV 8'F S*}9%aFrPHGWEf2ybPDhu ;e*; A~H!q$$'Iɸ ̤&/YT (?gHtRL*WJ, l,gIZ$ ! ,1(*6br9>U\^exy~kmsIKTȞyz~qsxEGPlnttu{27I^n.1Ahipbdj]lF`GPqDLk?Gc*.9,A=P\P{Ks:V6yP*8:Dh,G?^aqBdS`@a3iK=[2aG2dILuNZ.QB1[EJr'+67=T/3DHRt<>H'*59S{}ӓ`ahFGQ񧧩Z[bijpOPY_`gˡݲΑGu篠*\HH( 0`AQà xR()nʁbJ0cʜIf620!DP F @|2 p Z6jʵa: 4C za d" ׻x˷ 2p0X䀋JAvLˠB  (8:&!SE41$۸shѤM0 y:ѥV{Уm'*J3УK.Ÿ%0k6˟O?EL:aE;pDp `!6R'OX J3\U߄Vh!/LH~h@B #Ds(⊃h4,(4b]0֨<@@>iHI6P8QVi!,&1(*6brΝ_o\^e,.9JKTFGQz{02=DFO46A.016I\jLW{`p_n/4E*,:]lWe)+927I.1A5;QQ^>GbJSv>E`ISuGQrENn.1BO\27Jxy~68C/3CkmsNZJTwAIf:BZDMl>F`:AZS`ģIKT=D^HRship/3DbckMW|BKjVc]lP\`p{}񵶶Z[b15HbdjFOntu{7>TMY~9?W9>U(.6?Hdlnt@He=?I,.=38L49N49MghoqsxabiDLkrsy03EKVx),;:Uc`pP[Xe_o15HNY~7=S8=T),9R_YfWdWe`oJUxGQr?FbHRsJSvBKjFOn.1B\^e/3CQ]kmsNZJTwENn:BZDMl>F`FGQZiIKT>E`15G/3DLW{MW|16HDFO46A;AZ.0<]lP\浶ȚZ[bbdjtu{7>TMY~9?W(.6?HdlntAIf,.=49MghoqsxprwQR[DLk03EKVx),;*,:*,8EGPAJhopv_`gOPYyz~R^Zh}~Vc^m^mYgS`Ua\j_o҃ۍ㹹Ѐ蛜ˬ͟י(P(\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0-$f̛8sɳϟ@ JQ3DxӧPJJիX&-`֯`ÊKٳf۷pʝKUl˷߿ m:#˘3k@-0X[NJ+D̺װc 0 \腱4( No** EC&LPسǎ0% 8"MJHĉ˟Od 0`4W߀h-  AЃ!!j@v\!x`ZEp 1E7Lۇ4h㍃ Apu6Di}L6W)TVi9 Z`)FR6h f6p!,f1c(*6br,.:VX`*,8P]xy~kmsIKTtu{EGP{}HJSRT\LNW[\cuv{egnefmєŠ15G')5),916IZh=D^)+9MY~8>UGbHSu@HdEOoAIfGQrFOn\^e,.968Cz{JKThip02=㻼񕖙Țbdjlnt=?IqsxprwabiQR[rsybck:^ ɗS^f"0 Fcs۟>~ Nooœ+_|׀СoNMνw~O@!,s1V(*6br򉊍@BK68Cꊋ=?IQR[EGPFGQ.0<ϣMOXnouKLU57Bٜܐegnabibckbdj[]d,.9z{JKT,.:VX`xy~02=*,8{}╖IKTHJSRT\tu{\^eLNWhipprwrsykms[\c:TȬ˗0cʜY&L@@h JQ ͎JJUj N(wׯ`Þ"~˶۷TR( D!DP߿Daم+^̸| KL24ȉ-k̹3C깴Ө)anזWMm!,T1u(*6brVc:BZ]l>FaWe=D`)+9+/=;C\.2C.1BKUyZh]l`p[j\k_n`o=E_IRt68C,.9Ă=?I,.:VX`xy~abi@BKbck*,8egnEGP{}ܞϑIKTHJSz{JKTbdjRT\tu{LNWMOXnouhipQS[rsy02=kms[]d[\cKLU:U68C*,8@BKKLU8:DJKTvx}13?;=F46Aڮ㧨.1A^n39LF`@HeAIfGQr@GcGPpAJfCKiꂃ,.:VX`egnEGPIKTHJSbdjRT\tu{LNW<>Hnouxy~aciQS[bckkms[]d[\cuv{efm57BFGQ.0<{}ĔЁΠƾ؅Ɍˍ! qĉJHŋ7!8 ء"F2\ɲ˗cH<ϟ@ DA (P=JJUG\QQKY]2YȐuݻx$ `x`\˸+to @FDȠC-D$U, MmxZ/ͻȓNУKbM}S/0! ,1)(*6brܻΚJKTQR[46AFGQ.0Hnouvx}aci13?bck[]d;=FegnEGP57Bڮ㧨Ѐ܏p8Frl:ШtJZجvzxL.znT~hrDtGEB% L ~B!ۉC'$tEM31d}u*\ȰC7 !3jQɴ `pb(S\-2 cDz͛8[M3@aA9L4s J(n]ʴ3"HիX6Tׯ`qrٳ E˶vA!,1-(*6brZiYg`p+.<7=TXg`oz{05GMY~8?VNY~F`ITvMOX79CJKT*,846AܝΚ8:D\^e,.9vx}prwQR[@BK13?02=KLU;=FDFOFGQopv.0<ۮ㧨߀ٛĕ@pHcq(h:˨tJZجvzxL.znx8>~ftHvwPJQwNM̖v ݿ*VϞrUK&M$ɣ"." pI}#JHb.q ,!d& ZIɓ|C "QʜI(*p !AY1o J軜L0`DPPJ &lxBׯ`]ԨIسhӪc@YkʝK7%o˷/ضy LX(ළ+^.!0,1(*6ABL8:D@BKCENmnt,.:79C}~DFOFGQ.0<拉bdjRT\LNWghoGIRxy~kmsKMV̍櫬׹MOXz{JKT*,846AܝΚ\^e,.9vx}prwQR[13?02=KLU;=FVW_opvyz~ۮ㧨߀ٛĕp8Frl:ШtJZجvzxL.zn8~_qDsGvq IqnèӾ#ˮ BܼBs T*\L0ŋ36 AD(vHɓAAPʜI! f2@*3ѣH*ҧPJUtWXjEY֯`ZTٳh!,'(*6򹺹79C,B=ABL\]dP{FGQ,.9MOXIKT-H?Lt6yP(.5|}xy~abj?AK*8:z{]7zPVX`3iKOzEj>\MvJqNyAc4oLijp4jK[M]:VCBdR *:;1[E=[A{9S=oJCeV:e#ZGFjH*\ȰÇ#JHŋ3jȱǏ CV@ɓ(SD)˗0cʜIqJ8sɳϟ@ ehѕC*]TQMJJիX>z4ׯ`ir` k@ lʝKػ)WlA,MjBŐ#Knrɘ3G<ǿl rሉ5lXװ5Z\?8ă*81,PJ8hS…%`t0JF7tXH}BdD̀ Ÿ|3 $F `B0 5_MBP`c(∝͆(nX% |A +@: AZy$2+H;Xk%4D@p !x@)p?1 1 p !yfh,?  p  DDx1 (|x"[l %BqatuX xH9PbhQAZ2p4-SEQjVBC sB e8!д^I@%<RjknDhP`,UyVj@B Ѕ@6A $)A \ 2w(CMKPE22FFD^s0l@801C4așLZiCS!Ё@tclt0F7ڶm [B m呃@{ r,@|;9> GAG @B3{sZ@(u.$ @0 ".FBP`@mm{ks7m2 @?pR@4<$MxAd@ĠpWPB&$R`` C@J°HGz tp%$1&% Nt? @ X" &l@ cR1($RB 1gЀP-L$DwFđQ IM2Ӟ/^XilQէHMjBv:% Q >IeQHN` kNZu"Q=Z:7ڄp][*ڵsͫ^gϽ `KMb2wudwZͬf7z Q kW^MjWֺmBeQ JHW 3pKMngXc[ݾ`-o;WVͮvzTsLs螄nk|+YyH{LR󤇼o @jp,A!p2@@/IւL n/3ep6VH(NWpS2d%Ң @& ŨB~ЇOz*0AL~^vd3mL29Ek$Ϛnp <$V|a.кEh|3[ m[C_̘δ7idIV%f\~  QZ g|ɔFʣ\F7^F@R\CьL'lwЎ ey+Ib9@ b;N |&m삝Du-le_quw6} ;NpP Ge.06T%Pwo];񞷽ywɥli0Y;BRik[)ŊmQc!zʡ>i|PO>{`W;9O`ǼL/̠ pdpӛEG.l :.{ ;W@lŢ%OJ@+?69zРowIȻycL_o?fO|_anO??ѯi_O! ,!K(*679CABLMOXࣣwx~ҊQR[ꫬabi@BK8:Degn^_fCENbckjkqIKTGIRrsybdjZ[b=?I13?35@LNWPQZHJS57B\Oz、OPYծՑ{}FGQӂtu{KLUxy~?AJYZbstz::TTHp=8R_`g~ijp`ahVX`愅׀åG@`JBcD=[y˰nhip02=63I21Cqsxcdluv{:==}~؈m~ftaRVJtձu`n[uw|wa//@Ǔ톙')5XLvPFkfU96NlZ~cTMQEPSGz~..>MwN]KQ8(@j@@( h TaPPI[ ⊈>qPЃq#HPiQ\D:hH&SUAPE&@C8#<%biPeey@CaV4t䢁=Y|NǝE!і5X -B@= _T]%駠 ;%HAHBjR+k&6F+Vkf-tmۆ+z妫x+tJ0= @*9AB N<\gd@qkjTa| _ƣH)`qxXD-`7 &,L |la f5]mhJ  - ݲ "--^z@D|Ѕ^`z wTD p@&PAA8p`砋ꬷTl6wzKw` t;@lQ#V@3/0(A/o(\#P`AQP@9:TYoN'-m]rtp  g [.@H7Z"2 -dxZN` d@PB~8̡U` D@L;b:PHA7CO;3<`) K 2#=_H"D%-HN֠)VT P@0bJ)4 -%Ѐ((%@5Lpbp@@Y6UbU+ Xր/diK(}Fߵ0Ё^{5* kGb6vYNc(V"H@ T s 2zu=!\ jWڗl#u-]s pKMn Z@&8%&yQE2"׉dw%܍H1RD1Ib/*Ir (א*Bx#G2¡` H4 $Y.rqJD+0DD` >C$<#W@A .HfF/ 8(዇ ` ZҌf],!) Jah"UNɕ"Qx%H̑H@h&P$Hl6*@ %dq0[rA{8̅`3@y` ԙ6!$H#(@ U6A{7@ XXc/}fEfZG]CDӜCL5vЍ.`[qFP#!VX@G0JьvE`Qӟ"f?^t-WANrA.͹1 P#"{&.Da\Lc$unOld m9:@*Ӭs[P x+9`„$7@-Df2~y%;T\ @Y-ta"PD@bKu@o :G 'IO ^,fq)!'x8:@tf:  Q "a2X 遈`BֻA R:gݛQ@9`2٤h _$ ۙ/^|̠IQ0`@!!r#G 6a`#@U0O:KA~d& (fPa@- P2g aFÁ@`GA&g <$%>78}7C` 0}$dRT G!HAC8G}QXE"fFjDxp r 3a0 Wdd b (#   #PW 0! m3x>^n@FJ@X(CA}]?`MMN}wz(8hcȄsJDla 0P H ~9 Fba` /P} lp6K#8@R0&4Q J 91  IF*$DJKiaȑ& *wLU<N$H@  evXڰ y @h gRɶ#/+bT0U 838ac%@T&q9%8@0Ua9~ 㘦kRPXPPeO0|dig( m9 y IB)vuQII!`p Pr>0 0 (Uc0-U  &Ri12N YV0Wf2SB`B 0j詞Z%)c鹞XI@;S_Є )1О Qs!F%z) 3PEuTIUbTP%UTT` p 9zdA& !  3`r{Ֆ%Eoz?_ mzrjz) Ww ZWwEUGq QpjeY\$ +uZs:\Ъڪg:Z:ګz::Śʪʫ ͚Z֊*׺ڭ&Zz꺮 jZڭZگ[[ ۰[! ,!r(*6P{79CRT\MOX܉xy~rsyEGPܯ,B=bckLNW\^eYZbIKTabitu{kmsϞHJSABL:U=?IPQZ_`g꽽Ĥ;XjkqQR[{}GIRiW24@nou?AJ-H?Gl2aGHo1ZF=9St__P}cSuv{MxLu.MA5qM5tN̐؅紌Ѵefm*9:57BKsopv3iKIoHm1\FJr8R/QB.OBCfKr4lK8SAbZ[bp\./=UV_+>D=Z;>>)59}~HAaWKuJBc11C^_f7}Q;WOzEi?^BdtNy\J f҆+.=.qP%p %49Z.Ox 9k;Ӳ 18M|" Gw ,$l(,0,4l'i;A҇{H-\*T @O0SLxT  >d(}*<VksAݔpZ)3]8SJ>w+@3o 9AI QMQL@R Li#d3/x[b>P5i ـ˜@42 pF좘|FU?^3+^JРGV %P8P'4F6T '~ 'vǍ#" Dc}cڋּTaKܲFA%$5 DM z@@8 D+(@'xE1n1[,[= Nz̢ĕ ABXxt l@TYܢ&=аp\<:xciMdQy 8s@A8*J!b$#G GcsRG0-Ǎ8DJ"!)xxb\@f5# 7)b(&+=hJ :RDg$3N82Zt:$ꒉEd &@!QdA7)M|(&29=T  H_/BMHaB# ы@"iRYxz/(ĔHaS- }D*WbzPAJXcivB z!5!т !@Us#-U V\ 't#2AMed5YT`Q5eW*_KMbklZKZN,f7:G2@qԣ!,8 B1Ixhp lt$ǘCf]!>/,J@%ngV"I f&27#5w1Q8  $tPg1Z0!/hA 0X pO00Iz ` ~1b H+a Vpஇl! y`V@NF!FP (!R) GNr.$E8 q2Leid}&L52dP`h? nR|"̠^ΠXk r|,cv3sMgu_&Hd 1@ bW#G62 E  >@ `5x@ q(j9@ $PT@"+@ _IFz $+o)nqD np/D_ZY`sDB f +A!4 X1 z r\ /ov{pW +~0Cv *Afq3ws"  c[(@Zl& dn8AL@ Hps!}w!8 7B| q=@KxB|4)` c!X@vD`  V܃ >A|Dp7^@ x) Hݖ` ΀{-`1R'v$*aqk" p)x+p70|Gǃ>B=[PW7MQ?CP/3XRHD KBG N1zQ(F= d(@`~`Z@k U2H6\.X h!/׀@p a0GсW\ 0PЉ Ђ&2X cg8o P p @7dzhبpwAq߈ٸqɸˠ:e8|P\p)xYg^,'` KzhȨ|܈x: 'Hr 'А mq p JwRAg&MF8"Ipp.) Дu1 (nii`)fXyg9 dJh)y  ! (` A`1Nc{dY鈕ayIؒ Lǀ6(pM M\aT7 !/l3 HW$P)a6W qٙhKӧbII`i 0))Yי1In9 )\:bM:vpu5$a P)ٌʒ)0gpXY(apPZ)  W 05dЌ 0P@ Npn( Q)  @)Tjjʦ `&pyFkڦgZʥYY99zʧwu0IpV r 0`J (HSZWgj a:eQ9 bk-z(Zp 85z8v`` YE#`grzeڮ  ^!:皮A`jAp]EP*!!x" W9J5F#,pQPTb/C9qpBbrdP*@;DB[H%5J۴NPR;T[V{ZZ8 wPE0k[BC4F4X+&mU]ו]u N7e^5Z5^6}Pk#``pva#3A7C x~Nc2Fc 0c{U?dCVdlcĶkkf^fHlgfgOkugg>g&ʫf̋hkqeO$BFd]ŻV0J0ey+hiFih/8Pv6h6ibiiwi6jq99H3:s:::3;SQ;#זm- mn6A<novov#w<7wGp(Ā7s&gsWw:FvPr5r*t/24wrtsw="$nFCqV_ $q elwt tD'KhG! ko,Q!Ȅ\tqtttauSWu ==}>C>>>#?#?+#x,'Wy@Vyw,@zxz'8;'ͥ| Zܬ{~a1,zwzbz6 Ϝb7Wwӌ\i}ا}WW|II'CD,$.D`BBB) C1Cs:bH8=(9C=h"`+H]8``J[h#XȄshSH,IDXȄ~8u@eiM݉(tԎbziG⅔x*xq]^чcXl؆octgO=VE4E\E`4T{FmkBnT6# Q۩hϪutGyhыJLhf*#Y(ꨐX(o%Iq8qܼ񍒊J ύI(M1yཐА)qH#LI$IdIҌH$$۳}#89;-< BIJJ͕R)nM\ɖcIe _ɗii}F>Zsɔ@(?n$= n[~e!(*xj闀)[K^dLp7M+^#Y7 M q )I͙Rٞ9 )nѡI (f$Nm`')(s۞1ٟ)0*1OtOPOP5T`P P %>#/3J: 2:> A%EzIpLP *Xz] 0*d:-vzszJpj1߆)jM%z!@GY*.vf{Z0OW'ZZ/S3︑SL%C?T_TGTtSTP%U #ڬM6 ڭ3  6Ʈ*b+:p*A1&hreLo_u5@A!ر#[)/32K%{_؟ڿL102?0(SO2%  11/!@ DPB >QD-^ĘQF=~RH%MDRJ-]THP&L5męSN=}TP e,TRM>UTNUV]~VlԫǞEVZmݾYV\uśWޝrX`…5Xbƍ?dʕ-_UrfΝ=fѥMF2iխ]{l@! , "[(*6*,8(.6?AJ79C::T.0<(,635:嬆02=miWbR=9S74JE>\00AžqKBdv=?I}хk|Εw..=13:P{ꉊXLv}{dMOXxy~NEiHJSabiYZb+-6PQZ|~,B=+,;HAaJKT13?jkqrsytu{ゃCENQR[RT\̤VW_\^e@BK-H?kms2bHMxDžz{/SCLvGIR.NADh46A:UAb>\)58KtHm6yPq]GlTIqCg4nK_`gIpJr+>Ej')57}QA;W9TKMVeTNy:Vy;>>FjjX0F,[g96N'+6IBbL-193K~{JËOӫ_Ͼ˟OϿ(h& 6^@ lp@v0 0!(AP*h8@@Y_]\ PׁDaXfe)D*lihZfdp)t%MX@Q`A*(UTB*HP@`F*RHHR駠d@0j ꪬ꫰*무j뭸뮼+k"{'$D7<{*V3Sv-ac8ͷ-.W) f4=tM;W^!dAAc)/BV 9BR nEpTq+!*eCWtANtrA|+i;II$"f ]3Ѹy*gP1 2j/rNsAc'B^$pA1 59@yGˉAdѝg "_m8fm8Fc{7G.WngwҶC?Z!APv(@ ,!J%@DhOAl4DB`ф+@G/OP ͐CN0=I1TD ]y Ո>F듴:KWM$4^kFF? hP`^=XGbDv qK$M$PA"{ `@!S 8lЈ 2(`1<:H0lIbBAC;$҈ `r' gI 22Α z:'GH5"Dg*YE$IDG(@H4k6$ED&|^ANic 4Bw@0\eERy]G_L²FH^đ4)2͇8%kDx~XBiJL1`va nH3r,,H>s/"_YT{ <*HOB's~hF7юz HGJҒ(MJWj*Y,eP%2Ӟ䮐?(Yx<{J^Qz^BZEL 3ύ$$UM6ըNC0UH'[#H$7$ͭ*jH/^Jhݔ A2ĂD~%dei$LAH)Ijbĕ51KfX "Hl1!5;&܃ 5U2\,"|GF ʪ"mmE ݅Y(Ut+ԫFqoT$FV4϶:  ǞTIC [d .|#$}ք,qɃlR!%> *\'LI)K wdHqc9k1Af6#FP0% _!3G"M\K b\"ZMM qA|K64IO:)L̙ojM`8,'G]~Nu$ Q*ѻB0! , 4H(*6bck79CabiQS[MOXLNWACM24@jkqYZbPQZHJSㅆ8:D>@Jyz~@BKKLUBDNopvԀFHRNPX')5JKT13?46A02=/1u`1ZFⰉGm/RCFGQCfLv?:UnKs?_Eh*8:4mK8SJr=\\O{VJs:Vp\}f5qMEj3hJstz5tN*:;եuw|7~Q6vN6zP=8RdemcTzMQE!Hp‡#JA$,jȱǏ CIɓ(S\ɲ˗0cʜI3EJ,(&H'> JѣH*]ʴӧ)o T<8ʵׯ`ÊKٗRMR=[ ;Kݻxk4·ȲKÈ+^/ɵKLE0̹ϠCu<*Pc& V@M۸s&-`Q' 8NУKó[4>Ë/Zlƹ+TO原O< z'Jng&R7aVh4yv 8j"h(,0(4h8<@)DiH&$ ('1bBLiv$X HcR*]ifa\”PYh K)Eh钞AVɗ"/<Ԍ@_J%'C磐zgAOJ>DHP)H*@nGѥ"*s28G:*0J_(uP)񚐮Ac9Q6&# :цgI{ 3*+$dB*kt4N'cи"_iRO/S $%B~}"$oAbZA +(L$DP1 CH!Olfj/\PE (HzDsDXB D+L7==AHix@$w !H6ozB6@pqtb\ʸ1M #ᇗ 9{% [=ogvĸ nI($Ng4` H-mP dtkXP-y7{UA0JJiQ a4x({Atz`IC5r!$|l w% BwZHN&;PL*[XβeÔ6;KֺL2;#_ Әf[)AoZ \7R΅^:"Г3L{y|B(C!j"j' !'DșÇ\"tKc S4DD}O"NI +#Lj:,yI ܣ%Îȱ9l?$>sAdrVRrQ{d 6B-q/e 17 %ܿ4H0⭂, 6Ȭ=6uS"In!dgJ ~1(2qm|)Wlj0<#J>>'?572p,bI0Jĭ]-B\+-8^P}mA;WPFk~Ӭ@:UxȔvĖvQGl53GfU0/@P{78C@BK02=SU]MOXGIR=?IYZbbck24@ݽ,B=EGPtu{LNWxy~abirsyꁂ-/;\^eIKT..=egnjlr񎏒QS[jkqPQZ.0<[\cGm,.9{}ꊋ-G?(/7;XKLU;W7~Q=8QgaR~ORF>:T+,;Oz7zPFjNy/TCIp')5[Ny(-6]O|Ceyuw|z`QKODJqu‘t0VE6yP>]cT=Z솹tGKCz:===@?H*\ȰCJHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴSJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿@LÈvH̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_6У/G%سk]Ëk3_Go{`ޟf6G &@|W8h1 6~F(39AH}R "d_C&D_(؝0(4h8<@)DiH&]L*b21 dҵg/P)AHde.$PTI)H)PX0I'U!44DQl@95%DiG^q:9/U* @jMQufgB DpL5XRlCIe Z4LSZȤƴ02P =p"@*EKV+4lbZ Bn\/MjAjB gVF_ T32Y&w ,$l(,0,4]8sN%>}!\C1F)6unEz4CY'tL_/D[O B)AEܫэQ}IMՉ(7tJ3E=$xV?IpE$-CCw-A"t@+'*SxFޘ~UY(Q4!T!1) ^g$q}! ,5;(*6~fE>\NQEKOE{p;==8;=95MH@`?:U74JB=YE=[zcgVgwalZcTbRbSyɌqth~ӺQGl96N96L32E+,9(.678CbckABL24@jkqLNW0/@abiQR[>@JMOXYZbHJS8:D-/;(-6.0;=FEGP㨨XZaJKTstzBDNTU^lnt')5@BK53GKLUefm46AopvOPYyz~~̎ǘށ諬ιɺIKTcdlSU]35:]_f=?InoufhoWY`xy~prwqsxrsy13:[\cJMD+-6uv{egnUV_57BVW__`g.08GKCijp~{|}t˞㷸䇈ϒټĵԏՉH*\ȰC,JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊK6ឲhӪ]˶۷pʝKݻx˷߿O#LÈ}J̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(2CC(at5 0B(Z@,5@VA(8B4$B4D?( <*Ց Q)%dppYP_@G PfK>$fBs.D8gJLTAD坃ڧgD..=n[v68CbckABLjkq35@abiQR[LNWMOXYZbHJS8:DPQZFHR13>*,8>@Jㅆ婪JKTBDN-/;,.9')5E>\@BK02=KLU46AopvOPYNQE.0\;==24:?:U_Q~SHoTIp;7OZMwzcgVu`}ey˔vČp걊')5C=Z~D=Z21D30C..>+-88;=KOENQE{@pH,Ȥrl:ШtJZجvzxL.)hM~P# [ Bj[P  [ SQ!Uq!׮*\Ȱa 8x氢ŋ3ЁƏ C c ! ,[ (*6:==r^WKt=@?PFkA;V]O|63IfU..=?:U_Q~C=YD=Z;7O10BZMwgt`van[wŖwƑtu·䰉諆⣀ףغށiMDgLBe{d}SHo21D30C)+8--<24:8;=KOE')5C=ZNQE{@B(Ȥrl:ШtJZجvzxL.zn?t {}.1"v\ G!#E2 E(E-ɀ)t'م/*$7+ ,4Wu60 H P#JHlǏ C$A \ɲ˗24%! ,[ (*6+,;hn[85L10B30CPFk?:UMEhF?]>8R]O{:7Os^bSqktկ秃ܨݹXLv63IWKto\p\fUHAa42FyB=Y{dfU:==24:MDg]O|LBe=@?}ҷ톺ꪅ@pH,Ȥrl:ШtJZجvzxL.iX|N~$B#" s B sНZ!\z 40@A *\ȰCy4@Ë3j@  ! ,[ (*6\Oz')5>:T_P}=9SE>\21C+,9:==63H=@?n[@;VPSGIBbMQElZz~)+8m᫆Ⳍ*+8VJs..>0/@>8RUIrhVgu`wabRxb~ү祃ۦ۬⹐@:UHAa75K21D+-6//@oPFk24:WKt85L]O|+,;o\p\fUfUy˷톺@ (Ȥrl:ШtJZجvzxL.z.T!0$~zN 0a 4L%5J+&G9~LEါK3"(*I ltG*"m_'±ŋ3jհ$cɓ(S\E*d2`ɳ@jAn@ӧP$`! ,[ (*6\Oz')5?:U=9S@;V_P}63H+,9IBblZn[E>\cS21C32E:===@?@:UPSG+-6+,;MQEz~ dihlp,tmx|pH,Ȥ2pHШtJZجvz" zn|N3|hz{SWg79a:#*   *yee:!! ,[ (*6PSG]O|~}?:U35:=9SE>\+,974J..=KBd00AiWbRm|Ο}ѓvÕwŌqkz̶㰋곋13:XLv{d}ҷI@`NEi..>,-<\OzcS')521C@;V@:U_P}32E:==63H+-6+,;IBbMQE=@?lZn[z솪@pH,Ȥrl:ШtJZجvzxL.|N~T)#&$"%[B r( ZSV ޺*X V\ȰÇ6y%! ,[ (*6PSG~JMDNQEGKCt<8Q75K53GRGmG?_THoB\13:32E:==63H+-6+,9IBbMQE=@?XLvlZn[}z}솴@pH,Ȥrl:ШtJZجvzxL.z9B|N~R&$  sv ě*"[ Z}%Z G#   HAs'2HCI#JHt ! ,[ (*6{ebSyc//@E>\QGlPFkA;W@:U^P}xȈmvĖvş~Ӹܬ㹐53GfU0/@..=[Ny`QPSG~35:13:]O|JMDNQE.08GKC}t` dihlp,tmx|pH,ȤrtĥlZجvzxL.kodn|NqpX{y$ \` "5 Ԋ DUO"?!! ,[ (*6~fE>\NQEKOE{p;==8;=95MH@`?:U74JB=YE=[zcgVgwalZcTbRbSyɌqth~ӺQGl96N96L32E0/@PSG53G~35:13:JMD.08GKC}t@pH,Ȥrl:ШtJZجvzxL."|N~T# u x!ѥBZ Ш%Z Z  d! \ȰÇI0!@3jȑ ^! ,[ (*6]O|A;Vt_^P}75K85KWKu+,:C=YXLv00AgViWzd|er^jXdSxȞ}ьpkz̈nܳ⢀׺ު~63H53G21D30C簉yb..>..=n[vøE>\NQE8;=KOEPSGJMD;==.08GKC~{t@,Ȥrl:ШtJZجvzxL.zV 0Ю~u$']s~ D&! 0ȃ(",#-ٲ)D1ZD + H``#Jp*8<ȱǏ q(1 ! ,[ (*6E>\;==24:?:U_Q~SHoTIp;7OZMwzcgVu`}ey˔vČp걊')5C=Z~D=Z21D30C..>8;=KOENQE{@pH,Ȥrl:ШtJZجvzxL.)hM~P# [ Bj[P  [ SQ!Uq!׮*\Ȱa 8x氢ŋ3ЁƏ C c ! ,[ (*6:==r^WKt=@?PFkA;V]O|63IfU..=?:U_Q~C=YD=Z;7O10BZMwgt`van[wŖwƑtu·䰉諆⣀ףغށiMDgLBe{d}SHo21D30C)+8--<24:8;=KOE')5C=ZNQE{@B(Ȥrl:ШtJZجvzxL.zn?t {}.1"v\ !#E2 E(E-ǀ)t'ׅ/*$7+ ,4Wu60 Hkƀu @Ç#J,0ȱǏ bxPÄ!S\ɲ% p| ! ,[ (*6+,;hn[85L10B30CPFk?:UMEhF?]>8R]O{:7Os^bSqktկ秃ܨݹXLv63IWKto\p\fUHAa42FyB=Y{dfU:==24:MDg]O|LBe=@?}ҷ톺ꪅ@pH,Ȥrl:ШtJZجvzxL.iX|N~$B#" s B sНZ!\z 40@A *\ȰCy4@Ë3j@  ! ,[ (*6]O|m:==WKt=@?솱PSGMQEz~)+8^P}E>\iWbR>:T~ԫ74J..>HAa*+8VJs0/@>8RUIrgu`waxb~ү祃ۦ۹+,9..=')5=8RKBd00A|Ο}ѓvÕwŌqkźhꩄ+,;@:UPEj21D21CjX//@o,-85L63Io\p\n[fUfUy˷톺 + ǹ3< 2: =#C?0P8tD*\ȰC` (  xȱǏ C9L QĆ$cʜI͛6((tIѣH*F TTxqҫXjʵލ.,:2!]˶۷0pX#( @  LXC1P$Đ#KL9d%v?! ,[ (*6\Oz?:U')5=9S+,9@;V_P}63HIBblZn[E>\cS30C32E+,;]O|:===@?I@`74JWKu..>..=PEjPSG,-:T_P}74JE>\=9SmPSG)+8@:U+,921Cs^iWbRHAa]O|..>~PFk*+8@;VVJs0/@UIrIBbq]walZn[fUiXxb~ҡ}祃ۦ۹z̋ou`63H>8R35:21Dg..=KBd00A|Ο}ѓvÕwŌqk곋13:96N//@THoXLv{d}ҷݳNEi+-6,-QLNMR΃4 H=K6F8 H*\ #(nh'@ Iɓ(S$@:H?+X\ɳϟ@ ja P#D$JիXs!38(ÏZӪ]˶۷hZ˜ FA<˷߿kBG>*,"cGG#KLr  '7$x 2bװcWu׬Y Pp! xN 4! ,[ (*6PSG~JMDNQEGKCt<8Q74J53GTHoRGmG?_B:T_P}]O|{d)+8@:UE>\=9S+,9s^mPFk*+8HAa35:@;VWKuVJs..>0/@NEi13:>8R32E:==63H+-6UIrIBbMQE=@?96NXLvq]hVgu`walZn[fUiXbRxb}қz̋o~ҡz}祃ۦۨ솬⹐#(&$ "  Hp 6T؄#JHbx@CI^ASNʜIMR " Jѣ!,:@ :LCJիXs"‚N! ,[ (*6{ebSyc//@E>\_P}@:UmPFk@;V~Ӭ㹐QGlxȔvĖvŸ53GfU0/@..=\Oz[Ny21C>:T`QPSG)+874J=9S+,9s^~21D*+8HAa35:VJs..>13:>8R]O|JMD63H+-6THoUIrIBb96NNQE.08GKCq]hVgu`walZn[iXbRxbz̋o}t祃ۦ۳ź  H*JHŋ9H H`Ə CId% <0ɗ0cʜIS8Pϟ@ %U bH*]"W6Jիe! ,[ (*6E>\~fpNQEKOE{;==95M8;=>:T74JHAagwalZbR~ҺB=YE=[zcgVcTbSyɌqthQGl96N96L32E\Oz0/@21C_P}PSG)+8@:U=9S53G+,9s^m~21DPFk*+835:@;VVJs//@..>13:>8RJMD63H+-6THoUIrIBb.08GKCq]hVu`n[fUiXxbz̋o}t祃ۦ۳⺐@pH,Ȥrl:ШtJZجvzxL.|N~Tu"!x#ѥBZ Ш%Z Z  d!  \ȰÇI0!C 3jȑ ^! ,[ (*6]O|A;Vt_^P}75K85KWKu+,:C=YXLv00AgViWzd|er^jXdSxȞ}ьpkz̈nܳ⢀׺ު~63H53G21D30C簉yb..>..=n[vøE>\NQE8;=KOEPSGJMD;==.08GKC~{t@,Ȥrl:ШtJZجvzxL.zV 0Ю~u$']C~ D&! 0ȃ(",#-ٲ)D1ZD + H``#Jp*8<ȱǏ q(1 ! ,[ (*6E>\?:U_Q~SHoTIp;7OZMwzcgVu`}ey˔vČp걊;==~D=Z21D30C24:..>8;=KOENQE{@pH,Ȥrl:ШtJZجvzxL.)ڂzN~P![ Bj[P[S  Q  D*\&@1Hŋ\Ǐ C%! ,["(*6P{Bd;W4lK6vO*69KrKs?^/SC\Oz')5.PBJr*8:;XAb:UHo1ZF(-6PQZ78CBd2dI8SMx;W,F>NxjkqABL쯰?:U)38)59Jq,B=Oz>\1\F6xO:V.OB8S+>+=;'+6r^*7:QS[JKTbckXZarsy13=PEj=9SEh4oL=ZHmCf?_Lv?_@aCe5tM3hJ+@=9TEiKs-K@7{PDgAa.MA2_GIpFk9TDf2`GMwLuBcabi9;ETU^LNWHJS20C(08=?J(/746AD=[@;Vn[vPSGZ[bopvuv{:==嘙^_fegnܸ-/:.0)'s  #;ILA!X:q ql!@!d)=֒, b7H.Iz2A; HӞ1"Ҕ\IA<=u3$@AOz7lH Gф ji@@6`CP HMFe$< tc J:c8 aዓ 5R?/lኧH]@ F8:NUt]D#~0F4Aj<" Paz{$a @eq& !,N"y&"n)YAxnK-NPJ:L!,4_(*6P{pB\6vOIp3hJTIpkbRt,-{}kms)59-H?E=[(0746AMx8S1]FBd2bH@a>\OzGm?_9TKs_QTHoNw4oLMwr^MEhrjXiWp]dTNy[\c񁂇z{<8Q-K@䪪IKT枞efm㾿nouNPXZ[b*7:<>HH@`)38+>]Nx8S2`GCf,F>:VLv/UD=Z7{PEiEhCeHl}fHn5pLw=[Hm՜{ϩWKtVIrt=8R:@Jœ')5Q"4T‡H,Ep\hX\FǐS\ɲ˗0cʜI͛8sɳϟ@ JQ pL ]GA pQ ?(dEWGdmٕ1ȎM.QQD=h.nUTE4$^JeH`ER|wKA騧ꬷ.n/o'7G/Wogw/o觯/o HL:'H Z̠7z GH(L /Ӌ03x\t!88W;röaQv("a&:PL_.Pq<8.z` H2hL6pH:q~@h'0J ,"9+M<񍱐:j$xL0I "c )& Wi> G !HH̲, (I ikX\P9 @Jna;!xEF @t&4Vj@7\pAa#y9V !pIA&h/.†x@ȁqpB(\C@AЅ@nx萇CyOyΓ kC$+(x &BnYi@ !"(yd-]Na=(1|b qa@EY *-2 Y ;z 8 `@s + =ϛB `i=lA;Ҏ*KТpP¯1+ V*dRc|L%^S֩m-(ڇXvʆC ';Q>iG: uЮ@d 0E+Xڊ7!wp ݝAV]#/ OH#v!Ї?|@!"@` B/>$+n17x 8dp ˳ 09!&lx9!4&@z~p,YqpL:nT3{%>xt,\gςNtf>Њg'h aK?h8K<ҒN(͑DxA>I36 "TԧV3AAB!rl-5=jWx=F4b`h R`A&xgHHX D Q6D mh"DĮK4 9B>a kZFma'! NX8M/a"+@@P j\CF%D D%N z$!a qO^BReT g0tf  '1}TB6|HD%*kǨ5B]K'oI #+DŽ5a tѓw'e`yR()%._oA뾹wp` g/+_9Pt[ G# DX!o{@ ص2C ި>BpmT!" e(3=B u'+ p, s ` m}0}&}[k8 hp QPP| 2 "Pu:B Gpdkː(g , Q_'`ܠ A _| ^P   '`^p a8Hm;.{@zA  ' ~р~`@@zh `V@W] jp^z@n't  z7{9|~I(]G(P t|@  x 0pG^P`ڸ v`h Hpd xgڀ -Q 50  ɐ 0 „[P)oamp*A `pY//*ɒ< (=s˂JnPYG!6,4(*6ĢӽʬγĠ!",795򻺼Ȩ^bfJ輼ြsxQƤ朗hƧ켗áRUC㟥f脻ˮoз=?8'+5̯Y\EsͲ')5dϵy~}ꅼmqFH=Ѹ̻Ծͱkw_[_GJM>􉼯ѹy|T{'*4+,:sηмĢȫǪuʰ鼛켪ԆZk/12ɪAD:ǦkoN@C:⁼zß~CF;cGJ=(/7VYDaз悻Շ[{U]EH<^_bHX~VmrOq\`G넚dka\notPSWC`-<0344464%&-.C/R C[i|wzSMO?fjLې_;=8M:<7uy~TyimL*54P{0H.63Hm:e#ZG0C-NQA3Q)3L+oJ=ofA{VŤLO@WZD:V_P~}.QBC\=9SHn6wODh4lKLvNDiSHoH@aHKȰÇމHŋ3jȱǏ CIɓ(S\ɲ˗'I͛8sɳϟ@ Q&ф#&=ʴӧPJJUF6Ƭ%{ٳhӪ]˶-Ud{pAmn˷߿6ː."~ Ɓ#KLˀ=H)LӨSpAÝ!m =ͯ)^ͻC%+aŜu'\УKׁNËg0c#}˟{4Ͽ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*aC衈Bgh6<*餔Vj)d饜v駠*ꨤjꩨꪬ꫰*무j뭸뮼+,F3 awҢvmDnD¥ҷzi=f.B뮟dEsoHR4CkdFjO'p k(L1 _w ,$l *r,AƼ28ֲI2#3A7W3F6ё]R 9-tHW)I4A QSm|\J3k $562st-6|ewL=Pqˍ\7T~w1Otvߔ[ NwM>xߡ7פw[x=tꜳ6օsb=yO6īzϗ~CW{g_;O;vwn3y}z΄߷<?/xħ>%*?.w{[$Ey`vN ֽu4aad8Czⵇ(ۡ!?E<B&:P"C*ZX̢.z` H2hL(6pH:x̣>q 5L"F:r~$'IJZpA)>flpFQL*WFL򕰌,ɂt`e.j` [eP4HfLe^ԥ4(3VӋb6 |j 9rA7AѩNsQxc !FŽ$.;9\ hP~$#.) @@-QZq*"uH aW nıy|GQrTAJ7bX,1- 0( sFCӘt1H$L@8) P$EqF?P(,P!u0``[ĦbwFzҕtJ}T1n |x&h NP Op`b Emn{sc]s;T]3fC/]pl@]P<׺浯Y+iJ[Z0c4.b|@P/ "4,A(@`lJv#(JY`)0P!j t\y_nԘϼ[B!.a"'QQ qpޠҹRl#Yc;eYTT@woVtH4Y[+&@tPQ /XƘ8>P@hAc&yCx'[0IxЄ{WEUD%iXxZj)x$=0eXjH0@JHJSmntyz~CEN䎏鱲ghǫZ[b`ah_`g8:D:][1ZFrwQbfJRUC685=?8aR7:5ZMw[_GJM>2dI@C:kpNY\Ey|TZRGm1_G3bCԾTHp_+=7.K<{UJBd3hJW_bHVYDNQ@WJtH@`fjLWZDHA*\ȰÇ A(qŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPEA*ׯ`ÊKׄ3Md۷pʝKJdv˷߿r LÈ+^̸ǐ#KL˘3k~jϠC}1 DMװc4-۸s{NZ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|ũK j衈&袌6]"JJi(juƩH~*jꩨꪬ꫰*무j뭸뮼+rV&6F+Vkfv+k dvM+)I’' nADFqKk@ 3lWxG4K=,RL L5$W2wG?dC%I[E+D=c5F/. Um 5O#P. $Jfy"7=G=7Q= 5C0NR?ElyF09Ao4@POQA ,SG_m@ q Hu[c7C˔A H@:& '82|I">hI$B4 "0Zd0'B\p4h`~aaІ!JCD"k P*2q= rP'@0" h E' )` lhB08ċA8(QF=`T! "5{J^R $ABG@ 0 p! q#H4Q @ dD<T 05B \ KZ&1)]Q`(Rkfs+ 8(8Ӕ0`#P "z {xx@9"t9L 1"`~$i`8aC 69P ޠ2l!c:A H%S.9ݩ яt)IS*&@B` *QZT .u(DBX8@P5ah)CJ`*X:5CY+zQ$4H`[#Ux^P3`@#($B x!@ h! C!Ahr!d58KuQ;V P@pSDab܁4W 5Hz R^:Bf*D5n{{]A C?B ds 򀁂׾[߂=!,a  cm ނk"َ`I@@  @$6 9X$>@ @wU$l@(OY d633P2 `(x88HH \(g.@ǎ bX@@)肰9o-N:ԣV vFpe@0 1Uqy ( CP08@ NAf hpAY -mj XB!9tpBTDw0r|C׷J.p.H 9 @"rd 9IH}tsDPL8M;^k5`)޷B;VxƐ+(54{% %np =8"Z b.A =@4H{d;Co r@  AAbp~>P^ 9 zh czyA@hWiHB9x0@r f"@Eܐ1 ӯ$ ! Ʃ !&`OA~~ ((q!Lv$}!7sr# .7wt4h({<}X~h_"zH7|p0TvkW~|!0VVC @@PPV@&fun 1pf7K@jZȅ^(#7@}d@K8Nm]Uek8p(yth `(,[؅s7zf}z> hH{ahlsYt FW7xP $a @=A'戎q41 ~ю( ~a8qvuH@I5` X+p!m,P Wg`a0^294YH4 EIDuPٔR9PYS9UWٔY[ɕOو^HٕuHeihYfk)mr tYZwxicz՗~jAy@٘Y@!,5(*678CQS[񲳳HJSܣABLqsx8:Dtu{aciMOX=?Ibck46Az{35@jkqLNWkms>@JΉSU]CENYZbyz~ǮFHRVX`xy~JKTEGP\^e:`ahȜļefm_`guv{ghoZ[b~[\cˏ02=.0<-/;opvUV_nouhipDFO+.9KMV+,9䬭ѿwx~[]d<=Guw|ijpprw|}XZa')5O{,A=;YLtdem6yPMx,G?Gm.NAHoLu2cH*994qMDh:U>\7}Q_1]FJrAcCf3iJIq?_8S@a:V]MEjD/C-[,<3/SC-K@9Sq]MCg~ҹޱꦂڮ涍죁H*\ȰCJHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@] JѣH*]ʴӧPJJիXj(=_KٳhӪ]˶۷pʝKݻx˷߿ Lx(È+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXfY4h`)d~F@PS724lwMtֹ#0vY/1zJ3j0&]/ ČF:褔Vj饘f馜v駠*ꨤjꩨ*LpI2Ȉl!)A\ֺ*ErD"ghAJaȀ@Y]x*Ŭ.M5l3QJ,I +tw{JX DD M*P) 1620K.0H0<3 2K 4ޠً4B0.L1v@`|k0 R 0@C:(] :t_/>?~/?A.8A HȃP-(2ܰ9@OzԳ2H A <CGЀ<jD`H8z)hA/~Ab82p@`o@8 Lp ehC F2`x  al 34fJ @pыr1D|H@*`3t9@"r@f @ ADiNznC.`D.l@ c -HGpS@?(+ay]%- bJ|2!d3 N@ < HYD p7F5WBh1 nj,%fpAxы\bX!,yaD PЀ0XtPRdr 0!HJ[S2Mq<deh  hH7h :ѐШL` @RTzU5HLgZӛ>5SEZV*"ӝd@L(z^ր#B'+r| HHR,osą14>A XA /$@( y@n 4%@k\]ns F^0$0 ŭ@tAxua @z <D`oy{UuA\Je/:L`X DBb0)ZV@"PXAыb NU/z DfX\p@ "0vhBl2S9w ®@=p'Yo~ b3Opf0gDY -"3800 ,_lӑ!0F# E#]:F/6d>rO ]7c J~m +B!l!WiF0A bm涷 K5!@8'D@ŧvd/;/+~W߀=lx˛o} Jnp 10s[ Ba*ǽB`C1Pa `0r!]3 E#<^0@;@@!Jb  ` 4І<WP $h)v}mb$HqX Q؀vu{] 2wqwa+#?y<5;a. E~?٨F5tE7__!!92Pv(r@<۶&y+ fCtW; ! e`t|" .cYp `!X& @BA0^I'x8:>@B8DXFxHJL؄NPR8TXVxXZ\<]b8dXfxhjl_mr8tXvxxj ~8XDȇ a؈ XxS(xxX8芯H~h8]AȘʸhҘxؘX؍ȍ8Xs(明긎\KXXxX9/cD}p i)P)G{Up #PG .(RYPIe-yP#)D GPɑ) )qVf4IH> EY;ٓZ6cp %Tgb+fY#a+y@O7a&?[YM Mi&`!9(@-@gv04@3U Nd0 Cl֘8 )0v`p 1yAəT@| ?q RL09ћ" QY2-q -vq))- Q8&@XYf-DCuU|,p>hQ3No\@ʡ*{z5ڡ:Yp 2:ԖJУAKǤ#Zq9C)VД5A X:T V cJGIJʅ.vu  &@{P2ay[5+> cMt4[` >X $S<pТ@h C?n 6 E`v8髒%<J胨>iG9ح>8z78V3M7CpA6f6 a2(2q#6 ѬC 1`į @p2C363ԯCb)5&K[8%s2)3JI"5~9+5`@ ?9F8<8 2I{88 fG?CsTF P"PB4B4A` qD2B\>>Y^ 1Ŗ  StNP?P,Ю (z;}R ( z˷d@PX: vYB8+>ꃄtei ;ع+z[9:țkD4OC @[Д@CB ==C,4۰djdk! AA;۽>>˽;:!F;U WL DAӗ`+S> 5d.lsE?H@?3ȃ)gƙܔ nڔh3 GpA w Z[x(K` yքM IĊDL$|\ ,<KKGc1M$|@ d,|\ `NMPJZ<ξџNx1j<ˣܼʋyݔ<VVTKSL"h`"mR&YU q́k= CU\3,WKTl\0Sy V '%=MW9М]p P=WS?5AW j{*w5|5TVmqݾ=$`_[ a+m`@R]58p {+J00 0|9 8!QM*.dPJ =p[?{- Mi 3n5ՐM]ϫ`@`a`qvܘh[\ @_ q%ܠwlfh` Lߡ޳ΞX5` `E^G] na %ϕGi`$pY@I}eSMhͫfAДUWQ-rZPQͼ:~u0s4@N0 w׆.:y8ZǤ ڔ ~|`~طXؔ_^VhLw5YeW=exX 0 1nef>jNlh2e=w.*h!_)egii^z&qq&$dp5'Mq;p^3.@gރ(銕003`FPmݺ'5/@u>(?`:@M? _O9C _# G`pm[ea@p@oj@ \b(Pe!!&,x0| @5BCG `F l^fуB  e ӃU9j&מ?[ ECYǡSRDT ݨSp/[ B6T@ J \@EP OhD 2 D}^Ɲ[n޽}\pōG (LpȁJ?4l@!`!P|ZܿH ?@O@>#D 1@>lE$,İ P/2CV/EDD+)MĒ.q HR x-PȀ@2҆H+.ŀ)N#5"?#apvÐH;O: σSBͳ7ոM0@UYƘUTNPA e {!WጕTSQMfbcxRea 6@6BO=B!xk K7m&;7m('p 5nۚylGz駧z>{{?|'ɻ|g}߇?~]~?{/_8@Ѐ$Z m8C4*X)D<0nBQ %"$a[ZB&#,?UA2w8!!r? "Ěa cXnh!H8%9n\6Ф&i4PBю 3A2 (x g@  P8a} t  PҐqTNz-\eJ9JR%1Ib vHHJL)igK\LWx##9Ƀ/9̃_@, tLd `yDH=us3h5ѧqR;1 ( 4m0`AcVL8 y.L:))QdeB:R(mJO mB'٨~R;**[5Rc?E DCJW& ЮN`n *j)` uA XFyUSS|* T@g?>osi5bf6 r\D62,6ζmO yj 0A2H AЍ40R-`$([FQCd׆ 8ۂރd t7 @x N dkiƴ҄Ё@tb@gMmntKv ѐԨV7 H?`ҕO&.P ώ)^[#:4k*^hںbUAd ʰ0aـh~HwE-aQ mw $ Dys MձnzztjXzӟ.6G5:Kv64#` h\e@ @ rC : lCj3h]<ѓNwu݃aw MfƄŽ 's Mf@&!7o ж?>Ax uC8W0ǙF֥ [ b\@/Jh963yw%o8H<ȀE0c0 0 d @$Q A $@; 8@q@, @3ӎ*``5h2h" {c) hHH؍8a b Ы"<$dڛ0#DB%%J4 9tC`=!w>Ct@>8&'DB4 !d9dѐ' ?%6T+A,C(G?ȀA0ZW FA,f F)˨F F p4 20Ll&nFpGai 0f|;FbA׈08>yᓶ(+ a>ǃ ftˠG{Ԉ4/@@0M,ȃ 8Œ$HH+< h'4(,yč PȝI lI܍PLR\ 3ILI9TL8JZJFEDX]Ŷ,_$QxUԈh Bƃ˹x|*PL*0 IP!w<\|L üOKJDKpʀ+d<JF A7blڬ čM<3 4L0EPNĶ=8S0Xp&<=MMԈՔ OPPdDTO.zObKP Љ@/ XO|@ a#P! @ jXQX Q,=x ;Q5}P0Џ!PQܐO#@ K=[Xx#eҖp7u H% Xh>R 0.̠@ PPT8 hO>iTP-EԮ T@ /=Ȋ/ 8 pTxx -?ڏ FI 9ki< } `퐃V QVfImJkq5ٖ1:;X``ɣ 0> Y6DR$F"i:պ>D$aRJ&ZUڤ~GXr6i2\ZL^) 6pӔfz~򧲁%fajor\'cH' h3&x"@7n{;&҈کB()R->f@䃰)CCF)*KZ,c(]оqʃ e e =.T89>[_6Z5D-ʬ*,.Bffr,bAifA;fGfȂXfe-MFc&,YiA Ph "XԖ580$^(c݈骮.,Hg+ \fXj1z1/ Ti 1Vhꊷ0 0Le#N ^1X2 /(8h;e`jE6ҰH\31#3\FW?)k43QF\ӈ^> `!Ҝv>;wW8 Ȗy;x.TP5Vs5 5Y5=Ƶ|,Éֈg6i8ӈj[m#9C9H7n{˷}k;Pn}x 0ϞZmQnٞfof: =c: ;S:V:˯+pW p {pS~:Hxph~<3<0 ̶ ʳ<ʼ : n4?_+e\Frs?W <ɳq< {8*SJ>)"S,#)w#xA<\¶x< \p EWAtA@?G@dT Uf/lMB"EPD@ +' &) [, PuR/K_X Qc+)=K'_Wf^¦uDdJwv؄X 4;4м*`BEmg|Ǎ^G~4FdT{ȶFCGk̍~splP-GLJ/}FGcLO1) GCu3ק0NHyhȹ|HH!xԎe>"UzwVԈJ̆˪YrTKHUI܏׈M00 X`0Mylw}^LKL,LTLNk&gLDŽLW|N(|:yY(B0 ,z0 eڼܔ4!߬ |!{uNWXP=WHMhUOt7xD @3'ߤwϽwʉ`'*]%ER@R%Ս+j .h@  "Tp"0Bذ b̸aG.t( 2xsfC000rq PpGH()ԨR9S@ >xFC!>"t aʆPE@PÈ&]ԦL4'("5d:<Z HKO >?pHDrwn=]_Q9@JBB` @QP:UO? 8 x * :H(d.,@H5$  }00 zQ@B4d!D4fXF$ʈQ(Ѕ;cm#h"*ah@Y6C:W^+lm4 ^l K\`\]bB0hhII)入\.$FI!gxAL}@P $09g!⪂X**&(" D4tG%$&l(A@* 6t+ ;j%//?51A(10Oq 7 #I, 瀍P@q,3T`Pu] 3O-gS@E#4=[}5Yk5]{5a rD} p +Z01nw7 >8A !`^ `X\xN[>QWu%\9衋>:饛~:ꩫ:뭻:>;~;;; ?<<+<;a c@T>gsT  l H!1)5tCp_ S8P{3A2`&@Z@8 ! $',! T` =!WrG-r1G8 THG1*{#͈F5#džb v7A z) Cx4:YTF7n@ZdТ~r*5؁Sp k]'V+9f2I^2(JB5NV0 Pѐ:!jЀXDa2Ny4': 3!2ȧ蓟%ABBЀ6D ȉNAKC0@ⰇBEQ4* D: uPI7f)jtdAF&8iK4L P9SV7`d TU8H(ckI:>ծNq+Z:u$`zUB^( Y`r-&hЀ1LD,⠶́\5@=Z'"[45mai:ĴUmCRЀ?q"]mIa9d`mmV.e3 HHC\ 4 ؃h6: lg?R׺B-i'_& .B<4=Df .*%PI6/z`΅PqO ou W](8X!/# W+KpCikc Д6)M\c`-TN@'*&]j:,^|<.vB*GS@d%Mib 4F]IW@]Cc `1H.@#T oF׿v}{鸯 8

A FJGw HI:Ԯf//p!8D)(UcAZ"وHБZ$R@ ' V,A[UD$4(R@"hT`8 ɃVeQ݂9 Pe|$L쵯r9𴯵ARpH:APTBOVo;ܹx]i]*Ԟ!bħm;Z]@) \BeE3򷿆M4+ dą"AL0Ȧ; Qz Z@#]LKzu^Ӽ klJmjϦm_$(8: `Bs@ +>MQWho`p[)ބ4|!z{^O$n?4@wۻɍ<ѕse.{yAz-Oٰnfx,G $j0T|!C@ŗm K$tBkZYCt%` *^M`Ͻ!5^vCd9#ד .:50@& B~-ѓ.NZKb)yN[pg62 +X,7PaE2>D@BJ#};_J4QB`?IO @^f4k!QȟџmxACT@*ԀQ$c_mQXh JDEdAX%I!PMv$ @@'U.D< \]q&E!=-V!Cȁ8)^C hT"@@T^酢("D# 5`ʉh+AB4 @,@+jBPh#dS,]- Js4#-6_2b6EID 5BlFd018bx+6,V *cR2buY K@@.@ 0 f_,AP@I'=; @x^;Lj y(0 ($C2a O.dCZa tA%RJX(8B 4@@=DU^eVK̀apWT>CL==d D2CQj]^&$|d]%/%b &/ #(W"WԀH-i&)I M]4\)CJZlFEo] oۥn^4 1o*E!IEtZ{܀Tt^!$i&H @r6yz4h-EX#2]C{^,!lb[EA~gvĦGGg ݀']hCN脊m'fal vE@ ۆ((ƨfh!@ ̢Ds(())&.)6>)FN)V^)fn)v~))))BƟ V  (@Xh|Q^0*jEFݤv*ҙlF׀TX*U @ \*~6A(DiHH:GHA\p\4+^x[AX+yJK,$zB`F@G"BD= WFB.ĺDK4lz뼖~H+XEp h,ǮĶ@z>rGCx1DXXDRGt[Vb"Dr8dPIG`fps6sai iHhHHD|lJhIH` HHEpLȎ8U.bȂDf겮4 P/} 嘕M6ɜL) @\.B@)MDMs,J<ʱLJF0@oe/.D>Kx ʭ˰܉0F"LX0@枅C`pK0˳ e˶@W|4 P-yp {.hv_@ Xd̔hF(ˁ $m ʨ@\MIZIk ШMqMΜ Ta ]j/M.\$[]qP x Ѐ!QEhxd0 [!2dgB4PNTP0o_9m]P!F33W@rbOOT/pOat <@)T 1ד/ 51@`U_wq ,I4E9u4# m-a-AF-QIB$4I+2{n>sNR%uAdT@7a@@)ˌ"*&eQCtNltC,C\= N~,[@ @n!>Bi]2)*is5>:u#%s81(N&Z) D XC"_u@64 PTKLIXUTM$TAnTQ7mC@1uEjlTX6vKTuA@*6$P PI<2U]N^L D#nIDɔdtH)DVb1cWdM*ucv/s.30/{sؕ1ziWٵg{5}`+׫}ĎcИXZ}Ijyq9]XxokOL|ۘǕxayJABxvyZH\5E@ D?Z7[)[7DQsZ@9*CIq ;ϫBh^6Si P5Ⱥ=`u ɶ{YL;)Љɕ;ut;ѷ̶ ED tG"G u7$}LYcW܁ 4A 2HIt䉉 E != vBD:C7@0BXzB]CC|.4rπ/]eΓݴi@0BL@I  1_ #r_,& EO ͟ 4oc}E ^D8B0V=1K1IAR<tA_II }#: ~ ~`x>EH||_8C'#$R% Edϡ%!jG@0 oEAd/#@x`  *`@p CbE1fԸcGA9dI'QT%F`CHA  q Kf bAA > $4m℈@5op* Ox *ѫn"P57QH,m `gϟ8x@ .Kx9ǘ?h+UОTQƉ$VDMre2,X`&h",`4J/AW#``Dqˉ?Coъl|It& 8Ңg PQڋ|I#` 29@ {?:4fC GZh r@DM <` `;0该)+R-r2%.8" *#>M(Ί<8)6{"~̢ͯv7/}3 Aؿ} !`<l@ |C! }#ID//a]!GxL PB!=%6e?d{&YĊ#y2,t"P  j? (@8" H* ѨF]ai0#{EHRrHFRrE&br[1 Ђ% RRP#X%n q+ixfrKlJ Ɉd @PkBE 2!A%AYM!SvH:`MlB\ wZ=yx"'=O t3~b ` :@~p Ad  R 6`*O@'N2#JUR!H.4ҒZ$ X>8%RWҊ^4-NP~5a S  )Bzլ@&H_A "D꜉U!R?4#vJkЅ"%gٌVEHMo XbUA_T? up[ɦմ6Zf5+ZmU*B^әbr+t$5,D:؅L>SZcDF]CPKaYLXx $~ӕꗿ8P~$ȅ3Њ@#FmU`H>ȃpB$B`8`HPA9,WH(N@ZC^"&p$`U9Pmd'7##8A!ڠ+gyI^rSQ ·vr"0"H r8CF h! < U Ј:Ғr h ala =@;v|t LXAԎe.F^mjA@8n`7g-8`ZӜ{>gV zBS\Ef>qȂ0X4 h@`7v 19UN@JMS04Ћ."OȼTqɳr4d:H+  54+%Kq  4"BD AOyv"AЀ,|IEB'H)@81@vj*>!|'ϽEx ЇwHfG@/zݕFE Ag "[#$_G">\"'H)3">?7|A!Ƚ{`?Fo\zO2h= (#d6  oO䞅䌎@@^t !#^6xE3Cp2b pH}"^0 {P!b'aS* Ԁd 6 @^Jov!D# @b B,A# B P -`V   p t"P"P % Ő Wt j@C!HI#&/!P  !`-"q1 P:I"" U]  F2"1Fq[Ə#kp|#Pw ܠ*+"1"Ct# q#"% 9pZ@ `L(l (``Y,š""2"T< ` `% B&i&w%+%1kDrYx&or%+$-  $nL`V`.A0Q2RE * 0 y Ȁ(G/""j M/--O5ҹ&@$Nm 7/s0+/ 0;+2!32'2@ @1Ch6U5'2QtZ#‘ R*Ds#J3i,"367os;k;Ò8Q8-!qd/8 7-]<<#Z<P C ! 6 -@ouPB 'u,C5C}o>@%D/tD7"TAG`*71[ùr)F$, 2Dn4>n  "BP:BE/1ij" (0+"4+BICIb=/@GrT .9%!5KF}H)G4 Z!"t# |"QP]bQPq"bI3OV"HVN5R+UH?0T,"\U#0!4R4V+"4?k(`D T beN:ulY@Wb `5#` ` Y 5".^\^*^]5[YJ^\U9: %_] rGa<.TLt<#VtlXP" if" [g)/ uMMUP9+e_6ftM3MN@lBR"0#h9cA4?Udkke BCj`G2+#E v' m6.p#effO7l?H 6ssvj vr6u7 !"VkNV)b^:RxN1 f#4j2]oPew"VV&@g3"Ū$S2PLO8)D[nV$Z»;3*R(54~)42û8$03#)dȐHebi".Ý"嚬z#ǕoϻK ~  w,>@&)p #x檮x\p lKf f0/Lכ B |ZIBܪ9* ̿KW |3`?ʣ ЇFk à<#I*(}ۺ:cE3%[p[ď}ٟ=QC7O}c򄶳]۵M=9| 6  lL΢T6Ӹ3, " N ;3lͲ"`V;M E~+M-=B@*K9#57 m H},m m,V,NX,<0` }=؉۫O\M}Oס~ک͝//~Hݡ> (@H/Y)*p?@ @@ H,, >;#⻣*fA)M_@Q#P`y_3Ш A.@ԑ(ope^(=gQ"na[0m.p} `! ,ȰÆPB3nTh1Ǝ+hrD+UJĈcJ"aF|Sȝ<{ 4СD=4ҥ?O#M9 E?:aA'*,mhXUMJ1@GM],KD"8R&q*2`P޽},d@I  @@O(+j72k 44daYn|l M@ТF`$ "p >v/rdMYR3Ř?|WKK7_| ~ٴRkZג *S"Hb&b*4.UV ~Apbg`AI@DT8qE VES$OrfAf& twhm8QE\<NwYPm A @8\ @ iܚ &(!C@-0P}$*gq}&NZVX}x&ȬK`|vbI~mJӅ!N枋nꮛ2PB0].pD^`]9PPC](Rv aaA3P0yP H,fI%0rʀA)P$3j!PA@D]<^@V4:ё)QoAk$@,@ UAfx̍C4!W l nh_}`cԷς8.b8|S8Ej.Kz馟: PA!h3"#G! XR)B A3 {P@@, !/PG>ouq _= W2)ϼtY 0 "8|; 1b+ &:x & nP j~S@,P5o t@tyDB"b!HTx ԕ7ǭh1Daܱ1@ y +sVmF3-rߴĠ?AD*rl@4 * x!( Lx TpxN)|PM:H &nLd[p"/[& L'xD^321Lx@P3b@B H   !T4ej8)| ` P ,x# $qA4:6a-.XǛq(-)ja觔cpZS,"PTo9uY̴#/H2BW V,s'@ T F@xԬF]}2ע hU+\)p $ SXkQV&b`CQ[J)|guN]mjƽ6x\&דцum(PY|w]mpz!ʖ"*s Jw)r"+0hwu X5⍮sˋw }w x.+x n Kx/ kxQ+9^ը N,NCV<ƧCbX!)175k O Y DHA P&? A^QPB$PVdyc# dDyZvŹ%6Xk9|~L!c B޵ @H{!2 CvrإZ f pfZ S E^:Ro | PhN qlA/ȭT䅭i?lR>W *PyM + vJ2= 5CHar{! ]PK+ܨ "Kd( *@&\ 1+t["%CE_qwFwoÝ(:~c|TA7@9hx> (+(`! ;9@ @I08Z =ep 6(B CŃ%5{8x;>S]т($ =S> @)6YA(Kܳl>"4@E^Yu@\,dzIvh=O+GI?;bFVHG8\Lu^Vǀ9S`c@/qP U*7k/+p pLj3J ;@=<p tpp hWyVj 8;&`W&3!WD0(4(/3X؁HL @`Bq('$h$@vaD|v|'][2%x9 6`f85E<Apr"/jቌg1tA,cgHkWkDڲv؋"BHKSUT׀͈^E!? @/e(5Lpupt-x'e `L MnM PPGP K؄ y@؎f( 440_q X(,.A )H8~%"XQ9h*9_( HqG,08)qWCYA `s25$ [YpT^n8IW9gHt9Kln39_sy_ P!TaX# Af7)C~񎈡"{Y ~*NKOTXT ;' ᓞ9Qir]&/x]Ai 13ZI1 a A!dCnj UT[S?Ug %UǞQUFGW r:4zG}^\򒍹/3]"60W2XpQ"0X 2xsq 2$ q224&,35<#@#V3Z4Ns)  M5#._Cdso#js86!7tc7x7@83FS,߲t|:_ #9ҖBgr^::p@  T<J!D=Z>>1 0!rVZ LKBs?C;2&$D4AA0@@A#!CFA:T<#2'?+B/D곿C=tI` A@_htDIt>wD#EaE EA\$Af-fI-x.{HhTJ\lcZhT˹IDIIINTQ(($Q$!TM':j!@~`B iф6;$MXMK%N;KrK;S5 PtJO%PeP P*P-M&TQ5.@R1R*U0.eSHS~k:Xg招!:T! b19TUjn^5V|VYg7BQZw9YzV5l0UF,7XJHUYZH*WZŗKnT"hlm^pUt`(\Eg-} ^]]^}ȋ_5_,ɓLɕlɗəɛɝɟ ʡ,ʣLʥlʧʩʫʭʯ ˨nGZ cqw I&-+FPi5vc.'v^c>6,vbQZGH@y@-k 1YPǟ fif,?W'bKP Qj@ װ P0#q v=H=-;Ld. ]cA}RM-&1\&@9%2L,a=1f\&cB)f\{f:,[h8!FP  `)dC҂-0p0KǙ3yCip mJ)&ij`e1ǖא.LԃF.BѭM؆=A a<םmk^`ވf(C5^  554>"+ڱ-ٔmGtOֶlNiV45ws9, r7#'Bdy$@'tD->:{1f  F (5;A!Z8k 3?11%0+).s7$s  'O/ӨuZu^vbewvivmvq7wuwwy}?%B"pFDAm})<W51"!{9wz>'wtgw0|w|2|WP+0}zCCo|0J;uQ|L0FRc ( jpz.qN~{/G}ΧYu?a{[uTX2N0>$zAN O q ~A~?.p q55ax"LAХZ@BIP`" E 01ь( D F$!,pg 9"+1S@'d `5G|Tbb ":(Q~ȘE:%) ! B f"02Q wԥhHw@Fɚ=XcLwȵā<(H66|3hh^ g6N&@db1ھ;DGn>;2 z2.g.^uQr€(l0Ekr@ 88Q *"hCF"%i"  8(AJZBB#(!,:?? jK`c؁"` (4# 4P2BH+R")lc=2̉λ4 !." ยq@ Ԡa<`C  b"*,0>TM6݄5|s YCO-S̄$^W@ 0o a$ftldg-!hvp `pR#: !rAw;PWJXd0^\AS(\`H `3/Tųh5RF@Xȁb$@`*` @a )^x**,XcHz Nj .| eYޱzjJ'bW^=h F$ .ۘgf?, NI)bhC Ɓ8/5m(FTT7q!K#KؠBZp/@q>" H>RCRA68 hx"!C pJp *hF$2 )GXѡ~J-8dHFD#$\>b#ىD dp Iz/jaVȠ)y/!f`"Ʌ`eR H)HC@)08:pA$ &H gԕj$ !@*2Gq k^0*mE/ӫ@$l׆#Qm4C R  x i0D ?Xi g (>X3A$h@FF3&:&ah0AЀL Y/b d@ XV/ `l^͉o"$J)t*'osH(53SІ$ytAF؃@uHH.U!F9<'".~aDWxNySl`p,P ̴7^[Z7iHC 8P$  0PH ,Q u  ` Q.0CB2) F*H< Au8 #Aeg"H kyz&3 )gd݁oЕns64Dtku3?kXjC:u εJ_!|Qn^:D]BUbNA z K%YL@B @'h=춷K*CG{5YcLҗDLXcVHrhB)-Z7@04b{ݽ/{!BF0u[N䳡mqMxW-+m[- ` 8*|!]3A+J |QHH6D2$I7 }H2@D D`+Ѭ,HpPInu؀c'{&OH\$+pb}$#Oڪ8v=hRH7+Y݉bn_'"#MC.?.<@lR\,B qOy߻wvBv_Ab\ {T̘6>!R^*C@ {ہ_>A@@gAzE/~#8U Yhr38A">x:҈i1)6 H$8  8X~>3+p@̭ɺ;ʳxp@@ )dTABc*A$H hp>Њ2`v9C;C >(+=;$:@hÉ1(.$h3L58O*x%$pmaP} 6ȓ/Bqr`QyKHQ:A"Ǫ2S/(:A`r,Gƨ[E:S<5H8a0`Evw<QDP<Ah"X>/`ʼn#Q$G >Łh`9`DJʿ0;x83*j 0 ăȗ@gR/5ع'K  65:Jhb@ŞE0`,a" LEm$ *14E@3P/U=Tbӌ|SS7Uu! : ;Nԁ>fqY"LmЉ18%ȔK$' :3-=m@-&p n5VW x "XV%h54XeY&)`W<)'ЀX<{ ս*WוPAЂHD@׃X8BIc&APVfA<ԁ0 ;PQuсXK)-JEns cfEY#ZYXD ĉ=$h@9&ȃxˉۻ₁E[dTkڧLx۸[j'(d $Tg q (p}AE8;4K ve/H86HPpށ(eqڀ_%ϥc;ca>[\Q [^jfHUne)p9h xvЃ&c ҈m_g- }EMQ< h։0 [ܺ,}h1i쨺>!  X/ixY9.uX> X‰dTkjҕjjqk^GilZ%N^IR:( WO @귢njpm ͞тEtG8s?s@tAtB/tC?tDOtE_tFotGtHtII¼K ?<((u(uUU'' IRrx; ő NO&Kq&bdoHYq"hgw>hD@r*0$hmvZe(%jll7qt~L_s{]wuwJr:&ogq#?VhiH 8A"~i0H:I(:XK?#/ؐ_duߘ) „o(wyp.xT-8G"rNto+~opow\\{{sol4Ow.vn`FKbȀ7=:pSCYՁxK9l_ Gp<LSKY.LڷBwyOꐁf1 h4AHhЯG{7{s~3iJAЄIHHY hHWDK]ȄD` j>eJ)XZ&E`0P%v.bpq   "Sʕd :">p/DJSP(b 3լм8A΄Z%Aklp!8LŇFrV;SPR$aoN+ !UjC': t!@?xqT-۴6r1b) GLbē+o|夡3i:tܹ/qַGϏ~|_z!7QF 2ؠB4W\̢J*X4"/, ~,Ȓ4a43 -r p8 @JdhRԵ;%Y@;SVIEIM`&i͐E0\1!#D@'@)U|0 0BM+ɵ9 ' @aa@!)Tw9Ц~:9Ha`ʧ*@d Tt3Ky~9xX׭^&}J7"xt!y.{{Ynnv+S\c @?3sMN2/(@$RQ% 0I,WLoKWZ @`+K{WV34NfP 0DE A7p75*)+5>E^T mplk0qU@%l.@ 68]@tۭT^ 3a\b; @n&x^Mvwku'H "O}}~g6o^߿|귯=uȟ0i\E~QRG4b 9&fdYz8XEJ5$KT;BzXfi H CD!p@qPwR)^L|fօ Wv91~W\cǾ˰lOd'66&g;[-BYt.JDDL"'"@#TAYs'Ʒ|AZ@9Be؋̠ 9Iތ@!]L 2ʀ2(G2)%8HmX 4W Z :=XE^HৰѯqB^v5tp֐<7 8~XÈ&p!Hf[ ӣ+a>zmd}[Jz+${d vqLdaY_d+Gc ?9glfJxV_G1 @6x QT Z5bq "+DLdځC#*A8$KcМ88@pN)YJ z؃$źȴjPå8:a4Gv$mF!$u:n%7vZ S8`&@>P0M0mL,HvCl4&:h n2D 0^pP(ښ",KV*-Vs=fqM)Ue&#w9e<\ʪ$_(?Y5ײQ~#p> < # pWT"~AX21F4bAt1`dQ+:44'[8Gup5\-Kuqz.@ЁD"{ 5'8v C|]jE/i\JM4 -⭖@34 |4" 7/o(~ ` P )V[$W@A4a`[X4a+.ѕ!aZ!xU3  +]V\tP Q!EX >'t  "v `˂,D$v!#N P$%rb'z' "&"b)b Cם₄A+b+B':"gb-b..-/b0 **A1c1z0*22c3:3Bc4J4Rc5Z5bc6j6rc7z7c88c99c::c;;c<c>bx>Uh@H b`?h2D,a $oA &GG~EX$E  8$dKV h$ NdO%@X@|_x@Sz FAE[ZMDQ"RjWQ J8Xux2<@KhM\ElFGZ:|@< h`*  ,[%ebffjQ[L0ATRB rė Q='oD[.H & _:aJ*o~ 8g@ MP@8E@'bl$npfgy'6j$LC$@@C* -B/$B-'BB A B0C@D$uT0HAfE1 &xemƍIz,Sc"DAHx@||M_<L-FXDw OwI5A "AK$ @~k)~ׄ(T`MDKo8@DH8@d)@ET7z3z7hE8ׄM:[yOE EyE ΅CJЀx59|ğ:A$K::E,J@\ZcP5e-A4$@ != l}WG ܎x@ @S*#( t S:%T,š_:nQ|6Qfe'gKY$8h( f/x\ \8I!P@@B4EE@f^Ĺ;E{+H;S|@ < ;ׁb x{Z<|Ļ짇멧jo{E&A~! BPALp2! ӏ.|I/,B͍;(,L `-`XaB!P̂pb BLTRkޜrMR 9F:t!F⊄@""!?J X*vzH :E9u 4]V* :AY25 @PaZi @!t`7@62cXbNecLƇ"V\u]*!i7eZOҍ-(Yj`_r+Pd!TXx5YE&?#|R,2$׻-踂> 07fUR=pϠ%!Df%H)ȕY\%i$i\1fv1&[vم5_8|⌽Cl$.{H9#|p(<;K@~86]7#NzɁ i M^+H+S")l`x*P߁Cx };y , O><";8!h&n@ 4X||+HH7*G A';.CL` ah kXH+C2rC[ *E>Qಙe"wKP`7-T*.($*/,X WHCA46( M`c(%U:X@C , d5{D!L*cuAЀ@X.z`( 68@1q,J*R "BD ASqƭ* ւ Ea `%#P , ӱh @$>|h< |KbZӜ{ vEZZ%"=J+d3^F-PD`э^H^dP6%ViB A/"  - 4Cuv.,0PS a\eB&04%QȪzku8r2< @IHT$1`0,A4bӣ ) 8kPؘFAҐQ/\*d(At f*,s܈{IGOHƩ<)_yBPgz Oh {B 1n*4j8d#V8̎u-8Oyw5!e$/= )(*+(3yG"(H-d$0ػBٽ 4!Et!P@EP-pd CpNPzH f13ݢ.(hޭ*N.4"H "ԏ܏ $/!/oV JHn Ә6 sp= 4$ N޶*I P2Bq]M 2 ! "rJښ D -J,*N7 ކpvfc6#6(g8, [ "8/NJC8:"V`MDɎ@  P8@6V p\hR&@cCȭ&! 9.ʀbM1!j  I!R rqŧtQSHcQf `STh`NIP ` P#,2!e"q!@o&kR*F+&K"SL%U%@`I< B vr!bѡ !"(, (0@`DĤYQY\@ B",B/tRKNGG@kFo!@#9<ң.F *t#hNKLG?[ dF M SE@IT `Kƾ2!JMQ?R/U! . HIpOO* ?S4U p?I3}kV$`BŲHQTO5Uu#|lU1]E W-Hj`S d ,fWulY`4!ST[O[s#5!g` 4ح ^_,7!@ N.zaup7q5{G? x 8@\V!!V` + ` aL# BڋKGu!{+h}-Ov%N-h {`Zb; &GĒJ GR~R!`i$VvGj sc nukE LW8~8XU*X.X8{Ѹ8d>mp+z?he$ @R!*3(`}+ Lm!~7x'>" $VtlhB A ^n đMv !a .6&a\ fo!a!:qX $)B; rZk*sHK윗(̠Z!m $F9[ "oG=T૾  @qK},E7677* T`d@Q@,`-6i9fE D@xGbzkbд'jn$*Kiڦ Hƾ:KW!Xڣg ZѺq@  N7O| SH+JԩlE@:-5;! @tӚ(p,(@Z@P&>W!dhux`Yz#~PIqLYOY۵ } b *z \ 9 S "G RV6 :@j.;]X!2{#[;!;B+繞JTEZh@hKďHS:Z[1u[A\ "ιJB @t۷6Å<T+a!\, "p605t$vAp= Ԝ6%&v`0 \Μ :, C\<Ν"!"|,``|0@],63$f}!P+ (i\:{$kB,Ci vҋ`> cbb\c؋%N؅΃]!]\+N!r̗ .=#^ًm !0` `6LV K;S>U~0cOAe^]A @zU^T{ B[؄~賞ʕ]y^^מ^^_ _#_'+/3_7;_F^oT =sv%yIiZm?E?G0jPEC@}{k/uiVrt?͜\<_??T<ϳK?8Nq|d+%e H*\Ȱa"` E3.cA=fQ ɐ'7$ReH$Ybɒ4q>L@˟8u~8bωG¬i0)љKB=YjԈBoZITBF{*̡^Ӫ]ugSZQ2EKKK"U]~amGL|*!Or\u,b緄;m޾}n^ƖQ0v] ljM6 9x_7}k[ƲoMtѺi3^YYznѫWŊ l@<}bѩ&LG_Qbq&[kQ5Gjmׄ[lȝ&aWc#_uM9`!B]u VH!*7_~3jDf(\8߃vxIY=i~V_Tay]آ_ҧ\_禉dNrN myVEYgx2f֐!s}gl2v%%eYI]UҸ鋈fxcG'B{|EF`@]آ8avf+.zڸ+]B2[bgPi H*TXᘽ)2 q򙟖:ۛYxVj8K*z(2-Z䡉6zU.ƫwB9pNҴ: (\.fF&ZhQ (,sL)NzZޕK197 0"x#[jzt 0.,U l[AX"Cڮ3S./:.`4CNKB,/.fę>v)]{S7l9?]x.,m)7*~0NDžUvKu3yY=ۧ'~=oH*]O*njg#.[ lw.)gԞgxܓ]o ozTkb)k[¤OU, YyZ /xtR u# F>`8Ŗ/Żӊ/$-L:Ěח+jf#Z /h4ҨE0b0T]Z k(La\pcsTG%0lPg\*uμ䚌(cM)GL %׻)2X~9"C8'ߌ&%Bs:u:7Ȝ{[7NoR~/&t"&8&*  4 !B|\@I|K@   l/].St4D0O\2/mӾ@a  ln8@`RT *֐`'pB y@D@ ȁ`Q*g6Iގ_[kPT]ꮗi41s['MJsI @NA5q6p: (Q X[MJ6<8͍Q99}jr\TnnfIζs iHC 8@$ (P ,jD}!``pľ杌"}znV]Zۙ!l#ܸ[?ۗθ7 ` 9@&!3A1@!ܛz9p3@1:,f,c.ܱ17bZ'S^,\9s PT'0@T ̠{j$/Ri`t=g|Ihmmz+:7+ȕpY Z` GHY 00%6 @8  R1p O(" +Ȯ6W2jrт@ /8:`1@dx$#*p6QW I;}6JW2ng;|g9G@>DJǴJU7$X@z x@0a@9f^_\URocHU8XDXeQ,pQYChPfSD%R\؅^IOb8dXfxhjl؆npr8tXvxxz|؇e @\SRA]z G/U `hZAK0p q o@ ` a>r otTF'0  ho Ƞ1`z @P`{HzJ Ubed!F(8PЍX Ȏgq @a重E`r p puƢ 8Ħ#  Q a  ,B œ>- @I?v P!^ak q 1X. 2#` ( -0p@FP  q D``  4 0 `oi~P9 $ @ə),I@n)ɐ00NP#5i}g!9P:q)Y+ْ/ > AxQ i)_RSM2T *0 gIYd@ Y Q9w BayA 0{P Q :`$*9y9K '9QjEԇ p y 0 ڐ ќ'b>A8  v 0 p vQA#*(IyeI ~Z P 0  JIzC!{ Q ѪJ o 0z3Iz  X0 ^ZZ &@** PאP 䩮*J:D0` 1R& T fA=7^~!?* `Yr8 O ":> %JK)i7"ʤ"a:F@sZw UkIyҠ1yХϰ 0 ж;"`j@pC@}  0p0 ` J@htj@[=  ;_` ` qx;Aۻh!p a0 0Z«; P Pכ۽﫽;Jz E ˾˻ 1۸`[:` հ @p l,ZC + 4qg  1 !L, Б^ ʣ,~%\yqלq [P A!Aɖɡ_ (  <mn %Pg $0an u` xdA 1"/LY0ja?j{ I!#ć[ E+@-DAQO,,$p_ as|50 6/ AxZpxT 9ζ 0P M  @ {ש,}Pmpɛ 1ٓ] = $%  λ]PɶMٕ`z @ 1ܟ< pٽ-=غp1 - L .C+oi@ &B [ `(.2|040jQ`( ]XDH]G ڢQ=E~V]^ 0YBP`1`gMaXF  p[ɀ9c bQ5*>u~0`0 `I p p ;x`  ; ߳Q阞  N> 8 -8 `<  n p }-P Qמɪ90 MخM-, PPm401uC[:-:@GMTDPF@; ѱ,~@r0S^Xp..mX6s" A y^{Wy4 [p'T0 Ɛ.؅:'ީ yۑqos` Iϋ `!^_u,= Ip/,Ф ayf{@A= p*[ 0]@kP!@?1K43`A$8 >$S  JP@q nPG ۛ`LB*X%c¶V7@uJ쇿|%TF9VŠiSۋ:;/[$ýnMՅZQV.#,}jw<9*g9'XrfKB1vc %myxhR٢x0O׻ҙ 4C}Xh/]]lKV7nxu gLgt,b?y'՝?ekYp*&rIx#Ӂ[&Nm#*/lUO6~26F\=-_Nabv(r/\)jfz"dQ]ȮUWMvoiQZ :/L+e[c=׸9)=絿ᾬO[|߲nv̚o㼯5kҊh/yd_?R:K;㚳,<3 +=(C*+3 5 㯸j[@6l7c1,[3k;=r?1?K?+;Ҳ'0+̺R, Ac1B2cA抾01{4=ӱb.D;#[7B3î7d@#;/;?$B5T0s:0<7*B-$DH<{[ ;*; *Y2D+;72VL3=۾ 0*;E2(}۷\ ƚT\>zET-7bXdtbQFfphoBr ud&s)5ntGXbGqG)":IGGq9dy4DGZG,|SȇȈȉȊȋȌȍȎȏɐɑ$ɒ4ɓDɔTɕdɖtɗɘəH"H3@:ɟʠb: @8#@ʪʫT@ʇH ȃ3P2ʴT˵dKGR`i/p,pD H0tDŽl< ;Q01p(6`JHMK1H; ͡\`!LI|ό,I035؁3ΔtHMP t)P:P>N(ό$,z !84ȁ>838 dUH "p%OM.HЇKh5 ]*M(+@7Q Q18$Q"Ї+U &9"x!-҇%p"HAhx:AH,BITPVEVCydh U` Sӕp (pU$(%*(2P|Ufm =Q$vvES}|UXtWRu' } T` V%X-LOEцe8eEpa ]HJ0֛YJ뱅XHQx$YpZ@WxG}_TFPڜZ[HYVq}-ښ SXA8M#(p8 H"0x\U蠔N(\Hp\Dž֥\@ʙ\\8QxB5]]\pƜ=NPޒŤ~hy!U—h^YʀȉE5e e[ᅧb^_} AZDFaH-_@6 B*(LG8jp$!Mq@ R؃Rx(?ЁhW,H`]6a^#u$v^I~h_sȆp8<Sp{mH\^:7@@1P] H H<5a]ȄD5u7 ]6Ic?dUBHL8`c9 ՄIHPIdKL:v AHr#[p^HZPdP~X֗xhHDڀeYeYe_`:FZD_7S.fg>cJ빆DjHjld`w~E= p0bAJI0#9PE?x`hhE!aid+0@c5.^]?" s2ƳT PX6VO ]0 ]I#`pWWLd@k5:@dpMȃQmkkri^DX~iYPTkȋʾlvkF@ J0@YTAjm&mZ UYHZmmVeΕ]iJfm!nFl=n6F fk]N^ힹv^lm 00N& 30Xp8镐χ ;Y "hPX(i`@(l~mi))i ^.r6i(> *ȀR7w_Zv^h ouWp]@փG hA{#{_Ѓ^J{QsD8|9W}`whP/|v1o*zZ?E8sXH'|vտPS` @1`JITH-Ȃ 86O!:p28R`؄.\Eq^95]~Շ1p ,!0@2l!Ĉ'Rh"ƌ7r#HF'_$S.l!Ig z idRƐ$)Lj` NHȱX7 ĺD Iɠia8B SQH",H5L ~5`Qdheԅ2P#d׼{.դ 5r W "0SŘ)SK2|sYמ ^8CM"tխ8n!]b Y ~>xmԪY]`P&"mY8\"B0¨FRtC-$G N8_C]=FF8!,8(A+1PI-@SȔS!#A 9$EI3'0<8 7DM8P܀P@mѣF]fP"L 5 ׈*Q11FŖAniI"i'xc\yН8ĜsS-Je*,$m(DAQq¹֩Alu Kn-TI8ʝk.tCBX(( hvfc-`*@^C`DIE< C|Bpz Ux! ih )z&FDF @Aepf!<2%|Dб4БP#-d0 1DCKdiPN9'MrC0 AbQ#бPa+T %^ʨJ{VF֩Ƶ`sD=BL@u-uE:d,lTTe|P$3r5 w0py 2 /3īArAy~p>ɺ|=jm즙09vox+o;yq}p!  ( ^ip@VJ y .$ {$0G ) ,` CF5`_($?NX<@bPӜ6i({! c(&هPXa8s nS#0d@ /bA#~.2 Idb!z,ш]0p?2[ЖM "xEkΘ5.$xb1dbTG.zq"nd5S pB@HC"r.1ms%7uJKbR0b0 "X"\& k4 (K9@6r^ШF6P!(EPb~)JR2_&79\A - b8(l"x @qI+VaY!`Aa:h![8PD*JAHA/=,Ш>$ $0@؀RъXTHME>)Pҍv,@- QClZP#r2ĪXH0Z1ɐ X:u)F+!\7Ն5D` <acH_+ج@ _50nd(#$ ALT*jSE6,7ג!-D wkW"QRITNPoB2d!.z}jΆPU@Y+GeB}ӫ/,sD >]@3~0#, S03 s0C,&>1S.~1c,Ӹ61Ï8" ҏU݌6;YTJɐe-WO;x%y'Қym3#݋6SknWW8k،9oo{9_Ώs/t/ގ6>r2tŗg=&1妓YHtQ(i:u%TFTN%UVU^%VfVn%WvW~%X%A <):%[[h@CP D[%`5]2H@^xt dN&e>Sdx#tD|,aBb$G p@e&mfzA DA@@!fa:Dk%$ȑmfvngPyp'@@&sb 'q'}֧}C& B@ sZ 't}6>hHqAThs6sc |B(>DC\hyfCtA8z.D @ () LH(hCȁxR(,Z0 @v@B< B)CxDbj,D\ )V\ i.*6>*FN*V^*fn*v~** 7:,CHT@$*D/Q*ErܩA\@~A%eB5C%Fp!CdlC4 C2VHjE |UZJ @AA A4(TAg+R+DCȦuʪ+)>,Lͻi9Z.P2hC*0`ɞ*xB$.Œ6,1HN1.\PB2@!0.8C` 8^͢',ʊlAPONDʤ4-l/GBDm3ZDqC6"X*A|X4.C.0S(4B/" C`B8L+gT,B* 5^_+uGЁbC&(+lx1+̂+B&LC5\5uZBv_5xv\Ch53oCP9 "B/B$ `{_9*T,B-Ԋkn6g(nS2.`` %B3Ek wq6i7tW7\vfo6^\77/&XC5B8dw7r6Z8}[cG6Gh &C,4DrNKՏAXOB( gb+6 nJwtlK0AHBy5X;EWuXO A ΁0@CGX@t AYB&mA4C  C@p@&t+L=(?8gԀA @P yKS3dWL(P@PFaC2aj@=Z-Ĕ $#HeKcĈIp ;r~2@1 (qaI#p1#]MڐP#J a EΖ2*'fѪ{V`څʱ!ZʥkWiD hԊS 5H<« G/vL V@i-1 O %",Aa5CuK- pG:HC,oh8>Xzp E[3%*蠄2*Ol!P )BvD ?y !oɐ@:ЗdDG#8"bʨ˨҈\BjI$QE$dI*0ʡT^QŜ/[SbX4✳,9NE0$,M 4,23Q#'5`F;T hP8 HB8X.r(!*.<^"(Rîg㛯%2jXHБG|,|{W}헥G}@0Eh:aEEșd<˃a*^0J#edf (q҉Vb RJ<-rWyfvnyj!sfG$&K$bܢ3') 0{Z&HGKҏ;$]h馟I83!EQ&goOЋ+z AHZ@!fB Hص8Pbȃ(fU )3NUHYS`H<|+p #F`&6;}eHP Xp&W#+ُDF2_ t!A .`! ˠ0hqy8<,&q0aG <Ġ@dA#~М^֐< # $2"QHPӅ6%1H,^a=dbh.XFL鐇>lq=\r `.5ኅ(1BrvG b?1IїG"Š k4$! 92Q%-E/qU!%Jl'Y֍҇e"4+ \rxA xBM$(G0c"8*< 4l" lxs|ՒgH*"T@IuNT:ȧt0UaXb!Y>V@dI0Q5NV{ YB' 0V[Ζm[ s0>i 2|▸%Q e)*T*@յu]n.^5yћ^u{_Η}_`6`/ v!a Oؼ7PI 4DR0blO_X0!$qp CR"8nɎ_|cߘ"qdg.)i)BI9qmb*Xv [A[fZWֲga*`l\g"i25d28N|:Cr!@;r+79gFاL:ԝDa:t>ri֋А-j[ZJƵ]DɨuKie{5Iںѫ16a grcl'VmCB7 yyݪmk ޵lS|n{˶*^@?[_3,q vAn3OhYS[~y)p;Z9z;-C$.Cr'/=bFO>?YvˎC\ަ |ng{d=k]u?{܋nW;ӽ\UO6xMO.(N\D>O`NN'///MP<0/Z0$r0pQ.̮L_/'CPֶ. cP0IM/Xl/ Pn`o !̌O o~]1 ?Xvm輮~/Pߐb^PB6J1OXORn0hp<k* l #TcO:PQLQ<iqPql>j#Of  ynRM,,/"KL"1Ҿ! 2#( u=$M$Qr:/%Y%]%a2&er&i&m&q2'ur'y'}'2(QR ! h))mr 4!> @  2,r,3!d@~@ T<r.. lJP j' v `!%@z#8.)2-@ G@I Nڀ ,Y؀ ,! `.s7y7k D@4i4b1Ybf ΀` ||3;s;W |!S44Q%!T~L 8as>>9Jk8C `5dĀ pΠ>!4B%^z`@ 0?%=` &tEYE @h ~ 8@BSA@ x\tII @,`VfrT` " @@tItM%tVtNNN4OtOOO5PuP P P5QuQQQ!5R%uRm8%B&7 !\`TQհN!FU|g5@N UKU 6@ X"P^u'@.#d BUB$ C)5Y{<l1>D!0 f ,@J/6&@d%D[U %0X]"<#iBB` ;aKrC\u_($a&$tT `Bh-u!ULe%`f݂`!`\b#+<pCF A|D0`WR`7vPx g!d U TV  "l@l`'j6=z6%!R/@P`!bN Vr/Th' ^ >@2wV b!@ Wrb @@ ^HwͰja`nFiaA!C@A8! : Xn *x t"^W8(S8B\@ M@}s-```@%]cg"Z5vAYAQD4 z[DϜl^Dm R e5d `!h`(XP`n FD@ <~Cٸ[Y3@; rCz Ҡ&8:n`!` z [B,i8Y< ēhCyDgaוbn5@V] W8C ʀ9YޣZX @V6 ‰%f׃Yfzg[>C /!k%WR Y l GL }ᓱ1q"a|0$#Nm ~ $aq$'NQEO0ܦ-ۍB^/Ep=#Q-Z9^7[૑<59!#Ký6Q.mi[ uW_oP A +O; Ar? PcޚP/))p#V~!K QU?n_Y]ݠNem} j0{/~⍞i. "PR2oi,h0! :|p!D +"<("C'qcȏ%RtċUbhIi3Ν<{ 4СD%Hc|bȦOu&)cɭK:բϤ,6TSQMْ٭?U{I252]7mܶ|KXW`F4'7&۾#JlsL=:լ[~t͂S6XT?|vƑ.4Eҽ9H![xm]9wsċƅKmV?Za` zt5Tpw\{%M]| ʦ{aġa e]Bw؃)d be+v !C9x"ŖW*Ur.dN> /z-h`$RbV"ewVdN7AƘ"~jEe$hyH)s%Juh|!FwG\yP^ij ( gi}9XmwÅI{ș_ fxX.kojҍc2y@jT.i*j+z稛~ nₙunk]+CuzԱ -{"6b_[ |F(v8pczOp>0S|:"Lr&2j] j)e~Lr0ߌs:s>3@MtFtJ/tN? uROMuV_uZou^ vbMvf6DaWEt3viw~;EhB, @T ሏ;E/oy瞷 /NT DdN{dP(0!$DEE;|Ar 1|':F !!)`/='D '|1?|tǻS {pA  n0yϋ,xP SX%[aGu @C  ?@o-`sא$+FA@8( m`D'@) 8zqA$%d=)Kc>*@Ӈش_)OjVHI *43MGևJveKQ6`n# Q5!lC F ) ,ljWZ&AҐ'f2:@G @ #q\ @€֣qVA kwujUZ $/| $ /p][@ ' VXţ[xxC* ^FQ Y `(E%#].Vp#8;qB& 9` #5cA;x +8(KV@  \x@3GU6Ld (Q3;8tP;ySns-?p# LnTT}E6;lCĬnUӃU8nhbg1JtAGs iWM h*ȅBG8Txb7!wD\ @j_!I3:Ѡ& ql{~<&X^C4E U Àٹ5Wk|I@a~|Dp7Db8D2x hBP\F fxW؅xb@* x@. ɬ7@.6;: t "`#tĉʣ'.;w< fyi?@ T0!$0@؀[(`e_xj|i8fq(s%k%9v(1}8 vHH-PW(Hhȉ艟(HhȊ芯$yPH%ȋ l`H *ɨB i*,(Hhw b$q(yTP)r#H(f  .3)y`0N@!H() `2)C sx*!+~F@Odr$’;ɓ &hq '2r,=K9VgbL)SIUiWY[ɕ]_ a)cIeigikɖm)1Z@pwFj[8p zi}y69qI%E8 RAz.P@@H@v7E\f `㙠 pSBCy<IPI ٛ 9BH 4@^IL7n ,$t tG0 a`y'806 & w5m -0p@N`04Hdl)Dp!: ik`AB4upBy$@iJJJt ]:pP'%4jp9PD /w)]=J UU+ywP0* pek@k ?`]b %ia/"@ HFP%{`ܹ*:`PQ[h@T5`>ѩLjV;3P+ ov$zj`$B5@*Z +{1UDiH`]PSR*<y QPZ50Teu Cs҃V1+JC{ pF0(!@a "+@S>)&F\~ʴN RdE JhV?D"`u]AA{DI$ qKlڮp* Z`zOT0 PR ͪWG l9`0 Z$2'Kp*/ Coq`惩DOdV{ 9${dk+T  ɴB9IpKKtG`i{JN_ l`=@Q 8+= QHI׳mdak+;@Ѷ Q 9P S`P%fdoO~E40b 󛯌뮏g`f%;&M쇙 ?0{pq:WpsAT m&|X L! "p^W|W APUpLn<"p 09HOKP[蚀UbLi 7! @ I%TCG F* o*Tʧ8w(c̩,rL"Kh,Tk,%S47P WU[%{W:Y]P H Wʣ@A {`V/]0c~J9{ +ep| L9` `ʜj@0ȑ픡GP]P E 75]9=ӿ``ݨ3> :إ]:A \/:phpˡ[oV8Tmk|e e<~q=T "MһTp0ֻ[! gW e^` F:(P R3j]ϧ *`b$pk[mXo He`ݰxG':>ⴻ Ҋ%i]=~kT0bKC`_e`6g/\CJhMи M p9d6XAfMQ˕eIp P[s8:- Эm̩+ȍ] FvjV}fp&g  s=k!spgp;c@EjU9@k hlx04mef Sp:j)@`MEe0m6/pLmJ @Np|*g& #~wVpy P@ 1[ @S6ώ0M^%D[ݎL8  9Sn qrs M@Pݎp;^pKx' :6  )[0]9@/]KtU!:pFPBCYwܖY5'e%r*+O@ 0nv]^FW<M@yFמ5@.pkggI߃pS$ou0?,pG A0AS.!'n"$8{uC* >a! Lx^=oӻ$  2Ͱ#C/~FXY4p&]p\kAƏ_ǿͪ,/R>p @_@PoFJr-,=#EQr.yկg{ϧ_}p@ 4@TpAm A#pB +B 3pC;CCqDK4DSTqE[tQD^qFkFC)G{G rH"42TrI&qG'rJ*J,7L2K.F(sL24L%DsM6 M8sN:TNUbUvY eYvZj,\_tZn[[p7m5oUw^e Ӆw^z7uyw_~ _ux``|Fxajxb|bfxc;cCydK6dSVye[ve p[(+٠ X"Z^Iiy>hމR(l82 #$`k&@&40kN[6 " `x»Sר B )(,% .r@%\\H ЛoLۻ%"@&X 6hvo ꆄ>B ?Ӗ@pV{&k\#~r7{;i!n7i8pSF с!YNWvp 3AJ dfq 0\ jh7pg%MȐ~V.JP 'A 90P8!14zHc6| `A`G&I@ W'e$m' `pcxa# IHC$!fs"#i: Ӟ2&6iBJhD O # PeI MҘd*.y)8X@#D@R#; TȾo`>c=[(& ӱ#b<6F+p(6h& >; ,P00xR/PA$A @9\AX ;00!)2P PX2"4B@Bk 6h'A"x 1Fă-(&|(B6 @604lʧŃFjT*JT";"ȃ8<דcٳ/hd,D轓;oGqԋG| )Il>6du!E jɗK4LM4IFa$nj"5ȁ8;?*.X2Xhť2L:0Nt'&ʅ *0h0l,s!H ҫXOqOt>NNT/rP)PEPsRpD2*Ѐ.1O<,zOHϓWt܄6E(0q၌`+#U " 28xCPPPh-P9Ix-)G K @CǼ U-;ydG0 5RN:ӃHt$:SbxȡF h+lflfT\Z=^[HHHz*q&gsH3 9-OHg ȖA71 1u4~h?;P.x0+ohEagw&& H _>|vq.g).逶8[H`MC&gg~镐ip^]Z;,:a2ꌋfd c@3؄蚫6]ҋ'< bXh%(fH C%ֈ 6aalT$skV=*0)86ՃE5l؁ב4j&l> *1/ F Fꦂ(У[>^x9838P hU0kP;6ws,F >l|21+l>pb"KR.XIv N:Tl$&߇vo/pOa#(ZoMR>v+%?@@Egtr,, Ń  ߛu@x?Q/*[?.4tn ǃ/m*Aum't[x_ǰw}sKPvQ0 7Uzw_bIpUu3pQ)o)Á8(8qHq#4yGq"8W!:2?F!OaOK&h(H2\{'v@g( r"/ )(.>|*1P W88Ŵ{>''|r1|r!|@~lq|{hWg|G&{Hgz^0g|18}tW9`ֈLG1 G'0,sa&.pHy ($kLㄑ G@GGx4 Z(BOj8F& ?2XI@J0؃3!!)CΞ"s @.]9ŌCF"%)5AZ0B@%,i&%393/2PS yH7E5NL$ycȑ 8+f % #G ?>RN\fTM9Zl mh!tywSPI y!]q@yFU '@ 5Ѓ<@x")J H%f FD@`ЄyX+rVe) e^Ro)X&pP֛ph@l 0&R DX@j*pE huV@*"X0b{#Y@ "HX|ꨥ 5$D1J*2ꨪ6jF^^f `1.BVKDg'`#Rz عĴZ'^ABъqLf B6A M,'|Bf4l 8]BƖ x ZpI`ln+6l/[W֕G-"\e`t^))E<]s'4w͇_L^ɲ0B؜Й EԨ(e Bw⛾%S 2_˞)X,H2 @s%q@N2W tqSa-n/&T)U4BmDuc XU_-u^Tb PkF5}! @bDB'%D' &iBB M(>@4l&d5 ۑ@K|.!00PD܀FL.p<7)%v0E@QaKlb F5ijb>E:8W0"ɔ4 !K^YPy L #S%-XewI)99̀ qAB~I$Z5 O % qo`SCql+c9ˢ={@9@  B :@~NH4%(Pͬ>hhORy@B1+R;O:yr,A Er+D`@C><YF!!vT^XAE'sh*!k?@&`sQ^7w+f& JPC$qS Ie7x . Btdf` BB Qh8 Z/;29GL2hB)|̓Z1,AAB<lhgtC,!!B@4 d5;nwْ-H 7#zϲ=L A/ڭ <`g8v@L$ tN5B40M #a 8`O={_Xgu or~FdFCX9 M)aXm !x Z Q( Q R%`f` T{xXԀANGG``n@nTU%D4 tH0<!H`۹Aڨ ! \IXh ,[l<d0$ @vaaASRI&%SRJG.10}"b"*C% "@"Rb%Z5b[yX]b'z'2d(b).e)*b)b+b,, -b."/c0[/ 01"#()WGA @ڵ2EA4P*c2rc7z#B Xdݕ` 6~c<.H59 F=#;;jc/c@ d+BAa9 ) I؈!%D Z%=^IA 0uR)4@ |~@`A<O'"pP؀[m!cJ eJ( XA@ ȓ@0M`l8 B0dP `ʡ BT ]@H].LR\ !åG a&$TJ!ไ=LtI|YB&afn%D4gЈ  t $@ x!DmV"o&D@'B@@5ItL\t @A P]Jܦng}]=^ш xM_Yt~D•NBD^@E@0m}hf  ~.PB܁ 4 $@Bp`de`Y\_BAd |<@ D5ɓ@P*Aڢrif(rDv$W< d)v m FLcLIOY"|zB*l)WJJPaˌ$8Jlt \*JiPա XF dihjm `bVI 9B8Z뵦#NQJE\U+kΖYAW ָk*vbM!, (*6QS[!",EFPABL78Ctu{HJSbck꣣㞟ߔfgnjkqIKT潾46AMOX=?IKLUYZbqsxaciCENmntz{yz~8:DοVX`FHRLNWkms}~&(4stz>@J\^e_`gNPXoqv-/;{}PQZ+-8xy~ϕ@BK]_bM35@Z[bvw}PSESU]efm֠h:A>LODrHLB_58:hip13>[\coTU^[{at02<ԪCGAv{VvdohlP󈸸rĢ|^EHռx}X=\HnGl1\F7QT [G2L*;j!MB}X[H:UEjHAϾaݭHbÂ/ZRǏ CIC5s0yŒ0c̸KG:̟@ JѣH*]ʴӊ- FO+Ǭ4̜<Zf*E\xptDI y'mfټlЀATdk)0<$Al.%8Xx ؀8Xx؁ "8$X&x(H~mA|Ȃ uOA*x8}ELv"a@hAlgk2h(CNaw>HMHW6'X[zeYO8&Gh)xՆ)ID}5|kv%wkcHo(av]x`(vBz؈dx#6 $!}AnvXod(c4H6ۤn8nh.(X@flxf6x_񊞈XXBDa`FkU`5[wVǍ6aPh:&ēAȆP bɁd@QWG눇tz(5(7hwx`7Mg8nV~@iM21xLhW8y(Hɑ"||v19LX0Ix֒#q5 (*鐪ד6%!gS3@U 8y$HBQ1a`RFuixfW!yB2ItËh)X.v㏁ɐ;ٕhy6zY|x)yyڡhW)&z(a)ڢ.02:4Z6z8:<ڣ>@B:DZX@/pQHq`PWu1v`FJ$yq VPWJijaj")`1 QYZZ]zJHazz*|?01zw1p Ab4VQ*IJPWaj` xVV^V7QOh4gp|!0 Hf<3ʚgxɺ **`0*:zKp7p , ㊰m+P:A:+JF09OqG0!fQH!a4f>k3ЫX4!vJ.p1$E rE Tkl[\; 9qi;Nt Xsk2"@PZ"vj[`ѷ0'' O+DI B[EkX #QBh<« q l)"-b@./p'0+0<+K%dKnH0<(-+a pʾEJ@{^+0KY):a&q%0"#4rM"Tc2$Ea# $qAѸ2~Zq#\&&<=!<B!#$ _bBKFO#LLdRb )&nR!Bab'ZP^`z^eU-$B_p"wF LJ# <%2m 'N~J A`s2)s1?431Y!Q4m0P "o_vZG𵤛_at{LX/AV@sT,ʊ6!lq[t0. nI᫕ 42C3a3 ~p3--C <0K|a1}&M?.#M8:uM>6"@;># -uȎ-3R'.`JP-@5ɯ:b/V3\<4ӱKψu>zU2\$3>Qq:ޢע3}]2]=rްa>0m n%0TG>3x { 4\"T!Ps-N#{>C??]!>ҴDgb0sI0Y(/6>.WU0&ܕ hj $@z'$``@HP#1DQcZ%A-,,qcGStD7< I8TI\sp EB@ 2PAVDa; 2a\5-UXlɐ(%CȶoT+5T3,;C6:~X\vWFқ;,4-#LXܙT* J١*6rKo_|ǟ_~J'bHaA‡@H! )hn , B0!*j@J\Aeq )1Ɂ+I@I'd 98AL,;u s%}d1H3r+$`Ft LxJ#Isr***m*SM** ,/xO-HŻJ,C„#j8lAƤm@@ qC=A!h0+0.D΂@8! 8`&8Z ^ \r H#0$P;8"\#f{nm{k#&0J?E I43Ur}+D:#-Y"lbd&m*,Y0X(ysh!h05/fh ̗% AcV7TzKdx3$ U3{׿x.0!Bex'\\ լ5Ȑ6ČҘ4 NjW+#<PgOb8E*J#>KH+P%>pZH  cBHb> (84j1b8ƩH 2ms w#G1d؟.>AHiHA"-p_M'A,J\e+\R*RޤR3B” CLτf49M]7S$MnLof~<K0@x%.q tS,99OzӞg>y;mӟ vzԁl?Z4 hD%:QVԢhF5QvԣiHE:RԤ'EiJURԥ/iLe:SԦ7iNuSԧ?jP:TըGEjRTR*T#k 5A2#!bj [WU V*h(CSֳ^$iZP,5gB Cr5ܚOsf*u0;*?,yZ@ T|,}R;8cZumjneZa@>3 Y d Hhp?` _ hk\rǕ xUQ/{3w Mu!pw  p#@W}x[z73[V! ;`#bʪ`۪$))F>6֌c0 Tx̟HGdBO(P P"A`&;ʳJ|l"UL7Ù*j>@_x* lxf3=`BRҁn\(p! Skl"ɯ^8 XjbJ  H?ۇϴ5Lu!F ]m;Kі % ` kiߓqQ`bo PB` DH0Y `0Abi lW%T)scz;&./2|ߙ\0Π&/?y u0$а툅>LЇ{wa:78S/+oؽk\!z ԽЉ Fuc=fZz-C]G\yy~ fy}p? $io{Cߋχ䣕{x\ן90%x p2?Í!xx77 C P@tH؀%4 >C@ drS>? s>髾ht˿t[LP'C x ,4)Y H0xE38@p8$8* /C1D.к/ 1, p> -xA,Dճ < C79 8$85dGD==m8 {xU ]t p!8K[C> D5lCĹMlf#C/ UDQ hԂ)FA$ ƪ pE;1oDqTR4E @ESU܎xuřtn{ #(A!ppY,!,‰ ). Dx 聁"p0pHxŐp ȍHŀIHh 3DxGəTDQ)8Gp5LIJ'ߓ %u H:9VD C䠺8lJKl?`FZK:vBJ9- ˑ87 _Ex'ɬ9Dz./cjA@063F4V5f6v789:;<=>?@6~ZڪBp#$Bq2A+Ojd" #d=HLVg1gzIYf T *جаM]9꧐%#ڂ槀g=-ؤHnr&v-.:/S.r.`..PI#ZGގF .h C~.R8 l'Ɲ[ewrP(1 fHJ$&i(`V Dp1H ~4m2 d1,pj~C&&~牔 @4)C+B>X4cHk:;~24G4N) @?pa$Hܩk/ ih(~"P5LVκ"&P#5EgfоJRKp&.kX Zp۵_68N#wa6+onvF3X^`$L箝o1omrl:z[XRnqC*8/P 4mj k!}DwVR8عC{GЈC 8;[ h G d_ p#1ʥk: h:JdJƄp08{q Ȃ'G9 ny]Iysځ)0n<0 ? X'i<; ]>kʸAs D?п.bBmdw%l6WIv1wu@(\@Qp2u> t>IA)@w?~J;LΦrn ?124< `D8"/ DDFEP$2ywCġ_0 ɖl\)ЌF"q Hc :6Hӂ<rS!D o xQ (`V\8ux$` KXd0Pf HRgp'ݔAA5K1s4\2@=yr  fR=\G! um!;Phpw?PR 8 x * : >   QO\qQ #x`' _D@E`| a0f(9h"QGآ( |I{٧ы*AAp@[|D?hpH`Nn[q PXC^{&X!eXHA&ǕfAP^)`A4-QWo=j٧ @ ͚RPUfYtYPz:ݨW[6uT;ץ//C$E !P 0@ !i%*TȫRQDġy@t }L)M\1Q7; ?tVĖqT. 1رSgf -1!8EE @BPP3!j Yq@mh;{C|r0] l)%Xd; Q"8>T6k*EzB"v{lTf> C|yqmE?jB5Λ tÃ'漃3\)0k={ \gG![ر@4a"C BT:P$0* @X}VxÐ$R1Q&Տ|R8b1 5zZ Ү6&7 CB\v ,YD# Φ N\8B Tԁ ŇP1@ ANzA!k\8$P=Z,W+ZЏD P`IBV{<ܧ3@ I (\J l@J"2Ё% X"hF45:&HAX!8䒗&hS5e/h{&8)a\؀AH`riB@`4.0 C(=d =NvK|k'<AW. |*A SgArD4@+?j`s85VECD@I݁o8)l ӂO). 8 d<%ԕ>~^.hݬ4lk.x+ޣ%!! zB@Zhr}'daHr(qnYGр%@KP+D՛B$,`2$ tJ_\yR!ؘrp02N~2,){`y V`(Du]v`cc*ӹv3=ArTAA:+U:t=m!Ў43MsӞ4C-QԦ>5SUծ~5c-YӺֶ5s]׾5-a>6e3~6C "1~ȵ L@fжA}8Da1.-! s#pQtbPp'x3DH!*D6JE b "acBb$B'2>D!Da> cQN3bD4#"5Z#6 ]1=H>z>\]h 0X hѧ!D`AD (ķID$A\ hXHcҝ-E@AFd mpF8 dLz.(ThA @CBWMڤT p޸ EXAO* ,8O eQF%<[\@@"nB,`D==<rlH&e&b6c!nY !}"/r&  R2CإQS\:CHA,&p 'q*&c:ah&gVffndBg^j$|M>"\@ %$FHeY\aTVFFslvvwv[D$C8~z~S> tdKtl@"X9%ޝ̀dALE(ucDTgT (8A(4@%|(@ (hTШFL @DE EK @DL jE̍ ܀Iĝ6(72h=9BĐ*")iiQF>D*ĝ H%͕BWpShi^)Dħ"6j)*Īv['yFʄBLBS Gԣ< Bȴb$J<B(F "D`D<A?bՁFhHEq NDQIXK.AN4@.4zPxy̕mpPE\Eh`Fz\RhiB iʚjAPr0shX;1erD Y,Gs<D Rj ȠvDZ_D6+i,HGDC -Y,퓬mF,nnL^0BlBҾmrm*+gu0T ሎ`t~4AhɘS ҞX[Ҍ4ՠdlDsDX% Ĩ8Pm]H5eTiQ~I nBTX 8i* 8K & O$IҲ8 d%XDV-Z\M箐D^BHKD8C//&o%f( j)󖊈^.otp|0 n6 dD̠$+ hLۛbܻ6ۨWMNPҴnDpsY@XòF(qN@:&wqPHBT:1ELEO|Ep HCȯP02"[#.j! > ^11+DDD* #G+mx,qfpD,KD,  VzBB|=6rO5oɇ9ACPAޔ]B>11PHb0Ɛ NGinSF1]14KS!B!a`"Co/~Ҟ4$aC*)gSB,%O4+EHmF-`tBI%+DK4K#R( XN+D+%05Z3Q*G?.GD 2-mD([Uc=K&ѵ}NU2HUTl_D8vlDxP<#AMG-?UfFdstDsDJ|l\-C䮿AP^m!;Q %1[`n0?W`6ob)Jq@rqVVb!@dX8hYklMP QaQBPŁCk}8C@EwBF|qO,)HxgW mٖoV7|~%Chrŵc]UVmՀHƵs~8a7r8ViU a;YU{`xeuIDc4Wc]ϼV^5ȈU5 iDW l9D^I 25ȗ;ŌED?ěLCyG zdߏ7YuyWzٛw3~ Y::Ǻ:׺:纮::;;'/Q4eDQ@0-ÝEt;u{[STPTAh;D d9Ag #HXP˾eeO[H[[H˔Fhݤȥ X}Q:3{t <ޤg7P4@ @yl%  @Xü+De1E?9,L֧Ğ>5]A]]}DD]An%o ?Im)US 7HwW>Pv|BnT[&L \RXqɅDoToae6M˞.xD 5%B@F %E:YD\hf(_[ Xc?~o/#&AGZ@K AȁdD-NRadQP@TcF, 0 @K`%0"#8v2" '0C]`$9c `ytĂ#,SB#a#(2" YhBOG^`4A  i*v,asAѣR0J2! 8Ā_|ђ9d ̺ZZŐ[;ֵS˾H&oݫkϖmPm.ܸiU>zuױg׾{w?|y1(Nsfw fa !2VЁ 4p` !@B 8*$="Š @A28#HZX+ R0"ETQ8:l",A>ʠjHjV*@#" 6X4"" #/Dxh1#!iJ @Cૈl8EL"3FҲIs;TSIi;Dq NR[5:WwUZc}չreT Va-cMVYaC$aM;ns( N@$4?b0a^C0 PB)8:2i|i8`X bA"h#txceHm;DjR[O*!D88o w\U"[39N `/@H$'L 6PauT`EM& [ `֒cNrs$]ѮfU×M\o!X'.@d. h.@h4" `PB;,0 VAX"LwH,}0BXwޫlHe>k`t(bⲣ(u,cxf}އCB7$X@Ѡa B҄G#էf.n% 6+mT[ݔߨ7 $ F=c%`t H X*a!!cL 8ui!~,x_PtHb$ƈm<yP! )AGDZt04Q%H,ZX`)_Dl¿$~3 _D 0C)8aMH!D+%GV0: ""Rpci:0d57^ټUްn望OyΓ@6`V ؃hD 몋p \2 aA 覍 ׷ XܲAQ>&pK1HTaBo:/"^ u" A0;2PCVPX#GTy !=褨Ѐ2AC Z-?1dW: X NN~Ql9MBn}# X2oa{j֓gA LA Y"•3t(`Tԁ"-h1<?`C9'p `HՑEz-fT Ip\tNJ7m2Ub$^AHlK%D`Pj0[ emْ  >j/P!R-\#/}s`B<.b, XO]\Kنhopͻ9%cKSC-'K=Yֲ 9&])-@CA|00$q䁛vp:v&*[ 6(F#nė J0! :FwXîjB 3Q?qHa53H$`  Ri*aO`z@ H&5CT !,"d!p7CʺW8ә* yW6 M!cXDΛ\Xs&Pqgqo"A Ād'  pDJ;r;rÑ5:@r<+RȀzs|AoXyɟXP'Bq|t"ۼlpIp;vyAꐀTg|ͱp:wOu{}7xlkjO![̓G; d Azя0M1үwa{Ϟq{|7|/w}KU1ߑl)iT|g P[TA TNZj8;).د` <P>;T o}""*U8#OiH#/L0<NIl%t9! n9BvJ̣<0 d~'`& `P0BM\ET$r I &  ŏb< E"`* &  h @ESB eERPP8f[-q3:VCelʘ#m!,&-HB4gF :Pi-o#úJni$gHT:b*qKް<;^:fg@,Eh`lc#'64V#,JѳVhZ?FqU) .,a@")*,# p4("(ǚXţ@ z!x~$#F!%P˼'t"H0v0J).)L$` 2C$H*B/,-Ԃ-.Ⓒ/""6+",@0R)862"c22"66C"M4⨱DՌ lPCU81 25k 1<3!o MS3k߮190p`5!=ԃ=>>$Ic??@ AA fh#V1tnь&%"ur c"3#G@GTGA3B"S?$EJH3"q8e$LƤLԄM+G H"O48OkڳP ERJ"fD@R\CS41SV.KRE!6:Ʃ3w#0⮯51ɝl4?s0O'|F}VuN wx1D! 8'`bqF<.$G/Si<0b|Lc1DƒrP}Gug ~NfWm~g#}vcoج8$(Ȃ"4("5"t6͎FUj3YfWWPVTVL}HUL 0[0k^up*-mLKE3mUXȈ艮%bZ銲hu*$ Et^Bh cv*HdjiK!~5dV>1I$WA%r("^D܍?077"f &]i*:bD2##7^s6dps[[EQ.oi^{#Ha1EsqI]'xUVq .5/S(Gi6rs3<)kZ #JufRҔ2Ncd0 X`wFexTjzꢥ(萦,mc|ا~8*+x5J$ *"**"~ I+X j%,HED6"**$!JʯZdX4\ohZW@JUΕXnf^Ax?O _"ZK 浆cJ+N#T^ : x˷L3B"k *vC&d0A:`2%˓kἱ{KLM̽૵2,"ݸ١gWp@ L"'|p׋9Bl@`G 4r-"pDh^Z@Y@9k_Kٰ`yC pZр%6*x!#1[yyڗ?E(ކ^z?m#3_Ʌ5bCL`lζDlDwf2(`@,b2@n:Ө*JNq,;M"L;Llw*|*b{5M:DD`ق٢M#MX %mj`'"m:Mބ/͔\#0`X8mGY[J􁩧F93?jt82 ZHIo4{5攺u26:Gi6 |2t 8>.8J^[( @\ n@+:2 _`bd]VK:İEz<~1G[n P'X6`:o? 9 `T@$͠:P$B`2@DbD@ >qU0yn& >A y0 CWB :d)5@ 82EJ n1԰G)Z4|58h8Anb%C>q@aۡ !̀*Vx`;JWKY˃0HE|9`[m| % CTjjx!N\TzhT%|\ A#Dwݵ[F,8@DG4# e# 3фPˮޑ6eP x. ܲ! v\@Y P:2 ~ %C{˰;Qr<3$E+0d4P@~.Y C\G?N; \]=3tCz@ Y1@@!k#p A}"cW%G.^C5 3r_( sX#D_0V _Է砋~N%~=HǞ;l0i)  O<<fXaV w(ؠH@ TG8A \\S>b` Z0*"u@r4@K ,w4G?~vq,0 +Xwl` HX")S@ ء<)Qٺ a3V]b:P1OJ<2j6%k3{7)HBR/h8ٺ(\ s8C8NMZ"p!t`fP&5oCb3VP!`^!Pk2!-6niQc8MQQ0,J|.@r.҃$ JUZ1`T F0DA07I}6ũN9P ` !Cq( a.T25#3"А]:D_O6[C׫ /SͤցD! i@0 k!k tlZЃ:D @LְXӦvoA4Ґ|-i[݆d+?55̩pXf s4I[YC @I0 `37Dcr)p^@T `Y;ymj`AX1`gj8uo<#pX7 J-FFOK$(%3UPb/@1o|3 N <=2C _D26D iAy )&X ;+i-0c+f^Df3<8ё\DnŢ1H8v̀@a@0;!QC A#ov@RR`SN 4`0Imj` e erkֵ^2d y@y"p^w ` 1r )-iZ+C]\,7pmPjPC Sp0JU t ɐ9cA&&@{/ܱs! 0p )u`0Kb[f}1BI A,R"ِx0OR)sM J@/G)g ,PX0 %o9)Cx0jI9RCc w ! `@kx(d`7t`f8@gdi CE5 adtmOpp'`4f8;zG{ZЅ0/wz > aFwaB=,(,K;`|0aꒃ* WQQSp|6"[E 0b=CA 0$@! N R0Dff0Al+>( ѿb Ҽg*t~ v"`[`OsYpu"#p kA l . 0@7;L vk#Vn:T' F $ `v$|LĤ4(@Fĺ''Q< \07jIn q)| k} [tGPik9v1oLB >LY@*ǁӴƚ*luLJZrW b`#DkRPְ,˼K[sK', A!l \B2pjSWCǶq#rC90dk- `Y5 @}Xq 90hssl0-a*@Ut |`¦pػI pcvL<|R;=4 @.B]7  rPS!0bP"E@LRPd ,0R]A6-s[q}ODmFE}Tpm]ԫwnL7M ̫tK_*,~5>ӛjmՖ=@WM۶/ z~ c090MlPurk! 60/lѰ# <q3džX\ALP=ǝ*s]=|b0PڪЃ $`W7 Q A|JMrS9 Q]tI8M"kbu R 9ck_>@ԡ}> `9M1ޝ1=e46.k;"n|Rjf 5x̀rE],h]|%Ct`|_A%qX`T.^^}C? q P̅Y-`N~łbZ!A@[l^~ A1-^e^ 1~}0^unA6Oم?` ) '+ //_ŇC13-[.Co>OG"`Ru`?oW[_-Kw.u`bikmoq/sOuowy{}/Oo/OodCTQ}P:A-PY)p/p'ت(8'^a~B]Q.ppOǟE:I+kAFFaQ?gZ_oVp _y疀 PaZtzT,>AYE3.Nbq @pŋ)VXBʔ@)J8' ք=eƒ&dC?ǠF)HxS - mXj(dgIPSrp  _6n^$|Rmti}TȒE@ZNZiM@&SNJ9JDf`0il[QbD%0^CQD`J0k8Ev \0$zgM%*Q6&.g̀KgcXFֱ!(թX!v Y$` `PDDPy+;hpK+y(5rJ!, 0e kI^P@NCS$bՌvSҁ/c J4?X5JYEv{"Up`MI{""/}mCD8)ʻ-^S|0LDj%6kJT@ yy0dڣ+:'@zy<PϛvKK@ ؁p ѧF `ӛٶCj., (_m4`n'ܔ4&| x>C2Ϡ$S(b@,o) '@%_`ک'U>0:XJ2H$E[V9ܾR㗕/W|GtxeT^u~Qҁ6g㟒4_gc_-(@,@<@T1ы h@lJ@ @ @ @ @ @@@ AA,A+3$4(4hBj?(=X@̲B)tD;T@={D?'=?,qkB!CT:$JPJE,Z 7'Q8xBPؐ&/ AC DU0Ȋn lȪl i 0 p)0dImzKlR\ UBd =GId$>s|tP\HLT&ƒHg|Kl DȄ*r `$ (f{lIGDCDHPJ0pJ pJ؏BNdzLzlŘGA̚d:lHV IļVʺGDIE,KǫNK=õˢzn C,zLXJcLkLT F9)acH:.a)K=ț æS%˖Y4q:6y;=˨% Be1X(RHJKɔ"brYS3ITaG!YTYY@a^_a)c΂8żK}~L|mєM$WcUEI QPkZiQ)QWVH$!Cj|Mm]V;! ""Z -%hQ R{wRüuZ׽mDu̍EHgm\ǽ8WVMD(L%uQ~H%.WFTЕW%(U=])cܓH={$01*2*4X;7#Pӂ; MMB$}HƖ P L2$DE" JGMP%r]'_FfZ%q`N莕Kgiۘ+ϝmb &pꁕ 'sb=W5׌VjrH,\y6$Tal[x%.M]W![aqSqָ$ȍP']a\["3$a>t5M'~' (㡼9(m7x޺3+)KA`f8,1Yꉙ)4`Kr]*`pJZ.Éꪯj5d]*hHܞ0,RB*pQiP~o}g~݂F$)(.'FRœ&½Vv%b*FђQao-~}mNw{FW⬞Bv!#Բrc 2Z#$)3!c*Q6e.-j r&!/8/|h////./Sf/X !  I j 1A [#FzHʔi]5$ NAH _Wdj5\K̭ WDžfngllg^縬WT׼Lf1ɻ1v- 2"32H̋(xc 2B3= [9` !~3V }JJC `D Ek:t4B5P5(SK5VPy)[\s]o_Cg;d ;gljNT[mZ]L'b6mmPy6DFq%]EV)Ǧg!Q~yג$KN-l.q)#wfa$GW=&gwr37tS7V`wxz{˷}K 86](^.8_HY Wm~O799s/ȹŹ {Dk3::Ⱥ[LةM7[ۉi@ps˻뻉-1KOgu~r&' .fMWfWw-w6abv]FAwf>zvvf]s6a#ѽg"f"bUlWirZ~Ȼʻؼ? B=h?B`>pxc)о={ a{؛}-lkNyzCxGyULln˗/z/6y?"@ KO/j@ïפG_ ݿ?w9{+{ {T??{| x|_|o||ȏ|ɟ|ʯ|˿|||||}}/}7 ! 0Fc 0D`c5(/* !̠(ރ,[()0?BܫGmc]L~7 /t )H#OTg5lúXc1žH@ Ah hx 'PH@.r`q`Cokp0" >@:DI h.18{Dy t`[ XGnefqV 08pHqAv5C BCH{1Z=rAd AC{ 'H2dAI$#6޵@ǃ 72Ayގ gAT $qzLvZj{ZlGx5b'6$AiAVPi_Y5@ p%, 6U(w[n=f 4a\ % 0 A֨ :1hYpVh-0f) e6A T6tgp-N(+aPPf 1@a >qg 1ٹPIW1!V2͛I eH3? hrz>dzrTN`PE#E,QhC6O@FNCeSK1MSgdq%^ܕG`7-6K %4]DA}KvCKn q~tǙa7T`1)LmsCD L! C3H)4 VAPtc$h#ǚp"x+C$'Gr6[h`.JfBeB#8 (c ᄋ% <5DB ?ڐ`r9E  EA0Y8Ht0s0Cq @  _BgĜ0 ADn#F X@#}@,)üB'3 826O"$ E=qE!Q2 @#h)aB>9DxXCD@TbCRISdk+CGHJD A9 '*K̃x0< 7, !Ml9A2(š'Y঵h"_dZCj@Ϳ\43|A @8A| t u x;`@=}Va P1#;9WCFWRX4 J$ 4  0բJXm0X?8H  ]4 I#4Ah@0^ )(@//8 w Aai FsB`nc/+gw68#+nЁ, @D$6RwU)p`p v 8` BCy,Ѓ,A j3Hvt@ @"z^`n@PT RH 6 Il z AOdɀ bhx #S_"ra7A,M^Is,e(SZ"5t`f(Q3%X_A,_83y 8 $b=]:|elMv x$:DظJ o6Qޭɑm^" > A"r6+юީA0*,Ada xiOuk D8PlCUՅsv!AB=j$vOXֶ>فE!i@0 ds< 0Mw oH}5:tֳ= kNAr;у g(`F3̀a A " 0b)כIJV2֐A0 hHA^h5@ fzv[8XPa-tЈFXl0Rq_| i`ÌAmR`8 n *3ɏ&$&NUD?U s 'YУޘ ـڀK{t>#їH 2'7~Jun ED+9`Ћ}sq/ڗ]Ϻ$]D]x$B4U@6XYC0@@s-0A&HDHSD 99LALP ,W  @GlAX_ ͟GqAxAAbX\!!&i1/0ڂ+$F $.L(@jL.HPqcF $d9dxRG qǍ)mAdi} pA $ALT 4!TF`t+zEZdo@d,<%DD͇́+BN`BxLTdLd@ A,|AC.qLd~dH^VBU&yEddEETLbeMJ4@HdYZI L̀_I%RdDGaD%eV<@@d  $WcdA(a& &<DHlE&DHJ N A"$`CB\X&@`B8@p@xd " !m@8&8Ax09#p.\$HtO6eFf7A&`tRuƠA{#Agr.'X؁d% \"|o jS(e%a@1m4 @F;iAp(@"@˄V~/E&6AI$4]+'h%(5RB6ncmH!@f' m pHA4]F#I?qmPV£B)Ԇ|PC0cb$`YIt@%܎dv*K _p$Ё eN h8$xMjA@X% xLlyaƲ檮Dꘐ*pkץrx*Xv] @j @pA%%j/+F@`4k&6 e8 UfSCL6F, 03 ʼn"Ё@ūD )@ Al> P @DXB5[Ff@INAiC-  +zǮ_xVV FD6,A8-A@4%4@$uA`r Ъ+,B@-ISP垒)!4F.VB+(&j)5@f+NVv+"elQ֖wAdFߞc6rj$Bh RSD AdLGyzjejwPAN cRĽD,+,@Cn4 * ~\ DAL"(l4DgpN+py)pNP ̀0+ D|l]175DKpE @TGAT,QZ @!I؟m82\ h@Qdԁ,:L^&oCq OD8Й [l'S!n H-&ˢ [q x#C<+{Do../;s44]%T4`, Hb9:3qmDmx&{8г.p{7{38 oe*H]gB. V/,еABha#ӕA!X}tA@KCA @%mF0  @ֹ4L#-@ܾw@A4,4\LC@4i$%|*q*0{\Dd.uuˬL` Lnw@XH6bbF w0}ua6D]]CJ6tkD5uCD6Dl[ql^_ -Up Dr6v%XL0x7X pZpo @YPAD(v!V$ 2DiO[==K#x7{  XAPXtD8TGUS~~ pZQu`xux²ܴA`A *N4]x\yK6膷+u[A8'8M}wDSǑ'4ACHL`D:[d.79tK(6hv> LA99`L \Φ9y5]HA/8A&8+(xБD:`!pB(AH#+)A)0% \@@,p]G y 9@a@H@L %R6@4Ŀ ȢaJ \S06[Z\@xG|Ac!L`lAHЀHz K0f\iR5qAQcH DcgA`)Uϋ3{ AF| '4h/6*<";SLboOq%84d@k=@ $W= @ @A<(`7uE BGt>[*{  8O@r}'80Ƽ xxc",{B@ \2Ż gZgD>Ԁ~/HS_,A ^\ ,AD_tK tXC,pHbsvZ0BC)rr`+W8@Y0@H;y3@CȓBB!dEbP!)ԐRP(S )_ÃVkJ53($Btu(Du(!kQ1 F:Jɠ&qPÈ%J{7oVwsFZh꟫%uKܺr)R W' #0<ML6$+@:$7T?T = ҭETQB%J-L5ݔN=PERM=TU]V]}XeZm\uݕ^}`bQdۺ-g)4B%VADim6r=]b-+ejŅqwvu ^}7oV_]U &`lbj4` +@Kff yweMe暟`z*e#vyv]ig€dg_ŒQuvj}]6ەme^zcg f9hWc/ꦛ[hDa|maw %|آun\k5iqˉ[V|\l18Vsp;t&, Yw=dȭFwii_ˋ&j_/-~jOz{ޝq޶9Խ.~XgoJ(XA iq g/'4up~; U(l$T]!.oy$vW.q [8EuO{h@zi3g=z![T8AiWN90nmW=8 Xm|| AˡHG>rS$e-i/e$$IYJSlxԞ(yJWRg)JXehK_0YLcT2Lg>є4YMk^f͞Jp ,'EU1]d-uo8Mr|qd޲l (iEk(ƹcyVIA@3SӐX8_E,L4P吟DR6Hnwhh1bp\$vԊ8>|Y"G&UC@ |uκnlW4ꮦ˫d|"Zy{ R+CSLl"XIcS[RŊ6֤SbJXF$".) 6@fhbk, D40,`0 (W9є4|RuOKRpjVYL2pDaC*/CC^ +8tQ/٠p,DK BB/˰(P\8Ͻ+^! s8C:03( 40  р = \@CA¿m7S6iVP R?,ǩU"Tk>VesB:F&ޖ110_H;&xcXXFS*jNMnX[Գ#bѫ̳lLߣ%.my\!I   C\ 9 4=kHT`DPfKv1טݴ*jQYruk]\ F(IB՝Y-T 8ĩfb8'믾qFo]XN#u9IIOXmn3VMC *4b@Xp D:P!@Chk`'ۖHc!HW6lJ^`UѠ+sZTK8dw7VT9nʵHk6 '&Y/^EQt\w5RuAgMA BO'vv"(` N҄0p ~<o mw@.Zp*Jd U =Fng±Ȅp- 0( [ ^ P5l Ñfϴ1 ?kcQ)N-C/[oB $`h|`b!"!!nc&v`dP'T`t`K3K㺦˰@τ(Ў~*,(r&HmX (&/Ĩ+p q2+-h K Jn- 0.D*O&'68B7. ڃtحP ͼOfF 02o1533s'(1a zMS"I` 4i1gVdHLsQXSL rR 8 46xa֤RFIS7fӚn3|j ;:I:v<<=S=ד==>S>>>s> 0a}aBtb@`#;~"`^@ %@C7 6Ha'$D B" T@4B+h'Z ,a7 "6`,`GtG%vb0` GGBGItI'! 2T"N "tbRA-J?q AFEa~@Z`'4&d @' =0 R'> $aڠPB#TG' L!UB3EȠa'dU>JU'l@t!\ 2RWR-Sׂ@ 0' v`@B@֠B`2@>'D'<4`$[ `>`> ,\U`u@]? |f!&!u_ v~,PtPVA`4PG'UBuQ:!"'X@ TCh(dSV#$iE\A'Xy"jRy-pL.JC# "` > .*. Z!$`|!`~`G1a| 2@'lA+6b'q-Aqi c{.c]VKAdieTAaP4R ~aeC``b@tyf%B8AUA(A$ HA!$iff g}bg1(a'7y_y !>H! =z7}y}7~7=x"QX4A$ z%D(p!y1uiCBpT5}W~N8R8$Wy B}W' pXgcx׆x%"|^v@~l B~ @Enb` M"q *xbS,p `n q8rɂr Bbq lsy@"` 8we;YxCvtcA(ڡa3 !!fv!!v @ w'a B"Ay  !XA\\4{u4{%'ҡ, "Xٙ%!yY=@@h ٚ9} `"A 㟷"Wyb l a+O:-@JxEAaڠ3PaZ<XIڤ d%b`9$v!&XFgCzKڠڧ:9|t@rZ` WF@rBL vB(5 B bM %bcr!V- "y'&Y",V` coBydyyGULVwu"go9hW"z9v $Ȁw k!FX%n t" {ghC ;['F t "Y=!;q@@[ 6$ "rCb/4k%Cb{!2  @!.ydb   ``!` @` '@,%!qAb@b^@4L" 9$|[x"_pbܲuB . }kd|QU`Ta~B?ubm ftbe'x@d Q ~!tyRVM=(/b!j([":=AG}K[P]թRM\["A=$ڠअvIN= aՍaP V#\Ia'==۟ݽ#}C}I𝻡xV՝݋8` l<'2XaK`l D"@@Lͼ'fz! . "̀k`@B` Y" j<$|>b?{'a  B`VJWT"!]vBBF}p 6',F! ~Xa . _ {!׍!!? ^1?E[a|!,,8X8A\,,8t!݅OZsw'{Q!T_Ca؍S#%9 q5˘]:OD]9A02ӎPc"P @ɭ(MɴAXhTD豥Sםx3G]M 4]u^V9x@*,pSD,2D!W4XZ B4I҇M Ԁ;8#?e%1@@b Dbe\v`z *X9̒9C1|W_3p%RH1aasc8}p*@!H@Hٍ2 @IFmG!-裦@[D8@+md2:P#Hz,}B1;A+2z޷ J[",+%VXlZ򬂵k%Gx V:"DG  A=@z%KRHFKu0h$g-`p B 35l3$@ xPtD}%_<)"~jgJdmh*U9L2|=CN02@~Id?gXb!E !Gvj}P^o~vdFRC~)X2cy:oyDcҨv`F:~y!L\ВRYo<{gGD> ;_~e?9aKD~E.p }C';o-tC`0 ( ;H"F \EE&0ABg8Jt ءG&C@`<yYPa@w #3i ADia8 VH @k;\"F).mhLƱ=. X cpaAB+ؕL!}cY9/ '@Ff9 %IJrrD!!IC,j٤WJIȈ,< B"@/ky`eL-"zIjZk<3nz\1a/I͜A'8b{Ґ $+1p~ @ZOA]$ʦP4!"G(miȨF3z$@ HGJҒ(MJWz .mKgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲN Zֵp\Ӷ"n^׾+x :XV ]:d';5UA{ hGKZb}eQ{Zn6MmigKڶkAFmK-,l[[vkqKZw-],p}kjw&ҽxK*׹ yW{J.5|Krog_+Ʒ!]N|]2t, [ΰ7{ GL(NW0Cb B )׽>frdjeq0nXYR/&Zrr`(xS #p@ \  ) .PX- Y^.uW^tM3C+4X,iBkWY.?M{RɎ/0K'5X$`ghh. 3@5Z<^X2~a]dPmhڑw =ղvg抣a ]st[*E.o/ H DlA Ҁh& `00W:MshCDfK_ޭe jYV޲I/< yW.r|Vm,o˜-smB=͎n}T I`UD`z8X 8` B "j04T=m$'ri7'5s=挏UO~!?OfSU=y לީfoY.{=T { =h}^Trh:ӶDÛoӽO̟YO:|_w ܫ@jufn'zkgs0݅u{+5_oŗvv0 z'XjP@=P+4jl8{ y%xir8nfntrvtHmw;yyDyքE3͘(#p $pJ vpW@33xxoD XVJ'HymWׅ=9~iH`fWkkFDfZWmXz{[I~E:(#yFWH98O  r !0;#sJL 0$a0m79jb9y]&t9bEytP) zpgr6zhu8IGnIs[j(pA(Dv͙%oMwO"WrYr6؝iIٔ yIMlYM6Hy晜ocשxHPiZHRiTG6yaV~Y vjZzڡ ": 5 й>Zb r/z`%RA  0zMFzO@ %Pl< t< >: Pt DPPТF4JK PtZ^CP ,ptڑu$sZIu:MQ-ZAq(  zP1a~ʩ$MǠ^ -pF  CJj97P1< AcQ\m` O@}JAgj <`@[]:$ M ;0 s! k0p: x ; `)E9`` /0{Lt `0BQ$ qab3<qQ6 JJazIK\aJ?`e ]Ѵ֚%^% t\T^p 0L)0SKp!Ep81]!pz;p`!Q 6D4-{04M3g ?;G 4_q p %M jDPf!001N۪4@y w  wCP9RVA [ 0@ b֋ڋLd@ ؂˿ٻ\ѴƐÓ  R,lAN @b A aڀ 2%|$y@H[ъ *)S@ e-Aܶ=֍ܣ `|3Q!l0kJ`P0pS 0v@t[! Mpq 1`21/sW#2TT3/ A>En&uקEZ婽ڔFV@K uu4 < 2`˘(RPpWQe1u~ ]a]\ܑcNPn h~ʠ `kmPr* ߟh+ -N~žqҼ2rV/>,1쾎.eΞt3c sg 5I ^  0Mg=3`+8A/dq,`|fBI~袊N/D1,u0x anMV6 v}[\!#Sl, q0 ѭ pH"]Q }WMe0p?}~+`  q/c 0 !@}q+ m3+ .=j;εcY]:9@# CDHa !$a(aOymp|9 MDэHLWVY17FB+3A?!aW]a @ \HG>Z jD-^@baĘGRņ# ltG!Z"f"VSl9L00 XLp4ҬH*8JE2m&Uq*OXXbI'o`R] ?RhaW>'X\0E b&.@( ݄ .0LĒiQ0Ahݼ}K ( ǀ4 0zݿ_|ߗOUhsc&a )<(d"mƊZBB;lj GFӈ#!2kLB!7Dӱ!2HP*&L$ +DPKt ' $ `D  @ NJBH `а9P [  `aikfH meU" m!P5& 9KN /P{1?8`&`QYfeYrOAȜv fĨC׽W0K-Id C8q>@(bZPrjohQ89啽 0C&'jZth S ZHg )ļ"Me 5Ill=qIENh"\j̷ ;ӬW;WfIDm[rZĝ`(Cp)CDŽ=Q2.Bc%!b{"%# bZE0~l c8G:=ǨGG>q> #O$9!cA*ґEH &Kvғe(E9JRb`@z1",e, ;HJ``tK]ޢ> P/e9LbӘDf2Lfzrτ3[LjVӚf6Mnvӛg89NrӜDg:չNvӝg<9OzӞg>O~ӟh@*41AP6ԡ(JThF5QvF+QԤ'EiJ5ҋ K:V*iNuS5)L_TҔDiRTEQgjԐiSUvHPJU^YjZպV@)XoJV4mk^UbRZէհElbCR4rc)zWVֲEQY ɊlhE;ڮ v+iUZֵֶmle;[ֶmnu[ַnp;\׸Enr\6#/8!`[v2MbKW5>=d*TG4-g]vvvm`1bSզ}y%Khxj׾u.5 F=D t8 kuÐ{;җey?7!OW'#fKU8Ǎ=fb 8T,di '؛6F <<0@ |`YrU5cg>syی8yjs]lֱ;lտ6Q#"÷'/ H8 DlA ҀhR*\Mj@1Kd.^ ]BoHUȶV/lcl {ڍ6\m&6d>֦]t dWγ)$`K*0`ړ0! ~@W= a n|c{Ȏ˾ ^iUf`/{坝Knr2϶c~re#?An?o?3L*$ݷgR 4@iFcUe @^7q:H7.nxۇm^]Ʉ8.>U6rsnqʣZwۚg yĜ~o=0l:P QF4+^ƶg_%X.YtZ>^FǹM_`'ܜvW?uȓ_?}5_ pK82x30ཐxC3?; D9˻+=_<+79vK> /D5S>??,?d8ᓼ3AsL7 ޻"K˿fZ; P8s84[7+3 ܹ󬥓@0GF <6HD 2Y7SG1sCM ʸT1A`+#p)>APH^컞@Ca>bl?@,A>#bӸ_I%H;Lr>0=DI,kJKz4ǩ|1_$\'fK|˶Kw;/5ʾDĜStDŽȔɤʴohpL`ʋ00 `P1Ռ`#(aLh"6"VxL`DN^ R^6hC"l(.h((`JK# ـP-JO| 案JOQOO PO Љ8 H=x8P\ +8w`_8 T λMh8B`6] xQl#2ITWWV0]pZ 89IׁbЈ[HV\H%HG20ܤ!c3. )/c9VH>cd0c@-3 P]h0x;c2a؆B n UfeWT^VCI( C]hn~P\[e]d/8dVdFvXNF㉨4VQZ\Oh^g]VdB6dg0Xል 98f=xbQ( eـЁ`) vh Ma0X%)+Vb,a1VyУxi##Fb%&`PZ(؁ {mB qH6X!00eRv6HQ%[[?02@ jf jFbiHH Ѕ6@f)p4Z c %Al [_N vMWҮ8(m8E;5H%0(X)B;~E}8h?Ќx=88EiFofo8P @^ѐ>yPy Ѭ v U@ !IVB`q[p^ׁHyv EH + p݊ WgBpi %kՄ*Q66 ^ȃ_VbroC]@[P/s/6lf^b>4?m0X;s?8 ( 3Gs5{Et<[S9 0\$H/#ƈ` p H1 Q ~V3u`vH:WwqWpٞh#p "(~ !{Y'Wi'\Sm2(xOЅB{wm]Z[ Q0e@ׅGx.q)WN:Q<Kp NJЅLHe'8'qO>yԞyr!Pfyy<֦G_ (ȁ8ncx{ȁ+8/H氂5$FYQ|ƗvA1FO}~'7}?w飶ipyrx0q)4h@%f'!r`l@}h\ׅgswB# oL~W~ (R s}X ? T,,h\XD( 8Rąh BA7!:hB!X2,x0ƃ;,l qeK/cvq¥h c@ Rl aU\U+ذbǒ-k,ڴjײm#V9L2=CN0`-d?Yb!E8z+`B%]ruv#`WɂWu&A m|pJ}RP뮽Je,zt+X.4)G NA̶,&2/=/J'mAƐF.QZ 3 Aқcm tcزV(r@a ;0э%2\ҡ L0 yWQ vTPaAOPgP\!P@ pKYxF-wA݅ZJ^m^뭗>:饛~ZϨ"3d#Azln!苙 !Gk{xVP XA1]wCoPG_U}Y $!4X(O%x Z,7?(L b4N m7 R`Aa AGxy ]&B%d! '"@xA M@ 9!(!F' bkDx)pi#4p\[0Rf<#Ө5n(8v#=~# )A<$"E2|$$#)IR$&3Mr$(C)QrR8e&R|%,cIT %-e]򲗾)rY<&2LH2*YLhr! 3mrl3qf$*ud4ˉNyVS'9ߩ}򳟁T'5iOkӖ?Ѕ2t&>UYЁs (F3=tĔ=':̍&=)Z͕tH PҴ6(9Q42)P*ԅsC=*Rԥ2N}*T*թRV*Vխr^*X*ֱf=+ZӪJXB ` HТiFzNJuT-k8ϩ ӥ9N5زu c[,lX\S⵲,NAS_lgjF|4!Tp6`h=Y{W )^}J.ױ-.M+\-],s%WiN1{Z|.BC[тVۥA!,Af"@ f>ߵ74(_;\65KO;鲐c_ ] {,-.D)W-1ܥ!+ 7 "& i!1$ XHnjo1|ƔkJK^Wş=o1ëܐמ[&eزm5:x39!Z_)N* a\X[[ "$OeXy :9"5d 1cq$H^:R9CFK$NJ{$29K$Oj<$PP%QQ%R&R.%S6S>%TFTa@;, `@%]D |%YJXWE#TeX Dl@*Zď#t%[@N%a&1;,/,  B|4P BtfD \l 4F Ae^ffDQLdXW`Zd X$BT\aBp0H\@!DgAh%`cD p3|k&l{pԦFooR!r?̓*CC;:;4TDsB?4]sLW, $“DdgPThwkA@N'A&D<ӄ,-02+sjġ0+a,;4XtGHKϚA Ar@NOPQhRDэLV@LD~|fT].LppzS0ҝ҅]^_"F# \nD,I覺t I \|l iN ȉ 1͏>Mձ1AP]2ArN t4E4 B<|[FҶlG3,eʦE9,çz]?L91XX\@ؔMBM P  $|U,Nh)*2s֙lAH6*c~p|qdmA4䀪B Ѐ-4(= ,TEЁxx4Y/1P ѐ PV5]%LUy$l5a65a/6c7c?6dGdO6eWe_6fbdfwgg6idivWajvԦlv^Wk]~nwaz[2cqp'7Rw ֝rGR2%ߠI7v3f)1vvw)Yq[z!xHƤz{7|Ƿ|7}׷}7~~778P[N;=S phv.Wc&d*Y8hgskBQwBm[ 87,L호 dNθkxgBBKʚ98-ؤL6E\9 )e YY-)yi))A)AXA"Bh(A\iniyAyC2Z-,C@5  @ x7i44AO:z)dhAx).Ѻ3):yG9 %́ gPAЉk)'Β6Gl+ZZRkr@-AX@hAVCR71A໻ +;@lA<@؁D+dgZ V9-;k*Rj*AjλA-<ʿ^{«K?MD@A} hA XٳFZQC|jb5󩵺<9A\,D]*혶l'me}A),*lҲ)IA+Ng8 ្n@et,]9ΒWNk<;=|,0~A&8 ,+pvW]*؏>A>36}D;m@A $AhҨ~uDR~Wn@3Q`T`-@-@=yr ( X@ŊP1U,*†x48䦀F)W9Bhqc^X$3E _fٰ. "XthQ%Ę`X\" f4Ym[oƕ;n]wջo_夀.8L7 ;J,[qU]y_cL`A4 1 ]Hn!AA ('CJ@d @\9e, PH2, @m:X"'b_} aC1iŠI`+@بc(V~❷GB⌊ w 3dï-ݲf5[.$!<(( vJ h[h;zs lp)/028@pz-r`Zr 6Jm@`۳+v(;E1לsX\o3\I  yLڍ׈"'z\Ab(\{UUgD_},(`[(T pKY%]|@@`vA ~-RȀ-RcCe!I r"]&X ƥtE)NQ,iq/4/+*#",RmtG9h O"ϸ9BbHqFu!HE.THIN%1IMn'AJQ4)QJUt+aKYΒ-qK]<R`41L^m0pLiNմf_éA@7L% 49љTZA؉:OyΓq["BO}'5A+< 9 uC!x$E1zQKX LQЀd %uKazM 1MqS=OTE5QT.MuSUNUUmcmؒY]R8\ UǪ )5mkVpASE rx@v悁ՠKj`VU Hf/;U.5Aʉ/ۊVtEX]~De *}  pXN7o-18@MEq! @YFh&> H%*b+e"L`Wp&$f78&,T"<3,ÍW9pnIh-| p EvȷC/ _YȂ} v`Qӻ7DRW3uh"TehD SH/NFt H_aR0aE #"å0Iw@ \ /. )M;.w , 4l8R4^4ay@Z5\4@p&r8[s ]!  !8Q@tF#؝g*Q%D&J+Ј%ga!/f{F1P;H( 8g@,K|) +s0hB~.kS{0"V Kx  TPz.d]{( ΐē);MwAE4-X@5!p|e 9 XWD\@Ϝ5@䍬޵ + R@rn"0# 6@# 8B \2k8gd0 i xDR`K8MptZFtp nK (/~ <# %oT2Nz`@ ` 3.a  5po "*``: ڜ#6Cp+E;D# 2'6e@-`֣:2 4 lT" `0 t",M `"@pБB`fҭ (lM*b*B@9"ѐ *j"N@|J0+`P`]b  7@N` N`$*@ \"4|j_ Qyh 3v@4C*0`wr ` "`LQtv1bQ )a  2 I"@I17-` R H6*S\B `o1ˠ ֭ڢ$ P` 'u')@|@ϲ`2Р qRaAͽ >@ AxBp3R>,*[2X@ %`@U΀ B` BZ*mg40|(%ФR2 I4G01wf-' D;s@j3q |3337S Ʋ, M++bPq `")*6"v@3 Yۢ ܫkGf'峑"DA:@P;*-]" '@ĒBB$N6F *bY.Ԁ#lC:1) E+bE@.51" $(Z`@#bZt:J5T3q nT`HT;Kt7G]+pL!C!(TA",|\S<<'B톱t$HT Wmb& K+"Iy4uSm>=5c#"%tV, v@| -B"$MTH4-56H X T B1̥.MďQ*,U02-B, bU횔-d9آ1  u#5 LL MdW{aUViU"T#$PeTJ" .@*b\˵,v\*%T ,_l Rhf Sqv|@ &@k#d`'R5@=* kV-&D` j"hp FT"&&p0Axj!`J21Fa~ @-n-!b`&g[3$nfj>Tn6E^} z2anq">qw`tѶ6'j"VU'y,zn:c6k $<p% @8 NWu,wq7>s{A5 %h )Vnʂb+ O/kx}|}x4`!c5B3`  `.`#Pim+!xU}Mp1؂:l *8Z24T^T BDwTSxC-X Ӈ 3JQ~]~-`>@&4<3G@r&d+"fں͵,ׄQXY |b #| xI/+Z"Hxv"y.Ĭ6c2D3y ِ0^.-/LY Ë-|@`#< ,D:9-š+9cB]`Q˜S-Y@ Y:z :z!:%z8p(1(9CȣEzǕIQWS]zFezo%iqFL:yc}:Z.ꆥz:z:z:zɺQɦwEL}.-=բ0!)+yz~ˏ iԅeHGE㚜%J#6\32R &b˹N @*. ˵j C⬒"a@ v-~G+BBh..\+S{b+BԱ KȪ",im- KK9 ()CI,kj /}[K`,*Z,["Ձ%h{{k s 0<-nB-;aNaK"?|+B b c[*劅R|`lq]c}Njb>&6b/{ɍB:}^v1C|i9.Q 9GF<#4.~s<ީ񨓹\)1..fol-i b Šv/ήBD-q=,&< " .qSBYb02MoX,Z ^"-].B"P-`j-8?\2?.8#9vF-׆j^ "]-- HI?/f^-⭶`ӾY@QHږ-@_>- $Hb) H$AFx1$ pI `QqC7@`4)r`y2)#tAL9>H $ʚ7sK=J2/>0pkAPj5Y2 #b P0ت-HGJP(%R V) y57\qp\[YJ-Qj(PXA\ȐJ5oI F xK `)'8@'S rQ Tr&<>*Q!Ni Ca$AG+AP,a*\$bAV GHT@@1aR^ TuDRLmYAl!! $,0BA0CayQpt#PA%Gr@B E`l`nDBi$AkaA| A\ 0=Jz \rTAE8]t  0]xPj@!YP2gm 4d+t*'x@"`Av\@sV@o7\)[|է0.AJ5AfIЖdH @eS )д*E]m~tHr${  SY KhJ qC<nf(Q LY91nD ,A%qEIc[{O]A$YaC  $|rwb5^ 4F9QUގ30 8;G7wg(!9 t0M?}s w}\( Zy\1l0Rf׀KQ+Bh(P^Z2 X"VZ!D(A ~ @BXKIWbDk " S lA_$/fC2(B0J@= 4!Sl~_Ez N+H(1D#*b x$xDus $Cli;l# Ax9юAl+HdFJVUV` 1 (uB8Bh yJA*xAѐ@|]1[ l;>w>__dp_ PRaa^<2BQ;)O4!;P~j1sN  $(t. ³BeF$(,@IC[*J+ rIp.;PLE06 ʜ Pp$< HQLi:ZЂhT%D5La<KIU% A,qI.L40Xe s`Xh?RA²O%'73@&!60 kJz@7AR |@ q-re C L0:Ɵ mmr8mmo[2WZC `O d[ R T I w`SHUjehB[ "<00 ¢ %iQR4Q]][ OUtIxOVC),xZև$V&pAZ(u"&qc (_oZIe-ۭdyD2>dEz09<U.p$-Hh@e4qF[E] P\%$nՀC#I7{ԹR! XZ& , O2+jS`q*L,@9"X-׻5Ad(V[@׼5th^uAʔj>ެ5ăt.&Q".4 Zuu  S tW u}@Z-o>zI\̦b^ ̔${ hcX"@"QoEB@C PVf5< [",nݤ>E 2yPI|`  JV/y02@Z=T!XRDvEi%DpINb$0PAv 4D>Ӝ *dЇ^tC!6H'2Mh;CbYbhy/|;rl:o$K|%vHa]4ܕcY`]y*&XSWWzs0siW``W4sw7xY0pc|v dxGG4AK'2 5h5w"D>prACF^p 3S *ۣ@(0=Xv@&&E/# 0KOwpjw]u`0nx0jR^IňLHqX _X؃3A8$!iPPX'?V8;4AMJP=iiqYXPGJG=ei|NHp8Q?!]d ~0!TbDuDbkzW_!A"Їj0Eg9p@P,_Yx6A]]{hDQ_]$sIB6 zX\w8871eR0 IfR#`0E19J$ fBڈ*k؆щ@f`WIDD}RdU0 PS8FH2%~G}_DE)h;'cezYJ@ͤxW` H`svT p"h$xIyKٔOy"AqUt*qO0b8^842TH*aIYؠ$3~t%`K_!@506}w#ɉs,WPRqfv~$Q$ӑR'≆xV2%UP 23JY藫؊I!0iWQH~<. qPjQ(Z]! R!Mp> +y_ Vp@$:2F䧑!bXX*uPi"G)sPwbz~?4Wr/t`u`Cj5(gZ@yZOsQdl*1b^iKt@tN zxB.#H.`j dd"C@  Vl*iФPtwvPpybJQ#`*Z,Z;sV0{ A0C0x g:z!jRtzĞY2`XVqiW$j(KhHS$H9C`ΙdDC@?,`u3(-0`G%JDЯM$q [0H5R'}u!R?;ZPP?Q gPk W zS =r0pMPu 01@crϠȊk;ʱ,0`Z bx1Z| UZ.J#pǣ<J;02@q 90p!X `ͯ - a$`W0s1(S%PE.W Y* ӥ 5pżu9SKƥɚCTC=Qpzj*8<}%6&P$=%=@]`|A-^R)I sm0pI֭}sж 5QO՜$̀hlپ}]=_$]PM.^'p:-+}J@;vzl| r%,3$QEU!^мU!d9J~ ?c n  -R( yvN (nn9u>bCNG>^/z~Rp!nb樮!qӫ%ú~Ѯ,.~ 0Vi!N┼.ūlɔ .RG20]. z/$iמ/d[rO>.'NJ^ -/P 0"0 OEo.$ ?Q_3_RoWY[]_a/cOeogikmoq/sOuowy{}/Oo/Oo"AQ+F3s :0!?/_ZFOFԙ/Ooo0& p' p,0{ @0Po/0"_ppr! kjso>h!@%N8E#l!!PB 2E n L 9uOA%ZQI.eSQNZUY^BbNHH? -&d"& 3Ap!zpOFDD9p(.cfªd_~_x` 7p *X~S"4(@NՀ" <Pr` @s b3bZ!Ȣh4xivi.*cq{?tH \-"YJ(0IT"GB@h5&E `D:I 9r3|sń D@Il$brbcv($]C# Da8k9vo) \ap@DF@`A" f8@|7?#~')P'4~xQ )2PC'D'bu>&P d`B qJ@ fP`1T#$a MxBP+da ]BP3a mxCP;a}C QC$bxD$&QKd"D@1AR*Z!Rb$E-^Q ZE2me?/5>gQbj) ef9 DŽ*&2EMԤ|?Q$O! ̃7KyW4D'PZZAͥQQTlFԟQEEBֳf>Pti>oIS_h:9Pfxͥ>ЀBtU)"+Ղ֓hY9m-(=%կjOCRRhU3ZV)-jQ HbժmEXzҲm?E\*8PgGUlUZV+oA2ԕ]WςvegSjkDeV7\ +ֵs=UIZucy]F7.l;KRcY[a5*iZ׵eqK׶\k GW#q{;`UU:R vk*b {ijD_$17G/ywfqַlPZ]pmsKٻPr:{jLn|57 -d.j?-zW~b5qȝXO}~=8ǵ؟=W0+G?tݾ>k(ߩ:Z;o~Q};}ghݍ֫N>RJ@û{4sb7Ʋ?M38׊ ӷ=3ô2#A;C:(b:l;+=l:S3.25:œK.S@f;7J7Z6ANc/b%tR5+ B^;=A 5#8Ck@12 3dZb5?Kq[,p=>#>Su# 볣JùC4;D3KD'2Ģ5.j)ć)Fܪ3 NL<=!ŵS'H*2K,śB LQ" FE@E*DFcE(@&¤*0QƮœJbD2ckEf|imlMF.FFqLGu\GvlGw|GxGzG{{}G~GG HH,HHPn ^0$Z N<LԄWLϴ\;Oϳ϶TOOUK̀5{<QJu{5P }P,M$p`΀xHNb(f8HZj&~Q~|Qdmu!-"JbQ_@f`LR`l(гlB=K<-ԱӳӶ|Z(CԱJz* 2JMTOG0$(R=N lHp{xZU&dQ~VaJ^ \ue-fJ{Ȇ%g'R*e+254^WS2RrIt%[0pϷTԶ =SIeKc@RInK}WWtMɟTTу0KXXx<ٔ؍؏}՟Y]Ss@tsp@shY꼆H&ۄNSRیjG_}Z}R(a[VDZeXHRkp(ۼVR}ZZ [}4}sڡY ~d}Ƚ\kku`N_M{l[bPiHEکfkhmVpZRZ֐m B6XdϊlB ~-@U[Hq]`B^3md؆U TH^5S-_TЄd@ 6(_^^DS.^9 Op]O?s{pGڤkt{?[i@Lkx~TS`{NQKIOR eEرG(^_u'1\buhK#^qyRM@@7zb:? !hWsl}҉bJ2̗A7 5l@y"^a]7az׀ 7y~A(b@ 3X/xߌOX eryړPA0^zB>"LD)،TZ.1~PAIh"/D+35`Aݘ7iq&5BGc~p+"̑5dT^ضDnԋc̀a2En @?1gZF M* GX")6H \QEo)ܼyEӒሚ춷͋+$abe)Ӻ65@|7y S06p h@J 60O@ 3PjG*\,i~atX::NN.>RVwIAbc@d2P]>9ZKh@fp#ż-7cPGuv\Ԣ5h8<|vw^ anVD؉?}xÜ"QdlNK#$A#\rE0H{IBxIL@dt,"o ` hz}4 a 3H :QܠB+^{B\ϑvr;Nbyv;i:k^=w=Cpx9^C(=hR~O5:a b~z8_bͲF?@YTbC JD.?뇶}É$EO)|o ʛg: s5>@Q ~2%79}4B4__ y!D8`H',`Zb`*2q`2} PCr^T B  m_-$`n*2aݑ:`bajra۩C6 Y2Y8`j aa1_b b!" a"*"2Bb$J$R%bb>b'z'b(2b))ƚ#V%b++r_&ʢ,!-b..!*/#j0b11N,*#cǜ]3Bc4 SPc5R4bc6j6rc7z7c88c99c::3c:Z5>c??:;d6c5A"dB*B2dC:CB$; 4c\]dj]FFGdHHdII8DFcEBHFLBG[GIdNNdOOvL=<<$EdФY-Fv$PJTReUZUbe6΃*Q2HTEcL%S&%FzdALXndY@[FeVe]]e^~$L[ޤZK>eZʥae\ fY&&\edJdRfeZ&4 <&czga&eXYh^[fi2ij%a^lfmmVg~g&h$pR"jYb&M$n:sBgtJBWf fv&ifdw &Zw*&rgc:&k6tzg{6ffQ&ZtfjgSp'YZgx*%z{"h(_elcoڧgcvfy}^hVh%⧂舒heu0Avn'wr(fgp.&ʦh菒3ȃ*u0hhhrf~"S&diZb)>C=lWjJ,iczf院iDRE>)?i靪Qi$j꡺g@!>, (*6!",QS[79CACMabiHJS뢣qsxܔ8:DKMWEGPjkqegnIKTMOX46A<>Htu{ቊYZb̕xy~BDN-/:yz~SU]+-8bck华}~;=FstzFHR24@WX`z{cdkkms68:NPXҤ>@J57Bnouёuv{PQZKLU{}')5[\c@BKZ[bPSF13>DFO၆]mntgho\^e֫lh?B>_`gLOD02<蝞]_fHLB_cMw{Vr_prwhipoVYHotS񇁆[s{avġdoEG=DGArZ^Iᅲǧ|϶fz}^;>=̰xjlhlP{X359]`KlpQ6wO*88ӽOz<>7]bfJ@a-J?˼DhBdLu伶DQ [-NdV;ۮ_V_occlFg\:혾سޙ]Vl_E0qJ(r0ljAэɪ8H 6<sl 4 mH'hJsMG-TWmXg\w`-dmhlp-tmx=Q > AH $FQMG a@q91Cb9K0j` t`CDIJS2%*ULb]HR_3-#l U29f- K" PVd YEZZ2 P V0O $p/ ~1@"@ L J\.@/m@ $@C-A9CvRgSԦ8)A ,NC 'F` p;(@( d`$b"Xs mCʐT `p@U"*` {؁ 75 -A:0sh ؁,HAPնekKveV ĺU!  Bz$(Iף \JW~7 (ְ`;(a Ѐ$O1bA b$p@"pH`)Ť@l|/]As \y@ T.@`< D62A2+#xH2.l41 k@ tpd'!l-Jفae$H (C $ 4@&$27x P.B0P `LF$U4MhC?x@6BsMNp֮Ht!B`#Xs~YkY:u@=$d%@= b W8fH m j=؁BPj6Ph 9 0'L> ]AB;c<hHYMP!AmH eEhܬi e2('DL5; '< 4pv<;/08i`ԫY zb F)| 2@nkeG qdFjƯ|t2"^ A? с!@ǫ AB'/>pW>"Vp`䊥A6+^ #/$qç&@  A $`džT`EX T,,GY0Gf@h3pGk h3f~dui4PSx;ȁhke1Xw1gMqwv|2\Cw D{1B7c~%PqсH4X7Qb1Ftezq{Y]kA6X(~g(0|QQ腨|\ep>pQx@>(H H6P/eRHV~Q(? 7R'3P,`(tpxIPKUz>cHX $``U'sqM a tp5Y5uAHT؏G yg )pVUVzP@fM0KbP su-UI 3=y|g8t$jfy8yX@4aN[b|j?!s*vz +C+:ˢd:4W!w= u&lx^e=x yi>@Hha"q4>PJT{+Bj2:@=;a=2P3/L+jB/2aa)!>:c.Fo4@dB,<DCTA\tDABf d9B.tacE1> @FC6DFfDT:&('k 6#REQ9T[Ea0bC:tFiԾPHy:a BH,)FҖ=ڛb}HD1bG}DCMsI^^U.QJEQ~C_\>xuLS_VN[|׹W]EYyԅxvGqO[@[5Q\R /J &b!d1]-K9c2P& `E,#DH3JLPnvYfO,GKAekc<jeI62Q.xg|pƖlrm `EQ9bVn$< VVPk /nfn&Z+/n&a9Q.w|w|=)zPxZq>yAy^O= 1b ;~'|}7}`~}sq^ag arg~aA`` &0aHAlZbtC R(aÏ ?pO&a1 Ж@D QM>UTU^ŚUV]z} LQ#>0AC4,f }  AciGy@6]Q`#J 2B-+$9 FU ;bY8)⥦W*iPP` N1Df48JA*Fx@Id?Gsu)NC t.+#N:+CΠ@$ ,:,H/Ik9</Ҏ;+Tj@BɌ AK'ʀ.&uKJ863N9礳N;N!d0L,p00 L* !$&i:n 3Pk!PIA b $8,`V E)#Xs"0@K@ ( >(` x"V"8aؖm4HUZoՏb/@jv[T`lض pDVۂ2 @O Y'UcA.h2`@FMnhaZjȄHr/&FJŲgfI8!Sk?<65-Ofmv;>b\֝(2 K zqJ hq A@/|s!4Z<(^U ! #+ ;p$o -uXOM.K :,Sa[9Ȇ^al@"Q؍]r(;@̟Ѩߠ .b7臼- P:c)~S,1Qvf y \ɓB$ lU# UMfqI,r*UW '#b` Y0lYK)ئ9/Hp`!x-s!I8lA'`I_Am"iyD3ٚ'8 }"g ` 0qj h a܀)jjң`hg ,rWɌ@cw / rx0* !X>.+! SR(dtcdD5gFs\'\6 A>#Nz'"E!t320I$ϹCtW6!duJ Ƃ:R95iB0oGKet <9Z#1)uS4gQ+Ͷn >sU!¾te9_oN{vmnwy@qO|6D&ĵv=ozwAozGxp7x%>qWx5qwyE>r'GyUr/ye>s7yusgT.GmT92$~C b0@  Tܛъ[ 3p q hXi}Oɀ]>;UKE `b݅ >O0UQS,_g%^)}A c90@!@E8X kVHq) Uy<'[Qi@N4'T/៊SGT(ϼ@ZFPuY_[<c: J{< ӈ 4  5@ (H@JT=0 0Dp"X3 t"1`J=Ⱦ3 (ES l |(* 8© ëƒ04`-=('07 %Ȃ/'3=0+$|@p0=*-,.(==7TT̝-@0H#PL*P<7E6r]E༟0F Fh,bEEšLJ!Zx>q4LX;388X<`GsO0GE`FP%#zG *‡<8h T  !s C1 H3HH(I܃/(S.(Dxق 3'I$"(a: 8%`Il> HȄ $8ph,|ɃHI  4 ( :9!8èI˖;0LH˸4 ԁh˄( x)@14+@X<( LLi91S M$0xdDJDoMT HK33ZHTN+ x ^ɨhNxN` N 0ʇHODA8AxA#`$Z?:L=B`'pJt0 Lh.A@0`ROl W Hˉ:'Ύ!(̠!xB˓pС 8*"u,0 HJ0PE) P u$]%)DRqd{#Yc7 9ռN( ! 8 8 `WK0%YzJ$0`c+CETWn=K@QBm4LJ*!qo9iWXd?4! pSh0ɨ"˨؛QĀL88B#x taz$ٞ)ٞ Qv\ۑ] EiHUXY!4 1類̈ ! !U?$x+}6ܰIEگ^o;ϐNC= 5;UE_`]#FNJ0J285s(?#`'%*. 4#i lڦʃ0%4 '<)"+كH ꧩ8ŋ ~;X2 Rzei"H %aBPh⑺;$H1xZ>)'S22;cnW*6 ׫ﭘHGџ5ڻ=AN@`.@];iʲ ?j.?A2`[>Ίc H3~u q|s?`S?q658©w -oNN ,6CYjQ-N*٦ rdG W-;KV49QVqr^#4+#P,g g1"N]!  `V Tڀ$#eq28AFrw3*JN@ KEG!Hgб';sD 0,rl'vluޙW"et @Ԙ]]n Q#J< my'2q[EX)|NHn;s =6I <@p PÎ xHT_IP<'9H@| JYPg#  ! $XB3<7ԯZ o@'ƒ!<@=J 6<:Œ uhWw0A2|[_D<(~FTq%5 ?֗2{@ 9 nyB Ÿ-zCC?YFp bB-2OD"CfP%lH8Dq+Or`OH=!eǑp]RU20̆)^0IXPKPm2_%,a" R`.G0H]b%d}֤“U!=  @{0Pa3ZäIOlS~1G`N#{&ѝx3~g\a _p4KjEBmzқJZwy j5!2d\S Tx2vQqGfx'C(J>~4m_"$>RQ֑.zBJs\'5V#itt&b+,ӜՓFX3-֭ښ G:(Hl0rdN(+!Q,f* I0H'"kW؂(2%ᖰ `DpxE4bC/x9BA"`@A΅.U1Ѕk@ + OHWjhp/|Cۅ֥x&6$57v'nAp9` c* 9*A`ܝ-f qt@~cQշV3S[T8jx@„^9{ `3/ GUezp2̛$YQ}rt|R=4LP!і,g J-v@| ѼzA(3 |P7iO8lAQ`4'`l?dBIb{79[Y/3zal x@BqvGA@lugdUj~0gue T. *=ě!(H HS V#xO_;>8z,.FWƪ7kU`uR0l*Kyr ppX+ܭ~5p_Wַ{Usa`ObhSi4v. L? \ xB:2lxL0@z WPv6/zPy2 <@{t-3C|w3TgfO $Gj|ڹ<D#30/~dz?aT2H O`A&dBL^߮8X _=`6^ f-܁j - g|` ֠   !!&.!6>!FN!V^!fn!v~!!!^! QR sU ! ! b!!! ֡tp H@,2"aVfϡ(B )f"%(tDVH(┌I !g!cš)@Y"e0c㭠43a!`q'v'<P (@ lH@ ԣ=0=c|# t c;#D7Ο~1L8:T@#a"p$GA \@H䃄vT$fLA)ЬA $$L" j+ZQ*`5NcR6A6cS\ .efX"5N%Rj%6*CQJG"A{P&<$TC AI$B%!%U@TAÀ eAedbݕv0&a@AT@\p&Cx@p%@&Y XU5"զT4fb%W:_pv%+J0Abebvޚ P  ( <~PONZfhw|(E @hW,iQd@kMRW&| 3iD%i3PW7F%nFUF)Qaޚ~FYaA)v2#rmԢDEDLD<pAbMNE]X6J0]r{y#7̪ht#r[F h\A!SA0OA])"],=aAXw @A=v|KTע @BVwwd@Mͦ8A@tġ'ޙA\E_H] w>臾>闾>ꧾ>뷾>Ǿ>׾>c#D~l l#n%,Jg 0b_ bC}*Idbq.Ob,Fn/T?B;Sc@5Q~tG @@@ &4(PHv@D&6lAÅA!G4OT 2J @@JHM!G~xgȓ)K9%H&ȐLL8C1gjZlYgѦUm[oƕ;n]wջo߲6@Ӄ T @ * a,Daĉ 8`cAT@-y..rI*)̬mnƢuұ2B8" Ar!ˇ&nW;|yѧW}{5h 䡒+'0H  2,҈l2T $$@0 M`!B<pCo53c<4 .;&|X`%!CMXB:@@Vc4`a(1#"DB%U%PEa4G33(p(">N;3H;"3KTEmG!TR6DˢϬ:Рjp@',|0’,, A44@a@hth4a c & S8@=  ,#A#pb bxb <a P@@h(.:6eR]wA,(V  w\9(?D90A,W<&maYin.P Lp(5`@V!2O: 'h?h{c0`@8T`b($gZ` `ATu( ""tZ(R&(;;OG?[n4"EL@# , @, cy߁-=K!қOFPCz5F.!u`Y`jZkoh @:T!XhnP|c*@b!AY? 9iO8/ ">p p%A >Hda6"r,diL ='$r :S6 G@Y RHu!5"mt/ V8ap<+n!3(0PA[9~G T ֖;Ġ d$L!t:- 1O~B41Le"3PKLO@`BC7:X)x€TyiФBA!#([Y010 aiKF#>JKDV]`@O 4U )RRB$!Et'\QT.e;IB򧒌(K3(Bx5 o4童mv"#̇bĒ-> d$x6DJ.&$ыI;$$䉠 -i8ԥ6R|+B,KԯwUk]׽~l`›j_/vLӆAmmonq6ѝnuvoyϛMyxƒ@BF; `&Pi+  O8fH` `%3 [|&@ pkDyh18r 7K<3p ×v`nؠ?pK@ns1Xb( UdA^0qT ,%Ṿ]Sq@ &[9$K|td"p K^DmwH]%jeyP{3gBf!\9+HJF$`j.|[V'0FCL%PpB$<,@(' p  ^`2* 0/pT$<E+"*$!4$ݬ4v C * TNC(Vb*!@Ex0$^$#RK Ʀ&)϶5Pc0wނmpNK>B)P C*Q< I/ adr!9c:$< n `,1B 'C `%;T $*"ЕB"\) !4R% $# $2AG$A TÑ%y!$ $2'M'%$!Q+d!h BB jp ʎ&H/r\I`R`.992.-$q%>>#?&?WZH"R %!R%+]Bm :۲l, &`!yBPdEZE ~a j`J!P@lD I ϬFnD?JS8!n >9KD:AIDJH@3@DD)r *n<נ6T<@ > FlGG%*$RCCT%A$~T$O@SD5ò7 sZALE)$P@F`^1#E5S/H@b%KI&`Mה$࿂&H`  ` _Kq4!8S\l3MTVfs߳;GRRUԸ4Sf(K"N `l!9!BR&$KKx@F$$ JQ@Ab@S&I!z&D N1B&VPW`"cRIZh"0 t@" ܒ `'e$&m`@"`(!R |DV|jTNE@JnXY$fZB" v@/__RYY a@"[[e@eBdId4 @ B`Z^^+ Љ\ L`&PVh/lRH>%Z@k6h`aHmct kxdOV_EBBʶnזk.@O`oUV |6h[%UCBp$$PoBs5m@ \Lu9S Dp M3r *b!\Vt (7 8aZʮdNFL7$4$T`z cTJ$ , rkv9Ng*@{D L,ixW6u^'2aUJBzz%a!N $@uO$UuɷtIip]"Q{ G9`! 3 xWC jrmGCkMElF !X؅aX%2&Nqm '%g^BiZ^ !@TQT,A^b w%`"`B$jvTƬ8 t&lfG(dɔP3rr@s*mFnDd&V@)F` 0yr0%3D@`4""^jOY@5$:$N9?BbrUbq!YiE  7 U O m"n8Qs]`E T ^tN'uVQy90 t@ z!zSwDii@"L@*$b':,voUv Jh (ڡ!F `sK£ T!؜4 &pQ2$TZaj 9.$r:bvUiPQi5Qe:%:`M ÂF~  *:\7 :Y>@sw7# Y8t;$\g{@BjԮZ݃x``!!d0t`1>"0*踎`4`O_`A0D*  zt@}U9dY Nȁ hGT:T)G4t }⿌ 22 ,hoH% a .I[k#7J$G%{A=T{Í|{B,\F!^ր> iT@dU{)t~E|{,/}5<# h bʭtV^ŏxA@ |ɛЂ/_`fxT[zFF̭| `zb0c0(OW%N2nr@У{ד\ˣ‚#}+]"iϨ}@  Â2JcD Bԭ=#}ۃ!;ɵGG%C =2TЄPhA{][=,T-AH/'$0<@bzM `(L@N{{ hǂ5;܍Y,]%"i*i FN hU$iB4@嶀בVB DV B ^ ^A 2?HHɔ@ݾ>׾>$bdU $^4g^]$5 ~ &) ߒ מk<$`Bhi󽞃]⿒@) Ȋ$6DxɗD%P6oU> HȍH q `&:Ɉw|QQyPCB[jI~W"sizj ?1ri!:P  ā-F "E # "C#IQhA"IA't`@ޑBI !l„ (4ѠG1A(U%Rرd˚A&κ}kE0QDVntlW  Q*dP 8+,!e:5AZ!b+T,Pl8^2waA lvu .Ȟ2B,I.!0(0XAZg'ّeAA*XL! $`tJ N5g2PhXLPP!YԎЋ@@@Dڍ%di! hP aE)0V[B!TՄOUV pO%wG A^O1BADЅ@L4bui^z ؁}fAfVGV@桧V|昋"Y{ijY@ps^W+jB& t |!=))ˬN"O j~6V TAG%,\ދc%c"ŕ;/=H$!wyp< @dc˃Il>`CCיzVW j*0,-f4=m  Ewlan$`@VtDPOuAp~5ET`@j`1+% @_B!u"  F9N=m().>)W,%HF/-0Ȁ7jRpdZ J@$0 QB,t_S^3gַ> l=Iynvɍtnߍf(z;~J} UrЁ';x=+_8$TH!O,YvǗ|$N cQxSN[:ϻgF4ܜ<{ ]@)P pNA·>C}TowÀ|wֳ}d/ώ}lo}t}| ~/+~oK~򔯼/k~^a )Pz+RlhaH@*M Ypf6d W \pt7@ LT~JoV/"! @/~ %6 "&&17Qhu`Y0/a臆Kp Anm BFS+7v kCs|v0Z ﷂr7hSur)f^88fxƄ|XdHgHs@ P*t @fX!!?P1sLauqmqpb vQOxueZ/. b 6a<2PfP)XA 9vƐ)Mh "Ɖ'i7񦁾x04980Xx~C)X][7YTg<P)p  )}uA a 0   @4p K@|l:D"*$3A3) s|f'Ty n ɚ `G+ѳ.J)Nkts04`,PjQQ, &< &r.!)\6p:/Bry T&IyUy*)J8hI:2׉Qi zj#"&iH\&z#8jeH+Yo`@ z@ @ ` 0  l@0 @ `@ ` L$0RȍP)xX5ub@1: Pp1uHzApk*E8P3`Zi@NO0x p@( eZHp HDPNzRPf-t eEدhڠy$J4:*?:%* z"+ƨ8 XLjTwqup J4lt } Uj@ 0 P i)~ e8( I&vpW!3`s@3AKȨOQXm1K05CPvXkQA-$0˹X $C)`lTa@pPP-lά9zͱ\;1ڀlx]̕;sg():ʿx;zL *Ks ،2i 8Vj§0 u1S{$a+",=M Hvy5U<=%.;uBi ,BA3M.B=pl7"zBmb241rn@RCR$P݆*`*ua&o,˱֏/ xܰݢ{=f+,y7.p1 qdP 0 fi ധ K  D \e@a Vf9p5MDK-Pѕ>Z`O4BeT$lb FhѾ/gsѥO^uٵowŏ'_yկg{ϧ_}H@%T P!phA H3" .C*2 EfpCzj rƄF|+# A HG G€ y `ōQ5J,WÍ,BZQ%D6 HDPSC@ $pB@v@H$,"A&`x4D) &00CF:4!Ew,QҗH` !bUXehFhS'Ori,UvYfB6,`   @ L&&}Be0QlHx_)U)@x`(0{MxaR #_aNH ",. xdć#$rQ].X*f]M")X Bt KbMaM6YP8>FH H 8Pۖ$4@W"[`E]w@ w{5Dh  9|d3c<4@lȌ 9ҍLHa^pb(xHYah0أH  Q]_ r} q!|"E*h("A<^{" L}(l6g㗿a*XbYJ]t 4P 6#lb%`^P/j'T@ea$ wd\  QIXt-] 8|$@oO . XƠ ]@5$O1h@ `(O"0"6p,"Xb/q##΂> J IN4?XRaCBAA Ġ,6!Zh D@h} PNm&70@XA\8@3,H oXkfPl$ y DlqyDЂC8pFd A0/U"HL`q2Q&N@ J0hBHЃIL4 ? (1E3 YfU[uN- Gu&04 ؄ Vqل,6MD,"A UJV։7w |+iAA C@B 9|a$uXG  ( /|vCx›v dP gc;[Ԗ D 241C fPB" " Rd%HTyZ(xj{^蔂hC}ۀhbhmH#`qNLXAL^Pf)wmC!!= / @  |[ćA]KBk yxQB7HN2| > $)P?d& @ mzT $$4tP;7p_D'ZѬ)Q>K) "Wd#1`YKZdHU&m A!`X`/<Ha X(vT*@w"hY& !bCA 4` =QvTD3gy\g>w6T d(E%A4Sd×`A6%t/\KlXZ_D@7M 5L 3@@HB09$`&nby x(4p=0; 9 K0 HH 2pAHE,ϋA@0ףnk( 6&z7>X#D{C8l/XA@ P``?PVR1{  pD CЀ[ `!?,Pĩ98|EX*d xp\ 7EaFZT P`$FiFjƪ(8ftkFoFp GqGr,Gs&PʧȮ| (L$ GĄgJHK 4ȔIH$Jlɂx˸|LdL(I ( H̎˭ Ȅ\ȇ,$ #L&x?(ň)]+H!B(fTB@OȇlxP :pI`,A5aO8@5 Sf<>hC#OMz 0! N@xP\ 4&s+9H5x" 0.XFEb6Pю`5i"p$%Q0@@8pDJB'QMNX5X.Upτ@%1p`-œ\5-ٺa!VX003=0mtUb&vb@\Y\@p0h=2H+~k.fj1p@` ɶ뙼b݃/(peHRuU HN 1%N-+;nHSbDxYFɡB `88% Dڪٮ qr>7HȄ=g"cngV؟FB.I(^n}3%7q_0mG7hnbjزj_ fpg p RBf!pp bff%;,Us9!P%j86\Hm)灈/Umr[(ڀ{'뗡a1)B):0Ao7-('BCG$?hU=H<1hxQt <ۂ((ot`uDN\@@tKv(QJ=( i;`jt_ux(vP\'Ht_+J[_vLN3)ppWpʲrS=0h7L`>ݦmwXB@/s0@ 0{}'( '/࢞)nbhcr( +,ڂs9tulYܖ?y`0xHd5D8NH4@rYi H TqW'XhN9W/s11G{OeU',{ H$k&gչC8'ph5"Ā[s˝Ѐ@b5@ׂ03PV ,*V[Ś-eN,i.`,(-޺~1", !8'LYe7w/1u:P`A,(D$)K&p@A9xqbłX$p@ǐ'wIx!D-+[$;WpBSZP"H_$HAV*YY$QhT,Z@2@D!kM(@BʂGk%R5gLX(hSS$8"aܛQZM F4`jā8i !F)' P'҃1 x@C I!L  H&kϦ%h`Rƛo߅7^yͥZl@!X}\"Ā' AGXL9`[u @Ǟ{%ACvCAh0IH[rW)}-AavIfgz@o%Pqț Ý}1'dy/iT'A @FJТPC V)N$0@`nfP̡* *YAj '@{BB0C) "RФ!@o6+FD+mAwHA&}ZPF;-A:Gt ڲ j`"\P + @A êAn<,AM/I\!3q\O= Г2$Ssh2l3y"0{ @B_T dt5 @vY%T mR dQ(4-D` &0= !~Dr b 0AYPq!`ނ bm6 Q3Mܝpw [a_x |"F$E( So8;l3+/-_9U O_ɟ)6T\"=A܄=)O}Sݶ( OXX_ŨI)`RU4`[TA>E@ :AР(KUXH#+Z٪O]` eA  i`{ !$MBb<) ݉4_ HHw[RH:r4XT`Db  hMB6-3tCAF@4I&/P #+Dni!L,^h&Le_-< X!`c`"@@ǾmАdtSL$AA8AL4@'XA$`q̓;`(CŒh,)Mj:Y !Tw=8#&Ȅp,"C'MmSp@PL0==2ԡTJ^SR C` C4/d)U?)!`͚իiAV,t.o`H!A; YB@){ dJI\yL LL@/вQdQBl!1U @/v{@kp  8a+ #3 8U 63%|SVI^rQ 4}kd"PZA-u2,e kJ =Ildp+JL! nWn@2ձq,f$'6X]5 @ɵ@vƳOD @ҡRp?g$Ӯ<̑5a{ bjTEbDxk Գ.s5 !×An)T@Sx 4 RoD+tczח@##"q  F;RoCH]?*. @ UαB!^|n7hbȳH9UtS/HG^lH zy\.x ";MӾvd0t{jg;APkz"%x aS%" <0(O'@DҩO>bBF02+/s|P ؜˛)% >GV$y H ǦGtT0A< <j]_ @}%00DAX Н 8NE\Kq lpǁAˑ/ i5Ř^) n A A`AXL A\}`Al!ta(I %( Ҡba Az@\$!'H#dL eT:"KJeUZeBlUpeWr% WzeIj%xLYe[e\VZe]]Q J!X%\%^ea"fbd0fc2& %c:G)ebffjfrfgzgfhhfiifjjfkkfllfmmfnnfoogp pgqq"gr*r2gs:sBgtJt^J%@ Z\ Pb%^gVdzgIs$j'bdIH~gvzv'х*gzrv x%u͡zhh{b} 5hJ%Nhv'f=( (IvgR(w({&|(y܁' ddɋ&(j\I:orhhbFVhH((he}h(ghR蒢e*~(n &)9~iz~gj(nhj('J獒'~izYJgꏺgʧbyЏI~(b~bhr*J)fQ*Zh0j~꟎)^) ]*n*L橭ꌂ'ꡦ圎j6y*Ёiy+"N+ihJz뷪iR.)i!'>)^b麲+kkg2+^d謆rjk(Z(f>,kJ+6yj֩꫓*vl꺂g+JlĂ阚k)v(>k")Z,,-Ҭn+"2R-Zik6+v-lZ-rjjfk퇖j"hknm.Z,l2r,n-lȢަkN*i«\F:ެfnn)V..(ђikZ,'jȞn}..bɦl)B,lz&mnhi V~,﹊m껾n'+*.FmjoN-Rlk*&n(꯹n:./έ".:^..Җnn-g0kB/Z/^0 Jjb.ۢ- O,k U^+oٶ/zo+f~.p K 㬕plZ/nnbop0q{/ s/"0.(6k'_+rﮚBq k}*+j6*q#_)ˎª'ghjzl%f Ʊ*nNo0zl1-c氆o*/&w(rB!o!w)ï WnB). ӳBsD#cog>r8ӮҲî31o.0on2 c5qLG,.lo7QϳQ K4>R(CR7+EnU#.Bkm5K%b z>q[q nq6-1?o/F2Dk'05 SV43u & v2 Wl6\17pg{6boBf{035X30pۚO2vW)*{b*lqDw"7!r>7Y;w2qPtCovt?jskRhT[owq'zcx&|S]b|ZwpOt7ww2gKw^%ɚ&wGmB8=;b۩~O8g2+xxxxxxxxy yc iCyKkfd[T$Sy |m!u, 6 @CZ/A @P(@kpTxy;  #\A-A0P 4_EA l@l{P.>Cs p%P#@&< eU`D Q$tA ЁL{GXDN,ESSqPb0^@H@@B {3|lƈ{˷ Ё:RqÛɣnR_T%=PR ؒ(^Jć|} jXsE؄q|) hh`{U*|%bv}zؓ}q"걞둡' ` ƫ)]JB/y}%6fIFJ7J"#~+SgKaRc~ks~{~~~~~ e9>XC!Z&H,"d- Y~CF)Hb- %?bX;g)B0 H#!@"@  0A%D,zbPcaB64t! L$BE"dR3:ԢCVqUJ -`ϝC5ziRK6ujTS'+H5k= 5R4 ,ibA4%8G8ۘjaCTgJVމ* J+uxb穨H-X/QF4_W`GB>2RҸ0 fs$~ZAH(@=|Ejt Hb(!<j!^ࢤ*-s%) - ynr$(,k-<Kf^e1! ~ (5 JZk Oe"^۩Ij2) VRT@B )ࡡ'h@:b\J~L &"0ҏ? ETQ 3BAPRM=զ%2D8o1+B!cFoKW]pXpHŇD>(Fڐ"ZM")-Ϭu)k'ZKV6m #N჏ \t6܆ x2rG>,adMBQxr}H`"CY}m?cAH&xFY`1 U& Ep~ [maq$)alze vH+Jdz(IzF 1!Bt"X 4( G`O-ӡF!;4t)V"ףe0Ujju=l}Kcıreq1=Dso PbQw j+adlDli T`h(v}qEW9 r}{_0Ā %x/3 G`PP"}FbAQj8b,r6ġy$-aDV$D%2`ib%Dl")lt$.iTaD*LB!Np!Pqd%s@VY15INO6D5󳟆xr Vr@ h@ P |( I){慲}C8J,v :ѐM!P""t/V+!e*L \4D6bC  _%k(JQx!%`D:PQPHH:1u5h*`F1Br;"Gs괉8RR3p(N (jAPK "6zHUrU+r TQx\A CPǖ#R HBD+pHeDn P!-SQjlg?; gА}E?MJV )> vSzD`O!D8|9 4 Vq +Z}p:T]8RCaȦh$_\(6 YVO^co[V7PDBVK `Ć!?ŁC \Խu|l Q!7nH]b%.."4;Z!}rC` FB/-eCx$6 @B!OpCbP*  8@CPP ?,@vN_Ns)mѽv"hpCА`O6$b5xK"B*ryhsNꩃօ^7jeC4k`1E`fmPw=SP<\ X!cX w!^7d\Px_>h# NL_w҉KXq/Dc6x"CX}s|7[IQ/篁yȹld+7d2gJِ 8 >`#!Bz7-ؤp 8J029[0)SkH=(wO.w@"0P*0ȶm1h{)_mдY=6 1Gt {cD-DbK &YbRl =a2.(|Ch#:yCYI !Jb62؀K8.,FlMD?zb&Ɏ)D"ŷ=) tl"N(`RN" |L6!L\ g""` )H !P`"V 2rghr)z;Om'P BKZbO ! H !x{dAW!WʁL"A!Wi& b do(`L *a^cz"ANF(f" !c1!.1;iNLȆ H!fǥGhTXQarq[!Bja*BJhjn@Qh. !ln/q, @ ?Gs0,@r@` <(b4! !VAA΀@Ə`DR$L'8@B 4`7Z P0Җ$pD@" @ ˢ())"axV  p%AڰI z R'*A1v!ؠ@\<,*|/b!HȈ./b` /2H"S2)2]j VaJΏ b! 30R/o pN356Q2N*h:!n!a1!6g&2{88.2/!fl9@63= N!B .| `n2( Rpj p r`> >5Lku> !g2 &.'` Rp !-,h.)wG2h !a4T)@xi(`DJIIŦt%)`^l@a)aƴLMLJoMsK KbAtB(tNwG J <Ӗ:&Qu'Q+)@ B`%t"@GGTGaJUцMCt%lm UV ( Tt) zYYXUZ3$b-Cu%uZU) "bT]]+*@\U^^^_3_`V`` `aVaaab#Vb'b+b/c3Vc7c;c?dCv)k'Vwe"V\wIfd?2jZ0ZU5 5Bj{bYf{gMvHiHveogVgEVk'$j%jk2P6kOelh9lUgٖ(mukזB6ntUöklf6hhUoein7pvjӶjnnr!Dr"sVHvgll2V|V]'^ۋ`&b <~"F`z!(_^ 4! Ԙ)"Ҡf#\l>" ^_ _#_'+/ jpy"(b zd-jbd(J2Ȁja(@w_m bt2*Ƞf?*`d):x!8"> H$B"V".%#bADP"!t"?r`!>!f&n " 0@*\ȰÇ#JHŋ3jh B8(R$֬31BHm!y1 1h~ Z Wmk(!JZƯQD)"<%K!maѣI'0 L Q'40Ň4."D$@Y z@P 2P"@D&"g0kE`@FA$;~`_D1%LjνËOȏzCT/n,5fo 0dp5XOEQs*TTݗFBxQw~w ,ɕBEI& n:1 E<b &<& APP"p$`B@bDTZ9s !rFu/4p)t <{#A87=ƄDH91X7ΨF9m7SAK G } | 0B G,"A bbq]+pq`ZG>m}0#> +&b1 ^@ |`w/*W $ GY7,@x(Ht7GhkH2$VƁg(6|D)$/DBS@[0M(h"%bUDb@F Lf$pAE(DZ r8%p)AE҈셯b8ɩCY\" M?|NО10EUXB:u &jLsv3(EQAJA_@@p 0 r H L =3#Cv)L :XAA:%$'hf vLcRH@Z HzD .ୃX@\ - Kܤ`K‚%$6S>$'>C}r49ϝ$tHP()G8dd  XJ Vq Z E@+ڂdj mmv[A[ :c؝"\E $~ލCt v0uRd E+fAV5uoA, %B_* Ũrm\}P0@] @ o[amWo Y0! 'J(\Q$A0  8@DAP j@ 8@:`'F0*`ܺ&ml92!hpC@`OҚ%~ihBЈ.zX?9v}*eJG9ClI7DQ t$P"h.u[B`18Q Q@"˜q5 "kp$\цfnt N_JA/ BT"A:1D:q T7=Ay{fnw;6cMhW\H\q#pA O֞60A!OPD?#QA|# _x l0|YQo qݼ Ib"(ʡ:jԉ^`Tr #Ƈ~Ӈ P -U ;.HbJ|aV&m \`6yXah%b0+b~@  VP0fp1 LGUcm~ pfb&fi3ȎC !]@87i0IE~!(@%_!Dh0Ex$8Y x'|R}b 2x .Hwpi ʠ 2bp)P A l 0@@r *@ mk p EyII <6 m˴˾{G#,aQ @lf<e{[=jwlXokLeLø;q|m;,{;\[slŀ^Z̺K gdj [9\ɝ̷sB|Lzl+AN'3.cp%pDϴ3cg04">Jn@ NAP"$>4^jgs0R hc05P&,NPr.c6R>O0x @p3EWVqRKFH #T^v~q)0uP> m5@>锾r_ a  ]Zꮞ3pwb^4@P>P+^ƎE1A鸎#b3g7pw+L(S~y<6n23~&#=Оf+~V#iP}g\"?$_&(*,.02?4_68:3#bKO͟W[GzhbAfQТyp rz~+2q1Ňi\ 3DH-^ #*@*HEJPD(C1)P`6D "lL`?=Ԃ0h+U]~VXe͞EVڰhɫ6ϋsZ22#a;φ \C9ZB`xgI]XuY6V<)XKi̛J TD/WbG'P,xL(F?_6jh#ݍA==)9c,.ZP60B 'P5xhؐ8o1f.B"cFop/r` ;+0#N჏kCKJ+hG$DM ZM")Ţ*0r,$20íS.N4a$%RH"J, Z j7qKFPOLP$E8}K-*M#E%EXTQ={X|3N@TIQGd9et _ 5j--M$JB(:( +.> "RB+H 춻\ȿ2 ay#XX(.QGHyǭ*9fgfZٰûkıuęe|1ð0%X+Iڸ(^(!^> AY6.dV|a*Xbբ//̤mբ["Fy"v6m֛n> jؠFHH IK>+jY$Pf)wvuݢde8nkEO~V%"F,\6O}#a-$ax17g:|q7 $ )q 7,@xH eJ2L /p2\#PA: \>VEP `5ON DRikXֺf3.vы_cW(g2;ahSS e`? ^/آXt6\DҞ`nl#Q@Fde0b#j OҐi$$-HM*XAEpyQ%|?YD, hxy_61hZE EL0ԴfA@ R@)0b`:EL4TBz^"WCAaHHA*Vp p& SX-"D" 'Mi7  #  dT65sG<ҥK^p-i"G8P$I > =uk_kid&ƚ@aiֳzSY#fƮ#T)YbJ,Y \b_#hkZ&Fy]b#oYp^]fY- An=ǫ>ޢwd9Z @*J0=\a$A0" 1`"8*aQ) 8 >1!P# 301)P[SO pah+*Nr(e# NL#3҉K [<]Z?YEZUhY@ҤkwkAƮLo^KD. !X 8 >`B1BCOBE,2J0\[0)SW JSyP1<8  Q$L0a{T5nCf*/\xsQps 3FAֲv% r8ZY&\b+4&D;N `B]57bQt%|慚 T@؍{fmH{q-/p=v;YuװpEhv< x$+@VZE1laa)' @#XM`nXz#Aq : 2a! #?zҗ.d0Gă]~fv8 0ljAp0uPc8O7`:ѬhßVE>@*馂Hv+D f6 Y6@јAG#%0JF8@m 8B"4ѩ‚SmK'RRp!(|/A 'hAh.[.hK(܌'LCcC:@TC@4x7B/p 4(Ђ( 3x&+~BD("8bDp\E 1a+Y H_tȎ3 @9P@t@|#r4ǟ ɐkv;|PlG0oPe0;Hq J`U ޱ<È؅8`J`HHei-0ARYN AѱdĴ5D :Hc:D1@ h$@uŮx'HSŋPFC#x)@Lsj`@ЉBLl֤F؂|0m,,ȃ 4H?@Am0C5T EeFuGHIJKLMNOPQ%R5SETUUeVuWXY}T23% ]Uΰ_U(kb9dU Va- v$ÁcgcVڈ2,Vqe9q\nVZui]PV(U׳(|UWsUW|ױWXfmy=WzW pւV~5XeWk תփ{=#{Xr=}ةV1WTXX}ח]Y؛Y׆{דebVVt5G}XV| XgeG]=#X,k>Yd؉=jX{%XXUҙmٗ5ۣupZVءXmڌ ئ]XuZZؒ%W}ZכֹZ[M\οՑEWEym[\ ֘ZiͲUڛZ-]vڣ]ֵָ-ܳ=]ۤ%\e۟M\]۫Y=YX۽ۅ\f]ܔ o\}\Z^^u[MrמVõe=5Yӭ^=_ڍمXU_%%ZKE[Y Õ`~=`ЍϥX݊}_,mv_] Y` \}]^ۏ ݵإN a`naݫUa#UaV`]=asUY'^.ଭe2fXM_cZ-c6"]=}ZMa b %>c]7Vb"n_E^d&]}DFܜ0f`ᠥ`4nޛ3e5؍[D&e.D0%_ѽ^BfYU&ePdmdd$W"!FKNf,v, aFa*ụrgg~XfR`&ce*f5]mfeiX]fz[.hZFkيdx^Ɪ]u^W/*ۗ]]n&FEe`d#NRZV*>fhRfƭ1.~T%lVv<fm~cf&6FVfvp00P60 (X@ʼnC8vAnU h: ,gS ␂GD/x+1oPNx7/Wg茁'< ۧ?$pw'0xQqWMiĈ k H]5g69r8 `r@RuCGD7ŋs;s28(0tEQ'(=؈H( 27H(^uEW$&HFDHbԧHw7* _Q @Mmv( Hx@VCFJBn|_q @'7GWgw'd=r؇}(h/`Mg$+L XL-Ow Ǹ@>mto*g)!i6zx'P|Mec@Ȃ4($"[{ؘy}%M:pq3`D/X7HV9 I`:8+h8xxt@&P0"Tc 2gҬi&Μ:w'РB-j(H2m)ԨRR=?Z( 4׃1yfM5y=Egdc뵛1?; B y(C68A@ jtRB`чdXDGn @_x2O cL衣4 A oP-r@Gϛ vAr5l@dn Uӯo>z s~ 8 W"1V}Aưs~\2`xA"+C RA& T`a pZp*Buf :FA&@3M/\w) Ct$ EOL@VΔiI1AL b Y]$:8(%z(:aCK>:U9ha[o `Xt1#DG >:08 bUpCPagq1A: |\`Mvb;,Pքq_4A QXd&a4t6 EAnd )KߢU<1kLRj)X酞:\]*4.AgPcNj0K6f3Ly"0{ @gŀ71*]ZyKTA3%jt d}]" Ep襽9,(Ǎ;x^ 9[>W,X#`9pe(>|P]0a~ #(A* dH[&VQC1gjk5HEq$N̤a  ԙ{0&jHHPz9:9}6J<ʸAa*p:SCǠe` ng0VI0 gSCXA G@Hna\  `naXHN0kbBE&`?A b8H.~ 9p pW [A'éM$=@ "E))42Axp+15QNP; P2$&n'{hEbx; pt@@<A%`W0l/8>pta9#4'E5^Kx^Ʊ1H~0t5LU2b*Rzϼ%Oqf8;63[ zPsN7[po"tӼF_m7&CTyѣ.g9uNms^.f?;Ӯn;.ӽ> \Zz;0% 0G@HJ`IQ}xœ4e(ɵ 7*Bs/ B/\ C VP"+iK@%#_X(Dv%9IJ.+@ D(ro9~|a o H~ !dD؁IE0`LRB_YF1iɄbX@A!ȁC` @LA|TM@F nDT@\H$!m{ćEX!j! BBTOzpF_E`Pu`vaf!aq G z6K7EОHTQ ܁oDFP@TR6% ȋ, DP:e&MO,`K 0pH@$,Zd%Xcn^_VYGXFTZaZd2E0@`XnP 8uWg.Z%n&%TCL(eH8A"|;!Ќ r*'s' L!tnzJM|'A܁'8b2|EH h(|یgx$r6A肆 @ >vJ(x`YgAX(}& Ax'MXDlew]D^:Da ॄhhMjA:A6(M6OL؀!|BA%*rB `㬪~ *Q?uSkB>~R:(E TEeԭ $Dђ-ᒑTA]ǂnX=ٓ iE#bA Lb l=mAM`,F<--Nmh̺~l(L lؼEtt,櫡k@,Nl:mmv)Hp@JdDL͔HUS}MZPND `ZNmQD↦D.[  \TPAnFE.a Hb>/FN/V^/fn/v~//>N e\iU@oHqB$1ć/4ٶ gٯU@!*'|mXL0Shwoy! @Z雡  11Jyܗ0 [U\]Y%Xe{07؀ hYWMq[YӘ 0wq%1OtE={Y ۫iڕu[Er!Z p#[ױ'p D T$s!ۜ1‰Z M[!\ 1= '|N͛02s{[4{ؐ02Us/c c0sri2}) 7%W "Z$W23ѹ \󹥲;GDA<@`,?16 >sZ-3Z=D'Q2$2C1&'3qwD5U$:A?Z񰯅+S19K}4q!G3iu5Xtñ Zd@e2];q6G7_KCpqZc6sKbbGvn=\eK6fgfo6gwg6hh6ii6jjV|zYLx >pMd`k3@(ބP@Emwwn[oKmO(7s#u|s Xp'} q E$XOd@ lGEXix^~6;lPɄ$'@D @kzk!MpԸ O MH@ A@|NA@v<  NDD_9hM@_A$N KD AAHA#h x 9 88O؀$\A!N@&hIy$i| M̀{cU˄LA@`@F84xNAtSeP <DD&$$A9{<̭Gg&ON쁿̄UѤ AOT@7@eDNo;:( Dpz/4z3fJB쀸< `:+wD"PF`x Р (G3LDdɜ!|!& <ɷP}X" 4ӭ#I.^Ļًؗ=ٛA|M'H|̃IwF IIFELo  ,H|b=cͨLBd@H:}OUEG# |Bց' + Q!Q Ddm@"pB|l@ p ."Y nP`#H0bre>;#6X`•< D[mx`TWZ @  W\330"4̈ѥ}_(W3X1 r%88]yUbV&h 1H@(# VXP~|Ћ/l|W/T뭹[챽W~@aW%UA'H: V@bGv#pc'h6z<, ¡7@C@%!2' Xu<@ t` lT 3? ,~!Wx@rD.o\q_;"'X bg$M" 8".xD' x`B!d3 QB-Č6/Ѧ{H`AF7<8$=@LPĥIC)၈2 !+O02ZpBHa^)!rF!\ ȝVGDS `tFjdc  !RA/23'B B8$0?sh+`"&pY M WC,4yɅմ&n\Mmn7'@"FC!A8%!7 <A)H؜ , Nli^ 1"A!E Y A; 6" !pǺų` IL! 8DJRD:zbְ6(wӟ".>z(X% S/+7H !? #B`  u4= % YeO.Zqe1Yn6Q |@skrh< 0@l@jp!" ݂BX p Pe*(AЃ0!0JZ<֤+u;Pf&"$@~;"!J"@_msF @i{EWsR >G--p B[ Br!"A>BbI 7qU|`$ D\:-ح0W3X5򑑜d%c6M^f`.BJ(O9K'hvBʟ|(dD+ L8;dၔ/#.3o&hE/э n<$ @8$DFI'jӝ4DJiPӹ0LkYϚֵ5e^d& Yc;]w(aYv(/$[nmmonq6ѝnuvoyϛo}N&"N6p&<pKF 7|ᕙxqk3#-@ oy ͨ3w%ىc\ρN<"w93.g|^Sns/|yO>729/dGzgJtSө>s^{]lz1^?;^l;z8ܞW@oK{KW]߸s~/:օO_g|^?oNo|7>~|~(')ue|k]/>/"*O|o(nnKomBp%tnn0p0_npo^oȯo-0 W܏pO>w/axNP UO p m;6P0ўP DP pS0!p0O<PpQ:0cIP 0QFQ DPI0, /1 A0 . qOM1](p7PowQ]1Pg O o 1p1cs5 YH1D /`Cc1t!X+ s1N0#.0%2 r&i2ߴ3:r" CiQE&rѮj툲&)05PQ8r*;(QH&+:R)I$Q҄+ٲ--2.r...2/r///30s0 0 0312,3 P(2!e\L5@ ^ n~ 3 `5 !iR3f5WSF $`4 V܄2 ʹ ڼ5'x`89W : "# "8+9-;+V  H@7GS?:D2Es%:"J4!I@34A5Gu+ԓ5L4 ֈgK7# tKLSQ .6K#,A 0J# Fh @U t%vN88aPE V` bD@g(iN"b x{RuUW.`8(2XƊ"LTOu2HULx52z@>>B'R/(tlPÂp`\ z $آU52+pCu ScXXRW` "w(R1^O4S7Sa2XUPohOB "Lb%1_7C\Oj3nu% "buVk`O(R'4 +hfYY݄b5`P,dSad֮[jծHcn,4\PvlvR5v 0M\ `L Ҭ |+r'6m`mY @`ZʈS$TDļT0yr-w% fw3Vb_pS0V,q#?u%w2 T GpK^'a|@A Eex0w\'i" z@IqI3u'C `%vvGKz "vmVnEl2"# 8 6{"%0sb "p7cxG4w%η!8gfAnS +n7vwFhy'U~ͨԗ} b+| xY&M   @6 ~a j`ȗV d X h \@  V "rxLf` XFa @H@, +r x`@$W+|T]XNK"^ !BXi؆ u x%qALB_i `؉l ")Ñ4_XT "4! n@VF7ry\Kʆ|3DqEXy@ xT' @ EㅋBddن_xLN = bǙ{:q(Y!$2 )sy%:`LڤWB'6o:bQ#6E # " v@0 #>"$  Ng**n;weU PHMDx$ c !J("t'@ F@;@B,BHEY۵ s)"guW,B-jC4 |@~*8`«2;9#z{~>c"tnc A `0OZ?Ap"8Sv$y& hB&ęs) D `*  {[ZZ\ %G`|\qB' 6b BLp8Z` >@'P'N{d-ҙWۻ'@.T+h]CBG-"@ !h\WYs"Oݗsx~W<"CՅ sb it(W4=]2|;&\yǁO(  frhH F}Tu@]s#k /?>Ot_xDLDDWI~eJ7+Eg`8DC C$G^FFKdzq0>v+ed›?!菾=HB(BbB"_Bd8'A= GxQ HpL-D MC@-BQ$P`bB 7PTb'> dBCKh$ -?@O%FѤKr0Fd͢U%TH80!T'LgʨRrRz%RG!W~ݼw @+0y@ǐ'w06R2HB"Xdq0AE@L rz P"ApCwPzeify"ǜsA Ev&txQPB."\C;(hqԅ | ~ut$QN A LBs,H *At;'# AH=4)E1$&X`ă40Aj RJ4C: C@0h:%ACtjdgh0H=A~/hEAZ`'z9]~b9 +QH&)FJ*t/{r)>B KT)hQ1(RSOi.,tiZY0"V.P >TYXKc H``GyƤh|&|жͷI NkqUD"aa~CTpsāN?Ti '~BR e|hp8MjTwE:p ;<<0H:tyS38</1@.~zkCD=%{qsB@#*d) d2 UO*  (T!xAw1P@B` &5|Q$f!E T(-gc:D)X ( ~Td_k`װB] 3F]`ҙ4i4̜J)OU$=;?C$)T$e' HrNDp.BdDG(p1䦃a9D!`)>20 A@&#X!in2j"!"J=HEe*E 0`)g?鏣e{XS*ԩ"' Aafx+E4xD+c'Oa  fagE`^Ct`&&K$҃5]\ĬfI&Em rƽ֌ 3FJ:KjyxPV ȃ +I@<"(c K%=]vXDѥ0 *qECBc ` ݀w!`Rae#kO̖,H()rb"Ygaw/`~ʦ]{D>"6mԢ q ۱/l`䋰S6Ä3 B@* n |=`A"$X3n@WiH"3 Lo#Rx Gl)7џsS;z+Mq 0P`I|0 0>8` @vB. ̘XC,P!g (38!3 ‹(cX?:0!\oY_Ye6; " ^ϼ Q)`v##PZAAeqH<[v(Dlu$K*((n]g7A ҚVl@ϥҴ>o*@Ct:Wp @; \ kPx ~׺iI]LE&A6޼kg{@_= k8Wv;;8d KI # Efǭo]xR@H2fw&Fra&3w=H2PUw b+8%(U9 Rf &-0Yh%aHU4lh(0)Mm4mpennHpmQ.0DA7E>@>#p>v/P^"P8cIR4l f+6gZg> XX@reHsR3e7W[X9PP3)@!`ʦmꦟ1U'*djsɥ^} *Jjʨꨏ *Jjʩ꩟ *Jjʪꪯ *<`zPLx+r Pi)pP rbAxA0P:**JJ⊮ڮ zZ*j & :Az ۰ZK + ڱ ${&+ , ~&k( {4ѯt 6  xpNPl2@v*]-"ȭLAΪ ԫ%pd`'0F7 +* jU!OpXx`D1J#? ibA2`^*wǍUc<,im'L݂oʆ ɷ 3`}b-KmZʬ zɍ-ޒ}ֵ֤+ɗ ń]=F<Œ߻~ѝݑ-LN7 "9MJp**jDpt$b*Ft1 - `!? '{" WRS)7/-<:m_~ˬݽ}N, k| )n$8C 5,pg#N[ȶLAͲ ^س|6j;}{ދ)IDM.:4n J*;NSD4OJ(%T yP {KA ` ŁZ Քd.K,gE]-L͎lꥎ#nاjVMQ cՏ|n>͋Ά޼_=^L vZEM:`na];Zg'*K_Mv&n+#bӡG6Pt`WA].}l]>˽1۴x kvmнb_r^6W^̸cu/ߢ\y a q=ռ<WT#xWrr*'I0w\~6vr0*:hF~wovy{h<y!M{\m=Oͯ_Gmfᖎanv_͖W,  Р@ &DhA68@-RpF?$Ɏ&9f˕;jTH3&K27ĘeN:w2%ď@ ]џEaJIҦJ(TqȧONHJhUHh^f&dR)@f#ca#`F4. ) 5e$[WoU]ǒY1תd].tTͯO7:֚R/F9Ej5\>xοn>24=c/cJҊ@3p髰,êԻ,kKA"B!1z2 b!Jp!2 8PIQH).+s'[0ċ F|9įSH O-APDD/#uQHL> O tNOKl3"44u$ tWi}ԆnTWN;a5XdK*[VKCuv?EqKA l}[p4pǵWoUw]vE]\υwl^|w_~_x` 6`Vxava#xb+b3xc;cCydK6dSVye[vecyfkb=oU 7l*S6hY@j3g}VjzID>qELW\V{mN,zhn]won1hQ.\|r+dzMk:n/oRtSW}cbѿ~ܣW}w{_>p]C[Dl#Ah}wyGĴhv: K0C~|7Գc >x\0BϷ~Wwy;Bw@&PwT(u4AQ`-xA PS2Aa MxBP+da ]BP3a mP;!C QC$bxD$&QKdbD(FQSbx(@[bE0Qc$cxF4QfVF8QscxGw\S$f&GyLd&Sc)[LhFSd-yMl氖,=$Gfsфd`+vf7Kp.LG:D rs!eL b,!08E5: %@,dERtB|GFP)(@M@t.#@SS 9? *S0$I Qmڲp@8wT'+R &4QgMJOHzD"WhW%ⵒ`ElaDMpS`F5Eذ 0^(560H(l3W)?z@+GAGXnln964{A:nĄq ho0s;nHv+JP6َtUфbLh|ۚƗFdb2%b86񰉅bY1b e3:³\4d "Bm+GPlW[:6& 7p<1 +M1$\47u@ pB{ s20DpxtEXa&TCҀ@ zc?\CM_!/D Bz(bbâT b!z?f@`,{Kf&A ? <\ jhw8txqqt86:!ApA AV[H tHh(v7h[`[H0[`BhoH,p#"l#:j8,ie8+ix@Zd Tȧ @??i8"~##RASK XX828Т6>]lF" ihS8rH${@D^[IKԢLNU TElG10/Z8`TP#:Hrhih:A؄c[-rPEjN I2@l7""{PF7ZR2+mm@FmG76JrGr4Gw 0R=T!-#-69ļUм)RF(Hhah8 1*7R)#%f`6x'82?uVX3.e{#GX+}#B}CB=%(<@4( 8pNP@'J USV}UBUOUQJ̼$LK$L /@BdK24 r KLK K  VܴrA=#V ?fH`Pٛ=ڜ>0Rb\Ae B Pۼ59n0C;Xp#B Q@R7kR8!-Ǎ;Ru\u#2FU 7bs:эN `0@8H9-JA-̕GhGhk]mT7r\mF>LNu B< _pBz2e 0HSP=Ve˹ "~ΑKb "nD Pk4LoWd &Wq"zNYZoL|ap5da[`6؀} .`h/'th"Ā\@N,+l+L":7|,Y :,f|a`%و".hkM JPc_AބF(P #2KhZ"I0ZBc-A&KhU([#:c-c2=:fgmSJRU[e]7:9eu#<-o7Ghڛ17KȕAr[]_fk];ȥU\p_9jKgK1a~ov}#wgyܻT {mBb./I(0AZ~Fhf$^%aj"@f=`\i&(iu ?ine8'>"14; j]=s`nb|adW? abU! !wRX$4uWH :N[ b!jzeOس:L* V.6^"86p.τ#-Ǵm"{H[/jբ[ڞmdY c.8"8۲]#&>d5]ps•eZ$]x`P6h,)a=.%#mH5}}6E{vd4"SGDY8mrبER 3ІQ@"r@OKDUYv90[u-u_ׄOe%]uO ]-i8A{8s#-Y^vpn6{a6`=7jR(TEx-abFz +mV`IKgGȆ [ Jwg'qNr|-݂MRTer##|##BJ''ـP~pJ2zWz Xu>O_._LApK/PoMq|˼k{,~=%>"׿:꼤‚~@~Գ/,zP|ww`ss{"ª}v/i{whH >neoV%447g*Wz^LvU|FSFXhUI`#U`? G{X0XtdTh%i0`dž m @j%`T@QY%H=`6"% )R#`"+5+`P#G)s&Mj9Ϟ<ɰ)MSJxӤLԆج"'*6b t)U!ZN'`0SfN5Uv@-@`HY+a  2'4iIlbDV@uoRbY ;k_F|Spl^s6 zxCg6hl`\Mm{H9X3|3ˆA {t=7ŕ4C;SmB Bpk܌Vᙚ+R _Mf ^6`9@fЪmk&wm/g‰mI >(pmpwds`Plvfn}l5MnA&@Cp ~YǠ6`<Ⴑ۠A '[0*hMEs17mNyG&/ M5*񳝺l=' `.@&! P`%Қ3!j`a R@6PӦlf @d7{φ7DՆٰ E2@U<O @!ٌ6O~'cԧ^uElD&` b}z̙'Zzɇ8 4lq` ْA@qswMCeyfO7 z*Pçm;pfY T  A  LA A!0@P0!Ahz B$b՛٠ @ <@x ʠЙz S ` __Р J@aHp79`  hM !`Aڙ4ZŎ~re_A 6R*M<M0A|B N X!Ёb @mxP0x"(٠@&[#>] <@M[ c ѡ/vb&#Hb9ٰ@/ 0Ud6v776UY@vq5 6 L =#>$a!wBB$N u<"A؀,"4M@ϹM@ۘ 30A#' X @|A(p0d [MW AO:\@VIO H$]G5R #%x_ʙWe~^ B[@Z JJe[b ]f f&.fcxp@T A#HjkqMOXabiZ[byz~stz꽽z{BDN8:DkmsFHRbck㎏>@JLNW؞NPX46A;=FegnYZb\^e24@nou_`g{}SU]vw}PQZ-/;WY`滼@BKEGPKLUxy~')5̾FGQDFOghoćmnt13>bdj`ahˀhip*-7efm]57Buv{KMVѐ~prw}~UV_z,.8r乹Ьة₭ocdlr02<`}򈷸[]dm`dN68;DGAPSFHKChbJNDLPEXZaVYI_bM>A?oORFO{\@C@uzW.OAdhlQu;>=~Ziw|WmqSfotS|Yl_Y\KKrkDhfGl{6vOwRUG1^F57;BdLu7}QqvUMw4e@]*8:vIq/D,*565rM:W,=5[DAbDf>]^MR ?u3N*HnjoRJVAb=[B* HÇU 8q#=& \LX1p S "1 ‹U Qa$e`X& Ǎ?" "@ m`0MDjt'DmKV&S;["iMUJt蠠Sɰc%Z $Q-`O`@X&#KL˘3kB@JY*4qa Y \@Ix6 P_!Xeխ_+,!Misgmimw;8rpDB)6Ovrɔo@uG 1qDDL)$DeS(tDXBUe}1h8[ EIA\@jD@$ QZQ d$J2ИI.iJ^y`Iǔ9 M> @xVyeL&JAk&Yz Iba@f5$bEDc H)D>BT:C3j뭸檫e[pq E@r9@!0H+^ t"d:_@=@Bt@o\ru(+/v(>t@DC ."t8  QI!I2F*$EL$(tK{l;@$ J3\@gl675"D6Bz< .=w`=A$S2ja`i+x ]1^iyU{m* 9*A3^@c (r/7.;YTF c )t=!Df <;E% t `CH$(CDTbq  !Ɣ9!$hu? 萅9b (J'z_ڀ'j-pAp&`kÒA 0 Ё`a Ā`dR 񌇼 pHM'T# l!B6(b1y8v 7LH۾Fx x<:dA@x$!]RF8 d4#HCG  J0>62  A[^QҔ[^۞0Ib.VAspU*a "8i5I,h>T F*PQ @ N/!NDNsSPA$t #c9<1 ws n ]hCQ;:-@%d(V`h4@*PI$ !@,{@JPȓL&*Z_*zӠ4!r vut sDX?!!k@*5XA pȢa}YB bȂM x!7X(A=*PYt)M{Vc03 5MHN0 `XC!pCPux8er@ F,j@PD):Ău.t9T&z 0$dսnvk]j ƒyo "0lH BxUp!THL`(. &S `"sJ X'y d8VK@+t)+؂NSX(uzh>՗A888rg~ J}glPwux( 1%x){ƘȌ5= ȉ ܣ,PPbQP1@5hDO@ #'% _# Pb-P!l1`&?MQ5y{D4i7oȇM 0;ٓVǕ,Pm, jXXM3]ԋx8qY#pxT9ٗ w= țy֘#&,Y{ TYioX700bio3@PV*ɒ1yj I깒GG}`Q#kCJj *`}j60M1%B`J*PcR)*,:!`D*)*J ` :KN:BzXZ\ڥA  #_px^ `*dpr:tZu:oPzv#x|ڧ:Zzڨ:Zzک:ZzڪzvA0Ng&AOCaj 1Ú᤬HD1W!,"laoqlzq º몬*Ylnq1f16~ 骰*q * { *Ѯ$~.R!e!1A Avղ S QK r>@ 6Q {_[*ڱr'd; V+ "ha!/rM"~Ɋ˱ k" k ""Ze*A/[2&e"z'|>b&[%_RLj2l&e2a&"Uv$rXbr$ir`[I+g;J);Ҷa)Xb&:CW'rU*>K[*[#1r+kd l L0$J.0~.J%h Zp(S/z(01B#) 2)K44&.c۶+l2;)2tb9c7U3;:|#4Ec,;;/k)ʾ DL6bC;C839fS\p9ѓ9:`9`,, 7#:H,;K\;jJ6 k5Q\Nl8^#0ǜ,oܰ⮧<q372 3,L_ :磬>jACaAZb?#E ' @5# vlkBC ؼ6S~A4$x?>?̮AC 'K0$CJ}$FdK]AHH0aLPRBLe}tOf@VհAzیξLbZ355ud[5WװaW`TSJT׳TUQ:QۑKҹ=' zUX^XtXYknK{Y< S%Y?%ܣ ;VL};JVzPTWy[ͅ'>ر.A^`^_bb]|^͕`'M^jYa^˪3_-vD^B˽ܲ~֫x`6aeFd;fQfbS(+ cu|>cJ{;P=jfb6Zfftf%v.0\6Qv65=fn\vzvoj* VkIu00Xƶ ,̂ ㆍw mAk @Njjǖll`!0l`ܱ."mVԓήphu s⇍h&w7i˺uk: ppKO 1g1s428n[Gs=sA7tۯúTCq jarN>xvqJgР >QD-^ĘQF=~RH=Р3xPb C/S4XET6x2PɊBSCG(P@˗I $R^D.2"*-rȋء \( Jd+33җE(I6)&@@Ru@9HYw8CLft :De"Re dI|*\qDb7z(FΈ$4#]e-3 . x Âob/E-Xw9i@AҁĔ 9@ÖưO!)X x'6F)ARz'd `%/ '|t#HD1Bѐ\VNԭP,)D`"qDA ×k1+a B@98mA d! @Hk$Aڹɀ (Y@:aahU: $(X:XAisW!!مRw*qYQĵ(lThH KxL4d_ɀhAhk` r7^K #[V_1GhV$E^liW CB%c6 `Q@ ~`-c  a )@ Z@w N Q#UN4i|'d=%q,0608 PHJeэv 6,2~0fd͇ Z @߬ BHЇN@p0%Suw$d +  "3A@dȣ#=J_p`J*LWvbbț=r@VK$ H+ђU%rd A1 4i-0@ .@]@.!3?G< !@"jE*bH6Xci4 r{1Js 7@8gX*$P~@@LrG9AԚQZ*J3Kewnl":"/$(#X``7T!Nb iP@0 #@*Y< Gv)ڣ=A>W ȋR}Izdjb=!:A p*Eml0[HL?j, q+?: "@hpCᣈ>I0 1@3j.Xh)p!A@AG (Ȁ PW688p  zA{Mځ09BX0$ 6P \ڎz>ɾ9: N{LӖ'HZ Љp PD X)AP.h=cnBX+2@E3ST"Cy. W,N Y*V\m) 52$;-pP$'ܸ ~)Бp8880GpLj|`Ő+O㌉aۊnዣ'/ K;Fc Uc*O&z0xb!bV`,:ƈe`T~-U~r31%`bcF`P0 2G}!M$OK5XV3`ށX3Yl%`3p2([ @#]So+4v& HllkzV_CPzm#_C<b;f5W7IscDžI9r.F5|^"( 8pXP-26~ n6n (4*H$` @֊3i&ۉgr~5]5VNH-2znj۸(茆g6s38&7+`CkEÖA6 x`̔0>7U|7~/72͡c^2*;@@-+`9cC87hۀ zEH!8 x H9#€3;;f޿CnsB1[ƀxo':;<̉k,#K@l nQB nV.Iq} xH̻m ֹdɎq8`q^l/BA=S[~ 20t2h Iځ`:X\$>"`j2=Hۿ\EW?PV[FH3O5&L0YWbPpf/9p߂Rxس@IJv#vc _qI]u4c? ig$vO?(9߫ wlnP>zw $_rD%Lp(LƄ8 _ $\*(Px L^=('Mtpl.l<^^A6̄xŠ!XL/ĜQaD\DF\|Hoz7yΧ ?y8Y2p8D\ĕ"Y"`TzOm_с@,0*]G{[Ô|"vBޣS@)BC?U&}(!B. [*졭ZvHב'=6ǁ8HFFɆƅ@GƂEVQ@nƑdy瀫gOz$V@4A "`C@$ ihQ $\aD,g,VRP 9rDJi@8 )܌`&.{ZfΝ8LI)=ciRACP{*dbBc)tkC=Zx%p!Sк6.LST:P.^7@k2̚7sb% ڲMDtf7SsmEH"@ 󔰨2cpC#82 3p6fFr4`pC@x\ƊU11ճ`'!@ " (F !K VFttIBu!T}\dL2GXyy =ц~vyDلFiݒ`liqFxD:6J0UP2:Y@4d^YQ g6eIuS%wd8EC;&)X:f{v𩢢E_ri|U AYE($֧h~dg*,: mEٴmp0P xE4' 0,eAm0"A Њ7,PT o ( 3Ѓ#cwR ,S"i@ 7Н1 xa)A2 2B!{. M-/XXp w$G=@^@z4}-=@ Fw@ }ӡ ͷږ%.W [f_WQTO<wCsWκEZuplI/Lu.8f G`lzZ=k=@C5}O6=aC7)$ިH5#o pB!~, ?pUr>y * &0Ё< e<I3#g%3@ؐ=) wcB뤆~kB~;"%2N`1ŞP@0($:_8!tC.>1}"X5-Ubx7bQr#=Qjbo27OEn匁D) d% Ir$(C)Q<%*SU|%,c)YҲ%.s]򲗾%0)a<&2e2Μ#_²mx`" zod 'Cِq`ꔦ #'THEjڡ >4w yD)(NP2 ehCH6d&h& @ͨk!#R’]aM4o>ӓ)M$ T6 !nBZ h.T`P%@V P#ޤ$ N ) _Z˄ aGE`YX,jR3I Ɔa  fI BMJk-BR8-f4~Ow;[=ҶnO""oHkZn"'&eHB0<Y#-BXSMݎȭ\ˉ58@?yLj* тD+3(`S|4YadcbAx#Wo, sHV?\i*[F"{7qow&m# W>(s@!5,聶d 3%EL0O0ty!ޗWo:U \X2d <0ϕfnHX7:P! & +[r&~Y$zэpe(hAC,fK3("@, ik @F 0@!x6)uXnUD7mV`lpۚ7L۽{o.(ۭ8# ÃP'!p  "S RЁ4$X$ K;9N(` {E+% mHM"+1ZN \-of:z8q3<xv-$?< >e5?twC%?l?^>qUE\ @@\ x@"!PDd@ЁυZQ5DhH@ T@4 y\IȐ ɕ   J^ ppD DeWAH `EP  5El`C`*AhPO숩@C4  X KpLĀͅA\M"0@ E 4V\~ L5\v؁bө*ȱpǼ⿑b.*_t96ј.Xu"g`(⿅* A!\`$  F$8B4@|@eE 9 \(Fl) Ȅ$`-[ >#"=J G8_YHS$Q0UEc$Q@x\$#:KBDMM3ބF,e 4pH"'&)b-#2[XVX_%P #].bfD#-_ fR^Zʼ_ƛ-NUhf΁D<@##lD}@,\NERKh!^LtL1@8WC$@#,'s. hHl$Lt$"D&Z`^Oh'w.2E&N\ Q@ pwZx&f؀G&U zD#(@ @dz-*}%e*߻Z&de/^EO_a戦%YF&\&( Aa(hYxhhf<`!d.P,h@L`CKKR@\jW_x`. gBę@AF&|] U @$@i"')nA<`@Ϡc!Q,S$bTEP4tyHXDJħH"hcL _Rehb Dp #dj @ vLfW0i*cYzho%ś/)(*2 p%Z3o=V) \(T8cu@Z0 @L[GHDTB̸t@g4)M$LH4@el=)4ZUTHoJX˞Dk<^ WSE$ʢ`JƣT E|} JH,댆#pVH4,ree@ߋYh![0R_*%<%(Vb> 1+ +m-ǎv TۓJM"P0 EGB k0 Zl\ _$ZYDjBRA&O.|p/..S/T(AL>ax lL$t8G8V q@$x G$pX l%D^"Zn _ǖ&Tl@pۢ__]Y )KEevO - #dPO 0zƍI#fLF'zGY1gP _#/_f+!*/k 5 VZOEW2g\eAP'fppa1&24 1*% ),ϲ,Sr+2r2//1 3132'2/3373?34G4O35W5_36g6o37w73883993::=1k@:Gbs6Y=Y8E>MHTKIAC-A{Z?XFݔXHaSAo:wG[sUm@Zv  oC6B6DCZT4GLΊTַnFkDa d!?cŶ^O7rqbfg1SgNS4Ww54|WI5⣣ IP4WCu}lud]Dkq#uR{J @_^]yXLX_˭Wo6gRek@@El[D=o "A ^Kt@AM\5j>E1([]ShV@x ,^̯\$·o@ŀX}EĝzF))iL@JD"KBĢ"[*EnĦ"/Ƥ~xLPFOjO@mۭ@>">?LSS4{O<~,VHe`HAK@@`X@I*8 T4Z |AxβYۨ"t9<Iuo7!Zjv 0mFsdb0AecJ^*hHb;C.^:`(Ct\`81EI r(`@p<Y:H"4544Zj$lS(AR+P "*IC04 ^2*^Q EX@@ t F0`^yp'EDe`1B I ,Q 3JXD<P j$a@ !h@@1@q"3G>q}{gG?mfc >-Z0 r(DK)4h~{0cHN CL`5gTGR ?m=pX!6m mN@pDG "tf   ` @/eַ843ix@0 UuQN(VX31n&iO'й06QPJ0LDf搆&O-jVv1)X$+TR1\lKD53v % rƠ$BiA3YKטp%5dRֳ.S ^Bۗ6V(C*"XA`jr6=DA KA@,Mb+mF+Y!5 @İuIYCPڶ >q( w<< LxLXC;)&44R j40QbR+ 7Rq8$ 3[I&/<Dd#hELb dm~yc L+$/Ru:@8lȏ $́TUjJЈcbiH~  :s H`t2MkWdՌ` E!FHT07(1z"2Kb.[ _k؜@{n,aݡe@5W ȴ}mȗ} a* r/e,f6Lw $%6o@&^¢^ P1}+<&=?udDo~Ͽ9C|sGμ r5LboA m܁D`A %!K*J:" PYQDAzN6H s9@& @D|Vzx ElZ]!XHbf  %,p@*Dp}$zC&`*~F( $ 8 -74 *8 LpD)J%N/Vϖ/ZO~/O l(`)P"⠅D8pM`@ !//X*P^b`b;PE0m NJ -Pj r` b" #  6 Ҁ\>Na"܌ؼ B(Bg~ P L; @ RRAc Pn*(0&ҏO ! _B 0V% q=;5c/ `n@ F\ p s`^ pa_O"M O0$S%uM   `"}B !(M`*@`j@L(&@ (& $@q4H@pʣ* `b&mʦbB @ @Kkӣ* @$n L d: `AHTIR *&R r08$"-274H&2 +GuGkӆ62$8@ BT:@( >RN&.#r N6cm.M2Q @ʟ SɠPg/s'" @@Lr`P hb.X|" AXU $@}":֠ WB'BSh ` kb  WUYu3,& ZMU YYVquu^g0cbU[UٵZ\ p ^`]`!:We`B$U5v`dSL>@..^ɕ^}Q3Rc=d;3  QJ5I_)<5&0&AnWSAuK:`TvnR`K b`&` H@-;[wO+$P Jjb45Z"eUV V 3_lvm6gONPekR$`q4. dTov0wwb_c}>6d55S Qcv뉋^@#@7Vu__+eIXUuCp @`P:^,,#o(*Wi'JR8_1'BeB B4G#7Q"l@B@r@W'B"xQ[o 45@|CFPB%7& h X`8_ fN zNq]wo9؃_b;XT&J9%؊  +>Z` ,Y@bet_x `v` MK&MayGv<x85#:  @` ;1 bi\j2a.8j36na`N d2&8?2x  g8AVswß&Wj %&t٤TKGy83\ey.6&<+b9m~%9` ߆9@8&` @ w_#zh wq~_beqK `G` Z J֫343v5'RA |€<02$DQsV@ "{<%$ `¬wNK3(JxX<5CH6,؎8 ZZ2=RS;&V'zy7:&<5 `T`Όb)j;(E_"m=?c&(_`ʱ */{F}.ZK3nZWQ;5;4{qo M!* Hb`&v\ z·p_E8 s@^ :O#tz%TN*Ԝ/3+b#~tu&j&B&BS!`&:`=*+"%*O?fD $aΠ^=g} T~"(4c%bႪF=}E ٯ&":)A :Pܿx7fP] B)^ۊ;͝C~92:M0"1^K@pՠB |B` E BN Y9&>2;#6#a㧂 @=O"}z"F!B~ ` sF ֠}Mk"!F` h`:n\ x]3zf a"s~u>֞bF+ m~2 `\ f & /?7KX mBseڻQ:&"+/C06 "@vCtXc7\<< : r =C#`@p/Zr@` %='= %' *=IEeXi.`C<]DC&+"bdD`F H cAO(`p d4F TAbh' <&J$?9z)`FFTfԂ >ǐNgPA`A`jEũ!@ԩgxHA7 h.Z, sO[\!)@-%Faֳjצ Bj bFVR-頻@QR];]h➡~w,pPG.آh)2uq1`c5FnPrl 4 @SalQ jFtS (dWvVYzFA#hm HTIT*BLTL!]@$8 FDt$t~ hy蠊:J S,RRQz*aCO4Ił PT:JzT8drOR]*92A;F;T :T: ݵf+h*+P]; ZSp0RqB('SݛFSpSHN&./hBS ˗l6,p8s8J 3]Mi0DHC-5tGWu>Su@ spb(4^pMzw t|Mbx L݇a?TGNy/<=P͌;~y+nzW D뮷*{{|O||/|?}OO}_}o}~O~柏~>S`HiN}0B׿$ :e&N&zP#`%@ г~P*`.Iz06BX3X7 VRF|?VZY&:a|81⟨D&:q[D cH2M_6t$t8X34S@;SaLDP-F\>wtal1d `XP"-LS@&r#ԈL 7@+_R@TE@UR(`t*gbUTeUgA6&WZn"sH)ӏ;8K"͢}ъ%P| u]c5΃^=i;Jكr֝MC A5femf["vG4AڋVdhD-Ja(a"#F2 a¸.TCXĨ4|шUC&D! Kix@AiA tOo59)g VIX2@p<ưh8`/L֊[BJ`XE x (a2Ƀ4xƝJ2!PpXLQH_ ,\s Ńi?{Y*խ&jGVojs;=_IsW QBn[r5 zgZҨmځֹ4N(l_U 9jXF2q R.5G|%8a|bH6lh(#za F@S@ ` iSyȃM`wf,*( h,0F6(b ژ98 DxjV$(i["oD HAҰNs  q 0}P+&Rz?1v IUp P#S~>qd@FeJҋXw>p`RaUogMV*LRpPX PIRx`vqc|BRdBhv`02)nP.G<y)tJ,wrh^w{zwxyh[zynxxwo4h#iCwVhĈwyh[xFw(ɐPFjav/Y[v3Ph~Zx\`ְi7X `l { Gy0?R0IP_RT&7 VYXP I˴!PZ TD2GSAX0A!PؑnT_YY6 ! HgCp#P1❏5P p"yUGoh-yobyRIwg_ jjƆ xg}7h8^i؅}`4iGK)Ւchj h&Y)R"5y ` P| Ǘ~y}Y 5Cco_891`HHpQ:)_`gRU<_R1 CEB! Y{k7c(Pc?US2a qExk r94%03|:ZE*,F4gxUz|x:zrX;W)vZDi$3y[P:x̪iŮp\ @ 1 0l`} }% @  lEZ+Z o*e ( =8P **TD,<JPTNe((l*1 taB y# %Jr$fEE0g o?p(A^I R2[oҵ_ U?SE+(^Gk3byP K(W2"xVyr8XLjowx$iFڪE+zZa+Z*iۺvҺ[yʭɛYz ǭ/ O }p p P @  ٰ : p } A ,}Dp `̀Qzv$P*a0*+,pJaJJׁڱZBmpVQW%>1#ae/q=CSq9Aa5R'u<rqAAŶaŒ1L묧Fyk+e r'pFsZVǬ ;mx;% sw[y|ȗ;WklOf$x)Y6.z`Yj` p 0p |t)1$'50C.~-@-SXA)+R*,()ҢKFdt!/C ΌX#75ĊreY67 M[ =вEѽEO#4 *07c353 |3]=206=axy@  7A AS"_EdRQB C-bB6Ě5f-P֥mYH$(HItB@+hS)I`aHJhLSzkcNR4VZcߙ+ʨ̈QuODP nȃ)+o?sl_a@@s)*Hר6P0~{o٘?wuJ CRD F8bE&nG!E$YI)UdK1eΤYM9e`m=:jkGv¨0\+#BflQq%@h1@6D|R`F@ AƖ#F <pXD,;i!jt:xODN`H\` ,yC ꘠dg#=цr0̵sC8*=po'5 dq5Wz]}pD&B] G6\ 4*,(`?j8E1k. bCLD0`H$h1  :c>â"VHJL&*d 1-2Όkrbh63S".N0"^ d" \ԅkL*3@TSUuUV[uUXcu mhAhV \](a ވ.SJf"<O0 _>^*ic.f4)/`H  @;EHs$h`>Rh0J#& 1H7zx'=?,Á ""n̂P(^u`f D@ `+h1GXGhq GJDS 5%,p#(P18bMXȑ< ᨼpH$UG)Vq"O@L'Nq%JlJDQsc*8v$gqBĒAd(B7#tI"HHFR$H7R@"d'9:>'D#5YITRde+]yG RTIWRe/}K`S$f1yLd&Sdf3LhFSӤf5yMlfSf7I ⋜0 ȐA݋GT0d!A8@ӝgW2p`$`'z0'R8 @eQ;i=ɱCvniJ !?O Ѓԥ0#`B0wv @8&AC=Rp~d@]S `)?J47)})HRJr$\HJjPD/H@b%+GsVJ|e^" 0ĭL]Pxʳ#D`P>W+v;I /܀I&~Z&rk'bT  @@ "x ¨&l!*t ;^ `"FIqSd}'Gy{P8@}",[x ~І;,@"ȇC2rގ%.HK#z#&1s<Xb͑Eha88DF2g##&&H͒6w"&8 @4ӟvq%2"gRM%HYtn@%$ "8B(/p0  H@PhY ,`1$yjfChX5<@Ͻ"?$" 8G(IQ9@I)lD>}! #`F$dp) 7R3D—[Α1z! @"8GnJ,ɳ Z~չwH6:N5P CD@ҙ^TpPP Y{筢 `;͎+AHP1D Lx7=UBو#q$ ]1! ((w'B1l3:~;"G !AVpH,A;H4W=pG؂2oc5(,`:(&(,HB@4û@$@0@?Ix d.`0TȀ(h(.8]\lD؈ ‰x@B A) $@Xœ)@G8 ,C#I9/S /EB/<و94CXÎ 0`DZxDB*(ĉP;#GA08S[AP@É2DЃ^<!Dqs:XJ$(FXb 4 ) 5[V|EX>z>.@X'>E_|7AElDEZ܈CFK\q *3Ga$J$E@X(RUH`JFW<+HH>@ OԞ"$t7CR>pȐ V(AHdU eᛉ^YHXlІDk!#? osD hr( "`̄DhRL=x PP (6 XI0,:x`# 8@ `H"x|d|0L1$0$ƈ#𴌒A6h1H#8͓6ص* 0LTLM,M4M O4ϋsLȔLDNN@@8ЯdD)9Hl͉OɍxM 'GQX41ؒ(]\1,@B4؜ =#&؀2HTp\]6x3L9/ Qт< @6 hB&@@ Rʬt 8<8'R)˻S "=ĉ=M1@,m0`0Dp.?(v RVm,XEMdM" %t7dK1UTUՎ6pxO Q%Tx YL镎86PhB5h+Xi K8;"/8Jqx05/*8k? Xw܌S&(/Qh%b@'>&HT!EM;%!I" '!HHȂrYH 3=ڈ0m툯创ฏdZ%ۉ4PU+# %%UP숴]:Eρ=1>70+ -\P95; =\HXX 1pU`CҵZ'?HEXp] (^۠pJx>3pS8۝uv%H@`0qa7yWuN+yr\]-݅T DWh_kS@^5#=0 (6ЀHH#pصo`NщZZ_|W~/ ` v | HSj%֛a؈U(q s "h!P(pTy# X1Wn@/HG>S>@@7; @&37S0>!1JL@0`$&iGIS5(+Cd, 59ߡ)CeUfem?r9K؈+D&HHUnedLHkdhfc;2(Qdfebf%HgGd"26 4dh.V^. .8pr&}f=@wgsǏHh( />` P- mf%L&U&V?P,h50K+1( \^ۭ,fd\.崢b0c[<=N7ezB|i@LVjk$kj6{1T#΁ xj앛{.hd6evc3Fc0=647v>>gTh+&(bZDk:hײoPE eiwM2cTŰg ?؍ʠ  o߀Rp Hp ̄n7ƲW2e3pP7N8zZ0pWp爎`qq#p6 Ԙw1 " X,$ T ?7؁- ,?; OpWFe\gXq+rJ,r.vS p(XԖHhEk !&"6jȀ  (/rhi䞈 {. p8s@`> KtW'XkStth5ipOX95)Pˆmo ј-r,Vjl*ɺviI)ވхV)䓈n8>pn~F/뎈C W pn6$9DoN:o&A9,(/7$;֌HM 9`0@d D0735HzP>dw 21EHz%&Me-1o{ OqG!IE'E^e$H?ϔMO^K7p`ܓ^߈8ى@Pl7ß?xx xXO}Z |߈3ECvW×ۿv 8K$$-X` K:#C1BC  y ,t8 *ǐ70p%*r(ѢF@ a+mӁ<(+UE;!@H p1aJ1`ȁޤ wo(Cŋ&XGwh BAnKG@m }ͨ7 paFNRB_8U% Y2gؕ;븖@4%n+v [b 9㴤!=QP@5TfB  T@jT@  !4[t d jנw "Z0FYrsqF<emi'vq]PJI3^ cYb9р)1!I @!881@AE@"4Vhhyfk 9hf4!P qz]KD x\дL)$ JP k \ +suW*E:}`6fAOF9eQCT* @hy,]vU+;mڣ:@QPw.D28PWcYBXHLYJ*/zHXBkB 3u BL6^t5}D/Dl3FQ!J'g@'\0!uIU#, u*ɺ4ť#8Ge|A^qDj itv(A!leAmw UPAz'H_?ӽOP2'|#x000E1@| 4QD۱:p7"#Q`. %@ ֏o@> EAH@nqCe]/Q| TUJvTr H 5">=b@fE$P]HSbAD WDe ZPyPTI@iQ\$Hն=jzh|$E)$m @GaQM)!#mN^KytȈ 3"= JPJ'[@"%JIkbp`a\M8{ѭѽyssiq[\ G8'NZdJr=.r4lu;款8 t|'}rk?]ڎnԧn/ކ޺Go<`w v;OS.f׶SІ3&m>vV7;ⓝYNz/933T/zO{ڋOwOMNO L>!Wٿ]U~rLW'vs~0}ֱzKג>kκ=қwb7h竟^ޙOz~m_Qaڡ^\[ɕ]VI[\~՟е^%`ޙ `=  [ . ߻5 2 ~`1ՙa1`6aɠιU ٭ ]_a%_`ΞaUn߭ .Z Z^1&i a2a!ӡ!׹zM"[_j[M]F[ a aia _9^[%> v_"&z!#>a]F^,rfa *^*f5u4nYa:a9!#nc 0.!31:%B `9^2dUu"::2"@b :`0 !%ו?6e`н"c9aAQ"-_& ܱɣjܸ `U$AJZ:P6$1F8 DBN(ޡP`V d2$)"XGame,iQ7\B2bI[dPW >ce2$D_-dh&(f\Li1!keR&A$/fͱgcf%7_.JddR$1cc&Y=-e7c!`N ǩ^`Ρk6e4^`4Fqzqcvd+BZ-jbzrqI*6V3y1ֹ"I [gk"~E&(*b~v&$ERg@jcjg~ܱA݋ʣUag"bphv(0cI^0(FJ!9šDfaYQig`= %闒)Jf!M"eN$N)Zeh:j8Icq)(vR!i&آiV5*PVSI>ӥJ**阨! qjjjk k"k*2k:BkJRkZbkjrkz@ηknXy@ |\@ k+,+#pB@A Ԁz D@W ldHU lʪ, A(0ƚ BXI䈎8@*+|M( @ ,$l"lń APjr- @,t0  D@ʻf(A&vnj\!,A@)@$8BDĆԁJ+ (@[n&j hj0#D'" + ynJ˜#8 $ T PZ/(JdRoXLr<j4x!E_s939?@s6;H@/@@ 3X-hvXs]D/iS&JpGke(QKrFSP$xT.PAh @@@$@F B$D,KV  8'J4o RZwx],`(_GX`9|=xq8,^-A :`C'82. Tt8Qd0|0B:,`$,<11|B0HC1\#;vFd: D;0'#7wLJȓɣ#1X.fʳgȗ3CQ ǃ@X=ҫ*\>C6pK~ޛ[>ݳ>A<=ͼ@u@ 0;QAP;AF x@,3;B,IT`l{Դo;Qh A w5M|S'?jDs @„+p!r 0m[Lj d),tiӪ~SrbQzDʃJ,RCRxiO,xR.RN24cM\TQՎ|@BEփhZPGm)?U ڨ`HUzB>`k hCZ:"ư#Pn.L3: *Q~Xc!@&#P] !_o@-(!,Ǝ? b ABpY6rk;S YBf Y@Q4(AFKkHqDjQI2F6q'@MN:[J 1X ,'+&YjlD Y< yD '2#̸KRxWx}\1sI!=ȱ#‹lX3^Q% Y=a*0Y}$Y XCt B }Yab`$ќ t!EXl >aMd <' >ϛ8>VP8@.8i@dA\y"D K$1G8 O<0q-ha7̃9Qq#P!Qظ-Сђd oB$|@_$z1T %>q L"N{3KxELb7:!Ƹ/,`Ǩ.n*Q"d"(tI=~=}hSWbLGŊmZ#&D&k7 yN65 qev:߹sKd_ -< H6tmM9Z r8 @ V<* 0vdIE2D<& 5G>H mDwHA2Tyr[ hK$ sxBp_{. ϱE!PCv-N11JAJ0sf _tp)|{sGrDA82)HC%%qRF(\(D‡p5I·aAرcWIQsSa]BwwqE,z@&WӾx'ǸA7U%](m8 jH_z[GDd 9z׿i_{ǽ l{_|W|?ї_}_}_~ A~D\$߆ofC )[#PFȠ#w PJ$DBDPZO,og7H9HcpFd%PׯOmp}$Y% 0P y0q-ЅTf0Y / op0 C̰ 70pK3hj kpא  1OC0SF~0 ?G1C ?5IEm(MZ9  - pOiQɰ+p;/P/rq'Qeq3 50 ]PױCQ1 )qp1`Q 1Q i }&RQ #q#Q%1ˑ%o0 1K2$+W$cEGB&#=&qRW(2([ 0(1*U1*eR 3uQ#=$q$ *%D,2'g k-Sӑ u !R+-+2!/& #q*R 2&11a)rQ3(1qG3O2ő16R 11(=",32 s)!q4s2;Q(Ys.9!$2S-,/g!0:(I'+,#23k>?s<<4=73D }:! 4"3+79/syr1)Es33*)3 ˲0@=2g2Bq30F"E!-cS'.g3 H1I34[SG33!q8Ut'(;(E4@/$QJ&-(4ETD:6H GWMMA<-r>{FT-54i'_@e47״.4a32%/4R/uAQǒ!MS%4QT:9SW2:ߑS8w1%1P>yEt@'3UUD-U)u < ۔=mӾ,QXhX/8u3,WuZtgF]W]URY^Z˵/Z__`` `aVaaab#Vb'b+b/c]P+cGdK_C"D[ oB@ LfogV#$ >@. rViiR< Pm܀d@2$!vV$Χ@}P [mmg p!$`&>Bl;x  ^X@:vWzWgJ6 s@ xIFZN^j`z~C a L(w{M `p_e:b䗁؁E  Mv @~c{% z@sg@ *#&HX[؅ * dnb@M" @ J`}e酛؉k d#@X؋Xǘ،Xט؍X瘎؎BaDdt?h!H;d\z )J2J>1I젡r<$\hIJD-z]B`v L D `ٗLp`ѷ#zٙCș $!]#2@\ TAR+CY+ءKLAvDT:BH-<9D"=Ħf,)jCpJCA$DAd~eNft#`CF$hZDbl(l-,7*B&F @!h F~$n &.X($($Z@. F|F`/:A%* #@ڮ: `m{à!U *;*Q-]BB@CJ!\)E~J*l!!A8!b;d M.w,`DA@DVt!ZҦJ[;C^;bj>$\ȉ<tajs6A0Aʳkq}_BʮB$  Fż#ZAjΉHǡ6a} ͻ|N!ң<ȇ ˋE$t56l ݥ8=ͽ<5!Cʠ  @fѪH%( tka x?`8 qM<)-=6ܘ` |:_\I-aE!̡Hy""i<AơA!&8ؠ "Cz,z6al>af!#E n*)a8Ȋ& ^>gލ fHNdARJ>*l'>.Aڈ>̠":a$~m՞+IVvAK!^i&KZba!l~.Q^'烾C^BS!B tjp_ z! |k  Ϳ zC/F٪ߝ>#f@: c@9[;_9 %&   @d P0m HHH$f0ŢG,ؚiŜ$*~͚Ǜ8%疜Xf%L15xB.!GRl0].xKmDh' l @)X{߷ٕ=䂊U(0#K2R @7 ^87؍/ҔJxzNbJ$ .ѩ}vU>0H3l稫^,GnK=!09EE=mH??1@R0 pDJ@0k"{M%/M^AA"!(C(^ jV֦«ef(L W 8<ޑD䑢h9&q(mAOH$2.($H#J2 k@W(Dl'r$ PDߌצ(dH'(ePI,qi\A&DNBz$o|@!8V#xd?`߂I@"`EVx PM<lDa" @4 >dHJI\"8x2щ.̧>~0>pDv c2xL";APIbpHt"V  jك, b`C`[^>?Kc:5ґ4'͕6h!Y%C-Wl|"kC5bM DaD'6:ִY*\PMì>QآAZٺ,rr"CLqckEHq(6"E&qNdLzy<5'$yMYz]({`&e?ya@.1bv}4kp8D| 42ysXQÔ*B Op ,D`i^\`N+ -.)8 є(F "B -ER %f %rmF~Qp?8^"ڠh.9aη1sc,hEA^p] ۷DJq<<+Zb"#NAfhArC>򕋜8!U.(ENAӮ(~`0z@<8@$"P6خEK`%ŽuphO 1 8m( GvCUnE9BO_Zj&yTW-&tk"C ϼWֻ ۾*wOO;ЏO[Ͼ{OjGn!A} :;+bA G:p3H` 7qȀ'g"'7)$.b*A#G  7h+c8/X!!8?؀ ":XGt8Mx9TɊu-XY#I(p hICD 9i萷H`PُgMy p = iJHYz)!DHHxYؖYi))#ىi(iL؉Yj Yܸɕ/ 9f)ɚ虔v99y D隳ٕY򹔙9)^ֹ鉔Xx%Ɉ}#J'Jyg&J2 yЌ5ȟ6 yp/*98y(ڊpKJ=A:9ٚ:&މꨌ.ٙɢE i_~n09,٣9(0-B`Bf80[pL`1` q'p P`cb;[iQuön;!#p^5J 5]R`k1ou[;> x0u{*Tz 0+P/#`;Rc`%@+k7,1l`Ի۾7A%; F]hp=ܫ b$P  71 ݻm 64t`~P,|^0Ka4\6|8:<>@BB mA=Z0RFd ൯tM32tv42r9A!82i r A  }v2m!9 99 ՆP`'>0 S7 >0K[9P# # Q@;N k3MM`j9aD-^r "x 38c @6LP Հ "![gc 0u X$P 7a X "N؄ - g !>=+@.T rpR0 PZ @ qU~YNVZq  a)c^ jc^&p. r^5~ SXV xN}>uR%`@QZX+V @Qm7Q\PX%6  Fݹ@ $N7kP4@@ ^! y (B4a =4_1-0 6[ ` A }4 `' p j=!;:0 x ' 0 dYG dS }%i ɀ 0mB/bVV@ 0 P DVfWФ P; ͠ ` bOK&p/05Ia<9p z?~:,ɭj] O`5pgѻ[a`K5p[Ԧ zw@3 XPn)PC+2+8#yx MJMeqeDc{x k >!-na=:׬e`Lj,'DRZ5 RG*D.>q՛/}1H4\0WW !9eZ-R:U188(>pJ!8T0E*,N_<83@2l4P" 9n+:XB Dpq+#%:EÏ_^zݿ_|? @h?(?Xf gйe&fZ$` 8H3!L9*$ $FO;,OpC( !+h=G,pJ!G~Q,\JBrFe,(ćf]!RjD9(h$+ȏ=RxiO,HMlM@ kfԓpzЃl'OLтBEуhZ`[ hCZ:"ư#lPn߂+4AH!H9 q˝:8c52xC(:0<ɻ/b'b/x='/p<(|BI(3@ 2 /Fl˒PtU|q #=F1iGqHkⓙ@ \ƠRSUjZzm٤n2yzHj%6<^hER\qo"'T,RHZ B\q]Y! {lʆ;MD^:_QikVva PmYuc6#Jwh )f@p$΅YO/XP?2zA8(2By ;BuƳ5Ё`%x<6 wfPPv Ce0S8^'$ Sm\OTJI=| EvG-SxLPm]@ C41nPbdP#<Kѫizl|bmC чRpJ<͜2YD 6([Bj `LUty,'q[4WͪzT 1iMS%fpECbsuHaZ7{=2rL`A p (XV XA Py& ,@' R!!p%zT Hq@*K",Ts%@vYT )l mC^BTEoz@|DZ@(^2c8u( lhH1%~aThm$4pO\O0=쐡DLzFq>|Bi"/> HNT',5BtLaŔX!E^c\IUfDd''IM~Crш O}$׀)pKdCʙӼɘ6܀0lBg;sƲˊ&+J.8~`EpcBZT sc{Qp?O׼݃5xC n0z`ClFw c0C |q WT!_w x}aXAfʏp8G=sa,>Z<' r59+t :?/͟ !Er![OzˎudPWzց1 qjxDžudWon,~h@&w{wGǠA>"{%(8 $?yɓ>d yw}E?7 Hzַ}e?{}u{~?|G~|7@#Ļ K a_!7=_G]>}뗟g׿x_$>S s?s@jQ@@?K ӿC \@@L $(\  D?dAA @\ #@;A?A!@AtBXB@ $l%B*Bs;C BTC!Ӡ,:C7FGlHdCGlʛlFDsŝ4ɴ)HLH|ʪʨu<|HJȸ EaJc}$IZ4JLɃʋetɞtLK49,LktLvAK\̂T|G6Iy$HL! IDǬĨɣ\?Dδ˗HTCI$̘LEa NNMH\FKpD4O,^Ft|H;д;pJ0#ٌ 2P%۲5{!H3@%0P XI0Z6p083H;U;%띸8 r 08 0UP> ;x700dvpByom  ( 縀 "8eY,(H4xyzUn!,X͉ו,(&H7ӽl| i' H@Hp5p,l)!ۓځ%hgxִc9pf& 懞W1JCl v|:vpol@gȂ4 h(.(,pՒ5rg@\gP;#8ɉ~-Xkۤ]ZM[V2`) W`j uX5H4H88j>?843k ¶ȃ~s@ҕq}sz@c 7@uЇqhzhQD 8@ ܜ@0ޛn^8dX΄HA^e9CvҸH۵m۷> m0Ҵf[rT WaSH$H% 5x~~DPP xeinvr`l~rpGtHHXpv [X608H їs\=ghP=!z\*0a?h&8*(dp-Vޏ(9pllGAXQ( gijriaXnw Wcs|a9u[YB+8[t!? "PH'wH0Pȵ5,_w͍Ӑоl3 R 𩈞Qtw4@?Ȁ{Ppp6o7|(^u}*Ѐw!/%@HuI=ЀZLW^- o.(H!pu%Hp%XkZR&c w8D8 dZ%y`i&_וu x,h]Yu'PwW^7&g{mD{|/Τ'{|'~'St40+H􌐄Z.(|.\Ѐk `g8`#28UxM c[?{hzMU//w{#0 = Vs5X{6.% A "; .QBP pc?B: J<0ЀPLT90KhbdD qP!!F@UK`Cn-k,ڴjײm-ܸrҭk.޼z/]w;-0l]xB[Pe8pD+O8Sʵ8L! 5Z.QAϟw7‡/n8rֽKK? pHM˶;Ǔ/o&OdZQeJ%]zY cHYh:fjBio&\tYH~'}9ؙ)#t&FdcL6ڨvQڗj)y RUY:d嚭ꨔF*iꛌ֚i˦{엨Y>FZ檱J,[֒".8諆Bފ-ɦ:壽޻/jk s{J<1fo ~J0*hҚ1iڊ:< W|39[x1=۰?Y.soGk?5]/ӡj(&G*!/\YcLks Sp_-^7ȁ u>8sݶbMk㭷Ӥxか>:񼬨U']vgG{ؐj[Nj>좳@K^:۲ (w'N(/ߦBk[< nzܛ>=^[=_????(< 2| #( R(@f.CyXʄf7t` |9B@ Td&"Gn.^<( $ 8AV^"D !8*q&P T(f   b  ,p@*G,uC,Q`*AȔ6BP12 ceSN lrT!@TV@8)Oם4{]@zҁ/u\?yXOBؕt !A 8O:Tp!zU+e*[*! qHK$J&,b"ޚeK@0A2@YX1.ry{΁`\0A-AX"`f1Hb(q * bEsɒ`vɂ]7 4XV?0b T"R@tP]-?$[hEPdIC  K| Ca(CWP$ [rc d q$/3&hBL"]va>{EbHܕDC sb!H-/z̕YB`,[D`0A2;CǸ,tjȯo;*h!!(C(^0٭ $TJ pd/{͉-6 hLl,.A.Mnso'N0ppos ўvNm_ɲme ?x+o<hT5h@Y\[n-]~ۺ,gR8. H8'`Nl޸QB|} nG8eq6^$7 Ka s%Nx i:Y_%},$A3xȳ0Q0XdS_@28` ف&e`b/ $l:Y.0pB Sw6G~A  # >vav=A*q9@$GvU:ܼb;YC@@,@82b`vqA`_M]ߟ=  (%h _@\͜Q9D͠DJD܀Ā؞R0yX0  @T@ER2aݞCЄMN4 @ ADN$@WXԡ2RT!V!Մf!Y@|A.$^Q`1V#" CX{ Fel-jDZC@& CZL3~e^p<\>$AA$B&B.$C6C>$DFDN$EVEN A G"Oy]EA@FIȤdwd$Jd0AuN IOp%O.%hRФ˹`eheʥHKNȀLYxNːe\Z*%SΥ8xTeV _ UZ,N$4[fVveZFf@8&d"0\%g$tdTPe \BpOhp@@D[(t(iMhvn&ph˓ Nϔꍩ\i2mMLpX`fy2'lY&JMR&fc>&ʨz.(iX6@ipL*jN(:v)j'$0L*]^*aH*rz$ɭ**++&.+6>k AT.dt$ @jEL+"AEā Lb@ D `\ l׷ƇE[뼦 "ƶEȢk[ … `  E,k%'9 >%@ E\^@Ek+(}Ā(Xk`\"TZ,&J-P-!(X+٦\l@ѾLDfA p-@׮rmؖ[!OrInY@ PcE @ Њ|EY`@"|*\[ X8E٪A@ Dђm@ޒ D|fvE//!ЀAELDn8![Yh@ ;@/\H@ + a':BYA@ZV(5Fr/"(Y",X B@2Dt:; r0`  "D~AC"ko8q=1.%A@+9@@21"1ɧ2*p)r*w? UűWC?-A@  A< C@ )?o> <1*CJq8B"$rqT @ߵ}1 H"1 T@A@Alv`8m@H1;Dg x <GW}UXA$(CtLUJqRq tAY A D!T!RMXuGˬLaů.;5TK5Z=uTOuk1E1CV[DE_zB/C B( D[U\itf tx(uD99+qYԀ `YOA(h<3]VšPTP3 Q2((R XUDAHiCX@Ԁ@Om]-w8Ȑz}V{94w{.q6RCAt%ADw.@0wWy@W D$8/ @88}DB [/7qw]#o6pKLAxk,8~ Dr_D@*@X|ӷs2@h6g/xp/y"io$ l L@ C|AdWAnqGp< hAB\7\=/Luuކ @*@ٚCCz/zu*|ynIMNP.n/ @P" s/ R!,#LR%l`d,X bbH; 6 B)`( 7£2۬ℓFc(H$a*p#u" !Ph.Cw`Uɇ!X)1F)3nRAXL|T 䁊/[$-?8'\)=f#Yp ^ A\AŠ㫱Z뭹믱RH֬(dEP$HllSr,ldo.O\ #@ Ȃȇ\s}ΏM'a]iq]y߁^/O^o硏^驯^_/X}8J'*۟_HoOhI DX܏  * |_?􍐄%dπ)+(.AyP[ %8ρ,l` QDrЈGa(&N}6GB(r A(VQ8 /(E&]LJɘG ⱍ8#ZF.|!HJq-T#=E92r,dFҍ@ 'ψB:ҍcLjR1iK@F">!1u(@4e3_2rL&$E\֐|*uJL>iD1UHRVӕ$!}O%6Q'C\~r<%s,I u<9E.ԏ,e-:snt@][R2 R6C&A)Y[S҄ @eMZԣD; h>Ԙ.5&5A[γO(::#$Rny/ /_p,',jё^';&r\mq{;&T!NiʆQWyL~63-gԓ `{Px @*\JH"(_H(NQCr )6;@D4 P")4OUe %&.o O1 dƠ@zAP}\2J< $7db_"r<*.%yzDebI">`2Ma.(adΔ`zn G 2`*AhB (O(΃Qΐ 2ܶ#(0U" 1(4c EPN% B2" `1'pP# '<C!He| FԤ'C  up OPzb`#1e!⌢)/q %qqOB0*#!""SO\'E"P =ݱ'n1wr$  h@2` | VX ʀ <" ҀPB R< ` ~2(#pR'yr `@ r (R< #aJb r,R bF@2`R&i&b/ɒ!)J)=1˲5rr'{2+a0i2|@=8!3s" hD*k` %%DS(R3&dր$&``  Q | <(-/IP`|R/3]bB:` ;M(1J>BzB B@ ` ֲ-g4l \TBE}JFJ93?!87SaBGTM%}bdFW 6@@6 $`& Q: . XN `\ >5fZTY5Z[ p,[[ p@BD<%ؠ>^U_9 LK_5Y!&^<6 \ǵ\\ŕ\EbKv Z]Zc_d-v"@"2fN `XR!ue6wb!f^d"@A$A.@f >  ؖfjJ``TVlM,H`RP!;&2Գ*C>kjn'~ SRArE6`! #X&YiXUYOrrC;vmW @LC@P'`;=B ?~`] l ĠDcct @x1~6Jtr5``Q" $"` '7~lv@<pWE!63tV#j`M{MQMCr#l]Q"Oq!;!|WtIw 6s=BC *`F#rzzV`Bhmvn6g"C N : L1x]oVbYv 71W@ 9y;'ƸbQB'V Dw@e@RQ $­5DP` {\ t63 ÐY4S!Ee!#4`9{B{!1MU@Mag;`0Q `y qY(xm$kOb@Abt !ּP r9L M  !Vi&q30"@*@Z+wO*@jPz @2Kwʀ 4@eVğZo7Wrף!Bw`j@,ݹL-!n}BDCZ#t##2 TV <(} z2T!8 &jyoꖁXC Bb OQ>:|3XZ`# ֲ!@=+ ywٰ_ٲ1[ T]U/6Pc: F@y!@۸{bY'8Zf% @@@NwG F]F@ۻK [;-' c (@W*M:^t ۾Xu,% Iy !moɻ <۽{N/\óz Ľ0 &HMX b=:[~}S$T@.8FV q b B@ɟAx3=+ %ӌ߅"@@ ӧF* ?  R=n˻/>0ZOJN`*n=sPS=ؗcϢn"(t >';^S#B B\86P$ :=d6 45@@J`{ `՞^@`8`+~^~ ?e^T-=B! <ԃ˩>Z2!B  /5?@EZ0 q@cg/L`o=o{@QS z{H<Qv} ,1`',]Q >Pp A2<ХƂl,#M4q˄(` F6he6>PFj,!!]},B=U* 5oz$A#qsD| 5j/zҴ I: Q& 0cB_ltQ_RgЏ0l{3'nW&u#`S#Z,%FB b`#uI 41K%\}PP @.]x?rխQI0GE 64AKJ$P RhA&c#qEX!7HUh 6gx FR!߄Rx#,цu!C*IPA8 w\r]XP@F"R߇^F,"VȃQ#Ursy(WoF1ءQT(@CQgCw,-[LlTF xh4 9ZBQ!QZP!{xF6Af3ujeFWfْUX K,0CUQP@&,裔U Pm4oe@Hj%)/KBD@XjQbڏVc q)Wj[CC^7qAY YlzW[NLL9!")qvmm &(q"&BTQFG@~8nV9 X1уj;xQ%' $C8ByABuФf\Ѯ9'(s+M^cEΒ1@ Xa*B<Fh11jd'.Lh~fXf%@H 9P VNw @:%qkX VP) !#,54'7 'p%P5ȊV1!P|r, eDEäCXÂlZAv`A `!& @= 9m6ҽ] #@':ҭ TIsA$AP:ɰ6N0F!@  9u@.wK )4I5JoBG>/!h >f!dNqE)(. ;oi"v@$2doT}4 P eȕ6زOTA@T4(śG@Gß6Ap!\Af@ T &@D4+P>J'@!Q@ BH1uHTAjQ h#jxkB*FD5*`*Stqi`U*n$ (U)z/Cܰ^"d#*HpW?&Ŀ; p@x?} (N1e(6NHm P=0`*,@x+XP\ ҀDk۴3(x[VbgE@UDPA4QZgh]->[,_vT˨Fk[F$@6 uT iX D$.k9ep?LV(@ `%7Q Vx4FN_{b#n7]@A,_B^.G𶙴m:OSLlYZ Ha_%\ eI:3`{ySb/pk~<PtF0nÍI~#nzԫ8E /+oKԯk/Ϗo_u=08]r;@K`xYq&ЀVz6!G`&>q\+q,X|.w+R2.(9h3X6:ȃ5mB,QA;HF8AjQXZO8^X3MIHGXdX(Sm؅7ȅR؅MhljxUx>؇aP(axrt~Hx_؈Py(C&vpP|)S 4&Q% 6M %_@H4p # 5Hzvsxn,huh|xN͘(VHw(xؘ؍BhH荊8IHhx7ިH=؇HۨYȄ͸G_)mP 19/AQ(06i0b`*1r0BA pI0ՁT18Ђ7{LIwP9(UɍXayiֆ(x8Vn)ɐ؈h ^h|I])VI`hkTW[Riqؑሐ6&PObQp7p`?PwFjgp=! D/dpp`!0j~+ &wFWYxHI)yHYV!ȏ9hiiɍXI)r ٙI Z  ɡyɘijIHg+: )I a/,""QPi#3Pax%0O I`HnqK)DP.9-]4`6;ץU*^w':nyYɨd񙨃bi *8ZyYcisI8ʇ9w١%Z~_ih& zʢW9*l)hJZij"ZQ#+Qe (0 ,Y /i*06J&afDEr`i`G##$ ?Hb I\ #iܺʬ <*ʠjzǚڬ).Z,\XU 語ʩ=*s򉨑괨ɵ^ʴ*a{ڢYJ*a+:hhɫ홵Ǻ^{c`J PS%9P@ @ 0iD+'r:`ApA AiB QP_0 3۷JTk8ʼʣڲ.Ki(; z+z:-[+Aؗݩ}ZX΋l͉..QO |r襇=pqo"GrݔٛV͐=RхlIސHQ*!]o _wWN|ZK+ܭ[^旙yE^apzxfv'u}ǏQ K)}[vsl}n~.z}~|w>^~n.Фn꧎ꩮ.Nn뷎빮.N.0 n|3y59`>16 a1^0f,aQv(Q)(v6 Gj&vP@7O &G&HPB_Ʈ}yi `O,@R!!tE\xg8~ /P4oA " tjXonp1RK @mnOmp wup ϗ0M1 (ꕘ0`y*ÉUiAIM]K0aJ_bOrMtc`?5 kBtG{1ppIp+~psWeGo`f,؀ 0+@ Հ N @Р,ih!BCM&( A'<#BEwCP@y  4)0J* z"Ԑh9p@`:2%1¯a C2L@JGAլ^qbEe \E`4~yc%/&E h&6ۓ16o@a;'^qɕ/gsѥO^ufC fDwkWBdkG!A ~ȉ@ $Dj BA: 8ph0\)J2A +LJ5 @ @ D4`Aت+8Pp(!!H$u[@BN lV!hM aDw;UE 'P,F MI dЗ =0- 0VȘeILA bۗrK\쵯q{21cxK^} [ =< vH@ 58(pQn<<nUp-\#<,sL{ޡ%A[c P D@3"B h)@4ѡȆ \_0BkB`pTp\)@HpdPA__ˌ|pB0ΊX֌9GPb pB7 zf \ui^sDR9sY l:Lbi?0 AG jQ&@D /, 5IsMY!v:!gcˈlhG[ 998#nمUc-%o(Ϟ[ X UM xaP?ºO@8;T([54@7Fޓ .p/kO[#'y+̎u;6ٗ 0.P{;xNɍ~t']\syqG0Aխ^? DMEA`^'{~vkkg1v p{~w]{{w^'| x'^g|xG^|-yg^|=yЇ^'}Mzԧ^g}]z^}m{^}}{_'~|'_g~|G_ӧ~}g_~}_'~_g __ @@,@<@L@\;|@@ @ * @ @@@ AAA?;@TD4EFD@8t4\PhQh05J_, ?hV][Ov\\:ؠ\ YNfpa~aa^?!aa}^`0 ^()6a],b*ޏ(E:8](b%vc76&6' G;P9V(!c c-c@ r4^>b8-D@^&N_HAUV<@G(ԞL`;.mFmx pVnE?.nn?Nfn00`pBhO,Nxa BeNˆL4k *2p-0XT0׌j -R 2q/qqq_T7n? q_:^`x5P~(.r*W1D5`E,tP%?r%ߠ&_(=>w@?/3OsʺKGh7s:7B KW>@(0P7FoU-i*7DS@upH'`XUguWW[ ^V]Ozvk@  6ڠ+3@5JR5luWtw7y{wwhv|_w(Dㅈ?7A3!4xW[$1 V?w|ggr, M̖ߠ?G?xy+Gy}yxwBWX@xfGu(H[^B3u~ovFcȧ&4P]z7Nu%gϏo okj_Aak{l?ϖS/`Å9?ۏw|_Ǘȧ|ߠ7} +}X|WAՏ@|ߏ77} d8?@/v_~b,|~~{S~~)~~~?O`oԻ  p "Lp!ÆB(q"Ŋ/b̨q#ǎ? )r$ɒ&OLr%˖._Œ)#=&yN{@{ 0S(N*8D`Æ;0+[~}:@U 4j܃Y2ċD!V{XS$;p^ LcGʖ/cάy"K> AgТ=@Ӥ?~mz5(FPkn.@Ix 5<10 gu] 0\9¶q+ xpAȅۻo]~^'cwp 2ؠ JA\DX^Z衆 !fHaP d" D DY@JCVVu])d+LgdA: cbzWWWYY呉yueq V5&RsYw2Ye @)Pg*碍(A02`y _V W*98@ZWnd7f)`:,{h*]Y lHGl!B&H  R,[ʼn* ]b@+P[X lUDi Y @ٖaH#B8@XcH [8"l2*8z_"{mzkf|@;z,J (*G,0! Pw `CQMt;o |TW}w{7zm MxA&xVTpFcAFܐ ia9j|aG`>z:r~Z,Bͷ^M/~.-,?m,Թ:K"I0P@B?"ݾß`x@1?_'P `&6~Ӗ}; u(@r&V g   R@%p㻨fBSҲ-y>ĎԘV4KZag=5qYoڈ@>W!BE-r1T?:Dh3$#XYVNP5č@Hk$I@<6zcÆȏ#!>́w$V `(|$ {w(nOLXyTx:$,hK -rkC H S,1Ii&5iMp!YyMpS ߬9Ntӛ$9p:p 'kVN$78AB(ȗ E@XA GY*ԣ$zT^N˗UOH`?q@qxBk4: <` 鹂: W!*P@L* @ P TU,Z*( ,j@VU1p*[hk:W(IvYӺVXZUJP*RRK;dOJ&B̻놅B˥G/WiT@ 0 t௮_X4-J0?0`-p;\*@mr[\6ܽUD s&UE* ^%zˢɼvH0x;ұi!f>KբC,seZMviA@00A,bm]1[ЀJAbCx R(W8ֆ@ 6H>d|ZMN~?p@,]l3&d w2sF.:9zs |?R7Њ^4S9zn~4+mKc:Ӛ4;O:Ԣ5'z@g0 $xt`@ ^ 0/H" 9B؟@|AŁDT(JmQMJu_jݟ8}eAP H@u'3A D A @,D `AdA(@0S2)S` jreEؓaAHw A(A D(@DvX@ŀ!9 D%%@(h%p?@F($ #8*RD8Kѥ 4@̖@Ė>Nbb7z7b&''AA""A@ @T@@A :ADT@$0@T@ PI,X6^ %HdIz,4AD>P@5 `J$B DAxA--QDQ"7~Cd (>MA0feRjVjFND8d%WeZ~WY[¥±ne\][@!^, (*6!",QS[68C@BLtu{HJSܢqsxFGQbckjkq9;Eegn=?I02=KLUIKT46AMOXƂabi*,8yz~hiostzYZbBDN_`gz{~24@WX`TU^kmsNPXxy~')5>@JLNW{}PQZnouȤ57BѵۊၻĚZ[bIMCDFOφ]EFQ;=F[\c֠hl/1;68:ꅩMQEruw|mnt_\^eprwQTF@C>naeM}[^JEH>a-/:]_f[tpuSvDg_ţ/J:иӽd\TXF>]~YLӔc䓔ĢdhN弙>nrRLufjOx}XȻD5rM@`GmuyRHoFkMv?uMU {UQ <[ZJEj0@`"y5q $HP@C  !5 RF#Șd cY'Qs#@0Q \9eU kUpK,;$)fȧQ5- ` &L'x+XWZkʳiR1B!?=%tmU=MtVæS^ͺװc˞M; !@XC  68"P HbQ`BD?0 "^4eW1͓^޼CΡ#N:j,ĎPx`[qu @mlcrŧV XZ"! 6ȢIH!W,6fXTe Af4j I!^XdTViXfeph@3 DG1C*  (BF['acYb"c i#*)ɠHopWg'쀐0z )ؗOu0EPn)(H"V T`w!!*$kG&Il![lfvYT9PH! L|zlH*RޕC V0%(0(B8p<[/L v`.Bm.Z*H2B;Z\= X`A @SCK!4E b ZHA̱ {@F!DU *F-pq ,P[,;Ľrcus A3~ZvYk!8+ R3y,6 @3sL搆h@Uf%w}VUt`DIs B@$`K`hC3@@'M7Gv"[nB\<=ba^~xx:e @KsD(:`@HWbPA(2 PL`ecۥ_5 $-P1Yh\ __ Ok!PSXWC(.BV17+J=wW^a6odog0_$p sPFx;*J6 MY@' ^mXF x.vHqVf>?HAVhri#rV P># 7_% <njEgiI:#G{'\p <`O bV0i&ap͘H[HȂhwa{2\j>n<*w{0.kqCC0ba/471&0V2pL >$&C:cU4$wn[nBlw!q'?C3Tŧ6 @$05&$pm*_0MqJ@jVp)=chqs-9"(&WCp d+A3 CdwЍᘲ/9)ry= yQ`P8F0-VII$obue yԉᛒ)҃iBrvvUp+;e+2m)y{Aw"J81!%"wRQ'Z ,Ger `5Zg!\R@rk6d+YH)-pZL1XP5.6:~-FhmAq0 `%PD@EЧq¨ ["k::Zz%#"@37?!)@` O:ZzȚʺڬ:Zzؚںڭ:Zz蚮꺮ڮ7-@Q2րJxp62\O*aax BWĢ>>6|jW0=Qz,) Qr5az=a [VB;JE Z"ds*6K<'N±Q>O6enB%nA(pS15xy+ɶ=W+q۳%(R(2(|RRa`MW1+=Q{(%ҢW=Wf{zJYpbl4rں !۸÷|[F>)἞!6C0p/Տ3'*c#Zl1=1A!32S J6 #T9zbp``Mu5!;!9D2C363>1NTACT_[#6s%Z>#d>+A>3?s L +Cc !$aeF`Bқg$]{JxkJHr=@>S qذ%[٢Aڠ  I]JJYVSfٖqmZrܱUX<_5լ 1)XG^^^Mp. l1d ``#ץ[i0tP ; p p i2b=q'-,'/eZy=&)Le j5g{gݦb,ֈ ))gzgs]*5)YFg id6Ya-i:&޹EM'(fM/f9f>%7"ꦁmclжljYAlޱm,.n`% dq}~='Aզ+YLMN0F/Yq. a~9v5.T6..s2w}QsJ.uY>ttPRo9d MuQsL)1cXq1u$soNnrA-Ν,O~@q17VR=b۲z(AzgzQyzL{,# ~ >~),@F:2hWf/Bl3U T1Gb],!6ӷW*;gS0)\-r~dR?鿛 Ȁ3r 5>1-GQ]`߯1O i*! f@`PС*h "P⎱ʒ""?.vAh"a- "0bhAH6\ $RMEUUN=UT>U<4.Կ^L3+E P [uROMok(KDNYU%Ns*NsDW\Z6M2+b]xՏ8b#V&8c$k_J`9OFYcJhh#-!4l~IHf3{F?-b8AiX"9/ZӨ,ဗ plsAtWgu_=vgvoO"H!nםwq'xG>ygy矇>z駧z>{{?|'|G?}g}߇?~秿~ dH|yΎ $@*6ih 8FwXyR5P!(?-cMv p~#` S CK^9TxAU EqS_jQ4-@ 6$  6`,`@*\@.A/A ҀxcE3)QJXQHĊwn1XHMq,(CE~@{@/ـ`&PR\DR)#6vE0l^ McDg:iıجds 0*" Z0) Dv28hg A+-3%Z>Ϛy*'G؆/,_dd  l1 !/9)6Xn\:raHxl ؖ1`gt06eH"RZE%v{j͉7/*WF3xMl S.#TQa Fh@b!(M arA`` E @!r t-49B>B@2f4=Bce+Bg1@, x / {it2ǣ- kBA[$Iqo\Y+aBDX?qAVK Eiq:hy~⍊HGI &H.`ȱ90"8`J`pfa);4AcF'|F`Dx*A첐R*k"htɌXȏPP+!H/9x6XkICU[=%ш_ lLȏlb)a`Hͩ` ˊK@&X۴# "I{yD0֤Vۏ@`RAτ؈Z \0.6N_TA^0T# sMLc4Aʷ$Pn F/1=zl#%I p =\!/_$5(HQ,ȶӜ ;PIP; 0*jgiu -:Љp0&މQ)S:ܐ*R"I*_$F_&GS6S˓lODA S#6 +Hu QŚi_(+aԝ): hWM}&qZEGX;**bGB)2Qej}%R!z= "bH!!V#[=fSh׺5I[m5UkYi=TkJTF]6:1P 11 3d*f2-㲒oX &6!Zb˥3_4S5c N3&!+2٣lsg~1M a+BK:Ec4G4I#J4,NgOhfR#Sf}V155[õ8i^59 O ^Ż$@i~EԝVh#fQOTVjG5!\iW.AM\_R.\SFޘXq#RD7ukw + 8VSh&')a08(X_ 4PgIb8S8M88;9_оv{+{K оc>:# >I+[^9]:i Lۙx@2FE_fgf_Pl= | gc9H?c?4p??>, ~?˖õĠ\̀P Ȭ& o'da{6V` T6e<>Pn,6E%sgJ@ J9jSi PʎA ASd !#LBf)Gϊ+kM7H`:@hbS,IZ' uH5T6LPq]CƴŗHіDr̗@DEd"!_A$Jijf v7̒oSl ȏdM-As;1ZCEtb&j2"1 X`-KPcRheLH1e#l!i[, +P^:dy0kluBIrzJD  DܰR~u]h4Ԃ$0Oy'FFv݃0D  ID*S\nN܍T]F"]>5d@U_P*VqOXn$r[V^J fa~5%Qf.Y•Y~&iu5WC:5Wl6(z[@+}C$'R'D&RIh%\z)h6 @$R V,Yp J?Haaz 5UZԑ/jU3,2+س9賡g\R fJaeWZZbJ]G.{į |(70 ;0`S|1 DƇ1qM|r}2-r5|_輳83A =4E}4I+4M;4QK=5U[}5Yk5]{5a=6e}6i6m6AB yA lpY 4%e@xUс@%` #\DR8,>9K!XAS@mtMӹ}& .ћ3 ;Hr/7G+?|(E +% eI^1HB+q&::Q@wUnH!=(u!MJX|WO w`7UMG}?D},Q 0XO [I')<` >0tIXJXBHP!'H!p ">,,8x"Dk 43A<a#K4J@0@`,(s6`h Xs% HJ>P@ `֟%;؉\| ()L 4 :5% @fY[rD 3`6 "x7X  WJ0$BX Pp)%0jJ.&4_EJ0`H$S@( -ŕ$J6M'BQr E!Y](f8bX$'J9c HYNxJ L&  Pnˢ;!/? *h4)hPDI,UH`#<,,+,#F2Q0KXtQ=d`2 R$,?@Vg/$]XPNReв8a (* SEkm:a `" a\h! %]IS a!] kQ֕w,-ckD`\oЄghkݕRNPYVޠD1|EزܖuY_W Tw;[ (0J[& ;/`rqB* (}A*))0V:WQ "X  V%;pb!$yQ#0!` `G؃ L@"% ;@|x`7Љ^tHm% [Ga.@X@. l+%pSA@3%{BlUҖ@}iSط mhD+(I7mF" /0~r0 IA&J@mocS;$7mjD0@D8V8Waz&08Jr?xAy#KZGJ#[L=pe,U eIp6U jpa{0/nO``2sgFedk&7ega&}M8K^] D$[1ހ Ǡ?8Zg˳0yTdq sی |Yt06 |xǯLp(Y!"a'afrYjP-gk$z%en P'XT&@ F_ ? P`9N͖pAP!tu'xA<@ 4 $* v#"9'rh M ng  D~Χn|{@}< @ x%`K@ލuvơ ҊF_^4@R> @_D0@x] {DH@d@Ԧ6VW¨U1@^6'Qd iCn)|%E$hN``@"A  @T LJD1B*fj&gdop^]J,X0Ҡ&J'8Q}y@i"MTTxHHhA@fܗ[)Y$t^VwD(Z\EՀxUc@k|E7jJj h3Y^d{ @ ldH`A~f6r+x u^((`t] E(:, \j4ll,J+KJH@zV0 \` eܙ>)Κ$̑(xFllm$4Kڮv * d6dM$z-ؖ' Dd`D ԉ -K(KhI++x@d-`B8@@x0i@,h9i9EJ5g(g`@t`nlǢڲ@<@M#..JhSb@x.X8Hr t-IB'DF+J\"׆m!!V) /XoSaB" The\ H1]'t_NeJ@ХX$ cKYD8/qv+K@"wTx\@ F}#D 0SiEBq,wLxe0\4]  pA1/E\8q2qy1Wp/lq*Jq_q4>F2'Sr%\r 2 &O2@$pA0,;H/2K0'3sd*i&s'm /03A |@l ` 4+0lga2(pN* (*6)|*w nnmj 2 > 13s;s+Yqw pxqJ-48Ӳx_5\D@S¢ xBEz@OJP7!^8K9J@d@yQ_hzMDAM@0xy@dۗlA@O0a@0\HZ$V1- Y0'dniv @w6PF| h p(@ ԫh@AXݖmzJ[+6;D˖Ǔ@`@h>0C>FH"!|A l8h@@P:|Ͽ;$i*FM<מ'#Jh?{?ABXT@ @NH4 AA!F8bE\b?bcCp2"7D)E| HA ` "" iFʕVW%,XJ8V, ~P@ * X$&xA zWos@#3d,!(HxJX 3,f"(da`Ϡ`Ŷo-a >X3ߟpr|; )l;D %5 C آ+˻mb '0H! 04tHADr&R“(D) 1B$hpE U, $C¡L@-#/v|hz!>@@/h^$ A j\sB )Gp.4D"-J), % B"yc)(h@1 jӡWc5 EnBZR]V0@-rTLHmo+JH+5p%"4*! NP܈n"vasסPA"|#I(>!`aX$'r)ۍ! Q=aE@H"cRh >Xۘ+!‚z&N yg5J`&VzHi=nc[sv!J # #Ț7shuanvۢUJjj>a\po!\vɹ]#h6sn-<(t|X'G]uW{lp1pš0O>psi/~͍O^_7?zzr' zË/?_vl":'ןh: DW !A N1A nAB%4 QB-t aCΐ5 qC=D!E4D%.rWWb+ \@( ́]hET~]"B m9juk(T!{ vI _h :j_Xy" `$t] `HNR"?0 &lb j &Ю#΀vU8a PKT@0C-'Z8@U2 @#2hd"g#ՒGt;1L!'"Oz>D4'0NRXP DЀ@ 3`" uӔG} jPjA>2" C0)DԘ) `$@ hN 5kf߼\8;& N%Dvd!'->J@U2M]`qV_!45 |$x\D(FG u(JFd(6,. @D0 v %AhT5@NaӟUM&59 AZ=(/zIض7 ` X;wL7:(J0Wk0Ho8`(d{DAV$ݝw ׼no  aOJB09=d-.ȃJc񏁬A&D"cd PE+r!%҇NLD}$@ eTB`E%0 VBɁRB``' G%ʵb0ҼQ ]Z,@PF_t2rBԩ@Aav*5')/,@O0a .86' j/Ɔ"<+)u]}h\4J W POiC:;dAA0*0,PFJ`)W }J"rPI&DQ @"HKJWJa(&A"P 9 ԧFJUGs>dsp3pH.:&l (!.k ˳ϴP  S@zQ%cuHX;r;`D*wOH0=$w"}7Qz%\& @̞({W$"! t$ +PR$ϒ Z%' L sWY@ 4Db=+C ӇHu7Hh(`._h,s&hbr1X@ `o%/ !`v*zm fJn,j0p0uqVO.na p t/"aBnl ,a@ (!Rb b`  @b璫X'"Mp%8M"m01~ ht6` ܎&F`0& B%j V;f"/0p0.b B . f~ 8bY҆xQ^^\ Ȁ8`%q1ѬF>^"h* @:F. bfi] H^ *I*g& n#rn@,"C.;#*fÝ.crPE@ @ӄ>1lC L(1GCaR&5 +nq8@I%F@- n)m| +}\YTr2,2b V)z@flH!` !ʮg Ѷ+%"!"&j` HNH@-<3)i>(2 JrDH!e5"cI**KGKi h^g܉.a6WGeVe_VQ[ lYl3'_)6kg[Ml[Ino6*lmɖpɉqa4i f"+"@VN"#b bVI- s +6~n Bqp$qZ"&*,J=z)x+]0*vM5_kWn6n6qwVhyTvg{vGWv6mcw{nUoіq7ặfop a~wpaH"7 lb؀+@%% `%يt!Bl "np6/| +F .rN8-Br am J p9gkzX0up7gA8Yת2C3؉YKo}iwq7yɬ^2X{"kR,D8˳@ˎLkBX˵ f,‚Ⱥ (m6 @×&(xKLV5Xd+kċ˽Kqs!mS"  TK!nG,htSy&kdpLp–1yfo`z6p6{{xxp8BX| xmՍ٠V\8kVT_~˷xxuɞ,&,˶€FČlll-6*2VH-`rDv;!,8"28@->`X Tm` "ffLרG ٜ ^@q" E M 7d|m2M]!͑";%-|Dpmaם8mYhEV~}joE)AlB׶rvjC8!x:O{l!7y! "+nB8^HT/lRbI?BL n L0I^; ¾".* %1'. .f>qh% .ZsB9"Ý 7//6&xy%Q-qso;XCqKQڵ4W]UBi1ҪU8!?>}G{x3?v<}鹤ݺG9q>7^{<3\;٣;ZES?{e۹}}Q"Q3!B i+"p"R`Q^e5~r"*"-,T7'+)" 0A?:  J@2z*40R H rcG$ <`EԈq Ua.,F`8 D@d \}0p +fٲO T{lLp]ZzM m[]{x.ٻ # oŔwcś9W2滣35to_nuٰӱI.0ԨnK[2eڦm}=z: ,u@YmY:'!PQkZ=`b. a0*o! P`r}^T@ 'Ań10! ei,* 4d# m@D @ QB#RL9(PMᤓ8 I oĈBEDPDx  _pk H)S2FV[p6hs vn­kɖWo7iնXc*lFڦM\ҺbEhاviۥj *ۅj_dKa ! Ap3PJ %+ ŷV%aB@tB+xj%!wY),oXB̀?q?P(C[htgLb]8[p@" (a $xrKwD 91< p*@ xW =fEF0L)Dg !L|͹Yxٱ-g~xk][-h:k;渪'J+⍧zmZY[ywx9SzBZ:q+GLJu etOh'@}IshGFomMZ$ cB뷿@l_챭<|2^bf{+t4fի`*ex tX)ozB.>‰9Q(@> 2` oC1/<=ؽjmg_xpzF,NFT"ha Fb1d,# a+F ll#W @Hcx-qe#@ڰӠG?"ra<HBqV#/Ljr'? Pr,)OTrl+_ Xr-o\r/ ` s,1d*slfCP6`CR` <438IVͳd_B` @`Hp SIA`H_,!_@'dv +6Cю~ gUHI?Q ue ޴?\Q?BtE,}>9-lRXK M8H9m-(^tӁL6CQG]TzfĬkT4#I' T:`Ӻ}&-Ib `$RA5#(8.MA0h$ P /nCD  @HiDO@ejϒ  N! b@P@^, HCE>P@ (r: 0|!@>ذ`t&+A plp Wf:E~2(a"H"1a ! ,x HO,K+L7Kn1t@@HSȑ9F@aI{|:`oY`ZӜwM?@A)h@Igң%kA>Gv1" pP,6܊4X Ef 4 `Y@;vZƐIJ`  `Ck1;pS?6܎4 x.0al] `D/ oh! ^H a~^ĉ쀳<˴ϒ D('B$g3B4DkBleCȳSyG?y$:rg7=dpj0 ~שvл?D< P$Մ7PG6UE  H3IhOmWy} СVUQ0 HȀ Qz Q.0 a&P  @ +|x^PxI0fP+GmQ~ ~m=t~1~fB~ `1m1Y4 09&=@B:A7 hf~P8r0#ɚ@9eV`-O@D\gZ4O`^$H@DȜ Dn!&0I9p9Y9ٚg0p@cW_ Z0C@bsdy ,H Z͙, `>4jrt ]X1ް r;S00iXds@:a(YB& P Dx0J@<<\014ۈQ:i%AV@/ngbJ ")s_z8zVqgq 9u꥔j9_ 1H Ur[uJ 0%*F4ʒQJJ C }Jzk@ QPGY.P ˱0W0`?pg[p@4wxI\n&Sp;9)ɘt!UJs=qrqQX)XN0@&8`bpFkDk"Н "jᚃ,uJ2D@ѵA.lC iݶдgѶgaqu\j^#c0fWM7AQk]S{Z)+V[nn<34$0^0+hwPN uy0aH2:@@>`p #ⅷ {g)-B6{ P_S+u$@0E8r`F:kQ E e}';k/ c[Ѻ+X[{`^ q8g Pn$PJ!"@60^0A` k/0 1 ğ+T ~V [*m˱K8<! A$u0 2LJH@o} @a0 ( `9++ +E2Z[  6 "0e=`֥OQ- R:kViiPrepB{2Zb 3MUCt[2ŋQ3h˻|pŸB@@!Wd7sqS%l1 \|[5;q:p7̺!aLRj]c cȬڸ }K 0]%=U!! Mp.p/Ӌ!47^z< @&( Q @Ua͹3"ʮ /Y @=<07^h5(9Sue;q4ZM^=#3 ?0-Qq+|n rMwؑ Af`td 1!& C  A\1սݦ-JS')q =ʊV,06}:۳]۷H@Ӱvzw a Ӏ k 7\ZOQs:@, 2 Q}ڦv<$ф Xw+\OqQ%6U3#P0e(b5@`)HrPM^m!0P!W0 cN(AN0FpA_P~X<=^^npA55ja&}n/13HJsQV]o~Ip^_nx^Ej a> 類),@0(@iS d@ ^Xt0f> >E_ $b0 '1 ,lo_r  !Qg#.VkƎ޻.w>!a{`([  #"Z)D$&Qb>c^YQMg!!? [C/G +u@ `(a.P_ #@`!<V0oCyv NP g^0"%^y'ޞ|/H2r #+!.q!9B1 a-j`#B&qN lXx6b1%o_0`c*FAO-Bqȿx7/{($cX.鼹 q?/ ϡnO#Q1T~EwU[VA }0p Fq ql8A*h40ABZ`9d$2@C$^8 H 0  hjpdI*V!AI)0 +%R8њ7?4ukWDd?Iya $rD0f2tx3?98B <~aAHA,˞pȓta1X0X1ɕ/gnNC:ܬClY:'!PQk3(#rE,"/>NnXP!) ɏ?` Hba k 6t6J!8^< Ah!2 ?V >P" RJ*XF$"^(A4$6N#H2TӅH"8p`@WZ4Z*4KuCLmTQ/rA#'bӠKiSZ%!4OrAf 5X$L4 zc6 5P! 5X[;p P %sm a p j n `Uz4 0VL="w~HD#9h|b*Ԡ͠0aO.7LZk)> V,LhMp4Hd8 ,Ѓ-\,hj wX_~ANeaHT# tE 'Р6h72DM$® %0$"CJa|pP}"%0 ÐGAƠ@Ơ>B àtDЃݣdB$ QAGya 0! A%@!p Cp@{H=aD` H jd# JX2I#C ``QD@ wR$h  & P@AаMo"!0adJN0dB'D@('BQTl `݁!AP’1S4{>Ї0x%i2TbC)EΐXB71 B>ql쀶 FF#p0 ,e.  !`,`.L^: OL"G9 2+4]i`KYf3i$0b@%B0tYLfPS")(?|J~Tgʋ %.uov >yd$c#Bx dPADD Cg]&1A# 5QfF<45F6zPڀ7n)VYN43C#8CJHϭ $K;֯p#B,pUG)$-m94LB{- cM5?HjW jm56UmsHH-eqZjV9]sB]HUѮGp`FI .*Y<8pMn'+Y)y 6o1``BG`|u#d;< 95rVQ!=t5̜ g#&fcU+fqYbmݺ V6I3ce >r|Pd$'CVrie4b9FpO|8ֱYcf- bz kfC j9mn] d!Fdg>Z }hDu( @af#3}iӛt=iPZ/p35Pӫfu]jXZֳum}k\Z׻u}k`[&v}ld'[fvlhG[Ӧv}mlg[vmp[&w}ntZΥ6\$O\{7o|Ø&x ~pN-qp;{$oe~$qzC3p\+/c qy]񘟼iVx!\DxąNn}9˝t3(RwJ7#}]ojn޺etcBzwb^vxW{^q=g8-/r@,@? ,9cc@3[ @ы;3@lAd,K@8@櫽{竻 D:#[>'|A*B+D >s; DK>0ԓ9Ak;̹2<"+C?| ? A1C1Ó=K;6t80̾)1҃H>-D>lT3&T}B+RE]4@\<AEc4γC',U7YsFȷ*=\EK45;<7Ad=@j=3FL ,\H{\<#4~:7!Ie-BH} V˃@F)m+$/3}CJL<:IZDC<[DĠd:>y;I6ܿ$F,BDKJc{FW Ɠ/ɾ:S>J ̋{E6ż7 9$D5L;;t \I Ů{@ LEMGMHY=%9$9Dk$ CABl1P9@,+$207خ%:+3-X7X0DxY̿|DDP$$PVBCEE\~2 EP(G|IL= z\I˴tFLPTO8#h7P~! @+i2901x1p. ࠀȁ (008@C=C廂 DDuЫO=KS8m= C PS`LćGAmEɎjMTK3]OL=8\Q9ͿI-$4T5Ϧ;EKWQ%7æOCe-0Ȃ1u8  (0@1!X- pCD?$ɃT)ˮ @mT\5G]me_Pbk`]UpT\ӆ\}{?:,@ʹy `π>vG^mnm~m؎mٞmڮm۾mmmmmn᮶whfLHdȃp]p2Z;3[0d[0>oC/YPF0 >3B] j0j0HN6Ј?JpW8 x']Xp p.8p  ?(4`* 7p+kgZoa0w1 ~[W5( @+8ZpZ`4_sD@88GF(=jskN/_@o.r0 ^51OHh, DЀP`un8[Q7@O VyuUoucX_@_v( 0XiwDr7Ghj&cb&'r*rr-7hDRV0Lx4's{ϵ0PP@G7 Ox tq 0.0'P҂0aH% 2tX ?ݑhj?_鈶#r'%yrfkg~xvxOdpY`fY8wgɂ9؁?`wXK:0t_XW8EQP?/||+}_7P*??w|`M P;ɯ?Ǐo׏r'x;s@_Q؄7ٷ|8RhCW@;O~W~xx8طȃixWP}ׇ}; 0|W|O}8s*20ը:@(;v"%\!B1q$ɒ8ɑ 8t@;G0 -+hIRȜ]~X!GCG y6Vc$$|)$aVQe=LdiuΎ? /ె# 1Ǝ/~,y2ʖ/c$R>gGWo0~]C+.5Bb(! !ѯRF!T[>a:"CVy:S8etc[GU0^xG̝dU$VKGus+1IPG'4Hx t]g6R& d!rXnQ+1R$ЂN)Љ) !#gr.g`Iv$ ("p,Fߒ#^A@ ;&B5p@OyABPBuPЅIKu\Ej]W#ڑX VVGhQХW !Ԃ/\[oʛo*y:2L?01V.@ &j .%F= Į1 BR taD#t4I+z7X$SR禛{Iv4 #]@#q\ǔ RJ$.Q%8X"=v,or73 *rp#0Tܑ]ֽ^1 ur d DW  CuX(8AgIeБL 9\@)B oczVZ,㩶}o ^핵?LI]BcRfiB%X$c$ցPHgB,ψ*=3RYw]!iOB@[\luu}+~R_ $IP$XءE-?B|7hFH}`%ؑ$; ,BazE"ZŽLz^?9 A1!$ ;>|fAH@'I  @ !$@GRȀP:`+a,@'AOP >!l$]\6֒d$wY q64 @ @,#NW[`kFh2t&B10HiNl~>X:Nq3 *P;$,juUh|4Or1D#n%AhDJm$1F;s$7D=Mp"810ԣK,hGɬo@ 0$NYމ $[[@ faDU`_؃j '*X"7հ$19:W$H70pD`)MP3f5uib6 .A _F l!$!,b HtG-nA;w1vHlkӛA} ߒdSc3Б48P0\-.ðo{=*<ѱ!GuSZ"WZ:E$oޡbFC80H#$#_+c "LWX nvd_*2lO8# Dj|M D<Ha}ml0PY,/߮3`/M {!̀1w֘I2h=.D$veԁY"#`ԡ +PEkVxÇ8@# BH|FE!5yƢP8F;2"$BZֹqݑMbynщRv!ju]Y#}}aSۧmcC2#Yې/pO!O30wO!(5PV'[XXrQ(` 0X"WTrN.Z_1H!z泟>R:ի0as;9dg՚HE*@JBԅKtBz@ӡ#a8a^5YDЉYy{}p:JAhNco#9/*P nZu' b E_n޻}Kd8ā/g = a=I"_qxw#d #!1 =Lҧ>߲>ߕqb xBvL!' A4@MaB R((B A INqY@ L\`F^B| DAҩR͝o],Y] J]gQ.a@ 30 @@U!$F0U#THPJja*PH&DDd!eM!ZD,RI|aDB+CK' 4%b#j"c>c??ʎ@ $A"dB*B2dC:CBdDJDRdEZEbdFjFrdGzGdHHdIIdJJdKKdLLdMM$HbdNOeP e䠘G3eR*R2JbdT>%y%O>UBeV6VreWz%Gb%$T^UbT%R~e[[4XFTeY%^%UKX_f` &եeY\e^ޥ,ͥa`BfdJd֒_V%U%bGc%]fcRfhhIcb]eQrgDbfllTf~mjilogp&eZfq^Y&ce gs:si"sv&tZubg\fvrgwzgWnwxgyygzzg{{g||g}}g~{@3Q:z  uRt2水fx&n4&vYa"*&(cpNr&'o>&_Forr."'&fRvRhf=j(bL);thhZ%Pf3&&r:ƍBJg:i)joVfcXJY^iV%sJi](NviVc2FH\ $+4^j)2iZ凮i]4k)z٥2VBizꝺZR*.自a>.(eU*()1R (XХAy@"d`Qtީq2f(k^r(~똪哂+뫚c>k+rjfRjn(lgrV>(l=` @\X͠\pAPJ 0 @m(2*>)+*l+~)_2-Ffkzq϶"%n2hkBN-.-fl9N @O(X^N,Pp+ܔέN^jӂЖZB쉖"%-겪bպꅒ®~+ўfjnmB-~k-ؒk>^ߒ 0.FҀd@ @@| 㚲.mf&Fb-fz*ꈦf!N*ή:칶/*޾~.Vү. pjmbj",F>€Țnd D+ A @'^m֯b⦤Fnb-.1Zv/&0>/-.Ğ o0َz(ܺkBpkcĖ+~vp9 \&  0O @b@*(au_ꦮZjf-n+[pov11)K*rmq{)"o+0ڲn6p/7nҲBؖr_c4GX\ظ0 @ƴ T3 Ёbh0%Kp2.{2f-(,Wgn2l,q>׳:m40R+3E.qqub qTC3oWO/5V3Vt+rI_uڵYPЕ%E7A_/颊eq*q&cWY7o`7doVXsf/-E56.v *~DjojivlGd[Njwn6پ;vovvo pwqq#wr+r3ws;sCwtKtS7SY3|et&XA.cN= -H`zbJ-LЄM^O+0PBEd` ACH0\aJ* lFDqR`,d"Ksv@ FtjTSVzkV[vڕ |ΆE$ްa&{'W*feɃ+r $tF`$VLb %@z _,"FOZU 3PQL 3o8b($"$biK0mr@jAt&0B =Dg\E &JF/K YR%%R&Zc @B d(%4DLD)eIZ!V"! nuWE3PHUV C;[aZXvթ@chc>4r˖mڅ3[uw;SZgQ%|ځ hYth8@dXwcFV1!c%B HBIz@S*ÄX% ՔO}/Y Ý:E,yCpb)b>Y Z"6(O40,+$Ah :0EE,@0$`:8  ` Iւ& vcS51щP<[T28`9X" 2G`NIG? T ejSB`H X sUyp Fb@"[V )IfUZ+ҔA%|S@ gRV,j^y'y4)b*D# !f}ҹN9LK8iN2.Od2OxiG|P{fFHj!$*="*Kh )lXŨ|@Jh  $C P18aM (* `ӝ)H70 $ P @IK %3G~a+AA j@) h,Sd㮂@ TF@UЄ!=+ء`)˫AC9,tKZ]'&P W4D(Ca`PΪ4>uݝ.Q?}BF@ 2Do*YA[Љi =njPc'@v=8 ?J> 9ii 0b@"`MAF2%u*@@`5!cJZ@`@UzMױ)VW,5{N%, h@z\H$U>HDb&f#ihE'XF,g X@ʡ` HUH2lYrC/$X }9dLJ@&!DF'LgY=쉊qsE} -l"@XuyN̆hZ@@#;Z6rkre^xiRBw R _`BC dJxQ85JX@@h&a]C|*`Q < hqaIF đ_=Heİ9qc_|<\ z"hJ%dPDDA{%K Bz@'~tTYDЉY0HW:n(^E€0@ϫCځATmZf,ֳi,X9CAwӺu;0_X"q @s;YB |ݵ 򒧼} c!1 =[, 49SA;)*t*(T=-Ĥ,A">CA. 40=sI4AS5*"@? \a."8(GAS+">1.4G"D*@oKD4/9,*ó,KS)y%ӲCtAR.[L:TG q @I 8. !})`(!v@TBgTMa,ETNs;KtJTS#MQJkTF5<;_uDU3O3UOu+SU5TWuSEU1TBitLTKTO5'MiVuO h@2Ke* z)R%5r# 4ST'sUs,tCK$W` (ٔUaEoTV#a]<}5L1*۔ct%VJula-NEYǶ>_F_cdE5We/e6UoVlf- {V\Wh,6#V >^NQ`6d[fOu:GWd/f?T06nWTS7`2M2_GM[WneMWVJlVpYk5XoOwbcX;UzIBipaMW=vq/gm$7.*5R L w6Fj+4v]okUwE7yskm5OvU7T lw=?zwCx xud+vdIYӖ5vktj:MWqw^~|ϗ%E@ԗH3r&TxXkV=XwvNSzQpcd1{JX/X9+xIel 9ɘvmxzR!Pn a³^ bwTp`h!Q,|+*U@hBV h]!+p )\ȣbK*T0R>ٚ@" `&@A"`@@A" @,Ɯ`@, @`| $<R.* \E*o,ԡ)|0P4\ ^̱/Da82A4gRe؟? %)C'v}ɥ@gznB m@jQ ` ('L '"AH`` "d 60do eUa߃mTBj*Z} $E /4$AbկB)"m] 6Bmh ^.(al)FARס)} B3ɱv@iq~ ^Դ!k:2ta2&a6!ث+~`Ft^{~ Cz^Ac c~^%Dy+^!aZ6u<Þmd/ġ*@ ~'Ϋ X/7') ! "b"*"2K:B ͊o ~ܸ_z'b(\)Z)*^Q B,b- R1@PUHP&@~$$G;C@߂E0C  hJJLD2Q %Abn4sc<@T;s;HBP9PALЍECgA%#(RGmf D l3Jk]XK PH|0(P$$( @ewj a+x`T/GDy,vX8( 70qz39,Hz쳷 .[WN=؁ H "& "45,jl!M([$S6b/e\\5Gt⒞#JDZ Q,1 4Rԑ%(ш[{}%)-#R.X"6!Us%< 28]t"p#KWro&0{ǂ$5HpDd#A4Pla`t A;!2/5^U`EqOUV*$ XfzvBV*Z;eR60 0=l2ѻ\` 6˱"k'BV@J6q01Xl ud$a ȂP`80|!8$` "0EI@7@9A-5D<0%Ht‰  "JSߥ6ͯ~|8LGpaYNwUDR3|Q }=le,RU#R^*Er(O]]oć8@Ո#zDd$Ilbȏ*L8܂ *6,ĘcM$81f(`9eX2e P1*-:` !_;@bbeH(1''{9q|sbЊ8t%Gbe|ke(nuEW7+:K!xak>VV^%Yr }n9#f:){gm߽GfPOvL Ѐa!D P0? %"h[P<6o/C @0)1((Z/n#[Ʒ~ڔ]`0CAb >H g2 q$HDPU p^ #(8:Bq #hJ 5x#$Ho P(Hxp'p< A 4UB#OMRPmMR8E TZ_haGDA| d[_[pPmV u|؇~~QFHAs؈Dy qDfPX0$i0舞8XS8p8Xx؋8XxȘʸ،͈(ؘڸ؍9BXx蘎XB8XF$ ؎Ȏ58YYxȐ9Y2Q y"9$Y(ɑ'Ȑ i029G),I>@2e ْBLٔN'ט,ُZ\ٕ.9Wi!^yhA=npr9tYvyxz|ٗ~9Yy٘9YZ$?a6P?9m)$D# )︚X3YoER:–y 8Jɓc ay+ W 8YI~щ#n `"N>Pw@q3#T9ߘR_ $iٚ艕y 9ɩĉyV 홒QYYuȜ: 2Yx 3p A2i IY/Z4Z78⢡9)< ;)b)BIّٛR ɛʡmHC`PyN?fp`` N;>IGII/Y:jiey˙NGJ:dyz FEKړO:ڞڟʚZ ~mZ4Oj 3.r=@q*9F@z d3f|:~z*}#y *9rʮ٭ZکJVyߺ ڨ3J*yFɟ)ڒZb9Y+z_S*zB@0! @40 ʦ˥)ZڣyE7٤ZHV:)Y麩8ʣDʭ{گkɱ_W I #[j0ʑ# t@/@Q ]˶ a *Ek{9ɶfd{봣jZI+ٚʩߺKj Lʯ;4! 0Sp.+[`G@0)`Eh븯+<;BۥK 9[۶B۹ř[{ ᚪKZ KK+B@s{;"`O,Pt5p QJ۴ ܾzkZp{W꠹*Mk i;wM9VlSܢY{ʰ|̞Z;˴»k~ipImAՆ.K;**El 2;{ ?| yR O;K D;L],ʴ \W˴:ŭk붔c;mo<(@[m { ;BF@|%U!:)[Ýï Ha 멞 o lĨ: \\aٸl̇ʿK }<+* Sh%y2:;7ڥ; 0j̸g8'}^_=|Pl~;Ӷ @+ThѼH7-A-NFKD=|D:$MZ&y\`b=d]f}hjlL40 FOazmzPRja U W0e$nA"c@n1P 30 FqpJE C@]iC ' oAB`B0}T^qKz*y#Kp]=p##7;ڎ(~pSX3fT@ @CH{APx ѡFl.]jw] ߟ'!5#mD].Lt#נ$Ɣ  ``<c;w!s]  9~z4T9$ &E^GBؗ[hcpsG j  bNl ?wd`oa2& m@ ("dn ކ_r}  K3%f A:FEtQ1Q!Pc |CzJQ!r":x haFnpl*&n @ 3PtS`<c3{ . @۔@Γv$I$M†%pW%E #A07"0301s1E|v PR&1Gh@e 2Z2-˄Њ  ZP)^-_M:>n=^Dc4R4QV5V!> QC-Cw?[`@)tAAi2ӷ17/_}B/CQYp_N{ @D>F$`qR _09pxE5߈.OtM3vEZRp50"TOOQ.o,t!cݥ!Ee[{FP_5G1e?8*+#`HBB  +PH%MDRJ-]SL5m^ pfN=}TPEEL TkVXSX [Hc7$)H)\Y np'' *[QlA:$3n R$)\B#K I8EAO n@b8z,Q޽}&O8G\r29DŽ J\dM)& d|ՊB/QKOV.S-ۂ5X X%7ેx>`AZ+ / *4$֫$BD^(S`łB+# Kݬ" Aɂ*$ ",`KxK1\.̗3M5dS{R{1';_4G ª$hI%^jAH3(>  h> ԸHBT-P @!)tbW"@O7<"#fMS`  zCH> !i%ho!07r1! f@`ڤ^{C|_=))jps%hC[铒hh (bء$ Uidc>a Gj9fgfof\ƹF"`؊D8{fڦ}:j0)#) :Djfm߆;yɉw$ۛWz>os >wiϾ|G`7ӧ~R{֟8@P&Ä@nz`%X@$7AvЃ֓n)CUByLІ7!WЇ?b8D"шGDbD&6щOb8E*VъWbE.vы_cE`Xyz! Px% =֥.S3ѐ9\_zt0dZ ф W @FЋaЀ䑑 M&ԍ(INܰFҖ,TcL"PˋD$4 %yO|Br;d-J]қ*<a(( I@2+ 0pmaogAA M(H dp9$)`7dh=E OybP,2ԡ'9 K~0XhHG-0gr*ҋ,a T.DLKi6P\ .MP?dxQ0'Hy~ӯÃ@< npp<Hl6:  tЅ`hx0" 04-mA:Z-pH 6@`[X3@ _H*e HM0f,h,f5 WN*F/}y*i-h_~ϼ { Wq[A ] V^ ` TbaD=ˑd$jI>`!Y]k['D_H\HxKF؂B-'G5dpAA<ChdvI g/uw^,Q R.sA;i h0u Ax@P3"9ļ zW@ 5 2&ΜrAl), duII>`! Ar=vI u T 04Y%i]m'm%Fzݥ" a-0fҦ[=m׺n6$; fln "?p2x갋}섈θe<Eɱb/OvԼηTDgwF~=%aKS! !ёbg=@+|{ @4aMwNtp64`uڱuN >޳3<8zPDlfL萟 f$j >HB\3(>\ '`u'O75z/,)LB8T_zB6㨛ۦ#=$' Y+ri^B\*U_ÙS+f/?N-?+t@#=CS=飿ё  bzh(&10U76!"qӭK)Ӷ`0!OYpH!," `Ap,a*ȉ 89|6 [4˝  1,Ë>W! +]SDXfBYdL,0.28^:] dVff.o^曒0@r>F~: ANBRSʑ/ԉI+!@W'p)Z/1bb 0d 3Ixi $&PX p^ f, z @)@LdG42 OX H aK`G /[&H#ր"`%9 %2Akij12 !(˚fk./OHgX\ >j1F6!4S36s4> /-k9Hb- yhN nO(05 x2ph~,hi%I H&!4޸3l yjЀ`%,x$Po q& ?{8h-ЀZJC  Ofl۶ń,p/`gFlH&OHHu`r' [.(mAp#]K8[M`q^"n6n0Zh"=4ڄXr8p?H(-؃RR>.Hؼ~j .; y %X;S\׀x<7O  8xNJI;!#lU .axv7flSSN OwJt1`#`3;{PxipcOvcxp2 B`v"p "L3! gFOmH6N[:p:3@uG.Qs pؾ~.!?vN&h_XHծGi#ڠ0 OW36L v'Gy~}Gk?BĂLX*&L0 ّ8@}`pW} ` A`pIo(,WP' 5p!X-BDaġL" 틜0x-0&tB(+i%pIM5$((ˆ\`" t!xCȁ:\4P!Cp :!t q9b^|J<D*h` "\a$!Dh SXKцWr૊3n1Ȓ'Sl2̚7s3ТG.m:8QS" X Un1V ѤLZHaeeNif_Q @ H4 _@j|G D@c*|_ Za&e$D}C A ( pIHX[ 7va:@t5@qi}apaO, `DYAPvCUVV\ E#FЌ^xy'}' :(]yz٢5h=J%q]'(V E^MDU@dj꡽] m(ք)]*Ex idbG *% Y%܆z-j-z-^fi`` WYʖ p'd.z;oK[|0 +0 ;@(FKiCAOPl{ rA&2Ăr1<35|393=3A =4E}4I+4M;4QK=5U[}5Yk5]{5a=6e6f6m6h 7u}7k79l}7ͷ~8sx;8Kړ[~9G^{9o:饛~ң:뭻0=뾻r; ?h~Y<;>髿>>??=Bmh5Ψ'@4.?Fup h-*`o"e1c(`;d' 0@fcL4B&pB[Md( 8AAJ<1x A$/WYF~#+ I#E9ЋƂ4J"C <@ r{d:#2G9 ,U^G'B&K$(`CFqUL$-[lC1\C<R^ƑYUbIJ*FD2# J6hҎ%$gEkbs1x4 G\% a BVeH Rgk"#B"9(z;^|BTdLp ZpQnh@0RV u8Ϡ 5)L U%`oJ& X3@"9@"ZTh 9 D  @i@zBL HU+A0$B &=@F U4`\]dҋZY*r[U]jX eEXA`0 EEX  =)hްG2fkW.W?-QPPE:43IUL`bVMP*RL餞*%/ ֺ @b:Gap(W %v\'Ђbd c&oS#F+tK#`xVS0p*NP@%0>*e>s mŊᘲ5 T<~~ biLZBAt`D* )催5xt&Y iIS11hA'WI" Td P)Mg1%OL*u "RejDM`J$ rp@hdy(` 0^v5 ԁ>ei<.H= j?|Q&03u&4N7754 *VSx%oaoa p0L`A&Ȃ[a8w0. `7X`"Ghӛ@#] dUl@ \0??v%]e(8u 2+i״$ Ķ`;m fHrXA FB@@N D$U/sNi|<&$BS;pbMAA<е@U4[A4[$bLA YAHY܆]69p $@ : ǽDܟbUh|0@ L\vdR\R^ ՠU5xI !)Fa(!b*ŞUP@(Q#:\G ~ ^EY&5Va ߗ(P_,؊bYE ݂W)>!b$:āKeP}G$l @l[M8to@xxAE|@ <>?J<=A @$` AhFJD}dHixx, ;# F,llC|%=%<*G`'ƩVxAt U SdNl@ G}OEBąÁW$e6 X AL, Z&F   @l7(@A@|CX:;qhGxy.zRG`A@r@ S TdAd Z$(Z[D{dKUA}*ƈ^fAh&~Im.Lha  XExSU$.bd UeY%.&p gC[%GX'vB)"U((AQx 4a!2h]R]RU蕪Eh&Vr\SL b}\vU#5&]ui~>܏)сth@jy٤DC]Xt$ L j *!@ *DhA!dD@@ DJ 8H <В)A-Dl@U` l6wr, Jk~eKĸ}EpkdX) xB<_*PkkKd:@}F!pŎ@QY$4Hj@Ԭl\nZk@\ c* [zрtZl)B $T@į^Ū1&{Z!2m  ᎜ \4@,*)A 8U$Wǎ"BrjZ۪-N b'(n#2]E..|aA,,ijQZRZ  |:ƽ:%.n-Dj-jcZN[lFX-ҥ`!A^nզ,AƐ6D"Ćx:Ue/yA8(@ :0Kc#sĈޱgdR#< Oo # 4 f@ wD^(a0@ +8\Iǹ q# G f+W0rDoED0G0]F@Ne  GkM"0M9H4@sq $!hAY2 "U"5ȿ6l|xb$(Z% mP |$qfb$`D t@ @RzײX8HmZ4O$ K>3BZ'b$aA203G8A,UU,A'tUKoA S/ALv̴b$ (3/D!_֒F#tGQ6ss4tt$F82@rAt|]5E8 P@@g[(ZA[SSUL4c>&6llPO!j"2i AH,j%9MD X vo@63nB\wHp -lA _5Y7v'EDAwqAuKqwAc{cwv-n/Jsow~7'tn;8t rrƁm7D2r$o z39!rѵx@p'ĺw} HbZ$`AK72|#Al8DZ|m+F_[o{=c3*Z(.Z406)ZZE$XxyK p@ H 0@b|z& x7Lh &pMG"62@aU4s/$A("'^Q9AXP:Z_/7Ɨ;ACvDT.zA ÆZT;#&T@3^0Q[fWn@ܺ+:aanmȡ bODN/vCD@ t||P( )b,@UtmEDA zE_Afg{Ap͹5[>b+!8@ 4/3FJo!<[U?@` 5|9P np `7R #)bÀ%`A"B :hPX@! ! X9b5ȏf@cά ˎ"`TO Fjp$`Ȓ @W'B'|$HcG cȔм9@Ξ?$A\D#|ABVtcA?(2 kװet./(a+i3@ 0"DT"R.a>,=3\ 4To;0/LCm3S+63 0 ** ء :sG$ m>rW4 O`!:!`3<Ȅ$"tL(p>Lha "FTR 0KlIH>C] H E E V꺋$4dX(pQƒ$[G#?R(5 'p@@>|iDr (`qLs6՗_2 %z394H ʐ$dev!x0!&AZ6cz( 099J5PA })]=覜2,HtN,I2 Rj`瞯li b/@.3  Hؠ)M:0 p@г/3R[+"P"-:ZP&81@4[s͓@…cj @0<9qHϠ@A,V36"r 9 }At22 _CgXA3?pA\9DE,rP:' g;q j:C3aЀ LKJTR\YD:' FЀLpow R0}J.XXF1P>D!֩&ޭ.S! CȠT""vqыh !jbnEfmBaX2 gr@HQk2! r2nxI) "de# H"F$ =MB&yP*/$ yɋe P+ěg݌-4̇@4;tGU@E+bQ%H9ޱ>&I؀5F+4HخU1X W0Cbz.T*" oPÚ 7&h7'pn\HNS EGHi@Ԍ  ST P#Os,e?T W gӸ0$n]%xqk(sTbQ%f `o=:`1ׅllňd;Pp%HF.sZR&%Pk{[r8(ig\n-Q#^Zk&HAP;UvMp#VʤvltV Eo Eo}ߛ>U]R23daV-vac`Coۚ`2$61 a氇J&,;1y|c8Lf+ٚ8/N޲k" !؎Gau,;Xlvc19Ѕ6hE/эv!iIOҕ1iMoӝAjQԥ6QjUխvakYϚֵqk]׽la6),;m\ 0Q\ڳܑl )W *lYmj'd d `RǡS QЀpA/"M4An" @>4]sϜ%B'A$`ӹ'~$ !\ a `, . XA0'Ԝe77'( @ƀH:H  ,`=:a, h:@A[S%ЛAT f 49pg{iwa`$p !Dpx #G@"ư XBxPCvZ tO<@HG\'ԗs9M7VK P?eZ/k uP&*,0`n@LR2:@ K 8H"*.6D l` @T(r` `$%"`y`,0p0֎  @PT B1X@ VB 0 #ڀTL T@Mz#$ ` 1q[m  傉 a7V 9ڠx `@V)1^\ Ȁ8 ejn Xj! `Z4%1b6#(HBg!@tC1"@H`q6*(B)"!\ zd$`2 BM>ƩPEU\Fq)5,D6$#TD " l:NN `^`~` [ &q2'kiVR"@`ggvxɍH0" HRB$+B@t2-r-Һ \˨*2 #\lrk20s0 3Lh011+ 2)2-21335s393=3A34Es4I4M4Q35Us5Y5]5a ` @.7m!M &vwbx;#i!h r/G |w`𴸍 ;Aʈ"l3==Jg l C3$!?k> AOH 9A#<'<'? q(tA'q:LCA9tC ەlWqX5l'|!r!4#W AA<#`^ku ƀ v uB`~ ~`<4/7tb3Nc%j{TC+ 6cM(`r@bA44}  H@[6c`3ٛY[2> ( 2GVә%Lmsy5`eQb 7cE ͠ "!v  p :#y?CAYEn[4Cqj+vne\[QFxG\Z?TǶ☧8nOnGu8^utpيnm8_1#b "/ daWuxXxB"9z,N L`3 rH@+b4 %HI4z3Ø@I۴wAgfgJTe'C \2z座'[:_\R9Cr!)A;193|K` x@ʣ`ZZXH@@{Dmj = dc'`:Jh;rJ9hqU3$@ 6C}.:@3b@H.VU==V bw6' b`8fhؓ}ٕٛǁ#  n3~"  .M#!A?]?cj%\EWXlX؍ẈzGO&*+)2#`3>3^wekK'qB3=g0-3040Etjl5q)4fu}su@Wq!{EL]Y?GOůNjX<8Ey|7[_㋟h%[_8:DKT}?띫vհ)&3t5C""(B&016ذ 0 o Hp #pe%&*,ć =9AȀ6DEZYK`Pp #5;:0CG:! .vBYP NK( ! (`@Èd|G0aq,ڋRHЏh^.& O2;Rኑ57SқÒͳ*K{OH]ˇ^Cϫ2Nݸnwk[ztrK?^zĈ}'O޸⽥{_{|;WYywG_م`멷`{-8sk)= 4LBL-1 3 {%ӌ/_CnX0$EX4RB5h,@%`0*!FM= H *01D)Q^D AUELxD tRC*$nB Q@{ZBj ԖCt]. t](74 8@B*ƀg @(`V PDp!@h.=@>B0I3y_s]wngnw9r vyo~6{&0{ X0 s簀ڥ. w|;oqN\>0c9Ȑ,V./Lu˞@g%!5?y$A"oj!;` hq C  P+D!M X8G  50C Cq301CAa9?:M~xX:\ ;` 0@ C_ByCֺ^h@P=8>$X =<~#ϡ<4[~G#}) \ gOfB m0~1>] P&kŀ翤mԠl`hl!1`Dr0'@EO DDwU (DH# 1H MG0ٚ aFn$sr¢P@b\q49_DytɗٗzY1`IyQPu8ɚ )`0։ÙIԞ 1})Z0_0#OS`<c`6 hɛ PQ @ P~jJ)-uС^yk-X+ iY)C124$0^m!TjlSZWڥZ Y\JV^X4eJQ0]i* %Eej8AB60^@ q _m!: !_ZPѡJ qePY! -&fz*yǧ zz &{J \Pzq݉n2:꫅ 3\ګZ wOD:(aA !PЈ % K@TDZkW InQ)Oj~0BpZ ˱4`"'+50 `-;3 @%P<{ @DC۳Ф1A(k;׵3KamQa h[ofƥANU;MPǚ![1x 3r5˳ "I" 0* 1ʥRPGKۇؓJ PPxFꮏ(mtKk0kY 12F0˼̉;Aҫ8ֻ\ѽ;dм a Ѿ /@d'+ѿ I L+n XK {웽;kd !V@X* o!>W99a W=&0F7bsT}g^+<DE~4=!5L^AP0K (N; 9~T=7@ ^~ !FV*U.WI 0}J$`JйgoA:-Δ'Bz:0"c$9z|ފ8ްoCQ[2"Ht P !y~N4 ~덶 %4ŎGIQL+PJ붾_DCT0aFNT %`G@MjR>NhPjkeXX._jjL[/QW,5o!/#O%o')+Ogq9nq1/G !qs0px8t\r5?p p=_7MtK4}G}{Ro |zcIٷ}yz ~`yy*Sgyi|b/zOOTNwIpq8ElCXuwXy D~jhmxSvɏ-_PJ%) D*y䉠( q鸎O:ϏIʙYיhaIɠYyɹ0Cih1oKlp+#" OHP .dC%NXE5nG!E$Y4АJ,]˔6g֔3ͅ( `9?*204 ccqEA(ZŪW F :[qΥ+ E|IP}̡aĉ/fcacɕ/KA2攞7gLzBDCk1' GzAA l0 oC޽ok3Ls'^pՀ=B!ۻ'BDyկg /pj?4B"IpB(LB4#l@RTq\PȂ\` 8QFG XG rH怢 (#T&Hr(H)IbK+ 8x.X*!.Hb#j8N\9/t.p1HD%QH#tRZ@` K14N3P?RQOtT!8a; TVX`&a$"  l` 3jeXdegv_H\UVJ5W#C^vv3ϵ^|m!R&x25a[qΰXH|c b (K>Wn9+g{U0&h>hzꑚ8!z늺k,(lMjVhKn{oo|p 7pW|qwq#|r+r3|s;sC}tK7tSW}u[wuc}vk]r!wO]w>Oy_~"gz32뫗!蝯|7?{x/y_y$#z' S@!eP}%TO ;?+`q?"/["=qJ"+Pz3CIóbyE,21M_E(C8P}D(4j1^@ v=Ul/BBш|K"QX4$Jb}$c5JP8 x2&r̤-B7B2IɀTEJKtIYՑիCWO9N3i>5*e-+0ƍƱթ0*Q5C+T5Dk,Rz"**{UOdAyϧժ==KZjL-E)I҇Sjؒԩ'leZTN)QYZZ(v*X|j[s1fL)(]~7xZr3DzWf5elHӽn*{^W<.qUֺŨaqon<2؏%5iB[{ONwiHۚ%N%AW{fا]=?_ OvydT'ƥ8%uYIV 3IrX^6p FrkNs?W]&)O:t␚2[[AP`v\Bu r?12#dZt~iC;w{(Y_^ӑ&P'֎um}k\Z׻u}k`[&v}ld$gfvlhG[Ӧv}mlg[vmp[&w}p@fwnx[w}o|[NLo\'x ~p'\ gxpx-~qg|8q\#'yM~r{+gy]rǼxm~s\;y}s}R~o?ҙtG]SG̭~ug]9Ͻu];/z~T7Lmqln. g{Filg;mxm#~;0h.8 o]n2oV44`~h+ Kwr|Dp>}«''rO\B,K,gW{n@`y`"9O m |i?öԶy`RpX6Tgȼ^8;=H@!7 l#3„V'Tf7LK$`902 ( 5lX&6`@:irC?bc7Rؿi;DDA,6@HԶI4p(@J| n AyX0ySA,ՃZPEuQ0IN7S8)  3B_ cL]`PHЃF`FE°F+ ‰Ɂ@hG@F`7Tl7Z(3xJpElxEycMׄM$٬x3R0PP7DM՜7lZT7JM<[UFKUZjJpBX؅HE P[`HWЄZ(OCPX8}FJP> }UH ^H;^}Oj|P[P P+,`S75Ph>;p-O`50P@ R#ER%eCp&hQX2=S&P&CQS69S;(" ?H,ԜXL1qX1(yxDg?*8mІcPuԧ$ɔDE,U;XPc0s8@usPiTTOX8m s8PINd%h a-PUQ%U`UYhTlqxHqT1xqUmUTVpDnJ (XxW@O,y@#hDRR(lЀKs DTUQg[ȷӆuHX0xOG`E@QpPc^R8i0[ZX@jM[+D(C@}x@x8Kx7VO&XX\5\č\ʽ܀\lP]}\CDЇjVݝ=Fx@d^^RSqT`4(|!HmB`G߉ߜ)HPABfC%bЅhXzx6F-g6 F6*KSDLUhk|6kX~6^ַ[ILk<"ֆ N^aa~ 6z`[uzH(h+)λplKNhȆS`4b -L.gu0Sп3bk֔ XbT&~bk#ySZv;IP%=QM8[]u;uMxQ Exux+STVUneMH XmeQ.қ7XtM[wkH(m"mp6p#Dԇ>xOKC ]Pп%GFa xw.xg-(5c-<xP>@PQPwgF yL5} 0y|8H0(7hHq<Nn0G{j<_8Vkf#CLsJTUH}6a,[UA$D]mE6!v,g#Ma*TdԿ6kXc`e=cφA͞kHfcplˋ66ub٦؆p4&46 @n=HNXL|g3=k?kE> T7X^7GBKJX7CЇuUP]&JGeChx~0S8O xq$qq_sj[pPh Q{H{.m:hU!Gr+j r> Eх(gKZe8bir,7*1/J@Vh.&`e@tF[1r)g7?@^%߉3߉7tM_(Q/u&8IA5; v<$l(ʰ^k6SuilhUVJ~amǎEl*D>.sl^l@ItnlmqorG[wGDhXu^icH.bv^{vbm<nXk{@iتXLQY&FHyHyAu3jFPu&MhSP/\gyJ؅ p|y'7JPXy]Aq g)\zr{ҏ8s$/GG>& h7P鼿W8CjQZ"Cfnӟh\ O#G.ߢ^߭W8}W&B(o6~'~Q0Ygk.T{i6/n6wOS5g#gCDmvlll` o6SHwJ7JWI^ @Z63hp\q{8B +N,b( R@Jp~#`6R)TX *E6Ҩ`4hґ<>@Ʈ +v,ٲfϒ]l$M`0%SM*&UG*`IԢSa3A:n^P:1.<<"PǐoE^Î-{6ڳ ΍ZWێ&ܵD=*`[@k@~]qͽqWNȶ'=T& @Ǘx`%p"v]v1I}vgt%E8au#XI'0@" $ bXS8EE!#$c@0U`OBImEY^aXS F۬# (bL8ˉ! uZ;~7,2Q CQ"Bu0!l18@ 7T4uaԧ'n9GRy+7 &ЩZjbϸSx 4 fEAJ,XŒ4W[QRJ Xjo/q.k)kWx*~n +;ȿ( Y iE1W|m5Y ֪&%iO,R#=[R{"Chrq s67ꪳNtAW7w;.D3ܷ7uRR$rA1ycۀ~k#8-0X߰28a@1%Ê#V0d7 0cq [,&bæ2qy3cp0" @{K׾B2T"w ;a$C9R2pPjp/9b3c9jq-592.2<#z3g)?ςH mC#g~GC:ɵ3+><yК4;OS9Ѣ5KhJ:ժ^5kWúD2km[:ׂ.5{6X#Mg8Ҟmkc;6o;7ms;ݧnMmj;7m{;7mv[v n#< _8}MeCV;<"9KnCFblgsؼ7`<:9{@V.qf毉9n:n=z8bg #lQ;mvj{.›q;+oc>f3Atf=t6m?}?~}>=ַ 0O2ӞGaO}?? j =o_[|__ٙ^`A`JŸ= i`[ 8 Q ` `݃3CtٙN`1 2a:a=IL:]&i! >a.|Am(faݝa⡶i]aR! a!!"b":#Bb$.b5b$Z%bb!, (*6򲳳MOX68CACM!",bckabixy~񔕘QS[HJSkmsLNWJKTrsyBDN\^e㞞wx~@BKSU]bdjjkqIKT{},.:mnt>@J֫;=F02=9;EFGQ̮=?I[\dtu{۝FHRuw|PQZZ[bEFQyz~46Aghoz{24@NPXKLUopvhipDFO`ah~WY`ijp57BegnUV_efmYZb_`g/1<恂˷*,8謭ڭcdl]_fѬmprw57;\qPSFz<=Gh_036+.5e񈓙b¡06:|sLODnsRw{VvEG>k`dL^aLhlPȪ/O>]Z7?=AD@зchNTWFIo[^F潖ӽGkDM5rM=@?U gkK̳ϺιлϹH*\A #J( B3j!Ǐ CR%Hأʈ+8h̗8sjϟ@ Jѣ:SJttNH2eQG:qѪW*cٌ^ո+9ۺKݻxe8 10q]Xb߂(x2Q[|.ϠCMztQO Th :#69;(j"d0@xP8arz~y[kνwv4ADXc-/?p}ߘ&"?pIQ#ҵ_)GM@J7" E Ճ (@&"Cga(@`~?bN7 M7CXԎL6>%Z}I % BFFNaEDu0a`P#EXz([!E&fRF&t'Aa&AkB)蠄QymH hHB@ 4dl9THj:iEq:HOFgDj뭸*I~UY {Txn:YJdAbgQNH'|Q"y+Ee()*D* ;+"ՊEYHls5M|(uFh9|R@10,sD!uvF @&1M@&ASu1Qd5CJdńD5AcYG] }tP\HMHkDX5wDdG8୸g"Gc@2@靷YE9َ/o'7G/Wogw/_E?0'RT?G5փstïR#J'(iZX6dVf8#:K4tP# Ri*AJ48#<88Rֳ,g4\H2M$yM#DdGT0FseR* A)ndԡHZ kĜ&vX쥩[Fڴ?'#[˛}vJ? Il)+=iD ~3(BdN|EI4b)P""TlR-d=1ҘINqR(թ@vAjHA$b/BvAT'+e.kmd00t)oaqΪt5$!䪯t 5oRD) aD v"aW2]re U4,JԹF!DYe@Lc+GLϽ9 BP˜}FnHq1ŒZ9mhuP <3RHl8r 4C yR_f$C02E0vnh.On( a@Nv0wơXǝ.(N;x0gL8αw@L"ȖQQ&> L*ʶfFl1˫ l29*o4Y&tf(2.$gs)X xCDG:B*!F0}c'N(ZϘ^.)hR HzKڏ4/H R <ʪ3"{<KP!4F&GECʱ񒤬L@4I|jYYtRVsOlw];"U"?t2% "rvi5*".D9q@|BpKTMҖ Yf1pXpArP+@T~h6 !r\ #e#||T¯3_L¾Qtt OH^ #ͯ^󮛯*W9,.wC;N`nyp2w=xϻOO;񐏼'&qrN4҃f{!@T/m;콷f.v3Lγ><5v 424qғ&9C>?F c" Lt?ikW#nͬͿo#ձ6Ek?x]]ӇY p$lp~AulqNEz"s7tmsn&e;OQnQn9nxn6FSCobR(Nv8 pJX-p%D5q<8"rqrYUs&g#'}!1AUr=[\'X3rg(1:K!TXtDSM}\׆ E6VGEuyAvv!v>:egv#l'ȉnwuwwCx؊8Xx9 ˕g=QtTyG31e*q63Muҋ }C=Vj:!'q~"M'VeХW1O#A1  5B'yHA q, gE!񐸓 tSdEd=g-".QA{4k$t?!T A+ikلbAVH0a: 1q 1'ƷG DFZytqŎJXV0q/qIR h!&3h #.AcP72F[&WrRdIKs+ 4U"q/])b/ X]6ug#tt:5&rtAQAq.j  F#|$C_6a/m= G}fLu7r;!q*Qq*H$ܧ8+L9/l`13 ܍?A8͘#a" kA28S&J4V/X^`b^avDBs/81_޸ш8k. _L D'2>鐙ѴzSdy!#!ʔ!ѓIX$k6n?Kٺk:6|]^Sy੼ʭʱ Z ]CAˑ1wyDjg:p IHN~<]}  -1|*酊I>ʪQJN'՞N-@ ތ*oVх:^!ћ1f0)))Y޿a` $@ 6rBqbFࠎEăAE']I0ˆIyzfvDuz? q9o}b'hYwov)F2aa!8,/nK_]ǿ2I`ȀAX!SA^ aq%; sA$ _RA{$BC '2tЀX%*lfDK C ѥqH$0-̂IO2n9Ixؼ-UF7.X3bf-_/xq1"|;"adIP%ͳ`Yd&$'PĐWd].EP"0_*aH7 l&K^җf0%R+iP11L01AH<Ctf6Mnvӛ߄9NrӜ C#sӝg<9OzӞg>O~ӟh@:PԠEhBP6ԡhD%:QVԢhF5QvԣiHE:RԤ'EiJURԥ/iLe:SԦ7iNuSԧ?jP:TըGEjRT6թOjT:UVժWjVUvի_kX:VլgEkZպVխok\:Wծwk^Wկl`;XְElbX6ֱ(.!;YVֲ,Z9Rr?bfE;ڪ%rxu#ie;[r @[Skp$Zַ)b&l)ZId\Z V׺>`w]vצ J'B/\A]9Jl2׾($R׿p<`Fp`7p% iJy`R2$T@&:XAD6~;v(D HC>+\ k Q X!)m{) B(z)@헀9IHk0!}=z]2c dA`mxI.`1HXAlm \KpLjw8 擯\TȭmE1yr#-Nͥ՘S\<Ӝޒ "IAb'BqΙ˖>VĤ^O|hÉE"mo_kDQR tY3!D;?y#A; *G[Gd1(qdۛ~_Rz(A.yd썐$]";xJ^AQ R"EEK!K/|^ %B/e`i_*bE(:Cw x4+m?P?H,ࣴV3$ mX ̾6`AhA0At?JS Rѕcl#ZٽۗCJp',y,; ;Ȉ ;w13*>@6SCs@ ú@ zk2>!˳%!3bB"kȺx汼ȸȎx_ >ėD lDØP>M< A>h-\{rĩᯂ(#BIؑI0JTDx@dp UH\A $` Ď/o$Cn>oStn6;_;iøy4FG ~ lL 58k"A2i H k!ZFg692Z,;e#f{`ɈyZ`F}$ t= œHI}y6`t#@%4iP&e8ɏA^9"-+: ӵ!,˵d˶t˷˸˹˺˻˼˽˾˿$4DTdtDŽȔɤʴ$4DTdtלJ5aۤCK5!,-TΙ K-Y ;x-ΎFM޺L @.)*xS̨诮<+c'9B\P 85p ݩTu 6)1;_z1'\ u 1# !$k|2d#]j`ȨΎKxRR|(,4C /7[3B3=É6L@|)AZ.$<˂0GÌ<Tĉ>LÉM#OkD 5_2ӔFv˗}tS@ҩH`{9(US04l LU? lYs7qOs#osUŌukՃU_7î'|#}V7khGغ@B@аIc,.p8q k8x׃Ǔ9`ե9~ u5 eթ 386I: dᥧrOP8Gt jl'Hc4[@Q %]X8e;MRtUXr뻰;E `C~=YYJϓx<̨tť;>D0?s CcS&H [@ @\ՠUs[@T}սYeݢ Đ\CI[Y,A]Z$޺(݂Npt {Q Bᔫ̃#8JQBuWZW\S0f0VTSFUYES]_ͶWVk76bHo&e&fm!LjV@sV!Vo 8m`C8A&F!* 7;ߥhp אkn8~WX Dʼn  E`hآ3#PXH:# DZ{n5;U<}>m#YYF-] 5EY(xZ֓AZڿӋ50'aL/ܳmŝmY?@ޥ\\I^=h^u-@4]K]+D]ݓu]fot.vOaa?h<^Ax?| A!޼4˜ގs[*UADO=cZw@-[i4,h6_8D9\Z7n>BC`"#Ft h@vx861ᮋQK<r-ޓ(Mmlj%k垊Kt^x]|. 8oyEtFFh 'GstGA]r#Gs@rSv486.EY:yeOk ee WaRU'7GWgw,h „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*Wl%J`Ҭi&Μ:w'РBt0ѤJ2m)ԨRRiiլZr+ذbN*(L/Qk.޼zkhxh;1Ȓ'SيDxk3ТG.]rJP`55زg.Cm~Hh6‡/n̠t09ҧkt\Dw l Ǔ/ cp2ӯo>? 8 x * : J8!Zx!j!z!!H3ΨDѤB]҇Rˍ8#J4*YsaZ`oSFָ$ 6&@K35KX*II6%SS] AP[AsfKeDreYie VC9hBtNO^.PAw ؝-R栥:V+ b2=a]Cu@9Ad%RdQ*,d>d"J䦕!%AA4 IP:tg),CeZ"}z !1ddpB4 ;W6AڨOtD8KёHr"AAxV-\CB DD'VYjiP AEpA?Ȑu2U$47Ec3D(& 1 4A\'FnWs :1\xV >8K;~;;; ?<<+<;>髿>>?????")< [-`( T`#"{ OnuD.MbȟA !p3RB!'ȭ$ !CHB - rC~d9ADH p6 2"z`XQ$i,r+,*ĊăK!JAhk!TLc,Fx\TrDŽ c- "ȱI2:dXEM9!% rȇdr [ȑ>6cm E!)!z+2.2"YD~YZAxX*D !@cc%9,r6hNaE&Q fD0D K9"NgdEIAĝ)5)k#H)M%Dܟ駃Lm!ˈڐ ʢBpQ!*HCQ!%GyKM 9)B4_ [SM$,e yD!w*fm KUPTD $HV)޴7\FV*L!j E*.ĠtX<rWu/)Tؾ6@ -,b2},d#+RL$e3K8EUW?+ђS+MZv v}-lc+Ҷ}wyz诼-p+=.r2}.t+R+RN M^'ݦl"KuK [JN/UHPÙNRK` Dv @X!62-RE1bl!"HnMTa;$ )zrm\_ҠVȯRT Emo㦮"[$'Bn޼SqD j9%mR$ _ATtkvWH2惰z!H>gO03m- d 1w҇xW@'%/ 4 倕ܸ:-fH K&E+1]ڼ O(xp$A%o41 {Ef!FK&H)N\#䩱3D5>)̄ (DL o>-ݚmvB$k.WE#r{ې?h>mƚ}}/??ӯ?/ӿ??  &. 6> FN VFGvz$Ylj= EA LK BH]yT LYF`M]& ݕEĬܹ4A@p"ZD..ZW {YCC|S DFV1NDbKJ, Y_Y5#je2qSE0ف @"ACJx ދ)DEA_F|-% (q"CD"@V,R(bADbO ) 9 I<MݞCP$>#5DI4*DGfM%DY!A5!.K8vmVCd 5D! Z(A]DAA!:]B؝CWBG X9D .T@d M? DH@ DcGB 2Da\E HQZB( ۢ‘=:DTV*ApAMUeIhGIR"%Y#ٕyT@yD*PB IR$J,\D@ܥ2nAbDBZeS2dPAeFbSCeCIdYu,!PIxE$RC Q%FdBڐER/@AfffFD@!6]€&8" EJH:FAZ"VUDXL\TP'KBu'vcM{"E,gB줉)| D(cA,g@ZlRMD[XDxh09.^\F0D@(Bx2U^!!ސn谁[BDRܰX&%P|<[iD˷!D2'%RrĕNAR2vET)C$y&C` XQ)}bE.aC ELI*D:6E*C>=X;#HaECp"EvB81A(*=H Qf ĝ[CjnD> bC#B*yYVjV~G<1jLJfTD=fTOCfjVcB '.>DH=Idr꼊čVMd(n6џ]>\D4KhE+Ș+MBNz Al(2lvWGTiDD (JwBXFqTn CMB| r].L˒įamךl D ? Ѫ 1&6qFOFm,F}^C Ŀb*" mצDL ޭFK F+B {6kC  %.L(TMT5Qyc &s S)~%UEV)ZADv.^<%@H^DFNDpCBvY H$8C׵G."E fyab&)MzD F r=N/EVDM*nZ.eaD6iBո D1-]Ȯŕ*-?G$Bs4+yIEoX'-]pA80J@GrLD K&vc CFqJH2FOCE_ W_1go1w$g pKIqǖ(*F""D1V@$ip!e#Ep~_Z& %AKe!Ö? eBb`hpQ,B(hc\iH6ω( ĒJė:[l*eAl{-ϖ*/S+b"/h0'V֪@l ͬ&$m4JeUR΅3o LFDwDsqBt3D>@4AA4B'B/4CgEHԄ oAlN궔GGWpCƍ(JI0ʎ}aDl6C2\Hp L ]DBMPax%CdgYv1CUtGFKDu@u}Y@PwO~kXkK}7^CD~[FHi #J xCLȪM~AƒDeׄ'/jD|gSUV DbĂ3'?j2|J*$V-0*??B|P2Ӟ뇼ؓp+?o?5ҩsfgw\,.WBi{A8?E?@8@&T8Bh^8xA L$L( I'QTaF;~iB*q9wNĐPF *tiSOF:jUW ģ@L08:lV YrLh٠RaZpҍجޑVnBVqcǏ!G* -̬8? 9˝6 9Ȅv໰c٫qֽwo߿>xqǑ'WysϡG>zuױg׾{w?|yѧW}{Ǘ?~}׿ P ,LP0A0, 9  QDQLQY\j^lQi@  m R! Ȏ,R3 D'Ύ+$-h)1(Cj|9锪S=$?˚t:b4>T҉0ȯJ Lҁ8L% USΜTUPUau<<ՠPu \RKVxE(_EȁE6dbم=ɜjrfKuo)$F"!q{ ]1 ^k_RK5p !}ߥͫ)oA _ f5NK bm9AYI.QNYYnaYiqYy矁Z衉.裑NZ饙n駡Zꩩ꫱Z뭹[.N[n[[\ gQ/\}qas)`pFNqtjɪR9p*uVߩ:(fOI(}z)q[iZWFW^!tfGp`=!C|'r?t9@!~ky(a^3N=)w@(7dL?)DD @AlP ̫:lmD %[ >&w@ A"" I`x'D5|G90y>p}Trc Bvq 9Y rG4c{1@,)2z!@ [B Ar,[ duOш@gQJ@jyB.%BhU/%JX1LT V0әl TRLS!H%CiI(x&><;N|$Ah >RϝS"@@ҡ`.x@ q)C JOKH)b QT 1R^x\L 2S Sozq@:JHy q=AZU@ABAAG WtY5Wa)ֱ!A Mj]ʊ­l*1 @$[¾ AY;@@!*'Jה9@m2[v"@ ![* Hqqa{)Bkv8|DtWt$рNͥbbp OEK'D}x='ľ^p“)2c#2?H4ݦȍ6󞣤e.:N.di@1bٸ_HR>Y=Hۃ$[<0?g{IBuMF~ O?H%k`z5QOqi+ӅoˇZD" #~ O=?F\oDNDvy{ ~J=($̧nԑe>@9p<k_9 `.9RO AA=N\G0~'DW H" prPnjƖP*k j J' IHP(АNС"0XnM Z0*Xo &+r!ɳi >.B $Pʂ xl"G y( p'p"lka! ÈBAB؜ \!Ny6LQ$: Hhp FF/!N NL(T+V PL1A,^!%`-0,f ='0!; '$q'±!˨!Zh!xDZ,8An*!pqa&bo bꑓ 's9adQ"4*Ga!c"c*"2+np"BrE !JAR%V!Z Rh!w%!7$t2.z2%~RFq@!f&>Zat)&**2+r+?&++CȲ,R,,-,ݒ-F-.-͒.F.78 $"% !*3!R1 * *`"1u ,!,h#B/7 B  bQ"<@ @5SB5Ys"p3!N0h@cr.)f N`|7uB0@33FfP"H3  Z5"6#,6!S.AF75ՠPt @TDX`* l`" ` `2 B'B ` @.JTKtDS~4H#bFkF?(IT δG]FHD4JQB`BJ)t MK/6 E JbRb t8*@t FI>@zTO5UB3tC ` 4KtTaX`Ec OJ :@ ԄA . ` F$nP&x NEtD $[u 8f`@Bff`__U ^=   !!̠ A\U L@@4 ~@6` Rve[e5_5bU^`va_cAVdVg!V` aw`@ dU]aBfh `kOj5k @6`8U`L6@J@ @`2;cCvd>_`$A oJ]^f z J Te]f <$v` 5bBE [l +` @ `d!(Lv^}v\`Y@ Sgt =[3y]tLb;#H` w}x AM` H@8f} ~Wy7'v w |{3`yIzԀ*!~7IEX ~7R"xbv+5nXa,""ڀd!` `X dx Cz@*B@4~!@ %4QuaWhS 8!"#^&S[x%xY` @X@A4<[3{Vg=/bWBx x}k o$ (W `Q6u`)w^wS 9!@ +z *2 @:wF@ @ @KzKOuz f(M 2 3ZBBݙ3;B`  ^9bk[ ({i{^#z[}[ؤ $S[q{ #[Y{A$ٽM ۆS JC\!\kE@[<ಧ׍$y&G;` Ƶ'&3q% 0|Wxi;!`\ƥ6D 0 l{ǁ5 Eg@pb @` '[ݙ%M 9,|˻\ gۼI`Ts 9Bnگ=H ̜z}} e¤}}CB RATڱ]Mb` *[~ p^GDXߟ%.AjQ;> ^ \~ Hڴ~ wR=]' 7ŗ=g'+q @w6t<':"F' [' >Up )?%*pv~!"QB$B:@B D.$\>A[& 8<=s4A @!_CT~<_.P?2t` .B|fXd@,a՟}4|92;8B<0… :|1ĉ+Z1ƍ;z2ȑ$#A8eɕ,[| 3̙ ڤ3Ν<{ 4À!, (*6!",QR[68CACMOPYtu{jkqabiqsxFGQegnbcklmsHJSIKT8:DMOX=?I46A̗+-8yz~stzBDNYZb}~24@z{/1@JTU^vw}nou57BMNWѕ{}13>KLUۻ:[\cEG?PSF68:xy~hl\^erӌ__`g-/:otSprwnaeMKMV')4Ԑa[{t:=<ĢívHKBd󈰷o˯]_fVYIrLPE|OzǨ϶f<=G}zuzVx|W;XxhlPjl_bLKOD359lpQdemeiO57:0275uNJr.J:]`K-C7SVDZ^F.P>_ӽ|YY]K@a>]d*78~YDhcJMB1^F],=5sxRBdLuD7~QӼGmHoFk2hJDfMv;j!{UZR M-.0ἧV<[JHA *DP`6Q 0DC04,IbË Q! D+J$JN QU&q&PRdaOXB `Ɣ44LY{@;ΆAJX4@Bƅa߿F(x†+^̸ǐ#KLet)xи[t8B $ p BI 8P"8l7``ہ* xC(𠱺kX!:c/9%l~oF˟Y1vR0р]^o0%M@P4guGVegյP~ fvae ($h(jXTA Jԙ/ ?X1 De 9Ba |Os@ IAU(Ќ5.ffuF I6EZհ@E|1>XIDCLD%0!\E!`hWrZ٥*ꨤj}m Fvx4HDO ?͑D  I TܠRjPD ?dgJ2E9Д?mѕIiB P$4,2,ݚ;.E(AD ?l   -Q$P8C E<&qڋPKWABK@ Ӏ@ @m`  m (\弤%iD΋^ŭ rݝ2 ƨAYf*tm M jxFXk K/@LD.RYCF0JT `;xk @{.@< 2 3N|wPr */ALβj@YצR3- QdHV勒)[y`2h`gB RE5xi @WLF,@ H"&кlmR\tAt䃈4a@! C ;7Y'̯AlʰAj7r -0!~QHD0AbЊX 6 2!{ PA "I@;Ew8Zh;;ѾpO +a إA "#\pݟ0ZBP7` (ta~hE@:o AI9a /;,Dիճy:-~] ։Fa Tqˎ!/i<#4p5  ;Dl^B-\@XYH 0臚 (PeQw*rٶ[N(f_o F_koF9^^O|Jx^` 8` B$sDR  TN? 0S.pn dȯGm8 7_VwX@ isRW$6G~W~ }avrFсP75u}.r0 rw!avݗ3)hDcd†JDŽ6ֆBD7{{PVTuZ5;CG!w{OE$pp{iCŗz(R'pC@#v#r!Nq`54H`1 ؉0_Vr0CHw(=d5u"E"BtpH!넇LJxXPYQ (w5ֈ^ȌH$*Ȍ8X*Mb긎wsy؋)XX؏8y ِ9Yyّ "9$Y&y(*,ْ.96:5o;ATk cJr!qg>1O7n5EO!Tg`sfjPJ54)6ٍHe"lL!x 0*@tuH~:%AV/ bHK^1K6!CE!Rg&}a_ :B ]tY9Y؋sa& bO̩"ٗaDYF%Hi#6TCR$8$A@v\+0]0kFq~i%El(]F*I%D1[ ( ӚU2B]YA('(rK4(]ez`F%Oy}/!**+B+a0+2rb,R3m[]vS"){1g-mzB.~0K{r/"0i 29QsVA 11 #2$c2 * +] 373 5=3@#4@4FSJ$MP0z)J{PHoupsNO7eV o*1x)ECtOY:Czq$ЙN){7 z7~8C8#8CKs@s99=)k;;: XJ ֥ɟ :ɳ<<#@ӓ:9+8ʤ7>ޥS9?C???JIĭ! VtDAb "t4BDB&ĩ B{CZj8eh)'CFˬZOTfo)*CZI˵C[_Co^؆YERdcE`EE$J^KLc @;)HwC5j A )^j@(I$ITI%:IwJM5JLEaNTvTDKKKI@.-Zq L|M aMؤMMӔ NPY ÚoZMKp@+֖EXB[{'ٴvo%jӾ\X(JZ[[De, PQUQ uVQv&FRREESS>'_02+:=R/\SʪTtE+SUUUN9-fYCrb8k髸pg͊ɬ ܈=pu)p`xN[)ns\QF af; jfYfq6gSCjV#V0D<˪Qh!ijh5H;iiPw _wj+GӬj!kAFQk kGsӼfv6b~h] jlX^vzmVQnnn0{);jyI6 k!o j[Զw=}{L[od:$ݝ}pgqqqw%Wd#K?t- b0<@`N,`ttyKt@d!Ygv yuT5 gvhvko#Awtz$}7xexpxxGuEQA QysѢyyz%Kze mb۽y{.)NC ? 1MR;П5eyA&=il[6~OKn,Ǘ||}ubP}ח} ~q2W'4@9Q @4T 鴽_ 8ۻV$jjjb/w!8Iae^p_-'4! @,Chg8XwD~V(Q8Ua؅BAt~N2ik PKH ^خZ[ݛN!͵.-XSW~w83TXbhקr+MxnH7 r3$ oxh!&* $ X A9>KNH[ȎX7UϑH?^D]L8Ydjlnpr?t_vxz|~|SDp6 B6 Guv3QD 'M q"(06gsseIfk!T$#w)Ld%r"B;`Y$`_. A[L QYeI'QSHi_QfY{ү$ A/ MQeD CPBlHPU$Q!68 8nĐBO$^ I  c4jFQMRTxq\8Bjež :EVZiǦ x!EI4q +j(\ƒ"$QWaE(A` {@W PW:0pQ O-T:s[Y`9Zípt%~-!j|%] ҳP6Ɵ_`./`$)&` tAY L9@!B"K <Щt &\h%ZQ(A@#" kO 7rʯs^ jc ?[ B=tGN4/Jdgb K'haZ’-mSB/{ ?Z0a_6|ғ>%TS+(bh$(0vA!$a {& #,UoW'RkȀLhVUt]Tu#S5{NvC\ruf3 fJ(p b8# ^,p X :X!*8s"hIs!Ju;Nn%R YP@xA)5Zy!V3j"YUa\imRxֆrfVhvfg b[  ~X茠…cjJmmn_4jX" "_T*Z"J͠+9"hp!t]ȈAL2LXSYnP!62o9{(| B6A` c褅bp"p@(#+nPAaT>$}h젰!S~PZ?8vAl@ ] tt*&HWą0,d* '}_~@D&@5Q")lIPp!yR)0 LHQ!|tIl1r^@Б!{З!3P L`>Ѯ H@*l.G2Udg BT1,u A!-,taP =8!Wx;2IMvB 8 FT K @(EJ(\rz`ǰy8 DńR0L&5I jd1TGEP!77Ww hS12$E@>]<%C r(:U< OX3Ah yiֳt?<:z ZNAXiUp  խ*0=H K kxPh &@`b~;4~`UEKSH<V8@)Ah QDaOݩR_SX DKZ ]5BծpG2ֱZlc++_Ҕ8PHtKBXCkx3;=J8JV}TtHQ` B3 a@q@tHB Fր 'PnB@A*]! QzMɔaP *f I`١y$ŀ pA p 2K @N S!S"JǼ r^S@!Um6VgfYK)Sy!3= $+ٲq# S@ <P!U@x\ cz!)d+ 65>s#Q PZ"qp La>]jILumP+8Nvp-;xcT kx@P`{:Q]lu!5_5\" H 鯠RAK.;J'WhAP98k,阀 B qt`V[ X#ersbnN@_Qd&9C@.S +FR(a'yտޙIC* 5!#/L{b\ׯ~Pau* # [@dnIAZ9R!`M"N'd!@nLv/f@(:-I%p"h0 p@5%N>I>Z3o 였.…8C>\E^lȇtLdjʧʩȴ\.z4J$h2Y؄ ?0\$gGQIL+KFx<ûz1ĵ˼ -[njɬ%4yú, xր/4DQ %p #Pwyѕ~C n| &Ocg\ P}IP 8I( +<.y6($`OLE+хh YP *X-pu189 mǺ& %uDQ mG8UEڀ PiPO: mR,j0$R !UXR=xc7-/5R҅vT9;RI h76p@;/-$ h]Ҝ4dNtL(Pخ8cP`з!;Y+{N/SUDAO*Ո>TNNzbYUeULSUeUpUO'0ax6"P3'p=͍|W3 6IT8SEQiInIU>WSN4A`=Qv}W1d Yt[)̀sNb4Ȁ|*؉CR9f-0SUO؁1b -d'O PPx80-) EAhKBP!khUQv<Հ]iͨMǷu =D)]J"ʕ2萳 SDr,[0KeϫmFbs9]T*TDЅ)`^h0P=1m> َb]>9(6sG)N4UE@Тc,/ְ!S[):E$T[fu]ؕ26h*KE0+#0@@` D4n[ð#c@vX0aH1) PKa&a}l ,b% >N`ȜUh(h90b+"j- + %CQGN)V9 "P2O `)WdMV[+Ў[[3h (LIHHȈ庨ecIbgfhe2IYNKfqP3rf/"Jt.MF7yL2=Re 6UȜqfsKVg}xZ&&pX萉p+grhN9X$6;@+^v~g;"@P/@#EchN )%qi& Ў^<@!=Ip%h֯bp.x@Pb`(2&nj,9d]hV>hޯNjBVj]ҴP"X2]Y"%Em]m?WfjkU'& fm'N]Yn؂X*cXϸM3@/pw%Wnf P% ZB6ϺX!#i0|X[Kx cJH (., ٙ"x%xp/amu3/"W񅀁fh/JHr7$,O-*Wr\p \9@0 -$7}S2]0 Xs+W@?q"hP3"Xt%hw%%*_FoⳜA1UH%o„ᣘ=4׈:t{5#hHOPkt1rOhH]=g>Wt.]@>V]_c@tFwtIEefW[/oZkv?a72Itd%H%$1(ek<_A&u'pƢ=YށWWx/3JD`x]+8hAdG?_*8_~qT F'zJzgOt4~{z{_<+(S|xgG38?b"zY@ʟf3% (`!Oh|wo{}o US?N7~? E~zW Xv~ Wi+_X 0H0C,h „ 2l!Ĉ'RH ƌU) G5j8ѣȔ)P0-_ҬiC8w'-.4G $jƜ )T:Rj*A Zj$Ȟ\zj,ڴjײm-ܸrҭk.޼z/.l0Ċ3n1Ȓ'Sl2̚7s3ТG.m4ԪW [,J`6073掠 .`pQ $t@qܸw!7o굧7`ih}%ԃk * <`>TԐGÔbImqwA@ !TJ Db+*(,ΡBP<@DaцviA mAEMۭ'_Aj,a`_ @^ !0@3"Ggiԡ @.Q=Z#" +2(z>Z"2N6#x1TU"u F ` DvִMKkᎴI^$SPp v..[^ 7P ""fc: G";#:.rD?|DŽp!A{ `BOg q=ÂT =cy pU op"~QOhcA8TByȃ-3g 4D >HW N3eoəД @Xu@ӝ<J[D =yV=[@AwA~ 2 MB ml|A X@A ȁy 2@ ʠ@@je@p(@m!ɚA a)å(]u0B-=A%Ѐ%A%[ ERM C"  1 @d H 䜟X9 D<& ѕx)ZD%bƢ,5Þ9 NJ.aDT$0ne8ZJ,.#3Y/.0B\pM.IdI36T47~#8KPDV8:#;;#<<#=֣=#>>#??#@o AoAoX=9`5ttA~\"ٜ4  yI,FE< sG~l %UTFU%OQMRF$MS~Zrde\C H8 I 0Ċ)4e"Mȕ HA8vY昔 Dlc:I&lA`m\\OM8ePTV&FBQ.e[Pz0%rBlqfGFnfISu|`Y~ewDt>wYvbFԴʫ\w_@<@0[e K|AI[B|p |((AN8:ľ ^  @K`˵pdGw找DomץtVgDhwZP恶u Qgwv }Hbo:zUOn%e"hu'kYS*if^6FH DlL|̗ȔL$DxH# @z5M @a}pgE,\Nش`ٔiۘ GδM߀Qf ^F,N@ yB.,j.bno1"/wgpznjkvqJ&J+֮k31(Y - UA,!aT@UVȉ/_Z\ѕ@І DZBI_Տz`?Mn5G<@t,#DG[hijGlX.7(!mV(RB&pJ WqG(WyDsyD:d}>'w8+Bnn).3-~;oV//>)(_<Og&ksA/TEg6. >wyx 2AWW5@{|ɒDlဩYð _>-G' LB\Xa؆Z*{;DhM6GAِs u x5GHiS8>-$A[^{DřqWq1oG;,:364<–h*g=tGcClnb_qBcg3F>d6qBqnp&gGotSSYBQ)rV1ZA`9z QiH | @P/D^7ڭSGuwm[A[͛MԄ,PFkP~d۶ 5G|&[iu}׉DݛAEG!(feo 4?s;&oCg%gen0t{1>vp#yxknnv1N9.7yXf]v.Eam\iO)əIwLA\G`mRlm%X1RMݔ/Ay\]]# %)5EIQ^E !:A w^l8I^ ޲D7zgpd嶳s+>^ g9[>DS En6Zq?_'Fûj4C鶖11Wrn+9;e i'G.箅y"8KWa,L'D_=D@D $ @PAϞwBy$OAPb Z A~]AA a`  !k5+aBHzd yFle;i\СC% JNG#:bCA0wⳀ"A@󣢞h,@+n:mC{8[s8Ķs?P#)k>@8`D`C 6D-^Ԉq!Í/f9dI \:eK/ kބΕ}ygPD=UTiSO92:U$ɢ:^u tj׮Z[,vDZUbVHٖ`ʵD}/a/V\bǏEre:JsgϟA=tiӧQVukׯaǖ=vm۷qֽwo߿>xqǑ'WysϡG>zuױggQC"Gl U:|p aB H-=aQ8jȀ*RŀA/>0耠 ɧz:l' ,):)3q,na(k lX#C h?=((5 4HHB h yBH0"?Xtѡ'>*+T@A"F$a8_lM5 Ғ =QS C N)3SP2`FNϊ-´MKaJd#, ^B&0A` H%D#CxHȄD ֠"Ph=hA4J@ @p@` <*h *$*4D:a"Xև;K׮B5,5&q6agYx0X`]4cA&*pA-P(0(e-h袏  ZaVJz`@HHI<""]n(뭻c/RYg1ߠ7"iқo:$oYF7nY ZXک_jv{y[0au` rd|!_M+m"A'RvXBi b Dc^X$UH/b$!Qа) V:Є2!7Gz@B5F,6 HRD@Q=,pDX]20 P:A $:lm06h *@16Y,,9DOx@?!d$ҷ>~\ i RdJXFxE"O (bG *-*#bAMfxi&`)@tysP8n H#bQ d##x#!;d`ePo J# _(- a ڐiEc`"H&UPp(`p1  NA (̠r 09$ 7)pK61@RS$5؀`1! Z@BOXH`!Vݘ@ΚVh@ T@M)a )NuSUjU ZO1֡HUb}4Y dE&#@H$~rZUHP*5-&pZ@WMmP$ Z:w  ܂P@&0NI6 "j;D$:~Q@XdW > ppxWQ u/| zv%t p0!pp9Ou^ B];ͷ ₵`mApq@8*eBw@.c|@< CL8&Zre؁$2NJ2QP"hn E\#X@M $N/4 :$ fg L m+ !@ %p0 "*AM XK!"! +! Ѐb ,"#^MS&l ` jJv@~`_a`؎`[H<Ie'hJH$ 4qU*r,kD`(`!:`0Z,X ALqF(L z " i"M S2B%v^|+~@t@ #= @` F f*/L@ n`+X % 0DEt}b( |`.%c @Bd ȣ< aczGԋ  <"`>'HT+YES'MLtޚo >4DG 4AG`6@NmtDjj RFi OOt eRoNo L BSA22^ 6O $RR BO7@^ `5 H'N2"`L`%B'tL`L`oV @@^ E, 3Jd6f@>'YA4h02ZI bT?5T]X\ŕ\E2m_uZ B ~ tjfLN,eec6U&R_6I`_ ?_eVgb b`/PdEdMv0܀ O 3a`<<= `A`Uӡ<ג"gh Af (RQymA%!Uۊ@04wsC*(" T@ 0=[D#! (@s4 tMu"tŠt!p@f-W` jt `ъxx1@ob  ؇X;!$8uk7vN n  jfF6?w1-B{yXX!,y 0:t"n%0;-j #"< !ܶ@m*| *1+Vz-B' H*q _ȑ~ 7rE+0v:,`tU:!5u3`uyuE 4^ rM"!"4BfV! JG bV!rt) ԤB~;~yXj _~ `xH) Rzr"rh b VQ!` _ta  `u F` {@nZV~rZ ^T `дzyh3Ǚ H֍56:[1ixb;'IO5;yi{BD@*۠/Ap!4a,y[ ㎬;6 y "zz*$`_"ǻJ!  !h M `|~! X@ ߖb!  l|D@H<@<;@]oe``Z3oB|P!%h\eC 歡.1@ Zuc>t R?`|Ε}(" !& rBz)T`="#tb@\{xLW \tgדo w&  `|ܫX2}j n@<˄W!]=҉ fݻf\rp "ل:_D e`!2GH= 7 `^oG`v/ :It^ 0H: #y& Ƙ۽ vzܳ dH\5B~\4F#==Y C`ȷۿU@T`R  tBr~{&X {_X݁A Mƣix 6^cp! @ @~a>A0!* l!Й ,Wrk n ]Y't &%畲Iv .@ ==p DB "͹(^88' r8!N E  `!& <& oZBD.cCatrC<z05 +х$ġՄXPbv(*0W'd" E qd,Ȭp^[M=aQ:l)61gW#F7#9 ͈D*"!Xb\cFKw\7D"щ͂HHGQe*EiIRҔ^\%_IfcVxL\ !?Jqud+XQ&AyMYЎ,9idv;>$:YJ||2lIł^rfOΈJt  IusQl#jq<(A9J,S e%cGV,3[QRt<,ƔP2]R3/=O[b|5RhҤCj2oIәֲ%YϊV}ӦJ$Tuȣ&UejO 3 JYվꕯ+HjՍ@jZ Ȓ˥$8qPOBYϖЦӼaI>#RAPmUl%֚k\TF [YWҴUݪ`%.iZztn ^frEMx+'7}]o]I[ŷ᭯}#;_ޱ=t} ryހ+x-+ Kx/ kx? x$.}6CX*Q76ԗ(ae(^kc5)Qi^w)\qPRSR_oicTիd;䧊ٹ^f0, .7mbWJҗ^;7)8 a8a~;`0$< hxP:@$ 8,BUnbasnu˼_#[VXCϙM1Zm]U1L]^I#Wnk}%oCOe L߻6_!\ە3@1E0 8Pz kQ?pZ 6N/Wjr6i]dV\O;7r'T5fv5S_S5v,uYtZ/{Mf]r R8!q1P,Pp #5h$%Vm/(mut¥Sk|eQKe|eXcGdhE~_gNbaG|Xs3v\hOzT}|rE)fZ'S5ąMdN\I>(jg'f7̇o]PXx-gI 0mPxk9?0<-08]x&gW('ih^HW6k[nG.S3suT7s6I؍Htv5d#օJȑV*~VkhwEx}7hGF'lW|5 @RP)p!8ш KP *#:p +5K釂G5D`&)|XO\ك7n҇XÈeFyZ{IDUY|Y~V2xoy}Q n[ys֗RxdMe}+8HV)u(9;p2`H g pS^R}w\~Gy "oPvXv{߆ja{IhEg]EŞUYcSGu}؋9CGHOG}:rEȆq7 %?,l`t%h[߷'Zee6ɟ؞c6AhkrP" 'dah_` cȤ*Yug!^:|RteWJؕe_|-iitJɣX5 LJ|)){lڕ)~v%*Y+vu0e3490t=敦=$d)2f%E&Y SWZTZljg檩%Elo5_7>sb765܊^eDejE?zA#>V;>*hjc bhNf?Uߊ ˰  +Kk˱ 3 0 5$(p` g ,3!@(n  @-=nCK;> 5MK5o)9[:7rP+P`Tq6@:z2f@sK[3(!gg f; wcK t[@sk<ҷ89mG{@~۹!@4` 7 @ RR( gSy3q z ~ )i1P @3ȫri Ы9{>ѐ y@ݑ>üѽR p9(+*ۻk++YZN( "`h&RZrP`&0 8:81@,r % WE`$D L s(P8<@ |X1s*;4D t 9ŐNcČB^c¾  pм Jsi,*U>^@v1k7 q(tŷR`Ί e |ڐ |8 p ѐ Pq޵;@.Њ 0 08Mb @` b|}>(8Q }^p P `^x8q0r ~ Ȑ p5~9L:^0Om R  p PFJ>~ppY $~:8 ^\G'~c  (8a@p7`ڌgQ8RQ&pí`س ^oQ3 FÇͺ-}? p(@}m! `:#k2*9) p^P Pԫ5R*@>^S~ (pѐW.I~ ;*@@? .1:ql~@ C*@6any@ >A_+5a;?0s8qk@)0!0g(<M:! 발*%p7q@)y9ʯ~鑁Q5,o;\n&m l{Ξ?p(# ގQ@3 2B`1q^iNN wJ3_2`՟*4@$IONhǟ~K8  ,Ni p wb [~J%ChUOqHPƎz4o29Ō|xC9!Khh E@1 YaT&BdH\ա ^ĀMq8h)H8,0㐃A!D< U /Dx̌63,lfXlrdgg@h?sѥO^uٵo]9gN#5L5s(+bĠQ_Vb8N}Cj 2,#@;h$p [E$ *i)dO*EPr<"aA棆tA*y$7F"8HnrLLrJ)c"/Q@e<СHER)+r2R22H%K R% e/˯XąȀIR ' C&(a !Fc 5BUV]H: JB50xkpvZlv[no /Yhi{/bұhjhK7iw'oPC8G"ڷ Xd8DQ݇Q/)z8c|ù3d|e$.SrȏO"ϊm١/@bIC);:QbbF!JA¤6T:#$f06ZWv@@Q8 ӳ~NKbWXfM0@ +`^{ ͷ`SW}u[V|9eEf\k]3y:HTdNV^.C7Re1ڃ7pQ`YQT<@!z/P_ ɟ@%/4P50 PLH5:$a)hA ~CؾoF*-XGЀ| /A_< 0j _hVj]9AHxs""XC+``S8!nPE<t!: Ƚl0X Q !;A_  $yk^sF|AEHsP E!T4\ %l|9Ҁ ZdќCG/6͚'=򱈔!xV3&MhWhe( PY +iJ! IU&*C~J"%ilNiRSor+"V"uZRŔG(ײ0/80` C%,r$R$h@ >H{!pW Q_* p  &` x $sA2ٜWVemk o(C:CaLou A%aI|:.R4C֕.u;f +}v[Np%1%"!>Ǽ=K4؁I$X-wK\GX51. gX!2.#"C(ĭ#D6jf$X;q}cle +t.@-#$@fr+A@ACB\eUp zre0Yc&s|h@kVf8Yss|gZ>ZIwYN+'{}xxo~ǿ̩wa@]XK4D!ASn-9˷-y៞7}{`||}y\6^|e\ŝy?>Uz:?_:/C(.^@?k? h[#sD䃼 Ď>D;?7d@\K4üx8CA˸x[><88;j-)0$`[k4 I͓?ľ㫼{A+A/>#i[=`k9Á 1>ҳd=69ATAE@44=C7@$3")8A25 h+ #U`EOs8%P 8F9_9#D "@.L:|dDJ\@4@hdFdHF5CkF_7IB@AB˹`58,GS@F:J 3-5O Q5 P9$P<H(8-]$#$D?9GulG}<5 HPHR|KV[Hx0"` #x*ɚIqF*AÊkdIɨLlI<LuDGJsKkǞ9f<| QPTQ MIKnjLQ1TM<{ =d6xn FC83O{ ?FZMp46UXRP>},I>/,ǼF|Jm=G80UC:Q>L@@PR #NOUOC>ldCT)؀ Qh/Cb %g0chfT><Q%Uzsl UTtRjNlTULrJ,Lo<^B5JV|QVqҹ[Tu]vm/ 5 0*x '0X<LKi-@w(Ap TX1q/Xqp'R >jCkqw(?d#Gr%,OrJN [`R`@Kr*gqs2o Oqk`U[r*)oq2t8w H$8fr$0hfnf`Ulx#3( -*u ݇ƫZq@?A0pv^fp{rHz8DPf1}~j L_ fPȅ^(wRC`x'(ߊnhH<E`/x~qWv8z*y Zpy0e7xp4(IWH&yQ4 Ѓ98n5p+0PAH!h`6 KwhЀH8PŇf"3ƕ{b_u3Xv׬wo ݁芯(7 LFIE?ȘXr`}\@ xHՉ&aO>XzXXWؑ4Ȉ[(W?`)AN@0!*^0T   "o "ZXTЗEt$˖,Rä*)ԣOh# 00LlAD ֲTR(CӀi߳^ACp^K&L)" DxC1 X&#(a Kpθ" .`,&E*.(Z80uAcAֳ)':qqc[ǂpC^92%t\ KYf,e+kٖ(G"Js A`:Fơ,KܠZv(֩T<$| ,qOֶE/~mkYBߦ&[\bHy.ka# k  ,zS0VcKL[Ӳ6/cc e~#qEEan]`D--ETmXT2I$ 1Kl8ŵʄ \ȇq0vɌUlcI% Ǫ0#9J^2'C9RLeH9Z2/9b3l39j^379r3l;9z3] hC#:ъ^4gUh:xt+mKc:tI=HsZӢ5Kmjʂ# P՞km[:פ9OSZկ5c#;nc av"]ec;޶i-_{Զvms;O=aݳm{;64lx?7 n;K=x8+T_[=p<"7<*_9[<29kn<:9{=B:ыn#=J_:ӛC=8@QZ TN5K>Ou)ۥp;N{W}qC\{O;#qx}';u=/j 0! =[s/k}^|hd~n^=|/vMk{O>d7 @/z_ہ?vYw) ^I)`~YB ЁO5PBdtB Pi%T@MN̝ E ^- ş!9!镛*aU _`Y![^_6`%`ѝN[؝h[N`>a*P xe YM@ B `t,Si4_A_]!!6a),y-",V!v]-_/Aaq"0a(['B>!i"cQѰ @hA'Fy"=jk&a~"&}[֢B"_B>܅>HnH2!94fd6A} !ĝ#ǥ#D@@L =~Z *!AR1e>1aHAR*DZeBc/ZdI:%nz%nXvbb]\ZKBP7 " dP ## @.(@{Y*&-%zd4) hR_] gdK '.Wrb>"aQ"Qi.%6jcC"`&%Iʟ1ofi6#`>QfbB $)@QR(^g_sezJ@'1&FBҦY|> f%t^_^eʼnib: o&k $Zҧt&4@`Albfe ttvAdݤ@Ԁh$kn{gJf֧}JA>J٥?:Jijz)h.(i)1J%[NDܖ~".0z(m|A XШ 4AM@b\1 sUԬ 6zi5Zh=n*Q2cs\*jj>Ƣ"b5ejD&c*iE~Ѫhm2ߥ  MhA&'i$)B)*p gQlvWj\kD".Qk%~剽kkokY⫹+i :IBRlZbljrlzǂl:4[StO7 r3s{w78u/wsD) 7x&TC4xwړh\2dIsw9Y3-0 B)B0 %8IxoCx8wKB,xuHR*-|9H]C|7tF$@0OAD| @H2\ ZurNA#^UDA!hT.`D'c0zeKE X6p 7qwq43܃D:im@o0D2`T! X޸myAg#;*O_ @& A+X$B.GCD{A|{dhn# DCD , (3;̸F$6DjI-9DLK4D`l{Bx{A<++/ɿϊb^( dD\v B @ r^@yA@ E  @#b ٰ0D8=K=*DXDaAAzAt=y?<ompvhLg`FqKI-AB*,~+Sz] @@\L HǓq;>2B,xyt- Ԃ|䟱iGD~>D A8Bo-h/D|  qLCDPdF Jt ?ӄMOPTR(S]KUXVVD A C&?"x1 $0$h E%RhTLH34ZDPFX6% n 2`P3 3D4ТjTSVzkV[vEsIF$9jjVPV6ňUq/A3Āt/o,CQ=@2,!.u u#h& AÈLeʕx1%c2Lwn S T+U;sJEԭNzA/hJcNۻo{pIY~H0&7z!24; :"/n($P@#UF+M8A\@ LH"%p|AEhUPk૛ 2\ aHAV{{L8 i-J`"y.Ů:ޑk(d ixJJRPM<*> @Ң В%-m9Y1#*4ׂ$-VK^&J5D%3IS ( XA<ԡ^e0MyjaE" *  L FP9aR)D\YL,\@$B HDSғ&:(Th#łKDpC^9эpS9RQT ox#1f1ip1 XgS` , nJZ >4/ B|^-"XU+m/&4X{hbH|Ta,bz0@#IH` gUl"= K"ڎunyk(OpHBK)8dZ /hC;)L"Qx[^UZ&༂B5J/M^FUD$UEX̠ V`?ފ(\a @9a!I\b)VYb1i\c9ycA\dId'?Qrl*WRe/K2Ae3iVW0d.3\7kg.Xs g1 ɆhG8΅F, Џ9 +r3CQgөVY ]PӦj[׹Nګ'-gYY]lc[%xVMZіoMh@[¾6+Mmo܎6t]nsVnw]o{o ^p WNEe%1 8[a{ܳ *>4ǥ hɱca/tQuv#~_~M/@ '` @B'Z@:g0f< j+>\"/lO(lOo{p6j~}-{-JX0SƍE0O (&`H ( */\`` j t<5PZLp0_Ȱ/ӶMYp3F0ptp010-P휮 ]p0%No A sK `u" Ɍ$ 0 ,`~txOpePo ocK0lw1K+1kN op0;1q䊮h KOq cZqԶ  X& qۮQg#R171 q`?/Ob0-7O%-< $w%QqpbHIT 0>@s: tA Zk:r)Sgp[#]( P"$k%7,d%Ӳ(ANQq(O/QCr 0 ")1&` ր F`*L"`!L 8K041cr i(R$1/21'Vr r7;}8/1: 0 6nS($QRٰ;E̠ dh V*/Ta. X("Bl&sB$F BҖXVJ"&yp $2L/L |`y @ z X` B ™ h 6@Y Z<,B#.!":Y*4"`Zy*bdFF8 ` ihIR"J Y2~]BcP<^=%yP+7^_ sn rU Z4Oٲߌ`&J " f?K꤮$l%1HgH/Q<Q `\Xu`q+dKO>q>UH3q2o0M;IoW$|c "֯XT Z <`F 91T@!Ȱ!A9@~` '1Zl8N-(AL;4P!?s@#q0@78 8CIt(0(Y6n(P@ddɓI $Wz4ȎCZHgRKpP wbƍFD e$!J_Q̹ϠCMӨS^ͺװc˞M۸;Ӡ" Hd@E)£,U9HB{$HX&2҆` 2&㐉CK~3%3A z@o0F2A8` A .p8evکԡ6̠KXZ/~8u cXBkxEbx|Cq lp)tigZ|tq8TA'A= C  ! u0AD8PtwA*)AE \LGE pW` Ѝ~z$&g"< AG|1HC:(aC$(C*Alj$ٮаw]@E +ϞʅT$MV&Є_jd^F]{ 7G,WqS -1x[ 0PeP]Ƶ E0$,w*8!>,p/3og;lD  N8D#L*1t^7s}5ls ת4]@< *  z n' x_p7F!h& FJ@~չ5/t8̡wH1!|2ڍ j- 3Y. jO a#I;Р0Z|ل`݉ ]b j1`P0UPq"4ʮV&ɾ\(= Ғ cj{mRQV8/qыߪZ̥.w34 x@51 n$Z" 2C3PȽ, `kڈ۔ \(A Rr,TSh b7< OyD .dHD4i`.ġJ$]G#Μ@'+zцp@ )ST Yp`fh2ywTJժZ6@` fɧ SR `[ #@Xf&0 C]:u(D^@%, /1LT}. R>$jh[jZ:v% He5vk-jBVo ֮x\ D]L!+;׿!gM+SȫMzu8 xՌ`櫚X᳡&CX0oV34u%|>0g(1C1/0C4 x-gHpgX8B:v|2jT%17UG Xl򱎧L*[9~ a2dxMe6pg|f9xγ>πMBЈNF;ѐ'MJ[Ҙδ7ia6:MRԔ цհg簬ˢw^xMb48 qf;оJEζ[3'@RMr{ v;QSpMN 9`zg44OX 'N[ϸ7{ GN(OW0gNs#8A\ " `V/1*n.7a@;]R5"F(! ^ _ xZf\WAg<`] A]r//~v(3FN7 ̐xONGW26McH@lwo b]=](@gz`_'H@R  <`l  $|0 N˜B0TZ / ;BZO` @@&7XB+Bӆx3?O{6dW 0&Td' `" ̇W +8| 4|07}G4 W7N-"hDKXXf"0z`{s < PWP|#Q&aKNZ@E @;WUw~&P6 )1x';YW`?wmP3 k Q]NC%p{o qikh`nRZӂ.(@=0H8eL84K6P2<NP4>9ӑ;]Zv*p,N-pS `&aFGzH-T w6`O0:S[;68q ٸ @1NPwQM-lj$r)Y;q(Y 2pCDFE@wa #-JQ) f !$ >HAm `BxWՌ4 )sp{g`E@H.` {w;5U2@DDGw%Rb袏w*qp~5,dB.]!1!@!6(UBB%m ٘8_BS 0b w yg |3<>եy O@_whc9M"S@?&"wA"&R/L3 8A @(D?010 x9dV ap A.M E!s)&2(Z0]FNk؅$IY' ; *"FM "h5kV+-sZN.B ` P `k+wr+w+r` IЏ)) q%4 +qD!PЪןڪDZ.0NOQ}d=BɒkBXd?`qj! kYuw=Ɉ2ZUHC&@ <Q8K)2CZE[a|`@@; 09n 00[)&0P&P 5RF|--2}]"~ `0gDwQDDn8`1I4HEa A6KPcff0khp ˉ 10rW?adb$jQpת$-`0* D,ڹĄ  3 105k `D@NT%E1%{!4Bj.2E+1 H b#dS>IT} A`B5@8#xrOC>3S6U;a еɾ+pRRhckbQD00 ?c>}!B$ M;=aؚlٷ ? tL@D:k -X+D  C` 4)9z8R@J{$2Hz[` k  GtD(|)BpJ᫑xJHZm0R`F ]N,z/r FH !4DFQ-`,V,XŞ`&)hJG&~^w.g.a訞ꪾ>^~븞ںCd>3LFȞSΎf>q>W~؎⼞^>~t>>^~?_NFcD^bX.$NZiʩDN@ft6.wHtas8sV R~g~dOc`O 1yQy1XCusC|60:z xAlITps>`-__V4 OehK` _do@ D@BP#-.Lxb)y `SPЀ+x!EIZ8 #3-~dp`d@E%-$ DXAKxD@%)$df Î-;@ZqXd*juk\bUDF $j) ~:7!hK6.`%j,pԣ-L, PK"=hp2JLB]=P+_< DyY\€́'gxV5s+~-zK TKp..ˮx0 S1#p)~R"p F$O :,IF+1RNja!(r \MH(h'2It)<0d(r 0sͨޤJ0_(;;䏇@x+JAK EPM,O'"3AK04@ꪨ"h `?f5-(b A@*:c02Z}5VXuV@nNRZTA `( R3q5zѢ(,aeuVWJ[<X`CHTQ Tۋ6D6Ro6j.`Ӄ?e]]]cxU M2@B5"ZkQDJp:.ice"sU:O| )A&‰Z(≅fATB9CZ`;o6ߎ{6 ;o7iTQ@˱. \suP X V[|s/ 77Ljro1}w|"&5610!$R!<;Svam x' O 1*`hRaY`A$BP TP @];J Vb7'Zx@*.J+ =8Aڲ0 Tmxi[~D5! %0F!猲Q#e4f@EU }@d^ e@"y "0Qa#,22 A`^ ('XAAXW $!BYdd {^0&,R)h: QTHZO G*]eA9"}II- `Ntnhp-l9G`Y#fm,r]~ky@ YȋglWC! `JA(І+t& #a! &0HK 0` ?Ѓ^PȔ6%J[R..aj@ S4  aWPW"6 ,fWիf> CAhD8(HKzLX*ŀ*(A@cbغjU,u)L#ԚB,bS k8fgY,FP@&"U UK T3Y`=\a8A0, *%£ ==@r!\ @ D)p,~5 p(LAIC^]*W p P 3l& T D/IQ{Ql@"  >@Tbq I{ ^Å0( 4vaP zj4@Hĭ21ֱ"qp)@E7?7IJ"c C(>^j䗾$YD  ij:f:e/.c"V@ aa)М (lmBj/dn ia @@0Wˠg0AZ7-T#hB FodzjHd&6xm*  ;t@!> `*B( uL qhCAG"*q[p(@ VaT\ nޛpB|"LbJ7 @h`!UҭB.u {!OzAp\ၐ@s ]a K?hJ YbZ( ^P h_7U@y<ā),_Vė "|!߻@%@nd}G@fɏlٻʻ̛!i+;j4`` f2 =:R =?G@_y Đ ' q?k-#ӿ#<0(#L…TBQ>k ShVHBH:ø@@ D3(>X3;*b"(P ;I,)B`DG$&%#'E DJ<-Hr*)A DSLUP'Y|; FV ;ć`Kā"ƙREl6ĂX'ITTEi A؀GQ,&=xx$DDnF4)6;k-hM*Z\z1<bH,ҫ0:X %0b $b1*0 Ax @;# 2@ & c32I[1{ʟT>HЊ,4<04C(@P|0?@?AwI5B؂ ˨̙+ȁHPaIIESXFPCi<,A)ˁG,9HP8έdIT0wGmBtٴ4JȄJ"\8{}LȁH,O.P;hxK%ضP@̺T̟^ @X̨P ,PA(D8DRd$P5L +<;o $eQZх0O 0*XQ 3J Z"=R$]>h"X*-1hg"=QM8QzS;Q* 4E,qS#vl]Q?}({iM<1/Sy S 3l>Ƞ%1 R r:xs%'1%S!. ?.T=Ԉ*1ۻ%'0%4OChִsu7%U<+L7(@0݃֊`5Ax) Dr=WpRN(yXTu._)uTڬWЅGSHӋA`؊S =xc=?[AV;UTVn im(B`=0πIMT؁xՁPڭOhȘЂMUÊhPIhٗٙPz[~׋0\*i I*)(|(%]K-((, UAh$E!0Mp&0D`  E1$: \G\+Lݶszʝ?U) m{ [GO00 =; 9ID. 2 IHL988* (-=AcJ&/@kN\= rʪ6hLIEX5e?PW0bB?j{ !~4>x _#%8(6&4^ +`=c]ۭW!!`pLx5w6Eo Q/|Z>6f:Z`P+]O")_*T`R> %H%&P%X56N?A.db%l09>?1]P҅QD .H`Ɏ0nn,"S>+P 3u0x D@_UAd{^|.`Ѕs;DfUFg gs i/oR?Wg职,N<w҅԰ IMu6U8eL.H!T Uձi.P/,f\rf`k8A0h(hJh~\v.xQ.|R"Ǟ?@pif`Sh3Ρ05(b?`.S}a%kM$hN()EE\nl/@D D`xca>j~a&h9hGzvDjjP(hCh<^2 o.0 I mvg.]Il>lI-MN-i4l^o~0` F4ue n O%hW )>. Ϲ X)f=-ԂX$g'(rU xs|̍HH t0 s3"%x%x\7ـ 71Bc /"+?o,᱑>_r0/?A6`s7s%HuNjs9(P]*ZuV.:%uRu+jՒU.ނ ? X԰6:gQMk P%y6P60́hw/`p.iZ Z%'hHז0uk'9bF uvl׳oASܺAooe/?yueQC$=[oqtMH!rXVfeBs$qЇd@[18z 6H}'"=a> 9$Ey$I*$M:$QJ9%UZy%Yj%]z%a9&ey&i&m&q9'uy'yck8, TAQkd1BY,WH0ơEVG@E:'*"ل 4A@ p$T rhYj,q]8 DCN0"0 dB"8=a R(qkB} 骻.{UCO :$TuaLi@@@ @\U 2 tRĻ!<2%/d5 4I3(` (poeNB%t@G[Z!N m @rcB'@1 @m6qrRgb A0:bD)CCDP!x7b KX'|Al^.0nA/IP ' A ?<; p =oBNdq &V4@X$: `6L& Ԑ+!~ Z@3H |ʌ' RB4 )& (r bBB" a b ϵv&0%P DBP@|a 2-r1O( HR8қ`m@߭ Qsu @o<1 ޓ]ػ䅙Y]iY) _"q\9!:"["~"^]b!w8Y"&Rę>3ԃ ;Cْ"$b^`4""[a9cb5"%D38= S=Xs$1#2&,pu-@A 48 GK vb'%oT=FtVE]MolQLTL pG@d1uL ʧ}ro(gD@ l`rx2HaJ '8@!@v *XaLPC6oXE9@Zba"M!uX)p^ic-Byf&撾:)%&!Eig.\ ҥ$V$6 %e _e>?r En@8&R!j6D0 @)JD T%p <$ LJ̪DYaDIndu=Nz2\tD|B9,BI yDNkLDȏT5T[DPpj*zxD^(@@ ӽ̾b(G dkG @A'HshBP[0)Ce~Ķ%#4oY"4fBih.bqbf^)F*)n5&i"b,-bi,`):m2^-R⥝僡#,oF_" P @7i @ 4D HX@x}&u'* G,,_uP|D6XbB n Rx겮ѷR>E@b-nR(YjRR^ZGQ.,RЀvH)1)_*fU*ilfB[rmީ%N-mic)v!ޚ^צ&*VmB-i兩D"|\9,PL4&DC:g0@AB@  j\R 8x2PK@@x(cÈb 4P. Bh,Dq lSRmSSD @( -@82,2RHDL P@L$RRHfeP<W~*QD<)-+o^eap͖'~i_/f^vb-Vp'p߬cb5se.)f-c ɪ>b2Ѓ,@_3İlNnp6cܑE37NܕBAItE46T`xDapꉻ{FP$I;b@kB fhU1Xh%[s7/<$>'/>7?>GO>Wh'\ff,^菾dpȡo=>ʜzΒ^gi#q>к/[pl 9/? {8Kpo?;7C7)7:%Ns?sIiΒ0_f?:vsc o:,@ @ D@ >tQB /b4cGA9dI'QTeK/aƔ9fM7qԹgO?kJlD&Mtь .}X1DCպkW_;lYgѦ 5![ᖣRDzWcQxqsxsϡG>Yձg׾{w?|yѧW}{Ǘ?~}׿ P nؤ#4at4.İ B X0; a ,5 idj3xQ3 (Ҁ3 2HH TPŽXl42J"ʑ(sLrI- zKPŠ9s<B(L:&(Ү#I A,x$SM95M;3SR==5TA-uJ5U F]5Xw5 P`E`=6ٸj(Zd եI)h%800UTew_ز-4^w~v ~EmY\Mw]u7^NR88_5cphRhi *:p ff٠ "xgl]K7A0f飓^zj鬯iakĎ:vk6D`zH NP`(ho\ 8`(jX/h0Pݢ7[3t*(,!$вj'::gpMbWTnffz߈?i(h Jh c 6d &' d  $`*@aD8@Dm@DPL`( `!F1tFn*.ail@(S`P)H%,֪",$ c8qĈHtAyP `ƔQ \@*t2$ B j  !} `n0C"HF:'A)JL -B*Hf3`'ppT@Chgq4PH ռf7)Mh&7 BNq3QVBh3qӜq-J.>' P$$  05x |z Eab0B >8@jQ⭶Mm` L!yyHwTjPQ" (Ј+@kQ'Nԅd!jO*?PI\xAr%aI, F;ɘdQ|j&WU,)a[ u}gˢ/S"f19!< R0m kłnq;=\`![BK[=mk m0%&~`Ia qBBLw5B@H e/y"4 (QP`M/2HtK\b.wZy0O ӆŽy}ŋ߁7$ՏD,zC\ޑtXL-j}_|_k YN"@ @Вeb@5 $C.@){P2h 53%1Bl~.2@xsc=vAYЀfC v, Y!-ȧ2jC:A>"<@aNDԤpl!8J% ғt3i/MdyvrmVCH^.*-lCߦ i6L0| \ iV:"=lbtp` w8#YJɹ#Y$@mU@gl D 5H[(uN\ 0t;s<5w bb ~N̗. AXZ fH+̽uNf :XD֥^=<{"SH?ȋ4 {x{w aЧNv7~fGCz<b7A$@u.8;N0 FNoD ^3P : bq g'BEgpɽ|m1g  @E bB \Q ]&O!zE),/QfF`Lp +No#Z!R 4@9  `DP= JS ): b M " E# .!` O/<-Pp x a$PrNU  )V@@2p#Dh` .͊G$Kg   * C q$.q#R 4ԏd1'̠  g T"T!b\p 1i` )qQ12މ 111-:MD`b " t &@3B J \d`(hF`B"۱b#tAnl Rr%%9":F")""3r##Br1 &oR$^#z) :!r`T9B+ K$ј} r# Aq:+ Bx@D x+,$-=-B0.r,g11sb. RtDLF@% HF \@S44Cs4Kq555E5S"lB6 !ȭ6?7"F*  Jm#8 m f3BqbF9@ t`%]v@ *g<:B77M  5_î#(!z>E?"9! s<<dzeC7ys5A @s$$zB -7-` D V3:[iFTDgq6^$` DQ4Vb40it0T pTG 13Jg!0@G, >N J7K=L9BLb MKT%ҔK RM$TPK N9 N}FB"rXMMq$cXcKyQe6NTR$QU2,&>u#t 4'LCO+NUJ]51"R<"h Vo"VW4nc9U5XW VXSCY[YY5ZXU'Z5[u[[[5\u\ɵ\\5]u]ٵ]]5^uB$EJVNJ5N_WbO܄I8PHdU?`HTI#d^CQ(b}#cTW6]Vec\c)]JcLdDF "f\Z[B[Eed[T6\X\H08\)h4zk4'xxjV )'v%G lryE-Gkck jr 56m0H)Vnn7"0%(\p#$pŶ#ڈ#жm}g~>gzh#.r bmwsls @ןFwt9LWImo#**NJ$*fi\Nh9W,I 0J8KDsyņ̺zǢ׸d$h#@k-b|˿ŲyǂTk#L lŒ~,ъ̀ m0m(-o/&TB8bpF#pMal06bYٛ *9 ~̽m~1Lq|>\'[\ݩرk!%unr{^/^%_=!?{;]k>'n_W?? ɿ?ٿ?? <0… :|1bD+Z1F x2ȑ$K<2ʕ,[| 䀙4kڼ3Ν<{ܹQϞ=4ҥL:} 5ԩTZ5֭\^6رd˚m4ڍ1ۺ} 7ܹnڽ7ݵ 8SI$e%lXŌ;~<ɔ+[v7Z;{ ѤK M%z*]VwxVݨ6nĥz5)*NЩ$MWʓ3fV;xW*ҭ;e,BtSGh07)-s "ؔkS)  xj)70"mT3&耓'G>"upX#TŘP9) +"KQEJ^|R #à!Ea22LA ~~V H)z$hIGȡ*GhJ  9Q `@"vd*$AH$jhF.ZklQMVԳCBlFlVBcRRSu%`bBBnYtʙ& gti%ER (03|0H ôgk,R4A 0L;l2*DQаDݔ9}\Q`7#@lȦI 79QI )j'%mEng )upnkYoݵɗ )l9x7){x=F#=<M3-QVՎ59#4$-&9sٌ~F uRKÉոEa-7,+3E}-yS:5Xzu,c8 0sGgBx!!\cCb![_Dɏ~8z\~_η{^}~HKD%uT4G[DB0@ \tdb+.1p-rx cDtG㉏pD*|%6#G~).X$IPGt-9P4Gk洲## 9OrUL 5M^2Ŵo88j%":Q ,.g|kK+4! MT/Є&/rEhQԩgn6:^# g^ЦgQzMmo!p(2 {<HD*@ 7A ezP/Ce("b/h5#\zG(@&񦲞5k` 4x8S=,0DjX*(,5:!qzP8' !ֶƣ0af&~_#P8QvdQ[Bo;Ht;^f,`pKQ&P=QUC{+x/U'x,$ga  d@Xl!qO<@m"cr&2Jc"\DCIcN TKQ 샘F tU+)L֗øn"OE3" KOHcGڠ64Ϫk5./oH6d) xU~>aLl%GǢh%E;Pq"7x3юT3*m)R%%F4Dވ6$9gKYl@ZjBR m }\B 2S$0D)#p7 GmW}v/ɐ`m'0H&< $#+0G>Q S,@aJx bn ,$+>CnW a[jNa.cRngNTnYm\磡sNunwy{}.Nn臎艮.Nn闎陮.VkQM}nU㲞.$@jn~@nǎN4` 0 TF{K@Aq1ծЮp;)Z4 >P'D / ֎||~۾ >>A:7 2`B/1o 70N0Iqi1ƀ@M03Pr*j: Y`Ul07D.-`AOFP?F0jq?-@=_@/1R` oj_Wo do {Am^!To>WpN?C_E@Lo9rt_ ]`z=/ Y A֏PGpMP@_N-PO@]x` .dC%NXE5nG!E$IR_?g I1E¢ghYdB.)£"p@ 2&K2H H4ҦA8P%!*Rdf͛9P#b<Tc _J$>XFqȺ+rm1YV "VL H&΁&,<Utpw`o|aVL,cSP!q8'3&dD^=NIX# <?iS0tB>sȃNJ2@TpAtA#PfgDA [ P@!(Pb*˨ `tpHChaFs(2 qab> qĽDhBtPŠìRK.[|1F"#h{R!3DFhA!)8ˁS G9caO(D @fz3!0#:4TLDQ,*`rKHp9:C &TvYfuYhvA}q F8 8d!m[T%]5ʈ5C2p-W(ba@߲JM6&]dP\q"7(b6ahVfT_QdXj=t1%ia".Hc`<*g"i 7pW|q6tQ b"b jsUHW _Y MoDw~C?ktc#.<ړI.8;%j A(c2L[&DAlwlI 200 J1CPәДf_yfj4#;t@{t$niOS36(.|@4yPtх@,@XOH`)obWU{JR:T >EO53-p+a b%E'&$@$kYzVF+G9 \2@BTasT\JWA Jb@ BF @&f  y 9A!AY24ndZԪֲP,c+$ W(]`Abe-_ւ_A*nH D^kMmw4:׽b)$TLU琀@,ncFIk-|a g"7‘ @0fd̈0x 4i!)f @Ę"8 FpԄ"qEv'] @c 'P@-w!3"+d9$4f8YξL51È`D8 ,=Ͽ(\YYX Ohcs-}iLC+1D %8 Tn֧C=L"T17Zֳu]hk\Z׻u}k`[&v$[fvlhG[Ӧv}mlg[vmp[&w}ntsAfwnx[ۮȼ}o|[woO{'x ~px gxpG\-~q[6npMpVXò=rro"gy]\3*tj Pnރy' e&8@sG]厹Gm[1z;o{ush&D![-+UPp(^ "0d   }O1?pߝxCW(!rZ+[}]C G~|vfOӣmA]ۯy}Lf d(٧Ij0h+4plxx6K6*0dK0hPp 8 @`x d! Фi#{AlAgqӂ'a6 =q I? =@:,<1?e+8АdÃ! 8hKd#MINQ6PHC5ed2D@cY9C>7CAC6& e;D\6iA(Xe#$|x' LL6M@, L؁pH6(e 67CfdEIjALFe? @\FhܾB6Xj83Hd e6eeۜM=tL6u3mS X#X6+ISQ:QM78$R"6S5"-#u66`6VQ+kQ,Ro R0 S0pS1%S.MS5 Ў\S7R}S9SR:dR6S@cC,TCTL6D]TFmTG]!CG3ETKTLTCT TO UQSUKU`V$NOhUU ՎP^UUP%UdMVYVi hUf܈hHh@ CBuf}a֒W@g%u5WreWPcUVzWJUfqtiw~]H%,"BU\m|fq׌xbxaMyWشXXf#;{^M <һ<+ڼwZ~ `:6YMk=c@Nh `YcY[0-P"ecT`[qY=[Ubm<]ېd۰۵ٶ۲M۹[-ۼ[wE׍X-\_fYH,S ,@ L ਣx P^.M R\ @`FM6[^;ܻu[[۬UIߕ]6[E\^5\MݺsJ HA=% ـ IV WןEC5LC/hkD^ݕ`eې ^X `߷ `=^|U `f^^v vXS\a6M6FIFĝ^ bŗLNOVE-xҭӥNna+DHC`f)` _]^^> Fظ5&dV\.fdfhd.1auf)1:nadu^_t~af$@I8bd]t$ TfNV`$G$!ZF^ +J]:Sʪ܁Ȓdefakrch`6VFCNaoViBgiC6g_fgpgH^wJ(:ӗŽ~&tg~oZ&ZjtKP&Mg^=3S:Nչfcfg `jݹFikknNdF闆aC~llʾdտF ͊N--Qe6gxNENb5X즒N2BH`Y}^B۾& kqfin.Ȯ@[jvd.o؛fY/!KAk(VFvo~F^3%o[ƎSW׺ s5@JnouKLUPQZ46Aၗ]DFO@BKPSF02>efmEG?,.:ghohlꅂ24@:LODr_hip/1:57BoHLCprw68;<><^aL[{tapuSVYHvv{VZ^Hdﰷo*,7[\cr`dM[]d|.69mqROzhlPf<=G}zţ;XxjרlÞ5uN.OAeiO.E6Jrйǧ̰ɬ_γͼ|Y@a>]˾bfKd~YDhc]+?91^FBdLutySRUBx}X7~Q594DHK=Gm2eIHoFkDfMváM3N*?u{UZR :e#V<[J|VgglH*$ ⒈ d ag$r(R$nx!I)NHd8`#g.ɳ p$τAiѧ r 4D*I;b&ԳhӪ]˶۷pʝKݻx˷FY ƒD F(F$jH26f4y @6 &lF_ 6}:Q7՘ djydױ @%<6 … ͟ a.Kӫ_Ͼn +& 2#@x,!eug d_tZ7\k1|@C A/_p@P,;}Wwew!TJhG*[V@ @a /];@~p0{ W#F$S`0;p#:?=0@1L4$ _ 1o,xhx1'\x^겅1̇XHJ!%kXNzC|thx{ա `7 Y:~5-QN aG'O0 dA U-Qo(AN8C8$"X#1U'$qx۲8%.A {xy(88h88("H&荫؊$RT5 Mp?øx -  A~ .B!MMYjgD)/097OflQ &0cCEx!)…i1JnhB4`cy$Q@PBaFz}Õ^ ;#?R~QY-OB$PE3`a$HbX)ghD@?miPE|0Ybc>%I>rHwى5 ~6 :`iQzs)7h! yP;җD4b#8 IcX%\%`\5~1LlǓu2?90I8FsK:Rt9@(2{X5??D`}QfR!,xlp)r`  d*} + ``7l:+,|. 6IB|Ң/ѡF$ҥX+ +*钤Z7`-4,y %UTV."6y @A'*pч-2pm:n)Jk&V1w )HJPH<9$`D0,ɖ:/`D dM X p0$4c3m븐 2}6 ķ~@#pkrK[sa41H02W\C2Jo@;3?@Z6XADЫg48F7roLk@opWۓ 1Ӑ8p8S8*!A0#FK7@˖\*IofA6C N0N/0k,qг]@ Xv4@N v@rй+Q*M=\' 4:`0`0ȴB}!O|+TICpPŭēH[UrLǎ*1p,%*d aO!<& //)vIƁ}I@nJ 4i"3f&1 3lCTES:@2 I\!,e`Q~VQPaκJb9!?3-m#h0~9%3pD=z ѻ9$ :A@A>k_-j7 ;0;( EWP#_ЯJc\& :!MH TM #od;MGDىV5:>r%cdYD;?.-~HXHdQ+1|sTGw4[Rg `P'J ,K_Sn =1L'vV,nG<SkINpc~6 @B UIs~ !x,CY 0[ zNMWvúH=,΀jQf8P*&~ OIB~M^Q ]ȮeGJx@#7 ϖ>ꍞAT c]N.I߇I89E?bpTFi-.M  a%F.i!.tMC) "OO&IݤP P0 N@NOT7S30 ((`PX+6g0(-  #5p<*Vz΀ Pj7$uO/VPIQh[p?GploOOqdoP_;>sٿKć"Rf́ AL0xpDD!"F@Be?((q` 3% a "4p 90@cA)1Ce Zdȅ(!"fD0̈"`@wR%ˈF- [= `G&b `AnY D dt@dʖh0,atH2QVkѮ_3\A8,:98ă NSr͝3CY(/r,p Spv>AX:`̋/88>d(.Đj." X8;r8ba ;tDP Cl PZ<[<?2X?@; #B8|␺ Q6' Q<̾I*"!$\` hdF@`8E NF,/"PX!4O˧-rb(S [1'<(3J(""`!1=#ڎՖ*30#:"zU'  q M#`=R|=!#RƈHR@!?XG 4:*0f# xB́4ǬG94T`<3L^N#ƐI 0%tމ@*0>y^HD aFd`dztU ;\"=;*"4;ab@=.h HzzT@" ˉTeټ HXzԒnki;dՈ^HHS!Tgp@lxPQ ҺVРSAvkڃ,mQ4w "]!X*0[*.!gb<jRYM{5唜i |#8"h+K_%Ct!Y/VW\b4TCrTypNSJU;AB} aFyΖR61L2 >Huڰ 1K+!@r~Z՚+^7"-͉{s$fV>N77riA ~ iR$%s:DE yspI(}V8HbmA`9uD!A$cH=l8p/SFrm]C$Ι}|9~ xkS.!oy;g &EQT+tܥ NS;71dex `xeVh%@Zj ysVqZݽ5 Gsυ>t/Fgu.u/9\ NGձ>"l_t[Z@ 4p>w7w{+Xu޼w<"G{s2! O|xWGJL !ycG}ַ4z7L}u{~?|G~|7χ~?}W~}w?~Gտ~#^PKpi.C1[?+ s#X53+ R@ \)؀Ѓ4?VC=@X{I:刵sA |!#4B!A) BBa@'¤{-a2BVS+/LB%C9L(A0@ BX? j0@HLH-BH;>HE|d 3 W Qř؄й]EPRE"*HpHjAFe fd\;6,.$(D"Cc8p56`@XӂQ#XPP,3x2Xchy&XP+p* =@pkOdЂp  3Rq(Ѐ)DC8LÌHzK4Ǿ|H/uGÂˉCZ 0E4*̃R2ÄS;l ҤuB5L'LOStRKF=z.[ĥ+Ǵbl@}Z@=pcVP]598Zh8^/P}û]~ (P #H0Wr00ru͛`e@]Pi[G 88˘\ $T ٧"UHT:؝Mqe8X?tWc;R?aMbI-S,VL=Cl\[g\\ga-\-~ۻS1%LJC)[8UǿlH}FZLu/AMȃp00E.8>I@ =F`BHC60 p䓄*Pg 䟉ܤ[rjR(pkp0J$i}h脈-k-|X A| G؄ 0ivf(l8BObAfZ/߈P8 HfSDggmf16TTDF5Momh6hٖbFaF%b|v?^n3[~i0Rڎဖۄ,*MȕZU==dd@]Mp}j8vyLY &.OL=lH߳1W  ,1)ٜ8=y5JI"p 8 %1y;19qqiʞ-/9Ri@+`9?aL0g5T1eMo4s~[b7msnsmiVHFom6iO%oEn=oږ;bOM9 QDSŅV-]5HSPٔV0C8զ>pX:!fz&xI]# )@#h*81Qv$Ae}J w%A'`,: /%$.?@%VF9Tp'}*"4^ɧ}'w6y)FbFSn&t"Ft]n)$RݎS迥sNOi0Z/y(tgt{&M*uZaHI&ɥb)\P/8/07PVk(]OWPq8CC:Hqjt/$,@pЀ( LFu.M*8ng*Mgt+*3𬴏/x #`b".Xx'?xRs!G)E;ZHxCJ hMy7hh|!Vt4H!z.-37(hP&DȰ ÁB|8QA1^H`D!7VȑF+|8rǗGb<9S"G"ƴ(rÝ;si'B@2mi/lz}t:P<-sxyTPMѪ]9FKYlu \r0NߥlZb+7@VJ6\]AWNNk_5泚M~\*ӱ_+ݪmkW]r+m3U25ݮ}{yṅ?ZڗUm]/o8a BIPLD `O`cp+~;eX1Zp'4a", H X aAHh1_X@haUIH 8 If $. 8ALǒ G6)Q(1JF%Tኰ-$a=@xBqI$x i+H GpTPG" @0m XL /Ad|@A`mF|e{F/䃃(0xg,%/=@+ p!@NMtPUP4 P!D jU"D,k`K`h4x (H.{)e6{:B\ @$,{UJ2rADh E(+1D|Bwi!%9t`{8Q, $"́ (I TU`,V ֱ`c @$mYBC` a ,,`T r"C Ofvq He;) i%,P`VN7g AR@Dp,"em*,^o?*F -\VP(@BDŽԢp/E@OG P1W e 0 lrմWN0 ĵb+V$T`U2xd‰[z?H |A$xɂ Æ@a8H ڌ" +A\ 70\cHMV0Vo@iaa 0 DH$"!Cw^6ڃLɕ*P' *0y. P]`\<r"D`P`a ;F4 UoI  `9pȊ 8ך N r `qtUفGR;.3h,ְt`au9B6P~7 jD!OdC2M8",Y|GGV? ǀ IŬDȀD`RƯtAr v1H)<Ȍev1 T$< "p"#<@Apܭ9+ { zK*,"57АLAAVЂa# R"н[;@~NK)G;-R4)h\ȱ zPLa !rS9cJ tC&<1xTp_ R*2Zpp:@\BPGL/p`n"-? ),,@ !0Mp!^] G@~2D!_?!LlA&@t@PUAIDTd,pBA\`^(EAāA .)t58'xAAUH!BL4##x'^]%d2R@LD@p$ B< f QQ89@P A!Z Ap@1D<#Q!HP@@2*eDJDLkMMҘ#sd_J|e8FX%g- @Ha [E[%9cFJcpAAPgz1Jn&aڟxs|\'\rW|gKT & XT,$ TO (TR@A@WSL pL p@ l@x_A@B  25 i%kHYJRL@'X   A8@阖iALS@AD KSKx>#p@!BpA Ь̩S𨺰  ԩi @@Lb%i  4. j:(ńV6LjR)AA|jA~ZkZ*NA,4Z*jlh~Ah+zkδČ,DI( E,A D@Jh(SfA$SD άc#4DJAYmí%0ivF4ʔN̂%IΦȫ tZE-\dAnjЅpA@Dx,蔎h*B-A<@y5X]E @4W)9u?%>-va^#vAtP9:}(cGvpeQ1AAH5UWav^qDQgBa%pc0n{v5D m%a+PiA<<DD=34$@ $ @tAUsC6Sk/Hl D~.w{7xS E'`dzϵ{AKղ5Avpx72.)kj8~|wUl,tF0n5w{D*9lC}AuK5>tsA|tA9P<@'Gu8YMuu(hCd($ L@AyO@KSpaD7kDaļc"a-ޅXUYJXR@ee}\m\  ԕ@9a8 l[ZbqL$A A@l/įDo`DkPJ{ATdR8@[hk(?VAhܜ\AAZ;=@}QA TU>+qZKD%AB(b r@@ö @)7E]Ah d#zWe|[}1NXX8GăV@ j@ >| @8 B$ Q& 0f@'aDG G?3`Ǚ#NG0uhM }E $!RX֮_$`  #&+HX5 v8|$W` c *'W\ K3ztr`aĊh5| b&W A8HV="!8 WdG0H: A{#)Xz ba 2>z9 hr$( !*.>"<3 5 D DďۮB8 N2(C02 .! ha@n:2%`r-xPB -3\P! H<:c,s/q\? TPD4B MT 29PRE@yOST:7M9=QJ[q fM(G6 „2H6MH bYaf`b dsYP( PQSC`3F#d##)0\ EHW%\z6VZq%.bh[chX<p."A/־T#`Xsc`k5!WV裏H ?TPRHM 2 8fh`5889l.VH;()‡.VaEw(Ψ!A95(hcR,s7Mw5hF&reGqv袉b ȁX@ Ѐ /|+y6[FNvlu98,8z˞A j@1FeJ@HURԦ:@ d!jVb@/VHD!!G~&J0UP qX1d[j@xu Gj #Pā  3>T#"bjG0.W"@rQ" =5]tf\# Z  \#D"5EXXxBJaX0&l !@ "! H8CPv΃Ae}>R#p@X !T&)~0eb<-/F7봾Ql\,)MjB :5D vŧ8!A"@xD Ȑ"T^әDG.ux?} z@ y@iIOAFCxd[p^*"|36Aև1XZ@ *.*z;XiCVA9PZ/p;*X6D`kP2e9 #DP+.PuĔ^ @ @k" eS xpd\$X8 .qOB ['$/^"$/"-jU{W<,o!0򿩥JGڀUp#50P㉐D)$m'NVܶ ` iDa8\@|wP0 HP`U`(B2 4(" z5ֵu=1]pL]kooba(2ldA<xA@+f k+Xa ]z"daTRmB7.V1ls;kAI=5WV#VV 28@qgA < ?a:0bF'4z ۄ @a D/εY oq Fo*f(# \׏lP wF߀8I*z*J@U fMu`.8ĔD T+ q&*fAA6pmގ6X* O-7#$ ywyo[\sO? qOL/d z@䏸|%C}p`==GDK `8>z>xqOVvK~~Q\WS(yTr< AB;=L Kֽ_ї>}9`PjH`  6g#DN@bo#LHoLoo h@& !|  v@xq /#z~>/KͼW.!PP0(@ %#@<'Q41(Q'0$eP O`K+x15ArqPp:Oh,#?P:"h 6@k:`P&15v1qP!%1PV fbCr MpU!=OVAx`!"r22"!r#A2$#E Q4I@m"+$i2>&q2'uO'}'WS>=-R;ˆs>1m:3<=??=!7N@ 4E@8˒Cp=m<9/tAS>ӳ;@tPx0%%$D'TB]TFCBGyJ7"= B)GuB;4A;W*4:2@kC4CMQ<BO4?TJE;4BCLWHat94P=tHqL+HSI5>I?FԄ$QySHsrGѴDEKLKtT;MP UGGkNwH}D{5U'UUMSPf ifa>3w)(.)\-mOߧ~빾&>~,>~پ>~>~oޖ XO  5_ 1PA A <*a90 pͪ% Vሖ !d$ ,N ~P(#f@0Kp\)?,/巠7 V/(` t`03} aA@ 0aB(T8É'QbŊ\xȁFL{ 3PE8)B07ʤYqΡCj><СD+VJDCETȴJ %sL$M,ED?TD> >`zX ?*EÀD(̈G 3a ay0eL @HQ <ċ?<0G`Q|lD@o|t1@ +X$d)Pg( $PF&kX82= ?aBZ؅ejvn'@&ϟa'밓N:l7d@)2,f3| uw"l-RI s$,* tX#^ d!T+p@7n$N4+-~ d:G*H* [|3N4{$&d}Q?tg HE#ԀԂD$?{ϋ@≎t4HE'Γޛ^ @w#JHA08 $ kHpդ -@k0hH!@05`'8&GZ62b!(B2p 091s{!D*"qG1Q]g G) $K*z7MrS^R(h{>AB 8R$#%HNGL`j( 8Wj@-br d&-Mca!@*x;)̇j pPLBq͜$'i"D8hCw'ٴT?LdҚL< $ (rP# 9H`# `yR Eh` BL~04I X4"S ܂j%uMM8 )d Ђ@N)U:\" ׸U9>$701&`8 DPtcR  NphT'^b>EP("`= X#kE0LDe1.nB>h$JnMHb5\s(`qP WW{ mS aZ]$|?b O<%ۄw fہAJ}pɅnx|ߦXD%.Pkly* %T(Ppl"@Bp p"ePnP@Hȏ:ֵbH WB΀ H@lC Fqln d7ZrRE5 +J_{Ar[-銚!{fB4ow;w \b/7`К" B0&WR0?x00 hO1X@!OO$H4d7;LGh[7P `74`1JTwIu}u 4Jep8v!`apn%0pqw5q0!-kA#`4h)4{{DŽpLj 25y :9 [؅ =m̦ ÓRpEH;G`@xz&x_}8QSFg w~@a F1 B@$VpQq opD|EEWDWTa "p EM8*F0\Up!1pELxF'#Pg@'$h(x)xȎ@pR 9d7 ~ (u1HD @t0=^p^P)6uǃR @ D^ Nّ)(3YD)@  s?01 I apI9FPP" j % @ @8^06lbIj[-ysZ@ t іoaqI6 D#щ(~ <PQ1p GgTCG Dsv`T5U4/Nj@L]0| d9% fȝ v |&IZ(x@Mfn!)RA*Yi p@Q y?zPjc8yz :YYJ!6c %P,jH&*C!f' ў< Aڎ QWy%If 0w%PRZ J%p2׎X XJejHgl ^0 %qtwJLOb] !!I0xʨꨏ= *4ʩ꩟ *Jjʪꪯ *Jjʫ꫽*v!Ɓ(yu59DJfX!RqJ"x4 vSXڪXJg:JVG*hJZߊ֚ZZJ9խ*ut V'JHq'ʮ :he"K&3{;D;ڱxc"y<͜\z\#+P|TŘ$>ğ\|qĸ|mlxݲ,ŕL,͉oke|= ۿA.wܼ;- Џ<-{՚Z<<s[*ҧq`]m fLzFMMLkn ǁ6Q%\l}ɖ۳+*:ɗ-ضاtapڝͩ4lFتm۷m{زن,mǍܻ] -Mm׍٭ Ffc2m 50vE'- a.`a5 .`#0.decz6XQ?`! 0p!8a.3N$@qPBp}\Ra6@N.:8#R=XWY6U;:>|AmJI"`>P0 Ĩwy%S@ @^Bq =1V`0@zNnRZ?NV>,@J;U<OA~5z.@b%  Nhc6;87. 0*G7R8 ^  `ͮ~P D0| @7뽾}p@e}~P'"Y ÇZ0ao!/#O%o')+-/1/3O5oɛW{CKp?Ɂ@C&pAaHHn`Yfky` !a=d@EMd@Ypq ҈g"wow_3 UT@qU4P??[b) t S@)>o lR,&۰ ݀Xál$Ia*)-AxB<;*pvI?QATCV?bA[6hlp!tax|1M. ÀD@̈FabW@`D#E< *d8KDp $M4'NA%ZQI.eSOi#fݺ՗.>Nz3NЉI) r`#C L)@DM۔pѵQ3h1ÏEzR^ͻ7)) (s 82$5DA;YGeD`b 40M G6Zdѧ~ F bs{ϧ\1y[``)}ARg)EmAGjp؀="Ȑ90!9DR%&d9)x n7T>:c  x ,F2(L3`tziW%6\K hbB$ 5 o` 6` |?uIgff*]euѥc,(D`[8 *0D#GP$1OIyqFF6 hǠĉFg.QsL9dJTqQ{uy(0"@@Md/@mnt9XaA zPnpa>NxB2,l!ʲC\Wp(8 &s 0([Tq@d/>IZ)!)A1UU4 F]+Q)Lʊ "SDbc b*jĩVA!Q R,C[V5P $Eb !Įx+ɤ4P8ժSE"Ė2ybWLճ] OI:U A6U89I  I7 |"X3`7 X q '2Pd+T \ۉAPO<Ե.v%h@ BIpRt &EJ_XK}PE GՀ @CE3PHa ) H'X K4%FTB#bZq$ŕW( ev olq&ꁮ4P#'G#*u]ܚ<؛ !IϜB +nq͘"BrWRd͠@g̘@ #`o$Z3AHӷFA @HЄĀ  o KgBT~y?xC{&0xv^`@d .`(a 㸰Q@:W1C8BAm"A8& dӄdEgCknLH*qdoNء8 r|?D7 =q$5gGnğw ȽQR fXCfJvP^5AD Z1q@F]AB"C[HLk*P 76N21X~ v=ybŠG|z.zs"xw&l#+LP 8 ia&)V4* x)N?<)jDlapKt*93)~~P8IcYeO˿;LD99;4M٪;F @:8,;Ӻ Y"$v 8(x 78"Xp{``X  8sQ34 P.*8/,X 82h C>=G|DH z({XtxnRnj^`T@:1@ 0hDp/T +II: +h5Z`C`FZE"\P/8/HjZ/)FOWPUmon00@O`Gw$ulw,Wő5P8Ȝ~WX{GBFr4HTّFa$  <`Z9|: \IBsY :PX?4B 8[@<(!oѷ0hu5JV aB J&J?.H > āhWEKXHL,̘7l)Њ^EȄ]XEX6< 07Lͦ0<P GKB9I)M(<$=XdMD\Lμ 8pה4L1Ђ(L P?NI3ØLLO\O(^hDQ8́ϘOG4hσCP(6ĂGHPmP}P-LLÅ PU 8x*((Q-x 1HP}QQQ QQQ R!R"-R#=R$MR%]R&mR'}R(R)R*R+R,R-R.R/R0 S1S2-SLPS`S6=y9лD; 04SPӏz iL<:M7DS?5TD<@eLT|SI@T3 6mQ%UOmT75UHU0TBT4ԤU8SFUՂT#0VOUSE?WTJu buԘHVEeTXUEGUce!NUT\Uo% ]}n}ehMf-m(`wUtu-qEz=UyUT]VmzԠ}LS|UkVMe^MXpeW:umgo؀ i(vMWm jX YV3X}%BlEcU[]:֐5Ys]w=Vٗ=xX֍MM ׃W-YM֦}VXeVP٫YD~X=ةSUY^-ڛXiٛZٓ%WZbuVVYVEمZdZKۏM԰E\Zet=ZۘY]YTUR}ڸM[ZzZm{-۲\ԃԴ T^5ڔ5]]Y^%%]Z ܄De[WEZV%s-}-ճ\\5ݠݯbZ}3^׍_ȝ]4_=ߪؓmI\էޏY%`[-ߝ_^ԕ\۪=޽]޿[Yߨ߾[]}uaʕ_.]_Cm[n[ͭ[m^I׼}]`(^•ڱY"N\\1X޵-^T]E#؛Ȅ>]>e^'Dء`ڌ=]e^ ^V^ [EKVY_"Xs`MNdQ]MvKcc;^vbߖcŕdB`~XHd.7dnE Ye_]2FTd,fAf6~C _1lNl^l>аU(ll˾l̎M ,\"`lNm l( ўpmm}2chȀ=,`(hm~mnL@ *%xr X(dno*$v/ X> X?3o p ') (p>p Xhxp(0 ? q%'Lo 5N_r&or[ A mȄ؀Hesr2/`L# Ȃ8G+6Up8* lP8#4sC?ր  [h"+7>80/['ˍ,NGtQu -lR_uVouWuXuYuZu[u\u]u^u_u`vavb/vc?vdOve_n_wИX+PuKHpD(hјp1DSpp?(70y6\O[h~W 39C\ tuO(젠(#l@C8$1xxJ!('y"ω?yGϑH؂6dyHj8 BvU7zfL(;ޏm؆nP=#lg22GcT5XZ:8w98CPOCnD sD JЄ Ӽ' O .P ?8P$+ 'x׸s8 !y0Xy@؈XЛ/Lw zmy7] w ￑CƠh| x{۸{ǐŠ p%P p )H " 0/> CUIX  Q b ! 1L P`d' 0 9wt11`LA\ +v,ٲfϢMv-۶nݚ+&/]uPJ;tJ. f꒵:Yu}Zf [NzEL<͙ 9a^12CԞH5(ӢAnή}`:32@lbc=$iDM:gϠEm[ v=loXnE-´[%"V8B]|@DHhd4! X`uhAaAH^vVPj4'xb s"w^: r1AXF @g,qQaoAF5R9pSl1H@III$VRK5q4Q(PAMV4Yee_UbriS >uEW;餣6pL&.ɲ:5c*dxlp XEAp@a(zxAP##`!T+K^A7w8@W8LtQk-0, nV&lb&A(+&T29@s.VH"I#|@"lz=3+R+|15D1d1PkT)V-AdC0lU'4Z@[dX{ ĔS:9jA0C oP%۩<x<4/Ptec+=]z}F{YⴵDȍM$ɿ+ 7K  }Y'/O~Pb@kAH%԰!=,U0#4g(r"l;!d@DX8h2p , 2@0@L5a t &D"x)$,C@^0@D;@$ B#6 :׹Q ؟ptS$#ɺa1|O J*J2%-WC 3=V,!{y9 & G.FX"43D{'+TNq\gD8=Eп'\'x1/(bZss6X>Db-HAb;(j,F-H 4MF>'HRT%4 jĝF#-IP.N=;)U ABc8('I ވ* |"X`7X 9 $'2Plu+T E=9ʇ`R%aIh@ B40MTs Ҥ5k6\tټ%Y[L`p{ ֊B%(7 FQhA*a Wqby҉Mw9ݩ@2O58 7` @=;޳HpCRS i{dx֩eS751UrSI1s:xhIoaG[AV1X c[ A)!<t 29 @:I ,UZJN05[v Nrr ]B:ҒZ lأmd 8. Yx @, prθ(.4eZxYCapkځ5AhcP=D22a~~x!X跁c* { AchIv!dm*P PïE{O?[Ұ Š" tb2P8 H.G"@ <,NX`f,v|قd iYkhX˚c@@NC=R/ȧQ z*Xh @# 9\$D^ [ @@8d^~ W`a C11JW:PdRfe^S/hejf"!lU hiҤ!DgV Tgb#$mfnn`@|p>qfr^,,EAtJgtz[ATrjvrgwzwgxzggy' gzzg{{g||g}}g~~gh h"h*ڵq2EN.OZ䅅D夆ʖBh:hNf([pY22(i(W覽>E(".)(/=ڔN(2aEʨD(B^Z>F)r@(hhi(o閆iI)Bi:i"jRh.*Yi)6*Zjif}jXrmi@r* iBh*rj Y<*2¨ʪ:h(bh^v(ΩrF)>j踎+NkF蹢j..6k)+*6++Vjl>kR)j **n+Vi+^*kN,+kâjlJklfkRk2BJnZʫ²",*v,B:*+,)**lk~.E&-.Ϊ-ɂR,Ş^)"-r-܆k`l>mm .-.r. .+ʬᒮ؊iZ:mnk~lm mb)텖.nlڞ.l~:/ꮪ.-^zmn-.֫/n:mfnfo~oFʯ.j(,njVvjo6op o,pȚں@JnnέXp֢p-oF0,v*z.ڰmjB*Spn䞪&oWl~ނ: Z>NoB/. m.Sno͊,2.ZmF1k {6"k$c2q>Jop)잯zXrګS- W䒯 l-?Ϋ'Wk2/wnoɪ!0r+.s&g2(00sVJ h7sЮ.ojs9o7hs9R;93/8(wr-d{.?C*DS4B tEB4=gsktHHoItJJtKKtLLtMMtNNtOOuP PS$^ DH&Q;SCuKoB L$ A HITX58WUD8&` ̂ XuYu]|>@1CdmBP AtX@Q#Xl]Sve[vw PAԀ<@ 8 8,I,H@]vnnd`chwi, DkD AAt,` o[ucMp6q[ |龍ʨ/nʪlC3SO:DơHHsc4(yN4D@7BIxΩP|i6g{ T@`(TCdfwvIA  @$U8 }gy7dpO_?x 4xaB 6tbD)VxcF9vdH#I4i2@y"%L `[P*( U6)}Jysm`Ȁ#ᗥ|aADt9`ˀEax BA6V  LS .! xa $@,AS)D )(!‡3Iٳi׶}wnݻyvL3&Rf̙2[ϟd jB(N%e:FPq`ȍX0Mu(*m!0- `⛯>Z h)@0z`Xe&$„( B48 t8-2ܰC`"<$\&A:9 ';*KlMN z. @(*6PANH!l̀䡊OR(?рP M fAU8TX Y8cN rU 5F%؞<d]f}vzƞr< Y|)fRElDȗqȼ ` 8 5?f` lH (!"%h {}P 0[@uz@(cE Bt$_}ao"E6GA7(IYJSB$$ e,1$RK_zPLcS1T2Lg>є4YMk^6Mo~8YNsT6%'9e^uN)c<O\@ ZPv4$<;6ς̳9JhCO1%DDэ~HI$qA42.hOt!)Mi?g A*p~zѴJ=*,Ԝ:H}FdӏVT0A(RVMOϲNtiUZKU {J=p-"DeX ҸԨ*NR2 ǫEVVfihed)[يկ fzubTVSY_-_zղnYKQ򵯝}H;1Z]]qڱ^n\-gy]Nkk[lv؎b5uݫtK]”4oK9=|3_f$k Q-VE;޿VSp+L^!&gJ2өbϋq{򲘬dtVW  I侵M鎋cϷXsbbo gVU\(K&P^z}޹́,FH)O=hG?(|P9D[ӡI]jSթVYjWֱi]k[׹`. H$b8𵮙llB@5EF%>R}hKvv}b2۶f${7V;2-^@yl lB~` @@ `HN'4a?XO(Hl |p~І}<#7ph /P\w  FXBذk4A:hb렇9.'q[:rÜ nA.tȡ.vgf)r:@ *h1in@[L*ġBBFg3 860'P`:HAN$7A p) 3AH xP$@R{oګ^+XAF!>AyЋ$h A@-ww0oRbox/ `xR $& P$pO4 `( vD`/ <9OL2h. ހrb `uVC1"!6< pup1/\ 02 tR -!"1 X CX 1 " ! 0 w G N0B:a ww+E|y*@eH Z  T` tdC<<#g4CFFsqi?$DFDNCR, Vb p N@DDHD`n  e q< Qg!luK1 ^`tq`^vn RO#+UTe$ "a,fMDV Vh%]&i/%ITREUB&I# F`B dTN%Ut 2 x`Fc(UrMdV&R(u~ V%_,+R-  -wR./ R"#a`Vr# b*ײ#sB >` H r1 pe/ݼ1& FQG^#gD'c(G2  5 TF 8 s!L 8?!3W'+99"@FP5' 0# 5w @@1ߓZg8>n c ,1p` e3b Ƞ'cflhvrgw@oAr?tR u>4_ pLǏT hVV\z! XTCqGt3\B @+F1E0r>%Hdz<0qG"os2ttH>Ǵ^8>@y:ؚ艢h@(*` A}g~NSx(tOH TrFAtl + i  oQW B@ b‰H>Qq~~0URTsMTO,L!X5T3mn vJl~7xI! ` `e8uʘ؋5Lз57ūsXȖ 7(X @`rH y!8r%yg9+wɌg8xؓopQxICY6({+"|Ѧrym0 tٜ)n6Xٮ ʝ+iYٟZ ڠZ L@mVSV d(ٚR!.Czu Nhb ah[ @huH<@! #u:! b " 57 :"X: ` CEH L"N@`N'$Ѩ m -ݰzwM`Ob 6 x " @"4Lr!a&{p"p!(b Fv@! ~x8M`~x`;js ;`_  @H"<)Q!j` 0pO&$&;){5%@^V"~ 㸓{ ..(F!>` | ^l8@[!!Rp*h 1_%xO"`2`( ,q"j n\Lͤ/<í`| {"J!nAF/+B_hēsB F",^'I"_B "A!4 v\t W cT4&3840 :Z W`*6{ ]@V' ;e O@.,4R }D6 nf գN=5N.BrR՝|]شF^bʑ2oҳ1 j0 3 &^ e GcA|N >g<`ubg 8\@! A:@@ up%D@F|I  :Vt` -"`@Iۺկ گ`<bfؑ>xj0<E^"Q @tuq^=X* Ğ͞ 챎ŝ_>5eO` Of~ `f^޿b ~Ǿ (cc>fM ;*2 `l3@F.`n& R*W4(D@E ,r `u4`´7 `xԈ S ~\ <pDl0` &I0`+3BPqD9Ha|AC.l7dD!eM9 ` N] 3& ?h9@aq08)4!@&P4x@<2@ v$pr+"K[˘3k̹ϠCMӨS^ͺװ1h IADb_dL3:^ !c-F %j`0҃p^,P"J 8Pr( $M _*P@!\Lqc PA%dJ'M@|"HS#PC ƑbuO>dq]RtAT0%Aepwdxp u!~&a>b<9dlj衈&袌6裐P pE$y-w ABCFXMw3QF`W,t| !%`DFCdpS1B &E @ l  xm%,4 pAATSתF hAP*"hҎ[M KS1y ewʪSvP 9P&Hdp$k-PGFpк C V ̲tH E PGTTWmXg\k=ieDOAARpvKQO%'L[ w7~ggHG `Fpwv Zg@TdGXԃcӊSi0C0>I ,ZwMtOA. +|r*jF>8/ګ:JDH{Y S E_,dP ɁH@6tL:4 X'2<5>ӭmCiV"~0@$`>&̳HC@ 5`a0x`lN"F 3t0#Mx{I> !xrp$fi\caH6>L1Δ7-Dp 3Np* RuۤVH8 .;AP+@[xƃ@%B,$R>hY=L2fB X@2J@:y:}8 @ 2B 3? E~98A@U A%*F M`:`p!ӕ }>HGkX@Φ8DJR#-I}28B*ihp@DAS|BXf'(ai8% L`PXA e-+ZTAPɢ'0&hVqif ZfhFZ pK\άB<J',@)P@ MK]fսTLzMz^ YPDnHX .ݯ_2 M&;'L [ΰ7{ GL(NW0gL8αwxQ rF"W*r YF62f" J^r,w2Wfr<-_Gsg-X ?C/9Ira>l3J\e1kϕi ::Wt3#@Γle4O3VE gXΎVAϰu1gFc~m=ЇN3fS{׵6)k_ʡy-hcZ6KfZMbwܮ޶hIiMdWؖNul{{dž6F f4KQ\&w M'V?>ۇd+={om?v=3%tkri_Ffr6uŇ~tpgwlgtvۆyuw~LJzo|'  T"'z}(|c85r;|8|؇.mg gjtXlݷyfX 7eh燘ᆁxx@yAqh(vX ؋8XxȘʸ،ҘWKp5PS 8-BP!Y&q XXw&0(KX@8:huZ[31]Ie'Sa君#)4 Gp9ꘒ.#9$ɒA#)3IhY/CvPyR RX[9[D ;@g(Q'ytI b0xXhs_.c0(;y([q$/"5) 50<2mDV@ pVU0#p# 6#p$p:?@  @#dyrI YpO0 P1DpV9(ID ٝ5V,P0?<1tC9c=d-O޹# SXU%ɝ٣?z99-zC@Wyչ<mpJ+ -tQjZ I*#8bY|SczՄp pI8 >Wz-04% 'j%RFY]b.w/-U;PꜳZb3s_@q3UU@' 0% b0ڭ(A[R9'Q눭>y O9c ]O(8pi| 0/ߪqJMD[>ך`XV02)x# jP:;Ϫ:-hXz0O2`9GD  > A2'x1P{R[+qTa3KW-ar!)XQ?@)+`RuP7h8X|PkmOවKpy*/'9{S}p -0di UlF ڨj&p+tU'X` +x|[>q&*ռp0sS{$2@T'K|;>K{лuMM"9B+"p }"0p0;-+<L0m-<= MаC  L|:52=lSJ =o@ < {L=ә]N@C i \k }oz $#rmC)p` K5 8=@ X"<;6 Ơ8 [N0N/0 F Qsr Mt@69G0r]G@1 ,]Ņq$,]3"`󁼖0[ ]c ->C$ !݅~ 44_ %'}V]݅El:Rk(ɟ|+Zڧ0 q^ N-q uAmI dm֩J؍@.CUN04 P`v86f{ dg!.-pл;Jq(AV>*J = QZbD0eqjiNґ~^|^4)Laz.Q@q0 Y@&o!pr!خ 70x NVf$22 *5  E08$qK *"Kܹ~y_9.%oC9k0,RMŅʯVY!!R!-"6  uUZ] "!Lя]@>y#S) )$A:D# @… 0\hg4@PSG"ZĨ# + .|E $0tXbƍy@Ø3kLR" C*L V%ϕ/!6&$aHb* e܅1f0V-[:03 PX-n 1L0f@rYլkRA C)C>Y|)"CҦQ3N0pp"O^^ @8_] (VO6 c  jJÎ) >x  B :᭸jr  [c 'B z  &0"v8IFsB[(^  p $6ٖp E4``8(JjB,Abȯ6ߌs.఍!tX;[8P!, Y2̅n\HG8@4ӔTSúUW[Aӈ&zMpR,aؾu^kQP MbTNX(,Z ]HVߨۅ-VY6v!vU0ڗu}۵VA8~8`@um$\өXPaYRث:KزhH }zCX860 ,X  xw 8"],'*((4x\Hv덡h8[]n:vn`8̫k`hb\cf\C cl :`(ڔfګsf8iW\z#~fh`58!% _g86^(?]ۺk5⌹ۿЇy^, t@}c!Ȳ:!@T2(hTbTa/42Љn@Г!T 2M&"W>Vu^RdiQ<=P BS5C@ B4%8<1,]PA!4x28d)j"80%pW@ zL=2,*FdC@p2zkˆ09貳`ɦ<@Uh4 j.B!@fWi$% "@XAlT0ƹr"Dȓe޹-}k<Dʀ30{b!:І &: * *K PhEIPxdFȽE@i N/@Qa"Pt/)LSԐpʲ?ʪJBAO># 628!Q`JTRժO%8%FTxp50,$'tҔ؂41ϗV7D,Пps:``!XH2 &e yfqbh>I 8R.X_v0ze.{n,N[mzeU+r{ :k4B 8Xn \ղve U ̂"3@7X6|%@/@=58lch#4 ErVm bpq!,GWV&DVMνmBqdAS(\e/yB*h &`ǨۀN[ut 9n r*9^le&T Qҭ9͐5 Ilbm.jG}jg\0͠ {ӟf^Hg\c0u cJ?=%E^ȑe`+AֽTի Ս$Ч rʠW'` 8TzET9 b`E.xbK`SP!a@l L@я̂`D`%_6qT.0 e `@D)Cp+V0@0|m=9UӅ #zu*g܋l v`*}ma50E Xl~K[z! #p2=#,;KC# H[[+)?뛪?؃d`=3A;XtaT>>;ñ۶xSA[|:i@K6BS5x$,h3! 8'pDIԸL\;8!8(B"ŭңNL,A0 H: 5PEV 8Ѹ[$ { PD[\KħE^ _c$ ĕaFMd"-KFkWFkq x4p̻(spbz-@KGXG{LtE{ YXD]H+8|\:SFkdt zLFMw4rKw8$h ;>u5?=>,YӢۜq.=meтݵ٧[zYd[Z̈́%&^Y%V~]ީ ۨ׿m[ZE_e%xmZyS^/e߁_WMZ7]%ZmU-ۛ_~ `=_k(Bآ#('G@ݹϹP8!qxX"] +ՅXb ]EoX)/]MEr%c c)`VMڵW-6&Q)P8-`=ߴu` 6c1XEd5`dFvcS@.G^W &d ܝ߾m[{@UQez_c v۽[6[^5߷E_ubF, IG81zX;ȁ%&r XЏF8xKё(@,`kP}x`Q&ܻM_]`cmuYu[2fMV=BN~d[Nd6RDC&hZe`[^_]E`gm߇Y`h_m-㯽Z83Z W.jUSLiiXQ~iHא[5_:dPKf u}M{sP.H # 6 )Fu-ꪶd<>Qh?R%Mldq5ӚfMNN.Dfei6chNe]l=EiݥQ.iזh^eNϖ^VچVli~Lid6d퓾iPHXYш)n|s'@ o BPN8C );N`@.ffXcmY`+`Z'Xӂr^ nl\7$Z&['ws]/`2j^n*&vkGЖjR.iaWWm6Wsnmȁ\5^ )2+4]$k~ Ȃb֍Z%c2tlZƮ >8RR+3U6 j6Q>v_8LPyEҖu5yhpU,n.Rfi1|_yHrFefmpp(ntvP{V7fc2omb?{v`FPw A*#؁0  9A$%:C:>3 x8h<[=C P۪w_c7sv,c޶r>>vngYoe7]n㎦mz]>_u7.~n{3w_c5fvfsGٵ/{GZOuvo(hp „VN? 8 x * : J8!Zx!jhAbqa̼6N"^dUA3т@DB4xو)1U@Cm#78P '<: S$8PVcYG2ec ]j H2lJT5hZ)̔!yia&d!:u"ilӍ.UTpW X@@"x0  W~@AVt@`Cf)L8P 8 DBgpC3&5L X@$X9K/Bfm}-o] C@20e J/{{/S=lƛ@$Jpk, 8\ C@tPlz *. MpCBv p\T003Ϲ=.@K7tL;}Od/e}9_KP>̝#ĚM.l: d0/As oFp`1lQ@?Cd!B16,Ь WZ T $m _ i@  y|A,+@2 DԵ.v $ե EN%Ye/[l!I o_7Z O`@-7yľ{ {c&/ f:,HB8F @d4 DR` 98"e1`-{`,|e9wӌ !Xz1á=hz&?N*+E7ڧ M4dB `rkbabx>D2L!)zOtz1ә@hVîQ?Kd== 5C7t/0 ;XQ;*_Ld@LdNB Ԗ]W|h@BA% ĂR@xY D@t J,@ \BBa`J! BX!j!,!@!E|Al!""b"~_\p@ "B r}zQB>@:@d:<4 -"6ԋUB .p`ݱgÔ @C€h׸h aE`6†7&p#EX!:::* 8EJB< Z/"?cB/[lƢ% TS{`#$=FdD;:DEƬ#GvG<<`UjlG$NdO$OޔNB4TPQ%R&R.%S6S>%TFTN%UVU^%VfVn%WvW~%Xz@`#_S Y&`N @>Q8Zeb|AHiXMda%lŌIcbeJZ%T%Z fv]vXfY&`f?ne\@&EM<|f^c cvХgctԝmZnVDoNhQ*Jw]ff`$Ǡ4Nɶ%FZ:WY(sgx OH0&fa'aF&nZy'qG[^]tT`t&h5&z&A [&Lg=9 a&}^zx%zs'U碠t*|֦xbڧz$~fkfg]|fenrJu{l:'Zhnی"cV``=i:o:in)?FrB(){Xfgb~ifisc'R4)2'EXkr"svz*'*%fzi>j]j)bi&:ֵQ% bYڄE*fp>>&iF_@hB)mE>+t(F"h=gyj[u*{bi毮!*^'\DNLԫM$*l>)h gUl ij""fA(rjAwF,~,^\l(TDZDɊ+lA6'.©F'vGfj,jl(cFԚDѪ+^x*Sd$̱F`RؾZ0j"-R+vhbf-ע-thm^o䭍iޭ.x*ݶ"NzB.fn.v~.膮.閮.ꦮ.붮R8(&DIqYXDV.~T\ExY%LB[\oHhc Fn,oOxoooJ/cnD&/絛@M/@ 0HX*sTgd@D%˲䌶p0x T pXK p WD QBV 1D xʧ $0dAQ@O0 # @q1qO01 1W DoRx'Z 1@1B$2@ Н[H]d]p@XtѡX ՜Ԙ\Mլr*C**׼Vr*r.4j@X0 D$@/S<2;q8D`Hۄ/2*20cF1rC;KoVJ8WRQ@h"S;uO@vuS;\?]ۄW#TWU;X5ZtO6)^oE^]bUHIlTV'DFENh D \vf#Dhv6XgDG A q4ҡ&q,M-7R-,)wrے@Ȓ@DsO !)u/7,-t7wu5m,t8@<<Q,{7|#Cw{! 4ʌw[wyy7\W @\xw8w|w}{&wKҋ 8Sx78| HkT8 $N(C$7)vEE%mJ9@6[Ҕ;Om37s*UQOVUUST_yT3 E9T)Uy3zTpC$ AzϛWg:TPCPcd@ $! 4QBxE zyy@:G,lծG:\CMҦSD C Kx:3{w/T1(T@h4s p0 L{ {;ÙP9L!p҄@X XWyc<Ɠ<Hz@3J<2'UsAx{ס<|B8hC|ƫɏ<  SL F|-qC$ғ<7MTշ1ϻ7 lwFQ Xy ~!||Ar-ӡyBoD&ꋚ@>>X~Ɋ 4,@:s# Z(@,8CC~>~+$@ÿR,[sHx>>@0Ē fYsC 'C2$9$34tb  0b,7Q)Qt,B|\A8 zx=е- | ~r칱ѥO١礣>r;z-rxqǩ*WӫnjpE ", +b2ܾ0! :4 "8}3daQiFhL8"nȂ FjU( N)zG,J*0SK)$KI24|+ @@h@xa -P &*M J "i,43Ց𤊆 Ձ^5Zz4Rd4M/PG-UTt'\Ysfu`|snTJtQfhtBcME 8\swzBx@|[zA#oX)Ҡ?D V{DR$8`Ar?*ay^ek>g}mo祊hwi$8A!z @ء\y$nv a/§;Jz ma .[辏rNģ=hԎpKa~Ӊ+[[lFAz[(pA<@ @ a ߁~5+Xx Z$Z?}߬Z*L L "~[`g4Ahp4>xS6Z@Zt aCΐ5 qC=D!E4D%.MtE)NUE-n]F1e4јF5mtG9ΑuF8A >${EǑ`8"I &0 c$ f.`JsL2k:3ڼf5qyofs$Y͋S-;h@ IO{eg>9(danh.I\a !*$ `s Ai: RRM(G= ҆(0:&8PlNt'Bӝn>g.ԤDC]P_ թj8̄R'ORT5$+͚kO6Fr`& kdU @ Pe`>|!-H*ɆMQ;#NQS9'du/{9]#~b&y7+9EK $Ah~Dk5 d+zW;t:=$Oz0KV:Üt;lUT8L = >  ` \H@'Aߒ;L/Joy~t۾K,&v=[C4ncOԘװ=U @ ^z'ooы*M7 wg"&UB_>!P yp$9mADޔ`A%ј9Klϫ8}/L4oՄ/BL:vўd$&l'Rq"'Bo˨/|/sH=C$/$3GDLj2+CÒV# ~#8n7 <*$iD0>Ȋ0pКR]к* =KPؐVL QdJ(iNViY$!̤e METHK ̬V S4SO`W&*N֐Z$*6~E T,!F / "m 0OM:/ I,I]]] lA vKĎтpb"gBbrri'H8 nftbmm :闞 u}^4 *Y^yg#Ra @᾽`؈A `!sV HAD!!.½X1as6!ga?O?gS lA!"+Mayv׸:.?7u!,q!_) `XaWS6|nv  `ΗU`@A YH B|Ӡƍx2G$I 82ʕ,W,IX(s]2uц76q$J%J(4*RӔƔ\ZYJNQ}U)%#g)*=2YO) ۻ' ;W@]L͠0x p,Pdj.fXeQ²M]Vꌅ Z(L:a" 6eXJ܅ NIpij+ls\̿rZ=Yi˓{>ëD <EݽDK nw/g yѸѝ&A|هg #` "HS ^yYS'abnRHE2jq3ʘ2")c$t`J'GsH"0%)^xNP%В P2ic%dO"^J#)ɗji 8 G*YfOy&`i# E?n>vȏLbcJa^j雪XB"Fv Pj+!RPD.DT eJ-D=^[mm-JU12r:hu6d Od.'ӥ%p^` G)ۊXJД\n'TY&uTo \L2?_Խ'#0型:TPPe+2*ś00Wis0RpJ0=Zyv3^pH)Rc X G2cgowrHߩwXC{tf'FxLw1Mna$:]8x'*0ط nJaHsC=|hiE-y.A{ _›Nj0*SR'Y 9chaG=('?P$seP’w=@}K` WT{S90=RB^-2JUzUBZM;XBH6̰]X@ć@Dd+[[(D%N dC"/K%[bޒ%%њܸ6c`MKNƂ@/Qc\z XeH$[2HBE*xɾp1% Gf5PjҌ''GٳM IZA u1/8KOg%Ha0|$A-ڛ7!URЄ)|*y~t>(%p)M0)j|eQtJHNEx噭6[:D $0@ "f_PLWPX%LkӞ^@0ȶPF( k<8sgpV2) Xƴ(D+*汮䢑 fȦSxC b #8wTBޖ[E01R8(nwJmn) ȑ.DfuUo(R4׈n#`d%x+8`V\BjxZs b;A ] `tq,F7%BlnxлWnx Є& 'b~Dj4pJ~Cuc;[]|Mz nz!эՌwGC:ҳFM^EIU?>c2"*RX` Dx!*5[yʺsC@O}E}r$)_ "$/07H0HX0i8"/ӿcA!وAĀ火5jkk@ 0@d xsA LJ@ ]Q#؁!1m`[-80 000)4<Ԑ40AJ,5]6c,!OU]h qw EeK $ӡU3 `15p:x/ @|[EfMD zPP @ @uR p;t&: *%9dR' rrO gW^bhe(.P#fVX2xRc>*#{AfW g 7A t|l& j;8 `6SPjAtSt|P%Q)7XUg>2*H?^p^R۳R@ҏ3"% )eGH%@@ ~ *ɒ1WW4I#h=X-VY$I- d aX5 & Ti2}nÔNI- `_ `DQlIU NYoKіH]IM ZK , A,8 '"jPqӘt"C癬bʈ}izÚ)iA׷B f69%p盄&9/ٲ Y-?O9Y@ pE;k:ة)IO By\R PR#ELdi(!97ğIa@ ^* *)j@I@y&0o!*#J АK5k}Y" / 1*&77Z=zA*Lcڢ*zIKʤ;ӤO3*SJ2ZY[ʥ]ZhC aE*PjgikjUo vujw){ ړl *Jscyꨏ wʧzʩ꩟ *Jjʪꪯ *Jjʨ<&ʫ꫿ *jjNJɪʬ j׊Zʭ **Q䊮骮ú'0F@ ZZʩjJKp zQKkj 3@z$p  @o : <Z#/˩;+۲J0j"(j KK[DL Qˮc0` j$kNJ a;]3Z=?˪48 _0p@[ OkZP 0780M`h plpzгu긐+pWm08@% ?ꯥ۳oePe[P T!G pٻ;P{+k뿔/J{Ջ:K-llN?E@/*|+K WP. 5@˽ M?Vp[9|K;a,Zŀ "гUN0/``4`K_00kH0ސ @\Y~ <0 I`$@qPB t|@o@)z˹˽,Ȅlp|ʯǼɗȐɆ=P:u͜p{AvsΗ"p # HC<"p6s^~*w m;/_$}^KWM饮4GpAg0|{JhɝA`HεY-ٻhf۩*.z p7.@ٗ*D ~'`Վ.p $,^뷎׎b.^0 ((P+ZnJi02 A`%KlӠδ>̊˭ `pXp=D0p#^JV=Pb8|v|@p Z\پNi-1m?~ Z /]>o}iPDк^qmU=@<0ߩ'8p@< ]: lM_!/z@zߩMV1`#7y Dd1 PPC I|0DYdb1OxX(ӀMGLA @6E1GaR|f qb3nx$C[)(D "TM! vQM6HYD2N7s)&$pF,SP fh FpB˙7'B"=1;6" O@!2r$Hܚ'O<à):0UD.Da4(BPA+$!:ʃ*# 5l3H6  1F*2j8mHp0! H#w CQD7W./W`= ?bEOXdUvYfκgV>c lEzb@sU[pBJѺ .\2.=q˅K8 b `(b{Ą$W;.NDΎ2/ hevfccfs9X^ xHv.h.fATjX pY;{lz6;?V{m9$n`AUJpplSq#m+r3sC/rK7tSsUwI}vkX?w{Ow~x7xW~ywy裗~z꫷z~{{|7|W}w}~~GpL N0 xPN X ,`=A^; . g5('D`B΀Ya ӳ D7!NbA#PSbx&v؂P'Xa#DKWc)XzcxP>@q@ pPDA D ?PS( D B,~Q PJ1`8Adr$V7!^2<'AyO^A#-dd1;Xzq `!O; KslӲ';RhE1(H7 P!DO Ox (Hx h.ėa;'j+(7e{j? }_/Vh!~w{FG&B-V (>'+oR \.6`|M$H- Kӓ)*r.8x=2DqOqHq k0@;4Agɢ8Ӧz:>K;,?;B$L8, bI) oCĚ B sa7JCXS#"=;>C;"X@x# (5P ( E 6 -p UX*8yKKi+5&Ȃ eNQM\ŽHWpjӞqhbf2ֆuQղ)=Q x@ XAwWe$E .Xޙō^ 1%j :4 #,?A"`|*-Ö^ߋl͔5[%AVGHM_Np # 0'HU$``ac\.a>aNa^ana~aJ^aaRaҢa#$b#>bلzD( :yytH4@8UE},#Ƀ-G(H*60H,EU(*|{D5"]$dAfȨ&C1 2, + HWHx?@K G؁0ԁ*H@0RI#c@da9UB;"8K$˄0KXFj%|k%e+5e U6@UhH[fa矍d%8L_b9(虡 ]L Uk¦8&+eZ@\ؔMڴ̈́Xe?3XW7Krs&JZgh\$n%`gޤ&O0O9E!m1^*%J\j]GU`Ȃ58)ggrK~+X-XiC-}&⥾lmj55f#2j C,xiS6/ЁB Q3QikQ&;nH58JVlul<ERg.o=S>:lbS6=*X.Hl[Xf'fmVhVp3/8T7&w]qn2(=+g/Xeo(9C:؉m:k)M%s 0946GntYsvgQb3}s %tCD cilQS(=8l`^ڬł[YI"+w3@6f[W 0\=U*[x\𨁝T5[?lblƅ\5nh\LT'`[N@P@J57BLNW{}@BKϬlČh󈴴gho]ORFaeM'(4q@C>ILBLODhipZ]Ha}s_n^aLؚdvĢZEG<￘oDGAVYH{﷾rmqRf惥ihlPOzγkxx|V{X=?;zvzV026.O?{];Xdem359rwS6yPӽɬи˯ßJr9;8RUEDgotT*78_fiN:==,D>d1_G~YƧb>]0F,Ȩ,=4ptPkloBd]Lu4pL@_Ab[GmܻHoFkMvCR HMklp=o:e#A{V<[8R>?GH*,EhC("Ŏ0B( PȑBaB"1K"h x#Bqɧ1=*JdSRBDcR+¤qVKزh"$DڱZ$$X`ܹuԝ]yiItBK*k$Pg"|e_X$CpKI,eIH-BȒN5ƛ9t.]/햠j9cԘУ,1 ={[,iᕏWpрK.XQBMeE71>{0QZut6tf?]F0TA `d NX Bؠ8 1 R  7ly1pH , D3 b؈@Ej$4`НW9 @ X |=B j$:*qmе,0r(3^p>T+hE 05R3 CQb! p!1APъXԢ@DYHr#.8Z_ mG\z "-{$fkFpFG[ L@(D-s t03$p;e NCĠȨ 1X#E (P@rL%|F5fcOM g Ba-P@yF2t))a3S8!x-8 pnJiщ[$(pNOU !4@@PM*HD !d.p-\y< )J(5! V{jWuAܰ#G09 D $@ĤAmȳ?!Ѳ!6Bl#,fFHx@Pa@K\hX >,0f!u^ЃPBd d` ׼ muG^w Z#H@eĀ0XB%(!V@@aKl ]rW &FqW =}tfd3qw xj GZr E0_k<\U,#A(B|KC^3؁ `Pa96D %ImrH P&b, P\ ! @)CmuD 972 HL&XaWXpL5@P+$L –T{aժp OusiO[5µ@tvg`׫cs[Ҁ&Gh29/ `(ƴNku Ɔ7ʖG;ϸ #DEȠp3<'xU>?WQmb5`$= XpT["Z&6, 0M,ԭu h@+5?7ge6xJ%%{V`n|/S )1H]V"PFy7`0@~Az/${/HV)@+d+x! A =W> s?9D~ dN EffDN0v0hPi{%pPv?-d;HI0Np&}P|:4F~xo+%E''U%H7K=!!reg^FS0(4hoGtЄ2o?*P4P Kpk b@C}p1=0=Ň~sq!{Q{1@uRGu7uxyH)r ?mosp~jtR!a`t=06! L !1 'Ugu?J4 D{/5rGCG pP@r5gPWr/a(tyk! 1`Rp\07*TO-o@ 701W\a[)DH `= ٍ6.W386HH$93ɑv P8V:'E' 'r8Y?=W0\@rZ diUF5 8Yf`XZ}* /p=W/H5p'8%ف: 5hG  P†=T- 4 &O ( &F`RvU0!%VFGq5[2x򃓔\Ј48ra<@p2Y!Z11Hx J& CǕ LHj^(jx'J#9JPp>*4@*T Ѐ[Wq[) *YA 0>;1*@L6jpPѦkpv5!v:7#[@xx 0tf@\aTP4WCs̻k{ө|Ck֤u* ~%ɺ+TW){[css1$GWJQ{f% *JAw#a@Xkz1xA4` ZKa8Y?c ʱ{$ +9.,7<6D1),O6kdn%`_OpLKkm4pP%!-JܵaAChY,@Aa cn[QJZbfOP '}$)z}#AU[¡;E 9{#N.&O0ȩRa& DgrHen TX'}F6 `=BCiy3\B6XPR* m${$z[0є  T~Gb&:D"r H@#"%/'}a!=Z`qkQ=՞b*P-9`)aN]A@7H=! yZ=SɛbMf$%5Ǽ@A3KUh j`pKL S, C-{L,!-p]@WjD8VحY! aPMX&D]pK5رE,`ARRg0TtZMs<]?~Ѹ.ٽ}-2 4>Y0"9kdݓ|ZP籍1$8\[ ;0=FJ`"V`NRrTa+6$"Ef64@I>iG"4PPpN&MQt6@~ApJB־0 eOEA$/GXw~{->Nnn+>n rP@$lI>B~{5>F;aJP#a47b$=>0f[%^C6 c@І+4Z0NsQ`#@|ry6" amWwב̬ Ui͚v/+t'1OP 7TQqf +G#easA>+ͳ([UaMzWhG`oTɿO2Hc8'6eS_'#  DP‚)*(P~0QF12@%MD2G-]2 @@ Ip 0 UBXdeȢ3F_Eme G oFvd$g\G)tlJ̗,I2d*2"XSX#G7'0L0!AD8ZDA@Ё|> Ԣ)h:0`CK`U;הhH$VVaWPm)-By`  (eE3!]O2)'*!ǝI\\*,uxURr'}Tt+iWW&p-7&5G\pحJQƞ"O:INPXJQ HXK0 G: E%C:4.!4P;v0@"\&qy-:S ==E *1|ar-8`&@1~@ Qm05vЂ@ ]@S m42 &XĂڶ\!@y@$AU TM]kܙ38Jazݸ4xŮ^KJ=tj#4AΏR'elkKgᕱTsCZWV eY90ꡌz @)iB4⸝P8H0'/@ xfvpO pG1h7 8S7uK*=R`D8Wlҹo !Y׀n.C   FR0ȶnB $uU5XEx *`<BAqkU6&/fHQ2bNcW6:Ruɕ;ˑT^ͼ.C1΂՗gdHH9:7kcwYSŤ$SY77wy["#w&& Z6Z';QЈKVhݩ",N#p!絟 S:~D?24 l;`m";8BWn`r#hqoj( N 04XAD0R y "|7yW?7W3 R@ }V<L`>p>r\ω..?+l>: K,CVBQڳˤ{9*91c@J9;K:CȘ)Ne8PX.ЃOVPh;;!C4 ᾊ;x؀MpT?0R@R%eR0}Q)̓ % Q,M؀:7%J4!9[@Aj3Bp"Ϳ*,TT?MEw,NբMwS$A#4MM,lD5kT*@-8NBU43!n(?cԃF0>Ѕ>Pʎy\ee3@<Ƴ#a#X#a.x6)1V.cՂЀc (+uE%VmhEezteyݑeeVy%|WvǹxDdXZM~W ءψk_XbYYXqؐ0N`Yؓm2ӘITYYHAY;Y ;pYuZ-}ϸΔx 0ڱe۶u۷۸۹ۺۻۼ۽۾ۿ%5EUťh 51Ғ8:ȏ $Ԭ5 9}ŁՍ*N !@̪$%ۮE9{@HAJ}k9pڧؔmPIj;A@QS1hWx`߯I_fq9ifmIoAZu`@~ے5=юʳس^8C}Ҽ~ _}C9d=lD,aа4@Z7$a@)b#ɛ9M+b# 8ZhH;걞!D'*07V<$ :FCgD x" RF=}\"+ڪ5Қ%#!?!!W#=:Eގ*%bz (ࢊ0^3B#yQ!$`xfFQ%]%%Wa*cezg&izj&l҂mo2:?{hH@ "pμAxg4$(:(Zz(<((qh W Üjd I:))*"*)Yer(**zh)1N ++)iH+8q+t?h ~[. ݢ".mNZ 犮="N/>"Q:=P> d~%Ro7hs0ˑ %[v# *&)wր "1" m82TWiF6L4Nk;0=hS3X5%h3cS6ecP| "g5`ܶƞ3p{. r;7;) >7|7~sk1PCh!0#${ܦ8ø H2c֜UۃXjq6 {;膮鮻rϪ/#jゐpH>-@ @3΃)<8A!W=G> KkӦ7O?@˾>6S˼K?c??S?fSG0TA,!$B#DBnrY:(+g0%0v/a Cî(OC3laPC6\C7|s:mn>s\ vĂHDDxDD1S98 L\ĂNQR$!UdEYE[|BT }TGlHb:HlVH`^HuթȋžАtEJ Ћ $9`Ȃ hCɜ)IJst8Ȃz|:JcCd81$KLtUJED=3\ܿ|ô\uL#Lʜ&0ǬE2uEO o ,$dOA/ " OŞuRk !zPZ? P+e%Q-O=h9<u<] >"%% P(R*R7S2[BS㑞 hPb (Ȣ@x&q H;^ i$ʔ*Wl%̘2gҬi&Μ:w'РBϞz>FBz,I*QfPhɱ(FF`iDg|])ˈWDrXE4ٷ#\ e[{(/j2̚7s3Т%\ 6þ0װU&:/k{|7/nxTN-9)bV(M$s;Ǔ/oy^}ӯo>? 8 x * : J8!Zx!j!z!!8"%xbw)YZ̑ _\Un@"R:D)PA]+`@HEF@!`le]O`򥙘Y="]mOf9!Di!AK ҝy $KR9F`FK9DDUUHUF ma'bJ6(}BcDTtV!`Jrҙ"/X#Kp %(Iv u&ҰPF{ҩdA 4gKfI >[I LS`]PlE.XQB@O@@&(JRP&ЕDNa t0H0 DA 4`8 ~ED|UNb\skYݍA *Fc"eq\? !Leml =l=gM$ Q;KK @WLH% l*Bt!0@'='wK:l2lbqI:X`R6KS>ߜs.1~@l,2+ 5~SY ]t'D,Gڲ DJIF\@$ XP%^ `- Q \0<+gKH3,m$)հƴ1DGcl {C1@.PJrx7L Y(A">&@ 0H$  9GR[IMR ! $~PHءTp8*i#J"4x DJF*ofWR;4 .Aʁ8 Hg (KyJ0aUTDM%JD0L:;@RZp@rI&0b H@`f,I: !Bt0\K(D@KdT >̌@HV&4x+aBXE7H6ZPHV 1A-P+($8h=d 6U)tuU*H*3|j-j0֒ T+Um6 7@ t^ ,FBBTB-O@2א5kmZՂĮVŪVm@דVz5I.ThptB[Z.`6@B0:r S0z!@T 5$ `jP"WH !.2L*ABF 9sa-&uM_ HE:!T-8HGXOv'ol`U>!%H,0Q"D dx(0I ]$m^gAZJ8效 f.#a La 0ICUҊcb ؍F'$b(' ARP!>(jJ&`@dYmD .& Pڹ (0'v|@Y`&';L;* 1C* [ `~7.!GHS1-#;\gJA.,FD'd{Nr5ɛH}o\"xKs'Yqs 4}+6(2Ɯ]3HuPF0 Kpv`d -H@@!`-q,D,&D C7K8T1d+{aR'~ @8nU@`,4 xg$ur$aTjW-};H8y& [ٍCx=bOhwW D4.|@H|da%`H0iH [H@&@xv1LhBMd G Lu lsGH 0L <❄Gh @ @ 5;mB  Ԁma$U\ĕBh HQ$W@a!JvS:EI& (@lBhUAbY$T́?D4$.*II$D*IlA,".F""HHAX])@p@ b"3:i J2"5D @;@ x 06*|a X(@R@ TYxyAL#l|^Т[}H@ WΘaCS 4l@@x@ @A`Y _8E)D\M! !1H!@ԥ`}⡼c<Ef$eLҤM_Sf Tؕ DJdty@%, R1$@ 7j@P_`z#b"f`H@bWTUL>a^eH[ z@9%%]|,K`KGLDa ](xܱ)܍NKD(M*TH Z- pvraA-@I %R 4@B\HHpȣy'IDxچz@a}*pIZ HU' \Mh*Q!A,@2 8w(|{uh*HygH(H4@~ <X@tcRgP(~(( .f\ p`hA T@CdHdAn)t)6@.fx&Hh$iG?##( ]gvNE$0B@@AߞTЀ<ހ tJ0\X3!\mnӎ^j tЎFDR诬)6YjIdS"ڰV@t꧆R؁0QQ( ٤ AfE`k})*Vh$tRp|T D] NȘ+Dikz̯D@o~ĬDDy@~JVL@d4'm$-QN! T Dlr0F^}H,mFު ؆ iԎDӭ,!,DڂD X( p_@X->f-:nBmH"-jxӮ@z-@XA 낮f  *Lb.HTj"/.B@A HMD<.0 m+~܊`HRoB A|R0 PڪPon`~Z:mɂʆƌ1,-J,oZ0^n0&OA _ y Hq@^DqnJ1HЀ |\ pjfHKA|J]̤<"@"ȞlʞԱKH`jr- ,d@U(&3Cᙅ$-dh䂮IfWAH@ xr֓Hd=EےRF!0_l lD4HDS2 Q #K'G3I+7ssJrY*d8r=%)9..v# e:Ѳ@ 7[5SS ɛv}j@0p` &<rEp |xIPAA  b 4#AD0v Tf@n(bA SMA">3,dacH(s4Ҧ<ăL69E<$5`®>x|tcxsfP0'\oa,[Zދ o$#$<$n3nDJiƈ`4e$\[183D0J;FG'HPn$}?$C-pG3P% 1#x91 "I@4G;>l.9? TACP8C\OE;PR@/:8F%SM;/QI%RQJ+FH'MUG_}Ta6E4bŊ=NuH@1ިVmo w8"T8Oi lō"oG;W|HY]7{ .NXn!X)1X9AYI.QNYYnaYiqUSvdc=Bߞ=PhCҜ9QG%h:hN/i6~6i:쮗:8lV6pzm?q:{fwN;˥\?Tr)7q{SAm&vɶl-O𦵖Ysngϗ迁lɑtyA~t 'v|'^t\Q틯y|fW| Y!YD;Bo[A1rV)ⱑ4!92B҄"$?}` 1$"nwIRJa-E[v],kG򋷌b(q cfcy[.[ljSTe%]gl IHC2y%-'h>$6#"1{"7wnޏ@`OQ\I0f0G)75BtAPӁLd,75n ;30hm/ܦ)ԎF}L3P}Df=:IR#~Hsq2MӡhI_˕6L(PPzTL=O=cTyt{BAzofcC7s灞Z:EQZlLDyEl^FH]R/m^Fs9ylT) 9w5v݋l/[E]kn ॎ4zYux%;uU\ۚNMRNk S\ʿG^>ө_o\ż+2r'rA;G'w} ,|_ŧd?|_?1yoAzя7Qzկwa{Ϟq{"a# &}J:.xpi }a !8ȡFAl(Q nl``` ;*!`. 05F b(QGx@&,B@ J ##E2)r)@LE nTg*2+W)9f;0,,2-r-ٲ--2.r...2/r///30s0 !Р! PF2hD`@>\A!2 5U34&>5e6, rO<4r8 q4 3L ': V ;8@9S9s$5z!S9/)3=3;8ē" `A! LvlkVoowGl(b .pbpv$ 6Jr1Ws9s3ws;w$,pSA7^aztQ(wooj7 n,jvkw4@ ^DX}7uwxxGm9*!`qK9 \J ֯4S548 G 0 @4U %!_bG~)]cS=u!IT`$jIi6@ie+&bA\M !!A1#NA(#o]#׾GbmX>ԥ>֛U՞_]ӕ^֯C0 |49  :4&])ڃcB``H Zn , r,}98wS@y ,4"oV ]`Ǡ#aVeaAB0A㭺Am4(8@ .;F,0… :Lc <|CP<0X"$B‚$idʋ bHS+/ |JAua @L~dP?Mh V;j"PJNeUu!Gq@Pr AjQ=I6} Y"=0Wp I@@!2 OL@ < ` 2&ad@@PC #~b!a*g}{ $21$` `˛?>ۻ?}`~6wK~4-$:Ȓ-441 ~5 F!DwAQthr[q!,| H|MPEա FH1("!WLȎ bhIfSChHOj$"BN"d&'e%]~ Us&t'̜VW aGekYBH*PZ  %ybRԓ!ɗl Jzh! !Q. w[ tm$uT-hOdP%U2%th hB& rKE;d U b> Fuea̡p /p>ЃQ4SN9 &q9,P?2LdF *l"aH,bʐ0ɥE*jxgU ߌYd^&O3 A)x D EB[wEH/D '+R)jtDP|^Ћ'qݓrӍ&t}O&"}{wZsKTTD! A&Bw١"=qC*<0A P!E * `Gp UBLlP$ $[B - ?z=ïCAр %o R &/lf5ifip?kE %H rg|`Mq9[dRYK10/U1[U骵Cfq _ ͷm d5ϸf.px%kyzo[`Rur~|hߝrߚBU]~npf]ng~5 В-r=e23\D;xvtwk[uq>ǻ\N7_* O2!}tt^5m~<ζP ZϹ.yy ]E؁+`+rWsdA9>jL_V;N}-w/V7~]eu.nqe.ug=Ezn7p5GmxEyi3/ݻVUVwG~];S8PGWp9~?{_7-5;Զ7,{ٖh/ϝۀ{ԅW~o|!~wD[[nši36N-P~1n߸ߌ~dG7wa15_l^;3{/ ]z/oja>syqZ~ad|q_Iޖz.5}r"~ Hyǀys~%^hȁ%hhHd5%g')+Ȃ V-1(3H5h79;ȃ=?A(CHEhGIKȄ B !F `/MXxU(]A+`q< 7^0o0q@?uh0*1BRhL#P%- YϨdhW8/@Ld-(77lXh5⸍P`<0`RL(8I9JH$p3uE pL-P%`i1KQ2I` k#БP   QKp P6 [H4a40b.k r Hx[9:`+HT0v 1A[YU\*OHC ?0_9%ҡ! % 1+B(P<Y )8iɛ雿 )IiljɩɜIU'` $yX9 gDy@!H@PFu`C u@ɞ ~P QD@rJq(H`1  AP+ <@#J =ph :[J $Z@#zH Ѣ  (sN6.ڤLF `ė   6 4a#q Po j 1eZyJi PF$ b7| P;Hqj0 6ROPP\(z9`&&MAG 1pM0& 4&PjZj ?*C J##4Pѥ $BIqk)5 ^iEKgj;ak mz zK^ JDaa4'{>0.qJ*h`c RRsZPG A a@-AJ1 arY/Cpa3*K { ` 0p ` p 1 [nPBoKip  iZ K*KpzpD !k t #X`|t @'[Kg i Y p t; |HQ Pכ |X 8[p+[a# #o pkpѻ+< Q 9p@`Pm P-5`Ȓ,  %YD>/:B[1쵠1C E|q t-P6 ]踐+pJ k `` $PKv0 t`p  3@Lp . 05 j:K!F w Xɝ@ < ɒ"j"@ %B˶סˑ:jAkq ^@rLzҀP!l\ X` '`C  L<˜O2̷i@sA ` p l@ʔ 770b*j=!91bT` 3Q& M\:Am dC!h lȈ<\}ȉL( p h$tl$3# 〛b?#J ዾ #4̈Z@P{!Ό ,jpzpZ  A3 ],ܶ' ]9 ۈO٧CYt ΍Ѝ `L08ӄV0v J>P 6P KpB9:S$r@sNT6㝲 Z jL$ rNt3Ԡ @orn aV܍2ХP钞(-U~Bq({@ +>鳎\Nײ  `"RaPQ/pH: M0`. pQy.PDm7an^ AB@Z/aH)AсKnI>Lcc509wB>~2sP@_g ^BB pX!PzʈA +a }!k@ t`- 駻dR $ JDot `UXV8Y[1Жa? Q<3braW/d?)+;17Eq<iȦ_;0#Y-`70-FT\;bKQ % O 7P +n? ȯ2ef .0`24o߱"wk1ƍ0 {A7b+D.:YDPt@!HS+/|2#q1, GC /XТAK8Qs%" (yi"ժ`c)TԢf#;IdA Ҭ\EF,NHR*i1xb[`ap04l0H#A7 E' 6y2ʗ0k- 3@- 0Lz\qɕ/gsѥ7|T{fn a;%`[Ƽw78Xdx3È!|cD0Eт8 B>C %a Pc)¥z ,Yj)`,38DD$R Qd Rǂ @Db+= %QL.* L(:K- F:B )uY* o˭rK+9"x $J,xb  - B3v`Jp4n $48b 48Up!dI8k` " aB h5p`Y2A4"ذ v0Cz^|w_Qn࣢Frq/˹=1;SIΌ@TDy; N SXRG ‘8@YĔn P;P#MNz9 jq qKE /4񂠣yo fi =d8*Hqun9\Т'0Ӄ&<xjY/A*<8M D$3DuVo/5+?0ƶ.)܂ʠ`'c8Xu@&P dArtc?AAcvi!p|Q d%T5}5C#хrXІ8t CC笰JH 0=̡ 9bP # ^AA1ecxG<.'FG@NwP֐# dqp26RB`YB%PMR$e)MyJT^xAR1#Le-F +X|K_ :X0myLd&Sdf3LhӤ4MlfSf7MpS$g9yNtSdg;NxSg=yO|Sg?OT%hA ZJ=bg j&&@ (D+2' vB$Fяr$/B`jSԃ:e~LҘv0$AGKIjE+:SPTC(TWZեRժiAt:Ռ6(V:Rvc}G{S^j\k:ǰ^$%{j6iel\WtE(gjZ գhqzVբlq[7_+MSjϮukU T.ddJ[ZV}km) Xny+wb-og{Y}M{+5-,~- _6-}[Ί7nXָ-S׻pOaxم+roJ U5+]ۄ XׅzE`ZױrX1kR+3Y5>n!`, era/ؿ5%t$5e^s U?m_5Yq\'wfI|V6Ȱ ;9֭j9˛.# 迢7kZ7w=͜g*X9uh؎zɠNuj:GzjeLVgX!oIr4[i޶9m-6fY辪oM`[taUn2M7){so7~w^\ކ_|7bU[~%o]vAoڶ w<|og.蛡l=1q*SFqKꑇ.p[s\UgqUsw{=2nj[|5aMcwP [Inn#v^=++k<%hHG޾'~*8ѥ׽(oui./+[lag.>a>}ܧ^ʷpȭ(hњ_~l>{.iM HZk6~DNp @pZ"?v= +$@ }A@ L{@@ w+,AL$x#E0E`DE&2\PaSŒF \6`d$EeDhDi,\G{$oT{$cE8GPGX,v&P\(zG|Կ$ȁ58C$H`hGD"XP H =x]!kY,H!@Ѐ'Ѐ$,`o:JJ)}I Px@(x'APC 1(JTX BKL0c H0IPEЃ KV8+F 0di@dEc"%#\`;5"aVVP5=:ccX;W Z,у8AH/h/yG) i@EPK a.fa&fc=,0AX!UXEhhg沱VpQ.e8eZF RSQghfU[F]gdNvSp fXWƕ3(ؖua aKO_iְ!5fc7&n7fb}Y v5y3b :ShVۄ_ş>zM(_v g9.B0 6>H IW%V`B\Wu8׼k`l5 Ņi5=脲фW `~mWҕP`m$X=kVlxkdž!LPE X8XmVS.`楂(Ta6JXȁpĘ 9iof*2 șFpiF ru.h(l ,^eXkhk@&R@#'F`"?rD2Vq7 9rl0ț ByzUr#G$!L`V Hf58RHkWU%3gjPB .sB홐Rh o@ߖg.󋨚Fے ?tAozG_&MdpM60oܨ# p؄!(& hlQ&`#Fpߤ+/OjO!>%p0q3y_,\Z{(u R2k!$Yhٸn5OA\!h;ӌ;8DnB*#MR\@xxhdeHRM^=ׂphxSxpCL Pz`kzw .K`zy{yvyy/fF حt?7u(ƀ"@pYHh1؁؄`/"(0` #Ml?7 Or? (HXIJXh1xx3~w GNMujBހx pxЈ\r\L64m ( CrOw ` u3L|"@);j "dB $GZ 4Dpb! UY֑ TYQH9Bj0Wp:p < Te#y dLȀ% P i r]K )4ZBp @K9+ 0C4fÎ-{6ڶoέ{7޷,_G>[e5pkXN13-0bDHjܠ!hN ™g">@o0 Fmu1H|!ѡ@5@N; ^|ȟyX_F(PY "顆B@% @iZFh" |T7X@|ŒD&q@i$O8"AYx! -"TAz^{ (Sz&ɧnD@HV ! @)VKZd TEa0U @dHhphP- .D bf Fh{u^{[碛Ҷ2w0SNvvpKw-)$Sv@ ^hBZx Hn ԠmwxF8#q)u@Slh%*tŊ-'^LqyV|JN]RSmuAB񆎨qc&^Mf/ tts;_"thDw߂*mc .bhHhA[@P <@ 誖bTܠv4FP&E}T"?U4`B w/YkOqT8- 7k; 1߿07 ;a NAd0i X,5Xp=tP74|AF]zH DCچ!mBBET006pe-︽P5A-b꺚ng\]ºp ]E׹Ͻ\YmRWm]Dz649iɒz=ݻptHɖ~ō4;9f.jZ9 vl#3f=nnxTvcvu֦w;q5Ǒ}n"5m^7pV+Y5"Y>4}~Q5/Y=F2Xw՜WWZϸb_;:xMdRh+ Z]v3.emlhOб1iqL#fÝ/>4n[o<߃: oO⑯r6g*ʩIw+џO^wYڷGs2 p wO|Uu_7cϗcss=AwwlyO0/ǾM󏿩_* =M`bمAb`j<z`` ` ` ` `Ap$@Q  ^ F @p#<F܀ ajn@\ TR! ra0k | \A|Tp@%:#Ah`\ Ѐ!aA&A (4SIF"U-@&'4D!BE ERd;%pD@@@$T0 AT@F$1aa$q"$AH$Gȁ  @0$6iTmWUE$;U X@,@wz%f0B0  (A@AX@ʶ ~vkzkȂ2y.@2@5 LmXxL4kX@0\ 0@(@ <trXY.u9c (cea*h6(E%& A @dld;sAPeKunkC7T,+9D@5GA`qKAatDDLnw ƂRk#i&I%ivw|k* ,K-.F.wA$w (2#x+3x;CxKSx[cx^TIWE^UrjxS*_bwUMٟI.n[_$`.ߞmیy`x8;UU\t95U-֑?VV]ݍ?yoR ٔ;Ɯhe؂ݮ] X]\؞yyQM\)xY+vٹi9ҟ'k^qݡ9Q_[WDZ8'r9ɥXZxڰ)S;'!:{#[ yWߘ\ܵ^U{zzi;WUu y+:niOܿ2>- >+*a !A! "*D^k.l~ "\A Ԁ HA D Dª +D~q*F'K*@ ,jԀ,ݷƬ*} 6A@1c+~磾곾+#ĚaCJF0`B8Ć nHqa6glyeJ+YtfL3iִygN;yhPC%% 9!HDp@Ê ?8zNXU+WB O^!fa%Ud$q&+ ۸gn DIG<K.^H2u G1XF򰤉f<<28ٴ %kdɔ-o޽}.jٺ{R +^b0E9 ‡~Q <%2T$ʪ@ɫ.p%@)` †vHq8Cp…"8D. D)VP86a!Vk GvC-@ 1@0CԑY Ŕ`JhCr5@68WxY XE*3GDH%J-L[`f!E9SCRO:dA/eh@ iZb P[}5VT](ZH7xoZpX2c]@dmY h h$]UdeYho[Օ#q-c9bUXe]~%h56 th>_VQaMM>U^Q CQݴBOPCA"afu1.ak\Zhg">!>EXZܚ:@, BBwSk#v{/z!.z9bn8y3aem]$@)5BSbD.W/M爌gOzJB((#'>2\ }T>_#g)z@CCCt\c FWӥzY8@}!IXBpnX(210h #I $| `̰6& мAHh )]Q0@D%F k}(T6F(a(Y<F“E.zq!A@@ X/$;a/jRP%(_4CL6hI[ڤ!`Zֵli[[ny[p[\Ur\>ѕt[]^v]D HU7EN΋|[ߚatd M<`J?YZ2|"&p,;!.zC0w0[bF0ngb71cb 2qzS42G'YG~|)L6rEe/#F0pXuf(Y򚭌d"%t^Orf763,*8ss/?Ҽ @00=r d6XӞ5eSuJ2bTj3+YoR9 :ѵ>5#]lc] QȺfI&zѲ1h8վ3;m4ٜf8ؾcߐ}/gx]ݍqmR{ܱ.wzӛrŽ-  ߶5oi|ʹֶS p 86xܒRN2lk9ətj]֠@|؍&ַFw˧ЭN47[ֻѵ.t{;h0 s@897n縯}D%YW7x<{2,x׿^vC:`C_so85_|.qa|?~-K?~}n(G._~W~_[D>*a. 9*@%4+D $PZ ) JЙ$ X %`8c%H`̀hyf?0 $p&B 8 +Ї9BoV&ABy0I }R N@b B"|``!J,c*&&TLM€@e%(@j c J`>Fl`q8B6%h@cqY @&0QD%B @p&p Q91"qmbLB&ZlD$g!I`1T" . /`0% xC ;"[ "-Q%@[8b $@ PD T e!*A"d&x'2%*"D#`q&P%W`< $&VR%N`r%9B$Iґ&. 6BLd&$LG)#r%* /+!u )`m hh^"a#`Q i$*[E*sH "`Dr!q8 Y "%R` H VsQvs)  1 23 !bj@z73N35WSFb@j* hfFߪ*:s;9">2\%+O3?34,d"0B#jCHN cBs44R$S^S.?i`/O;/Kt2D̀R gҧ1#" &sQ> l00K5"q< =qOc< Xڠ,?o+9"6RQl`Nb€ QrFc!b(M0:F Gw4` 6#`I@CItJ/.bdT@,s!UP ,uPd%=*@?*Ht,CGA 4>L!8b6j6XɁă# p /@U#2}6T% D+5P7 "UZsDa3A6|1*(@O[: Ep_T@,#.]2O$Z  $``h$R "A1;8Z`HI@t_6bQ"F4.@\T2Р8bII&b*v!A|H3 B,N%R}xgYc*'TUh^TO]ݵ+W7@ҒJpK$,_(R  0FdOVXCpiYw6jo67oc"|@ &`A\B ` #9L@ @"^,af_U%x,k ;RB#"z ,Z%%ecwuw}wb1r [xXw [NA|e*$  rw}SoW8hb~sC7+r3ws{#@@d\E]ta f` %ayeCd.%j>u}oCZ5 fpȠ#ܠ~7"Fr)Sd  D|H FsFS{UJAf<6q`bHC]FmNb;HxgIt(gy9@ 1`g&m$fAd &`x9+8Lxq8^?x84lpXsxofs:$@hG8Sv$%zp >ٍenMRwٱ50J\4u…9by!j29#`  T&`Rsh Gl9b鷐_ b&RmW6j PbA H""], E}Ƨ|N"lK%@ 4x)5$z!vA9#Fy!9=w h~^a (&9A E4$F : `, $!Zo@20@HlhHp Jښ^B  4:铚Q5s_7@Z1=f0`=u~b{=EHcA"]l[[ ZIkXC@%`,F=[#?S]ʐs:GU鉢\Z 0Y#@6@!d5W\A(b #涶g[D٤P`ZNʙ`6 8v`dj h9%&b 6< `z5=ܤ)7ҡ``` ƴ VPT `\i|!|@!hq~Յ_|!ە|!Lɘ ^<xZ|K K@,$ɕܽTM&v/#P`R nQNKJ'| 3TUȺ` HґXPB Ph P`[M}&(}鯲. $ Cc"`sGƲ0(ؙ?@+vs]oZ=( 0?ѝS?`Kv=޷]^ ^'6NL n\o ݂nНo*>|,Jf䕯(τOc4/P\^ :~m>F语3YW^%XMMɯ>NMOn8~.'l۞>OFt^~뭾:@jτ1ƾN$O_~ n?^-p͠^9qB~Y?.~ ^P]n?04?tm`T\?:?H"0nb?./ZOna?, QGW<ƕ.m4 Ȇ3 %J>nSҚ/ڌTjǫXjʵׯ`ÊKٳhӪ]˶۷pʝdR@E4g̙;{%қyA{gԾ ۴U7%C jݢڭ:hȋjY1װc˞M۸sލtR΃79sQIɼf MR|7?ZY3[6UyL^xx˟O_(x whfVZ{9FT址!{t`*dr',0qLGv"R(_jn1\h?Yvf)d(V)`)di}WvڑEc)'Zb`y柀*蠄*sw3ljVj饘fi{hU9i8ꦨꪬ꫰*무j뭸뮼+k&6F+VkfFe/VZ !u `0@Z̑ k \%/ @)PADte@A@VGpwD2A+hkԋrW7wĿ,P s5Bc  `1pP'Dp@,1b[6;`, 0la$@}7V@SLfX}*2[#X9(x=qPnTGnz{ dFtOplӶ1e@#x϶BYѰ@> @! `]@ X0 zG@W2 @PpA#(\Ah{B<@ p W#* xOǐtP+MAne=VP'y8 x.d ?9Sn$+@ܠr@,{ .#4 G 0}k@~zybd!90MB%x@0@6q,%Y`Ȑ21 $R !eX^ [!x8x#900{q@MK P.Ł|8۲@VcYpS?G·+ @pah@! ?C#.  `[ - L -x@FP,u ) ]bb-+|@LʶjM8QH@ E3 4#@rSYӗfEfG](UVtHA.p @P@ W=hBю~+UR6W9lGM7`T-hZYITTCR"sW}*L+]-*{o#_ Xܶ|arQ  8:R\ tPT A :! L8wwpnx+ }zށ • C؊j %,o FȂŀ@fw<7q}b%oϛގ21 khא+0\Ev#H9W*m]GPJ'7`ydhG b>h3 f Y I*RTP CpZ.{eYR #dVЂ DE@VLP.brW 5N-Pl fvV!W@ E_e #Sl6i;rɅ%C\:&;PO#Xk_e?{,$a~7zw0SeN`0s0bSyZZx`9ӀyYPve>c|C|gpiI0T@aQ?fB OY`YV0&CPz{7~sWwwACU 0"pcl67pU.xm`|;CS7RT cV*e}\ sO~$KT56{gvY s5 U WaXqQ2H4[{ ~}8LvqqBTuIw~$grWaV=A(0HVXs8J0YU To:Qi6CG84Lp[6(}6PQo<ېsڳ뼙#N5]9LtqJ M=_v\0֤X1m8T@@ȝۺ=h7 q 1QQ30iLh@1 ߥ}FCX#2'Ν "N=Թ`OvڳIu6UOЖ}BmuJp|!D0.4^ QWmCV=IF`Cl08ܗS^ P6TtkޡmqtK0:xJ^4|(.A5yx!0,Vu:=6.s~NC*bhF~蘎[ӄ*B0xHs^ 0GJ  2=@u#4P5!0,PP B뺾.PWd {]@&ݜ(7Fi*ow-/G T Z\QʅR .;<bPWk`&Kύԟ_  C31v|d/%u^H/N"  G6n cx} ? _=(C.dde(P~-GCe#ێX8"اcdAWdu*@Kn܁tW.f4ȟ_䅚(\ (Ac+A`Bdd#F C.iPA v71dԪ&70QB;l*3 #@D޻ _<<  k\QxHE8MD8j;s (xbO<$B@3@0hPvrm=KX0 7L@F˴0|PClI'%PI ؉`**"X{Ѥ~ *OB! A@֐N;@#*r ÒfDqˮ " @yTǺJVr yKáI, "X;6$$9B&3vGThAb"WDueW- Fvˆ ^yB@'Q^ H,5*5᝴ޓ2L`y7E]}-⬞(]3Pи]}uyjY+g1(>Wj).g@h~`׌&8D^Mlj[^iv;!fֺoW fa#gS*Pbb5t$͢4U=* <}˽w paw+=xgvK]yeHWH1>|w <{7 Ja߉Pa?__7/}D`@6Ё`%8A VЂ`5AvЃaE8BЄ'Da UBЅ/a e8CІ7auCЇ9C P@Xf'UX &/8h[0@24 AE@ha dmnd F9ZNt4 uTG?Qc =f吃cH"$٥IFr,$(YGA>RDd"OiJui+?GORĥ+cT^2%PP)IZ240-VR|&3)IQlL`f2Ixd(V(AZ!Z N&il(2.@a\h8tY)!:>^殖Ʉ݈[nԗd/%ˋ$!sYhTYiE)ӎf1EE3hGCzRl4H9Sz")OijbPj/] 6`Z7O/ь>#E&GT .QA03!jhC 2O>B:0( VD `~jPjSj6vX ԭVFatNU,ٳWFud2sK֡zԑ\k5S6SQUNs҄E\cnYM:MD4 GJFճĝ%wMZ dHZ0 0,PX)3j|V4@H$h#p8Dh}*fl["ƺd*P E}zw*752VOcDQod6iX[x[/TV-vy-oqZD+L&Cȵ53 9k彲:3m.ƀ Y pĂL ,DXyu99S` QЀ&G?#rǷ @x TڹR{U(1S.lkJsYZSBsR涞iDgvsp0;XjU7 wx[4 sLzR>rzo|3׷&GyY ̽*&@ hl086oaH,4l+p#CA d4NW>B@[,A_S^iK+ ^qIcu@*=Z<|B2yӷr8 A{?˳r.ۮ6 C8Z7qS>3$7tAK Լޛ\#(2hr#?EBA0h_ /x\`&@H]HJ60&$px'8Q$X=(<˼@uBV9#\;A+1BCZ2-Խ|{'\$BΫ=65K^l=#3e@FEYF >1#C$bܸcFž 9Jh,B%WTnƙǭt&@x\@0Hc #Auj 5B?؀HX  &8M؂ $$9`ʔS4F/Z21cNI[Ÿ$3JqάTz óڷxECc(IN L"9?SAOswl3G8spEDFq-dKA,><͊2\I{2_ͬy_(]>rկFe]U:V` VaχI7jV,m%b[߁o!22b /=u! ~#M.=~W N؍؏ِّ%ْ5ٓEٔUٕeٖuٜٟٗ٘ٙٚٛٝٞڠڡ%ڢ:T[u0c&4V%$ HEP"1v)"@""u205pY]1!ʙ*5U#x\[ H%v 9dXd0\gE #XBuh(^]<0@ H0'hm2](h90@٥]U)0H' pR"^޾߿8pDh-_umM?ybE0 fc6ЁZJ/\e7ee.cnGd8שITLXgJdwngf5 '&98ftNfHLɣ cv1kxP5Hh[hc8>Uv㌶#e]#-x8h0h<khd8Y[x^D`P*Γ>t*M^NZ#pj# xhsM1aue Z> pQ ѬfX%80Sskk~'x1- kF둠+ Pk븞iiEUh?6^x&X>l63?uhv9r0`uma8sFcr_b>!J]ˑr`%Ay$EVNxSK fjkjVNŖeͨ &Ί plhkS DT#Upa 2">~ qOޚK0e{ a9 h؁XenvQ.n0+m-Wr↩4>n47z z0 +`< `iF$Jئ2Ȃ, ;>0$$p& p6Fp+ZO{IlɁ jLt'zhdppJl$x7}T]u_, (0#U ?* '^SnC_ *#movnod.Dvւ.evmS1|w `<2wYn=s:x5gx9udnoDc% 66 h7ہ HF Kd#TC$IMOl PH x^ (Hobqpx;z`v  p<"@Z{pԱܸǒ@`i{_#h}?hx'xa2sc]ㆷvuP6xn[0/t w<6F ?BIdIphLuypj%gUV h5z`50Ͱ~̀ ،vui} ~h09Iex@-nd „Uj$l8Z<^=8QSF;̡ G,idI(\%̘'[ʬi&Μ:w'К,ʡ" l&#5,Yud Ϙҥ Oȡi"22eZ0PI[R# Fk QarE -Jpp ,0)bkpx v`A+f@r9 P\  !pvm"/!s;r -Î@}` ܡT4A!]p `B'#G#Є^(YQH45bR.8":"-18#+ 2ZE3UHUWs)V[aէLVx MYjf `~9 @ PۚSfl(B[pC PDN '_ElD ;bꨥB'݂ (]CZJ`%P )@.,bdž (@&D X z%EXS <+4OZ}5ڔCꫯ <0|Hא5+ WI%eBLůZQVq fMGo(}!BsQPH3ל iFB5LWa=6e7A%s )/АCh/QSEW,622Mw9e6;8ڏ[>S.W9qB闫>;~;dw;;w!A<+<;>髿>o.hqv!quQN]I,llAQ"( $),~ѯS`=%˟$+aNB(CP&L IB >t ÞPIDR@FöpJkU!0&A| xCpq_% 1h#*f<#Lj1dEtPp<1Y9E,P@ }Dk9wd߈Mra$!/FZrrtBq# h?Vq*< DDB0lQI;t4I8$4Sڑ#8df#e)J^L99IW f6ihJ\c(iR&@J+T'̼Os|";83e=Fy1ё1d-YMS.}xL%'-9L%;f +O`^Ԗ'GCHԢnԙC'3!Х՘ai/wOrߣA۩̩jӦŬ*EI~T"S5nVQ*MGk6$+ @^^=,bVn1<-kXyU"җM˿NPU{=&KJӮ%RZ&UahYϖR*ed:_I ֟,GKӄu.[\rӡE3wT.Ӗm+uca)ƝV<ѪU[\ /\*-[{cH#w`ag[6/#,4۷4fx0˯$>1S.~1c,Ӹ61s>1A!h:HLLn(CQse*dVrB-'C J\8f0 P3 A`1WyV<7AKBh4I*j@iX1< }b +D3 h°}lDFO me?{Nlm!6n`YX#HZ jAҩI=&@0 & ٶ.mr$D0b 85%g|%oyN78>7ڮīrc"s`6~L&Dn<=NsaߊzXvכ e{n} n7;w]jHPÈ *5< ),!GQx(rh&lnUK]~OH* kfTp>` d __".`6 J*`J  H4D ,#@,qCTA\H`CXI@A DžI `C(C@!I B` m`l`aRKH!4!쑡@ 0@ŀB TF `BtK| jP  Da  !_xiApQu XP I  Ā@bTآ@b-"-"b//b.J1#0Bc D8!KD4.|(@\ TB 6v7 (A x5D:z#88D lq4 4.N#5Z#6Œ@hec5^㹵DCF$I:89F? @A$B4r=ED6$K dA@@;£<߽ #|@m  @O%SSFTPQrm2tJ5+u]e%^f^]_fabAbԀaBe"ffl`P#0A@ hi @!Xfi@8 X($D sJг> @ XޡH3:@ܱC\Z2ytG4rC HPtEcEsԘI4Þ?AL K s,TOor- 4|,7QD 4k"(5TG5BL5U?U2VYT$D\5[ \]u]@^^D_5_K  HI(6B@K`6agc8vXCfg Dǎci5kX*}UkǶl6m׶m6nn6oo6pp7qq7r'r/7s7sqQ'aWKڦ⡚1Zə@YYv3cO]Qv7|2-Qoٷ"VV }~DЂGsOݜe[DP^D%µą?pə#y@/c[ \81R.-W~˷wwFXEӐPv/_WHxILW׃]ߕ9~|PqeH+VZJXyyяy1[XɹL͹jyo|#ST3Qe:Hhqŕ6uz  кz< 8LPU@(` Q1 Z &l]{զ;8,VTM]i{g];V_uTÍ `ļ@zyr_úR.@4&@ZcB+Z9QT|W(#GZ\O`#?B@p=<$/z?}S.a{y}T;Kkizu|s;SiAԁc**];c<:+e;VeM֫kWo|,z*g3'@Z0PSt}x@}o爱o&pEx)1fvE/K s;^|dn͏G59osl9=\2El*m¦Kn4;+?_y;cUpuGhoj6^T(D_۷|C02パ bT1# bFM*\kWx&2ˌiiqGNjXe9)#9q㰖Oüf.̰aOܥm݈+ &曙{ܒuiBO:zuױg׾={5 l ?޻7vEns <ę f0.HɊH:`/ :#,Î%d 4L5Rڌ9:L3X Q1@LFO5dkɈ: 1CkppE$#c+R-" ;621a2SL;"2ӈ1-#9/2BpB"SA@ Z@ 26"@A"p6.R !M+JtF#O=,QѴ53OJ_pmn4YQP( z5m"*pv_I8.m]vD{W} *l`*Rah@PA-"Sbb%9 *`"Ae[-d#R_C +u]xEVNԵ&ՑXaVSDڹ喫Gڴ&-"6PN\jyVhze:YhvG /ëb#.R$(tr+B".2CPeĈs<](%WDP'oV;z-)\a)]c/O^o硏^驯^_/O_o__@4@.ۼƴ!kANiFlRAx%ޮ£)m*"^V.MoL 7hBd;ǬNmoJޒl%]HT $0Dl'p&" Q2VDk&y4brqlNhJYr'L)=)PE;`ì)iZFZNE"+kR 4_DAJ&ɬ z&8Ex1{шt'%zֆ:(/rr"MObaEcY.('T%eb FlmuhNp|cFMzB`r9)+=JJ|RltP#)SUSUUn]WVe5YњVxs[1̕u]W}_X5aX.pc!YNe1YnfTю5iQZծuka[Ζmq[O|[5q\.s]NՍlhk]nw\񎗼}+p;w@#^}k]/e ^6wӛ`/xbM0~Jث  b(ćQW0!N*F'>< (D6B<øqdI! PT'2 R|6C1HH).ײrj%˧ skш19 ܪ<3ߪ.xz5, vpz!< dX^lL 0Nm#9i^ÝtQHv~mq1i8^]= v/qdK #>%c찋A"0\eKM] Y졵N @ށ hњn7HQx9m h[10xCn^o@rC…E5q XaQ黦\-SWv~yrAk|u`{em6>YK<CeZzC @!v:m]`VXC 9* vp0`_ UGɂ:ϫyB"0UL"Ƈ Qܮ,?bX/c_bثY/į bPI\|x6!zT@6QWy7O!B!Va a<B l JW/PjBJ8pTnhJM$A t! ( t@ ! h>\A9ta,93s:K و8S9" nn2L!@$@8A@>E f :B%P&TADDk4a4BaTLJaF C7CEc4VE]Hc4CCtDKTH5HI?TI9C `A4L4% ` @& @tOtTb ?FP ubQRb^ "wb"+BXמ:A$jTK^^cUL,'W'*W*V59BZ2fuA%'2dL8עujU&T#2Y6R VuA  !_^_̕zA`}6Sj^4)P2(a*Kl2DxrXZUԶUntFkBL bP K ;'+:@ ࠲df @̢fs JZe P$DK@1Pl mm N2!DaXADk:B @JKGҀo owtooE:!K 8Awr`AL8!qq!Wq[rTQpQ?q)pnA wvwQ` @,@JN`F$zzw T{Wbw|wb6@ USuS+^Rozu_Vu7.]uWVMV3@)q\Yuu2{%3Zb^*Ym&*X,FŀW{/_pu ]5h!t\gAR ,8xybCrV{ 5ϒ]҅d-Ԁt&Tr`BExͱxֱr$h8Za Yk   v!ѱFo T!@?"@dy(gpyE`nO+&t.w17ppW@ow!r;Q @Z pt!@ *19YYpAHrٴJԜyVwJ+O w :H n @9 @RqOz}Pl6+p(u7v7%WX'8bX_'Y˂JRƘS4z!ڬAZخzՊLϮpXXڮٕyf1x1!5za<xx2X4S!Zפ[ӯJ"K @g  aad! M 9۱K;a{ 1Z%@E @ ,ZѴ[ٚ[ @ @Dxw @lGkTpNp 3|vARa !!s5\1됴N\W|yoy'ObnKq  @|<ȑ\%F$@2U.O쫱UrZܮ؀S+ԼW8BX((zʼöOYYCx\U&㼪]а XjJH56u/@Q"JCXaqz\] =Y{j#kfPE/a m/a@!F&rA̛]דٝݻ{iܹ=kS헃rB9ĬYha_wv t@D; ` @} mhTݴ I b|JkNwF@$n`bTcj  @|v%f}rL eu#:J1MX@5% !qm!%8mί:^u~B[0ZzvU/ E<[ԦT{<ƞPrXozdҞ[תa2zM]p`L2!@m$B)@8|QQ8qp$j>u2D@I#k(a;5A1 4СD-4)b^ ,T#i!E@R:^5"ZJ C)W^ TPn `ZH!5+_M-E^itlYp ȝlnXo~ Ҵ,Z{C*H+e*EdlӨY˾]`ݻRB ^8ry9t޻F?hQ x? ||؀s7`l찃;|lA|^0aabxa:ddd}=ra:(̅-)$|;c򡘁, |C%8N)9deRVi%730cB3 3r%1 7 x@ɂk‡J <6#DM/L({J^O~ j=A2 G,$‰C^aD,\I@ $%|Dx`a B2K:kz H"d `&{%oFISOE5U[%qU)j^,B ( #jA^AJeH,rŚi (Ţ`H kpjL,a%H'xXvpxQ^Ű/'2H&B=,nHYI\FEv]s]s_WvI^rivˇ35և> }y}wsJ#~wWM0 _݉/}M{y n_ߪO(@L5z':93ugθ<2 yAmľ(JqTEpD/.^)4q;0y sv@ƇW g,Q4^ HJrS%Lj(&? Jq$GTrl% XB-oYKZ2/ Lٔr)(d*sl3hJs.ljs+7`,9ωtsl; xs=|s? Ѐ Ԟ,AP.2 ]B шrqh +ьjtG? Ґt(HҔt,eE[ Әt4MoR=Mr`6's`8 *9s`tM#rcXyoɧ[D~y֧hW}C`~~$f8NW4{'iwiWT0NPPqwNb2H|xh@ B@BpwN$0(ȀdEg]OT=lZS/xPPV~cfNN]·}R@Ndg#|P Gb~Xixn{`PTtNks8Nu~wާ7N 'K@ׇWGNWNNV2B{։th䄊 &CNGPg(v\{)0-o@UxJ q6x…[Pe/a@[8NcH^DN_\mD05N4pt3pN|\5ND7|ܧ\3pr``N$i߆70NsM_PIX[C|4S[cp70f䔔'D amy)ɗHyvu@py7dP 0Nɘix,IN5pi PN p 07fN#`Wf딛O^U[d Hw{OFYN9PyNǙ`Xq9Nԩd'N0$ ovpN9N +0W ַrpync2Y)隑XijNd$@ pyNO aZϩ 0v焠7XYƢ$s`g 'LI:Nc0cNyV#LNQ*4T N-p]@7p{RZU&_7[VNC [ĥ^υC`g2ɦ0`opE aM@E&djJ *pdڟJx{ʨ:Nj}n9Neie_ZN  [F:NZyF ibE `RبiYx%*@( Q*P5`?ji&jDEJ)$ 0h!R*ONJU SKRI;PPkP 0w8Q pz԰Pk7PTED<N% C` j䔨i2FTPОJ7N/XBK2&(,f 0-Zi{+MxmKNy` )*;|EJ{*Ew)gsO Y_PЩZ8 N@`;+۲3N7w{lKem)NEN[v 0b#贼d#pDOF;#^KNۼ HO֋# d+OkNKNXQke"`ԽN`j[c *{SۻlvQp N#PDpy+ ,£+)-ֽ컁~m]ҍ؏ |sw>ItMxpgug,7y&Z|KfmV^|9ab`lvDVr7xv͝ o&dC'e˱mZT xM' HI}{?aj~9@O >Ӄ1yN6ZЂjՃ<,9^Zmw5Vp M؆\ֆMKnY>SmVW`k,JY$q>۬CAX+n?¸gj*@QPf^:mU(  V;?O戎V%HGІ8 x-lt^z]?λ]^lL슽}]Q^PN|\C{/h&躅ZF (I yj4"Yþ͘rk@&Y2p/?$jky,`-bRNb>\_~$빎i~;O??;KCA` -̜?A񾴌A]~hE%`P0PjZcqnㄨчZVO5կ?J:ЮR~n^^n%?(/5N&.@&^p? ަ ֲݤ *K?qH-]__?]xJu *:\}J(@_YoO ?>]/oM A .d0@C FXD5nG!E$YI %,VؠÓ@yd-(aB)@` LH  ̘C0ԪahZ@/.E0e88 |Ղ%< Mx]74Pd~Xċ=:ʸn0:iLc~ӲSݭ)3.cԸ]$>8E)O^uٵo.%w!ƈ:aQXbF?_>B#4B0:#>h(!/D07 "*E2?96tKC-sEcqFkF?\8yi #R@9"DPLI|t&rK.K/[Q<}L4La#i H 4\H1@H$, OB 5PDUtQ:pQG RH%eRL3tSN;SPCuTRK5TTSUuUV[uUXcuVZkV\suW^{W`vXb5XdUvYf.!!# #2` b[p[#C qvPHh5 "-sv_~ߐLh&H #" 2bhZ26`Z`= a{8+` Ɓl!"%N`zh`#҂ Dd֢[@9@`E3\He]j٣y!(n h?8]7\, 4 7$ B8- V >` 1 !'jJ}ڷ: PdW~y(g y:C =" 0+|@- w?!03Nڈ @D2ӁԖ4+x ^E< fPAHq Dz  4/AXP , r x}XhD l@fxBR !f(6xF41VkDҳ0 !9Ё ! 3V<0! 8H' PA^P(`%x R"A "h]JX&j0 %H@{AzH.A"Gy ǃ[M@H"S\,A*‚g!OA ͉3gB,IKejS-$@*@4d@j A 0*ZOHAUZT@ ` QM Vr09Ad>@A&ʆd%u b0 ZD SM{Zf C"dFvғ@qhfP*@9*xt{ @ XD# JtQa@L+)шHkդVH*F ">dHD,d@@<*̀-y92r!tW氦|"~Ehm KF'uX33 1V@:N \c$'Yԣ,d(GYSr|e,gY[re0Yc&s ǻYxsK7ZC:  paP-R3*P@ 1`4z[${r^2sM}7!!m򐛅e6TK FX\`L! (J+ #ρn{+A8 DpT42Mf@Tm B(&EF @?(y OS8+C@ 8#lA75%`OLs_r!яCzQa%b`B(ℌ=2)?;^*;4@4;!y "Ӂ vBRp%^ CrҒv a 2. I (/! Y^bCY5؂3i X&^Zfxi*296:$؄8. #A7 8rh% j$x79h5)>@A*$?Ձ0XPXp Tp%8-C 9t$H-8TAE\D0Ď@A \8P< 1X"0ebf h&j˛ˮ&X xp+bc w8yZͺCC*Y>)#ӽe4>'~bDnƌh(Gh< ):P1@ xA(X8( 8Ë HA*\ _;c )GYd; 0Sf\"( 1[f F,I( \`,P`+0yFP* +ѡ:,X#x-"_\y:FI,,Ѐi ,)Hȏ  I>K/S5L0^pIB3Ђ8 0p*.vZ)@ -X 7Pk3r8 (hHQ`MɅ ((#h5؄5IA#8t/NP>hWC0#-@ K<먰O I#A6k/0Vc`1O=&=τ0g S(#CP WjHP QQ-Q=QeTбQ[Y#эxQQ==Q!͕P PQ0`R@Uc  )P< !RZ)RӑPҌp()5SIR<03RaR- pQCkRB͡'}`ahيFHR洴z{LNWOPY~DFO8:D䠡[\c57Aopv>@JbdjTU^ӗWY`YZbijpghoFGQstz-/;_`g+-8uw|lnt,.9۱KLUdemP{}~hipprwnouKMV^_fcdlSU],AMvrCMR 3N*Ho8R9Ta]249PSFJrv?B?6vO@a\:e#=oGA{[VJ3iKZ0VEnsS)26^aLDGALPE`dNJND[hlQWZJuzWZ]Kw|W|YGKCHLCdhOTWHfjPqvU1\EbfO}ZrxUH*4h` qj0fE\T(f9 Iɓ(-fIɲ˗%عb=oIBdCA|R&MD>E  ӪWv=cYQZ̪5Kn:.M`#bM˸Ŏ#ۺ䐓^}6YX,)[.w7#TuJlЍz &j#!(.Mж@HlhI r|ˑT/}APQJ<>2 _ H6BP@˹sBn(Ѐ# _Aet-H^H"$ʈu>1҉u%c+ tB'Ahѐ ]@&"RW*T$Ge أ@?6BeIDSgTF2Ԧ@ zfHOkR@ieBGEAt٠!)v @)PQBP&Z͉U6GzwA4K@ ENg -)kBl !{!,X6pjB t@-BC%tNɩH8!-ABy@݂iPA4E 00A:\n@OuLH@t$B^DAY#gaAZP$ ͜2A3WЯƁM"ds;f3 ]2wUHbL@z4E]KP}bH.dgy$]i0Hx&X mR >7I`LbAҡG8rk:"d! E]PQ(:^5 A/p!inI]D#NYƑ[t 11|;$%t9 ԞEoD@~}AZ}S^1_oGK/\@ /%r`@I^B9lbaiݔl/KGpD:N"^HAKNK -Bˤt>W8X*A(? nĆ^Jf@D^Wz kD1~KX$N^X5,8"[Es<.]QQ27 XK&;S') q%Rȅtų ,DYRJ #6hT:a+ #-HY[V L fdAxcK(ר{&bI8!$9nn @hm 4E wҥ$=PR4y7b-"%vġq2Qtd$ߌ6EFm$%͏c $0̑c2PNLNXβeB~HǼ@2 Hӌ5`DL:CY#E>πMBЈNF;ѐ'MJ[Ҙδ7N{ӠGMRԨNWVհYM`w K ^r4f=b؃69z "6HrA1q n|m8;EM9'sOM>u;H7|qujfCIThBwNQF/9y8H<6fɻ2Lozu z—r Hw%DVOk8g Bv8| H; d8Qz $x@!s2I ׺Ih TmvEm#wϹw$IA'dW;R=ucG(Op3ȷR"EC4:}W$f4cd`:2}eVjD{ uÜp#D~].1mWA {#T<Ǖ}?ac A%Fx/,Aoɒh ߷?pe◀ jܰ 1P@ 'C ,?YNHq_`(}N_I 84 @p!gjqTas!Uek-""`X! AkuOR^ h1t8 '`{!l !Ue~ajQRGqee]r^&1q] y>`8i  >8}q4e,&I|NxQ؊h0Xxv{|fj8XxȘʸ،8Xxؘ&qb(Ssk3msAl<ڸel/D0qm:6Uq&Igv3w_$qnq!pFrop9;EQ"p9 )pHtt1q Qq(qq'{aw!1Vv%G%w1u-W7Y2s3!s?'gCWttJtUu[)Eq^G?aGscW9pvuWfᅓ$vSj'txGL wJINgCc'1jѐ7?yggUzǙ|i{{pg&!g4|>lYg})a;Y#ԏe}A?PYh~~ ~>~!JιF~՛ق8!byHJiy(h6L85Q'A!He)Q,-/8` >;h1$QqA 'a\GATX$2 2JQІoHrtK\ሠe1U%HUv2ڥYgbhQ^rhW6vzcƋl~:Zzڨ:Zh6y)a"R8ij~ f&&J`uJqƃ/a 1mv.7X%fXn:h1ᡮ <(L-QІh!Rf nj r-8;fT  k 4t!;Cio ŕ&r:Tbb4 4!!rG1)Ct[#itqGq/:#M 3b$UzAYgR&L$ zra%tpek$1'g&'\ƶ&Ҥ1{=>s5&( A8k1b0a)?9Hk;> f*S[J{0(s'tDgtS)V9`YTr 1-ANoabUd-t'w&0UR2TzM:v -Z+# C;ee 5K2iJ5u 0˜,c91#648s,t>=d/XO{4Lӝ{MN55%6dv /0!9equMOiH!K5[$wt9'|n92AZT$A#')NQq\}uRguVyTPOxO-V\=Iշ$QQSŐ1' Q$C#Ňjk vaS#!}-r7Njw&!׏:B"CUp%T$fVvUQ%Vb|FD=mNmKWvӢ_oT 1JX]M}Ya! _ʻMɇ ˿Y+:%EZm]Bԯ\=?5J@M6->j@jNdK 9z|gr\SLj yM]P_y,a^7q^:ս~KQ__ֵ_:\P |g`bJua{kz[qȑ kp+=>80i3Emt^v~xz|~>^~芾>^~阞难>^~ꨞꪾ>^~븞뺾~F>ns(,Gg͵q%`͞{XL c]s%NBLSȸ ︨ɕl\Ce:<+yH3ҒVS |o=% D:q?N(d,.02?4_68:<>d 0@pE?`>IOaPZ֔T&eAj񑌽&iK P sA/Q%!)1Ys{ρV|`D9$0&ÿ~nrxv?z?|c?fOg&+* Q C". Q  @bpA)w/_p(Aq1 u@@380v%D(^(bFc#b G(GEJ,aQ((MEETRM>ќ2'@D}RVjF/ c>(:K [>/ q  w@>s<.^tGދ WGì'9iբ 3XEx S7~Yvqgl&0Ƃuc&z `E㥌} `; )RA# <0A<`|ǟ_~$s^0rЊ+p,N!/$B0$QN 4dYD]$Fh0b#uhÁ&Jh!ʑ"#(H *-캻l(=h;<Р2D3M5dM@H"j&􁩙B)-ŽDBQA2|I 6XèR6eǥv$hҗȢhjqH73/!(DZz '(D(*/EK(9 lg[o"c=AtA:8u J"BC]w!rM w^zGz;HE  z=TPa T06hHA6h-%ja#%=z2΋נ%h]2"Z%0ҐW$|רZ _e 1xl mH`CN#4nHoXlH" 7h =Uɟ5)j3 7٥X;n\G:vga`EY w|n>u7nQ:zh#u/ q~\ı#V=^ ||5`h9 >Ɣ3_R`& p%9*da~@b*sW;vЃECF`X,Na@-Mz!THs8My 1"En^BF?P H$dF(*&DEktR0VBէf O RG&gHF+E92Q,B3h&O+QN)))H5 $*GǂanEQGLDf29^O~ӟh@:PԠEhBP6ԡhD%:QVԢhF5QvԣiHE:RԤ'EiJURԥ/iLe:SԦ7iNuSԧ?jP:TըGEjRT6թOjT:UVժWjVUvի_kX:VլgEkZպVխok\:Wծwk^Wկl`;XְE,b`\ould'Ŭ#lf5 WR$fE;ڦ^ӎ@@)Hg)M싴ŭQ/ D,Hwt׸J-A X׹j# \V tݢR7!,w;ތ>ZȻ^N^]>.HtR{߄^ S^_Fp`7p%9~B Z„mb^ qhqo$d> 9DQFbKsv1Mpeƈ;ڌ$bA@&a:2!@3]#LL΄e7%@槜)j.Ȑ0hp%wf"kv$ /A(]\;Q h' 0eO J+%QD(!Ep lװ > p7ČQG1s'C251u{7qƼ٨ܙ LXvwL{%&$'I!cK&9IO~+(QTr\wXb"ғq($ lyRP͜0oo&s%!ÍpCw3Mt^(rd^zkPd0NnS\1aD5 ]ϝrDe@L3̓I|Zxx(P(A/jP(N $v5{)>QAPH "JHRO!vHYAdF}_R )!ξdFDP[WHKL G'9 #$.?%ELYöYz K&7'SX:3 QW[ɕ]$ )cIG(k+mL;o*a+{yzҴ$4DTdt$4DTdt\*64.P2-bj-x-.5 M.FUQPhѓhтصїQc*E(k #M2uȄD*=(2ęx>+/01%253E4U5e6u7S20LxCK;EBٶSAA  P2029Ĉ=< &kK(+ȌӁTe2.jp˳0RjHUCFզس\BC33\LH׼[PSp{!֢ BYY5A@E֗Hׄ֎pV5PVƥc- Ofs6hi#mk ʑ87t[7z{XX4kՌ(8e7=w|z+s| 5McXפXvbþ ̎}8aE؁(9*eˈKX:қ+ +Ԕ,q  [:ՌZȉc= #xĈ!Q =ስCx[k۹騎및z%xQ\@X4pȈǂBt#&C8CHC8pcșb C?Ĉ\xA< dXtb3ǒmDI.F^䁉䵹ħMD)ƈSd{T@јUF9J&-tZmV5Zm6<]o՞Q.#o/l!#e8UR&'wS,R'7GWgw/9I& cz?N͈4 /qGQݏ.2HVe} iUv(ǏN5r4l^y܂׌hT0ZR,+gi[(}ʻڞ- > oUSN˄sHZsڕ3Jr T9dB'ݣحH-(HʦR0XߕK*mKa}߄2 LXO^sa*+:X?QR|C'iie .q'AHvs`J7Ia3mcm9,x\2\*=?A\eS1/`ŢppDg(y4ZFQgp\vx_. ' \'Z.jz !Z!袸6j4pGjid4ڊL{z˱/~a}4̿Vj8LJ5W{llz:`]7GWgwׇؗ٧ڷ'7GWgwn Տ(6,)'~q((rC'o |B%Q.Eg' (Lx@'2c"ƌ /jxB X`NE(OKTX -p'РB-j(ҤJ2m)ԨRRj%Xx&3ĭ I<$pBP@0<+0B텄fUa:/ )]G,<ѫ@ ̛P-kֽ[!]r;R`Y᧱ J˚dϦҮm6ܺw7=%ްwݯ] YcCLJZ]:Ǔ/o8~8+8;8K>9[~9k9{9衋>:ǟ@iz"RvA5Q'YdF ~Sw %QQc;o 5 yZ.PI%rk.uS oT?}oK W'JHDA(숪@H #`p3]LB?!m~&ס,P . a@Ȅ. ]TFD(-TH |";aBBq UH&Rl1)Q2fdM+eF#)?'y DihA 4H (MD8),Wj_$*(~(BS"VS b ˦ 닦R~ʪ Ub5RJuBYd ՗ɞѳ1_c[uQq/ koXX1٬Ov""QR\4ɢ( BpQW5DȸdzY @K w)\%d q@PI6Ľ2]oGk-&=/f$*ѥ@& ZX**U7Ĺ3 #⧙U*H"w o3 X+ѬېF@>2%3N~2,)SV2-s^2,1f>3Ӭ5n~3,9ӹv3=~3-AІ>4E3ю~4#-ISҖ4R|1ם6IC5R䳳k{eiD`fD)ZOӡ3@^WBQOk &[Èd~vHeaiI XAS|,RteO" Dߖ \[ Qp{ o/ߊ`o*Rw]RQLfuuX4b4 tDLj'5/3ۄHC!Vzs *M6Q xGn7BT` uȜE8RH=(jJOJ&&JNR&y]R'ڂGCArD r(`B(ڀ*DqRBG175 $WH'$MAoĘ'xD#AKg%7/M?>"p"_>HȘ! 4 !59E`1@ TRo :Ky mxZW$\R4X@,RtID!MD ΆKpOإm"&*5!6"EiE&"PD'Nav\u<D0JPэS2>@ODDTcQq4w PtǁGF(71@#QM`.)Ď9b~d@Y G%BK@T;jQu"NDc4BjEcdDK#RL\ּ\O;eJ:VB#APJOmPKcWTHB?.X3Mk(N 6`OԤUdUV%U+jW E[[P %PĒ6bX6fYZz%XZKle0 1AIRWQTJH(.&F XG8eBBHaޚU`veD"YI2,Iw&@lgl)g'=&{ oғ@6ĻiB%XJY(x=Es5RJ!~iAVUQhOhjBG^~fTׅ@&騂ZGNmNO@~JE ` "6F cG4KBp*FBKzMurWlNoH 5v'nY f JjCitaiBUm *ĥjăDZE֠ZlPUĵ,2\l*ح,! Czh4}nk(gGQ>bn(FTeSPHvHº:E+ p\,S(!þxxl pZE,&̘jĈNb--&.mϻUw[B ^-5Gz~tֹukxBض@FsaM(Ŋ(,ej RŒiF-(ZBTl6. DjDiE-RT3@P~;f.~ \aVBBkD}ܗ6(^U zE~6*%P@f.t%D=rŘbԏW~oP``i*FA6D/rH~,Ra/@S,00'/07?0Gp騂* Eѳe Ϊ{pRN)ROtZSXˌ9U*@5 jQ|jD=Qp +"0 '$+\mTCTd FDF:wDQT81/ ?@Ԃ-/$ ;u ўF*bQpKcDy./m+q 00Ȱ() D@q9(|@C+'/2,C-(E5ҡ,.R<a{g0=>)2;c^~2u ZK@@.@.+L28hDB\@ .D U5,B<.,2RQTGwD#2E(r*3I4?3h(xc*@4@MD0E: Kqdͤ HR𸦥ONOR+uFhpu"W}Pg+*4Y ' ,rKQ4/@rB' XT |cD"cQB3e^ cR/6EWne2u )ogD=˵p.tD.F^^KJFUQ3'6\m"3f%dUivoFF.sK/79-*6i0+, *'DkD޶Q|V"/3PԶxР7IEST1ɓaK/)+LP-d%{ dN/*Á,2I,QLQYlaQiqQy R!,#LR%l'R)+R-/ S1,3LS5lM-N޸cY"HUeYnlyShfuɛ!&=b-i0rRĞZxScJzٶ.쳱<'w;H$m {"Ѿ1 \ /O\o!\)1\9A]b ä: /(J(#2=׋""lH/H<1GF4'"8LAҼH` _G>"1~{6V֧ ש 0^ q]r$DM|$^i`;Q$D %}Jz4`i J폄:2(Q[B/rT̈P` 1X3p`6ЄFz]|aR!G0\!hH p9 p` P& P;q}ܢ/+qm!(J"DD$AN'gL䏁@&w$#MyK0:؃)p"R#A$$݈A H2LP92R 'Pҭ(Z w `рBk |5yf:u@NQ5*lT誅1 w_N _4pL^1aL%p  G ( ܠc#Yɵ"rx@pZu޺R`@8ق c">HA,icޠzg1Z 4PU a@l~ DŗTE5}bt))(2 `#A0oqi'AddUӡQ 5<g%j}k`[ A.q.=Hm=\t,-YU2B ׋6DVW  UP`A` ! o < ~{rS$ [tNdH4j s zt#*  5DJ6{f @ 4  z s^˵ $zMKz^@ A:ANii= 8|~y] g.X#0Pw6X@Hz6uDÅ@=k 4(@laO<@W0w~b np"@1קj ugn+(~W~~@| CB` Ȏn ( NP.N!xp ,  B, HC0-&>Km"/"0 T#Bހ@vzo, Lp$`ߞ aa$<RaA``& *>/"` ς-"4pRbB ̪2 x \wN P  (QQ '7MPI L bJ H tx ,,c}@gq  B}b୐ k/#p @aaiTN aNр"^` @1"~` @کR R@&& h1# :RJ%> `@@Lq 1 )m#o `R&!C(M%U%]c.H2);)#%R)ę/ r^l ZRT`b+͠*11Mb4`} @*,W%p$ r6#!j"pAy΋|h' C  (3& Ƞz^Au|9W 3( g:k"%"$aFB9"h@b2PR6 E$C/=c`<66a 4Edp9 @=7J9^TE4HtHHH4ItI$4JtJJJ4KtKKK4LtLɴLL4MtMٴMM3tNNN4OŴ&OO5PuP P PTNuQQNQ%uR)R-R15STQ5S=S4RAULFJsIMGJKTtxtUSVmV SquWyWEQM@ H4XJGJX~WZMuMVSZ[WՁuMNUKѕME\u^U\4[5_OɵM(JCiX 銲Imit5] ^@JJ6Z L` J@ɘ K?a߉JuJ].a`_}g}2hh_^`z4** .ԫ Dbtk4F|JI 4*DB6N+V7pj[m3ifӕV zpj@Kh-r)_1Ius=itlJ66nJ5IwL  6J[Wd'I tKW7wvI[ʜ4ˢ2z J7DMqzVPwys|5"T"з}tt ` 8@J~T֨tOwۺ|wޢ@n7I "6xԁi @Ig Df5oJwJkuI8K[}m"W}ͷsqh#JTt@`@J؈]wT P8J.J' .ԮIX霴Ҷ肝t\g6 T N xIwXJYu oK~L5V}tԒ ~t&6ӤTt-@k8K~8XJVgvԀypJy!JyYJcYJJEyKyUh+u[QǙT4QT4hY_K@^VUyuyVV\`t1Q ԞQ֡74V5I9Z4 3֜TCN#siS222',2-4+Lw/k؟e@K7@d - JCr$Kd:J `0 ӊ1SK:XS,tyZ3,R46uq[35!K ;Qݔ9G99;#౱4<8K+ws9>ԴT@]}eQh%B-4LM@T =q5NNAzQc[PkS๡MLgFo4;J aJut{4{%Uػ{Խѻ;T;Mb; <\=!<sT ($2\'^K"7P@L+6U\=S L%hoCT+L`C ^6к(4(d4$\#ȣ{z"ğUY\'|%!(@@4.@@\ ]`Y) 8edyQĜݪK!0@vɇB}#u' ɛ\J#]7{BGbR @! BP xg@"n @f%= T`[y%b$D׍b%sJ\)9-^_yG`I`IIaa%)0Vȩĵ}}cwy)v(kVۧ@L՝d]ۻ*hqpcs>=n==F2yCs÷[sgaq^})گJiv&wm (+˪41E`}`"jT&i>V>rA-[q^-ף4zChG~ !W{~^Cs~扞ک݇M{^/yK?ʇ'"ۏ~I^!x^]Y̽n]l ^축vE m"Ɉjzwb>˕|]ާz! @Rh@<8W(X @w36/ [ @,r….TP0"VC`Ɖ}@PcÌ$CR4rdJ"QF,Rɍ,E s&K7cyRЖC=4ҥL:} 5ԩTZ5֭\zZtf̰ L0sY! J`@ \#G'C=Npqsh " &<0l&˸ڝHLw!.4BB4Jd$ȗ h9OAQ(QC@$[ >!؏%Y.62y+qZNyYRKv|GV .`>aNH!W꧈:0Sn puiDED~FQ H# 6A,x 2D&d$#TQi% Il%"͠LTFXqDĩARt (`Bt01%CAjxREy4@ =e)^R| ؓkjJI]Xa kJk_ HV}!P@PTnxLtDgAi HA|$`:#21fD: -|8 FA |R @pC%? ^=CCQLv9+G$/a6zx@E{MGkEm[-"A 4TAJP AlHDS <togP@9@"%N*LG}/<(l$S?O5l@~O>3| h018p$(#H ІmZiX6 Ѐ f@/?wkQ<` p@f)M ϗl2)bsl;rNW'=M iO|*ȒEB SUB*t>ﬨE/ьj( G;|!H=Zҍt,mK_ Әt4MoӜtEJAhT1 ,((. j Z@TݓChPTU-GDKE<6ʦ#,%: A ](K|?oA-I0[)ET`pâ-/M  u(B,4` K 1IB`4h"! -JA&L@R<Pr)Zѿ[BP|tR ڽ*&llBh3B @al~ܐ'vDP2` %pL `zs b.@Pqb{K5b8ȉd,ԯ5$`+?#R.?EGAC Z:Dd 88#.CC%&ber&7'Qi ,JpAzFhC!p?HX:XA,!Z'!0-@)*0EɴB-B AQE9&MVe#1*]N(04ADugaEqz0 {cv:FbqȔMjP'NaK_`(" 崁CJU)(JLa r[ sIuY2& *NQqM4Q^3MvɘI} EOXv #GRP %|pPY1E!y%tR *PIiɛ雿 )IiljɩَfN%s*dK#J\t)ĜO 2~I1R2t3#ڞ K{zVڮK)V+#˭&{Gڭ$˲-/ V5V5k6:t;˳ٳA+S;= r>*qiNF<+;[ SE{!;`MeDz:FʵsS^KNZ;_焨A㬹n Ad t,6(0us8 `0Nv{z*jAFz:3:=GJ#맪=jϺNa@<S8C񳕩ʬTYأL:J)4K2ʮkN\ r n0euʪL4﫽y;ڞk-;jz{ʧa:齸jx+ 4Q+VKk lJ<kb33JcPER#7R \*p['칝i4z՛,l˫IYG `-Ҿ:\ O:=3nXvZu Q;dž&Gwzcr#6dC9Izm{+uQڴ={RbD*zJƀsAAt@5/s׊pܶ!gd9ʪLǭ,?ę + )ߌʺIU d;*LzyڜG(`JQH\!O**{+l q\0L+=G}LN #M_!%)-/-3,M7;:?-> CMGBKJO-?N SRMWV[Z_^ cͳbMgfkͲjo]n srMw!!, (*6QR[68CACM֢FGQOPY㲳qsxHJSMOX]^eؔIKT8:D=?IjkqKLUZ[babi46Atu{stzyz~uw|xy~CENkmsegnYZb:@J24@MNW@BKۘ浶bdj57B{}+-8EFQDFO13>!",efm̾/1WZJP{,C9*78dhlQuw?B?57;Z]K35:UV_@C@iCFAuzWKOEpqwf~Z_]139{,=5KrLuBd2eIMw:WJq1]FDh@_Dfx}X5rMGmAb7:ɉ%bbز&)l2W:W3ocD4 |KJD/dh)ЌW d`, i 0%^1 |F(xVhE+5H!5DMI,еX S^0 y=^\8.@8` j<"H Czq$`\ 0 !!NdR7(*p)H {A 6`NXX]xB Pʷ 2  GaN ,jDoHB yfo*2: zxOM:B_;Ž VНFcЬ/2myOo|yCְ~.FAPd>꤂QFL=bo=Ma K}S`T 2A4*񉆘'@Q)Ps&7Сb a/*6j8߳U v ׾J#hP"XɰA WQ]BAh#ĉ]~9@-y :V %(,x8+ 20!YXsHF8qYهO]4_V O"]]tLhsU^i2@v 飛-^DB hoxGkz`S*K{iKc!JX@LfXÕQLeh 2>A[#"(Tk2s0>0FЖ1i-A B,(4*!%[#@je y6 Bj\=@6 1`pa'<;`"fv Cx H !h@ } A'D'ke_ &nuOjg)gKF/>C_!xz.F >@ c0 I2 D  ! x R12`@OP2nU:0V=4@QQy-Pn%z %@ `z(|pIw_  gt83`! |@u))>`G)Y ђ q.P!yPz{{kl]vm3 S$H^PD?9j7y^7 QPR/nk5OTOhm^5a*:kX{D{ 10@0  o0v'0NcXn5PT`>Q*K,Qthqʦ Q``¥4Ҫ&[6#zR0"P͉*#)S*#* |n ؚrP\Z ҬZZ1˪Z'Jv$*/گ {QA{2 -yZwʱKa $[3an**KX̚&2;4[6{8:<۳>@B;D[F{HJL۴NPR;T[V{XZ\۵^`b;d[f{hjf140jqt^f7 K:Jm1OᣰA$[e `!dki#˶KA &2؁}5A i)eK'ι!#a+Z% ""%"$b"!"B1"x[['62*BGW6" \Qqb5BXfd!#feG-Y[! -`,,,B.r-C+J9z@;;2%s2T22.0 @3,q3933?4CS4G4BRj# K``"gfC~_<7sh8rǸ9#U;3@#0a:C%3:B+57 hHsxs;}#$3\)q_NqXrur,W2`vasYs*ty[^tqtQ7u-X +vcl]zAvk7mp'wiTwww{FIn|~M|}ȧ|͗S2&}ԇ Zy+<P`nzpi R?}zr; Z'^ rAqwa` ,03(y8'=@gAF?LP ${?(E6CxCCȊ8r_Ӎ5(? =i'Sʝب (H.s@ *ɎIhg~@ѐ6Ix? !|&)yi/z2)4i85SR^ŚUV]~VXe͞EVZmݾW\cyu3*fYՈԧU+VɉGgQ \Ya\009RꖦV[;-:d{~M4Ae79pōG\r͝?W+_+.pn̹^^zݿ_>|aO_a CkNгn>D0AdA8&pBo7C?1DG$DOD1EWdE_1FgFo1GwG2H!$H#D2ɨ>8!(+r;8Jh`.Xi+.2*>PdӶ!z34 "`kMKP.Au3$TSCDRna AL74**xုd %@* d d#72b<ɬP}U_ jU4]5:D)"Ť`(4+\u5J)J$Ʋӫzn/vj"Vrd5hT #X !Ԯ5.t=˫N8^!.(3(Ei ,bp!0`C q èpB 0 `b*0p 0E\(A t- !,@Xz,2Zb :;4: khڪFLm{O׸ސ x!*% #DaH%j@*$^`h`7,A L#A B ڠV?0 r0( )ZqnϽWn+*`f+w\TEx(A9 =T VI@ b^AB?k9YϢ~R+@\ H@eA oEJ(1  pP`ĠhY K+aUP}pҡ_RH+nARWnD C~$L(ʄo~TFa$sB8 (ĺ, L6!0ue2pM\! TP.*U@Vn4rT&p+n`.@L@+BZ@ Z$Vp)IH܊-a-`V6ɨ첗V gTikx!r@3O!4˙ ie!8EX{A`*4 $\P>S=0-2 L"NC*GT> 1 & d .JY_~1R!`@-A ! >E!^( ٌ `07$3p!v=~a%H% T^_hgp`ˇ.4*!J| @١ b!J+ԖA Tuk^j7*,eɒV $h XnJs ZUPAp) `6K A ^I*l{'FE ėߵnT*=i^e"!%J\  ե_X bFEX0$3QB BazE*{" NAP 0TbbjTЅ xcK;k|ۨvZPJ"YL @Ӝ,y]d@T$1TH-0Z^qpI[lf-7*ʭYz3ZՅފptmhGCTLaWDe`LG*g EYBC! 5YIUQm@TCă0v m6R Ԁ MJrD0 X0 u "0A5XBulJ 0 ! q@\ 4`  8ȣ)H {A ,ႏIT8: }#bs3c $AÛ`6 _x u #/Nr<[߹ bW3׸T`E348h!P9],="Tb·=UTPaԫީ Pژ$[ A*b oA kuO`*vrM'!\z5(읳$  !/.68 P8@ٶ(۾z7 c:p:h3狾=2J"2`?$4J2#S0/حؓc>k93Sz q(A 5G<W0a3c gCCZ !mӀ09`/x#'"Ѐ! ཬb7 x/#K4(B .0S3## z& ?8K@/(QP<p؀ )ȃE&P! 00!+ c, e@ 2hR`GD8F [FūGG ǽsE@2v|Ly7Ż0-E_F>GdG`…|(f)P kc Ȃ%I&$0H(P3 21 H %+!1#B@6G R 0kI 8P0"H03G-2ȿCPʨèC>0CCv T+>FTI|J0DKPKۣ<+˲L. x:he x%Ȃ?0- !KMMM _콨0tl rLB<ĔMڴM@.̀RM`MDf5(%r[h6Ci;Ǹ.*)   0!p3DLD$8Ѓ# y* łd#w `ֹ`"p`ǒ!cQ[C- C+@Xh9H>1 %Dy %?8AR%espx2;1]&]QS{2H8{5`%S2SxHT"ЀڡpѨQC0HQT2U"@xWU*8 hF<9vbCIb _ Ch pSvw0B8}!8 5zae 2q65e3905 iJ)peRP@@C V0\5QDp Ħ .PBIt20rZ`Ďi*+?' T-XՃh~3/*_Q>&0 `x P;hI%W!  S! ! $!.a C\ z) Fr !$×)7ࠐY)M !EZX0" B @ABN9͝,B2(۾B=@`B Fp+c2pf\(PP I^B4'g-⓴P(uQ dPa᷈ HVKW !C<f9[~9k9{9衋>:饛~:ꩫ:뭻:>;~;;; ?<<+yoPOR lV OZ=nF}s<=S(?Ͻ߃?Zcߤq?G=9 齃׿aygit?!,< 8? :Pud!'Oˠ)~ t@;8E-pm@tXFC~kaG@ % YZx]c$!ȾOabH%,68 _8U 9;io!&v( owDn0}l!Dm2|▦=:{<|%QEѓԟ);)<2Ri )Tc 払v4MV|&/&ˌ M3*r1 @2^HNw4u␝wNxnД"XG||ds2dOf%.!ngb,dҋ?hO&zX C9Mʱd)ƓrK\%CN}7_:E6q*DKzpsMZƬQ} ]V=+IhUJ'[ՊvW*]zWԮt+OPU1ZQTub[ne,xUFD3 ڵU6VRЛF_XݘE8 ,_%N4!U[V1qЦ5+Y ݿ׹gVA[zT}ծ45\Qm̯~5E'b ^UmmCʕm6w.g^VGk3 ^4NmqOم  py`V%XÂoZ ַ;pI!+WȲSq5/aZ=(?;ǁv-rÖ j+ZP#;*5LѴvcQ& ӭnm\J^)fK$]߂ie1R׹Vt)UnrdS"gX+> Dz2ڸ,w+|}ZpI2GZէ2sLhyf#_+k?*O'mZ@مhi;PΨLXҖdͷ 3Օz+M^vW&J#jw4l?=6V8 }lwe*L3{冏ތXZѮMgR<2_o:;|AsWRhcp=O*gv:~`>v1wh{XR7?.YɸoQ.<"^uL=v`6pZ:(CknbSYw; JeM zy^Z LÃ={K〗}%c(LF6YU.֮}g|L4=i|ݾ?[R:JMӽԌw-]<)_"$F#Tv\hC@ LA 0<"A xA p @ \؀$/"0"#h d.D@kTD A\K8FAp@L< L, ԈĀ$A0#=#=n T3>D J("|@K@}! D#GvG@X""<$@D TCHA PACD pF!bA @G%R&4S6#@lbT0C|dA,@KDOB- AbA^([%\R#(H d@HM$#(%gKX Wƥc>&dOc4 .@}L3"@$`@2vX0 OF&m֦mƎZ O BJUXEAІ$$arDPE@,A!mN'uVgKhL@İu~'x'xy z'{{'|Ƨ|'}֧}'~~''(98tx@A#X ;xgH t\@*: .0„.V0DB$BhB3< &L D0|hzI&g @E \KdȀ\ ,.>4 V:.))n)dr)MC@hi~D"48h~B[L7`50Cm 4I.ŬpƥN <㓌%C(2l14T‰NKj*&BR,\Bh*g&K©B R * d@>@< BE-D, `@+d gt@MVD`lH#y+|֫+ @+ <lf@FR*v<(2PCPB5@*lF>r*.˧D>I2Tk%B+) ÙPRIj2`mD/p^-ҖPl J $xD*NL;>Dd<d H(PHA@ \D(pFDl z,RbhlfjɤVjƬ礃/S/8@*>0A|9C6t7/IA,D+B'CA1 ;@D&D4$>^ОIѺ:^k$f>@,$(<@$$B",joo6B"*%@)+(oA0BD#\C3 ¯vpz00tK4/ D /(0 p0p"/Kx(Că DB=,-0/C0C.H`C8eDtlȎDhtnBd" lnA0t, :mx2DC|nA|lV.$:ICAB TJ/̶,Z?H=><<6t405@A*C<:1DUHA:I;K&%4&+d @C&,B41Cl C&|A'(7lp7s8393:#B(<@);3&'?hE(7ID@8'T5o[36,SÃ/*HsAC6 sA32/IζH@6oI%| k2D#<$psATB0j44 Fj#<@8pFy7 z{>D|?}z/#xjIH^& #0Ăw}'Qk ;~A•LK韾/,?sI47D쓳XS9>>GRw3nY74€+?~o+, '<UC?@bP̴)Q r%kz0" =Ȅ:Bv0@a`HbL#NxcѢzĈّP . E H Т#&q6 h ?`b.B*^"0:H,i *r7޾Z,J'QI8BЁ%G(pkׯaǖ=vm۷qֽ[w)G om+o*𗷎3jw/ |b6``:R%KnV; hzX%B P@ |m <:e` AhHhrZ ]0 0 Jc!Kp#HAdDK)@Lp@Dr$hG'oݸpH0 (@"`s3H Z(  cs^,?L2|@!##<OO#OSQMUUYmUǗ{;;RYG(F::j;2GTlDXe[d<*ݶmOrA"q\pX`,LNqV\y`¶X#)qb*Xb-FhH8xdD&Jf|~pn:$hW䜨Yc:bz,  ,0l! lRX5>~ p8!C|C37Ǧ ( !B`#FX(p![s K0S-:}sjɓZkY{Uy{$_Yjƚ_٦ym%RgT]֢ I669\2LV}ABB[<@Ǒ"P , [BD46<~CHdF\lbxXC<B:-!L.Xx@Q?nD _.A}HҀP-#gK0, a6dA P@D3@ (&g(&H\%d ' @Ņ@L &3)6Ѐ @!4:J^"X`$|%}ymetKϻ͝k';L^z˼t?B/2mi-PإbNZ.Cu[`ucj @֤.8WsϘy=^݉C?nVg>v2#炧>ktūWDs$_?sgsxG_\.; FJn璁m֯p/ul |6o0-=qcO P P,ov/!fϽZZ0ȏMll 3M {ďM͸ pP0 *m sݰo|(Nߴ~ */w0P&Ao /MZqGM^Ԝ 2oPհb#vGC/IpIo9hloNh*ޮ0OQ ajL*Y Q1a+yxD 1qn1q2 r 2!r!5n`v R!12#5 S"RJB` +P6r%Y%1@2!8   B0`\'2(oJ |2 &!w@ 8!`"-*T bl@(r,` F@, 8`D& 2&#@ b]21s1+ -DN!8.r/".ˀ 2l&` $1Us5Y@ :B2R.-3*".@$ B j`T539Sn"-=6n/ b ha$ \J<" Ġ LAo`="@IDM"~ p`Kz``B"H) 34%!` AMNH@`K7@I4JtJJJ4KtKKK4LtLɴLL4MtMٴMM4NRվA7< ,`cP!6@G z0p` ,!pa 05S!!"AB!UD7`_vQ\zGGiS"@d@ pXqV;":Bl sW@WAUS5[gYa(O!@s5!>2; Ha&6TZUNg T<mA7h ""*60!6](FV"J6i``v6<0aVandWQJ.#Z6B"bV7d@H*f5&bgc2Hl\ m`B@ \`Q"`Bn0 k1@Ȣ: *j%p2] 0kvolS.n%2f`26) rUA!\_sCs aJlca Buia] gnLagfIu/B RwS%x#fbxv5& p u0b-&A6 `Q` ! nZ \@"!D !@4 @vP “B qBqEWR P@6P!xs%jxB~E7RyAto@ @`;v a ! x^ !\W 6vbkb(i؈a ņqXy!2!!x ‹sx#AeH`JXAFw8!\ATxBxy"  k8^ "^8 A"ˆQ7=e8z_ n$aQ FC/  o@҂ |  {acqY ry …aT91W)AV!n!O|C7~aā}2r d"$FBJ'&nag^a"Sa h,!P n:wڀ2a*Xڥa\0d[+_u+gV<XAe<5z.FFRDڮư#e >!F.b{:Z% [uOb#$!4[k䬵fG6(P}ƘQ @@~V I*@/SB dp@6`tt2BPZY+!!| T``R@IڤYCK a|a!)!&/:~aRAJW%Ab0|0# L J!ŕ b]` S\Zy!AC [J~x [6a[`aaE]\܁i ±gF "|c#|KObǑ@&frag]"3x"OEBl4``Y"  l@B: J!02 .n@ 8 sPyؕ([c}ӽ;Bp(S]/<#ii\ȘC|A<,z:0z¹~р N٥weBxa nCX AlC!# 3[ <E4ύ,+!yy<"\W6#! J)W6.*@@O!t>c͛C:"gXɅDNW1FDȅi덾6>{@2rv̦3(} Ha B ` ԍF$8 "@ ! `"`FQmP>U؁U h F` ?4P):y]B)5HCݍwb*xCBb$$B6~_ec#JAD$Tzw&a  Fb>!S 8AV!@ jƿ?6vb(A| A2\L$ %+Wyz c2(QڃL A9<1d)$\tAAYR#я:M:G%Sd C -LBia [# +,60BH("*Rnd cڌ]fL נ(6 ł zbś.#P5J۾;ݼ{ <ĉO2U7w@۶};~ U濼8N6t'}LjPHqgO0T0E,n aeob~gPL3d{GI@Mx$*@ 0I)aRScRb(8ЉR8NĐ%Θ$#4$aF@2]!C ##IP"qhQ$eRGDž @5QD@8jDll %D@PA<C)'W^CZ% J,f[CF0AGzJkފk =xAwL6T1UJ2hǝwt 8ԨBy#koxX"}<&TdK%r*& (A'|%nB a~P'uR#0Ʉa 3옇N9 mR0bg"A<0cT˗2"RWaSM6X,5h ugA*MHRE-H@++ L/&&V! A@?( HĂ2!(Q$ M `2%X7Pp9 % A/(D1CxC Ą{ IpZ4Jl%`M{HǦ KZp@؍!YC0 .P E-@ @Iq$  &0 yB. `ADVnO!Br@ d)D Iw{_jLyԬ5%GV2r eIȴD)Z63vJM,<1]=32,t!P4 hp♑ 7t?raHDi%_@38 @)H"RbȈ:S|O Ԡ^SPԤ`Q?<:EJmK,jUsBo0 Ċִulm[: Hx!u?uI 4/-,a?@,h|mc Jvej64g? Њv-iOԪvmk_ vmovo wĭfyͧ@p\sRϛiVJVr|}7k`] іDIU|3Mr[+%n  6ec^kY\p;bò|۫a˗/ۢa*ΰ\d8&O, k1;`#;%ɿc=%78&2P`,#Nq _Bپ%we47jv1%-XnDf!U^qx <sMP/ЛƮlzarshPu cs6!]jT/fv}s:Уn;-nf~v;l :N.s/|;ؘ੎YY]֝}ZF٥v>+Gyz/jasWɇ{WWp_;WxwcN;dn|O~ͧt3hu{K>!U>}翇~o`BgusϿ/3-N~>7ba_a{G]cs ~o}h+tM}'wŗc\}V}w6st_lfp1mfylGf%~Gӷinb—fmwWoRhWX`oRWkke'8pBmwrip>wpFvo{FgvvP'oj}ڦqRg~Hxt"vhWևgx𖄋tV( Hss}(VFeb' ^]N{TtWVzGhxZħhohgLjÅcxM)fh׈Txȍ(Hh爎騎Ȏ(Hh؍7; X&H(I~,D4uTi!2 `g') \0PHH>`Y0 )% O! e6GI [_0RP{`#} d">&1`H{/Jɖm閕MS 1pr .p;N11CQ`:ok5Z`uYw}) UPpW )t\ET 0.P% Sɛrd`70ɚNq&t@)I /K:s`ii =0T@0()I&g8Lv~ :p& A'Оʡ !*#J%j')+ʢ-/ 1*3J5j79YtB0r2 e+pA1jm cN @ c@eʥj%   1L$ BYd0mA0_P/p: GX@J|*``:)JA.:=ejN*pNQ}DjCQ ` 040MN P Qz!r@񬿑@N1 @U Ȱ \! p ZVp xѮB! #HG[+ZAP7.Pf7E! .`)@ o`!7`XmRp`𱗒8/P,_*k. K-K7OQʩ}0J=)&3Fj!<Fu6,P *eQ\bJ+t  {p ed IcPvErzVA+K+P"z1L`1)<w1 "R>`W SP c@ ! o0AJ_ dSU;Jațm! ѽf+Уӳ 0 ̡p_@ a] tЬ& 0  (k   p vu0 l+Gڷ­˰ $ Q L ,S,4Q lB a `rD.` <p `P,Tb< Xr ł ^ 19lč-1<5/-d PZ2_lw\(u7@Oa7N0I O[q]`Nc @r  k+I+V)[ PLhPUPѿ+ c + ǡ DZR @9@  P PF @"R  P  bx /@  _ t 0W ` PA $2}ed3 :DЍ+P F 13 A=xC p 0 C2VZhi? BCC p a=eMCI3!tm3]7}Fll PΠ%|MfJ]F ,һA[79^p*  JiʏCj9J ): (M5 ;}[ >*Nϵq0s - ' z`3Pގ!@%@ p  Q Qp/$zD$l 01NȒ>$n&~H,`ֲ 2 ;ַ 19NC<:A# %E(~ .,ND1Cй` XY700܏` ~@upOhQS% \tb7_) L `阮l2ji-! g%1>_`D@Ëqn 2A.` ^ [j ! ynT@5<^1c.n/#m + ͐ vn P  NP͠#jRC~# ) \B#B>讜q# uc%;tcy> O!=UO#P ~ WP@q0 `f#@ )TksO[L0Sp@N۶~ p ^j^Q1z$,l< k`k /Uр ,a.q%ab,Xj! 0 q L1 A?Ba_:R-9zKr%k`ą FcG=b(>bD$&ZhJ8dXI0\aIGB @/Prc wvlٳcDujU-P(ޤppAX@|Uaĉ/fcȑ%OL9@2~Q @gmܷP[hyK2},"FMyi'W`"qyʈWĜiq"A<Ë }qs75xY,  +4'Âl! lR`1>~ p@R ¨x|C' (Xn=詆V x'[Xo 0O-V[y[ {sC}tMǗ|`,ULlSj]SSgTTiL; \ [\`We0G~{⃖ie,@dp%jQzT&U6$a|BtQUzՏ&#@^ 4@VUkek[VUA]ZWU{k_WV%la {X&VelcXFVle-{YfVlg=Y"5UMOJkX%괺K-cjZT*-)h[ƶiu96ut-[&W-j[rCYzwQuxu]2U/w_׸m{{5r׼b{J՝.;^Vm &80%`8t/aB8& {Y }q#/1}UE0l,ud#j'rilXV2uL]",qa,GアWd7y6[LfY3Fr~5  8bư8'yj4}oih~Yr,)^i&gі0~ѼwYՈ31W1CQ./ַk==c^̹\n> :^(#rpۼuio7+r2>3nazQ6Mpc6+{{ݩ~>Xir3O3A _xlnקosl 6Gwaw:Kܙs+w:>񿓼_{lz6{ u2b& g7wՆ9o[|5!zE}W&{GxSwg=uG\f ?do_v+ھV8K h3~{x_|Ǚf+}d`6\> k=[{>+8Ժ۽CD曳?C> sL?ts;K+@3l㺹=As<|4GK袗3-K<4][$S=ӴX X4 Œ#PH,,SUyPe7TjyQ0/P0+HhqRÀԉb9Y6@T1YUrQՙP_qf/ TqІnqШUPuC؁0C#p,<2@CcE XhV  VTq҈֏hP2ʎH/"J>7@-3RHpH H) %7(@$6YH5rci|xxxlhkP_x@q[Ӂ"`4Jl0 R?u p@ABT GTU\ƭR BɠJ`G6JYP 8P!Ѐ3Dp(?2]}XS^cy}&ClPXy۸_XTHZMs((` PMR H(XȀ!s: V+؀) .ˉ8 p%0/(7D8(?h^;q^!&А_o2[v}/" N"p`= 6 `53؀OE,c,((h`s> j 6vc8` C+@td 0H) *(;PaQ>Jqb[8$r&[(nx*n 8@@@֢á <4(2(GHVpkTp ̢A>\Kd.D+L6WCb6L):Jӝ>>w˿hBw74vpwXj8/b ;vg8Nxei+=~xJuYum?My:e8SUO,CxRxx)W>?VWKBS>'<冿Il3z7Ę_~wRX3S7;?wKwS8+7:Qgz_4'8Z?`۵kn^w:6 TwGyA9<{' Bt=$=ӿd:wtdGukzK5}/|_\L^7@o*|׿}z7wg}}~o||/~?~O~_~o~~~~zCGu,HG}cW6~G|SMUǢWj PaJzx/M7~ n,_r p w0hDB(!‰/> #ǎ? )r$ɒ&OLr%˖._Œ)%!J ! Y&)(L /%8 H 4 @EТG D" 3ڑc  CJqU+WBE g~ ;7s~@`?tCG@P|ʥ@5z BpqSPZٴkVKjxȺ+hi~TE/s4@ <1Bn۷q *޿/~<ϣO)eH@. @D Hn4#9L "$~S*I*԰@: ,1P !LÃN H lh`C:g!{7-GDd@GdP&4ڨ3D$t =BB&5޸a8Гdߋ YnQ K0 ɢ@0. XSHЛ|rک E g~qvPCCp|#D4G 0lRj+ )yP+[Ѣ$0CF @e4A4pD @k,A:h-; $PiP ~,A5.+ܲ/7ǨC$+/A\L01_K ;A@5,--LHLqV L:3AA vB_=v@@ DA --6f; ԍG8؊]pkk}t.yދ (B? 4tњA!0i\=2m6̻޻x78aPjij>Έ?YO BȀ EE IGg" G |b @41Kd "4H5y  'ƹQxsFHursFFZ]`4J۩CX:@hJS &Qjԣ"w?@2P H83 B 2@|vb d \h,`r5k} 0  ! =TBU8A:M%ܡ ¹ X^r^$ ` ָխpk!JZ2,hU ;kb}pOm;,k޵ Z*gp[ڤB7ҝ.uV኉0͈<@|ߕ.P\ 6F(@Hyz$㵣xɻZDC<OEE\ c8$\  ``P00[8Ƣ'ʓ%XA*{9B2l##9J^2'C9R2l+c9Z2/9bFx:#Xsbfeq<;9'y$@o.Ƞ[h КRLGC: !/A4G NKmj=Q^siQ:ѮHi=k[ֵȮoi_:,l;ظmggZ׾uemd7;m& S;VBPvd;ŶvgZ+{ެw|+{V oK ^kYvt]o;Wr L!v^lo#']od'!RWo>4ch~\5/SCxD)Xt,r\0z-mj<sso 7{m =<N)O;3ca6IS:?n=+Nv^otzuwg;s_}z>/P`~[I >C‡uK m_:`} U*͟ޱN\%`1 -\ fv)`߰mC ElD`[Ea"aU!Q*`z!qa1Iܨ!JvY2aaab b!!"b"*"2b#:# F @8| @brLHb*p |bD$Tр$Bv }WE\ 0D 0Dx'D(*D3D,"3ҁ(, 6bD-xc*Z8"!@bH /z M45b@!}4:#M`0lSAЁ@H@!lDD`@Hb@LA5A@D @!T@ L X@$(D@ eQeDPEFB LHK D kDd`G @$ H¤[ $J$FPB!pGA"UbV D(@@ C܀AArm l 4dAlT@)FF`A DA)@DJND@ }BζTId@EDt DklB "GEAA\A ZvDp>D@g% @nf_^ `tDx@cFdVe lfgZh}@f@|@b(Ppl ɐ% # #tWpܐq" l@T}@\rz"澬h@b,FcHT ˔fr]hhx A X@`) )X )'|i@ꄑ}fphAhu`P lFgq!XA @A@8e>DTh^hgAJ*g@$҉p,x5 &BPtQd v$fmj$$NL $P&c4 㢊` d!D4A,.ÞKnP},pRoAmKq&(0\PW ҮR@*>cV(e@(G:mA f X@ pA0mw*@lj@"|A`$#X@6j IO$@ l @)lNMA.A 3gל(l@X@ ]QP<@@H@܌ A,A[Q1s, `A cA{^} ? 0o0p 0 D@sP  @|1@Z- D q?p-X~ $@tADH@l1(|A:MA<,P\k "p P>.p rfe#1/D@L1:K6 M1lt@ ٧7L[X|@@ ASI7D!7.`,343Gs qh29 6D ,??@$@ ( $Ec#@uT-SQrHtĀ 02?KdrALOT3p,@*p7ۧF b%9g$<o D<[! @(,H@P(BhVR^'4a+Vfw4j{vGg}6~\uZq%-ry8A0sW!*ec6AD$PDAvA6k+8x3VyUD0 P@OqX35Pm X$xQ@4,A LT^ W8|9T8žD?bG4NwxiMlbm@x6Aܓ`@ĒސӸ8ˎ@F64y{'xEN@K7C L z ja8AE ,3@p(9o@LX|@@{7S6.WW OiA$d$ VoG8A@$ ,dxG-ЖӺl2k @HB/:B7G$jWQ@@s/+;N'GO/@X:kq!|u ` Bfmn-e Ժt DVa@!l tzfğ{{#[|Q!0 `@̗C(NJwEB4&_@@w<ʫv~ݝ`-EԾ] w > "FwD[àE$a ]̅  3ݱ? ~WQVHP`B ;MiSF-&h5#ЦJ uZ5Tr qjQaJJ՝kNܸ[K{N 6|qbŋ7v7YZXWiUm~ۙ`` fkVU֫=P;&zis~u,˻mztөW~{b\:RM^5k=-4i|t/?;ntȊ-?Kϻ{% - Ao PcEOg̾+9//.<$\&I0 OݨMk?bk+3KH$n@it ƹxt.p=564@B }ϴ+o=3Qr<;\-Ѷ0ՓSM34rZmH,MHA$.1핲dcԄk468i=O;ĕr=]@Z;kQm9g묵ޚ뮽>^~垛ޛ\! Pna l<@/x}t%.\OHE&\F @\ <t>,tA/F:b͙~xi{hv={(H`ሄ@!B}_\ X}tw/AYn (}.t]ﴇ󑎀02!!0#Pp!]J-qcAOzu@p2|KH ,otK4WC-dSBLo~[$]c)f4Udόh3X!= ك "l RpA`0Ʌ1d!I $` Y J @ P0`&0H*<. B%@H/)̄"xB0 !`Bˏ1!@.w mrӚA pP,)xr $J A t0b N TPo21e TID9 B u(G HENSteI(wS!7H )p B H>ΓS+yBjP*@2)C&rɂC2&dV*d 9[g*$`>ϿFR21 P351%;h@H1!cN ($D "Kzub ?@!@r,k)[_8 p.t))T!!{@%lB@rV/1xmq;>! y H FW"y^"H!e/z e-D򶹅߆;,Emx[DД!Btӂ"ABBl_/Q0HOZ @Y_Zl5f8WU" p@ 7k&HL4@ @@xTl4(dn0r >a%HChH+ЇN@~B!H@@B C@5&կ6 PC5a!p?zv5MwZ Bmъ@M6#Ar>t)ʠ |\}:"@>S<"%p@)g 4"YQ Z4!t,K2?CTvq.s7M,,TAbC"0i5*ݐDMTOY-P}lW,\N4OHg`VqTX^ÄUOtaϪ.?J <TE` z#GjWyu8= Ve5! gwg#h#Cr! F` @6rAv F{V r jjv $j}c".#K O/ֶm'I3V!nղV/pmikv&kWp#l6~vjV W!0r-D ~#R!&p lu 0`V@ `B֬ >#pDT"MS@ nbPӪAA ܰh !(r!^ _ {Sd]!>^!6wmCQaƾ" Ġ1"|" Rfu`E<P9ܺ#N4vZS!A`([Ƒ9'$X@"0x@@N f~)wx%^G3d01PP" D^Z=@Q0u d|TD5pc*)GPБO3$|4 >AEKX`5d D_LB10@TZyD4=$DM. MHRwfs4Ayd)'Zre[vPNB)**82P ``hec~D! V> L!d!F D0.@Z$nqQ M `PH.P$BP]&=D8nI<1AoA &dC CH2'q*F"y@ FL73;<)/4B(PUV=d/ YDt ^K7ޝ-)џjOZlPAM0@Sn0E-gJ'L5pgߚk'ain_JuŞkgՒP @" AJ[Y04#,@ &b=Q(?o#:@ uBo P@e>MRT  9'RB/ aZ8 dh8TX+4N` (t9C)z*'"48F/Vgr<aR#~4cVh6>1{ceE,@IL䐊ؿ" R>pĤ(GQz?zL(p!+a֡*8\x`3r"\<2f `*UIW:f1%9kz7IpIvSA\2S !(3 &=JЂND4h-~*lg=!JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTMكXua_yaY`A S49#=ȢU/SW0ѯ3*Q ,a0"AldJeX\vmlg5Z̖vFTYւ6e +imm+$ҶkZҾmpyӦnEK&i[J7.dkƖq P! ( 0C0 yU.W.f\ؼ2e%".h+'˶9>1&غm&Q!BWoD `,h* ;`8 k6@$ 8 Y1Vf19d);nl72Nss]dF̴vruwd e1OU9Lr]dΏW 4ZF2=ll0w{܇s՜He{.yF;S3 I#`; MYJ/ 0B ^a, :_X Bp"+% B_HB` ε-n@yy~eh?gvfSYn2^m;~gS';ۻGŬ;k}oݽKg7趷?}!:Q(6`h @&9iF J)H {Ab!B@r ^Ar*!;ʀX=  x^\mCt? _N|sZ]r/3?W^ۋdu~εdu~fyh'uvo=wAxІw hwgfkysGlb6tcwlvuw=t*x]ֆdt1w悽Jvt*o\`tb6@`<0hI`e@.pMCSPiP7`ECo!G0Xies@]oX~7k֖c$vk'eȈ(fuUրwsVyjfuTm|~jqq^k'o恷8k7uHf]sws(u:ȃ8&5veƉ{7ӶXwXdCD &zYRaZ YsUC4W,WfhzvvYS@ 0l1x(mwcHtH^hWؒǒ򦌤Wv~Ex0I&x'Fwe$MY~-c(G~;vIHxneDG0F׌sH. A0Z~5Y=X7P*`0C^`GlZ  e*Y+iP#0dPE01xu)Bs)v-ȂKX'lZǁHj'nmxp׌Fي=Wy$s!(Љ()~H舧Hg'/bhtɖٓ;W(\?1 P-9Z`u@X@0P$Z{Z*pP`8jd У>V]vI x)mh Z MY7؝xG(x]wO'(v:ijNIaJ(lE>\ꌎh1`R  w:{k hX B Ч1Q? =ZiuYw~ŚGmw[(m.) yIHws5(hY鹂vJpjyHyꪞsb eӺZIL9ꚔkYи_x媧I )xGjE%jS9.0C1.`B#k)tn'|1 c#\iUC=/p8"X0`u,)cIxZ'Al9ůH ~jfBjgˉù umQGk)vىeWtnF|kjR˝9z ڃe{+:}9ۀyt}UC'A=H9FE ZLK4jdt1GuD1I"V*z:KnT;EKZ|KkռKyԋ[e\yynٛ{O4M{lDdtD*UgDSQ9(ſ^5BX%P+k!E$|R R| "<$\&|(*,.02<4\6|8:<+ y㹕CB`커eI6h`U_l!VO4,Vuu5$Y%!T pQ]<$Gq;fDguLm,IuL\D{<tiTK@Ð+E&9$݀ fS.^`Oa> 0s}00 0j43*~,`D&`j9h"`C s_ ,$ma7#CEF`ʹjdh0 @FJF@MŀP"KZQ     0 -839FY%&WuXm-6SD DpҾ7AA  p-;8?"pc.8T0]=ڥ=)%ڬڰ=y}= `9=}B@0%4 $$U?T ( 2 [P@Wb G| W Y  t~:2@,8>H)mmZ:c10ا}w;.:* 1Ǭ" @yD zA31`UwY]N'8*PnLP& }0)nRNZ#^$9 &T Q>>$5Cgk7T;#@ 0.P 벎N Y N᪾pYu]%ЉMb`uW%qj034$@T]ԣ!>a fSaG)&M@hAj2*b"넆eo Q.SC p!4?ր >>$ b&0Y0pWD@0E.lЪIY0079..`0h(tC lpGڲ! vnApV`!o,U`% \EO2 T!oW$5!E 0?IM_@/J0p % %wb MXG(HV~ q&"B)7DDi AH(%MH(pd'MR(II# a&,J:;APhRMAJ` {4PA*e@( ) UI*AnIzK@ F%Ql?I*al3T<*dҥMFZj֭][lڵmƝ[7m~ʔI"mJ@ПfmNN79}QP 0tN* (3@D q߄pDqI!&|@,(lGЁ,F0imJ+@°,D̻> @"wGM2jТԘDzrLt*2@F$L3D3M5dM7q;(z|G<;&m *Q%o<j ·|(@"㐣p O &&P0FU ̀ qp-89 @JH'T(!J>9 *Mo9gwg<(_J¦lOjYZB 2T,CQ9 PvA uԇrXha[`n4IBZJT侠v$!"7i  Pp:_Ĥ ąbAE> uYWĎOr5 ޓ8S_l^' 1$Wq/a\(yXs,x)'Sߏ3xt`8(1T*Cke`Ө<xWMbAϚpGb .`X@dQ! ÓX/r` &>$M @@9 0)Ӭ68E*VъWbIT(f8jRu(`7#`ΐ x&!DpdG>яd 8|,@vF6! 4IAoЍ+INvғe(sR4e*iSҕe,e9KZҖe.uK^җf09LbӘDf2Lf6әτf49MjVӚf6J,&8pJ0 #!)hB4 j(<9vt2% F0ll p6$ hJÛɤ.9΍/-FJ}^ǟ6FY~BR[C*Q/j' A]iT*LnNRCRqrrTMUǣ<jWgҊRo]GCjU32ŪMyJ҆ԤDY uEl0ֽCSd !'NFjֱ`_cU4r%'HGKV45,ZיQU:HjpZ&ַbyK=vSҺEfYn.ZSƒMdk[vͼ\,k[fս-p;_OTh{g^ڞֹ*u;ݸt_5.͛w Np+bkV),–vEpt;bҾ}x }ϛb61s#g}^o6:{\W~jzex)~q-lDrdۺ״t=n[G׍<^;Mq]ݶTV&xaZIv;gVouKx(Cv"e43K8Ao'GyUr/ye>s7yu`< e1pE7:)s+Hw O" :ԣ~u H9 H0`IZNՄ}%Q>9Iz0&AֵOfs{׿~x'|~'{'k%q;x& }I@!=K`o8q)O"`" #0{_W/ y| ̿o BJ'd&C ?5'IP8 `1JС$DXɰ>>>{,Sa@| @ sK?Hʿ?@$>4>, l \{A? I(PȂ)(#, ۹=+0()+,ޡB+B/0B02B14Cx+\ 4$6 C5LøyA*@>\AC4 Ȗ*hCǀ3,Dx;C)wD< R7@,ɓpFՀHR`,KћKKK dk/)(=u> BJu45,t ,M$Ml|M DMОڤԴM: 0@x7 0TN G6Ȁҳ \$ -p0 5.tJ[\M&,,Ο$=4N OE`Odc:::Ha0 \[(@d1F D Z '= 5<>@dE?dA.T X& I>DdN~OePc_eJ YDV6ef5 㳀Cx-QDH>h n.1 BW.dfa+F,,P; ?X`/ kPIY,hI1h.ah6ݜ~hh.5Her0i>]#9@ "pH b>Fh,6 P0N@j`fۓinChhni~& kpY0$Gհy ;;!fz1t)]P办p PilRU)lÎxby&?hvjӑ멞롞vm֞mmݼlmmݎmՒx4p PjVn[)^.0`xh%@UmƞV. @׳C . neV*9on&no  ?@A'B7CGDWEgFwGHIJKLw9* <:<(ȼ6" V;'{Y?;TǴ/S0r8}a?E1\KvD7 vevkSjG87x+tb>t>8Z0m ) ĀA?{/A @Z 7j8x176Tv53Hcv-G5Gkcf"\p욧:a\84ԐDJ'˚)=`ćp]E_N`YuO8GĚ 2#3?1_Q{B73V;zY/(s0 5p/_yWjǃȨI&8ȫN#3vuɋ70@P`cN0cd3n3{Ɨ3K.r6w&M+5s GsDGxKt,bǶOc/w0Oǥ ʪP ڌ~M~rҞ'!gQ?6H|DO|_c,(hȝ{9衣 $s7zd:G{DHP4HAT@! PPp PE$t9;HrcܕkWsV]i99_ a Q 4pKÚ4/bd9tINt6!s !AjBP"<$ r"z$q"&) @O,Ȍwx(\@rg==c=(BЅ2}(D#*щR(F3эr(HC*ґ&=)JSҕ.})Lc*әҴ6)Nsӝ>)P*ԡF=*Rԥ2.2&:\h8Ƞn+ ^V!K z:k<%[ג(*M+`;FU<.sW hAR'HP$!~ρ>6`NV#s&6Bs@}:5Б *0/ZD T`#Az*[#hP9I 2(5%bW$|Yy f (4xAhAЀ A!@LӉAA Li`|AlD`A `b >`8  D `A! $聡 S!B|XQAЇNDm9؁AGA`A$A@@<n:CxhS ] e` %ȃ/ȃ*Q D5 DGiqUA8S qʅt'ŔA8́ DLЁ ^A0/c#ƇH +z`AAآGH4' ~6z˸A"D/Y8v#2!`:N >D/* 4`^ J 6Za X@ B2WA@ L03`;fEb\P:CA`5P"PA60˸,8AP(ءsD2dA( bB%}D4l!Dp!B@(,[@^aeqN&(ܧA(_M`BJ~Vf$@AXhRcN! \ @f|AAAAHBD$,9ۺ-m@ ڞ@W=@]hۚm7@qXD4Gɘ!1 \@@=h@ګ9uH;4~sѸt5ġ%g=ݵйc K$" t-Rga+W>_I3U#H\e&ٙYqOߵ^EJtCA5$=JXYO]G{Q )@1Lc?R\]- ,EMcL1مFS T#VgRRgi jv5k_x4\74cnӔVͽ@Dh )V!f8p^)3 7vAQH@dD*JB |{|K}k6AʈcE8$I\&sVv ѤwSU+Q,>jH>Wv|^R+϶a튘C8?JoWE} ˏNZ0X0BP|gC~q:A2հA06GEG 1-UN@`A h „>PĈ^PF'R9dI'QTeK/aƔ9fM7qœ&{tf̤#F58RJcP~;lYgѦU-LmCFL1(qQHuŘf8U8ٝ!G8dő'WysϡG>zuױg׾{wyc &}`Ta$߂p$ H >+$*E(ċP )в*JX0)):(8HF pp!B  ```VH(S\ql +N,o:2pA2EL|/P@@Y@>H#|3NĨ F:$.!TI46*b)^Ԋ,O #/J`"0ꉠC0‰,QH ,8M :Z@Ot# RA" )MWu=܄Dړ F @"@ `[Yȁ n@"'":X96@N* 7顀 6d\[%Tn9F0\ $hNm~ ؅*ׁHB%@&#`SLA 28xYFHJ#eX$(!=gm%HI  ZnއhD@hs&΃Z r!?36AeQv, &ڜI8(&^[c懲c FJBƩo|꭛!@\r~1 @H"dH`a9*:EB`d(Tg# @nBB #@=IJG sH2q-@xPn60/ 6f `pM>Gp$@׆B јF5֦^%GRSIPa1!E1`C‡| A Bh (p²F@MR/?@-'BQH@| EK^p  2.Z/LT`h a A!s$ 5L#=؀pp: ! م@ @cbӟ@p2mI`|mo"y m»H $;^'H2N\5"[Cࠀ­#"Nb8 `=IKJ !X8 JSNىR7< `"`AbYњV~D4!ZWΕu]W}_X5lARx+BHWB^BapF3@$Ep\APBK}@G p}@B ba΀ E3@6!*[ @ ."\B }`uM`cRVFZ "\P)@"h`(F2P30OZf\ o7J1LRSDI@>eA6"H.s CbdMz7L/ XAa-oy, 4UNJ JȌ/T\| ^Dp}E^ Xj'BR\|T0 "@B|#AI q\˵Lh=ȡ3I,0# JJ68"lЀ 2L}ⰰ1,P ]Kd8p% am)F&](zE.Hb %A锱r #؅$CݚoI1 )CV\,@0<@D րIWZFV9"B`95xPc < lz$(0?KD@'%-bIbJ-w[G teR*Cf d@@a+ BO0ۂCrmt;R;"3mRW$+iX #E_$zA9/SPy{PԭEܙ ^7yrwIwlIp.' ] !cpHɏ~% -" bm!H!"6 .tFٞk-!v 4=%<*%#H&@N,b`FAt`4`(!rht⒀h"'#2!,@%"| $ㄬ"2p F #F#lX"h  Bb@I VF 6 ./BɑeXlܖ !p)^p a$$bV\)>6 @CeZ6 >M\dj^@&E2i" @.CpمFb- b?""'PY Ҁž`\ b  p œI-$Z!H%' T '  J c\ \@ m@b eV*!:*#*"  .Ugz' T'1p ?N)ߍ$z*jj&+yc)# ')F$.rp ,r.g0 (?!ʫ(-A%ʬ0 s3s111!32%s2)2-21335s393=S'|{>,+B4G**sJ*5S4[ 3qn%bK44_S#B5l‚4#63937sv3%F%S'Z528Q89i5HH8K39ݢ:s]iޢBGP6-3@s8Ws@t <AS T*5sBBs?98??q<ٓ63?MsKt65$<76?@ssE-4G-I=sDSFî@HuEFTC1pH-.ȑCYJ;"6SKSG8I34JYJt>Gg<[:#mH6%E{DMM4_9)TJtP#LӳLF4MtCF>[sO=TJ#<4PF7QgKtQ4TDEQ#5CTN>#u8GTVoIQsAUݔSiT?O;5KKWKQ5YC *7bkv$6l `m=BomX|np l@7p qWbl6!X$BnsmqW%`m!Pwm% p˼NntKw#Cw+`tw&xw17 vtgb:%r4'k @Z0PgJ E` <m {#w!n@ 'n$82"h@#mb bL5̠ %*` ,:%4@"n82om"9 t  @a`N  #:@~LJR" \ ?b;Y"dW"Ү .ySYIY$F9VS" b l $;" "[KH6 ͐Yj9  G $o78ÛSb+oy2Y% v@Gf Jl7<@HS>!Q$ N] o`N< ".:7 F0C"(M#FgKORZQ" z \΃" 9!ZZ"#:@ &,> l  ` $y9Y"1%¬czو+kv+ beVj%Z"z=`J +iV-A>@ ̼=9Z=bUbx aRH :! C e% 56`!D{ z#n bKϨkIGbA  @ #m;:G۱뛸Z mJ ϠZ#L̷ {qiB۽#1|;!`[ 㻸!|Y"! @@ b L@ ϫŠ~[V @ `~@b " b  *4!!&GԱ# >>`#UY_$% @ `lݨ #6G. ˑ^ 埞͑jH! _ 0X !>  @>b;C./ ZA_t tlC f#e` o?%B ?E%OX*`x 1Bp~!C <(OA@Ԣ~Dl4edȲ$絘_PHA 8ҚŸ @}9!G&"ll^:it nS!}UKBJPhaA"/XAhK`^!OzZ+vХ.UNze.@,RZ 4իhtBC!VX 8-Ҙf]Y.F6,>+IK+.+G[.W '*( J q袍FHK,F߂lp4n`!er=pĀ<0\A\sKJ-jUĪ?q,ELJ aa f; 1ý(}qCNDžp (40[ ]Az|0! # [xB$r@! =yԳJd;5}0 ihÂ<03G|p'sȓP |b7A@)ֈE1`5A8Fj2h%II.Q,  48a_ @ᒗ%0;D@Y yAMmr^HKiCp 4} uV)AdOgc?}J,([7@? g%Xx`^yc~~%|ge`BPr"}g}w@ȁEXQxW,]EHC PRP@mq@-a'Pq8Yk.xb?HLMUqrx67@🟣mntСJKSqqxBBK]^eHIQjjqWX`23GZ[b34=#$.:;CCDM"#-éκ%&0klsdelүabighoST[/0:()3[\c&'0$%/ABJRS[12;fgn,-6*+5'(1'(256?-.7+,5uv|(*6)*4JKTH*\ȰÇ#JHŋ3jȱǏ CIɓ(S\y˗.YʜI͛8sɳϟ@ EУH*]ʴӧPJmZԫIK h'F4]6Kh(rvvŲg~e9pA7sh՘33d"!%8C nh\s3 hM!EQ01uAe0C.}bխpT I&R` @9+yYA0G#TQNH tB( ۃw\B}9J :dy`g@t]B@ hWJ)^Gl Cq0 %bAurQԂ@t^܅*".S = ā@P (ɤ@8@/ |)Q L`Blk9Pw)g=efP$o 㦝 d/b\G:M4*U^e[:a<I +vɦɍD4_00~4ckHd"P=pq &HNG,0@ ; `$0$0 t "pGnknrpLu`/ i &Tܾl`q( Pcri#+ WG<.P POG}t,"; ,мkIk=0s ;,S, $%r-/%rC!x' GAJ4 sI0#褃@餋ꧯ@믣;굗~{鹓{괏{za re,oK j@ @*zz" A @S/k/ 3Wu#WޱNv $" v4}7 4DBP+hʐ˻p1/_Z@$o,7 $ + D8zA؈ҽo|b8$5â z蟓(/ d-ۀ @B t"E. _H0%]It(eR4@P)WyJr|e+eJUҕ-cy]+y]&,mY\2|e_p [<5WcU-l 3 Xa<`=h_Xp̂ Ħ@9ǐS鼣wS*S\*ʇ43f󙫌E;эҤu Rʋt(-G9KԦ0IIKD9pPx x,AvQoFuUSn 48# TM=A;*Oz_?ln~+Yʠ Kc&` 4(Z0HК)+1 YF8eEUFHV9P3;-<9A 9Anǐ5! 8PO YrTVkokq}hYJ> @PIivV @x.ny[ d["uo;7[n V Etk]bw!٩Emv ! (Nq& 0w5]65qR'+HC5# V9r e<A6cBnp;c:VJI4/ ?0z hX fF8y!zh`Ͽ"$B3uI9ϱ2rAz\3;Dس7H`gFQ8Aq0R P7Wd͝[pHBJl`y >@PwBU4q&pQ!AE)m)RpEXvhLaE8n 4!b0,oL`~@. G>@>a6 *On 'o};ڢx ސK]&(`@6 Cl Wo H{kf dDWo y! S`b.T<@V!аI<"d`)^0cGi7}𲛽 +ZV!χ~A zv?U |x+;{?_Z|HǼwݧ~! —.W viw,z1vUf/V p( 9`@ &0hX MO؀kz " !cpq4h i #&*Ȃ(qa( EX``V,9a]0x#a7A j(vml1X@< *z8iXX8 Zha~8NaX>78XSъ8a(J (Zy(8xhx؊،HwHvkwoڸZ%x8XȆ6&!AA>pfX؏–T` aWSvR 9>xYɅ,u\ $P\w2'*&bB&*ɒ0#Y'y@"/+Q+X";) a** Bjm((%r@)d)fY)b)F&8" 4 ));i6 8 gr5ɖiTY2ؑh??C8VnP# n1C2S4PH1S1Wᙠ)h5pg>63F+& 5^6ǁ4Y=2id%'+S8Щ4Y5D5ٛi1ߩ5 f@ D.iN FE7`71KcA: T6%g D4E E?J>DuD@e5Ef*bD 0S^A!,*JKڤaWpRF nE?FU Zx:cZP<`v@KNREUzOOZJiVPeNWmew^`uTF# Q}lUWŨZVJ%JJ: UpaZ>cu@Z*jTyw: .{pYGBg p!`Xz5_u_s#a5a*: _'\ez?֤v[<@  }5%_%ˮ:WX ^?@v\ K/oP? p:z庴N+ Zyf,PVe{&heqY[ f[fi :Shh@6 0$f?eVFi!WXw;`=?A+DOHѴ8xaq0_>:q!gr3tV{ɐqw:n >, rqs+@  w !ɛrGr"'*'1ks wl@``۹C;oA/ #˺Q[{:f>}L|Ǘ|{y!<|gxx+q[B}ַ*}z&Po<{|waN}K "HlWM90sg0[zBZ9@al|z<Ly`*<8?'D($8A8R 6(|#<̰ɓl|#! фMqʬ{mPaPi oTZΰ9j0W0)` TFrD#`d  `ך0b>ٱԲip0p`z1q]92p_° !t-(.脞Wת 1@k怍 D  70eZ )`c>XvaX Pr '@$.PCMͮmތq`dsAV~yQ upH Ez$ *DN""{ HR!1l0=γa0pN}`H$q0 S pű1?_^ZE햩+c@ 1 # 'p ˦g a!B-zɗlq#/ b1 7M IIM)& ZVnXr#=pP=?$C2Qmq)qr,kj >$BB$s%-Jap4+ wFd2^@ac@O)PC'G0qp0 +Up \/b@` .`@֍Bl ńd&L3#P@<2|0k  7X!@Mx̚n E՚N܈!Ũ'.X"E+(Ӄ-63Ǝt' $pA'hqCZ%L0_h69 `*I 5t88.*OgFL̄Ŷ}6\q%\sE7]u6= /PcX!z/[(J$OPbxQ$&!>tˁdA![dIZ" F `0 bO2kY K C` FS+` X@&әg?SJAhWb@] ,` :1("(Vp8 a] 6lfa",R"@(D1NcˁSB XT/Q=ZNl]߇'xG>yuW@!F)fA 8M%$"08. 3LD C6/~y/`="c& ! lr1!!0&>4@ h@2A 9h A d|@vb pD4E. /pm$4?h~̇>O4a I*,` D (~s$ 0` 'L!H@c=/I>B! Nd&5INv2]k $= App{ q,D" x&ѐa`_ (Cz@=9pn2 pBv3ȠPXp&K&P`!$Y@лpIM [rM/ 0I3O'*Sy"k8beCD#"Cg(L`CXILf"Oz @h ,4 G@B41@T !OƧjVUvt(^L@'PHI ,G4_(<@80XXqD;$! Wr)2au~k rQ+1-jaA!r-%(z72aApY  nb \NJ@"Ő| aRB}hRp|v4rHIX~e> E@! D)\/=P@ x{hUu'FqUy# ,`$ > B$(`\(Bt+pb<``s1$7:/p2+x A8L`~Ѭ恀 >XAn{) )'n6n@;0 tPd ]]hH*e)g*$NL #'yaD0ȩ@_N╳e7xPOK B)~'ד ʃ9b\w=M/>156ۀhsv;6{|B@33w{V4/Θ~, .pH<Vhx. %'wmɕ/y92׏#ZE< rx'󞏫GO@t&zq~x_{_nn{]!& x Bi_;Nw du#TpgxƧ쀳?yWxg-yw]oQ=?zҗG}Ws };z^mP;~|χ>WWs}M}~]o?xǿɟԾ,3@S ?tT> ,> = = ,==4<Tl<t;:L97 t<"t9 4$*T&*='BOB)BB./ ?.1$2 >1D35<5d8T7D;1=$$?6"A9Q{PP9x"GD-HDdBBD p+8 gL:+8p3MDhfSR0 @äa\EAfT\\p20pج(8HR)ɀN81:#:7]A2PprmoЁ{ 8t\GH8/Gp~Q@xX$Hă0iFktȂ hw< tGx ȁɁFpGrd|G( 3GzǓPƁ0JEgtFh0C(7H/HhX\98 ҚKp k˷KB ]0P@ @(70hnj,q ٘P8KHKԄL˾89KT ôɘMKQXr VS! YdL<[$E(1iI,@8SϬϓP > @;*IY4` 9 OXQ@U6,PzEP f \2J`SO *1xQ]Ofl) ă++M20ѓ8Y;9:8;D 1QɊ882؂-`"Q8R蚞؝60 =S?Nё)*Qc G?M5mӓPS6MTS1ԁ UNETe%KŠ S9e<5=HR=b%V +Seee-#.T8 !A.2(/  ց`WEK.҂lbz "@2t]vP#hX RmWwen5o}Xoum؀esWk-AaX}#!q%7@8S!8UtX@4V@|O0uؖvVxV0)uڎZ#`;FS. ~"pW h:0 ZhOY75HZHڸ厨-ƧVl :[e&EPkّb9 Y"5AY>x˂ǐ֓ I*Z!8 H%/Zͭ-X:8Q3F "$@8JbO0xx؝݁/gc+܁H] ݁^(^ׅ]]{ *eYS/+7]K@ɝ\<\@8c :'~#='Va+òв3ߔ]č. 2Hb!vS 4~{P :\8 RUP XwU@:D+Kdd]7h ERBMNc=fC>9qɅ'M_斫3Ye3e#<;Q$`$4c&C(dj*kf[CpgBDq6sC4tCt^uNCv&g8DCCxigiNC|f)808]nE`^^C&=:4hg`DWd+X&#X%PL?iai@𘂉N.P̙ +Xh뎥7У;in>^^I*| Y (0204!Ye̞Hua nd '0H H[dX+8β-P!*3`dZ 5(0D +8x%fmu (!Dx^XH- O an]$ eYH`(=K0ֆp]@ ȞN-ؖmV*.(:@ZxAl`!pq"x,&G(|F ni8`D X"N7R1@#$.F^_hrv7)G7Lx# ;g10'QЖ -1 .p6s+ F `*r>g P/R%d;MLAKTے*rBH`L3 ֐@pI@!<ף/@{ӯo>? 8  DydPD`I!IWX$W!QEbNAU0x#<APPIؓD@p RJE2 #SQiG@*E A͈EYc@n E3!]v$R&XFgSE 4^FD11F) #-+sZQ H;/f PЂD| @$ZH$x01H vݍ90` ͽTj{Zu-;.{.׭ zK%TI+A2$!/9gFa/qqQD_4J1@ 4@>KS0A94i@΢KR2^cTK@ ~%pf)/+Q(-2ˊ5aA0ėmj $IZb)j pjs ?PDS7ͲN//4AA(=|Z7/5 @qc40U m:뭻:"nT;w@e,T95 }P{C0JD@&l2*]*hp)-c)QA+DX;)? L.*HҜPFL %bҞ9pIU0=QE0ډ w 5ĭ_tR0dIVFN@2r |a/`$2"J$xÎG^L ȝfgԩN^"(1v]G wdpQ *NZ|8dhD` (D`Ћ)BD8AHB"pEp@) E(>ApK! fpU@vrg/a6"̅j0"I0 @4`Bjռf6ͬ9k" R kڢX2uh/T"cD6qЂ¡5W AJia rAHF:hH%"9юؙd%/ %θ"Ș.K>)P*1]ht3_@wxgpE ` R@# D`<$JDC&#A z1V2C6#{Y0L9"@%N "siP Яi"pxS}kPZ֒ӜakYn@"%DL`1eZ´h$6L YpkV7ig&0Hb$kH&>`Q+OQ `p *r` ' z/VQ |x?hJL!$3,@˨1z2L *6׏cU°9| J ^!<=0- 5 y P\j9rs8|pтT"x4|e:І>4<1X8UE!"&pA{Ԧ>a Tծ~5}j:ֶ5Xk.ȹ5-a`c3fղigSV6z fksۥ&-nSz>7.o),δD|WލxIoQ*  }lP"O%?XB* X@$҆zSN $0Dy0\P|ǸM- aRH$A1 k HU V@x~odAlCCb<*>rf:KZ"Z A0x6W]& !;>>vm곆+. !иB$B `KtB nbt ) QS`O/u'X~w`zHP͘)AЁ@n" /ΨL %X 炮6"_˜B.AHpIT Đ+pp& . X^m ,0PhD A [.AJOE&Ky0BL" Րdf$\4p`p>H \o!DG OlqF^0*6Mp 0UPTi2"D j1ASQ|2 pATD,e&.W@ 00k 7F;!Vd95܀aG}[W|SІDd@PbYxu}4 A(8qXGXGQiXQE)ٶTX)4TGD8|@8yL8 K5ڵ8(XYǟY}ԘU} 9X7ۓYEmZݮٹ[D9[h:':7?z-:A:ڤC[g]Gt+i:ڒ:{Z:: ZᶺۨϺ+ا:Ԫ:kѩ;ί;ΰ';˱7;eYGK;_/m;ok;˲;dz;ǹ; Ǻ;W׻{Tط{;|£{7<3<ćOKk{gc?~0;sW> ۢ>ꧾ{̫~~>㼧+~~S(> ]'!?y?;W?Q?oi{[G?5??[?3ٿ[?@8`A&TaC 0aE1fԸcGA9dI)ʁ)OƔ9fM7qIK;:hQGCl'ROF:VVպkW_*]+XgѦUkjkƕ;wر-ho_F paÇO8,bǏ!G=Osե3v$S/x]sMz!}S=|7L8 X%tm`<\t_A݈d}xP5fr⎏d#?9HMqDW~YFe~1Uo޹D}9 >B0T@9㪡d:Akd3d^[e[tĮO޺]w^q>1/DX~|;%/ʭB=_#.?9U?qdv_rrcH`wo{>xyxGXyg~o~zx|8wG?>~׾y}緗oK7@*P#o\`)tNp '~ xB ~ ]Ka ?p}7! BP ; H` p `Xd@E0zq8H`L#7vQp#8rqa"hG?YdFDģ" I@J2pl$H>^t$#) LFҒܤ)ɈJ̱b5Qb\#gAr4* iF[s$0?y\Tf-I M^ҙ%30&Ӛf6iFn,%[y.BicNyq=mO|t?+OTp%AfP.tl eCPN4hEefQntdG)QBmZ&,dUxC(FJ1LGpN  E ~ĂiQ cS @49jUT@Dh@jF(C\```u4W^J9<,!/b pD(@ 0 `%w E#|!7 zPI|bY6kA1-h;Ԓvk[Z@^ @0H% V@$j w9Y↗(,xgZ65oz^w}|KC7)oؗ44 \Lr$5 n6yp*ĥ-\ 1scqoJGe0Xb `P!zBP6qa5ΰcLNA &K\rl0gn,[D  bCB`L /^8oFv r$X@l hB7dN@eF*LṔ$u@Db_@4=B/FAMjqN  dq* K !PvmnjZUE^M< *[^GmDop7b&=!@I1o\<8PT$QQU "'ILrJ.@;q1Da \  ^fՂ|U_c4' @)A504xA _խwެ-'H )VM2׹ H[s r!@X.RD#Q9q w}lP:98}L8`b K@ r_EIfƷlvu,/oV[֬v@dM̮P d޷=1l\C~vM P2m !0<-)^*0VE=0< EP8nUnQ3&0Y.Vap-^0i,fq+n0yp*v)~0(p'0 P .0 >jN p P+ "p ͐?pU0 p(8pj0"k"0q; qG Q 0Q ! %q- 11p9=y0EIiQUqY\1P3mq0lrQ[5q#1 p/_q1gn19߰1)'Qw1m0.  r!! 2"m"),"-#528##ADr$$M1%%a2q&0&%ux')'r((2) j)))***Rr+٩+rv2,[g,tV,E%2k-3'.!,r.'/p//os00 m31f1k1&2%j(221h4s3}3=g24]-Ie4S-UZ@36Gf6icl6/&7u3bx77S`s88_39e9]9%:3\::Zs;e4L3<5^ss#q==!3>%r>">12?=Ӱ?=?Բ@Q@VtAJAX4BUrB)t B/NCm<=R3DAxM8OP8dYXN\$eLhqKtx}J8dI$XH{䉡GxgE8WUNjxɸ 8i%ݸB8IR$X@׎9xX 9Uxa8%y)m195yy=A9xIM8UyYa9eymq9xy 9'4ْy39?yYK9Wٕyc͙" !M,=!",yz%&0kkrppw펏NOW&'0DEMBCL?@HTU\01:᧨EFNxx~ؗ67@FGO#$.䬬]]e`ah23?H,-6@AIqrxBBK󁂇mntВɑ9:CHIQlmsjjqMNVXY`KLTopvƴefm{|迿=>G-.7UV]匌delүST[abi[\cABJRS['(2'(1+,5)*4uv|H*\ȰÇ#JHQb3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjmi`ׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8u^Dit> P"dTdX~Yvޕqdih陘9ij)tix|矀*蠄j衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무!Zk&;6F*Ҟf v;ކ+ +k,l' 7G,WlgwK $+r(Yؾ2l8<@-DmH'L7PG-TWmXg\w`-dmhlp-tmx|߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/o H:'H Z̠7v201s< Z* S̰y!:D q: б@A$*ZX̢.zO!Dư9C3QcCF8 he6"z, 4 'X@eC&RddfD 0\ p&`(R(Ey'` Cp@ Bn$.yY3l"hA `3B@Ʌ@|#5ÄLh \ 3 u@pdЀ70Yz! ܧD#O p `! I(iSTLgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'KZͬf7z hGKҚMjWֺlgKͭnw pKMr:ЍtKZͮvz xKMz|Kͯ~LN;'L [ΰ7{ GL(NW0g3u86 r\;rF"O.A2Brd#liY^(N*!,E!",yz(*6kkr+,523<'(101:IJR'(2xx~%&0;G<=F-.7񧨬CDMST[ghoEFNʛtu{67@ppwVW_|}hioz{()334=骪?@HBCLYZa͍LMUȸXY`>?H,-6')5mnt12;ТJKSWX`HIQjjqTU\KLTopvƛQRZ{|迿56?Ӆ*+5nouUV]匌Z[bÐκdelklsүabi[\cABJRS[+-6)*4uv|H*\ȰÇ#JHŋ3jȱǏ Ct`ɓ#KVT˗0cʜI͛8syϟ@' JѣH*]ʴӊJE9tիXׯ5ȕXU%:˶۷pʝKjںx˷߄ezːÈ+^̸1_ :Ld*krE(ALthӨSu Za빐U˞҃AϊCuȓ.rKNسkνËOӫ_Ͼ˟OϿ(h{ 6F(`=aE'RJmH"h!,"E(&XY4#v!C?^DiH&$M-(אQ[KT#\v`)dihlp)tix|矀J%ej衈&袌6裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*무j뭸뮼j#Q*l*Xula{-kR&9cM'gvӶ **zkRB Zֺ{d2k+QQ#qr1+t0m 4 k~W1 gq~OT$?q(,0,4l8<@-DmH'L7PG-TWmXg\w`-dmhlvo!-tݷQtx x?w?5C'.ȝ9A3T9G;i!I+.:؍kys飷.nTqfWJU_"a'/ܬO_Uo=#=+}F;>[$} /^]O?_C":'H ZP P/8nQӍӿ1`Cpj#|a']2d QˆF)aCC%C0yMQ#E4q- 2~_<6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @JЂMBІ:D'JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMR %+0 g`jB`^l4ZȢV!ą7 # @ % ` @T>I hN^@5X[lN@ P9 pdt$( 6$О<CH{'lgKͭnw+3j@Bpix@[zq >D)0 a$|p 8k 8iY Ҁf Z@D`& kl<ۀ@RbB|k`A (q* -"@ -`p\bX @\I`!sl8XO VJ`tARN!, (*679CQS[ACMꔕFGQ㢣OPYqsxegn46AߞMOXHJSDFPIKT麺|}=?I8:D/1<ґhioZ[b\^eabiwy~jkq24@yz~tu{CENjlr13>68:stz;=Fbcknouuv{>@J57BWX`TU^z{-/:ٚ@BKLNWbdjrYZbKLU_`g]+.8&(4̂ꅏozORFlntKOE_cM~HLC`ah؋`KMV˪mprwgDG@}gkPÏaVYI[O{35:d~fi038lmqRӾ.P?tY]J.J:JrXZa_?_6vOotSRUFy~X`@c&b! d0AClP! $tK2Fst AB@KuKmD=p@@>v ``n`(@ ܎PM<`@X8@,$1qa :o|rI @}AhO@@JdA@t=VfC6gy\&.L ҿ/~G-n'V8P6H*Hrt۾\ IV8 g@  d^"48b!(l!MvY .VBXЊE,BxAH( L` <Û@T@  R  Ā9 PԠ! аk Cx7)0H| Sdp@q ͖@`)KZ: fB7$R! , eA hMm J@P&T%+]Iln L1ЅԞ,ky\r H=Z$Ā?k4-D aB%6e2SQP 4iS)P 5-)A`8%-|*p*XE TY`pk\ɂ&\78zJ0u0 Bj^]G?2hCb o+&PMrR T' '0Ed5c׃u m` y 6宨\00T"P$ԧ)@x ٯ: u4`Zi;C R U! pk\r͉&|! ]!x)0Y /AR (HKbɠ(@c @B&`u ZH KHCan #ַ4@!@*I6!F,U6`6C&;T) C @,pA LY09 QЁ2Xq@S,f2$[2Sor@{9s .(^`FZ<$`phl@QUC" $SDMjDqs g9 ֥L(>i4 ]k0ZC`l?] :9lFu F6m4\`` vlM[9 ] r?pHdAv@t<#>  _| NNXw]!LASA LX p\+xG'!z~dw)xۼ$_r%+o9VS@HEjt xx/ֹpjAR 7`}{~F>t=KqLBqD/!A T01K@. VlbcW\` @:s` C0``(P!p0)s sI,:px@3Sm ICVc1'BD@q@)ׁaEzC+D;HUvslD`s0O)=-VI `A(,(hSXOXiChiUVLWAd ϖ-\>0@mf #+@p35P]хbvC,~,hsYA- z[MPX4pQ  U/@ h(La;`Ho$t "L pHc0e @2LD}}N"~g~'SsK71hSJ[`P~种XHd8 ysss`D` Z51Rȑ svyQ~Q~7wi4szB)+50`av Xt`0p iP "&t q w}a36UiQB !%+c %WE 'u8S(u1s ;62].hJ7gQ=%``si%~-J:kQ0 5W@ϕWLũ #09'✡j6j@"Az&(50pzOIvP O)^1 @ 0{ }`Iy5&fz?ak04$&97Q ]VpuE8C2pKLFG /b91p P/G,; ȱEpk"P=)s- 1?8,FX'BK. \"V RTg+'K`/[' T (?Ё$$K/S 0)cGKB9-jۡÚ\;r@6`p8P3s3*1`Vp;0Ms(;V^M0xөQ`@>E!pP]Z3Z)3:J@= ۺK^-@{ʖ n %3@L`53 kp ,\`<,<Q6KI 41+ hE03\wHcfIzY& @(AP8;pRt_9d  7DZqcM1щn*[iMgfM}]پa@0eՎ31b2 `Ne_"%+`삕0A@gPKLQad)1  [6jp8PЬF4ih0O `Pz{l lܑ κL6n^[} .-Vuw;{( h`!`5A㻼-NR-WRd`_~QGV,~(09:pO;q(nܾbNBPǝ܁!NKP[,ti.@p8L+`@.jm/-Op k Ng3U.6'^C^0{롽݊w O4pA0Y+ RM㞴#=z^.szQPDD}@S@gn[x]Þ t0tp` |DL B |op]@0`SA@Cp鮉B3/ϋ80/p3$@zkFN4zx=5Km|jsePZ0(Pj?ul)BOP~J}O6PJaVϕ6noU?fd_@&wo6rhtPk1bUj MW @0v@3@8'0>Q S}@@0  A4aH9 pB 04P@  a l4@2E@q)8DFCCp€K d !hH  RnPT )rydy2J : `ցw̸@+x``9340;Ο0erO$j \Cv([,G1` `hǑ{?=:թkJ[HG~2HtEsTW=~ -29*B(@tP@`<8!6 . oDLp E0=2TDoFL#Q: 2x # a$1:5耪Z!J/!J(4(2 зP`LC¢:Tr AC3C/J)=#px&TSO5:SSOE5UU.O k5-ԿZ2 \XƸzV (~ 6E6YdUXsΖU :."(.V(Jl4\wf=5wGAeWP㯔K{ﵷ,zc_}MFW.9h`Vo`8bT`CS PB&ԃX^O]Q_vهaYfo9gwg:h{S•F:ifCn:ji:kk;lUzڱF;m ߆;-[n;ooU]jy\GxYsN =Эg`_w䉧z1O=V7'|Ϟs5vхyw}?GOG7@px]` 8A V0kL>ѯ}ΛdEЂ'Da Kh<1yV:>ow_ UC,dZ"áOb& ȶV1|lZ1KRDcՈ< QuC4f9PeNWն;0}cFBz| <ґIJVҒ $1INvғl>9ʰ nDeTҕ$=KZҖ)oKPt5٫ EDLf6ә`f>u(hlLnz1f8U6$qHRF 90)284",a/ȁhXdhD%2-n4P0YC2Xq@J)@(JĐ>P T&<P`גztM$T'BU0`KxT_#)H( 11( -EB+;81Wu*3@  $TkIHJ ([ IbNմ^iyYDBiAlpqVs RBIN1>nt˶w*J5A<nlD @omZWn~MD1x#ؐF4Й$(4GE 5L D b،J$f;'Jkb,%k+h .$X@5a@ ``\(A. Zr^rV$aGp~¼f6<ͺ.V*UҎ643Sd87Lu?DZe;ٚ2K~:0EfS>4G/آ7zE;A.(QnqY=0~`"t(A@`0<OOfz<H=nO8Aw9'N +߇(у3]Cot W!9bu/ŧQ Da&>aX#$9 P"ܱ~ [ڃDTD3Ka~ hEt0 p0(@{!, "|9ߊM^]QfDhIE_@| :@Ї`)ЛUn/gXaxn3* `* sC!;oc[ ;`:1H;& [JS;uNxK1A \"d :xUYbiR<L @: FH'Ho"H*` F+(+ - "`P` ,DBPS?[6|?俭L5aXyvUH@RXTszv`@1r!h4ȃcx@ qYGмW8;d0=;XG(;޹@k;^WYE[E]EhEX ^E`[ElE@ E8Dr;qt pŢ{Cd(;zG|,zǢAn,zQ8HGsFnxEH΁FYlZ$G];VDVȁ LGI, H(R=; . eZH4 * :D8h h9 -6ªʩ¢H;L?XCK PK?8.&(iH+SLE1ETCDaJvsy aU^fhvbQL@ ȃh CL8( *X[_P M,$O4G%ѢN$NƁ]Jt P6\`&ؽcPAOR -.5.9hS~>;G34_5H, r!G"cρhFot4gBtA? Ђ"30?x  x 8po$X. J! HA :bpvg5("S@v?Ԃi, Ȍ0j4 qq[xopn~qp TM؄+P6$(q^7xlूR::@A*'AkjOcȄxcZRp6y3`> Yy'mPp>785gzoOO {7=cЅ7@`y=Ӿx#3`I07p{?n/ŧS-,<=UZ!C۪`Xrh`#`8bh Xǀ ?eү#}.`D5 <vxwTxU_ \Ru)S.bk5@8#_7WV(2% „줧B/xP"E 2TQ8F0.j.5SXe›8w32-bO<`4)Ԩ R7d¯Vǒ-k,ڴjײm[ܸpBMۼz}r!َ X8MD)' _Ћe} .+Тv傘"WXb AֲgӮm6krwɣ|2iZG跉{4R`~wi?o<oӯo>#? 8`hmF * : JXQ7!jap!!8b`UH")"-b'8#5x#9x;#A 9$X$I*$MxQJ9%UZWj%]zB=9&ey&Y&m}j9'uygZr'}  :(:Yx(: J:)*j)a):jz*ҧ*fya7jJ+j)[u,Jo*{k{e;-BU-NEC a 4yvhz{&.@:u(؀ H_ثdͻ0Q֋:$ 7TB"vp&JfYumbQblq-N 뜔X4X@2D#W/DEd1Xbu̪^6,5b3l aP/0A@@5 F }PEGS#t8@ qhUObSVu~rBT9[jzm8]Q| .dAZVP+N+i K?}a=aBw 5PGBvM O}{b% d='/qACH )f@ T%7lv?l59u"880EX@B 4 9D,Bf :Xx NYPx}MJ !գ0`r-te H ="4!X"D$p뉚x:~M;&1E2. @ 0B8|.Lq@ EP .`B $6 1yF&rD/i((=!c +{)hҏ=bI;r&80IL4;fx•ҹsƙ˜<éGsFH>},(BjY)ahkhz%&R"?-*xjTg4UGC*Xz}.GzNsf.Ltjs$MYS~zY:cy);zČhjuئBNdD)<lJ)'0ES!=;w(9:[. ^G0@DͩK 4U[H1L@\ Mv>@ْ'pI 85%m2Z@m;[ 0^-LYNeωAF1(:3I1 oP{ ـq Da&>aX4-˕|;djqAutU9 [A& }S '7/Q<kl[>5B?M1jHAU*IY {F` B@-N; (d wR}E3+"8!*޴;C*QvcITG6,Chi"0V,bT"L#f,DK`[ ^G̎ftD8zd}]BG;,{k^A~S,"rxspAxw$@Q ;}OܛR$D(.+{;/"Eb0zO/x8" \z'|7!u $P P' HlL` H` tA,AB4 DO$P0 T@H@l\BܟO@^` 0A!HC<B<]M J]ɜ@5ă0CO;7|N* @y@@@h(S6tL5>0Bd%]l@0C=xXDLĐĭR$Fq} P"DCY&$hRia!|jD +Dޕ:#\Tc) @) %\B(@A,@dB9+$ijٕͅ"@,,ǜi)$LC?:Í^+TPS ~sxͥR~ܛpg~K۴K{0s>@q/$ 7W~~iOW3c>.YW¾^ؑ_9FXc@,E#?`JGl?@8` &DB:( ŇbXqcGAPH'QTeK/aƔ9fM7q gOnXHu4АL 5jѧ7JmkW_;lY/}Bgrm 7l۹oջo_n'ͬHqqFSn|rī+ ּsgϟAW$Y#Dбխ,2v t6^(b,i16̤C>xqG=tDQ˘nTu_ZqѧW`zg?ߨ|_׿OLPAG{n!$ ")@ 9A QDݳoQo9SlNkiqLy  U*H 'WRF)Yzt(( - V/LS5l&,[KBh1:Dh?HJ*- Bih +ZbvLH <zSQMUUe\0\(  #>(9 tx 4`0Zu]l`#lUW^HN 2v pN{W[ ]j- Hb bpa}).ۊ1L"N8`'& w {O Xi9'p"(hdӅ!@f.(}~j egb5:!90"*/


C%#HH 8`*k4% Kdb"x g|a7 {dJV_hFAʍ#(E .D0 p0(a \ EpwABp@ (@ @&@"X;Pz`  ŐM @ZЃ  :ç  EҐa)ӠkV!H,H'AGB&OOrI`q2bZPB `%H*ԓd$F x֤0H2vi &P @@ƀQ5"1$aPI *DA@ @"`` wFQhmRI:.-$L#{Y<y"))ցc;1dm@CnSas@+= v2 Ddv@;,|KIN(Q&9 H+faGS pEyzⵯ|nxceh _ 8aGQ ;(,v1 DxKQ!Paw[z1;"\+0z rOb C+pL(X Tq,e $(B0pu ]AH&*hi Bڎ 7B@&v @``` +Ў6i9u`0D L(4T5v[=:vߐ=@ad@vtQ .D2qP&(A /uPL"0+. _B* Bx $`$+p$cCBo{{v7`w# $5LC * "T*l rOߒb@q BT 0"WX@  T3o36bAJ۲u#J Ѐ>Fg? "< @:: bTb81IDB,p Ha` B!s(Q0*:ӳ=R!`ZaHan %<4ֳ =*8A'*F=$b@fو~@F 7#Fh>@H t "-Ka H@ ^r 9N8 9a365cGԀlj# aԯ Pb #S9 " P H5Db q0 QT(SGT "TW?"AJ@yRW !jC2~r !{i'aiT nWv2 VgVba>A uFU*Yfr .W B iL7c*c  @ @ b` &I>Iahc& O n12KO1US9 jV nPA، 6a~!k"5aR ΁Vu* РS TJx5.:&mY sU#$k&A`\WkulpjVoa4pkm 7閮#H "W(BwrclZ:!ZD@@6W=*ku oV]ݳ!娮iv;^owpyk%@ H `"6R8(8@DV# V $ 6& ^r P< ( XWBUjn;&Rz R.lx؊}W# ja AtxJ"\ sI.% ؅f>a8yZA_6?xW`&^XؗS%"A &14<2V *AML! @ @JXg`W9 <ǢHbyS :Z b{Cl;SxytD 5P)7[CDǹ|[cH;}gɻE;՛w{i{_仾; ;M| ;Y[B|)!1<#$5=>8E|cIQɅq"gr*r2sBG p?X Rgu:>Nn tGqϕ艾޽{<]$șF<-~ O~+ T:NjXb{7Ԉ <%^Qb $􈏤+ iV j*ꢬ4 H@2C34!B>WBș%b@<"X`e2~#&0@G":@K 2Eq d#F%P+^c-{z#~kDk빢K r_$dR" w Z"4  CQ-3Q0 (+8 > )(@`X4"LVIR _@r qdfA 3+4(㈴F zXA79j,eh86ڵlۺ} 7ܹtڽ7޽| 8 81bS6Mlɣܰ62Ȭ;xVMpV]NpcA \ G9!Cg|QQ#]2mʃ+XqQ en`p p#X gPSpP4Ay5P@50gX~b"Hb&b*¥b]Ë05S:p+hZ[%Fm@HQ:GiQĶI$ 6&,WT2PBGZs]'y@\TDmuFpr w߶PB LpAG  a | !@@r%{ s馟zꪯ338XfcC O^%(Vmx^d]x:PEipֳQ@@javA 4JG8U Apyo[Q (Kyt @,Ip/1a/1ѻ0w!9OAQ$ P|`('@R9!}  PC 6`=l `$/H 2o|}`@$A 1bRJsh00'? PvuY<6ogn#+< *M-@4G`>4r3#*Q RAr,xZ|S3i@%;)u6{rVЈ\( 9ϡ0S@̠ n  q fz2 @Qt$-IAIJTGo ‹] ![*q0N$p| ^[Ё;VQDB0Òc JV/ 5& hvf? ЊvA^GԪf=HkW ض mov]n \ֵ qms 莊ҭuօw r7-y;wm{Q*w/+H} w.`7 nKxvo%_Vxp\.<؂ЈZbx%vS)x4椋kx7ޱ (D.#+yLo| (KOV|+@XrZc/ydvKˌ4')Bz0Ǟ䅪;pVfЁEfPj]`ъ%AϜ6pOrCWS*MD.&KNKbR9^^ 4 6HXR vABH# OH^Wl!=A<;ӮAa@'렗ix#V C @BІtKpA>=YMk^31H@~?=XO{B9xX'Mj!'q|]yj! i9vw<$< "w_?>aPw(Ѕ מ?П]R os @n"/IN@=b*Ӗy(QQ}O9U|PxW,03V@e>$}x'1$a:ݗHgI` X!0p5m/rGlǃEJ C}tť|jqW\6J02Q1#0b,9K؄OzE00 2Q0 ?Q0 WA0 pUr80d8,r,Ah8IȅNHqhT 9 2OyG0HX7=(EJ@ هzxG 4Іt4N5#jC؆HTs82Jhyk"ghgsGS8780` Pq爍h44h8!hQ؎" r#0 ^D8MFJSEh}x ,@Vh) T31i@=Y?Y3y#LAQ,Ye@Pt'0+'A6BtwR9iaDYnɖG6ʸևAs9`{t 鑪xP@5),{\G t2y!h`]dI2!lf4T641TwY9ivG{FYjp8UihxuTyT>p %IǚYCijɗI{lF{YqH  [Ol1cU9өgQRLI(px)?KT PC!$z jOAnF%}ۂqJGOauY_ա U9$LY+g6P%چPJI @?@20IyP,pUZK}[TAH7Q4UiTPœHjkLtVTZSlaQTOj~jiV&bH9P"ҕX&"ZլJdՊ]˪ꭤŭ* YJ犮Zeʮ*RerEJ+ DPTA2vZjC_@D h an @(R+&beoѱ{ojq"J!(, ^p=+{A:{+uAa1+CwZOfZRD'F{dTpRapYW a;[$K#x4\¦u5B73V:+PVk+q"Z{Utku]wxÊccŅfdQPN@|BnXTG0S(!)`1u8PLlpI28.90(I hU I '\O@Y@P1_&!LCF`ymL @$@ir+rɳPXGS]AqbP~.XLX[֍r&rbԧoMm1`oo_oWF@Um؋و1!}=i@M]sVl(xppX],يX1؍Xgr35 ڥmi0@h5m*XP 0$gםA|0X +8e 1ۂ6Vhu@Uk؆oX~Q?UH V޶&wnb33 IZMm'-RU--i<'02O8E11=âPc҉HIp[iy˗Ƽ! 8]<8cC`Sިs\,\`Sz7#=? hN<6lex> ~ نoV\ix@k(3p ?N0ap @KX4!f +|`<98"2|it0qN&l9x.#8F0! `,*l`6R! % K7BKApI4(TstM8sN:N<$4R`b ` <6ܸ @@È 6` "B8(FH F."V u É 8$O_6\ tM@Xd_ .聃<] [p=oUiDZ.`QO 5S +M9j>%(%B2PUV] ȰH[nJK8xՐX|#sh6hVzP@-R pсܭ7|P># i H1HrB^ #W: `%oj.jl` nO +JP3C`5 W'uUg] ;cm 0 mٖ -6@@qat޻Ґc r[HuL#` x@&P P14jA"#0CH(hA`   B 'l^!1! %RiHCH$b_1s2h28`D(FQS\#8>I$4p<SP@F.@7QltF:0v#G@R 99hҀh&(A$HHJ @$'Y:5IP&yJTR\!YJXRe-mIKWRe/}K`1|0yLd&Sdf'5SӤf5ydf7MpP$g9=E9&\gciNxpg=yO|6sg>~>zХT1 @*w>#ETNuG9zQ}M:?*RYXT)H @ƴi,UZSR9ŌFySԜ;:*'U$i9JԣBhReR}ѥ):լɪBW hV:\ ۊ5o)]uil4x _:2_*+ͱ VAaňle-{Ylg=٥m%miM q^emk]+mm3KLVR6V%.Pe[\&W<.2OZ;tT`+@rp @V"`/{Rdy=^̍K@yv 1@t%I8  3MA58$9`{%]-1~ ` yRp*F1b;Al"HxS0yEz`82?nq{|  :na2Qq+c`8$?@L L9oz74D2=-hyR$!7$0u0 W%4l:nX`c`dfjT' ~#027O ZRhv,l€  <8`@C`@Ua \11C.(@WD-oz/^8b0 Xp/̦M0 \ WWA\~شf3F#oKcRȁm{.ѷMlc#[673k|ُ< ~6 , {I0@r@1>r!Dj l X@@ .H#<[&?a,qsP!)²pf3i Ny ZY_3[dJR-4ڭgt\(4"Z[D!}{98 !X5(" suD (@4 K%‚$sĐL"!W` +S>E#x9<&;L$0xp%t#EqA >3X 3ưA .:f >  ! N( Ļ僑3<;L;I-t"Ԣr (X9 4Xб% A)`B$h7ȁ/hH@#H/+9`JLGI$0 NE,XjI!h4[Xr2 CLE\rF\ Q,Ep$EE[ƁEIDKœktoę4HbY5Vtǁ4 .#SLU yd u`Fa-р?  pI$HJܠ+HРא+hF?p6`'$RTL;3&aDX (x`8%jFEdJ J{ h KCTK B1G)PEH0ʹ (,;Xz| ܮE`1^Q/ Kx|ɐL7HȘTBbtc Ǹ<pD``šΔ}|E ̣IlLzLJsBsxTJȷ P7$ c#f `+ X5`` $2h<M6TT#~ FcdNvN&v; [e$]8 ;Ϳ ;qE,\E&vb(J>hPH\CV}d&)a@amf%7:|8cڀЂD.n[/ d+Nhhh艶cمِ(,V70Pxhd8hPG#i5(PXĸ/Ki~Bx`F_׫Bx^ ViN$j*@ck˃ȁ6k6İ պH耽fl:ky&J0Xߐ8,HX\{MjI6(J8.%$.Ȃ 80ڱ*fA+hS0f Lm:cA @S0XH?8$%,LC80K@kfBH[Q F pf Cmfm({A귶 VoN\e\; hvk@@h?ڎk+n& n ϰH(+޻ ^g(rH)YGUgon1@C9UoĘ>.B/tBAKLx hk8Gj]F/HR6"TH uR?Ȋ3xRQ/uStKw-t`uPNu_ua?Fth8E` Lvf`FF krA>哝pvY^ gxuh+h i%4 (`lLnf&0LE 'Zt_4x`ؔKihI}evb|`r4H? P ,xVO.iw؋e=;餂Nj) !HG5oWH!òG @nr{u ;r! 8hXl P6A{ S)nIOu 6qO?`4`8Gu-w"9h܅>OW VwovOWzPC]~` 1`HG0#0 711  ? I(L," X%ff/TtY” <`h`" 0Jf $@D&!bڵ!@ \DiE!(1q$  \K֬"k !ep Yr;Ayb?&n9qԏm?ZQdb5PCar@0vlicHF*:saʤqːPE`?^'V?n ?nj!pdDDZm%W`pME&P$\/6SMq,8 _l B 94 O>("qa`@H2h8pt@ An{مjTep%\CNMtV`FALL$D |晽` dt'gM qIG@UDi@$*Тp@!\t.$& `j'z*ʨHhN4hJ+ H)A*.C#N$Fd@ЂIXHJڔ@-`q74^?(p'P!xGa!XA @^4 p uJqEixr\sH@m(wD#$P{kc:D- Ye,\sc ?qg6( sK ؕ,3AB">̤Ij|,IKV uRji\ɤK6V>9VܻQl1hii!PTQ3!uߞ{ V[dL(Tzv D 7,& V^KV3l5q%mN@]P~\I(f4v d 8X@I Mޖ#K H ,AxKbKI(E: ./)N䁔@* hХ!-|!V(4b 'G0HrQԧr%IHD'(*P5awH$щF CG@  y>G|Bfcux$ '8Hzpg`r K_@ @— p&F4jL  l4 ./2Nrʧ `13v TkV @̀+$qL&  md a!'&)1 Qt& PRGBRDHE+@ԈaӘ$iO|ꓥH$HNd^  .) Ţ2-m3.+1Όf"@"]^7 v!k\` ]4.Bk,Mi b  `qn v[4l ؝Hy/ q\$-};`zA<` dq\x"^HApIV+ 0 8l⋐AgMq#\\5 >1MظWtd\wG魱@-1r[ex'o?,(D.&o#6Ёngp7p> #Pc=9Cr4H:lЊ.pT'mKc:B$[;Ϛt0zԪ>6`md].-ֺEh h@ Eⵥ `tSbC;.p=mEʾ6 la7ms;^7wZ7m{;7Mcy8 n#< vC<8+n[8;< `nɕZv"P^ kwksX6t#$&&\f45HC=R:ե. @$gA HpP _( E „A :BE U@u% U_<C$$!a\j.5L$ xp8pb.Xbu/2;(0H#RZq1}{?<&   ^ 8DcI FL%W tV:L=@qu! Hx&h `t "`Z$@t00BT GHA @%@ NA AQE @ > B@$A ($\^A8 P@JPIYjr!1$YTh$@0 d hDlELXwTn!XXF2]̞@cCl@@pb'"_b((F<D@D%~Dd EHE.^8 4@"^d$AD">tqB!b7z7cșb 0UD!8S,2^@t\DAce]DX쀏)ED@% jA ؀ 8GpG|HdI_|h_D`4IDcQX\r\]@zH$KCzd 8%E0DTR%eUjV^%D4DM0ᬀ#<& hK|O<Τŝ MM`tLL\LƐ Ee%cdnYd~5 WjfUW}@?@ LP% aU;S Vݥ̀0{0SMQ%`DCLD @YeJsNtrtRgɛ@"WWIZXXUg{{gߤڄqZ|~{g5۳Yfg聆"2h:h==RhZŚp ׅrhzNe(h(g6芲hh" :9|CyEGٱܲ1E0g)@\ 4ilc\D@(ރܤuiX$Zi) bWJ:j75=|C7CIH4`h}t4e [ ޘ 0Av~_0@Z}AMDȀp| | c]aWzٰ" iBF]X(0q57*l*j"]/<3pâNDG0Z PjM܆NNĪkLfdOz]@W陘y@uVllʮlȞ]ɸʛ=?`*C=o89ùh^C<=WC=|7=9 @&D0C;<Bq8E¯E\rU ' D@@h@o9E%^b& Ģjv'@mHH&@M D$ (/`E4jEDsPr' 3i d7{38os7s r 0E!1"/WC78->Dt<D1<4i5>0H,ro 3CHDqELAF4]I d8#'B% < 6f27 +?l@K4ra0Ɔ/K$A jSGT$s3='`TS@tG7]4f87@l[S\]uw@AEGPH͊5&P˰bOJ4/ tASM>C?:CyDG\tqN4ʈrq!}BO+F H\dFnJ@~=lnrO<\fX0ǎS$vtc>f7wwwdPR4?!h7wO7kD|G@9ڷDܷiDcI#Wb/?v#/LveKxɛ+:;x0hC2@bTD!$qGo S:@%GͅDU\E;ϔqGT@ R%MR*GZyMGDwc5]z S $HA+s.n@ty|yy@DDTm= C=cʜy&@|COx}o*8/BßÇxH9p*HDK t`m_M,@$ (fǨA\` hA$Nͭ_ʪ<? ؊\TH0@lDTI$G{{{fx҄˸ ]tS9CC6< ; Db9<Ɖ`v<W$j7@|s0C7c&lŸ>Qq1ts|D L};9qq^rJYqy4U'?T8G6 vA=;)#l;G ԓ9"Q}ٟQ@D&`Q9@S$yH xI89D3}߯ 7CG#á»rt/(/x|"L5G*)͚`WwWg|Yx~ItאDlA{37p*oX}է Ed?=%IIFHA5 ޠ >4xaB 6tbD)Vx19vdH#I4yeJ+YfLgN;yhPC5zi҇7mʌjTSVzkV[t4̮cɖ5{mZkٲk۶sֵ{o^{ep` 6|8_ċ!d|cɓ)Wrf͛9wk\УI6}5a̩G.ulٳͮ-gwmjm3rʍ7ws3S~{vѵ^{ws'^2w=51~}GFΏߠ0<\0IsP %t 5ܐC0EI<U]|SL.mE}{ "dpȆ衇2J"8J)rJ2,K+r /+K1ұ 8-tXÏ N+ Ԁ+h2D x54 7ePB͂2tO}OD/-M4XcR,EA-2B%&0I^ l@J6[]V\n-\m=wt+j\x]" !&B /J0 }"5 Dx@@Cà( [s^h(f UfeWn!.` `/.Hc=^tU\QVY_6  z` @ X𠣯;l&` Ek.P! n;;.H _1<.,J`V #bK#2(( 6V((&!%\t C?hMe!M?(ul]z膄ŭw `!J~?hmm{f `EH F K vdn' b  x" C#L Y dpCT8L!BܰCTa 3!p28א $&p }~f,q(HH֐T3 9 ҆nw>.``'@}3y~_)>Տ(@z"$   0 e" B l"b̯:{o!vvxG/LP!X!00 3ۯe!\0O ye"n #lFNkP"0! j A`A  !` C1> $@A 2'B21* @@ "+q!q-12Q%QH!"/3q9!+!h, hq1"  (rQ!x1n1JN. <@* <  < :Q\QAK d1!|`a!!"g011V!Q ¡~ vg `"N! wQh@6 5B"A1"*`6' (2,U6$pG ۰ȭPZڭ :! BRE D[%piɖp)yi b @v Zck9;lz` :cS B B ! Xʬ:Fp`1A!ơaP!!vI?*8` ²+􊤵 `4\Bə)`c`\ yN;5 "!J . ` "a@^\ڵۗ/nFz pݰs! Nm0¼o Mw6}qf^`wn``a~=) l~>i 6AQAWHA !|U:vN]Fo?~g~.P ޞ _LP ~4U _3Z?̿Y` kv.وl ᱂P`BlA#8 pT@%@.S8PB,Ҥxа,~ #^<@`F$Raj B`ÊKٳhӪ]˶۷pʝKݻx"/_j~={m&s}ۄ*@7+6֦"k:jYAOJ B@JV9̕s4}xㄚpl!iL PF4!yСE;ΊY]o P-Ƒ3pZ2K)@r(+(B\6ƀ XyZ8@[YAupCkmTjyY$׎<@)Di[~YZ(QNx }ņ4҈PCf̈PŠXQ `đ 7G,˕d_0E9t3NdN7 VMRQabe0x@10)Q.,=nt)!q@g0fQ_((Bfg1H1]4g>-4{gUt ba-li|eFtMF+JA%lB#3\ i͵i@Pqz꫋'EPLT@x|1cAvfIWog}`85 mB +E)uY!PX&~h]vI K jł) R2AoBE$ K ` ,ؠ8̡w@ @Eaob8GBKYO|vb:<(@@.,hL6ŇS‹vl `t ׈?@! F:K;E̤&7NzZcRL*Wʸ`#l,gIZ򖸼(^ 0sKXNrL2f1Ό4IjZbdzЃ28[ BͺAw)5vs.ЬXX@Ё]YZRr|e;І:-؋$ .RFbQ HGΈڥVP4@ x(Ah  PX,`EFD \%B=Ѓ0)N{` 0J xҴK K$I&x(HLf,؂.IFTXD2hQ5lc@Hcǃxca#@V&ZkHZ?COvQP4&$bQ!p ) ) "P"REE@ rUZqpX?2&A ŅxuB2ms[D`A30PADpNZ@(@@bAxH)pPeQm},hR*R R15SGPS75W\TP5TaTHTqT@UV%RC; 0c7AP)(z<M(e"aaA M8uaPEwҸ HfXY_uY]]%[Z0@55`[Iv(y[ H+<Ǹux1 4袩9)>0 2ի:8N 89C@@pS^Oc:\L@G@i/t*X;b+R(SՍu;HTTK%R`UuU` @n3l~PXmVU7Vb)z$ dl qd]LaPjpˑ)< yb@i]RPгdǝX%Y5u[FZ=6@Zeq[F/xPž]\&ąY]๱3΁ @VV 0YfG0< 1.GW@0Ra,6x h=_+Y#Oyfh@V``OEvfV2ɧ!Wbca0a9f)AԾ)H@r a 0`RO}4 dUMdJ:@*YbBX.2!y*lMNQ*T(WRg60`)Z:QaΈB)!axZvk⃺no6.o"f2֜,;!Wpѩ=nPsp[.p8P]Sqgn5դUɜvzžIM.*{B&A'Sǭ2k*skPW.-O7.QtY-*)ۯ pv ]+\ P>Mίi0,eB[ћ*r;&HVxC/7qqt8vW03K5;x8+6|L;yaC[ekh  618)G=۵_ @Vwh=Fp3$L~NKl;'W||{;'|i5I˹eKCx~?禤ҷ#g}h8K{_kG[~/Whһs oAoίKC{k1 ?$ofׁoCo$k?F_w!h9@@ DPB >QD-^ĘQF=~$%IDRJ-]SL5męSΎ&MTPEETRM/,TSZUV]~v#ԓMezvZlEkn\s5ʽm$ .ʾ]µ붰aƍ?i`*YfΝ=%[sP̣MFZ5Xf%.Vl/gZn9།G\_s|խ_Ǟ]{E0R}͟G|zݿ_zǟ4}0@D0ATAA#B /4oB 7C?  āҰ :!FX"~8+J4 0PT! W,jlH$q$Dž# (*+jEA- Ɂ̒I'h5fRCF sF$R# x`DLI·уNCͅ 4$Gwt&Lz.Hҋ B)T $DDX!@~cW@@Cc ,USB [sVku啠"¡o Y 8%lhl`]x(Yt-w}mŕ[|6((iajt5_b`&bEvM`l @%(c `A%gvމh(`D* t "س꫅zOZ5h jhkr 2(BBh⁉J{Pp!@\ .,!\&V6$hO@s[S (1 `! p-?;лe `@rGKbǠZ])kB 6za 'e8BlD (#!>7=D 7@e_@bx" ̧E [%r ,ȡa 64k*B&-[)͟ _{ϔ@҄) ;! XZPD hAL"•@=0!n\`m%pǢ0oBv+Z8w' r|QCo50A ֲ["^<)A`1"0jG$9e}lo` Rp˛w[Ey\h(fGo[ i{ހz-38zBF@n N:ԪVB $0cǀ:ӝ>{O#?yl~ ,|^U ̦m/#l{ C2 =E?z0Gj#9KR+/zG^ S`d[*Oy1P39R?CXӽ34{+ Ip@j&@w )PHЂ/B6SAtlĂ8HBX4YSў 4¨!*,<(1B!A?8$I鷶 "(B*!Ä GHB4D$Fp]ȁD1PBg$w40@F*xp$ĠlȈ|.ȁII}+X@px +y4p4@.7ˁ(KK4`H+8@KLlc 2p dL…RLX ˱LK4@&SZ,)P[I~qFKrPSdu)u`Z;"H$΁(V l,؃Pؔ=XXQ6jkٜԝxqVl%ؒ΃m+Rl}E 5=&?(*8TXځYl{̡ڂ8YEZ%ۂ P+8Ց$PQ$  `B)5O 肩 )I#܃H@/'\]>{ 5%], "8s M@-ށh^ݝ\1pZY j׍]s]]'^$Z 0l-*ޗ'8ܩQ\u\>+ɕ&ZuW -8?`-`EY \FlV!E@\ BxO'H4@8 B@Y M DVtt_IV{Y/vJ-.цZRߡѢs]9 4_&6^(^(F].8.QRCʜcba1&c:,`S,5"zcN?~`e@-<6dleRNeE  )K`RfiQZ"PqP0Ve߄;fsfhfZۭg%/:vBj5`J9 `jnZi6iX_Hjo^ERՂ @ڢj$[꒮+P6$h@H$kFkaق0*`   Fni[~kD®-.vkHZRhf f̮g8躭cK3jhnҞU~l `'>S>N%^m&m>jllnnRP'L~1AJ5(o0Dp(Rxvo'}opp4 7(ppq8Hx2*qopِqp?#$wggq:vr`x؁rH 0Ms(74s"O 2G+Js;1v=/;W'w B7C$<# G֠ PD,VC@N^ŧwW{zi8;S h7>R%9]o'۹Z8<ھwwgiZvIDVaB9@#Ȁ1cGԏx pymОj@EXTIoVbg*_)oq+gg~Iwe~Y'7WAwߎ ` „ 2l!Ĉ'Rh"ƌ7r#Ȑ"G,i$ʔ*WlBaҬi&Μ:w'РBe(s`ѤJ2m)ԨRR}z`լZr+ذb^0,ڴjײmͲpҭk.޼zy/۷0Ċ3nǒ'SlȘ7s3hC.m4ɣSn5챫cӮm6ns7p/n3osҧSn:ڷs;Ǔ/o<o=ӯo>ǏVA7 w * : I8!Zx!!YWatX*ۨb^'"-]18#5!N "/R2I"v@ Hg &M:CNZ>Z9eݖu)6X3.Hi .oG-r'Ǧj 2(R_*#^i;Aj7+dǨ֤BwR6&uiiZ%bz%wjwhuĨx"z K+i_5^B{\)j{ h^I.t'.v ʘ ֱ[vgowrt諚Wҗ{R\|j>ge:)<+,3ϐy]g 4*`52":MbV.uA7ItL2HW7мM4یt@ &Γ:v 5ؼ8 ?<<,<,2';*]77nȒ`J.N;fb9#,@.vuԽsv,J 5 "xa#^3 ǂ^5Դ$;L 5zx2ó';W n~N{ܜ:#+C*c?qP- X7f 1Fށ@jj$!6$OaCW9 X.T 1:gC4ZQ^ @jpO X"LE()ĞghE+p FjE+( cǑd(iILa} e\}EԂӰE 13.\8cdQEuHGMmNG^i`)*Q]F;D8*IWOcNHfAU4I `lg6I'RPL6qRghE3pl(25LJ% ϩVs@?w*T#kcba&C̩ DPmJ}E5A1>\qUѻVUFaxARN'yD~4@ZX-)Sa,J 0A}R{CsvV'Uk*ϽiT&΢fc7@hQbloS!V:=jujv:.E-ѩ5׹R,mu()钔K.ӍIl{[QNPs _(s|%V=Qi C3ܤXwG`O51LdȂbp_0`Vaxdg_nRq-:" ` cS:vb͊:0ru8;At(/Ŕx gETjDm>468tmWzY8u HmOkCS'RnmejX]P3@(D53x=էX X/D/B`BA/ELֳؕ6s^4(Vw=Wۧj-m(^ ePT+7&[gtu<-]ֱQŮ" * \OPnr{%!ƜsčK蠫ݑJ o7!k}{H1-C 8p,}@bWATx1quFIim|W8L+|v tD}ֿ>n=G_z pfh]73=fFOҿ24UDtтA1x~t"q 7!"E&B5<|B+/S% .O| Y[!{P+- xOvux 7 ʭ^j-`Pi3DTb}YwBb$bɖ֪rm"lnj auh=w 4 aiW b Cx™9la|Nm9VZn{Ԃ5) 5A{-\5P .p{B 8/^%8)QŇ,Ң-6 ."/B/A-X R*+G H@,fLF`!tNG<ҋvH*yO5T:gC@N?b>R=^v4CJDJ$wX!Fi$%B"$y"$L$` 1${X|Ԃ1+'ؤՍ"KK${d}JLJ RB3V2x.+hGT#>n%devVJ壐WvGYrz$O %PN[G\R%E#p%ߠ_%`v%a&%b GS:%sc>&dF&eV$&fb2&bIg~&hwX&id&j"^hkdlfR&jBm&of`Bo'q'[gBq.'s6gU 'B(sN'uVgP)ADHuvw~gK/3p<wz'{r\=?@Dz}'~UD<=gCg~(g_\/>Ch>(6gd>4D(f(s50B(a3<腖RFz((X_xh<(hn\B}.)\*giV{s\~mD8(zt)dE@!,,(*668CQS[GIRNOXACMjkqﯰyz~qsx?AJ㣣fgntu{|}IKTEGP=?I8:D̹abiҩopvKLUڿstz46A02=ήvw}bckCENԊĤz{YZb+.8bdj\^eMNW69:]35@DFOZ[b_`gNQE57BhPQZWY`efm[\cQTFŏom:=smqR˭oǧ~)'|uyWލ} ^-w Gցa!LGZ  x߁Qbv8_3F"v@(_{I\xG"_~?E1~݃'Z{>IpQ.E%v-y%1fWzO$CE8S 9}e*>l(c{>)&@n ө'c$kxQiT>]CvV]iV`鉼t:(7%F!r6k_jc:0謔JWb-g氊 " kꨕ"*fc kn)Rozo}+j0ce$ BCaފ-e++-2[$s[˺n- 1:yl  K$Ìj2<7m̨oZ^)-k0#KjvlL"'/ji8<1ɺ,pwruv5CdtwMSGxR} z3>3 O[d1\|/*_҅9# Μf$>d7USS{p{%/9(}96jջJ/u;` b+'6p 'gq~ܺV }#ϐU)Mt#nF%a M<rlZ.ŎmLD g!uFT!F) v5|W,pk6{up@!ZBig{]ES%/7pC`WHjTH8DsG/5 Y1Cx8HUQqc)CBLM3^ }T5 K! u|d@ʩL 2(>oW2Ͷ4Lr ɘ bbkCNB 6g$M!+wIlWT{e5!io9c[0b`%OfFgy}ԧJƿ%"xz&.jvJ@24p!>h@ @xh(Q ʎF;z/3A 3@ @6 P`7` 4`(P`x]lbI< ?4@ $i P$/TPF%o" @5A FA  0> @4`1wA PwtCy{E)Ёw i~J. t& w7Y'WHON Ո),=p%m4 "?`4/FZ * 3Hi/`3"F"sϼ 8 f0 Uǁ0@P R`uҼ0 a@$pt,%SOOϿC0 Ix azaa0 k }  "8kPs P  ,k q p aQ cC`oG<0D8D.fGF86CH TX0S Qlwg@{A]ȇGN >H0 C@ q`X;@&y q `X 00 X؊1 X  8P  `h6H 1 በ("|0 =p V8} W(`-@   p$ p!p @ 2 Hx ؐ@ !{i)b0r(C1  H+!q  `8 (R@` U Ҙ\9 c Ѕ^ Mp( h'6` `(A=0&Q@ph1q9y %mw0)0 R  0 p8 Qe+~ Q1c0WP ʐ qaCa  pة 0p00s0ti詟 : 0cpqIz )ݹ: YYz0Ђa`a(:JV7Jٹ j_c  @ EY*!a?P=u !1'0dN@gY<@z) s e@)ٙIj) xa0>')<ْ0 pTp0 @ p  ZQ"~ԙp1 0` p` y  1H~𞜈p 02ڭJ抮N 0 M)ʮZ* ۰ !a y 7  1  p '+{=,;3 Xc P a8_ư aG Z: [ I)9{˶T;گ4G q/ @N'8{yt!H8P:9٨{w@.Q`!j䆻zzON0ZЛA*Y8W 1 p p #+0 %ۤH+Z0 K[+eYƠ 拾ܕ ! Xy` Q  q lm̳|Q \+ wk"\.\Kk;00II5f9#d@M0J@@d  0KiY 1iYUPmq)&0n"1!9kyq}ǁ,k@`8*@unu2CQMuL*AHN `cPĸ0 < `)0 px,N0;2LE 7Ȭ̒ x:\jXp` ,ǜ0 0  1+ /Ϡp DpLc ͷG2PQoEyzoL@.2`0`l WXN?Pl<*sڑGF.ɶ{(r&MP Tڻ\4q05* bq m\(0Op a]݄.$ܚ 1Biă^=⩱蕮 :Q 0 0yeda~<`@` p-2Პ n ?lmHnnq2@W }`z 1x ,p-@LPE f M(^ŀ E0I|X籍S ր q oޛP w^@ Лs,`N^ 1W 1 Њ랍.Q* W %@/Nl;F x]a y% X_/I3koϳln 0ÿ z<7M#VA? y 9 Ioe/;<W'@@n 2[i&l?x``_U,7e: p< 娇Nv9?>0 \(x07W\ISCiE<ȋA}K(YG IrP$M$y ,@ }:gA+Xl4 @`p)Rw`Q1O!3k䨓Ol PFJٴM¼<2cСET6`UAC ]|,9@jeH`GV-hXHwi۝q2$I @\1dU.шcBr,H 't`#i@JaЁ'@`A$d PN`b:1($0-h⠔VjYr:wG2H!$H#Dң`$ #.I$:@" WaE+<(.;K!̎Rɠ~Wk%~#ȲMɺ{{k{)[m?_Ÿo7pS/'ms5tFt 7v3j!'\cy>v]rwםs3:ugaW={ו{>aH™qj\f羱IU0bC'X<#[7n}389Vr!PGplD=-pt^t:s O?50G@'pM!'&.zҋ[ gA2r"Q1χCdȷN}ox<#.+ H@8vQo]y@mWl HGMV$ ȷA&esYGnue1kicOŒeC70mz2%;.r'k(Vϑs-[9O^1+, Oc -)r{|&$BD:Zd3d{T#5iLgFDmSPAn:BڤgNJlcHO@RsS!`SO,%L TLOFh,/KhOjq!%YU`btU f:Iԯz%XzQ5T\(@%LwhP6T'~UQ%B)˼4%.hMf3tshaRVU'5zؿJT*YipX3mm naZX=$mP0f Tӵ Z)NzEGɈ׷]n܌7[jiϛ5-"y918 CO+oM]TouV+kQX3N'*HcRu)oSksLIKo:N11[ gvF]G:;gy5&r}r iUGps2`uG, AtedIV7 XLft܁ pftHt&z"pxpDc?{?p 'B?'iA;0CGd, }E+-H (lX x NF, 0"n$ -FsP&% @P@B%ʴH p܎30? pk]Kc$[UuI`dh 0i t>"T]4.x7'x@] ;?0 (Ђ8 %}#`ldS]TenKJnUf] HG{x"< fHlXTn')N~pemejkEx]/`\-(̓(b ,I % UJWx mfJڶmtuv`Ȅ`++m{\}`RMSSWȄ 6o?h(郘g> oqVWb@ۃxni#pf܃#@cHՃ~Ey0G("<-@q#`)ȏt>p]k8T$_rMƒh'8#0584Ӧv -rTl!_ 鵒lpKlqxx'P1K=f00qv@h =o9O(gn'uePARQ00g(]<Ύt(u?qsAo$f> oFb*&tCfa  ],nlu_uN.3CjhܓAPԓs"oma88< (&@!wd)oy& (BӌFs`x;0_0Vdak_lp*6sjH*'0'p/s1@Fh@P1&Hڃ8%Qt2/)yz HyTPHXM?E ,8 ]pmz ڪq6{M{A{g\H@/Ѹ灹`zMC@uA4|ķ|i:8> }%ЀYdHw؂ h(@؏ 3T` 8;}،_~ XH/~U)pyXyG'Y@oK%J,!Ĉ+I 7rDfM> V`d+8Ҭ9p 5ذ, ˄p 2UB Eh4 @`X)D!mxey@ieXBXrl*2ڶou 7? 0ˀǏ!=7DZ!Kn%̮LpeD> 3 -Ξ+R_>}.(ahB",HH '@`a#iJ `  $@~UQ< L'@! F&!M DEiE w")"-"18#5*L0B"D<$Q-*z8G ;j*Q1 Dy0A+jŠfs #'XD+<)=HpB)c4  $<"#! seAH*Q**n ;(aںA) LMhF[*>;-QlF@-3P܊;.{.骻np#IDs.iDN1Ů\<̨0 ;0K<1[ߦxx;鼻>m:ې6[~} Qu=t7#_.~t>3.?7N<qfA<mtr+'Attk6P"x O Wxya Zr۠Bbx q#G)N["fhq^4WN?)0F xzk"$c[Px6ֹ&O# 3@ŕHu^F*?w7Fqv5D*V1#EJQ}_<¸>Oah㙐dٽ1|,_GPJwLh˥Љ$~Ѝ|!hJe)(?^:9cR'YixyH[Q]d iB&d%1;Y\24B)G61&/ KN'FQF3?ws0 ;cYsz=IMj%'Ju攋7.8]'QYχ(0wѪdZS%mjQX4LҕZ<)Q!͕e_OSE+>MNnՍ-^yYY|kte9pZת>s&Ա"j*`ӴhA:9+e<%y[WM-&m\iukaVj eݚCDN ]gSְl,lOTBW}owҗU#+ƟkP5ۑ4LqKeZ׎.p ה Qe(YjGl0Zԏ Kb 7}q;TXUS6UV}1qSSo܍Knx2-VSyQ2^CI {ֱ۱D wB9fnՕ6W=33Ηps=Y 3AІ>4E3ю~4#-iyҖ43A]؈( UZ865Sm15p K`wdVj喙׾LWC8h %8 ʮ] ED @$T !3@r$dI@(1 !x`Z.bwolߐn .x Rg @(7p|=qپ|LHC p3z4~o Z a $X.Dʆ P\yjAG@ ` o#B/"pz @P=A= D O=D a  R U3_3 3't g/ӿ??  &. 6> FN V Ш݀:4Ȉ $AqhO" .|D@ ( Gd.d ." (B)l+$!\- Р fp Ч}z{'r(W|{'I\| "`B(O-h&đheD(Bz犊(J)Z(ĉ}``.q)& @qDmDl@4plڄ @DDD\c5j@flJĤDmdM@Z@  A%x)yqK8:B>`@88܃7L:t6A$C" 8<:JxS A 'M@|" #|Bj!B$@D3T* ".#B*Bl.l&D"W0B#@F0 D)Rk뽦~"2lW+GNB1B(2$Ҭ]E bB$1T@-VmR'4@D#LB,B)0+(@R-κE؎-Dlɞ@+lABHB+k%cFFkξ@-zlhī(d@Bi nDPmjlL d2H#&Īʦf&m$D:""@ tAd{+Hk"k,6`5$dvkBB"t9N DkR@AI$ض1,LoB0DTi,L$|F84gtBeStO 3&P2h@1 qU<bD@A DhAl\@ 0B 2m:3'' \ӵ]2ntL;. (@0P6X6-+ '?/8h2A4%ux\@@33LD& 3\p_*o*@+ ksNdp' qD%lL wp ?s7Ps.AK؂&@87F(w,B&4`)ķ{\H1H}~xSHWu@v4_ @evZ$x~t/80oī(< HD @!Ai'  Hy\r^&d!Df7) 4 {HlSlgę,ov/$%b+vi_ikjG1R#<v4Dl47n^KJO,hBB<; T#}jz0^WACD)@(a|d:Gh8e/Zzִ3 lDK.2xNZ)gS8jp#eƱV;\{ TN@X (\hDL:vuMX $@āAX@TG6A}89FD([+o$D@ yP|A4<5`@\//-vDh5t 8hOB-n4/:*F$h=+(Kz;ĚJ“W$d@Bv@$}kϣ: |`Wh']0DF=~}=kqW.-GD}ֻ;{B ,|.G>DL@> D <,h@z@'s@A#DB" H(Xf \D@ <@ @DovHA D|ko<@Bg.@Л+k]ɡ4l$E-B%JcI'+I 'aDfMYhe+`l4 @`ZhRW8Ŕ:u aĊ=%BSXH~PH @e%<jfѪ'+NИif藧sG#z I^H##$R3ybCI LRjX`!t л3 !B!yAC(Ɉ=`&Ji^% R!,#LRI| &N *I!|ȣKYTrUX$0L2ÉPH2τ)c =$3 !5a%υϒ!,ClsͩhƤE{4N#+H !9 F4HhH+) *v˓nUU]y_i_ cMX\TB5PUUV%8)&a%gd MWumwY8Ĥ9_s$'X.NaQb X)1X9)AxI.QNYYnaYiqYy矁Z衉.裑NZ饙n̎D)LH v7#&ۣIR:kak[Ho[m­vWuq!#$Nrs +t\3Z{ta?hrWvG]ԧޜp{O =p=&k\mC5[x7}]wzc/WH~w}?>k';qyϱ-}ʳA}s jW8sE;n|fW- LxNy x 8s)^BөP{ڛ`B&?PW"QK .@-|s6y}ۡ} ϫB(~>lbm;Qh͎D` WJ7b 8m3 1xG ұ[4屺AR̸JQ\ }WoO~Qode`H>a0,39_qy 3$?R%A6"m!I8EbLV*)KEim,%b3_ MC3g5K4V,c8!:Boʐ%3ɈD*T)a/wf~HwLd4DNҚG Z$%GK·:u$5O&;aTҦ5j  օ0gU G>zՋ]{%K/{]jվU)Q/LZ10RN/F_UJs]R5+~|dUk+Y˷mwUJeuPU+,M{ظ* 4f`[fvNPmQ*BI nd`&1RV|l.gOQvk*:'w +nKGxU-lJMpվѥfǯΤ\'DC1McI3lAv%NkYjԚ*( `>85~TƉeqw,b3ՕHqWx-dw߷t gg11)6^ydFe4˺򚩗`԰p|g=yhtߕg@Ѕ6hE/эv!iIOҕ1iMoӝ .:Ch%a>Ⱥ$F( miOx"l]A* 9A<2lZjϛ^#^P t\.A iP @!>7=6VqogLHñ=p# KLnqXp@ qA '9^p4`0QIxq-H`\pUz~-\$7 8!^x#j%>wK-8Ȱx}`?I "p(Ā0 ̝w #jE7&Dr04p$ wa " Hm|K¡ hBF}%x}o~7џ~wϟT:a~?`<@O` $ + Pp!R< ێD H  !UbAdWJ$H z@D+<`*N@40& B`B &&H &`DI\z^a'mX,vpW,@z@LPD`@<`B  *P b = Qd% -f!,J@aPh #<AAw}28 8如N@q!2D `@d q!d״ 1A+Ea$M0Xbn TPc/4X@` .pn \@B00(&a bfHa((J|GF 6@K SD~ @. O h=B. `::$>@n!0`do!^>SJ02N<"HS>>\csC ԁs|3Jx<83 d@`"b  <L TDVZ / @ T ք`!s!,`2Ht= &| F r`@j@̔=o@, <t!A p! "tBmONM"C9QM!hDAL4(Q8`#Ef #؀+X $$F `9N b` csBhu$ "|.JZF` OW`b:PK#Q I{(904SOJbGu!,o$`86 FJ/&zX@Ar!*A#6Ju $ NBf>o YPSY%e@BZu!xp^ &m|!h8SF!v ! b z@='@ LT b*t*X jEa[PB"o*O^P DptSnl`g@`!@@6䘖@6yk A loA7"&"."#Ζ#A$H6`D@, ??RVA.CumRBrC:CB$Y}MCBwELbjf@BG!gq@Yԣ d@aU eJ_NJLKzHMThWX%V`z$ @uK‰ H >kDTl mXd$%\*\Y؋X` fSFaH% jO"dBBx 9y!96LDjy `o,AĬ}ſ Ʋ+ ,:4gBy]ƮFh̨ „sliW'n(œ+)LqyTL̤4(| d)y*{ކĦYlDJP΋h. yŖ \ L눽ٍ4yQ&˘렯L{Y ٗN1h:kJȢab48)ڭ HzYSWZ˨ bz LGɧCꝫHRZ q#_K'Y߫̉ nrj:]:>VmV :mIkI";q1Y| %zk'F۬2eZ֧ GQ; xLELcYy{ct;h{;{[@MԖF _X[ _-̣fxH@PX]e.;$[Cſ! č QR%Y&d``| Tw$Kr 1#$"-r;å0 "E< IRD&l`2!`=N&|%[r 2[ħ1;F9a4AMsSB~d#܀ӧ`$z# #:^ r.-d@QU=h%$z$t:~$0~!s(V(-J"yNKN1$-^ ԔFm4j)sOOu~=BM>MhK"hU**jxDM8"#.@o^C-3F="1 1Pص߾ Pˏ =~!pޑuN8/N$`FJTS5]?n[U>A$ x{~!%% vx@ |o2Ls6ߌs4jE4_T@V#$ K8AB-VdI0yY֕T@5LF#]^=Ӭb=LC :w~ xstqØxpWONy_9I 8pyNz馟zꪯz뮿{N{ߎ{{|O||/|?}[H:!Zlܿ}ӟ~SB",Zw}UnE:A#g/v D=?/qo WN0 aBHxB*D!D\BƐ, e*0|44_ q(B0 _X4b( JqTT!({JL GE&&q,4QD'^LW.¤d,a(F;c84p?!H  0H"э$+hI9bҏJN9ST"(5r$19TrQ;e% ` 3.hD쐃>DR|%%q#0jl#!(MX,v.wLOz#6/9Cs q\"*Xsg8@ tf8M=Zs# yB32%Op4=? a"SeBIJO4(*Ym,RF@&;st6DzQ5ehdkUr1b6vũ=ԪzB ڿMilU"v%Czۊ3;-7qz\ޮvmnZ{.]jwUnt =rw-yϋwm{ w}w 0x妩hیD`g?2xA2@i I@ U `@8@x 01Ţ'dHSB̖~P ?,'ްT+oKZ捇!b@wZ@AU 2&FJ,<3B &~X6tHH5X %@i<I'`P [z  @p>; 8>P" t "òF(h `-.5zR( 5D6-x@A`z t}n tӡV g|P8NH b@X~@&ymAL!Ѓ0 `H'*h F H  Px!!M#!$up``d˲8,$79P#"ExZnN !![RqE_t|5OKz& !-#Ha h^!p;+O f>R|h  qA8 FE},O(Yk |P; C68@8D<)PtyEqQ@j@HP#l28 Y"Qz)  b0( G|Ɨ&q[&`G}Wip|]vp{4jw5fL>@Cf#{:1 Qgxp_14wHyMLwE$-vHDp,z.Xc `w Ws3pR( a ` xqn@NPk,ma+/Po!{Ȉ.Qx\`  D`3pE`%x~x|($=.1-0!(rh%5^с9rGnVhS hpN@&Au2K%hpBHJ X`8Np'h%fxDp AzVQi 20k.vi0 tu@sX09M0J@cS)q@kp]oh7 gqI*@Ai4I dpV єpF0qw"  Ahh=0e.zH $*ɒ" Zv,p4YzJ0t2-:@ 8+]apEY3d9 7<-@%07q@p` UH <Б1SvP`P0* Pu(ki B1+PAv-8488WJ'1cy&xAa cg,92H08x :VbZZpvY٣?扞5ǝ),b%*f-^yI0Ƣ;? pɏ 9IdyQh0+Ṕ+<0#/O 3Ԉ`A{3Y 9AZtf\ @d aP5qY?p$\3ҪpBR,DfPF Р !w ښܺ0Q "B5Z RA::x  @!@{L*.8-Y. KJ9a`us/yp   `RpKw"yg0uGqmRک|8⟨7vQQMڦIʔ:l.*'apjbqV"@8BM0+ Q &!WR2);h]BAK K&uKMp˺)8¹k`Cpc3y7&;!2IGs۷(&j!{Dgk |89+3ЀI$o۠['8B[iof=".+ʖ%@Km4{[;/ 1[㪁̽*8 AƋ#{ ]%^&"(@(>&f1! b̊+K<X! t 2b+ | PaIF@F11j׬ 7@M0 A==0`l.[z~s-X*/; @hL5R̚ XRc  ɓA,ɔdZb߾ᢝX}ًMݍWDܼKda[~)UGa%Vx Z]n?9nVHPn:UEmWNSwB[S-.C눽<~T7nY}^뮝4Ewހj?U"SSALhރڱmat%~Y.yJ@0[N~EV살_*^ݐU~netw4nN VnlNo-҃CN? 5߻7?Ү|F=]0)Mc `Q?tbZeb~blvw=#z0`hAUaN}gNeF5oYR`TP>ƏifOb $XA8 4p 1`Aj<RcE4 P1 K8$XcJٓ'4}%ZQ<PԠ -*RR^US­<' 8]PG>26%.Q64Zxb` >-0 T A {z!Cfp @hҦ :ЂD] bn-ٳn? ljxD1! @ Al.slL,@cKc?G,hDЃa[迶ފ"̨n~ 6+8 #0)TjKo"bK.⁀4`#"H~ r "{*B~kJiHDD p (K O2Bȡ 8Njj+!@N:ijOA B  C@ hB/FFt(24 NAHҕWx Hʄ7]"V[q [Q/jI5!l2![Xv#0H^y0 'fڞ"a $ !7F-uS*%RRUW"TBrz0< "K@!e y%6$R~@&ܴ .pؖ)%jf>L JwvD :  .rAUa)X; -P:Jn.()PP$(0FlX ?"srCԂ^3ߜ ? /\n!;|rw |$U% x#O| : 9iza&ʚhl9|SB|AL@`N.@+"Hj"' 0 a `[ru92MxF੍my@dH Pg*IIcH,j\NǢ2h M+ :$Ve"vvÂ{c`# T/^1t+A AJԐ d% xtR lDhdL@` (*da)>WF$Y $;4'>`RG S>d |`ndp>@ViFEI!f(xJXr !cw6 4`(6 Q@0Ѓ78 Hn3p a098`9HKJrԣ )]@jd @^46ȎFE*F4@'ayS)jQG@RRb/LGֺ((OEJR@r]g` a:DAwFjzӜ i c,]ړF0 0 , MjDo NȂa+[ڲ L8 #x*M0Vfp"Ջb bxa{a p(,` X:Ban{.`)u*.#A@i Zb 12~@pW"*d%KY;RA@|+0 dE>mU4Ylյ*VZbyQБ`@d`@$9C=ElA~`umc#;Y$@ڨ N-~бI*jY h{ܛN405@4kU_Z (`$61nYi*%sa0i<'+dcX}Ll i/kXMn@ ΀ 2PAs2Ȼm< Gml S bx ~)І-mR 6Γb+f-A"ACG0 mDګIAx aM$y_ޣ4! { +nh>j" YaэUܞ˥~@Rnax]Ka{E4a J uU4$'*{wGx.q[<\<{-yF Z%!f=n͙`Ms[ d̴ 3A"=' 'țbëY8ѓ~v_R6O~;.Cs*&ÿ&)10Hk<cЀWK?>- #<#L z$;#2(7i0380>X@A*dm" 0}#;X 1Bف#B*'-T ~#4T*!,C2,(<' &ĥ#C@LCA܋F@CAD؀/9C DF%C 8*SQXV62CT`-ˡ+(Ps68XC_`BAi'xH;B p;-H P Qܷ@AgF*8pAJ XBf|HI|,HLDmT˘4@(ȁD!@IjH4O0/,00ڪ?AMD#EIC#ǁ ;]" CSRy@!3EID'/2 6EGԖQzY@QU$4V%P%xDVFxۘO,@4@\x !J5P]&T@K=%mLa=MQV"CȤRc-SMU5JPS{`˦?O#2\N 9,L9 Np xM8/ЊX'X0SB#؂ٞpY`Y%Ѕmφ2E ͆VDHX$0!-- :` K.!тi`XSŀL"0Y聭z pV!RjMkSh" q,  "0Ч5W/[V$۽m{U:52%|ہx"-g* ٟE܊E3CG`2[%Z%-%%ׂ %x%x<*z)ap'8۠X7¡[]+ցh]&^n= (-uPM5g&X ]L uXs^^!:2]- ׷-- XP1*0   0u-A!.,#r8p'x,H(=afAPuI](&`n+.bf4h$,6ake&*.:Q(8FcQҕ3ũ%Ѐ=x= eȗ.` @!b eraM%6xN'8݂_a>@Y K +F-~^Q!fɢ0&(忠#Q>X b=]iff&!Hj:=!Zb[Ge!eS`hYdHUne`,b5rgQ>1ZӖ6G &Qg/` HF^ggcF[\:cR_> 7>Hevdnͪ(h&c-ZvKƁ"#K-}b {ij 8`El`_r5"؂aY  Xv{n$ CHw c<$"H@,NmU"]&!!mm#nFnWق(F8.9(ʰnnlEx0 DZ+/< 0J4!hξ N- 8 + x&W` # [^")q[F`G3b@ll.'\Qhp!w"?rs\8oѣ  0b&'gKn= !]vm~Ggp/<';Bp /=#mnQrhHhHJ__X@(>'оs6pWnsZnXq hq-/g/2ǹtu6(}v(-ok'I^k0'h|rsG*HbhP.X' ywy0@^m2g7'9- zxxX牋o{~xX6 } `fTPhi&z'/CptH7?x!yw `U^x#*z/xyGy#q(xzP' `{<9P' _O{{ ,.08hw_w 8&xK|&'`z||ߊ'|'|:I}Տxȟ|ٿ}H}:}Glz  x(8 p`4~뿓o~/?/?O_o p "Lp!ÆB(q"Ŋ/b̨q#ǎ? )r$ɒ&OLr%˖._Œ)s&͚6o̩s'Ϟ> *t(ѢF"Mt)ӦNB*u*ժQZHCǝ0IBK uȊGFth)8@1X7q#;Ysː7BAslhա]szҵSͨ~-{fۭq~\yqeg^0漥7טuxӷF_{UPĀ % DA[CjH D?A0n%y$lzieS^%l6ֈ{?cEEgU@ 451@H@T%2H(i \-4 PL!P(4 ")])Nla9g;gj%krI\_Ɔ$y6klgȺZ)yvˣ6ιmiw:抁 )oIJ;#˜B\D@4D6-`@> TDW!0Xа@(Rh6@'laT @ pò rLӡ堙uMa ^nnTio-VMw~'7֛ghck͎&n3.].6'e^ kp_} Pkv0`% 0 A5B5tHȄ(`rJ,^xPAeD;1*@@hG4 JTV {2Sַ4en"`ߒ%{IKl]5Ipu ۚ6/-Kc[6Aj[ 579ҁk`2H55fIӂ֖2G8pѐI5LVk(D't ;[wBq;\g:4@Ie Ы!Ȁ aBvP 2B2 @ J/ @ Txj < €2+خWpoeŹ}M[ ):p\ frL6:Z m&E$ilǥ: 85hzol:h_6Ӆ\C,rsߤ&jIX*&/ 2 86 p A̠vlYA Ha3dbp{ Ot@A% ZEVi@RE h!0A|wB*Ҋ [j"Sr;?cO6_dT#fTد(*8ZiM$dC0 *00!;)^5&N``(ab L$  0tX+<qPp4Q@J t8A b0( /y@k Z` Ϊ]>IWa0:y-dzU Nile +$R؀ c̻Ām%|CezӋNkp'yX ~B)ͮ} @B( 7@ɤ`h3h0$X܁`dAf(arNÕp;㹔\c(^N}ܠ-ؾ+_6~bO_ˊ}N"W ;5S|+W-X vk;7|hЛ4AQVvj4K+6&BED` d8Ίd&DB20'-H@`@7z)} lA J<QvaF2;:?6u@8`4P8@D dA hcTAeDfuxLhNRij"DoALAfA0'AL;hD L舖@pA@ (((D}g~ |H @Y@`^i*0@̨ALlBD'CЂ76Ђ-@`?PC8uj,a;x4-j-F2$%h/8,|FdGā=incƪleXUЂ: Zjh?_F#0`Re A2F@s«#@ H-QR-Xm+ox@&@-u B @%4Jew`L+nN@4@R@s D H H<1"pꢰ@Avp3kA|°B(j"[V%/3bv8hCxH/^*5(axb64@kB TLh}PM9A VDiD2Q%,ǣDcD+ɣ|uJi&HpA|#Fh|u&A ɧ}kD}= GT(%};C}KS}[c}ks}{׃}؋ؓ}ٛND~Mg } "ʭ<Ɓ{ }&bAߡMܿ lj`6١-^|~"K\RG>c` z{~ R yZƉaNa`M>1[ 5>ݗ{\VNew@% ǘC%U D(A 6tbD)VxcF9vdH#I4yeJ+3*h!‚2g2tH#Η0!\hACi τ6{4jtMR.Zs)SM6 uk͠cN‹`[-Ksֵ{o^{V+ףY֭ZRMlXe`o zg\ʚ9ϓ=wulxNԅN{wnݻy;؄GM;-š; Zpו_580Te%^[+l}´%J'-;׷~[*o踄Fs*3 ,, O=c.,lP@`2(k?Dl3@]|eȿģ ĬC4cFκD.A-D4.,jP6/S <4\3'tt I\nNޓO!ԳO|G*:RՒ$݉BDL5ݔN#2%YڳʜRƺNLQ tT\uݕ^; SXpؑ$$)Y_}hYV^~%-%A!ȉ3tPG~dJdU^OeXyfk&( T " H&6 ŀ,Yg#Y뉦&lnk~hƺlN""`~0 DAQ5VB,,Pp yOGtYWuc_]WGw)B@s"@B~:`I CC69j`F C,(D"JbD*.1U$.[t" JȠ~A( pFБ e  i`" 8pU$Yb"x4Z=pBC̈ƢIT#8>ď$i R$ #ݘHjL#+f/ ~h, 3t!H~LꐇdQޠh@k#P5 \ȁ>dU H s$:@Nsӝg;)ts'=I|"T@IP`4X##D$h ]:4"A P`"{H!"g|;ѯp}x[}{X?"X9nȿ!hC "o!I&ཌj""o,b m,O6pD"mVf!(LkO9 <  sn%x :(˲0 B ˗ld@ ^@  I & xO BZ`~Ȁ H`  V I@ q q0q0ǎ"" I` 6q P"Pt !D!J $0BI " b"*1} Q4='"(9?!1""bJ!"w-1" "nG( +(_1#ql n "1t /k 'r.@ l ! D6 <& PR%Y%SV2&_&g&%q&i'srz"v'2'"! F@ ).2"RFgC@- @&!'2)! @"L/!",qR--".uR&}0"3"2wirf: F!@ mqR+r!L!l!z# 74@4 5U5b6#7KS(B7!2 ):"B DFp@z @"b dB g=ճs=3=>s>ͳ>>"9'B>sx3@!re@D@S TA@B`=Bt @ nԇDI:?= ?3! /Өh!n#4A90E D EUtE]E_4!7A#BGk@{`$$9?q(J{0 'B/4Cm,(IS9Ք @ Ҽɐs9OMߔ\S"PQ!h@.7!5"$!(5R!R!R3$6j0!<#Dq"HU"P!`USBT@Tg>\.jT?"p$| .%BUYu/PYZUZZZ[U[[[\U\Ǖ\\]U]ו]]^olp0"_obo0jچofވphlG"i&a&k__}#;$UjGT0CTLEKR$>J`id]^e<0r]^fugvw4v""&h}BhR@_ؒ'{b{{8#!H{G|!v@Q0L4>#m%GƂ*JCQP$ieEQ6o9B@T/l5$g'2 hq h#JhMdEp:pv`  hI{hJ@bÏ"*4+~ev%%DH:f-~E dIt80WLdL Z$A"78nx/VzP7I Xi{{ "i0*w⟜Jp d6LʡtB4&@'1m7Pc%oGWLcd9Pz SxIzGcI Ҫl׊ \0Q@Lv!#B j+!d t*xX FQKk:L{Kd  u8"k-enX2vN^RHc*@DEFq8*H=vCʸ  6kf+dFQmD5#w˶p]xbh٦-,7B!̷V$.5B6YJRm0 L T9 *Ȕnsld$QO1FxEJYOdAE}Y L` ݬJ'ؗX($JHّ!X` 0*ky7ynnRB|NN@ybgcIIzVoY+e8Wv5n7k4Y >o\:VX@ڶe%y=df9a`p@!!ϒdB V!z0KIIP'`7=TX:-#-Xn9zbu4踭mUL9s& b OAcmuDw;Kq QB"0P"C#aBBoDb1Q&bl٬嘮uģGyCxxz=TDw-2#:6.עey(b%'m- .u& /is-*0@,,R7sO,Ϳ3!绾wJƄJr}q-XY:+xRj)X<;a6 b+ "+0*SY崹D7ǹl9;1<&t@#"<;HUov4-FEMG#C?4DIZ̩/HM4dqz/ZPxڭMzl[ЋxbB,L-CBp]6R;E3 Y\?3Y9RwVmvcX /r~Cb>D`$:7B56<}!x167) &`^xwAZV.eN#/,BU#@L0/A^ fA;4a! BAh! C0xI'3'j =V@\** dj Ϊ |U;əi . X@8#,ߣ|J_&5/`B! ! Pz0 $l0bB+!@p %>蔘D 9# x(((*I%O,8 >@!a$.\őv`B $ N-+LJdӪۂnN`nݵx˷߿ LÈ+^̸O\wI m\NYힸ `=pm` wU Œ;E8(ˍP ټ}HHQ$(CCo 6zQ-C\uE)L ǟoնnD7]uEȞ{a5WmQ|݂#&4`B^0Q }($ ~ M!p4XVHw+Xs.h/.<^W!&@YqyKOش;!.01(jȆn,' Pn6rQ !(1BQmI Zthhda] 9RhȅppM_T(R8crDžTXTBcYgWXhw.hU!AhKT"'" !a#b o ("aJBƁ[pH!,cnEx$M3$Q2%%@؏@w/!&$b"((Z~ŠUpHTɑx)-(Lx*it0Sb/Z@9r1-=B.#rE +0y/n00%'*#-+`i9m!<6Õ.s0Քv55midT5tٔneBEsk1,M.YsLyC=cAq::]=<U>Ŕw99O9#@s?SJOYo1@i@9=H őpw)EFCxM#x wrTؘ깞gm1^B)`$FTytHPIH]TRVFpHMd/E)2@<7tJDJO`P&2cY9goK"ʟHۤ5dM5 UtI)i:iݙGRJ)~3b5T2EZ>e`b8oThERP*r-pVUR2N0PDSs'1h_%YMz64eV)?UTN*}٣"׋!ɤr5p;ϙ)8i=*S:N靟 E#a^^A_ua_[`` jHn^ 3 %[l)Le-pm ,Papa&ƈ(r[ɢBj-_"pdA dBŗ8sϊyAdxx (UjJX6kfjDk&(&hm1Slvr5mjb *ڶVn70M"p&Sam2𖱾v7Dz<ʝ4Yd] ՝Z_+ ۶n(I`BX5;rkR7sr7i1Vuv !v=Ȃ[uattzvPjK~q۩j)-o{k1xqx! Apy{y-ȺS[r넷86ۼ;~zz dA8 {{;밂껾 0;[ۿ<\| g~F.Pa|CzTk> QU)6@6|4{iA F 2 N N1;@' @0q G"x$PJkApǽwBx`2B0ЁA:!0 0^qZ9'U|*]`<ȅ fmЬz!J(Ǧ|ʉYt! jla$V=AصAY<@Pe0maALY7i}rp~Ėr5 dhȏRK̗`PM9aw P82pQkwA΄\PLL|'2dt 6h/*-^ _0iEUP2n" LR-ZL>\"xAuaLP\@3ngq=,,:׀fӀSiU1@gkV~`01ZpZsSma raҐ-ٔm֒{o۲(b5]&ImFFTf)rA&Z,PDZ .]ΑM#D'10 }U@m!vN@ q'7:]߈ςjqӋ !{D ܹ_Jq,Em=G'3i0[ ȆsiE2\]0j}!]68J1‴<†e8{a^&PjjAQ=1L !b1p^NuCpt U4x1`->']Z8Ez~k:?wOcGfb@t&i-Yr!\R]^bbqo) [\ pLp b0Ra~k}rryq)ZH@LwP`gT4~kwx!0ͪ1Q𝋠|1\v0b+S>[ "?$_&(*,%F0yq̘:rw@keU*Lr&#5)0@ia@/ rP*PX1<.%(/(DR!2P(p='`i pf|BH2Nr2հ5!>LP ,Ьih,y!̸}ǪoIndPOL}Vr+- $B ?V^" y+X脐*3@(|ǟ_~p,(SB=2HcD(^C#7 ` ``G ‡V#PP # &-ib %Bk 3#;!`*~@1,8a߄3N9礳N;ij>v3/3̋,#ȍPĪ' ,QAj( X `@9 h ?L9Li)Jz  34X-"YBF)R3+(bC qp*؉/&!Ow߅7^y祷^{s@/A'귥 5oFXTJ `& 6?6bX(8j4R6Ҡ^ ))7Tp` .bdiūRD# #9n^kz;{H+#ͳa('$ hH p'h7.)Rb7 Af@5gS AM00a;/'pO+xaU)u6("lww-*5$*Vۋɢx3 P!ZT(聤Xh ؃r񂍞‚DH 0A[P Rp  B-I 8xvHKtCЇ?O86lE$q-U { ш$ 7pZbx'xn~NB@/<@pFpbdKO8E<'`BZ@LØĤQI:Cd j-Lz X E@I ҕe,e$%% dk-a@@.t%a TH#h`F(% ٖf9NrӜD'^vÈ"7Ȁ!(_eX@9PԠ)kt;ܪ0 ͋5vŋRԤ'EiJU ԥ- Je:SԦ7iNuSԧ?jP:TըGEjRT6թOjT/T/J՝Ĉ7ɉW%U` k4ֈJխo4zչƪ|9VSqb]NZU}Y :ձ׶xY,6GVvvJVѪٔ;`7~6mlקXӫl `3 Zcskm+Ѧַz#lb/Zn. L-kVVוmvݶ-maݳն,j;ؾ7du>j:׽J/v ,]Ԟ1-w;-le;0`C`8ǵpo[Kr} ZװU0g_ذ-}kZVּr^'kb&7(F.|?N9 .oU;eh-ycjNDkӳYvMBC1.vknwT3< sy/u]r. ފ;ЂI5=ozc1u|hkRg4^7e xkpPl%qwyE>r'GyUr/ye!6Πjt770w" R<{,m:ԐnZ^ `@r=zvI=/*-Pajo ӡv/T ~Nt n^<" H*˥qUTeL$ o VR^ w>Hc@dZ [Ea@H!wb!A||1N~@eLA A ӿ{g`oB >C>G  >{!{x (P>sC>?Hh  4 @8 |"$ H-h #Fh (DHDY8$9t[C:E@T d B8XB((rJFOhF8ƿ所# ;TS ]QwpCEvt]<`$~$ HĀ @H܉FP lGyHȈF4HG[yv,IGXIHl >x8X@(:$h8-Xp0Gt ȖǝJ_t |çLG(sL@.H ʝXJq\ ʡ,ʣLʶ&P䡌\ˤK$!+K\/lFȔL| (H)Gk̋ JDJGļDKdK(MhJɜ͋lˋɝI^ɚؤL gB(BtJD˿TN,OOyE((" ٴOTD !Np͖ЖϤ ԉ A(Ʉ&p#@IHCII}M"((h)x}QURC8RQP% XR)]!Id %&'S1%ӝC/.eu$Qh#pl6eS8e)x(;Oـ6p.7p@QepQ +QYt3}R|(FѶhQZ @=Jş_ LԵ` P)>%U^}[U ،0֖?h)H q-׋+} @ 0&bJm dTM VOBV 8WPi^~Vd؊W4 ejUe U֋hGT0T @TEƅMWw׷3h(F` @ؖYkԁ!Y]{}f}h MY ٣FxjȀ.hXWrLmX͌n|YYRհW|׋ۥUX][Zځq|ۺ"vLQO7رH퀖H` `CE)܋Se]Z*u]E;RKPٽ\ -^?RlߥM^ =BQB̾? -E)Љx&ŀØZ%-muނݝ}Ee`$'hvU\] N\%"؜X7߫`l` N`^ɬαT u y݉&ҘxC D⋀ԉ,ab._aMT)@_e_* ! m 1qVq\ufb6f jp@%@x n)"?r7X?eC)le^[\~ Ptt{&V(be\Ӊhi] hZ6,&mιfEaGihKɔM' !08_;$fvLNƤ^ .6닰jjψh˸jOZn"hh f~쉞VȞj˦N&6^Pf kFkFifl&n@kC~h¦&퉌=Tԑޢi  e3# !'/Ō̚Gn &v X%PXމoxНopIp ov"g hp8>8POjW)tS7nn~0o onUa@(FnoHoi-#ҟ``<)@(0@-jg`N~xD*h6 љq8rn3Osɚ.i`at)Ao/OJ G Nwq$-'T>sr`U$To QtWȘi:wsYoD: N9XkuZgvEte g  YCn 0cn2 Z}7,R3 %P%xg_h/:^ Pw( {)> ݉2$ <BQEATQ bGO* qY b0 4 AEO$x v#=zoh*(G:0az`GD$&3Ij9r 0͓^, PIa`|%,c)YDt%-s)[BLB t)Lҗ<&2LXL&4)iRּ&6mr&8)q<':өu|'<)yҳ'>}'@*ЁgЁ(42t -(X PCGX#5JH=JQt&(R>5]@:R0$MoSU%?JRR)JzGRDe7?JBZ[WӗLɚSF/<GС l-)@^k[QƞRw]n/bתgYJW' qqSl^6pIJU[r;†,>1h[`ZXۖP&2S 77Zic1y2,]8^36hruiL) *Q@(R$)RQh?:A W034 2jm,cacyȨV\e̖g򸟜Z6yvs~ov;&ټ{>>a$PvMH)h'u3P&/m2nrky5˶wu \մֹ^C{ٴ3́ nЂEn9,Uk@/rc [_͛V[}9.}M']e7vvZlls7@OMt&(x@Z7E kHh@ jP"@z**ڮ7ց]dgWAFq]tghW6{]~Bƽo~ʀw_s^tN{}~/>?/}ۯlqqMY4<{%̈H[@|IL\ tvi_-nٖ! riVUؽ^٬ }%! :Wm^Tuݥ"߈\ɟU^a>)u!֠ \E]@5D@@PA@(H@lU _ͩXN1!L`ܻ%]bI߯_rabuYڵ)!bo9Wݙb_Zb-Π]U5"cVWr@TEIY8@E rx($m* yAc C]%J#-b˱iG㊡:N!'\a"% ؎dL^Df#zNݷi]m:Y-J:5,z*]aE]:6${ {M@ŻdN]=@>^ЌP(PO=ߝE$Q ``+!Ѡ[^1$ 9e~cIcoEzb]R]`z%%^Q PežE2ea͟N_EGff!͑fhrfmzuW]ٔg%U& 5cut g^'¤vTQtgOalUyf!"gMZw xЅukr#ѧ~|vtv%+) fS &'hB&']5ef2]fSg}Ӊ.y(m(ƨ(֨(樎(())&.)6>)FN)V^)~}Ndv~)Y)SgYh) PE.&$a橞)aʹ_`^))Ϲ%&>*b-B`NF*fآ&am*(hޞFrd{*r+"q*檚B)fk;瘮*&.h(>+F+8V^+fn+v~+k+ ++ƫ+֫+櫾++,,&.,6>,FN,B,*fl ,v~,ȆȎ,Ɇl,ʦʮ,˶^lj~J֬/,ګ-,,@в+1X7k+)|07:v׺knӞ^m-!|þ!,",''0'B)Æ-Pll+)B*6̢7+2@nn~"x½B1/C7|!+mǒ-nrnz..V9B.8:* :/2$hC<@&)ٺ'¿.|@-/֯+kk+o"v,:/^o>wV&# ,on>,;+7C; 7C;0 ?!dמúp-,..{mk."B.dC600!8C6,!4%Bׂ+qqNqG ;p9p Sm #C; q  +07!'F2'q%krv. 0z9q>0(= 2,``A)d)TNpl,-#) (C$L/s1L A$2,.v2l.C4i 0вo+7@C/4H  곺~@10|@7tCBn.E.F{B7B-C v(BnC?/0dC1kg0M۴FDWtLH"RCC9 # B;UWR{>SC57HV3U[5VV1WIOtk֢tK2.0,B1ЭI'0|@6HLu5E+r-|0(A&`^&́2, &Bnl(\A.*fo)('j6g{h!ij-6FdWe;&`133s,=3t 2h30?p/~xkkD3kntj4}k-$k_o--1.CCKm2.xxFn|_2k+{q/#+2@t7#oxx{O5x/xD[%.ZBM-!720_7'L7"væ#,nB_$,-0*s¶9=l&j9o/93 -&0+,[9;l3x9-2=[wbFt1",5)t59HX%wC+kDw."7\#oD^lqF4/D!lzL3t-Co1,{oHGt>q;뇏8+븿x{/q-75nK;;'/," b9AZA3 1C,,*<\|<7vdB<)l$ B()B*ȹCcp_d|.X&A o|?y<, C,+`vKRj7>=%70{Ag{Fzstړ-/9;C.k7t14W ZG4gS=$}[=3 k8}W~j~z8KGN5C7"/M'~=[{~)P7$Cl +V1U:@0p;aaB>8Hꃰ ^d(Ł>,E tcD Вe>|Kx3" |1!HK84ȜBЫJPTV&7g}6PI*xXے?iRzuױg׾{w?|y W}{|ǗO=ʑsWN 9>*Rы(H!lhIB Iѩ#]~ƭR <0q R:p]@F?H_h<QĒ,!rXf r󪪯0!$Itɩ2( aDK'v=>\Q"]"&%pdI&TmTm`3YES Tgѯ~N0}*: NW: x w`QBx' 灐5tIqxWi9' E4 ΆXw4UE-']%e4%nmtG9!bGä}Foё4!IG=. " !IINZ%1IMn'AJQ4)QJUt+aKYΒ-qK]V/La41I].d2MiNմ5iLgn$b6Nq499Mu ΆNyΓ=N} < v5APS u(/YCE1k>m%YC%5IQRz-U'H1 |lC5MqzPĤDSEfOTHt0SURU/鋀t]WzUuwޫn9yћ^Mz_ηey_[7_k`/F{a OX01a ?šAv8"6Q,Wc2-vYK˜5^7el=dc!ȁo!\d%/ɤre)OU򕱜e-o]f1e6ќf5mvg9ϙu,;transparency-dev-tessera-3cb22ee/cmd/conformance/gcp/000077500000000000000000000000001511600621500227355ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/conformance/gcp/Dockerfile000066400000000000000000000012511511600621500247260ustar00rootroot00000000000000FROM golang:1.24.1-alpine3.21@sha256:43c094ad24b6ac0546c62193baeb3e6e49ce14d3250845d166c77c25f64b0386 AS builder ARG GOFLAGS="-trimpath -buildvcs=false -buildmode=exe" ENV GOFLAGS=$GOFLAGS # Move to working directory /build WORKDIR /build # Copy and download dependency using go mod COPY go.mod . COPY go.sum . RUN go mod download # Copy the code into the container COPY . . # Build the application RUN go build -o bin/conformance-gcp ./cmd/conformance/gcp # Build release image FROM alpine:3.20.2@sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5 COPY --from=builder /build/bin/conformance-gcp /bin/conformance-gcp ENTRYPOINT ["/bin/conformance-gcp"] transparency-dev-tessera-3cb22ee/cmd/conformance/gcp/README.md000066400000000000000000000004541511600621500242170ustar00rootroot00000000000000# Conformance testing binary for GCP This binary is primarily intended to be used for checking Tessera conformance on GCP. If you want to try running it yourself, please see the instructions in the [README file in the /deployment/live/gcp/conformance directory](/deployment/live/gcp/conformance). transparency-dev-tessera-3cb22ee/cmd/conformance/gcp/main.go000066400000000000000000000116251511600621500242150ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // gcp is a simple personality allowing to run conformance/compliance/performance tests and showing how to use the Tessera GCP storage implmentation. package main import ( "context" "errors" "flag" "fmt" "io" "net/http" "time" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/storage/gcp" gcp_as "github.com/transparency-dev/tessera/storage/gcp/antispam" "golang.org/x/mod/sumdb/note" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "k8s.io/klog/v2" ) var ( bucket = flag.String("bucket", "", "Bucket to use for storing log") listen = flag.String("listen", ":2024", "Address:port to listen on") spanner = flag.String("spanner", "", "Spanner resource URI ('projects/.../...')") signer = flag.String("signer", "", "Note signer to use to sign checkpoints") persistentAntispam = flag.Bool("antispam", false, "EXPERIMENTAL: Set to true to enable GCP-based persistent antispam storage") traceFraction = flag.Float64("trace_fraction", 0.01, "Fraction of open-telemetry span traces to sample") additionalSigners = []string{} ) func init() { flag.Func("additional_signer", "Additional note signer for checkpoints, may be specified multiple times", func(s string) error { additionalSigners = append(additionalSigners, s) return nil }) } func main() { klog.InitFlags(nil) flag.Parse() ctx := context.Background() shutdownOTel := initOTel(ctx, *traceFraction) defer shutdownOTel(ctx) s, a := signerFromFlags() // Create our Tessera storage backend: gcpCfg := storageConfigFromFlags() driver, err := gcp.New(ctx, gcpCfg) if err != nil { klog.Exitf("Failed to create new GCP storage: %v", err) } var antispam tessera.Antispam // Persistent antispam is currently experimental, so there's no terraform or documentation yet! if *persistentAntispam { asOpts := gcp_as.AntispamOpts{} // Use defaults antispam, err = gcp_as.NewAntispam(ctx, fmt.Sprintf("%s-antispam", *spanner), asOpts) if err != nil { klog.Exitf("Failed to create new GCP antispam storage: %v", err) } } appender, shutdown, _, err := tessera.NewAppender(ctx, driver, tessera.NewAppendOptions(). WithCheckpointSigner(s, a...). WithCheckpointInterval(10*time.Second). WithBatching(512, 300*time.Millisecond). WithPushback(10*4096). WithAntispam(tessera.DefaultAntispamInMemorySize, antispam)) if err != nil { klog.Exit(err) } // Expose a HTTP handler for the conformance test writes. // This should accept arbitrary bytes POSTed to /add, and return an ascii // decimal representation of the index assigned to the entry. http.HandleFunc("POST /add", func(w http.ResponseWriter, r *http.Request) { b, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } f := appender.Add(r.Context(), tessera.NewEntry(b)) idx, err := f() if err != nil { if errors.Is(err, tessera.ErrPushback) { w.Header().Add("Retry-After", "1") w.WriteHeader(http.StatusServiceUnavailable) return } w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(err.Error())) return } // Write out the assigned index _, _ = fmt.Fprintf(w, "%d", idx.Index) }) h2s := &http2.Server{} h1s := &http.Server{ Addr: *listen, Handler: h2c.NewHandler(http.DefaultServeMux, h2s), ReadHeaderTimeout: 5 * time.Second, } if err := http2.ConfigureServer(h1s, h2s); err != nil { klog.Exitf("http2.ConfigureServer: %v", err) } if err := h1s.ListenAndServe(); err != nil { if err := shutdown(ctx); err != nil { klog.Exit(err) } klog.Exitf("ListenAndServe: %v", err) } } // storageConfigFromFlags returns a gcp.Config struct populated with values // provided via flags. func storageConfigFromFlags() gcp.Config { if *bucket == "" { klog.Exit("--bucket must be set") } if *spanner == "" { klog.Exit("--spanner must be set") } return gcp.Config{ Bucket: *bucket, Spanner: *spanner, } } func signerFromFlags() (note.Signer, []note.Signer) { s, err := note.NewSigner(*signer) if err != nil { klog.Exitf("Failed to create new signer: %v", err) } var a []note.Signer for _, as := range additionalSigners { s, err := note.NewSigner(as) if err != nil { klog.Exitf("Failed to create additional signer: %v", err) } a = append(a, s) } return s, a } transparency-dev-tessera-3cb22ee/cmd/conformance/gcp/otel.go000066400000000000000000000061171511600621500242340ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "context" "errors" "go.opentelemetry.io/contrib/detectors/gcp" "go.opentelemetry.io/otel" sdkmetric "go.opentelemetry.io/otel/sdk/metric" "go.opentelemetry.io/otel/sdk/resource" sdktrace "go.opentelemetry.io/otel/sdk/trace" semconv "go.opentelemetry.io/otel/semconv/v1.4.0" mexporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric" texporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace" "k8s.io/klog/v2" ) // initOTel initialises the open telemetry support for metrics and tracing. // // Tracing is enabled with statistical sampling, with the probability passed in. // Returns a shutdown function which should be called just before exiting the process. func initOTel(ctx context.Context, traceFraction float64) func(context.Context) { var shutdownFuncs []func(context.Context) error // shutdown combines shutdown functions from multiple OpenTelemetry // components into a single function. shutdown := func(ctx context.Context) { var err error for _, fn := range shutdownFuncs { err = errors.Join(err, fn(ctx)) } shutdownFuncs = nil if err != nil { klog.Errorf("OTel shutdown: %v", err) } } resources, err := resource.New(ctx, resource.WithTelemetrySDK(), resource.WithFromEnv(), // unpacks OTEL_RESOURCE_ATTRIBUTES // Add your own custom attributes to identify your application resource.WithAttributes( semconv.ServiceNameKey.String("conformance"), semconv.ServiceNamespaceKey.String("tessera"), ), resource.WithDetectors(gcp.NewDetector()), ) if err != nil { klog.Exitf("Failed to detect resources: %v", err) } me, err := mexporter.New() if err != nil { klog.Exitf("Failed to create metric exporter: %v", err) return nil } // initialize a MeterProvider that periodically exports to the GCP exporter. mp := sdkmetric.NewMeterProvider( sdkmetric.WithReader(sdkmetric.NewPeriodicReader(me)), sdkmetric.WithResource(resources), ) shutdownFuncs = append(shutdownFuncs, mp.Shutdown) otel.SetMeterProvider(mp) te, err := texporter.New() if err != nil { klog.Exitf("Failed to create trace exporter: %v", err) return nil } // initialize a TracerProvier that periodically exports to the GCP exporter. tp := sdktrace.NewTracerProvider( sdktrace.WithSampler(sdktrace.TraceIDRatioBased(traceFraction)), sdktrace.WithBatcher(te), sdktrace.WithResource(resources), ) shutdownFuncs = append(shutdownFuncs, tp.Shutdown) otel.SetTracerProvider(tp) return shutdown } transparency-dev-tessera-3cb22ee/cmd/conformance/mysql/000077500000000000000000000000001511600621500233315ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/conformance/mysql/README.md000066400000000000000000000055211511600621500246130ustar00rootroot00000000000000# Conformance MySQL log This binary runs an HTTP web server that accepts POST HTTP requests to an `/add` endpoint. This endpoint takes arbitrary data and adds it to a MySQL based Tessera log. > [!WARNING] > - This is an example and is not fit for production use, but demonstrates a way of using the Tessera Log with MySQL storage backend. > - This example is built on the [tlog tiles API](https://c2sp.org/tlog-tiles) for read endpoints and exposes a /add endpoint that allows any POSTed data to be added to the log. ## Bring up a log This will help you bring up a MySQL database to store a Tessera log, and start a personality binary that can add entries to it. You can run this personality using Docker Compose or manually with `go run`. Note that all the commands are executed at the root directory of this repository. ### Docker Compose #### Prerequisites Install [Docker Compose](https://docs.docker.com/compose/install/). #### Start the log ```sh docker compose -f ./cmd/conformance/mysql/docker/compose.yaml up ``` Add `-d` if you want to run the log in detached mode. #### Stop the log ```sh docker compose -f ./cmd/conformance/mysql/docker/compose.yaml down ``` ### Manual #### Prerequisites You need to have a MySQL database configured to run on port `3306`, accepting password auth for `root` with the password set to `root`, and a DB instance called `test_tessera`. You can start one using [Docker](https://docs.docker.com/engine/install/). ```sh docker run --name test-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=test_tessera -d mysql:8.4 ``` #### Start the log ```sh go run ./cmd/conformance/mysql --mysql_uri="root:root@tcp(localhost:3306)/test_tessera" --init_schema_path="./storage/mysql/schema.sql" --private_key_path="./cmd/conformance/mysql/docker/testdata/key" ``` #### Stop the log Ctrl C ## Add entries to the log ### Manually Head over to the [codelab](../#codelab) to manually add entries to the log, and inspect the log. ### Using the hammer In this example, we're running 256 writers against the log to add 1024 new leaves within 1 minute. Note that the writes are sent to the HTTP server we brought up in the previous step, but reads are sent directly to the file system. ```shell go run ./internal/hammer \ --log_public_key=transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW \ --log_url=http://localhost:2024/ \ --max_read_ops=0 \ --num_writers=256 \ --max_write_ops=256 \ --max_runtime=1m \ --leaf_write_goal=1024 \ --show_ui=false ``` Optionally, inspect the log using the woodpecker tool to see the contents: ```shell go run github.com/mhutchinson/woodpecker@main --custom_log_type=tiles --custom_log_url=http://localhost:2024/ --custom_log_vkey=transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW ``` transparency-dev-tessera-3cb22ee/cmd/conformance/mysql/docker/000077500000000000000000000000001511600621500246005ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/conformance/mysql/docker/Dockerfile000066400000000000000000000013001511600621500265640ustar00rootroot00000000000000FROM golang:1.24.1-alpine3.21@sha256:43c094ad24b6ac0546c62193baeb3e6e49ce14d3250845d166c77c25f64b0386 AS builder WORKDIR /build # pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change COPY go.mod go.sum ./ RUN go mod download && go mod verify COPY . . RUN CGO_ENABLED=0 go build -v -o ./conformance-mysql ./cmd/conformance/mysql FROM alpine:3.20@sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5 COPY --from=builder /build/conformance-mysql /build/cmd/conformance/mysql/docker/testdata/key /build/cmd/conformance/mysql/docker/testdata/key.pub /build/storage/mysql/schema.sql / ENTRYPOINT ["/conformance-mysql"] transparency-dev-tessera-3cb22ee/cmd/conformance/mysql/docker/compose.yaml000066400000000000000000000017541511600621500271400ustar00rootroot00000000000000services: tessera-conformance-mysql-db: container_name: tessera-mysql-db image: "mysql:8.4" ports: - "3306:3306" environment: - MYSQL_ROOT_PASSWORD=tessera - MYSQL_DATABASE=tessera - MYSQL_USER=tessera - MYSQL_PASSWORD=tessera restart: always healthcheck: test: mysql --user=$$MYSQL_USER --password=$$MYSQL_PASSWORD --silent --execute "SHOW DATABASES;" interval: 5s timeout: 5s retries: 10 tessera-conformance-mysql: container_name: tessera-conformance-mysql build: context: ../../../../ dockerfile: ./cmd/conformance/mysql/docker/Dockerfile ports: - "2024:2024" command: [ "--mysql_uri=tessera:tessera@tcp(tessera-conformance-mysql-db:3306)/tessera", "--init_schema_path=/schema.sql", "--private_key_path=/key", "--alsologtostderr", "--v=1", ] restart: always depends_on: tessera-conformance-mysql-db: condition: service_healthy transparency-dev-tessera-3cb22ee/cmd/conformance/mysql/docker/testdata/000077500000000000000000000000001511600621500264115ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/conformance/mysql/docker/testdata/key000066400000000000000000000001421511600621500271210ustar00rootroot00000000000000PRIVATE+KEY+transparency.dev/tessera/example+ae330e15+AXEwZQ2L6Ga3NX70ITObzyfEIketMr2o9Kc+ed/rt/QRtransparency-dev-tessera-3cb22ee/cmd/conformance/mysql/docker/testdata/key.pub000066400000000000000000000001261511600621500277100ustar00rootroot00000000000000transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DWtransparency-dev-tessera-3cb22ee/cmd/conformance/mysql/main.go000066400000000000000000000207271511600621500246140ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // mysql is a simple personality allowing to run conformance/compliance/performance tests and showing how to use the Tessera MySQL storage implmentation. package main import ( "context" "database/sql" "errors" "flag" "fmt" "io" "io/fs" "net/http" "os" "time" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/storage/mysql" "golang.org/x/mod/sumdb/note" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "k8s.io/klog/v2" ) var ( mysqlURI = flag.String("mysql_uri", "user:password@tcp(db:3306)/tessera", "Connection string for a MySQL database") dbConnMaxLifetime = flag.Duration("db_conn_max_lifetime", 3*time.Minute, "") dbMaxOpenConns = flag.Int("db_max_open_conns", 64, "") dbMaxIdleConns = flag.Int("db_max_idle_conns", 64, "") initSchemaPath = flag.String("init_schema_path", "", "Location of the schema file if database initialization is needed") listen = flag.String("listen", ":2024", "Address:port to listen on") privateKeyPath = flag.String("private_key_path", "", "Location of private key file") publishInterval = flag.Duration("publish_interval", 3*time.Second, "How frequently to publish updated checkpoints") additionalPrivateKeyPaths = []string{} ) func init() { flag.Func("additional_private_key_path", "Location of additional private key file, may be specified multiple times", func(s string) error { additionalPrivateKeyPaths = append(additionalPrivateKeyPaths, s) return nil }) } func main() { klog.InitFlags(nil) flag.Parse() ctx := context.Background() db := createDatabaseOrDie(ctx) noteSigner, additionalSigners := createSignersOrDie() // Initialise the Tessera MySQL storage driver, err := mysql.New(ctx, db) if err != nil { klog.Exitf("Failed to create new MySQL storage: %v", err) } appender, shutdown, reader, err := tessera.NewAppender(ctx, driver, tessera.NewAppendOptions(). WithCheckpointSigner(noteSigner, additionalSigners...). WithCheckpointInterval(*publishInterval). WithAntispam(tessera.DefaultAntispamInMemorySize, nil)) if err != nil { klog.Exit(err) } // Set up the handlers for the tlog-tiles GET methods, and a custom handler for HTTP POSTs to /add configureTilesReadAPI(http.DefaultServeMux, reader) http.HandleFunc("POST /add", func(w http.ResponseWriter, r *http.Request) { b, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } idx, err := appender.Add(r.Context(), tessera.NewEntry(b))() if err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(err.Error())) return } if _, err := fmt.Fprintf(w, "%d", idx.Index); err != nil { klog.Errorf("/add: %v", err) return } }) // TODO(mhutchinson): Change the listen flag to just a port, or fix up this address formatting klog.Infof("Environment variables useful for accessing this log:\n"+ "export WRITE_URL=http://localhost%s/ \n"+ "export READ_URL=http://localhost%s/ \n", *listen, *listen) // Run the HTTP server with the single handler and block until this is terminated h2s := &http2.Server{} h1s := &http.Server{ Addr: *listen, Handler: h2c.NewHandler(http.DefaultServeMux, h2s), ReadHeaderTimeout: 5 * time.Second, } if err := http2.ConfigureServer(h1s, h2s); err != nil { klog.Exitf("http2.ConfigureServer: %v", err) } if err := h1s.ListenAndServe(); err != nil { if err := shutdown(ctx); err != nil { klog.Exit(err) } klog.Exitf("ListenAndServe: %v", err) } } func createDatabaseOrDie(ctx context.Context) *sql.DB { db, err := sql.Open("mysql", *mysqlURI) if err != nil { klog.Exitf("Failed to connect to DB: %v", err) } db.SetConnMaxLifetime(*dbConnMaxLifetime) db.SetMaxOpenConns(*dbMaxOpenConns) db.SetMaxIdleConns(*dbMaxIdleConns) initDatabaseSchema(ctx) return db } func createSignersOrDie() (note.Signer, []note.Signer) { s := createSignerOrDie(*privateKeyPath) a := []note.Signer{} for _, p := range additionalPrivateKeyPaths { a = append(a, createSignerOrDie(p)) } return s, a } func createSignerOrDie(s string) note.Signer { rawPrivateKey, err := os.ReadFile(s) if err != nil { klog.Exitf("Failed to read private key file %q: %v", s, err) } noteSigner, err := note.NewSigner(string(rawPrivateKey)) if err != nil { klog.Exitf("Failed to create new signer: %v", err) } return noteSigner } // configureTilesReadAPI adds the API methods from https://c2sp.org/tlog-tiles to the mux, // routing the requests to the mysql storage. // This method could be moved into the storage API as it's likely this will be // the same for any implementation of a personality based on MySQL. func configureTilesReadAPI(mux *http.ServeMux, reader tessera.LogReader) { mux.HandleFunc("GET /checkpoint", func(w http.ResponseWriter, r *http.Request) { checkpoint, err := reader.ReadCheckpoint(r.Context()) if err != nil { if errors.Is(err, fs.ErrNotExist) { w.WriteHeader(http.StatusNotFound) return } klog.Errorf("/checkpoint: %v", err) w.WriteHeader(http.StatusInternalServerError) return } // Don't cache checkpoints as the endpoint refreshes regularly. // A personality that wanted to _could_ set a small cache time here which was no higher // than the checkpoint publish interval. w.Header().Set("Cache-Control", "no-cache") if _, err := w.Write(checkpoint); err != nil { klog.Errorf("/checkpoint: %v", err) return } }) mux.HandleFunc("GET /tile/{level}/{index...}", func(w http.ResponseWriter, r *http.Request) { level, index, p, err := layout.ParseTileLevelIndexPartial(r.PathValue("level"), r.PathValue("index")) if err != nil { w.WriteHeader(http.StatusBadRequest) if _, werr := fmt.Fprintf(w, "Malformed URL: %s", err.Error()); werr != nil { klog.Errorf("/tile/{level}/{index...}: %v", werr) } return } tile, err := reader.ReadTile(r.Context(), level, index, p) if err != nil { if errors.Is(err, fs.ErrNotExist) { w.WriteHeader(http.StatusNotFound) return } klog.Errorf("/tile/{level}/{index...}: %v", err) w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Cache-Control", "max-age=31536000, immutable") if _, err := w.Write(tile); err != nil { klog.Errorf("/tile/{level}/{index...}: %v", err) return } }) mux.HandleFunc("GET /tile/entries/{index...}", func(w http.ResponseWriter, r *http.Request) { index, p, err := layout.ParseTileIndexPartial(r.PathValue("index")) if err != nil { w.WriteHeader(http.StatusBadRequest) if _, werr := fmt.Fprintf(w, "Malformed URL: %s", err.Error()); werr != nil { klog.Errorf("/tile/entries/{index...}: %v", werr) } return } entryBundle, err := reader.ReadEntryBundle(r.Context(), index, p) if err != nil { klog.Errorf("/tile/entries/{index...}: %v", err) w.WriteHeader(http.StatusInternalServerError) return } if entryBundle == nil { w.WriteHeader(http.StatusNotFound) return } w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") if _, err := w.Write(entryBundle); err != nil { klog.Errorf("/tile/entries/{index...}: %v", err) return } }) } func initDatabaseSchema(ctx context.Context) { if *initSchemaPath != "" { klog.Infof("Initializing database schema") db, err := sql.Open("mysql", *mysqlURI+"?multiStatements=true") if err != nil { klog.Exitf("Failed to connect to DB: %v", err) } defer func() { if err := db.Close(); err != nil { klog.Warningf("Failed to close db: %v", err) } }() rawSchema, err := os.ReadFile(*initSchemaPath) if err != nil { klog.Exitf("Failed to read init schema file %q: %v", *initSchemaPath, err) } if _, err := db.ExecContext(ctx, string(rawSchema)); err != nil { klog.Exitf("Failed to execute init database schema: %v", err) } klog.Infof("Database schema initialized") } } transparency-dev-tessera-3cb22ee/cmd/conformance/posix/000077500000000000000000000000001511600621500233265ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/conformance/posix/README.md000066400000000000000000000034441511600621500246120ustar00rootroot00000000000000# conformance-posix This binary runs an HTTP web server that accepts POST HTTP requests to an `/add` endpoint. This endpoint takes arbitrary data and adds it to a file-based log. ## Bring up a log This will create a directory in your filesystem to store a log, and start a personality binary that can add entries to this log. First, define a few environment vaiables: ```shell export LOG_PRIVATE_KEY="PRIVATE+KEY+example.com/log/testdata+33d7b496+AeymY/SZAX0jZcJ8enZ5FY1Dz+wTML2yWSkK+9DSF3eg" export LOG_PUBLIC_KEY="example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx" export LOG_DIR=/tmp/mylog ``` Then, start the personality: ```shell go run ./cmd/conformance/posix \ --storage_dir=${LOG_DIR} \ --listen=:2025 \ --v=2 ``` ## Add entries to the log ### Manually Head over to the [codelab](../#codelab) to manually add entries to the log, and inspect the log. ### Using the hammer In another terminal, run the [hammer](./internal/hammer) against the log. In this example, we're running 32 writers against the log to add 128 new leaves within 1 minute. ```shell go run ./internal/hammer \ --log_public_key=example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx \ --log_url=http://localhost:2025 \ --max_read_ops=0 \ --num_writers=32 \ --max_write_ops=64 \ --max_runtime=1m \ --leaf_write_goal=128 \ --show_ui=false ``` Optionally, inspect the log on the filesystem using the woodpecker tool to see the contents. Note that this reads only from the files on disk, so none of the commands above need to be running for this to work. ```shell go run github.com/mhutchinson/woodpecker@main \ --custom_log_type=tiles \ --custom_log_url=file:///${LOG_DIR}/ \ --custom_log_origin=example.com/log/testdata \ --custom_log_vkey=${LOG_PUBLIC_KEY} ``` transparency-dev-tessera-3cb22ee/cmd/conformance/posix/docker/000077500000000000000000000000001511600621500245755ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/conformance/posix/docker/Dockerfile000066400000000000000000000010721511600621500265670ustar00rootroot00000000000000FROM golang:1.24.1-alpine3.21@sha256:43c094ad24b6ac0546c62193baeb3e6e49ce14d3250845d166c77c25f64b0386 AS builder WORKDIR /build # pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change COPY go.mod go.sum ./ RUN go mod download && go mod verify COPY . . RUN CGO_ENABLED=0 go build -v -o ./conformance-posix ./cmd/conformance/posix FROM alpine:3.20@sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5 COPY --from=builder /build/conformance-posix / ENTRYPOINT ["/conformance-posix"] transparency-dev-tessera-3cb22ee/cmd/conformance/posix/docker/compose.yaml000066400000000000000000000012401511600621500271230ustar00rootroot00000000000000services: tessera-conformance-posix: container_name: tessera-conformance-posix build: context: ../../../../ dockerfile: ./cmd/conformance/posix/docker/Dockerfile ports: - "2025:2025" environment: LOG_PRIVATE_KEY: "PRIVATE+KEY+example.com/log/testdata+33d7b496+AeymY/SZAX0jZcJ8enZ5FY1Dz+wTML2yWSkK+9DSF3eg" LOG_PUBLIC_KEY: "example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx" command: [ "--storage_dir=/tmp/tessera-posix-log", "--listen=:2025", "--alsologtostderr", "--v=2", ] volumes: - /tmp/tessera-posix-log:/tmp/tessera-posix-log restart: always transparency-dev-tessera-3cb22ee/cmd/conformance/posix/main.go000066400000000000000000000141741511600621500246100ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // posix runs a web server that allows new entries to be POSTed to // a tlog-tiles log stored on a posix filesystem. It allows to run // conformance/compliance/performance tests and showing how to use // the Tessera POSIX storage implmentation. package main import ( "context" "flag" "fmt" "io" "net/http" "os" "path/filepath" "time" "golang.org/x/mod/sumdb/note" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/storage/posix" badger_as "github.com/transparency-dev/tessera/storage/posix/antispam" "k8s.io/klog/v2" ) var ( storageDir = flag.String("storage_dir", "", "Root directory to store log data.") listen = flag.String("listen", ":2025", "Address:port to listen on") privKeyFile = flag.String("private_key", "", "Location of private key file. If unset, uses the contents of the LOG_PRIVATE_KEY environment variable.") persistentAntispam = flag.Bool("antispam", false, "EXPERIMENTAL: Set to true to enable Badger-based persistent antispam storage") additionalPrivateKeyFiles = []string{} ) func init() { flag.Func("additional_private_key", "Location of addition private key, may be specified multiple times", func(s string) error { additionalPrivateKeyFiles = append(additionalPrivateKeyFiles, s) return nil }) } func addCacheHeaders(value string, fs http.Handler) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { w.Header().Add("Cache-Control", value) fs.ServeHTTP(w, r) } } func main() { klog.InitFlags(nil) flag.Parse() ctx := context.Background() // Gather the info needed for reading/writing checkpoints s, a := getSignersOrDie() // Create the Tessera POSIX storage, using the directory from the --storage_dir flag driver, err := posix.New(ctx, posix.Config{Path: *storageDir}) if err != nil { klog.Exitf("Failed to construct storage: %v", err) } var antispam tessera.Antispam // Persistent antispam is currently experimental, so there's no terraform or documentation yet! if *persistentAntispam { asOpts := badger_as.AntispamOpts{} antispam, err = badger_as.NewAntispam(ctx, filepath.Join(*storageDir, ".state", "antispam"), asOpts) if err != nil { klog.Exitf("Failed to create new Badger antispam storage: %v", err) } } appender, shutdown, _, err := tessera.NewAppender(ctx, driver, tessera.NewAppendOptions(). WithCheckpointSigner(s, a...). WithCheckpointInterval(time.Second). WithCheckpointRepublishInterval(time.Minute). WithBatching(256, time.Second). WithAntispam(tessera.DefaultAntispamInMemorySize, antispam)) if err != nil { klog.Exit(err) } // Define a handler for /add that accepts POST requests and adds the POST body to the log http.HandleFunc("POST /add", func(w http.ResponseWriter, r *http.Request) { b, err := io.ReadAll(r.Body) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } idx, err := appender.Add(r.Context(), tessera.NewEntry(b))() if err != nil { w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(err.Error())) return } if _, err := fmt.Fprintf(w, "%d", idx.Index); err != nil { klog.Errorf("/add: %v", err) return } }) // Proxy all GET requests to the filesystem as a lightweight file server. // This makes it easier to test this implementation from another machine. fs := http.FileServer(http.Dir(*storageDir)) http.Handle("GET /checkpoint", addCacheHeaders("no-cache", fs)) http.Handle("GET /tile/", addCacheHeaders("max-age=31536000, immutable", fs)) http.Handle("GET /entries/", fs) // TODO(mhutchinson): Change the listen flag to just a port, or fix up this address formatting klog.Infof("Environment variables useful for accessing this log:\n"+ "export WRITE_URL=http://localhost%s/ \n"+ "export READ_URL=http://localhost%s/ \n", *listen, *listen) // Run the HTTP server with the single handler and block until this is terminated h2s := &http2.Server{} h1s := &http.Server{ Addr: *listen, Handler: h2c.NewHandler(http.DefaultServeMux, h2s), ReadHeaderTimeout: 5 * time.Second, } if err := http2.ConfigureServer(h1s, h2s); err != nil { klog.Exitf("http2.ConfigureServer: %v", err) } if err := h1s.ListenAndServe(); err != nil { if err := shutdown(ctx); err != nil { klog.Exit(err) } klog.Exitf("ListenAndServe: %v", err) } } func getSignersOrDie() (note.Signer, []note.Signer) { s := getSignerOrDie() a := []note.Signer{} for _, p := range additionalPrivateKeyFiles { kr, err := getKeyFile(p) if err != nil { klog.Exitf("Unable to get additional private key from %q: %v", p, err) } k, err := note.NewSigner(kr) if err != nil { klog.Exitf("Failed to instantiate signer from %q: %v", p, err) } a = append(a, k) } return s, a } // Read log private key from file or environment variable func getSignerOrDie() note.Signer { var privKey string var err error if len(*privKeyFile) > 0 { privKey, err = getKeyFile(*privKeyFile) if err != nil { klog.Exitf("Unable to get private key: %q", err) } } else { privKey = os.Getenv("LOG_PRIVATE_KEY") if len(privKey) == 0 { klog.Exit("Supply private key file path using --private_key or set LOG_PRIVATE_KEY environment variable") } } s, err := note.NewSigner(privKey) if err != nil { klog.Exitf("Failed to instantiate signer: %q", err) } return s } func getKeyFile(path string) (string, error) { k, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("failed to read key file: %w", err) } return string(k), nil } transparency-dev-tessera-3cb22ee/cmd/examples/000077500000000000000000000000001511600621500215105ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/examples/posix-oneshot/000077500000000000000000000000001511600621500243275ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/examples/posix-oneshot/README.md000066400000000000000000000035151511600621500256120ustar00rootroot00000000000000# POSIX one-shot CLI `posix-oneshot` is a command line tool to add entries to a log stored on the local filesystem. ## Example usage The commands below create a new log and add entries to it, and then show a few approaches to inspect the contents of the log. ```shell # Set the keys via environment variables, and a local directory where our demo log will be built. export LOG_PRIVATE_KEY="PRIVATE+KEY+example.com/log/testdata+33d7b496+AeymY/SZAX0jZcJ8enZ5FY1Dz+wTML2yWSkK+9DSF3eg" export LOG_PUBLIC_KEY="example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx" export LOG_DIR="/tmp/tlog" # Create files containing new leaves to add mkdir /tmp/stuff echo "foo" > /tmp/stuff/foo echo "bar" > /tmp/stuff/bar echo "baz" > /tmp/stuff/baz # Integrate all of these leaves into the tree go run ./cmd/examples/posix-oneshot --storage_dir=${LOG_DIR} --entries="/tmp/stuff/*" # Check that the checkpoint is of the correct size and the leaves are present cat ${LOG_DIR}/checkpoint cat ${LOG_DIR}/tile/entries/000.p/* # Optionally, inspect the log using the woodpecker tool to see the contents go run github.com/mhutchinson/woodpecker@main --custom_log_type=tiles --custom_log_url=file:///${LOG_DIR}/ --custom_log_origin=example.com/log/testdata --custom_log_vkey=${LOG_PUBLIC_KEY} # More entries can be added to the log using the following: go run ./cmd/examples/posix-oneshot --storage_dir=${LOG_DIR} --entries="/tmp/stuff/*" ``` ## Using the log A POSIX log can be used directly via file paths, but a more common approach to using such a log is to use static file hosting to do this. A couple of approaches are: - [NGINX](https://docs.nginx.com/nginx/admin-guide/web-server/serving-static-content/) - [GitHub](https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file) - the files can be accessed via the raw URLs transparency-dev-tessera-3cb22ee/cmd/examples/posix-oneshot/main.go000066400000000000000000000154661511600621500256160ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // posix-oneshot is a command line tool for adding entries to a local // tlog-tiles log stored on a posix filesystem. // The command takes a list of new entries to add to the log, and exits // when they are successfully integrated. // See the README in this package for more detailed usage instructions. package main import ( "context" "flag" "fmt" "os" "path/filepath" "time" "golang.org/x/mod/sumdb/note" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/storage/posix" "k8s.io/klog/v2" ) var ( storageDir = flag.String("storage_dir", "", "Root directory to store log data.") entries = flag.String("entries", "", "File path glob of entries to add to the log.") privKeyFile = flag.String("private_key", "", "Location of private key file. If unset, uses the contents of the LOG_PRIVATE_KEY environment variable.") witnessPolicyFile = flag.String("witness_policy_file", "", "(Optional) Path to the file containing the witness policy in the format describe at https://git.glasklar.is/sigsum/core/sigsum-go/-/blob/main/doc/policy.md") witnessTimeout = flag.Duration("witness_timeout", tessera.DefaultWitnessTimeout, "Maximum time to wait for witness responses.") witnessFailOpen = flag.Bool("witness_fail_open", false, "Still publish a checkpoint even if witness policy could not be met") ) // entryInfo binds the actual bytes to be added as a leaf with a // user-recognisable name for the source of those bytes. // The name is only used below in order to inform the user of the // sequence numbers assigned to the data from the provided input files. type entryInfo struct { name string f tessera.IndexFuture } func main() { klog.InitFlags(nil) flag.Parse() ctx := context.Background() klog.V(1).Infof("Initialising driver") // Gather the info needed for reading/writing checkpoints s := getSignerOrDie() // Construct a new Tessera POSIX log storage, anchored at the correct directory, and initialising it if requested. // The options provide the checkpoint signer & verifier, and batch options. // In this case, we want to create a single batch containing all of the leaves being added in order to // add all of these leaves without creating any intermediate checkpoints. driver, err := posix.New( ctx, posix.Config{ Path: *storageDir, }, ) if err != nil { klog.Exitf("Failed to construct storage: %v", err) } klog.V(1).Infof("Reading entries") // Evaluate the glob provided by the --entries flag to determine the files containing leaves filesToAdd := readEntriesOrDie() batchSize := uint(len(filesToAdd)) if batchSize == 0 { // batchSize can't be zero batchSize = 1 } klog.V(1).Infof("Configuring options") opts := tessera.NewAppendOptions(). WithCheckpointSigner(s). // Hint to Tessera the number of entries we're about to add via the batchSize parameter below, // this will cause the batch to flush as soon as we've called Add on the final entry. WithBatching(batchSize, 100*time.Millisecond). // We're unlikely to ever wait this long to publish a checkpoint because of the batchSize hint // passed in to the option above, but we set this interval low primarily such that if the user re-runs this // tool to add further entries to the log, they don't have to wait for the previous checkpoint to // become old enough to be overwritten. WithCheckpointInterval(100 * time.Millisecond) if *witnessPolicyFile != "" { f, err := os.ReadFile(*witnessPolicyFile) if err != nil { klog.Exitf("failed to read witness policy file %q: %v", *witnessPolicyFile, err) } wg, err := tessera.NewWitnessGroupFromPolicy(f) if err != nil { klog.Exitf("failed to create witness group from policy: %v", err) } wOpts := &tessera.WitnessOptions{ FailOpen: *witnessFailOpen, Timeout: *witnessTimeout, } opts.WithWitnesses(wg, wOpts) } klog.V(1).Infof("Creating appender") appender, shutdown, r, err := tessera.NewAppender(ctx, driver, opts) if err != nil { klog.Exit(err) } klog.V(1).Infof("Creating awaiter") // We don't want to exit until our entries have been integrated into the tree, so we'll use Tessera's // PublicationAwaiter to help with that. await := tessera.NewPublicationAwaiter(ctx, r.ReadCheckpoint, 100*time.Millisecond) klog.V(1).Infof("Adding entries") // Add each of the leaves in order, and store the futures in a slice // that we will check once all leaves are sent to storage. indexFutures := make([]entryInfo, 0, len(filesToAdd)) for _, fp := range filesToAdd { b, err := os.ReadFile(fp) if err != nil { klog.Exitf("Failed to read entry file %q: %q", fp, err) } f := appender.Add(ctx, tessera.NewEntry(b)) indexFutures = append(indexFutures, entryInfo{name: fp, f: f}) } klog.V(1).Infof("Awaiting entries") // Two options to ensure all work is done: // 1) Check each of the futures to ensure that the leaves are sequenced. for _, entry := range indexFutures { seq, _, err := await.Await(ctx, entry.f) if err != nil { klog.Exitf("Failed to sequence %q: %q", entry.name, err) } klog.Infof("%d: %v", seq.Index, entry.name) } klog.V(1).Infof("Futures resolved") klog.V(1).Infof("Shutting down") // 2) shutdown the appender if err := shutdown(ctx); err != nil { klog.Exitf("Failed to shut down cleanly: %v", err) } klog.V(1).Infof("Finished") } // Read log private key from file or environment variable func getSignerOrDie() note.Signer { var privKey string var err error if len(*privKeyFile) > 0 { privKey, err = getKeyFile(*privKeyFile) if err != nil { klog.Exitf("Unable to get private key: %q", err) } } else { privKey = os.Getenv("LOG_PRIVATE_KEY") if len(privKey) == 0 { klog.Exit("Supply private key file path using --private_key or set LOG_PRIVATE_KEY environment variable") } } s, err := note.NewSigner(privKey) if err != nil { klog.Exitf("Failed to instantiate signer: %q", err) } return s } func getKeyFile(path string) (string, error) { k, err := os.ReadFile(path) if err != nil { return "", fmt.Errorf("failed to read key file: %w", err) } return string(k), nil } func readEntriesOrDie() []string { toAdd, err := filepath.Glob(*entries) if err != nil { klog.Exitf("Failed to glob entries %q: %q", *entries, err) } klog.V(1).Infof("toAdd: %v", toAdd) return toAdd } transparency-dev-tessera-3cb22ee/cmd/experimental/000077500000000000000000000000001511600621500223675ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/experimental/migrate/000077500000000000000000000000001511600621500240175ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/experimental/migrate/aws/000077500000000000000000000000001511600621500246115ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/experimental/migrate/aws/main.go000066400000000000000000000120551511600621500260670ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // aws-migrate is a command-line tool for migrating data from a tlog-tiles // compliant log, into a Tessera log instance hosted on AWS. package main import ( "context" "flag" "fmt" "net/url" aaws "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/go-sql-driver/mysql" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/internal/parse" "github.com/transparency-dev/tessera/storage/aws" "k8s.io/klog/v2" ) var ( bucket = flag.String("bucket", "", "Bucket to use for storing log") dbName = flag.String("db_name", "", "AuroraDB name") dbHost = flag.String("db_host", "", "AuroraDB host") dbPort = flag.Int("db_port", 3306, "AuroraDB port") dbUser = flag.String("db_user", "", "AuroraDB user") dbPassword = flag.String("db_password", "", "AuroraDB user") dbMaxConns = flag.Int("db_max_conns", 0, "Maximum connections to the database, defaults to 0, i.e unlimited") dbMaxIdle = flag.Int("db_max_idle_conns", 2, "Maximum idle database connections in the connection pool, defaults to 2") s3Endpoint = flag.String("s3_endpoint", "", "Endpoint for custom non-AWS S3 service") s3AccessKeyID = flag.String("s3_access_key", "", "Access key ID for custom non-AWS S3 service") s3SecretAccessKey = flag.String("s3_secret", "", "Secret access key for custom non-AWS S3 service") sourceURL = flag.String("source_url", "", "Base URL for the source log.") numWorkers = flag.Uint("num_workers", 30, "Number of migration worker goroutines.") ) func main() { klog.InitFlags(nil) flag.Parse() ctx := context.Background() if *sourceURL == "" { klog.Exit("Missing parameter: --source_url") } srcURL, err := url.Parse(*sourceURL) if err != nil { klog.Exitf("Invalid --source_url %q: %v", *sourceURL, err) } src, err := client.NewHTTPFetcher(srcURL, nil) if err != nil { klog.Exitf("Failed to create HTTP fetcher: %v", err) } sourceCP, err := src.ReadCheckpoint(ctx) if err != nil { klog.Exitf("fetch initial source checkpoint: %v", err) } // TODO(mhutchinson): parse this safely. _, sourceSize, sourceRoot, err := parse.CheckpointUnsafe(sourceCP) if err != nil { klog.Exitf("Failed to parse checkpoint: %v", err) } // Create our Tessera storage backend: awsCfg := storageConfigFromFlags() driver, err := aws.New(ctx, awsCfg) if err != nil { klog.Exitf("Failed to create new AWS storage: %v", err) } opts := tessera.NewMigrationOptions() m, err := tessera.NewMigrationTarget(ctx, driver, opts) if err != nil { klog.Exitf("Failed to create MigrationTarget: %v", err) } klog.Infof("Starting Migrate() with workers=%d, sourceSize=%d, migrating from %q", *numWorkers, sourceSize, *sourceURL) if err := m.Migrate(context.Background(), *numWorkers, sourceSize, sourceRoot, src.ReadEntryBundle); err != nil { klog.Exitf("Migrate failed: %v", err) } } // storageConfigFromFlags returns an aws.Config struct populated with values // provided via flags. func storageConfigFromFlags() aws.Config { if *bucket == "" { klog.Exit("--bucket must be set") } if *dbName == "" { klog.Exit("--db_name must be set") } if *dbHost == "" { klog.Exit("--db_host must be set") } if *dbPort == 0 { klog.Exit("--db_port must be set") } if *dbUser == "" { klog.Exit("--db_user must be set") } // Empty passord isn't an option with AuroraDB MySQL. if *dbPassword == "" { klog.Exit("--db_password must be set") } c := mysql.Config{ User: *dbUser, Passwd: *dbPassword, Net: "tcp", Addr: fmt.Sprintf("%s:%d", *dbHost, *dbPort), DBName: *dbName, AllowCleartextPasswords: true, AllowNativePasswords: true, } // Configure to use MinIO Server var awsConfig *aaws.Config var s3Opts func(o *s3.Options) if *s3Endpoint != "" { const defaultRegion = "us-east-1" s3Opts = func(o *s3.Options) { o.BaseEndpoint = aaws.String(*s3Endpoint) o.Credentials = credentials.NewStaticCredentialsProvider(*s3AccessKeyID, *s3SecretAccessKey, "") o.Region = defaultRegion o.UsePathStyle = true } awsConfig = &aaws.Config{ Region: defaultRegion, } } return aws.Config{ Bucket: *bucket, SDKConfig: awsConfig, S3Options: s3Opts, DSN: c.FormatDSN(), MaxOpenConns: *dbMaxConns, MaxIdleConns: *dbMaxIdle, } } transparency-dev-tessera-3cb22ee/cmd/experimental/migrate/gcp/000077500000000000000000000000001511600621500245705ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/experimental/migrate/gcp/main.go000066400000000000000000000070571511600621500260540ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // gcp-migrate is a command-line tool for migrating data from a tlog-tiles // compliant log, into a Tessera log instance. package main import ( "context" "encoding/base64" "flag" "fmt" "net/url" "strconv" "strings" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/storage/gcp" gcp_as "github.com/transparency-dev/tessera/storage/gcp/antispam" "k8s.io/klog/v2" ) var ( bucket = flag.String("bucket", "", "Bucket to use for storing log") spanner = flag.String("spanner", "", "Spanner resource URI ('projects/.../...')") sourceURL = flag.String("source_url", "", "Base URL for the source log.") numWorkers = flag.Uint("num_workers", 30, "Number of migration worker goroutines.") persistentAntispam = flag.Bool("antispam", false, "EXPERIMENTAL: Set to true to enable GCP-based persistent antispam storage") ) func main() { klog.InitFlags(nil) flag.Parse() ctx := context.Background() srcURL, err := url.Parse(*sourceURL) if err != nil { klog.Exitf("Invalid --source_url %q: %v", *sourceURL, err) } src, err := client.NewHTTPFetcher(srcURL, nil) if err != nil { klog.Exitf("Failed to create HTTP fetcher: %v", err) } sourceCP, err := src.ReadCheckpoint(ctx) if err != nil { klog.Exitf("fetch initial source checkpoint: %v", err) } bits := strings.Split(string(sourceCP), "\n") sourceSize, err := strconv.ParseUint(bits[1], 10, 64) if err != nil { klog.Exitf("invalid CP size %q: %v", bits[1], err) } sourceRoot, err := base64.StdEncoding.DecodeString(bits[2]) if err != nil { klog.Exitf("invalid checkpoint roothash %q: %v", bits[2], err) } // Create our Tessera storage backend: gcpCfg := storageConfigFromFlags() driver, err := gcp.New(ctx, gcpCfg) if err != nil { klog.Exitf("Failed to create new GCP storage driver: %v", err) } opts := tessera.NewMigrationOptions() // Configure antispam storage, if necessary var antispam tessera.Antispam // Persistent antispam is currently experimental, so there's no terraform or documentation yet! if *persistentAntispam { asOpts := gcp_as.AntispamOpts{ MaxBatchSize: 1500, } antispam, err = gcp_as.NewAntispam(ctx, fmt.Sprintf("%s-antispam", *spanner), asOpts) if err != nil { klog.Exitf("Failed to create new GCP antispam storage: %v", err) } opts.WithAntispam(antispam) } m, err := tessera.NewMigrationTarget(ctx, driver, opts) if err != nil { klog.Exitf("Failed to create MigrationTarget: %v", err) } if err := m.Migrate(context.Background(), *numWorkers, sourceSize, sourceRoot, src.ReadEntryBundle); err != nil { klog.Exitf("Migrate failed: %v", err) } } // storageConfigFromFlags returns a gcp.Config struct populated with values // provided via flags. func storageConfigFromFlags() gcp.Config { if *bucket == "" { klog.Exit("--bucket must be set") } if *spanner == "" { klog.Exit("--spanner must be set") } return gcp.Config{ Bucket: *bucket, Spanner: *spanner, } } transparency-dev-tessera-3cb22ee/cmd/experimental/migrate/mysql/000077500000000000000000000000001511600621500251645ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/experimental/migrate/mysql/main.go000066400000000000000000000076011511600621500264430ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // mysql-migrate is a command-line tool for migrating data from a tlog-tiles // compliant log, into a Tessera log instance. package main import ( "context" "database/sql" "encoding/base64" "flag" "net/url" "os" "strconv" "strings" "time" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/storage/mysql" "k8s.io/klog/v2" ) var ( mysqlURI = flag.String("mysql_uri", "user:password@tcp(db:3306)/tessera", "Connection string for a MySQL database") dbConnMaxLifetime = flag.Duration("db_conn_max_lifetime", 3*time.Minute, "") dbMaxOpenConns = flag.Int("db_max_open_conns", 64, "") dbMaxIdleConns = flag.Int("db_max_idle_conns", 64, "") initSchemaPath = flag.String("init_schema_path", "", "Location of the schema file if database initialization is needed") sourceURL = flag.String("source_url", "", "Base URL for the source log.") numWorkers = flag.Uint("num_workers", 30, "Number of migration worker goroutines.") ) func main() { klog.InitFlags(nil) flag.Parse() ctx := context.Background() srcURL, err := url.Parse(*sourceURL) if err != nil { klog.Exitf("Invalid --source_url %q: %v", *sourceURL, err) } src, err := client.NewHTTPFetcher(srcURL, nil) if err != nil { klog.Exitf("Failed to create HTTP fetcher: %v", err) } sourceCP, err := src.ReadCheckpoint(ctx) if err != nil { klog.Exitf("Failed to read source checkpoint: %v", err) } bits := strings.Split(string(sourceCP), "\n") sourceSize, err := strconv.ParseUint(bits[1], 10, 64) if err != nil { klog.Exitf("Invalid CP size %q: %v", bits[1], err) } sourceRoot, err := base64.StdEncoding.DecodeString(bits[2]) if err != nil { klog.Exitf("Invalid checkpoint roothash %q: %v", bits[2], err) } db := createDatabaseOrDie(ctx) // Initialise the Tessera MySQL storage driver, err := mysql.New(ctx, db) if err != nil { klog.Exitf("Failed to create new MySQL storage: %v", err) } opts := tessera.NewMigrationOptions() m, err := tessera.NewMigrationTarget(ctx, driver, opts) if err != nil { klog.Exitf("Failed to create MigrationTarget: %v", err) } if err := m.Migrate(context.Background(), *numWorkers, sourceSize, sourceRoot, src.ReadEntryBundle); err != nil { klog.Exitf("Migrate failed: %v", err) } } func initDatabaseSchema(ctx context.Context) { if *initSchemaPath != "" { klog.Infof("Initializing database schema") db, err := sql.Open("mysql", *mysqlURI+"?multiStatements=true") if err != nil { klog.Exitf("Failed to connect to DB: %v", err) } defer func() { if err := db.Close(); err != nil { klog.Warningf("Failed to close db: %v", err) } }() rawSchema, err := os.ReadFile(*initSchemaPath) if err != nil { klog.Exitf("Failed to read init schema file %q: %v", *initSchemaPath, err) } if _, err := db.ExecContext(ctx, string(rawSchema)); err != nil { klog.Exitf("Failed to execute init database schema: %v", err) } klog.Infof("Database schema initialized") } } func createDatabaseOrDie(ctx context.Context) *sql.DB { db, err := sql.Open("mysql", *mysqlURI) if err != nil { klog.Exitf("Failed to connect to DB: %v", err) } db.SetConnMaxLifetime(*dbConnMaxLifetime) db.SetMaxOpenConns(*dbMaxOpenConns) db.SetMaxIdleConns(*dbMaxIdleConns) initDatabaseSchema(ctx) return db } transparency-dev-tessera-3cb22ee/cmd/experimental/migrate/posix/000077500000000000000000000000001511600621500251615ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/experimental/migrate/posix/main.go000066400000000000000000000047461511600621500264470ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // posix-migrate is a command-line tool for migrating data from a tlog-tiles // compliant log, into a Tessera log instance. package main import ( "context" "encoding/base64" "flag" "net/url" "strconv" "strings" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/storage/posix" "k8s.io/klog/v2" ) var ( storageDir = flag.String("storage_dir", "", "Root directory to store log data.") sourceURL = flag.String("source_url", "", "Base URL for the source log.") numWorkers = flag.Uint("num_workers", 30, "Number of migration worker goroutines.") ) func main() { klog.InitFlags(nil) flag.Parse() ctx := context.Background() srcURL, err := url.Parse(*sourceURL) if err != nil { klog.Exitf("Invalid --source_url %q: %v", *sourceURL, err) } src, err := client.NewHTTPFetcher(srcURL, nil) if err != nil { klog.Exitf("Failed to create HTTP fetcher: %v", err) } sourceCP, err := src.ReadCheckpoint(ctx) if err != nil { klog.Exitf("fetch initial source checkpoint: %v", err) } bits := strings.Split(string(sourceCP), "\n") sourceSize, err := strconv.ParseUint(bits[1], 10, 64) if err != nil { klog.Exitf("invalid CP size %q: %v", bits[1], err) } sourceRoot, err := base64.StdEncoding.DecodeString(bits[2]) if err != nil { klog.Exitf("invalid checkpoint roothash %q: %v", bits[2], err) } driver, err := posix.New(ctx, posix.Config{Path: *storageDir}) if err != nil { klog.Exitf("Failed to create new POSIX storage driver: %v", err) } // Create our Tessera migration target instance m, err := tessera.NewMigrationTarget(ctx, driver, tessera.NewMigrationOptions()) if err != nil { klog.Exitf("Failed to create new POSIX storage: %v", err) } if err := m.Migrate(context.Background(), *numWorkers, sourceSize, sourceRoot, src.ReadEntryBundle); err != nil { klog.Exitf("Migrate failed: %v", err) } } transparency-dev-tessera-3cb22ee/cmd/experimental/mirror/000077500000000000000000000000001511600621500237015ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/experimental/mirror/internal/000077500000000000000000000000001511600621500255155ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/experimental/mirror/internal/mirror.go000066400000000000000000000155141511600621500273640ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package mirror provides support for the infrastructure-specific mirror tools. package mirror import ( "context" "errors" "fmt" "iter" "log" "os" "sync/atomic" "github.com/avast/retry-go/v4" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/internal/parse" "golang.org/x/sync/errgroup" ) // Target describes a type which can store log static resources. type Target interface { ReadCheckpoint(ctx context.Context) ([]byte, error) WriteCheckpoint(ctx context.Context, data []byte) error WriteTile(ctx context.Context, l, i uint64, p uint8, data []byte) error WriteEntryBundle(ctx context.Context, i uint64, p uint8, data []byte) error } // Source describes a type which can fetch static resources from a source log, like // the .*Fetcher implementations in the client package. type Source interface { ReadCheckpoint(ctx context.Context) ([]byte, error) ReadTile(ctx context.Context, l, i uint64, p uint8) ([]byte, error) ReadEntryBundle(_ context.Context, i uint64, p uint8) ([]byte, error) } // Mirror is a struct which knows how to use the Src and Store functions to copy a tlog-tiles compliant // log from one location to another. // // The checkpoint will only be stored once all static resources have been successfully copied. // Errors fetching or storing operations will cause the operation to be retried a few times before eventually giving up. // // Note that this function _only copies the data_; no self-consistency or correctness checking of // the copied tiles/entries/checkpoint is undertaken. type Mirror struct { NumWorkers uint Source Source Target Target totalResources uint64 resourcesFetched atomic.Uint64 } // Run performs the copy operation. // // This is a long-lived operation, returning only once ctx becomes Done, the copy is completed, // or an error occurs during the operation. func (m *Mirror) Run(ctx context.Context) error { sourceCP, sourceSize, err := fetchAndParseCP(ctx, m.Source.ReadCheckpoint) if err != nil { return fmt.Errorf("failed to fetch source checkpoint size: %v", err) } _, targetSize, err := fetchAndParseCP(ctx, m.Target.ReadCheckpoint) if err != nil && !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to read checkpoint in target: %v", err) } delta := sourceSize - targetSize stride := delta / uint64(m.NumWorkers) if r := stride % layout.TileWidth; r != 0 { stride += (layout.TileWidth - r) } log.Printf("Source log size: %d, target log size: %d, ∆ %d, stride: %d", sourceSize, targetSize, delta, stride) if delta == 0 { return nil } m.totalResources = calcNumResources(sourceSize, targetSize, stride) m.resourcesFetched = atomic.Uint64{} work := make(chan job, m.NumWorkers) go func() { defer close(work) for j := range jobs(sourceSize, targetSize, stride) { select { case <-ctx.Done(): return case work <- j: log.Printf("Job: %s", j) } } log.Println("No more work") }() g := errgroup.Group{} for i := range m.NumWorkers { g.Go(func() error { for j := range work { log.Printf("Worker %d: working on %s", i, j) for ri := range layout.Range(j.from, j.N, sourceSize>>(j.level*layout.TileHeight)) { if err := retry.Do(m.copyTile(ctx, j.level, ri.Index, ri.Partial)); err != nil { log.Println(err.Error()) return err } if j.level == 0 { if err := retry.Do(m.copyBundle(ctx, ri.Index, ri.Partial)); err != nil { log.Println(err.Error()) return err } } } } return nil }) } if err := g.Wait(); err != nil { return fmt.Errorf("failed to migrate static resources: %v", err) } return m.Target.WriteCheckpoint(ctx, sourceCP) } // Progress returns the total number of resources present in the source log, and the number of resources // successfully copied to the destination so far. func (m *Mirror) Progress() (uint64, uint64) { return m.totalResources, m.resourcesFetched.Load() } // copyTile reads a tile from the source log and stores it into the same location in the destination log. func (m *Mirror) copyTile(ctx context.Context, l, i uint64, p uint8) func() error { return func() error { d, err := m.Source.ReadTile(ctx, l, i, p) if err != nil { log.Println(err.Error()) return err } if err := m.Target.WriteTile(ctx, l, i, p, d); err != nil { return err } m.resourcesFetched.Add(1) return nil } } // copyBundle reads an entry bundle from the source log and stores it into the same location in the destination log. func (m *Mirror) copyBundle(ctx context.Context, i uint64, p uint8) func() error { return func() error { d, err := m.Source.ReadEntryBundle(ctx, i, p) if err != nil { log.Println(err.Error()) return err } if err := m.Target.WriteEntryBundle(ctx, i, p, d); err != nil { return err } m.resourcesFetched.Add(1) return nil } } type job struct { level, from, N uint64 } func (j job) String() string { return fmt.Sprintf("Level: %d, Range: [%d, %d)", j.level, j.from, j.from+j.N) } func jobs(srcSize, targetSize, stride uint64) iter.Seq[job] { return func(yield func(job) bool) { for start, ext, l := targetSize, srcSize, uint64(0); ext > 0; start, ext, l = start>>layout.TileHeight, uint64(ext>>layout.TileHeight), l+1 { for from := start; from < ext; { N := stride // If we're starting from a partial tile, then just fetch the remainder first so we're tile-aligned from then on. if r := from % layout.TileWidth; r != 0 { N = layout.TileWidth - r } N = min(N, ext-from) if !yield(job{level: l, from: from, N: N}) { return } from = from + N } } } } // calcNumResources calculates the number of new static resources which need to be mirrored, given the // size of the source and target. func calcNumResources(srcSize, targetSize, stride uint64) uint64 { leafBundles := uint64(0) tiles := uint64(0) for j := range jobs(srcSize, targetSize, stride) { nTiles := (j.N + layout.TileWidth - 1) / layout.TileWidth tiles += nTiles if j.level == 0 { leafBundles += nTiles } } return leafBundles + tiles } func fetchAndParseCP(ctx context.Context, f func(context.Context) ([]byte, error)) ([]byte, uint64, error) { cp, err := f(ctx) if err != nil { return nil, 0, err } _, size, _, err := parse.CheckpointUnsafe(cp) return cp, size, err } transparency-dev-tessera-3cb22ee/cmd/experimental/mirror/posix/000077500000000000000000000000001511600621500250435ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/experimental/mirror/posix/main.go000066400000000000000000000062401511600621500263200ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // mirror/posix is a command-line tool for mirroring a tlog-tiles compliant log // into a POSIX filesystem. package main import ( "context" "flag" "net/url" "os" "path/filepath" "time" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/client" mirror "github.com/transparency-dev/tessera/cmd/experimental/mirror/internal" "k8s.io/klog/v2" ) var ( storageDir = flag.String("storage_dir", "", "Root directory to store log data.") sourceURL = flag.String("source_url", "", "Base URL for the source log.") numWorkers = flag.Uint("num_workers", 30, "Number of migration worker goroutines.") ) func main() { klog.InitFlags(nil) klog.CopyStandardLogTo("INFO") flag.Parse() ctx := context.Background() srcURL, err := url.Parse(*sourceURL) if err != nil { klog.Exitf("Invalid --source_url %q: %v", *sourceURL, err) } src, err := client.NewHTTPFetcher(srcURL, nil) if err != nil { klog.Exitf("Failed to create HTTP fetcher: %v", err) } m := &mirror.Mirror{ NumWorkers: *numWorkers, Source: src, Target: &posixTarget{root: *storageDir}, } // Print out stats. go func() { t := time.NewTicker(time.Second) for { select { case <-ctx.Done(): return case <-t.C: printProgress(m.Progress) } } }() if err := m.Run(ctx); err != nil { klog.Exitf("Failed to mirror log: %v", err) } printProgress(m.Progress) klog.Info("Log mirrored successfully.") } func printProgress(f func() (uint64, uint64)) { total, done := f() p := float64(done*100) / float64(total) // Let's just say we're 100% done if we've completed no work when nothing needed doing. if total == done && done == 0 { p = 100.0 } klog.Infof("Progress: %d of %d resources (%0.2f%%)", done, total, p) } type posixTarget struct { root string } func (s *posixTarget) ReadCheckpoint(_ context.Context) ([]byte, error) { return os.ReadFile(filepath.Join(s.root, layout.CheckpointPath)) } func (s *posixTarget) WriteCheckpoint(_ context.Context, d []byte) error { return s.store(layout.CheckpointPath, d) } func (s *posixTarget) WriteTile(_ context.Context, l, i uint64, p uint8, d []byte) error { return s.store(layout.TilePath(l, i, p), d) } func (s *posixTarget) WriteEntryBundle(_ context.Context, i uint64, p uint8, d []byte) error { return s.store(layout.EntriesPath(i, p), d) } func (s *posixTarget) store(p string, d []byte) (err error) { fp := filepath.Join(s.root, p) if err := os.MkdirAll(filepath.Dir(fp), 0o755); err != nil { return err } if err := os.WriteFile(fp, d, 0o644); err != nil { return err } return nil } transparency-dev-tessera-3cb22ee/cmd/fsck/000077500000000000000000000000001511600621500206205ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/fsck/README.md000066400000000000000000000016651511600621500221070ustar00rootroot00000000000000# fsck `fsck` is a simple tool for verifying the integrity of a [`tlog-tiles`](https://c2sp.org/tlog-tiles) log. It is so-named as a nod towards the 'nix tools which perform a similar job for filesystems. Note, however, that this tool is generally applicable for all tlog-tile instances accessible via a HTTP, not just those which _happen_ to be backed by a POSIX filesystem. ## Usage The tool is provided the URL of the log to check, and will attempt to re-derive the claimed root hash from the log's `checkpoint`, as well as the contents of all tiles implied by the tree size it contains. It can be run with the following command: ```bash $ go run github.com/transparency-dev/tessera/cmd/fsck@main --storage_url=http://localhost:2024/ --public_key=tessera.pub ``` ![demo of fsck terminal ui](./tui.gif) Optional flags may be used to control the amount of parallelism used during the process, run the tool with `--help` for more details. transparency-dev-tessera-3cb22ee/cmd/fsck/internal/000077500000000000000000000000001511600621500224345ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/fsck/internal/tui/000077500000000000000000000000001511600621500232355ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/fsck/internal/tui/app.go000066400000000000000000000063431511600621500243520ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package tui provides a Bubbletea-based TUI for the fsck command. package tui import ( "bufio" "context" "flag" "io" "time" "github.com/transparency-dev/tessera/cmd/fsck/tui" "github.com/transparency-dev/tessera/fsck" "k8s.io/klog/v2" tea "github.com/charmbracelet/bubbletea" ) // RunApp runs the TUI app, using the provided fsck instance to fetch updates from to populate the UI. func RunApp(ctx context.Context, f *fsck.Fsck) error { m := newAppModel() p := tea.NewProgram(m) // Redirect logging so as to appear above the UI _ = flag.Set("logtostderr", "false") _ = flag.Set("alsologtostderr", "false") r, w := io.Pipe() klog.SetOutput(w) go func() { s := bufio.NewScanner(r) for s.Scan() { p.Send(tea.Println(s.Text())()) } }() // Send periodic status updates to the UI from fsck. go func() { for { select { case <-ctx.Done(): // Have the UI update one last time to show where we got to (this helps ensure we see 100% // on the progress bars if we're exiting because the fsck has completed). p.Send(tui.FsckPanelUpdateCmd(f.Status())()) // Give the UI a bit of time to render... <-time.After(100 * time.Millisecond) // And then we're out. p.Send(tea.Quit()) case <-time.After(100 * time.Millisecond): p.Send(tui.FsckPanelUpdateCmd(f.Status())()) } } }() if _, err := p.Run(); err != nil { return err } return nil } // newAppModel creates a new BubbleTea model for the TUI. func newAppModel() *appModel { r := &appModel{ fsckPanel: tui.NewFsckPanel(), } return r } // appModel represents the UI model for the FSCK TUI. type appModel struct { // fsckPanel displays information about the fsck operation. fsckPanel *tui.FsckPanel // width is the width of the app window width int } // Init is called by Bubbleteam early on to set up the app. func (m *appModel) Init() tea.Cmd { return nil } // Update is called by Bubbletea to handle events. func (m *appModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.KeyMsg: // Handle user input. // Quit if they pressed Q, escape, or CTRL-C. switch msg.String() { case "q", "esc", "ctrl+c": return m, tea.Quit } return m, nil case tea.WindowSizeMsg: m.width = msg.Width var cmd tea.Cmd _, cmd = m.fsckPanel.Update(msg) return m, cmd case tui.FsckPanelUpdateMsg: // Ignore empty updates if len(msg.Status.TileRanges) == 0 { return m, nil } var cmd tea.Cmd _, cmd = m.fsckPanel.Update(msg) return m, cmd default: return m, nil } } // View is called by Bubbletea to render the UI components. func (m *appModel) View() string { return m.fsckPanel.View() } transparency-dev-tessera-3cb22ee/cmd/fsck/main.go000066400000000000000000000112341511600621500220740ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // fsck is a command-line tool for checking the integrity of a tlog-tiles based log. package main import ( "context" "flag" "fmt" "net/url" "os" "time" f_note "github.com/transparency-dev/formats/note" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/cmd/fsck/internal/tui" "github.com/transparency-dev/tessera/fsck" "golang.org/x/mod/sumdb/note" "golang.org/x/time/rate" "k8s.io/klog/v2" ) var ( storageURL = flag.String("storage_url", "", "Base tlog-tiles URL") bearerToken = flag.String("bearer_token", "", "The bearer token for authorizing HTTP requests to the storage URL, if needed") N = flag.Uint("N", 1, "The number of workers to use when fetching/comparing resources") origin = flag.String("origin", "", "Origin of the log to check, if unset, will use the name of the provided public key") pubKey = flag.String("public_key", "", "Path to a file containing the log's public key") qps = flag.Float64("qps", 0, "Max QPS to send to the target log. Set to zero for unlimited") ui = flag.Bool("ui", true, "Set to true to use a TUI to display progress, or false for logging") ) func main() { klog.InitFlags(nil) flag.Parse() ctx, cancel := context.WithCancel(context.Background()) logURL, err := url.Parse(*storageURL) if err != nil { klog.Exitf("Invalid --storage_url %q: %v", *storageURL, err) } var src fsck.Fetcher if logURL.Scheme == "file" { src = &client.FileFetcher{ Root: logURL.Path, } } else { httpSrc, err := client.NewHTTPFetcher(logURL, nil) if err != nil { klog.Exitf("Failed to create HTTP fetcher: %v", err) } if *bearerToken != "" { httpSrc.SetAuthorizationHeader(fmt.Sprintf("Bearer %s", *bearerToken)) } src = httpSrc } if *qps > 0 { src = &rateLimitedSrc{ rl: rate.NewLimiter(rate.Limit(*qps), 10), delegate: src, } } v := verifierFromFlags() if *origin == "" { *origin = v.Name() } f := fsck.New(*origin, v, src, defaultMerkleLeafHasher, fsck.Opts{N: *N}) go func() { if err := f.Check(ctx); err != nil { klog.Errorf("fsck failed: %v", err) } klog.V(1).Infof("Completed ranges:\n%s", f.Status()) cancel() }() if *ui { if err := tui.RunApp(ctx, f); err != nil { klog.Errorf("App exited: %v", err) } // User may have exited the UI, cancel the context to signal to everything else. cancel() } else { for { select { case <-ctx.Done(): return case <-time.After(time.Second): klog.V(1).Infof("Ranges:\n%s", f.Status()) } } } } // defaultMerkleLeafHasher parses a C2SP tlog-tile bundle and returns the Merkle leaf hashes of each entry it contains. func defaultMerkleLeafHasher(bundle []byte) ([][]byte, error) { eb := &api.EntryBundle{} if err := eb.UnmarshalText(bundle); err != nil { return nil, fmt.Errorf("unmarshal: %v", err) } r := make([][]byte, 0, len(eb.Entries)) for _, e := range eb.Entries { h := rfc6962.DefaultHasher.HashLeaf(e) r = append(r, h[:]) } return r, nil } func verifierFromFlags() note.Verifier { if *pubKey == "" { klog.Exit("Must provide the --public_key flag") } b, err := os.ReadFile(*pubKey) if err != nil { klog.Exitf("Failed to read verifier from %q: %v", *pubKey, err) } v, err := f_note.NewVerifier(string(b)) if err != nil { klog.Exitf("Invalid verifier in %q: %v", *pubKey, err) } return v } type rateLimitedSrc struct { rl *rate.Limiter delegate fsck.Fetcher } func (r *rateLimitedSrc) ReadCheckpoint(ctx context.Context) ([]byte, error) { if err := r.rl.Wait(ctx); err != nil { return nil, err } return r.delegate.ReadCheckpoint(ctx) } func (r *rateLimitedSrc) ReadTile(ctx context.Context, l, i uint64, p uint8) ([]byte, error) { if err := r.rl.Wait(ctx); err != nil { return nil, err } return r.delegate.ReadTile(ctx, l, i, p) } func (r *rateLimitedSrc) ReadEntryBundle(ctx context.Context, i uint64, p uint8) ([]byte, error) { if err := r.rl.Wait(ctx); err != nil { return nil, err } return r.delegate.ReadEntryBundle(ctx, i, p) } transparency-dev-tessera-3cb22ee/cmd/fsck/tui.gif000066400000000000000000015103431511600621500221170ustar00rootroot00000000000000GIF89aX1   '+!$ZJ+(,/!!!!&""#"#2($$$$&#%%%%<_&&&'''(('))))5)*++++,+,9----...///00022233333^[434444@44K55556667777D78N[999;;;;F;<<<>>>A@@@@@O>AAABKRBbBCCCDTED]CEEEEaCEfEF[EGGGGlFHHHH[[IIIIQGJJJJRIJkLKKKKZJL6-LLLLjKMZLNNNNpMOLPPPPePRRRSSSTnSVSVVVVvVWWWXXXYYYZW[ZZ[[[[yZ]\^^^^k_^r_```aaaa~abbbcsdeedfffggggghhhiiikkkkjlllmmmnnnonpppqssssttuuuxxxzzzz{zz{{{}}}~}֊dVݑiWŒq! NETSCAPE2.0!,X   '+!$ZJ+(,/!!!!&""#"#2($$$$&#%%%%<_&&&'''(('))))5)*++++,+,9----...///00022233333^[434444@44K55556667777D78N[999;;;;F;<<<>>>A@@@@@O>AAABKRBbBCCCDTED]CEEEEaCEfEF[EGGGGlFHHHH[[IIIIQGJJJJRIJkLKKKKZJL6-LLLLjKMZLNNNNpMOLPPPPePRRRSSSTnSVSVVVVvVWWWXXXYYYZW[ZZ[[[[yZ]\^^^^k_^r_```aaaa~abbbcsdeedfffggggghhhiiikkkkjlllmmmnnnonpppqssssttuuuxxxzzzz{zz{{{}}}~}֊dVݑiWŒq?H*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴӧPJJիXjʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TViXf\v`)dihlp)tix|矀*蠄j衈&袌6*aϤfz(R:j*jPjCz饣f2j亁,4*lT&k~R$D!(kFm61-vX`֖Bۮ*Zn"4ln?Yj - ܩ,LB}PnB:; }1sq,9mc{*ll@X(,?t!x0G7Tk\>Ћ?-v??F+6+D;A w @߀.n'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wogw/o觯/o HL:'H Z̠7z GH(L W0 gH8̡w@ H"HL&:PH*ZX̢.z` H2hL6pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @JЂMBІ:D'JъZͨF7юz HGJҒ(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ^׾ `KMb:d'KZͬf7z hGKҚMjWֺlgKͭnw pKMr:ЍtKZͮvz xKMz|Kͯ~LN;'L [ΰ7{ GL(NW0gL8αw@L"HN&;PL*[Xβ.{`L2hN6psp!,W!,MBD?HP࿃*\Ȱ!C HG> jణǏ,WŌ \2}%-bLM"%w@ 0p,&0X0 2Q KRӨ!Ё[  34Kw cGXEN >-"#l`,\:V/*Yt&[zE{. ͻm[= Vy#A'rl ȱWo٭vD$jr&BDq]QAc8 PۧH ZV$@ՍMNVI¹yF# b Q%\N$86 !: |{L!?'G R Q$XP:D6abq^o9DP "s+~@lhȌ5IEӣ@ Pnt!#ߒ8d:LL `B$:uQ(0=? "N9.PJ"E=Ip8ހ8 C/!F/^f"$lH9dJNszJ%3u"s:A9D[8񨱖#9lQ#vv/ !U"\a &prCU & d!F)O3 dp:v}SV&'Kee@݉R+xB%tH 1JC;\4tP$?TWM5(U(N?}S8hJg)P?T$6Px'P@U!,B?Hp࿃xۗC~ (a…F8fQ"GIW$eŘ!g`CJ#K|3gЈW,,RcClڰDxui >mI~ l'-1WcR𲂣}TKf$phۼU#ViajӞ>= ׏z 2#Vb~'?E,(D!,B?HP࿃'tᗯÆ" J0‡#JPa`Ń b(qh<,Prd>|N,/)Pnl" a Z$h^$2|9sVD9?(̰Tg !Ldu"C8XŰA薍,"C*,H?m 1JLH:X6PԢ 3 R6=J9O#*HtvOcS8$[2bys0xDhP94qԁ^;TÈs`]0Y*\!;A y$0?J 6I ?Tha<aE * )<=? ,O@!,B1?HP࿃*\Ё~"JWOBj!D)ZHPa ETha  YdI‘!ep@|Ŕ A [YrOE3Ӊ5?t`AV`@@8YU&@xaʼn2g`8wC==k'A 3ȭ"A1.`|" XXΜ7u!ao#"r"[>{PiECJv:t8 [Z &D@b)EXx"89TTA`2A]M8XBkա%cڑ" 7+/`^ |"\x)VQ#;$8]UN/aAĖyXCG#c8x;V³vpEܪ1b a2oFlYV:b.NR.ی &| c} ۫ed0~Oucl(CWjEgAEB8bdp'H(,U6"7ⰓN:r@<,:缲B CM, yAp &{t&PC84dAeLa#M9UUЅ TPt Y5jar 5NIE%ϬcOBk믫|"]r$$8!,B(?HP࿃*7۾|#{U 3"l0"ĉ/ԨǏE$I*ld>11jts u3 ϖaS Ú/S(pʝ[*є",$(@ ":((KV¿̰Δ6aJC:uÖHHFDp*1e#P:  34m]FplF6(C1$>m{ƉmdPR?dXμ@~;v"q:hF yxʢ!xԹ*:tk*NwI.}HTF?=M]pld_lQU28Dxχ fdA$~B8 *0  @!,B1?HP࿃*\0A}"J'JAj!D)Z80C +H0Ä= Y@퉠0gL7RSФ3TFZ:QrʱǢJ+l"(P@V;"!,4B?8,~*m*LÂ'Rlxĉ; 1_H",`PI'%D9.2s @'H H3C*K^}\Q2jG$LCԒ! jణǏ,٧ċ70!ȗ ;TP$I18aK8G` =8AJW BiĔa VR ӊNwFCV`""HHlGFviɦSn H!7v֐ثܺeDAA3|1_$\.҄E8TȽ۲PFC=xօTir/N32i;V:tX]-:`1 EQǔrlU+tN9u@`E5*xK@*9P}. - ek[s:=LpPcΐ"}g* 9d:\1ShUH ~X]s=\ $8mCL4=rKeʲI`,W J4`s,QU# /@? P 5G@ç0Ql TPA!A۔CM'R"I)W`R6G2)(D'UI V`pCZ=P:I :S3GTD8xqI:A8Qh8A&x" T!3HPZ{BFHgYh@hs<3W'F"80@D >`I=L*Ds[/sA %!܌S5EMunЁxf"0U.*rBe+$*@B(P(Vfw@i!,tB?Hp࿃xۗC~ (a…F8A 3(#A@ѤA*E$8hT@2# BQ8uτ d;zh2$Jgʓ(8 U(l|@s Ԭ9DV7fQNԁ:!94 XsiևD+ϵ g6իWʈ*nPdnYX t81Ü, T{Ga)z1BCHsA HwҩtعtQ"}%C%!&n!Bh'iP@,} LelE ֒( 0N?b? bEE++xD(@ id!,B(?HP࿃*G"ÇmP⇅Q"E3f1b \H$)dY-8X@ ;HX@BH-9#4ջ͊Ν3"#>zL5ƐEܹWD1*{ˎ{5’v+!jljgE+D8CN 9;#:PC Bc K:? B!1ZGhV{YQ!y̙7'8M8hm|EtȀhNP(5V^yZ7d@$7NH98 Ux I3TG9ۈFxrԁ:xGA< H)d( 'Hr @eSz$Dk" SQ8OMY BR2TtLf>RJ|fϠgP裎"2PfiO!,BD}H*\ȰÆӗb}4Ǐ Ehbƍ \9ʑKbԘʏ^#RbL3QpcP$)$A 0 0X  * h9S8f|Af8޶)"6dh3r<0ʶC2la;ۙv(T;s=*dҽv35`s"X0Βr:G.[x"ƌ)0@&u^8U1(EPDmފ(bO Gɠ^|O,PpigAwߕJ8p)3z@9c>WBF Rf') pLE8#*ȢtP2pȈ%:rb:.:&A7BȢC"(3'1@#]# P%CJ N&%\NuM9W9ZUF8BC{M ;rq >"YrWN:C̖q>~8)&Î<ޔ?lgЍ%0Pt@h0C &h"H7Wߢ%#D饎"RD&{@yE?ԡUν9YN\/1BjܒJ.$ыѰ3"K?t7D@ J@śAM `"j, ‰@[&COA"6eTߎDrnL nV"-lRw7HK᷀/ Q%Y ,\Ӄ'Œ@$F8 e<$D9da9 JHe7ꤓ90P;@'3O4Q\'lB {` 3tP {luN,ŔO,@% h՝<4TBvfS&T*3@Uzeʩ%C jk !,B1?HP࿃*\,~"Jm@j!D)ZHɆAHɓUrK)UQ dV`p,8XBǜ Evp[-!cžnz3ee1Ċ  wܲs,vԫy"rnCWw 㦃^8[Æ{nu8 4+0e !ex1k+T‰+:|` 0u N ‘`5ڦUp4Ώhg c9p281cN9Ӄ}2#z7P6r*"Ê 2+(}I1d.vpB2D9M 1TQ )@17? 1EI]b%Wf+Sq 6SENZr!_n 訓; ˴9ȂN; jiQIQel$RdlRF9Ai (~X\,}m L 9`T%4ՔF4 )!T eH5m%cO/3PϽ#nP~ E+$0+JR A dq !,-B(?HP࿃*|#s 3"l0"ĉ/ԨǏE$I*ld>130p΃ $LXӥGTN퐄 B!bK9K!/JcR FY\={ɳѵQ:,["( Ѷ0zwA {mg,T1èDS$D.'rZ傼"'Hv7 [LRBr0Qe^@B)s "r+w]䲋3woS^) G"X$eY}1u  D? L]ˉD(ad7v "㡇ܲ&="?Xxˌ$xʏ@r@:#$L B@!,IB(?HP࿃*_ѡ 3&pd'Va5&,)a„ CR<|b$T/ D $cL-,1eύΔ;TcϛCB8<:3)xpX2oV#H#'y "^Pѷ30n[IW[e7/܏[w!P.]Z)G?$YuܧH*t].!? [ݲ0L}TERJ6Z :-:zczME*L햂 CGDZ1 3Vwx@d`6\yBWŝ5dЃ"?(fBPxߋƸAytXlUp#KnK(Pe1PHP TpW_1A [FFőbFr$@6c`#̑BO B"0P@ \"!,B?8pA$~*N*LÂ'Rlxĉ; 1_3DX`+,u$P1fCh-~Ps .G8?{N >)b€سP " !,BD?HP࿃*\ȰaC"J/˄?8ȱA%V, P!ɇ!EZ,iqfdp/9+SG phȢ,8A G.PgE,`(v`2P`Gf&VMÌ[=0CxHtP`j^>{^p9sLpir$"&E"ԑg/2sSDS9T-p)@u(+"2NE"S9H:Ԭ'K4C"N9CGE"80Y&-$:c\㘈)u"H / ?z(Uj SDG`ÃJYC8'H61B E aǵYEH9ꤓ>0^=hP~^|9R$?RFB#k=9 z)!, B}H`r)\O LP!?P Z aE!fXՍF?滘CWl#KAmЁ=|!Q hEW򆎔@)> 9,q>^tWh̪H2j-2,pq UHt""Lt.Ȗ[% d ATvLU gx%ׁb Ȟ[@ ~(Wjǻ?S SȑzUF?E((D!,BC?HP࿃*\Ȱ!HQ;3~pȱ%Rh $d(KG(Ё$v8 'ǐ2hzT!PCQH ,a Z#0aL;%]ՁBg ,T-p,uzvdlZ{]eL BRhݳDv0§imB@RA=I` 2D6UY98 Ĕ`wr3t@뜺h I`A*΍XD]9D`pPrʃbe H}31U쑀v8D1 0¸#/0P}:&!@#` Q%-.ATR?SVA ءW {MЋ:r >tL u +MF=, Д<ޔ]#; je!HX0/%Y\فWXB !(HuuXaVWb@"QidPC$ C {1d&PzϸcO6!"x{[bォ|29$0 $p !,KB?8~*̷F-٧oa†?d`A` ZC9@-`\ !Gbνo$\QHB%ȱ+hqfxȒt)ЇvȔ?:OWLSl, D\tJ.8DrĘceNi۽E8K1y ɰdYr׎U4"R=z: т-qXuI C!"FjhADy"A+gt’~kc 8^O~޿H}!,TBD?Hp࿃*\Ȱ!H(3 tȱ%RhQAfˎ!GhhJRυ1e\d `聚ӃAG- sތ4} ;,0jŋ"$P^۲!HI㜜mG(Qf|؃Ǹ&h; "C(?'tQH0IvFŀ TSHGFZE~?V) 4,WXBaJ`-W"(P@;"!,B?HP࿃ᗯC}$a…F$aB*d1D'\)GF4Q`L1xRfɓ8z΁0}f<Ⴥ%iQF 6>ZNQ=1,RHriĦΦZ1l۫4>U̓efJ7X(?Dΐ"_av2X[UrC ҉u;\G>!j+,P`䘞qSwlǯs~" 猰’WkGEjET  !,B(?HP࿃*7>}"Fl@jDĉ1ܸ#ď-HRɏRdRK!gf$i "pxRL$#n7Y+yĩr Cʑ[MRTiAALhm"W&r^)sq .G‹lce=&u/~Bu3+NX a!Ah3,)"ƌ3bhl#gbO5j;=l:VyX;Nu0$ԕHTc+WX1sZX!NltzURK0FQ䚃Ax`M63P? 6?b ʋ0Ba( t@Z c!,B1?HP࿃*\p}"J@j!D)ZH2t|2dE%Mj坣X sL S'HDLp` 3DX@ 23ifΏG+) ۳r$AY;G(BxH@@7}XSѲR >n;铥#CyhsTזD>ҋ QF1bgÔ߿IOr:YZU0(h-;@L3/M-?"HPI3LsGCc΀RBC(2AFV{t|A8J/x3 bl 7C>bA7TaExRNt$@Hl@G;1'0v=0 * >񈽍;Y# h>̲t@,31BF NN#ۜc3E O;7Æ ;I "@ 8LS|2j9>C, B@,/+9B 2'% 8WLQBdADэ$ԑK9a9Qh ǔJ".Cx6V!t#u6[8^@#d2,E:* `8̡wC\[0{8c jD !F8Q G/#1! 84G!`K7Tc =4!{q~0HqKT43a d`7Hܢu}` ђ p#!c.aQ\a& ~"{rl0+=^q#Р*8ZȁHҧ, *0D< Lȁ -Cy#1P(ӉX@51@j-ehn*~/ p,#4pcz.t|T{ dࡧIMSjFf=8p-}Z&P#/'hT`"@ 8#x"}գcL`CX;oÊB=~V$.${ PQ@/a0B$8t {l0/32yŽ@iHL zJ W0+lPE* E43Pd@T,OmH&pCxhHz-0L\a:Jq p o@2 ,~e{ "CuP&`/YF4` y2zula-C躏0xE9P p@#"QSYY0 *'HY x,@1E cBF"vR7F6/OE8Wp<`'7h 107$pCȡP06@$.'!/~0IVO@lߥIpCA' VK 7@H C "A1ȡ7@C54 0W @P 8- `0ψ'v` rPLl3h抮^,p"J: t`&P G3>pR@ `lPkX 1p xp> :x@* t٨@ B0  ҵkIt10pPI[`2p ` e`@ ̪BeUq!@$H*C[ @ p A p {Ylt 5U b HWPK܀̮s= `R02QE QI [p`{ bp` / 5np<PhSP@{W% >؂=BE p <a>`p \4ŠJC۰-,)RpWufw= `Nxp  PWd @ CJI`V ~t:0V`.+Pŋeqz' *4e F@op }mZAEp{` Pp47>Sp M`Y$p 5T}8  ɂZ|eȬ;ۻ-b jaد)㴦W10| 'SF (GQPZ'<PLPv Z ?ќy[$]p)++- 1=ӈPWRR^B Q0^eВf pPĊH93AaWLLpdHR ?G@`@ Ů܄cHkSy*0k@2 HY%G|V$HT`x5GpʔPgweH`EaOBFh@ w@`0 e@ ȷk|_c9TE7^E @K p) N K 4 f8@H D5 p ?0 Va Y+G`hQ?96E`zGtSް@>+(q1`wScП4PyQE4KmB`xx`PFנB%d0ǘYNoe^qHr/+Q3N@EaNs  PaM$CnKc{u⟅ -A5męSN=}TPEETR 0E4mn[2Ѵe )pp&9iv6A*%a2 C8G8> { X7@mH0XB7Ih78 {dA ".0`+$a D ' =qCY+9R!R"OT P9\@9!edXX;o>^ D` ܸG:̀`M,PjܣHADu$'C9J E =A #|2"*xAREL<<娀 ? @!8 @t07c 8'ó@ucܹL9\;@ :C@& @|KtŸ8;x. @a d;vȇ}0É*\1 H l<@A3}ks8#w4jQ 뢔E=oԉ 8m@4`|H)|hC0Bˆrp0#0V| ?nHRDʉpx|(͘!)R@pxx2?x0xE}ʀ0Cy(G! [&Jt`EyBz.(U, @J1n|@C:B ɀsG43HJ@@vx8 tE|HÝPS-(8PWx)E'T@hS0ۉ)8=BʨJGx{H؂r8Sz#O"zpHJ2nv0ȳI?Uh"sDpH8#ȼ Gs`-LGHpXd`RoqJ@LIJSa@ cPqPc\ 6@LJ w1@TPG?%)pv+PZ SXG:PFȂs!Co(-ptP!@0},)&zxLKu8O+ pQk A|px Ąt@8< t8tE@ 1ɥlrXI +ѩQR8 0ro8P1 "s } [^ x(D/p:0~0G8Qĉ8!xM-# xQ 0wXΔ( q <5(Mp+hB x|m 8(T2XSXMI, S ~xҘ7+exZ dB PU( Ox!zPpZ($DX S13zVpn^E#ER2!HXT @ `mU,3o-Ș |ce$x6 X7Xwl ooX6v `;N#ٜzRpP@$ npp`E0+B؆W( JM iDpHs 1ppj X 8tEQq+ PKY4,lHlX"[ n8uX&)hpr ) v00Woȇ,pCP?3xoP@Zw }!!xt(Hx0qUЁڷclhbUZ$ Qж͐-8bI%]Oy(SFyv6 nV_khn Hps(D` S8 -9OT^Ё䈣stIj8ad1 V@sx2gh,r|v#88K|HL8ap*b%qg"pSnrMR8r q (xH=K?W5 Є- ro$#h$PEXc q$2c |Ia{p\n]X(KHN ,X̀"XnU]#{P>},MF yH3x4 ptTQXwlQH!CMb܉@tT} m8W؆v@TLG.%UP 奲P`rzQP(dy@ N[r`Y #@-wݞ NU؜cW{Whq(`vz@cr`oP:X wr6C6sD"ÕlPV'`XfUx wkpor8duD#K9|L$W[=HqH{؃p`\ChWPJęRLTpPGoPX{X4Rf~[mEGU'@nw'[zYG!n=C]6Xzm8Z#bI`inIxz ҍ IR%,xX LЄS2;l[,YDh%(NGT+`^_fP.r8yo8PXq`Y XnQ8X4 (<&2Z-@Xs@PP9 YX0upl+xX(35?8 $gP0 9Xy""@!݂y =KsfV,erXbѝIpp>Ȯ {P|7P(I6SS`[0~ M(@iC 0n6 ȘRsx0Xvw{C$TP)tI ptH1>fQ8he\[hu&qt(+s06q ')qv0KH?pu^H?PQX Uko$mTX+so"Js0pr<}}-p^"`%u6h hshR%ah0z,z+j.Xr>(yTtkk8 ^Ȇ1?C0\iSx 8SHqݙsUw~B̥!kFPЄcX ~a7FvfGw1P@Uxx2R$hWEPP@mI&p"I! b! :2չtN6r8 )T{C21!{3: JybL8*[q+PÌC ǵ{֮('KBj„T!P9<q,89gO]C/,(X #y JwF݈ p ۔ P}""FoE\$ӹuNrOz.鶙ZQr; Nێ/Fu"{$Q}UmD7\q";<,D9fQxɣ83 QAl3F Re@)(PQ B2TcEvtb`lUZy%Yj%]z%a9&ey&ifar .#"Et5puQ$Lp +15IJ0 4@ $t[pcH[ c=1&XO8e$ -u0&d#"$ 7ƉPq:o9dm7p?Q0C4DO6, ]wTT9@ïP)N", 1>db=d@δAߔToVG\(5'KD!&u}x-3G"Fhvyd"@&<+<;|H==D1 ll1z) U~ĕ +* DK,0 DEG# `X` @&P d9^a) l(@ 9@ r  =S~AKC H)^! nCWX@T L @=$p,c8+hUB0%xM!Q}c 2@>08PK @0b6~p qaD`u Z A PD9$`/K C,I~=(!:ac @G?Cv; HFB'! np5葴&=)JSҕtKB0B$!# X~h+v|X˜pI AA,`s,IU AdvF@Bv'+D; G%' İR^M!$|ȁHe:X841İfAAx {L%ni6mQjX6x!I.e@]! t@V8HPbx=~K؃W=\ b)A?(CxE܂ #jSNeR$j|͍ў(,`_FK$n%Let@Z$A&\%K~ aLSSeb$]y'zz'{{ cp=,%[d{z 8'(&.(KU#x, <"2hA(@6lP((((Y@-&4hJ!H((vI$"萪00%>)l^l$JF)v~)"O,(&)jɎ4 8((И))h<9?:BuqI$7`= 6#\;A4V<-֮n-K(1<7 `9u:UC5t @a!INA$@^ 1-R&@E (df$*}&/2t!`C7L9!dhQ2H&PŢ0@6`08PC(7BzLm,w9܂AT~C/PD8G1 2H/\=4qj#=ԃ70SpAD/PC7qxmh:L6XQ:&mrB;\>C 2!g+@\v&9L5<H!`C60 \D C8m4*DlM@-,\9"z'&'D0313ȾI0*̍  %- SD83C4޼ ؓ;@ <3mw(+tCP&1@4AIƉ%`B؀DA$XB$ 7\AD|_f:B<tA9W"$9lh+B:"8>踤f:g &tT/:=[7^,joP~,7lPGeP:,%@= pB>h0vQaC4Hx>Ow-2VAAq%AjA8d9S/ԃҀ T >B 8Iho'>rw90NT @4ɥ;$[mW?q%& L@P@ !R܀%`DPb?TR)@@AA2@P E0pˆ#.p1C b9dI'QTeK/aƔ9fM7qԹgO?:hQG&UiSOF:jUWfպkW_;lYgѦUm[oƕ;n]wջo_<0 EIRĄDs$ 2`y3<ׁ#:Q |cӨhA41CS`PZ8JZ9 Бa QvPc 耵g]~mk bKX ) 1HBxAB*b%&`HPP$EO" DIC NE>?^ 4V7f@ LdEzp=:hC:A$"?8b@CK'# +$i+)2MHHI( hJ'8i1Fdi9ACR/ S8=LKP ?h@xA0& M@1e*@ *`@GtC) I: a KP}_:8H;$U}PQc#:xs9'SNTDH`z(qyO ep@qB^icvbdsعVz'UIGhxGipxϤ&2q$ʒXẃǛ.Rt0v'EfuH9Gx<<rp&fG`3 oYX'2PI'aed!gsGuPa^qdQ GǓvp@vH9l%@9x0Vg1j_~:Gstf }Ce:0K9 o!`+QH8!$V@܃HaCฅ t baF~`#L $b3OI&N &A\!ဃ± F h`DNvN 8tB@>pD:!BĄ<< s$! !͠8aE`oHɑ t@ψf@cVZd*#2DEpe#1@0?8(Ժq8$!e-FFr>!豇!@ `>!GCY WN('V9_=I? qlAR@$D!jbG<`MWw?aoGH ;a Nu(a-CؒaT # HA H`" (\  R #qpcRA0$cp :q 6^x 3a\~@yu`np!tA;:peCfTt# )qUb+6E8 ͙!t@CfuxE8PNXm&@Ý4<6"nB,+-9" "+†WD!"EYdc E7$@ !]B`u`m 4# R+EbޥQ-LS<> 2P+CpX p!>ؒ ^1EEd%/(G+YXF nt7)d# "1`@/Ic x:a{K5Tb#h'NHrV8(qCfP쉉Yı Qܣ "0pxC 8 7BX=@3`F0Z)@&`r|b53XE=pJC VwM Fޙ4 B< {`0$`D;@d䈇q wЃ "X+䁏yYT7 A`H ASw==`E" poP-Đ('A GPjCO"'B4ɶ9VA '=qdwcٚp6Nya#X:PflѤvM2P- aڡI2Jg@{>AHH :H2a4OL!=Hz!zPp1,aVn`8tJlV!@-^AzA0ALʠR 4f@\AQ11@-dX!N @ |! X ġB  *V Aj MzoOV>mAC!0a `F ~a#HaLr!o7~ck4V@a""# QAe O,  e$b a| ADBP hJ=A ơ 8 `z ":!2"IPͺ*  @AG6`8JQ>lPaH@PC6!c<$ "J 2%h21331aN#%A4^ 096a a"l$0b^a&1F @ a,8PB3tC/8qZDCa"/v"T4Bޒ $UR`,S315S55/@ ,& xc`S6%C 7tF ҍSR(ea A$^!Aġ+ 07|o<SP!ʡjA%WSD "Р:6b!_ԅAA /A|+!"Y5Z شb `Ē.pN P^a'-Ht$t!o" BmKWa#0 s0R-#&MtAAY$Ow/PA J2 heqS6ov-2* 8Jf a U]u:/Ad4,`$  a f)/"9G@"P~@ܾ*E"a" !!0`"Sy[`s  crr EvddhheFX82D=,nx#As`Bb@{0Yh{h$aLŲi8M  أ4g$RhY5uX"b $!blRHux:CAu@@,^AN=aH(zʡL,Z2RXH*n32Y^x{L؁A`"0p`f0!NaA2~hXna%!$!E$!Ap$Z2  4/TlYU]$azAo$6Z"T2^R%>r- %<(:NAD`Vo^Yh`!,vdV3cz@3aŹoMAHN$djeHA.2&!\2@n 3!أpA2{ - ޡ^%XS&g2ã9a|聡 x^3~vnGz@@ dm]A#` }ahAa- Ҭ!ez ب v*`z@ށP|Fݝ@/Z^M A r&Dt`70$^!z`4@h@s7xA`}wJ@?] 􉁊T:}7-&Obn)$NqCƒ^Ck A&j$@5Og8F–A[6m4,4fBHzVN!"7h,#=-#1fAC;-8`8N$X8!B*XB&8p0 2 8 K<2ʕ,[| 3̙4kڼ3Ν<{ 4СD)"vY*EJ rt<%Wzױd˚=6ڵlۺ} 7. 6ZeoYΞ_;x8Ō;~ 9ɔJ"n߽^8޾vN:լ[~ ;lI&=;ݼ{ <ċ?<̛;=ԫ[=ܻ{>˛?>ۻ?ۿ?`H`` .`>aNHa^ana~b"Hb&b*b.c2Hc6ވc:c>dBIdF( ,]4$K2b8MAKJpI@$ dLr)P(uj~dSɀBU`%u@%mUZJzRt:5Hh@FgI,iA}K*j*d٧FED(ʤ)@.䥲 L1 *Kg (' ,H"H'M+y哵nzR$Aˤ*;RT !, TL.`0Ac݆g00Q$gzְX qrU&LȺLl׆+vF$&Qܭ+ 1{\ )pb$FWg(xb#BVeYrACĀ&dA@$ơ\U= )w3$K\~!j1PB'"v$eA aZHX(ABty7 Hq 颐$La x ,'uY`!ԃ"Y,aBRHQlBXsݷ-$qTraA!'M>xx-j#) |w,C1 !ae^h L 0!:!юs3;СR"X:!g LhKc 9A p^8 %?ЍԺ @9#C8 ЮĭBJ@{cPtGPvh»+Y@86tpШ:Wu'{}^8^Q+D$8B8Aa9d@#ŭn%-lE!qC0D^#X WRbؽځ,y01;P,*@xp@R  A(w;0LlP2W6;N8'1U$Xb@ZXH9qD7q n,!\ 8Bzl@Inm(xU2Eq8|u:jUJYBJu`CVՆ=dA aH,€|) ~@o87|++3"0,$+ $uAB()" (cl 9c99Q"q8!pYrDbq=wl1ENrߊ0o0!@L4KL'jH@;p$p @e B¨p3q Qo 'Uu[G3V.W((7-i' ~/XH{P@[j0@C @ Rnu6$Pk. @# F `N1? &ӝe߉lJar07X' pphK ۰~Jݩ$pHF>@uf*Y pR3v$ݹR%eP]BY9gVI80 @ :'4 IHIj PgF @:P7IT0(7e0 Ζ& П =YlBl nP {pʴ4P0@>P {,x&QE:aW "H^@ 1@a0> {AP@u"0Pj:[+eT@ϐ p@ 90p $Ҧr&puVPSt< b ,QIR! pd0 )~p&nzz= @+&>*]pPGp$ `ÜPj%H >@[b` S e j dB` 6SlhP{bU&zKP0+h 気B'sjp 0 b*" @Sr6@k@e &5<@P/g' ]6YQ o|(B ″{w: @ <ؾ @ %JMl p[  ԀŢE1R&w s PG@ ౰wjVg3 P W :p@Ȇ&; qt2%AQ{(E ‼& M>Z Ulka p 0h  @  0J p!Ϡ!#-%M RV俟)''WPKQp` kZr N`&R qp 8\  m n>ۼ  E. :\| zt<(0† vM` ,<1Md4 0g@ 4HބeځPx@ r1 } * b YLJL kpIWL oSJ <-4wQԨɾ f `Uj @ @ e@ m ` u[ Н`~ u}.  z Pv+G}3Z / Pȡ= rOrؔ- ( .]Pu= e 03S * nWpp.+ `Ā (\:T:y R! AfO be >A^RݰH~`*فK_Oc@ T#=ڢM?@=  Nu8!^ ZSJʘһqW8W@ UpRnSH84hJ̫|:x'Ϧ<{ϒTOA%Zh INPA E-+pBs$aQuX@ @$^%H9L \GBr$=O7j80`ែW*@;DXHo2ۭ,.(|8 az~"$$q |{H6B[xU71 B'd pA#л1!nBEaq~Ds:ž,ǂ SV@qx-66)2*ȢL %`@!%931(Ŝ,JP`z)'zK^G! 1~Χ/G?Ɓ 0eq.F <@<ÜWʐ}H< Mq(`sV’v8쑥s ņ!"XOH(8q(::'Gh=*)E(G~2EyV\ lp`nbT)c<)' NeP+&bp2N.?1'0ehp>q l!+D2e6gAnf lu`=-?csU׽xԒp|4 p'?[™"Xl hO޹ւ%̹ 8qg%cQg)B80 T1e[c0[}焄Ʀm9BkyE,`@@2m"s=_z*А,DEqqD?:@VcA9`z:MCw{.c3hыb)qX V9P0":1C(=~1)X2uDy&(14mDNw*!-:`Dv3!2Au(!?9c'BU *d@c& Zeh=QPs4xE9V9d-Y>d(b EnL8[D XzH* (@@HzģzC`HyDc*re؃zٶUX@Vle-F2uK؇>0ឰcMA`[%@L zRra8G= H؀uX dȇ; $,( B؃3vӲ 0n bz-q $ ĹӛY);-z3F\ڃ:,Q<-0Q.":r  ZGj/T-$7 s`'1`0c210q)(Ё0 ȿS߳W9_CpXQvrp@8W@#`!2PA#JX&Þ'+l;)vӿQx<h$(1XsǸ8C9rЄ$#Y1:=Yp(|%GKEUl)hEWl# !K$ŠU+"SBr| % V1J)O:,X}$ɀ'|XC+$xP!p+=" } 2H9n w0-zB ʚ#HHL |C[H7 pzH(n]bt!P zG12P +0i}GXrhD 0PSGprL`= GX v@pz8@npApN[\Ẑ#z?Pgx"pA:xr8Pmq\(l;y@|8pI;ªʫʃhBL@pKCK2,NY=QRÚL P CdwNp81(EX u3l͆2±(T:,#LNY$Ȃ++؂#pULVe=! hVgmM")^VX $i :x$Z5 WlmXoxЄ2Wou F@w ul$xLHy l ƕcPJN |xiM]((vPzcHf8J؄m8` je||LX\{2+`{`x ٰHaE Gߩ Ξx|w1@֖~阮sKBʲp|$MHxV 0sMzXjk5I@0hY`u-{ h[i͌~eMxxxYX_^(\s } ЕAbpT tX$V9ȇ(Pp4p0nq+Ecѵ[qwRtMPʞ~kzR}󕸷6 Pl~[1>#DC`H[Dy`*S^XKhsq hP6܆{(!nOs rSvd.mP{S5yoIp,  0tRuH(#6DuЮt/ h+H,1.Ўvx,F^vأ0٢qP I)/OE`_[{z4(lھmZ DqR1h#h %Xq H:hTƫ3gK S'sx[|XnY?TXG zF=K8+0e9`Dt#r%9g'cpj?p}6;gxteքxx(ʰ`{=&_zЇe0#o@Qv83(m6 l9sp@k|(0n2K )t֜Z8氊+8JeȚ(sa_epjpXDH|z5StP xyGńH^]u@yd1x醗EMǮbp(p# h2B2{xx|ppCzhh8D0-HQ󄘞|Tp{`/>mP|WVx"z˟{\ x`''C+fU/| D-P`GA䐻W|"K^`We@=c' ^T`9! 5T<^i=x,tpkRS1<#CϟHQ"a%nb$dxۉ 3ɂPlh(?Q #3L[r[=̌۽tf d٭- 5p*7*S1:H(9?3;tP["NPa 9B'BEE@+ 4 rw3G݌ml$7ԓ!%-` 6eTR4"d/Fmv6,D, SsBQw!\q+@W9VKM, n;"}t8Y J<,5NLh[|ZCZ .UU2@=xB[J=Dc"ϕClrANA8l1!ѠEEcO:M洓;ڪjL :@JjѬ' [AeQ?Lk"XmK@fPY^TnPBJϲ+*nK"zڳ{b4RV`m.9`"f0o U6kρ,ЇB42P rQFԜ)I_z+XU˂CZgSañ T)Pon}Q1,V/E#حҴYUR gr구 tETUl8O[eѺz9[@ ›NiQzЦt!Α}8.iY%S-J^.Z5rO 4%]-k]*q5aCsqH !Lw XH0O DP'V7cM#*a^}xC2 5d$NCp#]PhPUxRoE 'lk{ط U >F E08MC%MXYlCu`P2Pb(f2W@7V 2x" p+f0)F,C(}FOdP@"! ?,4wmH@u q ap+ 59l 9 D=9@?BAT K&X `?A8>( ;,½ t _Z8x DA4lF\A;]+=$A 9 (!&0[8@t")(-dCQڊ\A8X b >H,C8 A#WբH)B*Y*B ,:(zD-2)#CY*d@ |:,Q)3D$!"WlB8!exB8lX@>>`-'$D<E(:B1-C;,C9|)8d : (1XCH@Ò6cFdr$,!l;=wn<S `&Pbj`\v\{#(_ mDe@` Bp b'-A& ,BubW('4NL0HT,7ަ=( D>ЂlΕp'<mn6'@6+BC4#(C4h/r(<[H7C7C/+-TEXZ+7h X֣6$2d @*B;81^iT8'ЀB:,'A<J (@;f[0@6 AØ/ ,:\=-E(phBΆs?T &>x`X,Jl 9҃:l X=B>58$@M6 =D@%dT+,A4+ jԐ=XC *8@D57d`c ,!Hn߸"a[9LA7?5I-P@3 mCu B=u*8= H9\7$P;ԃI{1 @ 8_L `: & =8#<`A8 L1 0lC4 t=[X„-A:RN14 9@t"ݥdXd B8@gnJ 2&`C8kvw/o@6PB9h)Cc$@$C88 @<\T'`DZB>,`D7P @䊁jw=, ,@6,~;MWtN8"5dC3S4Ks8!p96gtA9:w5)P,@4 S ŃE_=>GGH,9)IB?T&(>DZDdLtC?d2:3))'=C8hjyʃ\!TD9X6 |H'C<?X 0h29jD<7(deī:58j9\998t/8;BAc/$-:(CR)C(@@I-oW,@CH8 `16K@840:H}@;`:?2 0@SKv|05H2@ t(XX(@߽3Tn/\-D!}m$k:C9@/т$ `8Aę!$lu$x3^>v\8y0@l9A9,XA;zք:L0Ppn^%(eIP B 0+@?JPᅏs v.[0@sb,v P;+Ǣ!opk+Hox^I2IgL{f1HYQ1j^Y 6ge$9 ܘqp$*am iCX!p@{BqdBق!V&`K@#DÞxΉ@qQTc@x1ǜvq P!U|L YglÁ8(g :!ObA+*ȂI8<3  s{.3G(?QdN DyP5GTRpb=nq.$ %UL!q^y!4qIB<FEG1ʑ$ Pǒ b(Uh4 a *V fEā Hg 9As!"›{ҐCcyбT2Tfe$xPULXgꅐv*㪠ȡj*D8. l vbT[h, ,1^01aez9O2vy! !d &0?o! s[o.S`q@0̠^F Z# ԧ0OWAЀ=<`" `I!Y l`s|"hf8P!Fz|sDI mwbH\(`0(pmUAďB *f@w"u @ r:$0U,&5D/@1fHZ :a{%Cc @ VYaH!)pSMlt t"ju\tVD 1V90,f9 =X@}o<1tp WӁ)hp#/G+Skzz`o(,[ؕ0IZM`PXFVM )[49fC   B d@x4mjR#!"sd-7\GX](hT g(F;΋fo(VmBNp tĒ*w\E#ѥEAݴ Sytjqo>A}uNXF7_=k}5HA`)5qqܣA܈#ʐ(p[\Y@xG)ctc 1E@8Scr90 /zaP*'(hы\du@^g @WWJy 4!j/7(  @?B؃9L!B+<0B|vF`*@$$PI&/ iũ.hxg^+<>pBNPw:ЩOM^4 Vt첗ҕ.b9Р: PD9!NB9P1#1Y!>Hz%\7ۅȥ>- $x8V#Dwa0A Xys~7'ȱWKobPDP۱1 ,"˲9"AP?ҳs ظ)_ !#]ڮNdDYm ̱ܔb1蛛 Ip36bܨO~#HP@ jG(B/qTB@iPa ȇ0Rp ?C#X0:: 4o,ebؠ d#t-X (`r |2p fAl D`A:f 6@%\ ̡C`FlEѨ Bh`A .p@x C!ȁd:ځ,Rod0C~ ,l aV`)7f&A ڬ͠MNA@H́ zmA`AHJ@Fa D`AԊni@ B8 6~` ȁz8O 6dQ|<^a aή2'NNf@ W  fYDv# P/ zV zln! Hf #x/ edA --R-ג: aβa V!>Z!|>t 8aXzbAmI:VΡ>ENAYj?!42z`fae%ԁ.L XD.J!A!/^r@039@̡;Sd  fOLAkh`ڡpP!a%3aG"hA@ȡ 43$#;&>wdVX!a,  .%C!Q?%Ak2^ =h!a %0k 8`;% K! D AjH FtLĠZ5uQ6i6qС@f΁f!4hQN; AC-vda uABHD#@D&SA6#3NC4؁XS2$5)7ܒ6t TATIuQN5UW4: D@ff@ ^ ++NOȡZ0 K$T!`IA'Y?uTh>e@, /D` aV,h4 >!X ΁Ĕ-Vii4$A> @l6 &4 2I0(*?$!`.a4,`,6`mE ( 4a8. $4 $aS0a& vr *pC\( pAp'@ j9f@p9ah l.Aoˎ88a Lz0ၖEa Bzy_ 4 ar1.W<؂, ޗ1 `£ (=azT0W@ 0}K%>B~ | v & ~ ~7jvᄻ@~@`r# "}3tr8f `҇qD%r߷r , K7`$` W(!gH vAHЀ73^*ׄ!qf`b&y9AJ qs w,%7HAXHo]+`u-a N@ t 7ӣn6ȩ?S." &-A ( AbʀR au=!'@% aRlWy7$X|iZZi@I W|Up8ࠩE*`,B8ZKäJ$|uT:dzƠڤ7D :Z{ 7"Mz"`'hJzE&&|Ɍǂ& C;%eZ6:{zgؠ Te^dZ:s㡋H88κ9)㚨HzXO|{:7XZ#9zQ&ȺW0 %` t:+{3ZK{TLG2_[ݚ^`ZYD ˇZ- `@8 +(z{Z3zE<{,2@"㩛8"B@kHΒ/3\7;ÍKCx"S(2L!dÃt@@Kz_ Cl@`aD"u`To\+7s\ʧʫʯˑc [ g{)㧎^>T8?|La77^@z껜tf'1! ]ћf<V~{ѡ,^1d8F8^dd_c]g}7i}@u=-!-؋؏ٓ]ٟٗٛڣ]ڧګگ۳]۷ۻۿ]ǝ]ם]^ ^#^'+/3^7;?C^GKOR^W[_c^gkos^w{^臞^闞^꧞^3^뷞^Ǟ^מ^^_ _#_'+/3_7;?C_GKOS_W[_]gkos_w{____'B`<54п_?4` H*\ȰÇ =xŋ3jȱǏ CIɓ;d˗(vÆ 8xɳÕ _ u(AFDʐEP**2MFJ`Ê ^`c 4Zh 0*< +0 3pT'BC6<@[A$u8U _;K-a g,8+z|K{vkNM+N]xAŏLpqw͠ X ]\`,zڱߏrmhD!ȁE Ds keC8dŁŰ:9Շ#h4N<0 "BpS M7$#Ą7Ȓ PW<39]a<˜26P*"0p36*I B0l:`c%ϸ1D9 v:CY=WY\e %8"+A|RuB|E lBNJR)eg,Hߎ8Mv+D=!yC%N 2ʰ (܀^ KL=  uBN@ ` Va`p *$3 pCrP\ @ RBuVx B( Vp?S `=e  W)C p 0PPHt"np ,`uX4H"6SӃ 1lP*r=fK؄O0Uxm"]a8 fOd5 PuF@ l `N0v=0:o?TCp  ` `C P'p@'h m&]! # @:fs؎/s $t ]@4  `6pP$Ph04@@N'C] 06PP0Zp 13``I30/ xCx9xx \(:cy2LE{(^ 9q'-w?"/Q1UQ@Cp rp |h}6_8 +14(` 7qe# @ 10Ҩ(~#@@8P KPm`B Ǡ P_ݰ 4`b H2 E@E`p /` 6 x僎C8`Z5Nv2q 30 V@r}4  <HՏ@ȆUeIɝy=㹇&Pנc< "iYٟJ`ʏXD&԰4@  @ @ 8@ 3 p<` vS1 %pPp9E?1 p`! w49XrZ% SWz鎔Z~W|e  -X>@5cH=0@ p  ?e P*Z9ݰF^S [[xj D)y;yU~];R-# W`]~g{ Y[ʪsY paz)}m3Pq! U b8bЫ VncG! YWi$[F@ 4$`=W1ZU0nt0 y $Ь0((8$>?x詞QI4q K|0e0L ># Ӆ`0`e{)ڡAk`@p s[wⰷ*{4y BK]2S ن B:$P1P P3. p dBofJ h-=l)p/p @0^:>@. 0j,ڍa#,1; <*&W{KPyJOt(pp t@ `p,0P! Pc B0| !ښCޚ *@F˓LI`d{91--H .1ˀ4p:v}B-iH qA6@@D4r#[1W T. pg֙ PiEШ E~0PcSHc8:'~ۆ ;S V $ ␶ ѶncKp ` y`  % V;v p bL= ^`~1gY~< t@~ `Pc}@ Q= lϦ@x08T\H p~&: <@`; p @ړI[ 0 N:]P p  M ^`zp^`8~~>P@m ɀ G@ yi` @Iy !ȶjݎ` 0G>OP =P$K 'PZt 0)gl0N  Hp߻ F`&G^p a S 1PK/ = 谲pAU>  ]*/Ѥ.nMPqk{( T&Zb/2Qgn 0u,_F` q@ / -}Bf.Q-,wVVPtr{I +N;Ŭ&pZ{jA$1Hr)ð:8AzsG'DEF*pNsKd #-#̡$z8A) 4$ HM:',H hXAj  - C 9iTf;xSt r6acFpԑGH#DI#Y٨҂X0Xӈ@sN@R,mΦ (R\g|)N|QO#)VL4nȑs h4jO Uo'hbXM0$ o\&GcH;vdAH u2` sCipn C шJt")r Ew2S- < $(䈇:)L Gcw:ƍml搇9! T tVET@TABJ*d a(C&7&v#w] 1Dx#S'x2APUU uhr Vax`L,,CM,0pcYiM9ƈ}╼c44<[^a^Df2Lf6ә0MjM,4hY t`pF2 Gl4hP)M8P N|Ȁ`K6 JF&>WV{؄#@B2Zs80(c@iJ?&&E" -%|o-"vP>0[,N8" Y MlE" E d?p , @Ռ.!8 TQ=p(9+vk[CmJqpE+ 7P1K85wqppxϜ[(8X@)uVH9e!X!K`]rxQS`:` yp]CxhH'eKMS;[79U+;78 ۽SҥMuYn26h0rp څxG{rrx [*Wد4x%p9C*Y"X Rj;pAc 9۴R[\y\LbC ͍\%v\I I^m{Sg(K]5\V,E&a :`8T@X|h x(DwA{ް<+S{S9U5^8[rZp6<9E;;6ڤcR6~W [b8%͇Єr 'Ny{0z= G ='C< ^ q HZvpfsx hbCP<  z聬OhWpjx#X.th8 Hb% V@s1`szA%p`l@pcxT[chWհ=T؝Uq`gLrnzZc˶`PͨK.e$8CH:V/ lZeeB`Ȃ{ViviTaW.V cVB &NlVjKE ih@I0s[Z6vPl(x[hifYplfn~h o 4y+EϿ. r0Wa7P4S81rxXt8Јs98Xcˊ)u DNnXr1΀r@[HM:T Or\ņpC=7OfUikuqlhoX4I~u@j8nXZ@kp1{WhxpzQP+P*Xp 9Mz)r̀^ЇJIܚpn }ƾ BtcÀqqc`#4k"|g$h] [^z Ue'LyTH9% 8Njx 9oXyKwSQ)Q[98J;0rN-8|̀+8ͤ*с x|v' rY=R?T_ut*&PStI/ytXrNGuu:n,iP HK8Ddr*rrr.mh pL,PIrW|0~ xTYm J܎ ?GT sq?qB]^mKڋنv@kC{b  !{U`|8BPh m{h[xw󵚁C߶')1\v8hKx+FȀ$Pp@nM5wG@P.Ȁ6g[p{p (jP{me>tNg?:hy@Eep#Ȇapg=uqXi{O8wrv0~yo8ۏ?+?h}e8ȪFY0"nՋ1^Y!4Stbc?,$ Rhq ]ݚaN"p` {~HLf͉'C 2mJr{SM<[;{֤LjVK^4eWgapցr괓NQHĴϠC#!g*Ȓ'SmH @$IP.cOli [QtMP W7a {P0np‘OPzW\NL Vm=peě%#_ܢu'\ 1e#x)7YPY Ü`1CJP*3t0!ȳEz sYG=V /D[JBDEl 4VqpK6R:@vu*:%!Ġa F 9F6c 5s8XMl7-\:p]Zz)jZYԣ9cC["QAl3' h88@3I/tS$~OIt1rHNI 67g4Muj9\A,b8c %!CӁ+,Mq&nz0žSO;l r9r(`E9eu9d琖F}$)ԡ@3p{9B=# $P ,0Ya7W㉆f)̯rL6^J+$6٘s!l[dLx ktSvo+:.Q@81FAڣ4Q:`sh \gdWC$S$L>X¦d* V"p!H*HJ~?l,#b<#.T Y ; ϼ(x}H#suฅ X@޸G;a I(SR`%v^ (1(~pD,1%hG/0LG~TUd 4|rS :aW(B U@"9Q&?E`G>-*| p8;,,&&Cb0 pCZrlJh $j6vU#TUWͪɟ8d%j\yF]Q m0s2BqU{8)NPiH)6En RE7J9l$ -% H l ؄9,{2t%>q`>&WeCc3: M(V~8-1(8SbؼB<(;Y 6eHq< n"PC%4 68 Fc 20:mnk 6! IꈸxƖ#ӛ7DŽs2D{=V4^1OS=)Vf @E4qKsdCd#RLv2'<^Rr,胲Cܐ-oz F8 Z"O7`49bSC3*XvV2zn1 ׸Uܨ`BDbui'K40/w^$ &s 57Q ZРT</L969XaGP E*.X dm272h%%""DZc)"i@81 +Cptt6XE94ߔ PB(zے ^`ktNPH$xC XH)PC66`:SSD@&!D܂ Sd ̀9퀁,A9B=DgCD(C|qe&ЃSD,l3 -AB^9P9AI X;ST \*][ 0@LSy< SM9ȂHԃ&1[0@ HE!T-*]M|eaLC()D'CtFBZmмg A?H,\A$7x1$@@/ A  A>8 7`DC?Le&78B=|h=UJCV%؃%\(S:K^8x888RS:%TJ%89Ag8@4ȃ,f7l| A,)î("d)!)\%Z`R5=:9 = N=¤HtC;bFF+ă,CQ9B;VaC!CnRn3V`7LAw@]y8B$IA4|-9j8J v,e=xBZC Nku9R;A)C<Y lC,|K8ȃ:C$d;C:%15LAV(C-zm8@ #aS-<&ll9,CB7A&8C/ܩdXl;`ƾt;)B9ɚl@7, nX9CfA6C4h"oA@= :/k:Lo}GCSф=C:-f <| +;ȂHd%:x+>DC n|90n4DA,%j<"m;<*SD#=P9ß)؋,|]<(8>m:,^}%0: wq=²CþgͮX&l!@U 'MVL$|" S&Xd hH"B' l%\"QP8E&(0[(+u" !"+h%-6wE%_rLAN[JH_N0c'x"O@H'l& 8& H'!eX%Au@D%oX h&p"r,2I:EVD%8@l#,G;p3Hd #(AV(R/tB/4G 9NsSXP%=ۏ  Agt$hFoGp3'HB $ -l.// 0O"eH"h$LA@Hkheۼ'>k Ľ٤G~#+He"Ђ,H[`T>뷾>>x$@K!,W!,<& H*\ȰÇ#JHŋ)jȱǏ CIdɌ?\rdʖ0cʜI͛8sꤙΟ@ JaόERĨӧPJQ-29JWvuβ˵p][A]v^ F<0`>4,!a,LA&6-SHCP·EVVqAQ' +ֶ5{ԮO7#٦=snoglFJݖ:9B=H!BHqLXN=>du"PCJΚ;kbxZjRDED+VkfR[Y+.ߎGꫳ2e\ےl'3C_YC6M=$`)J=%D?SĘ@ A P[Gd Rv(eVVD8$PSv*d^B\Q')7ԜCQY/蠱9̩dU[+[kUuѽgX#iVp]+dkڽ#F߀ N}x:w3>8/@8WNy4<^w熣8朗7.z8Vn~ObukL7:='A HB$ ,r@;NA"(r~ "(C#O@'~f"S , $ql!# Qxbm& H+`4nQ^T(GD|& 4U@pƁN2 =fM'9`sj;!H)qGJP6#LH^I( fSx]O{t+p\ Li`~Ug9!3X$ `C 0JAViP:Ur}*XcP8TժXժUU[5+<ɚ.<C6nIʁ `XlndAG9LrBbPG=hcG7iʯ0]3 ߠ?l/2z0J=-p8Є<`D4)!„8f'T 5atH, r8v]beJ̦V=I|6N n#ᙶ4WN ier ARkh(´*C+rZDi`$ַ*TkYU!Ȫ jY88hU `@QbxZ0h,C=:1~i/Nr,4L p&͖R0[h њhG=TK\(`(apC;ELC8Pn.Ÿ՜}ұn6e.s0p֓@ at~-P7O`*CSCVw/ iYxf)-+ zaMC $A{{1Lڌpd@$@EHXO4!zy^ A{2bDr`8A'ݐp</B.cq8 ,$ˆAi["P&H!OPG/ꀎE&\8N1Rdio"؂;as`x#h`<}h|; (=1ms[5Hi26=8aYUd"|o>FDz|?_:Hp#>į0  <򞋜:yU;yhnp}~U1~wGrptt~$UrQg6$6Rɇu$Z5] 1Z QPvp` PQ@kr p w%kWkwk ` W'P Y ("^pO Q [pФUBH{Kpr"4- u)u}7|GYE|`SRvooܗsXxWAg~׀LtdrWr/s3ۧ 1T1~(=@tJtC)~|MBu FׁC&܈ B@)Z"3qKP 4` 'W0k` mFkd\m]`++F!+ la ِ@ PizJzb- B|{(JZ"Rb`ڨ|Y677; | ׉}u Or3fh ht~'b8+G?(bO`S"~V)tHX~SuBoKIouݘJ @ x / ` PYoG Q@~p`8P@k'JxML> E`C@""0H`E@PyyO``e0@0P±Pu~Pp QpH8YHq?E7iuA9@$}pH}xsUI~ritɊt)7Wwה JG7鷕"~{B} ;ŁY6٣ q`c1r@p.#bP԰@ːh @4 x*hp nPŅEQ ` -[h`ص p l 0~B s`` ݖQ Pq5R[و9wn.VI fq]j{z P?vN4$ 8@W>PuP4` F` `ĺ*ʺ͊Ҋ׺6j*䊯j=Yʼnؓ,I$=@ q ȩ`3d  Y @$!6lCcq:`2%0r"` ` R$\`dxb~@>A[ 9DW TS@Y q;Q@ [˵y}hY~Y"kKa Jd;HsH@uF+umn{n~p k+rK+{Pī{F뺹뺼{+ڋH鱂)V{` k@6`2 S/`p %b 0PH*3o* 3@PzZ$ 1 %3 3:lz*E;9(|16³B+CI/ß ErGqKpV^,FblE0clW`QpWIpVa Ei,lKNp\MNO~CRNI\GP^]Vbdkqmosj~f>g`W^[sny~eNYt蒮TX.N~C/' Wp ` ePW p 8 PPF݊/8Ee;4ְ27'KgNp2F 9S$̾n1W6P`ccpAE S  F !S oN` qoR _xo?KP O) 6/#_'f!;/@o3BS"/A>? _QVU 5LY/TVP//#P lY) ` Pp VPI Rp]wp,'q"H  yPxPz P0@ 6c+1&6[B~h`.]F_`P~/~Po\t?!O0 FA0]Yx=Ȱ`F]X)BвbÞm;Ǹ|oc ~5LgGPwܺcBh:0Q":lbFc׃er>hvB5s|,ALq%~پpb Wl (V0>a8yU'aX)`=<%ر xB9?D20?WVuUYH:2ozJR(Q:1f3(TwVЩN,)PҢ5u;Ш7)By:RjtQw.]*paPn׽vov[7X0!*"Ё1HLl@ ȂqA3@D$pubWhGz k>oO ED0丬`$N;HaLsb+*0p(1:<^jTKkY XA p9%@Bn9pVԎ@xn\xފs !COi+0ЬC6tVssAs"&KϹn|U9æu{2gكE]B+M뾞 ^?鄯9vS).G}VEP{,(G bKcb l`sbD n z E Lԁeȍqzٍ9tnH#` P{u4[)q0ˁχ^H6poЋ؄wky@Գ@^ۃ @`$ +8="@+ <$@p | \<$2(!L($ tB$,LBd$AtBA0A) 4C#DB\&AB7B+A;@) C)$C@|TCCl$|C>$B9:<=?BABCDTD"d4A)$A1p E}D@{(谪2r%X{pO ؈Uz rɀ P 2Fj'nt FI2mqƟpd GvAGXGGG<8G@CG9`HGD`H?$=`EpHE`=  ?`HDHH4HTH|ȈȊDȋȔ$ȎȐDȑ,ɓȇ\IlI|ɠdHDHHDHDIDHIDHIJ,J;cDE6d@d::cIv>d:G.AJcP?dNQdF9^e?dOdCVndQvRd8dIFDneR>eWeTeLZNd`aY>_~eMfC~f[V\TjNhFleheifjVekfa.gm6squgtgubw.g]^g|fen~zcp\h~fVdt`c0v+XDzGЖy!GlrHN={% tw{W(0!$Pr@s@%(f*Mp#|(O pJ[P뵎5@ZXkFTh(H6kkA!l^2k)(ɞ<8`Ğ=^6Xо0(9Φ?~98mCll6llfl~Ⱦm>llv>^mn׾m٦m&lܶ^lІl~Fll&m.ofm׆m..oF^o&pVo>&o6&l?pn.xn8nžn؞mq GlpvW7p6?qNnqnn m oppqqNơx| remxT%CP1Pz0|N2b2i8a(Р.cxm|0S([0Lv0fշMwF@YWX `uX}뻎r{< ^: bccgιt- >T;)P_/k%3vKps|1^tw Sz= @(vH w0Z ^ ]suVapZuZu[u?4纴 j:408!'>tx2ѡÿ b(B0":X؁!ѱc4EFVre <ܐxĞOZc,RٴhaH LؑÊ)>Ĩ(Q'Nv,5ʯ-_bI?1P & ‡PYN|x1RYC~PeXc4[Z>Z+EnB0;2ͫp/~2&?05w T;mňBDZWдEWl7ǐ@UVui;ڦn?[GەWp 5xWGCq^eI%E5&w4EsMV\d'rx_TFVItZ$d{@QV^[Li)EWq~rvz6WXMJSuݮ J-qsGyzB )W*mvr^ W-Eƪb6*mT')YfvwgTٷiEXp SVe @Kް1)}C8H2D*F76HAc Q(?W# pO m8HD9[g&F.zeOq+b+ VpLrR0RT G$&GIl1.RԹtݜc: bB%;P=l:;*F_x3Jn'zeao^vRO*e+g.M}zf~[l ;hP[+P: $G:v$`v")B7tH*8@sL'L@ֶ|P \-ԁ`s_F!Vo , ?0gq;lP-ܛp}g;"Mns;-y7|.p8mqc\}FyUqSnK|qANoV:Np< :ĩcZ9ǻw{#~~{.kGqPo"7񾇝?^ۊsۗ0 .zC^2v#o'^ B!!V&!2!F a z ^a `! b!!a a!a*f!"R"!J"V!`B$Ҡ `#!ޡ 'ޠ&!'>" b&%b)*b(( &"&,nb v+/b0"(ڢ1Z!02.b/ !,&4R1:b--FaQG \&#'Q" 8ZO\%M _YdZE@=M<Hk([-D-=LCZHL(PD؁K AK$)-XOZOPeHQ8R^ RZTZAQ^DS&ALeXTSRn%,S:P.eQ^eR>eS>eTNeUʥV%WzVXRVSe[QeY%_VReYOWR\N%W:e"VT2`O^dd%eefWff`vY&P&_&\QVePo&P_jQrfYza%[bo"%nSFTN`Y%_f~'RBgveb^Ocn'kvk{"'f.UN?0@DD,?-d$?>Z/E(V<|(>(2@LhZL\hX¨(\ը⨍(i( "i2hB)FČ*Z)i&Vz.i閦iiF))ri)) h.*N(>*VBFj]*n*v~*j*V_L**:*.Ϊj*jj*~$+&k&.FN.V^.fn.v~.膮.閮.ꦮ.n~69< Ti׾C7tC;98 P_C/88)@x&6,ɞhG.)*t cQ8tAF#2@l1ޜ'Gf/V0l^ Op @S tp; x ' {R ' 0 p 0p pp +' 1 #13 c W1 _k_/q{q'71q11Sg1sq 11 qg1o! 1""2!ӱ"_K%Z-܃A9pC,B:,37@4C9B:+N@T@G*3@dS*G R7XpDMA@A B@/C,9!8G#8"Dv A%@G{t%<tG{4'7J49J,LM%N۴,N(N[AxOOSO8AOuQ4NSeRNSKM7POTN_T/5VwVGQ5NTg5TWMVu[5OV^4[5^$Z^+Q7SO`oua[u_]v_Sccad/M7vXgvb5\3\4]uhu`SR__fvge3ud4ggm6dnϵkgji_m l6m6d6WO6rPocW7t#v3L=X5)CB9X9|B0ԃA>75H;H+plQ<;C/ 0">C=, (MC7=ڎ;Ȃ,zZ;5LA8@ C$%d[<9C:HGt :9C/R{C/:A+:繦 z/z#[3h:kz'/9˺:Á:Sz+{3:zyG9 hz?s3:z]:::Wﺧ;7{#c;';{;?,C 9ř*L̓=`89C0+^D@\I4U ?_ATG-~Ψ?+7?\I?@T8A! FRaB 8dh0Þ:cW0DG N GDG uhtdHGGD8!(yaӠM">Bŋ5r1$P&dL6q' BzR(M%Ɨ1~6kϟB-va[o)Z+GVKeȗ+ʂY,⡊mB; yr5pw68tԌmW`CNϡS!ɽ@'gW$+`˷^##RnI>>큃 8f$Po8)' G*"E"W`z49AnY!Y'OxDYG(p@ S,(afEM5c!#5մT; A,p4: (:M4E4I5SUSPuLEUUV]VYVp5Y_ ETQFTi-#PGմTXg\%XsEu]f㍮yb{#}uŭT=[Twkux =XX[~V%\a6ddM^Nmy'|-Ƹtcze-پFaf`yhn>ÍryQƓn28y@qx`P1`vLĹ{#Uynf &r,Q? n@La_qA1Gph!F$$)G-DBw4C <2!AZ|s93=3`A&8FMB v/=^TԲu+il T ~?=X1EQl+eVk+B9̂DZP]OYn1 \VJoh`ĶRet̠S3́>ǂ8A頫.U5D*4+f3ꐂY~,F #308\R39>'d#b׼(0pldѦ|-^PG14H@CXF=st8Ȇ5!q|1`a.@W5Ab($Tgu(v#l Qu0Zm/*z1<,VK#ϑE8C_8zq F%Y?w;-?1?5@UT@=4=r=t5ITA:t6Asb`qoR! q`!"'Z^%(CL˴Dķ6 $$(%`~64&W*M% OO#MyeN tNNKĔ>2!`S=>Sa!UQP^ʡ "lVAnuWAWmuX!XWX`YuXX a5WuUXXUYZYUZZuYu[X5\ٕW\\]5YXXuZu]U[[\U\_y`yU`y``aWc^^y5_'v_w_uXXV]eo`y5auaka5bu\_\evci_cs6dwud{[W!_%W)f-vW1vW5vW9iUiwUg'V :ȴR;Z" TE"l.nVH|(AoRp  pbhplt@"r#W~8s=sA7tEwtItMtQ7uUwuYu]ua7vewvivmvq7wuwwyw}wu3/pxD !yw>8.zzz^ ;0z7|w|ɷ||7}w}ٷ}}7~}I }AB  8LF`8!8<<9`x%-X7x|>|wG8Uxsa8exE.)75t8+|׶>7|aXxwń8Y`xXX˸E 鹞kNj"29p)h|wzG2Z#A8a2 Ht) lda8χa р@L#1 X&\ZLtAY(l؇cd&:"KୣäQګGV2L**:p z :^aRmL> oE-{タ`D[~y飰{/[L"{ Ak 7@~ X~ٖk+)Pf!`z (۞w  }KLQ`hŤ5( - Upށ>!"A t[dZL( Lav@d H \t́ >& i8܁C:&FmWA4KL"< EO^R|`C" \U,XA[%YLNac;ke.o- \L<@>{~~`GܥzvnA[#0}y~{L{A>HXo,!Ѐa;ᡓm }I@r'w'HλNLbKDڜdm-NNKz`[N>&KaT zͳczo  P@S@P'Cz~@Zp`!:7|>xL!6 a ra"[bo ʛJJEK&"Lݝݧ>Rȓ|LM ^쫀PN=m8A@޹݁9A=:Ny˯] 0AN¡~tW>Ed@4ݯ޳V>LR4 _95 @.@-2Q6?PNCU[d*jO ~Z#@Xgd@iqN`lRRK DLp߿ ,Pep%fo[$`<2ʕ,[| 3̙4kڼf{ :(l;Bhѐ9sD$@sbtn( vX'-u YeHBm[! I!žnힵ E+ВÅ5p%*8MLCOCΑyeL *Gt0/6ȐBǓl 8:WO`[Yʥ{EC>) ļczqQ"Ah=EpWS#:X u "%Zn:$R>^Kfm) 8n3hb-=c$($}UcM9T5س eC7ǐeٴCL8PJfɒ|),#'v) g Zp)Xl)7pN8pP7A4HU$3$a/t!-!Xr<,[KaO- J\sM˩!X,c:Ox= hTz9 XNN7pSRN. s2Ls.s* 3 4RQI-Ք-Դs'pb5CM`sO6z 9NX@C6 =(8{XpJ;nX ܳ +lc?S *CKdeVvYK\/LN- CڜtĚkrN/nTQL<AQBpU0%N Wx =N7 X*ʸه_bG8J7XR?b88c{DcdpyO88ls+#,%""QRpL2vFHR#IP!"C=w( Q8!!zZ;z zC=汌[  `oB]CHIUJ,pmi%?,BN67IG44EG 6rGB{CiLLlq,*QD ~2ȴ(j%)pV"O@1>T#G9юx#-H:QeV7qC_юC 9j %󪇽X2_]JD 01 *đ.8 A |1ыdق,1xF>!@)t mCNAFTO& FS%^[C 7n1ԡE@X!'$-х$Ʊ@n~v p,=-7M1%(W [NT*!mt;!%nzl بB4ۅhw; @:qCwS(tc4 ĠHW/21 X` ܸEVTdFGR*P b r$#*JB<u=aA (]"NTSc $xK0h8X!p phiMo+NDɪ*#-+odNA.b z@"X80" 8E:>ґ&?8zJ' igu0LQI<X H@8 o@,b*fa,c£ ?tWFANG=#PC "@7؀~8r!{U:NG7 uy7dG$p"P QeG0 2 Im88 ZY" A$ pbF/|0[;@E8Z\>7u h-]p=qw1nS01 UE( V`!"nQ(<xwn7ɓ -&La,ZxNG>``Bb 4P.e"r7t ppP>'%/rM+/ /1j 7@ `P <߰EBw"1hnls7xl]_4mCm54&or:0xqD !@ pP Bw0 v0OXI00d'qrg 9murrEWp۰xY&H=-hu!Y @0X 9ڙu!{}7 X] xQG4 3Z@ n^@ ` "P!K0-𐋀z P}~0& q6W{]~ 4$p8Jpq 6 }vgM7߷r|}*(A JA`+,A'ڨ0 Ʃx)7 &D:'ٹgyaĘ-@,JK Hp1V Oq b "v)l!i 'QF ۠ CPwřrtU'pRJgEH+`U6{G@ rbI0G=KːU i90' eP  b W X@U  0A=H x p ft]Мx(Z蚤{0;P!B:VP[H`1} m Zͭ2i1! "0I HQb>ۯVpКp@Єpp l`&,&\Iv$$0Y'  Q  P?@ R ` `pǰYp0YHˉsW xPKv` ? ^*:ְ ` +A@(`&1G0Msސ @ }l`" 0~/' ?P 0[ i I@@ѵ ]pQ 7@ 1P l.0]P`F   z= 0?ݽ%i ]$P$ $pHj G;MpUc|M߷``a~t4˞;^Үn؞ڮ }(-~^碩*0~0^7@t @ 0Pܰ K"Pu Kp _<`x eoeO Wx%~ W` ?`AW@Pt 'rIpIE 1`  w}B`.V" @3 SpOJWI7;}P3BoT0K P6 3 e #R` `(a ppY`1 0@ KdQ2C~{to 04y΀}$:~~π1?81Ӂ,(]9PGta(@b  >B:7aes:Q! c> pCJ@AI$bD(FQcȠhp@Lq S\A~>$ڢ1o+n !0Q0oc[>-J8HD&Rdd#H8I !o@Lvd'Przc'MyJTRde+]JXRe-myK\Re/}K`S$f1yLd&Sdf3LhFSӤf5yMlfSf7MpS$g9yNtSdg;NxSg=yO|Sg?OT%hA zP&T ehCPFThE-zQfThG=QT#%iIMzRT+eiK]R1kЛϒ/f$ 3cC~ZEM!HQ30tcd8Pd>RU6/FIU2UT [QqdГuf`Gc*VY`Vq5S:VG~b2WN}, 27zjTúV IhkG}bFV 6=aptz eOKq!.e z $`"|@G< T`_=61L@0.8мC DP'VY!HGzq@ؔ?r<)B]\$[xK26!V'{Cbh!~x`#H^ &&u䠇7@aD 2>ahR &̘ W\Q|b"8!KOP:GF;8 rA4+'p,@#"A!x|iK$ KC3NL5 k{"2A$܂e?;CtgS`OC> AWԍn8!' @E9PGHhoPc&A z* xŷǀlD-)_9-r4s '^H:9n\t̆uD:(q)`E%T@5aE a0lG7>!p{8-C<G B6psx"W A{- XPF J C7.ǰaUG$0JH`$uy3>桞 R a@ DV0*;Î(Myˉ{؃7C L8e3阄ox 1ѓ Xrx =Qg'e,+YY93\'yXa d9"?{ r(Kp7p3r U(/ v?9o( à-2 Y`)6>,{؆e(3B4Hq h)`9OXpx/, C1$C3|=ASC6r1pŢAl؆+?h{DR 2q84p ñT@XJyh x{@ir4n49T)|̓y9XpHCB4+H:!xYwQxEؕ B>6s(U cXpwЄrPZ5 Fy{0]s'HG0S b,^`G? qh7(<4`:r;ux 8RصPSxC4qp.?@Ȃ{nz xnh8!lXV@sv`sz`GABv$cxR[pchW&9;]6Evx!LGˤ!/qL[x|X؄pqhNn\)ˏx!`M8 xeu2^18s{(=(sF<( NITLxy) XNTs8H`f۲l+q0TONhsZO8wr~=2Xmp@B vJ{rȂlVnO$qЄ@jJFj^6Xk'PFnЇbSPL(~xqsZr3(KWtȇ)6,?|؂[DtSNAY#1!IR8"H8,pRnӄ6q«p4:T, hm2Q:V,p'ϝ4z1c'l߄H aP91 ܏t&)\1ֶ)ve8@4x9GHBؾ~,x0a; ;v>*(x IlL ÝgΜ:|*c/޹hh:@̖":wTC BtBZ^7O؂#!2\TXׂhui@?9P^ l?\N=l 76ˬ@aD1D(p?$Ng%sIGF8V4'H|4>KH}E,rl LdSfC9$9 d7%EyBe g_c #⦴g:aڜs#X2A<{CToqm6'&ؠa) :˘"-H)4$f9ଲ *l[PkO*MG% ! 1A3 hf!mG!-XҴ)3:攓8S01#)>|# t (8h\5&[ A ti? fN}Ӆ)dqN nܹ8`CDBdQAM@xr]*bP)N"7?'H2$,-9$Bfv@ 4(l⌣-rW7؎6+ЀuvKh87"B# ${BRW*@6 7z$$282!t +4q0 8xy:⑏  FbDpz`o,(zY $ 9a9$p-daI)ل9,{a$pr\A}_z%?^@r2*QuԈ#0CF|v"26Caa趌$X2$:xD'Ai'*(uSD q6S TD?"P t7F,pK>#8?"B!>c(&'I*H`8&,dhNB)AmLD@ d(#XA#fQRDw>F`F4!mR`CAsTDGhe@Q^dQ:@T,_N7n8b ;pX N~q&oHr6&2zXA#(@F[#q@&s"+l' D:P# !`"B@s$20O,&~V?E=Ɠ h%BЍv:l bMT%fbD$^I8(%Vɜ̾[\e_F lC,CKy<8DB0A6CV &mmf5<%aL@ɑ!>yg9ȃtД9eX6\9"CÌ~W^@͚!ˆHA7C; )FBf)?ej$@:h8HVQ?,@8P::-Cԃ$(P'CFCP>68:lN%l&tQ؀p#f8H',LXn%H!RD !`X8&L,'P &H&Iـ#,J%pBʖt}F%`!ZDʂ"`˪,N&2y &Hhg)(%^:0,',b9UpIԁmX%'lJBb0,! E!pЀdPOd\%XdS$A$,Y :6+ƀ$O@$vd!$NL&XB`H!g\'pB$ml@Ha 8A.U@z% RB=,=H%&|.RF& Rg%l'HD q(Ų0jHNo "dosX ho.[pB< LwS8P:ANl\!4 B-H dt̬PBZ#. x-  4O57)`B<Sh3]tl0_2n,sXL7H7 Xx4`I3=o"_1|44+; sXX4+@x$呑X3pQGx?FsM ttJۊOdN y5'tHg]m'& j^@3]G0';0@`gx`3=W593\R{3{i~v$I0seuJ5=6YN(uX<-Iy3}5aouuO@8;[w6t夢,[k_@i9y|wu;ă4ekvLEB9l}x},oX\A7DSx[A6)$h|P˘ ]BBɠ0F")!Mfh埀*蠄f=Xa -9$;YG;;<gY xy$2,$?]꫰*y'"N )=,Mhb%6K8IL`S$A %mV{絪~ 7E +6~)d!fp7"d@:m>l8 kM43>0A13]Q5 ӅPJ+F:sO`MX ?uk9 du#B2 3M#8x2T9SFbG.5DV Y0HraEkG9pDs:lr+.*SQoP/`A e H2"t?10sַǯ HLFN ^BD0T?' Ah(t B@2eB6qFb 4"H@b>0)H`0:8.,89-h@GݑEtC.@A H9u# |>q IG'L"KҁCG0@44B$q dE4" D*Vf+] J<8vn\%9 EdH#qTeb@l WXB!V$:1 t(GѸH!'2 \!&1H "[(/lQbEq0~,qؑ>GJ(.gdc 0@ "̧|0!aRA HMig 4X431#cxCcُSUfqT #HMB>=0`.‘@ZKQJOK@1X vjD;~̱EڞDZ-^7S 5nXa}2Un;tI%##U]k[sN V`KX@K8&P2ZQ#Q*< ҕy& K\8J7GT W`(qbkE]kLM=S`'-Qr(i(,vXbӍG/~ b&mV8` l! <`/K@:|3={ |.OxγA8p>/hЈF",a'MJҘ-N{ӠGMRԨNWVհgMZָεw^MbNf;ЎMj[ζn{MrNvMzη~NO;'N[ϸ-ҕZx3v9sMV`NhyK>D.\T.dr:/B(L72=)X:D @+8oHP>d99zn2tpi` t`EZ=QCLh`ccf5`XHGd iz l!h2NCSj2#l_Ix v6(SVy8D(>!"~ 9w7m,?vƳAva'3X=N n=W*a'dQ p `V++p 0P7"P 4`P0Py`qxW@@ R6PPtA?X`@ 0+p@ Y5~p LH{  V7 OSQ'P@^m _i~` ` 1`݇[ @EK L3p],@:0p` Y<`@G?@ $  rG Pb2| +1F p10MWxq@{g;*~b bM Nee pe! AAx Cm ''sV0w<Zvp6qpH<. P@a:P/ @( ?P ` `G?EP:h DUj p`$Pw + 6FAsBPZK=@ssZ8y` PP6A`Gs/ @. ؋~ P)(QSZ0b %w3@ p0  Г $x,eX6.H p @K p fy HnY5aQ!#4b#8b|i! 6x,>0 _2` 'B6?0PNGip `@)?`݀=Wn@qS4p IH`P"5   pp FP` cNfg:0/ +*c p [J{H΄&lRr0/ ' 3x aH01#`A$m@`1i I {PWWp \[S-]PC8n6p ) $0#] `.L xdS@hP&D%09OÊ)3+H j$),XW J?7 `2IC : sBpl`mfip ph6M*W: eYA[ @ [ ? P@2p۰  PWEwg /Ag6҃p DZ:Ȣ,bPz c p Pm%y٤ `ϰUrx G": Q4 &u, 8W$0 1:  ` ^S1i9 <e ,X@ [: Yy * p +Pp Bl&7۱k/B 8 AYFy15@Le p\S3P `C6>pn`szh W0J 0; iA@]skhW@k~TPJ p~DiH$ Pa8p RKPS` ͕%c J *]Sh` (/plpNжM&^4@/@i`HpĈL 7\6!Ph#=gV`9¬05x!|F /PHFvˉ 3:pGY`>' > ` ٸ|FC ۤ$@ 5u @8PG@S p ݀CI~^9'PH`ϗRN~ݦVS9* pp Q` P ICPP5!Q̬8 cl ` E`.DZ'xV) ( *,0Dql,w74!WNw  0*F݁J |"@?+Wp ĭ@ ̛F Y'.CCPM3 }O]`T2 HMSC 3@ ++C tpz:$p:c(?bP3x  J g94 ޏhpY t9h s.!| +֓s+̘㋴ Px~!4$ < ܋Jsic9M#sd^>ɘ%LJ"DP '&vXxFW ]0fC a.ni^:x!GBAdX·R" !,W!,|.` H*\ȰÇCH(1"3jȱǏ CIɓ(S\ɲ˗0cʄha‡*L@0 @%L0(ˋ-R4ʴӧPJJիXj %H9O"y=|Ԛ {v}(È+^̸ǐvs -UzX(#E)+7pB6!kcŒAIѺȓ+_μy"!D0Y,܏ "V ÔE̕OV" VV`ǯހh&˼u)ވ#t@ L(|-,ڢK.jK-2D@)DiBHC 48l0 (la`G:Q-I!€i,"bc*- !=@0: :F]!A2Хo! 2*$sD% qX02qMkBP8)bUD;f`H 1YHj@ 5HF3A م#:Iъ.IJG/f`K0C$@1&chFiQmu*G8-Ӟ6G @W%5NEeju4=> XMANc,a)a 'HZ]\oZVlScMb%Ȥ /HBvE"se9*& `պ/`ʩ?Zlg;l@ {lx5N9ģ 8a PVj&qUN^5lli`G-!4YTc &P@!obLЪV]܎zv+9d"($( @Ex0oW"mmN [ jt;&P <Ӡ~<jPW8H h`^uu6Ҹε=r\怯} _`Lwf;!GvMj[!6n{MrNvMzη~NO;'N[ϸ7{ GN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺ>UT:V6Xo]9!TC2pΡ8&l:Iܥa @jbАsA2P)7wW^pЍw>$Zс4^|c;ri{> W,;tNГ>86  j c;])7X,+hQ(Tؐ? +c '@&@ xC/FΧ?:أ  0`D1}˲p ,7Lؗ [PPPw @iG< >zT̒14 @yC }fcp p YTG PW!0KN Zx +/ ?'WE ԰ym@ STP' Ոu 1 2ְ@fxC?Gk[pR Pt!  + A7~7): ' e `;4ǀwwR 174)`0B !E" @ Q,Yp43<\D p4 ? P)!W =p =xm`R!p ͥp =0è%-!3@0$ ݠh  !$` `140@ 9!@ X+(r*u Ԁ N4`P\R4)xP e+IybH4 @@ Pp Q $%@HS`@ )g/ !T$` r)9:z1Ț  (@7 G` hP@ZwVE 1% x`@|*j0FP ۀ 9J @e1@TQb`]٤i0 ! ` SeYbjR~p 'yp0L1P w:ZY9}` $@ #SO` ZE1`_U l6)@p =  S  `Zs )J^f8` 2 v0)# C ] 5YڤN' `l Pǐ ``۠р*k{ P/C@@+ N 4@Tɤ@p~1xw PBnMcyp9Qmp۰  Mp K }ig "P0^XJ}8@G ) @7 d' `NLjbHzP >P СhXWb[0s@H; ) 4$ A/ @wp@pB~i `' ?к:ʃ8@р   G!m `H`PД+}p KС6ae'^h0$@PHrPQP} HVmξp lBk 0 =4pb 4\ =*yWWYZm,~ ݀ *Z]w ԡ|yx p  h>p)`~ PЋpNbp p`J)e9ӣ; OkL60W/y= P2Op]w ,p?; ToH. О[㣫+_)* m"s!1(JeHTe m{Z\h}ޗߞuocuk> )6)D~Bvϯc3 ppǏu:CuӗD)@ϐۅ=$~@AD DPB >QD-^ĘQF=~RH%M|h ~ L5AƔipBB1\ڔ b٨TRM>UTU`DW~}94a`c"1OmݾWܙ!,W!,W!,|.] H*\ȰÇ H#jȱǏ CIɓ(S\ɲ˗0cTa„ ֔ @;Ttѣ$1bӧPJJիXj {a߄VhfxT1{cJ  :W%W Niؘ1j-N* tF*餔VjiG#G %VGtC&] 5$@:e'zg-?IF"(>kC8>l5fP9``9҃ԧ@9'K0: k/KA%a(Bd =lqDD* U`E$l'@. [%ذPsB8́\CȆExe CB `p(gxL` 0',`a !p=!@$ XM ^*ZXd_׶H?` H2hL6pH:Jv 0@  04@F:򑐌$'IJ~-GRA( hֵddp.w^җ%ILKehvZ &RGh:AH6E" -¡g \Ns0x=]8a V̂T9 B)! 䄋?\lvqPG;@C0֨H*qSHTvO{ZGHR!:k$*qe2 e@q( NN` |(=V pXX&6C 4 Q iVƘF'& Jf`j>m8B9qe!fy(xBh2B  6j.H/tP c5F&!!)xeK]eYg#GiZr<!=>9C89tjl! ciE0\]zUtvH6̱ҎLe1- s,"689+Āfʱ 8La@B8DXFxHJL؄NPR8TXVxXZ\؅^`b8dXfxhjl؆npr8tXvxxz|؇~8Xx؈8Xx61aI|TNT"5~8p5;1_JhT< @n@ـ@ p pڨ6 V0EP}8 t]TpP0pKeFp 0[ @baPy m1 WP3`kUa?) )' I$0@I@`8P H@iF0 Đ3@ er[eB@ BSHh`3@ VqaE +$x$b`Y P E'Pذ w.t '@ KpK[WN P|i= @+`pD `KDN/Hb 44ma"|e"˹;i%b6 0YI 14 Ж @a>{>5L 091[VD0@7 [20QH4Дչp @ -~@7 # @[Up ?!n ``C N0 `katd )iG4`,P& p   0hprSYUzG /Iɥu 6=` ' p A pqIm#n ~P]H3>p ѡpxPrP 8@  SP\yٰ IJ]@V%2 @" `Z wA!~[PQp4 `1UYp#uD[`WX 4 p K`  ` G @8DC\h8 X0e<` 0p !x<Ѱ$ e  4) @h'= =@QpQ S [`Yy%1Ց 0@ 3&@ed p-ٰ5Iz b%ŷ@C=`ؐ ; p߀*]P腫&k!QkŖ4 29Ҳ =0@ ` ؀ 카f ї찠 W p J'I 0s 0@_1@ ` IZJ3 @  @ P"ۯ9, 0h=@ G`݀  K/ 㺯`KJE ǠtA8p p& 3‡[I  ~ ä% :/"]@r ` @/(ke` 8_)A<p yGIRp)1@< q %砨 `,<&d*Y@ zß ZG@ ː!7 RO<a%Ÿ?;A ` P ^ Ƽ) Fuka7@fA ' K:mpKG! <,'F0A;T0x "l` @ kìѳWʯAOi ٠ ܰ B0NM;@L7@ +0υw  P%0@n@@EHD pG@9:-`w>pG`⩒w sbC `FGPE'^q`WL`;P/P Ұ,kKm< p0 Ҿ,4=J̹c WY p ԌRBPA9: XM@4Վ`5\@ R@ GpQPSq<9/p *+^ ѠUkTE 0 $R0 pp 30 @<Wb'P,B1 ~`rR ?p@ Q/| Pt GNxQVT1 ` lp:a>q=' ~`Md 4PvDpPI:@QY HPV~00!`9!`"y rB` VH`%)j0ћCƳ9C[ [C 1v¾qlvq1NdKBPNbtr [TPBn%!,W!,|.` H*\ȰÇ# A"E/JȱǏ CIɓ(S\ɲ˗0cD!ÿ vС Ο9e 6 fsӧ/3VĘիXjʵׯ`Ê! {34' BA,]#;X2 &hެBΛ-eLӨS^:5 )0)|nޔs9tHNFD:!M[k_,sy2{x.\Ā{OlmrTϿ #[Z8E =oA" "ņ>4WA9ԔMC (bU-,k-ҏZ(Vk҄!b#Or}ITf 3"C#hps S* B&,2O6GV)؃8up=u\X`9ӋH,Sz Cۧ;ʨ ,DmtHPmP@]u(@Ѐ70OI:;̠%Ѽ̋ l|?:=n"LqP0 4`;HЃ:SN>HOD>]K-R5|#hЦn!P`SP{pNKI" B1DGs cԙ{D3pw/3H<ȀM>0)`Xrb`$$ yAD@@C Hx짐k}9X,)= gHz?ڱx =OTrCN1 *0Bp4B1 h$`F,1 f10`!1K`>Pl$81Z 'PĬGm` NlQ $EdN~L3!Sx@+dZ0-m[8 bB(IbM Hf.dNj5q*, ɗ1r< I <"͈픀b@m09~Ҧ @*P}MBІ:D'JъZͨ$f2e!HRmZ(MJWR0݈` Rh" '0A,`MB(AD`LТRԤ2uL;ӪZբB7w 1C tC (d2P%( \׺(^K#,ᯀz091=1袙`#!FX$F;JT M-$"aq㵯-(p֠XD :!!)S>1Êe:(f>T4A]j0FuK ;:*x;L/I(xF}@QN-4ĂP-5;OHy"ډ, y,qD=.&tu٭j WL8-q &A`vYhlG9  ezqY :8 |8`v(|, Bm0,M`A&vlf;S 1@ 8V{r11{cEnA"vE0b~ҁME|gPэW#H=ґ?A[ڙu($ q">Hӎ}3 ,8w@39OA;*aW [%A{GOқOWֻgOϽwOO;ЏO[Ͼ{OOOϿ8Xx ؀8Xx؁ Pm.S !xb%! 11FI@!c-A\P:rQQ*Дhi$q(<X6@ @ 5p U @! $ 7AB``0~0@5Hːk@Qp0͖ I@⠆kR S6ِF ~8 QpQDp\1`@ 3@&PaOk $Sȅ8 @ PBpٛF6I @ KbPMAa` -X>`$C p`@ "ޠ )F@ 0RkHC'@`M!`_!ItbDИ! ߖbVҐPr) F `p m!=G=†W#:Th  1@3P1 `/BVp1+!5` 'A ` 15~Cb 8 R -QaZeeP! p@ B ~p8瀧4P8BJQ@ )JK0jeh @  `h7$8+@&%H?@0:( <= s*`騫j[VBg b r ` {1F-W P1Py `_V=@ z7c0FP#s3J18qPh' `=%b'+ P+ p!E;G5p܊h` ັB[sY]<@ 𴨐!S;# 6P Ϡ8" +SmH|> @Xy@WNTdw`G0keH -VN :p <T z pV0R!n 1PPhu`@BVz%rQ7 0\4PQ6Pk`e"Qp +s}j W @U= )s1I Lb ܎P L `GpBp +hE ـfSN` HG@a`e0A0Z,9vT`pH ܠ< p}j&  @ 瀈X ` ,SP -i[ b /08`P^r@ l,\?X  $+)QO+ Ɛ  %I@ pphe@QIEp?@F ge kR b04"@S $QS Vy p1 @L t W0b` I\p tM=$tIbS$`j3ɂJ81߆uFUƢF'`+BN?4`?1`߅fvHR v6 aA# L;L P$I~F R1?L* KGTViXf7A% D0E=xeCYZbUK0iR˞| G*Y,GXs?Q磐F*餔#A,b#?!TĈC ϐcP0PCR{Y-:&h?n*k,WDBN:%#BH;!3+s+ҞxIz$+koDA+#@=#2!29݋KG*纋,$Cz 4A*3D\e|Ê:8p :A%$ڪƃګPG-ufqأ M07a@PHtJ>I DG)G6騧:Cr:$ A:鸣*.n3H07 4M$S/,ʏ7Ƈ/PW&p"]P% A p&~@>BqBH8́eC 8[)@BzpXXN)D'!,HBxpfH@Fӓ`LĨqP"g*ZъM̢.z` H2hLƈp)-rA8@3a >cIB#Ar)~p0g R~x5NzCDC^|"T,:! R89OЩ((0Y f |%8C47Y 2 G< G1/V $9J # P#$ăd Q I\l c hZ+K.zb < D ^ `]@xhD#qcNIyð|D20 Ґb`^pKXP?\D^CI#@-x^xUp3dphAVQy $<(=anlዹC'rF7` W>,N{*O* pj<հ^ g @8 tkA\.6}*^qÎ}@ZζMk{MrNvMzη~NO;'N[ϸ7{ GN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`zQ &6Liv /g /v'ag/N|oЎkaCگܣ.6b|`Bx;ܑZ@#q@@*;D$:#{18AHb +o^^٘X?g!d-h[-h!<=TDAu ̧w|}ޡȘ P K |wU4% YK x zKQ#b k  }HܴWP yDp@ ȀT'rb\%@ :e$~u@-j6xUo"~xVtF *yQG*Pb 9ӂ~`@\؅2? bpOdB` XP hp$-[h  ?Ї `p~1@ ` I"fO4K ~@j N0yX 0P8?l` P pep3U bQ¨/ :W1@ 333=Cr>*66` 1@p  0%騎2 p5P$bKWDa5``dCp6iCCp04Χ%p WpH:`?(S{@j'WB@9V 9[p9K ?G pW0e0%FYP@-~р 4~X; @1p aR~@  pu`  p0r(1P~W ?%?6F1&-#`iP BB0TqGW9B% B1wYH!,W!,.] H*\ȰÇ#J4Ŋ/bȱǏ CIɓ(S\ɲ˗0c@3ÿ iZ@S 6{JhA3j4ʴӧPJJիS2 d ?X[:p.XpB ݻ.̽ۀƒ &l[=Pj 3d' ,@$S4hM?nb#wΖ!8hb#4sHIϠsqE;nb*#-lWoYD8 z{> @Dp`"X"AvG?Ct! Z̠7z GH(yŐH` ~b ȡwC@ "`-!8A. 4t&"X%(ā`@Ȣ-f؟`W2sX2ʑ%p G;aB8&d>TF \@08(@g$VR#<A*0gp9N@!AW+,Te%AX؆8vKsP 0/62ԃ3#B"r|" C=ՁL AL0 5SlF*0&jta$ re@E,@Rp"D!bQغ-t A0Ba ڢF*# PBL@<^TD2G|7?(u2#j(1 +HM$ u( `)$b`,βcD`BJ8 ~y׼>6Y3 p9ӀBxx>#}BzЅN9L'Z7\ tGMRԨNWVհgMZָεw^MbNf;ЎMj[ζn{MrNvMzη~NO;'N[ϸ7{ G׿ D,3B( л'y`ls\ XF)!I+pѤ]pG$Hl"GD򸠅 h'4 Bh#i7DxP]'X;28p( + 3pZD؇%V:` 'С!XV ‘04^#$H#p#zZ$9nGDE ^ Y&Rx0@vDfԻkZ+8B=.lGZZB$ "+p3@ wnDPx">PpH>@aC ABr8 Q,` pf@i߳A4$(0o-x\ S `8}0#R'Ubh 4 lp & 6{z]:@4!p )-xpXnhf@ Lg{ p`py@q@ :@C{ S0U @ @z 0W6 PcG' 00f  ` 1: ~B@ `ix` +@^H `4P~4 ,c ml߰  8`au`6WP)}4pe@PpGoሤ s8Y  `p  ` yP e0`Wp  P Q`0 Wpg@  + `y Ѐ`x $0 p p烓@ 0 b @ `6@u@ p 4q71'7:Pin ` B x/% +0C Ԁ6p7p ߰4@ 18 r/ /PX{ Pg 3b3p"`8ppр =uNNP?6Pʑ )l`pKG`x@ b`aaGnd` =F> N-j  ^G3 $ @x /0 p3 "@]` P P3'VP>-00W00  IF` @P` QՄ 0 I`syC @ IIaB` p (np  6 : *w` 4` ` bA!0d8: HP p *n 1 @ N5Mb+ 2_nЄW`2 x S0 ^&u +@AO`䠖^UW$q1=@@p @z(TE1pgGFqeP"к<  x x "԰ K`>`3 68г? R F'~p @Q`S[Oz7W`h ԶoQw{ @{ ply 0 p  p@ t)UiqR``-6n!j`R=W0 BH$ ް}]25K k@@~ `P2$ @/;$@ 㰾\@xP'Q@~(p+[0ا0lp? к"kצVKǦ"`gBnypGIIe*7Fn @EHk:pG[@'NK`0Fg`>S p @ W D ]ᰵx \K@ rLhQFЎl0J,t߰ PPrK prPP' ɴF@ (QG0H xW@bpNk : x ~O@6W@ ܰPWv~S0>2x: @2el Ԁ` ȋW  .O˰WpfQ0U.: 4@5uQQ ߈ #nG l7 ~˼@t p-Fbߜ+K ,xB`6tl=h p~ + ڍ xvVw8xf $p ep\W(DXs7.$nߘ` ` >$@F @!5 N:t^s a$.oT2-d h!/RU>sR >LB2bN8ڼHW(^L&hC}@7BSXV(("P(N9OsӔ2x!,W!,W!,.] H*\ȰÇ#JX0ŋbHǏ CIɓ(S\ɲ˗0cPa¿ 3LS2 zrEFC]ʴӧPJJՕlW #a=| ټʶG.@h@n\u7߾,[È+^̸c+';1:s"G4U.`npº5!OV'MqsBSnܛ$0Mȓ+oT91"z3tA~-b TV])e=,XG~vק(*5PA3DaaG"t`az?dbˇ *RˇJ=?x8.! ((&@9jD FDH#2!  ͖Q8G0i"0`ƃ> d 0EX x%K0^V`"f1Zjzα J$=`>!ˊ(pV" +U+W])W).I  1HI XqTX m:1WX4&ls5[ ö(U阴[ߚ-I_Ma l!C:^Ivu-)`!u2ElyP8tȢ :`Rxc EASEX`/Xiul`C<{L j'`X06X#hS%D9`&if#,a G$=qt hp15H1 )E}l~6d _4;JH)c\!xsQI$ZѰE*JO?N{ @霐 DvRH|@ZE؀6l@l`>?f;=mX@g==p&9vMzη~NO;'N[ϸ7{ GN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOvW'<Pܷ؉O(c {CM fV<-Ȇ=r[I* )cBHuyDC`8bp 0H 7(N?x`WSȂ4yzs)'D9 $#Oy"Q .QS |;@BZ0~Lx1PpS7~RpZR Rp\<$P\k%s#V Ee  n'Ӵ /dHx$/3p@'`-HQ ݱ6 Qp=0j_/` @2.V+ p$6 01f!` 0 d5Cw!6x'pP RQ@ yz V p/ b PȆ-$0pp$M`$@|>M:21[@&dЁM4}$p@ PbQ!=fd@ p b @Crh = j`ŨPS@DAa:  T 0 V ]=MQ $( F `0[ 07=N 0/3R$4%KTQYH_s + p $0u N ߐjP?@7IBx`f m9g ؐy@0 @WU " n(9R r @<&݀  @1S@ ~$ 7p e@FyR@h`0p =~b`#>/@<`#+p_o B:  `ɜ` ݀sP`x : `:f F p 1PЈo  `!`=`` O-;qj @| p y` ;`r ;p`X PA`8xR1-`u /EEH p? BV '{6$ЩHI W ҈zqrG0$//@MBIo P Ei`dE0 Xd` '3ސ{/p h#ESW`p07}?1 ` @& QB% D0E=xH(N>U R?X),zK(|S*R&f뮼F0M/H<=qA@84 9X$(K<뮞**YI?jk Ce1M66 AH;!d9ؤrBtꕫ֢fj{+0,N=  <F=#2dC:rxL"Z.:dmRu-q27bXPD$A߰s: 1(~1uʭ klwD@N/ HPAY 2"G90@0P&~ݧ_iٲ/ /ԣz|Hd "L@<Q2%4ސB̀J=t=FT/ŏ*9C-l` qt8x?H uc( xwI` R0Έ!3fl]xW/ H\ qM8n#%! `KI&$Z `08 D:ȁȣeAVByИLd(`W@Du"@P8@J)GyD`9?b)Ġ.w^& &09 b+L2f:Ќ4IjZ̦6nzII$dqpk0; \1UͪVYc F>p,&@YIX8, []P H}}WV"bKS,Ul9PZ9%`:ԨD9ҺV>>1ׁfM2Rmn5jEɲt IgLt ְy  ή@Els !:C@Bu ź8ZL+ê2Lp$'Y1E{*2Ѓ Vz?x0 uQ+-֔~M!B? k%dADb 2 {AF:q@l,&ɬLk\n4!^Z G8@ׂlnsG; 1`Zv n0-ˎfS9xvGzx V#l}]a WE=jȳ$mrB']&/ KxYp {B;N,6y& HQ1:f7EN`VH7Q?;x8{<{&Q}lb)a0%t-Aaف\dB型T!s{.^!"u;wx xg,N ;3`@p[*@ы6 Rz_}u,lϽ\=c.ܽ{{:̏O[Ͼ{OOOϿ8Xx ؀8Xx؁ "8$X&x(*,؂.0284X6x8:<؃>@B8DXFxHJL؄NPR8TXVxXxaZ'`XyQ`XŦ;Q1K`~=@Cp 6v!9vb8  3(4@ P ` n p  /m1 &G p QG~ $` " oA$0ol 0Kp{)L'0o'@ 4` Z Spo]!d @~ -N]!(} @ǰ u6cH p n ”u@ gP0p[p8yr@-8P( x}"@ @GS˘pptlgAP將/8p} OP8,P& `%B ԂjC @H}@ '4)Cw jyZ[Y@ I1X}b`p0Ui8v=R8S C ؂pCRv;`o7C@) v˙HpRp  LZ `R$P#~ :PpQ L1p 0o' ޠ~`Qa !CЛtbt+:DgN:Dh>H ~!,W!,|.] H*\ȰÇ#JHb3bqŏ CIɓ(S\ɲ˗0cXA͙8ssanj;JѣH*]ʴ)E紪ի(!\h+v@„h'TmXʝKݻxQV`%rDP`g ˸q.7`< !T(Sd'MAs"ShыV8^ͺװc'pbŊ<ΝzPL8Yb87eevGխZJz3G6J^?~3{x~myξ@~lˆ(l(OO$I5Ȕ-䢆.Vh.jb!Ȥ? ;>W8,bU| x @s#: 8A:2a-PBy@-c"?di (A,#tRG;򠣈1R Ȓ%hjD%(Z he_i襘f)lH 9 u{Ӎ:ԁ"t09>Y!h-V(H閖6A+#:QO<E:A:1ꬳ:-f&&/D20 B8q)'0p:#c$)"*n%b^,$/[ 3%?*=~A|i.i2T{~\o>7-vG8=ȉPFDc@#}bC]fƋq"+>Vmx-N<!B!<E9(cLBL$;2s .|i9_#FUU&p* [\O16^#$r1G7?r4<bB+cO'ܒ=2ESo$B (0 .w!؟ p @Yі, & Z6r@ G(L W0 gH6T!}x!k@&:MT #@?X (%Db&;*EH8f,[>16E9qz?D t""n)Ǻ%(]Q5V"le+OZ.S^P#,@!hGzv˫PȦ6+=TBڴ+!r3 9)jM@j2pB9B*xY2vd d0< a8`D)ЇF#_5% }`g!Qp",HА"QE3 UJ(Ǘ ¦HMjK, XBdԁ "C@1J ܦTQ11Ԣ~a\?"A pǵ"# (O@ u%msXUV-Ji]PaZ &q}d5-%Ѕr`¥@^@WXcCX(EV[e <VaE)ř;~ru -Lk(5QU+[s-U<'lȭl@/0E:^Ji,ml}ڬ"-xO 䰄&#0K ΢%]k$AMln`B&v(>X$  HC慒Il"~tD,9Q({W rHT&R X͹D&\7"3>ci' FY!!&M HҔ r4l@O>@4WUs_-Z_#̵[^C~Mb$f;ЎMj[ζn{MrNvMzη~NO;'N[ϸ7{ GN(OW0gN8Ϲw@ЇNHOҗ.٤`M)2mf@~A%<qEDkh-%t.Z S8lթG# Y񃱓()Q1Mޠ9Pc<pxTq ܏6(<]#DHg96(`޳ >7|V٠q?^ ( )3#3-DEX>@=J |%hHnPP alaн@2 |#R0@3 Qp= UR $'/'u0ĶZ<N@wYW {m0 ` {3` @ P? # & Pq1[0$P`lrr3$` 'Zpmr`"`DHCW`@ p  QqSPu D- Tlp@Wn@rxؖǰ <~h ! F `d~`@Xp<@`P'"Iv3bP 4`VpsmY@e?pDH pI` ň  `WI @ R++'~4І-؊l9 "axCP b@ B``0ep q' `0E..6S 0 Qm@Z3{q1=`#  `AA)p   ɐ 7#1@< 1C12a` ?h@Q@_ٖB ϐPRx` b  :` Pp \)#F0@Aɕ3;S# ?H`1j3P~u"i4 6p5ӤJ15 W @9N Pp"nues6~w@~ Q`~P<{rS@0 @yx`Z `y @' 2qa쀣` 0#9Ej C K]` R x1 6@b` IFUAUI u <` /zp0z1#C@6D4cFx C=Ǩq2E6  +=W 0v!,W!,W!,.] H*\ȰÇ#JH"3bq㿎-Iɓ(S\ɲ˗0cʜI :(0? ;yhͣH*] #HMJJիXj5zHw-zc{=P$!   0/܀ #Cз˘3ky%l89rD -@ɑ,ztxAhH,̻J.G`N $\cdHrb`D:($4|kν .ض+H|'fPW$Ç_GlBG&bLRHK*d0 0c v (D^{(2A)@7$L0⎖5SH(bː #B-҆1B9,ۤҏ?Xf*C`)fV%ޙ,Q:sFiR4#H(⧟F"MS"˕ZbwF*餔Vzr Hа99I瞺 ZKYhLY: ik챼-=휃 'P^I<ӎ;P/,ͧ!+nDR렸ںSVh++S +#ZC$Pc3 8 Tnk-":jo ,@CvEs u1$8aԪ.Nlkf짯1H'tH@P4E$bpW,hNnߌ78A;S% VÈ*̔n:4L$SM.|z2?!e a 'X !o.Dp1GSosWnu,_-rηHY E x <'@ H N̠vz#F (L W0 gH8YҲt@:P; HD%Pә$`I. p MȂ%E`hL҈F X @8,xz,e# 3JH@vz:qlcy PPJZ&kP$,ᓠtBJPF*WT7 !a|6XF;zGVbvC%*fB3LNH6IPPcf9@%cG@t͈@q( CNN` |MD'W Pv>-]A!HqÀPЎ؜ `JFcBIJB*`S"W˒58o HMD,z' ?DJC"&h{ jj[Y]ٲ6- ? \z B(P)թx3̱7ZlJ+6`aF\Y:=q+&xP:<=QslYM{kf7^ѨG/|p"q@!+ @Qȁtdܚb:5d\6J卯9hЀo`J": ȗ&yQ8 Kc7ȢWX@f ! +L@X b0zb~I-LaCYxJhk.09Adα{1LAut@ZG^I0aL@/֌ \iVMkYjQ i2!*A[@Z$; %$rJWAi]bB?aP~ N0 sSBMؠƶpldL3ﮧMm#sȶ}lk; =jnv7Mzη~NO;'N[ϸ7{ GN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOp$4h ۝(v O& (E1[{ $J`sCہnأ.cytYȞun9` p-"KP4p$HV ;J-"PTAD` vc$`<$HI`op ?   @ H K 4pZ![PP-"@|E:  :m C0Qb@ g 'H`lT !C3=`_Y/Nb@F7 ~`u&!@T?0"$WZ? Za NND``!,W!,.] H*\ȰÇ#JHŋCh(pGCIɓ(S\ɲ˗0cʌf >Y„3 J!:"iӧPJJՓX<+8MD2۷pOBB ŋ  3È+^̸1 l9Gݲݕ6EfǨSK2 ,paul[yě'B+T Np;XÔv$(=R~2 Ë *XҷKMXJE2(u18bHWM ֨3C: ! aCG)Œ!2ϋ0sh81%ASFg>XЖPV?bK-X‹1| 2 =NDlicXIH UP34)$ti[SVye[%bihj饘fO2-8C)=X۴CN98сeYK'~嘩9ikȆSܒjE]騧^ U+: [\r ID6q@!qAs/<eLgY|> V, 8>>컏@ &X10 g!H:'H Z̠7znB-&@-Ԉ gHCTq` P Մ `H<xDUF 7*Z(ʑE:1^ q!7r J6@m# ` ',xLf3pn6dXF`gPe3pHXhDhƉDZآ]f686GN!-0X Nsz؃<~иgma1H5GW*mpx9H@P!ITJTXHd GCᆟ:@4q}DcTC\10T atIL=ZR't@/ [(1j3& GVL[ *ZpC; X`lz]8C0ˋ"H/B^^Ema8 d ݚH  X psN"®p/Fq3ÿ N1d *~@q-P6  YGz&;e)KS2Z.?`. <0O&[6pL:xγ>πMBЈNF;ѐ'MJ[Ҙδ7N{ӠGMRԨNWVհgMZָεw^MbNf;ЎMj[ζn{MrNnKI@J7yN0H&x,>qo'uMm%ԨU^Ma DAԛ"D8. B 9!Vw(.$; SM{p j$f A@эKHbxO3 0 Z+@:$)?=@DSp;$P*aoY86 t\Oh0E$Q"<G/z@|Arh}ݤE9#/X)aT #K ~S_86LЇ@0(x k,A;,6G6hbYIBqdx ,`\siaX W sS8@6"B4` 'fח}}hpP߰ "' P C {.A=6w8Q ˇySuP}hy`PpPa2#xP@ p 4p ]P$p bSq G˰ Ml"B`݀o&pp$zF G/ q @~ deP=RIW/gk@of` `qq xP` Q@pp p S@DQI Y0/`ppHoFp ׌l50pqNd1@r ま5cH@p +` 1rP0x(p@ Cbшk@"P5bFĠ` pPF2ppYP P m7h p=~> w`(c:p k3~@c_S"4P zKPpπ hPH>B$/@w @6g4XQiq$Зf H.V/?p 09Kha5s!bI @ H@, Bu_$8 S`~`1S Іp@}F~ 6ҩ@Re@ ~b?10;]` R@ a=@p –c'k >+: ; CB2p S@>PQIxl>ʅo!,W!,W!,q H*\ȰÇ#JHŋ3jȱǏ CIɓ(SlApf~ɳ dXIѣH*]ʴӧPJJM$!XVQLY:tp∓U`¿ *P˷oH(Px0X0 60Ņ1eƗ _귳ϠC]QDs̽oRcJfstIv}@=XDЧrxQ#Gs{3{Azk?=w4P#ÊcaD$qDF Ɠ^ #(wBT"㌐0E(##ghH;2?c:X |$Y#f䌕cXr\v` !A lCT0JS!AR@c 8V h!Ӏ4cV#,\ji3d24^* .tZQ髕JG.:$T1դRc 6ri 43VR8@,+/භmnq[ֶm@m{[7%nrs[VtK2nLz x"zMz[oxKͯ~G6QN(j7D7$0 ĶcLE"|ܰ z#gJHx vd@́{#q!F~ddPLX08Q $/0= z,Ïs#@qTYN : 7s1hπ'`7)! B$oBrĠ  8W(Hu+\ъN!N5,`A ~$@^! 3|P`ct<lZBvhbԿ1&шf(laJEr#׭nO!@mݠ(dԁp,P'ZQ /nvs 8+yxMC4 3af@4޲04)^qus4]v؃ # b8h!Ձ+&,tcL9ƞ3k@B5!XDAY D쿔L<|N@ HN L!A.40ãh)e ~f@ݸ_ tK 0 ̐ P` Ѡx`zw4Ep{ a'P@‡,2Os.sD`/Xn :x eP @(T9ay {$5*r@PH0SpeQ6Kn`bH09uӖ6l؆npr8tXvxx^H( xYq@QB{!_e3' ` ~ap`@ ~(0XHƅsŠo䇡AT`p` #0Da `G: s 'H.Ph.(p.KNH08؎" F`Vp qa@aWBqAxbKafpWP  yWБIiV y,{@Poް YqrB'1 L) 0 ДLY)ϠxeG g^9=   <0GPp ! ai_~0 : Sn snWqϰe 3zIppmB >PpD8V4 @9f%Wnu)$1߶z neטI!N nP۰  dް SKP:;AR x`¹Ym#v ns9*Axa `0 H0hpi1Tu)c@aw|:$B`hp xA B jr0H8W`AAxs n"jRF x 0@ `~IpH 0!6/`6/5N礎 ~Z;!K '}qy߀ "~bPGr0uRT yʘ{ѩ D#( 3`}ݠbCf0 @TO q ` 笼 ޺10Pbe,P F`p1kpj % pkĐ kk PZ !e1VLYūa(`.k84@6P66@5K4BE9B;IqI{J۴exjQKT[M `Z~\`b;d[f{hjl۶npr;t[v{xz|۷~;[{۸;[{۹;[{ۺ;[{ۻ;[{țʻ{_aaFLviaghaAzQky @p <`SfG`NT j[0WPFn=vc; @l![8Pdrf@ D߻` 0pfKpˠjNj4 Y'a?j& f$h P pwc lF@|c"KPp)p "~P]@&VNP!i?l"'0 tdPR@?0 ۠LWp~H8 SK1`P,:P@B ?x.L p}̽j1Dp @ ӧ ׂS0'0f!}l  0K b T= LfRPu'@ H @@#.'=17D1,ƒ  k+aG 4 1 0@@ ` Ck.K`0< KPGOa @ l |b`PppqKC@Y"8p | RU;&]@@ֽ }ᰨwQ x=[|m . yQ 0qaBYqeP~c`pCFR@h a] i1Eh83Z4+! ;q= S 3p g.8ݍo= p bi{PV`]`` *`p>p!  P@HdNvuPEjpۇb B}V @j{$R>EOFIRE8bp:E@ F{!P`bje O[@4fE|р ` 2#1ʱ uO>:N;a k0"`p pས& {0 cR b!.0 ` l21e o\l}Q! FXH C0y4@|0H/aG狻? Br!,W!,.] H*\ȰÇ#JHŋ#Q Ǐ!2Iɓ(S\ɲ˗0cV A„ ZB΅>XT`ɴӧPՍRjʵׯ`:ܽ{IP3ÂB8XyKpCeh+È^Рq/8 C 3O@8ϠCM$ ?qjWF -s랣 ? \8G;Vopȓ[s Xs-8iھݓËONXBʽ=$ꅤ'^?0I}{7C/3"ᄹQʄN?ta?\$h/SC8 B<M}!E"v?'Ա =NHd /ic˓P6*@.ϖ\I)d2X?t0KsL醔S`IqAY栄TK-4y袵4*0Z-%Q覜v)T * AP9Yx2p }둓&>Zz6 >p2O8p kAB@ˬgwqΦk^J(0j-^櫯"-_ܳ&+hꮰ{,wǤ 8X!O 'DJ[|6πaI@Bn10Q" J=F)ͨj|l;Fpn騿:ӎ,'t7N:"D!D4 =̨7?38#L0 WG,`2l\r8o:+?CnPA&8BR&$ aX`02h`o)a  δ% P *dO(#?< y@Cal̐FҡH*/Xę@.Ɋ` H2hL6p"*"@ [8 B2&(H!8A. 4hh6Mjex2L*YG/D6ɁP92g% J@b,2Q',tB|Lh> "6Wp#x7 !amiB% AzV g= bC(9a(;ZHTi-tԡP<$1 aD+NzRgG86֏2ou&C63" S]1qzpIQd00K  h~T//A&$l p?@y&E`0mb @>P =N Q1Ġ ~uL؄Q$`  `fP0pP[`Wp*[$]P z{ K`@p`q+p p mRNM14@/3p @ecc W @i Pp3qiUH4p Il :@ps @ ;Ā<~ P e 9ᄷ Y&A%rc.`Oyl`Qf(I s@?~@a,AbZ>W&>ٯ_֫SQ,J7XfL9ꤕ2Jxv]00Xb3𑌉2K1#2:Ç8㎊&tuЁ%x"AC fꈥS-` >sO=uc8`1:L@B0r+c?>$""K=DbX=k+ps,VLC9 `Be<qrIffH']P1@Cb ԳZ3R!]bJϷ~I;p.YX; Bd`uC ,all FJ:'H Z̠7zQQ"X@ (3>0 gHBŅ8A Q b#lDpR,1@*ZVfR c{`+@ A0@#B8t PB, \$,ᑐ JBۈF`#耒|A`<|c-!1ВP HҖCjyK[> e%*n4!fp "P EQ@5'1ph%Y=hL:YT"T'2 cij zrz<8@ PRh!& !l?w%cL [" 訉 ZWDWJ:hR:A;0X XZ: G@&%d)~6jP.6`@ipJUa`x Wb3Ȃ,юg 'lOZU5SRuaH5'-־`=[*|̃8 Ժ7UDq5l1U̩PeG  V+ h/!Qek i6ڊJ@3uuOlD5P%-A܇-E ^W(b-iTW\:ЌV? %q!.yy 0E]*yjRa K#8+[ఌ{Hb6xa HdpS3žHIb^ իDvE = #sMNs2|q5\=?@4lhB@+FO' I[ײ@:ӠBjHRPԨN bVpհgMZָεw^MbNf;ЎMj[ζn{MrNvMzη~NO;'N[ϸ7{ GN(OW08M%k6,kaȓ֤9rlM{ڛ߁')T@~ XA+>HT~G8BX*U W';zJ {`!W?5a@΁A!ˣ"8 N0Iȣ`L,(챏(^>A W $G>^9g3W?ay"S  DW8skC@ /pMY@  l6Q@ =! q =@p jo6f?6#BՐ4%>As"t BGp Hk!,W!,.] H*\ȰÇ#JHŋ3jGCnIɓ(S\ɲ˗0c*|ɳϟ@ $B*]ʴӧP2d@ ;Vq,RÊKV(  4X -Zl)P0 ˷߿Nn zZHL^*Cp!T6w""NRgװc˞ qM\CGHa2pCSq媕Vr)u:Q?߷,ӫ_ϾTTX7A Wm@grD;ށe.a /"a0ƨ.2,܂? O9G$h(T+# 'PhO98ADUK-ᑵ /jb "="XEv`F4!D!ЗPEY 髰 2dSᢌ:;N/SB=,O4&`N 2 퉳bk-XK؄ ƒB{ CӅ@&qB%E8B@("PrSYP,@8c?9(@wb Lɦ!} Z̠7z GH(QW ds@PHB8ai)@'! X8z8B `@V̢ % XAȢG9# !d h9Q 2% K@`.pA F: Fp'ɽF(B΁p i$p { # AZV" zD-!QJP8Ih \,r`B,[# >dQK a`D)qNa4PqڳnK@/&"D)  `S| 2,d ^dD(CӇ Jң0 * s>H!ɒa0,$c!/x 2JWT3# BhpBmL K8 P؃ :UUDQ`,C!j[.)`HOsG *ΡVnHA(Qk1WcPIpjj+H(Vо!p!ơTh@ʞfq GJ : ~mx"5|PHƹ[jZB@{%Ta_I @$0l#3qe{{T]kHG8P?VПd,XA0E9A#?N XۓڌBt;2zP+ [VQy`=J$l,\6 & δ7pEiPzԞ9V!:uZA@k\.MbzNf;ЎMj[ζn{MrNvMzη~NO;'N[ϸ7{ GN(OW0gN8Ϲw@:e* O&zыd`Fo3 bK :~t>^ӆ*==!D=` 9!>s%.-챀^ԃ xN |> Vpc$7_AͯO0RRxdؠ|\q^ۣ'1uʼnj#]Pv}DrBo_P7?`{vKc8P(`>CUWq3~V`omP 1 MG`up~fl` yn` N}aB` u 1Hl+06` n1@ 3 !U`G*P`lu nP'oh`|fʲG p  p?G p 5&HV :AGIP`' "`4 Slp 7 >р 4@Lq`@ j C0;Fٸ RI` q:ebl#MmX@p>p~!,W!,.] H*\ȰÇ#JHŋ3jǎ?~Hɓ(S\ɲ˗0cjPa¿ ;X`$jAѣH*]4H!hJիXjTm!3aؽE#Cݛk x  4 p`„@ɳ1 K˘3`D]9Qމ0 S,Ɏϒ͸s.Xc% ‡aI`kνʉ㜩 JXMĊ/QKʁ$ĂrL?Th!?\h? (bn P$1CwPxl!|$ 6(#b1t,F10".MڂL.Z% AdiJ DC2?bh:3%"y栄RV?HvbL-‹.Q 3  _nK?*ꨃVP? ;TPAB,cN'%G|~@'ԡ /4 RNZKf馝~*fDO:,Su`j;; ħ[$A %| ЮTkKN:˲:+3Um6@k/ lCxz5>u 4msވ'l$XBNM;m<ĭD.{-x&v@ ;9}#"̞a20R 4$c4Є2 40S|24 ϐ rI C>eNMB1XA?vO{|![A Y@+vp@2Lpv@dsJ1LgHPu8 dG@ x"HL&:PH*Z*\\X"*,$ HF5H@zO8DHNЅKh _ 'B8!-mL]ds̃?Bv$rCiq#4H |% \5X.w(. f&z %2PFT`` s%=JH/ 8Y?`!D%6A pCg9nڳj 87xN*H@Oe$p=!9N,OŨ4\jdT02a=1L([6 8 ?C AQvh)PjF^eU/Tf(HF1R!60ـXEXP!hB$nBx*hxc?6+_ -j H0ݖV+?86@}%HHwXEt{ȢXFiX(kz_ VpmrX AG<`9@Vk<􋺼jU zL 0" kҫ Lt˦WNaKq0E[=TX<X Zk *U尊CRxc }a/vǠ"~!K\)F!pw{\ b |)w5و͈1x`YhF0lfd6ɞ缛 v;b< |CFt.R_$:E(4i|tf|2e$!FHR#@l`4HPָv. ᮁl 1N8ncAj[q misMpN7p@wMA[77*}}?8A zm; 4@ǘff gCN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOpNxϻO(k̠ EF=r}M&0!AptneU{#HPэKȆbx/+@:` /#@qޡ= }HP2ӢEXq,y b|-\\7a@9VkW$`&N~M'2nt~X.` Q~`x!`0`:p~ppdqr`p+Y1pzf` `~ Q`+@ @m1@q Wł@p U+/@:nAA4,|jr$0)'GKfPpHAEHI@ۣ#F@0J!: 9@Ha1 as1 @>(0 !,W!,W!,.] H*\ȰÇ#JHŋ3j0Ǐ? ɓ(S\ɲ˗0cf@3:a 73I2*]ʴӧG'#ԫXjʵWDif `Ρ4a'&("L#߿K@X|!q*LLy'W)ϠCVHB7}rHM@ V8B&Nʝ=?5G N'C`Y!T/w&Mر{SvVO in0WvlbI G | <ļށ&+K+FD+bO?ta?á ,&2{L=l؇P/BD#;`:3t_L6BG3Tie0dber>)?3Hl٦όDe;iR˩& @8 O(YuPN9pаY-?1/Ki0ibҞi-- +6~@`SXA"ɒtgm3673;kn}@ H H lqO>7Z i <*ҐEpVR'D0-Ql-8#xBx 0ao$($p4 BTR'@XD%JdU9AV?ZLæZD H ,#HP(N*hèai2vd kM@ 5C,4dDF<@.(M9X6` # /RX8IC$@,h dmKUFR-6n Gh{hbB-@Y> 2H-,ZBF*Qҝy8dQ|OG=1nWޥmh0uls]fzWo}#ሠ D 瀇)^`sY@.RrwFgV3/}u"{GLA#$Ps}1._h"-\/!h 1{#)q0ʕq qh)<2E:ZaJd= aС`,B[z+"P& A 1@ĺ%`/YђV!-i&ȴSv)' 8@QBLaIh`oH+Yؤ ?."6 Rv0pEb2WDar)GZwN-V/([<D.,^q`# y;%aqA&ږO@} f ovS@=NHp˛;PWr[ձ>uLV2`ؓIvdZEhOpNxϻOO;񐏼'O[ϼ7{GOқOWֻgOϽwOO;ЏO[Ͼ{OOQ-P2X_,s~v4!mp 0p WHp81p 6@Q|F:tg5+ p@srDB@ %(x@ 2 := @K$ `K4e@&|3 ` Pr'+Y@Qxix36h'jrH @p6 [ $`K2|(+` vw` 0w9+3 Ps/4[p$ @  (| <01(q@ wb54@  4sW P9@8` @ ,>c s%hs?@$ՈS@pЅ~Gp`6 ]mZa5 4 1  @ $`P0p٨݀ \Hwe 8pԅ $xiр W;p@| @ C@YK"` Bp.ȅŧjEiGPW:@P >PpPqe< @ EQ HR,$ peД1S@ ~$rI@>hR@hp;_wwq@~+:K|4!1@NP5p $p B` ݀{9x pPV8j F`[Yw5@ cF!P ~` 'Pgb`p1y4  Q8 8`mIw@UB;V A[4FIs(-8E@Фv4@'p pù{5P,ӛbTyu#ph>@( ` ^" >_'`<@> Xm_w+@h@. 0 ` l!Cq:`™jEp Bkx @+K `WAFfmZ*BgxgwF)v!,W!,.] H*\ȰÇ#JHŋ3jq` ɓ(S\ɲ˗0cXA ~С¿3 JѣFI#ҧPJJՁ wX6@]˶[ 4K\x3X/ ~LÄ-qwb"$iƪvW*D̹D<Ⴅti.N6]Ob˾$FOBlͻNXqǹS/r=RvNuϭbw1{/k_O$N);۲!>-'lSA d0"L-ThK34"M7(?3∞,XN"2>XPhOH7@)AG*Ԣjd- 'K!rې\vT$"8r B 5$@<})ydO69QNe?Wb tjI 9 $$C9rv[v"d-z>''?~꫰V )#$ܳ2D)kTx.Y*R:j y 4A*3pZ{ muG8=臦ϐϴ0kjqV 13Rr:؁N9,T 4 3\s]M&tu2l#ΪϬ|UJ&,rSA$lq%lۄJDsġs</嘿B,_~Ke\QW bu  Dg;@Hi`X_+W7(>gw/o觯S Lr/OҽYK Xﳁ#` ,X"AP0z ,@e/&W0@Er $c; %HK$.0H%8H O`vsF<2ʀ(1*>J<"IMm4%Jf'C93O 5-7Z 4IK"_3AKZ:|F-iٌf K0b"- ONf{(`@XM\hb.RaB2}`): # @9}A9P (  AiiE鸳(Y#Qh:Z#@B`vb =Hhp-\bue C!x;hnr`T}00 xpl|B= :pD;6F A4ÿPƏw032 C:|Hb F_'Hr]1?BI.n#QOz <$J!* X ۀ{3i!(o͈N/F3p9`Y{δ;N RԣNWVհgMZָεw^MbNf;ЎMj[ζn{MrNvMzη~NO;'N[ϸ7{ GN[cD6rhy'MXoM4AGj_a,+8ĂBA{Y;@Kcj!PRa{c"yAjBSB<bL 8%@ap0T߇w(A'(Hw[8`8'8n5do?Ji! BQ8AԱ! !j pU8zQ#dKaÒbX# E"j2w<@!oB  1/Yu4=pR`&@ 4@P~Ǧpw1 @IC[ @{@ ?5+ go[ 2PVfP0p1Gl{pCY ~ p bS6[q` =84 ֳopЄ&`p 5lcO=0;p SP31IPF RM(ހ `` |72>ЊO( peZ  Ac P~:BQI Y0/`pp bgJ P1{(ȌV$p XlR03> XBrP0Jh ` t `ȋb0$Af(@-b`p`2 pB/ ېEh `0S@ p <`Xpsyf µj0 wlp@<2K %@4@/N3p @[ @I W @Li/DHEio0Y1 :`{pQ# CS40=x8b P b a/p  8@hoÝPs&S `QGs "086GA?9% ~q I` @7p[IopR-jaX9xaC:-6 @ FACʗk!,W!,W!,|.] H*\ȰÇ#JHŋ3jȱ C I2Ǔ(S\ɲ˗0cʌ(AB ;X0΂$IѣH*]zNA2JիX\V޿{TݢaeJݻxp80 x*i!bJLe 23}a1V% qL۔ o,\۷-6iZB7سgemY+V,ވeA IчO)OV*Z`? 6؏7 s~f qwt "yE9,02Ak/PN>]ph8%L*|cˏ?@8,ۤ?L688d;KhX!uYbhWH>[dpT.!L-xL,jвsD6hJr E"BB,cN'lC7p 838ۣudŤ"kA psO;` m!AucO;ÃOGS f8g*vYK09+z(vVV@)R;&A+'@4 䓄60m*zˠRw1v/@?Es D^,'A %dЃ:SN>H<[knKRLUESU)ވ B1DhMmvJt,';JmZL%ty"` BFux'D?],BWp$Q 0&H!2Fb 0,;HG:αtcS`o $m,e# /J t"ȂeЖ 79%(KQғ6JV:>Xe+YMrv)%/(pM!Q[t$IMHT D5+Q G8Ae6v^ee+f/' F%IPHB\ T͐1G|Q(CA7 xBB X?;ЃtMQxEE3Ԡ`G-({xQL  8.@=Bd]z TdX4.UږqW-$ˋ4D[" F=zUtKk*w7!1?qNjqKio9WJfK_,Ѡ_v21`>PD#I³{ݪY{E7:_O[rXU=<|0}% [MS1W΀ Rln`B&vp)20]D E$PEP͌ d\uR\_ 0"bR:y̅?F;bNE.\B?lH(ӛևFMޠ8 !bH& /v Zw pa NATf;;MmH[ԦXm>q=p&vMzη~NO;'N[ϸ7{ GN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOv\p'ٗO;,o+OnRM#Pvww2@S 21((ȱ=19 !~exh2  Gke % LWZU!|s(<"A Rn!'@r('Oܓ{5t=ˣ_&Y6ְ|qKE 1@?`y2lgrݰWU6 [{ppW p|^Rw0 }d' 1c 8` }x'E3y~mA$n"b p]xPWTQU: `_F@H@?!`L؁$)F%>p =@c`vnp `@BOTSn@mxL2txKg p g~ g#݀ Ӣ%gU% @ $I  | `p@nXww s胜H Cd(  -6e: %C@@3]]VH W@/kTUXx pZߠxL!0Ԁ @suxw 1rP0`QxD ѐE :a}6` ݀s+@" WVI` ppW/b+21` P`GI P262V o  QP,W4 @X$,p>Є"qP~cK"4x> ` ya`eu!2'p )gQp {2bp×rBQ`~@yY`yE A zVUD)sѠ 8" wn$?g ]` RVA~ 6XY@ #3b` I8xH`/`UU8` @WIC v 1 `DiD% V cD$T 4/ ٦c }!,W!,.] H*\ȰÇ#JHŋ3jȱ C IRǓ(S\ɲ˗0c„ >|`XAvsBH*]ʴӧ*K)$ԫXjʵlW;Fp=nbp0lH˷߿ !\ >oV92˘3k|2uD{'&BZPcSr'2 wNsͻ\<Ⴅ8.\!nx<4InI =ͽK#*'_sVE(&"sĊ1cxQ.dK+K,|LKJ7cRCH(R?$1CuP/BD#u# h@)fLSKc #d?XfO?h#? )df@47AtA`?nSCIe*/1jRd-˓Q2,U%,s)@觠jQu#~Hp='( =ۉd(¨ #,b饙n#6k~A42EPe*3X =(HZP 3˱>eR[Ӌ@]0~S4T <䬗$voJoNZ_jp4|x > "4#D 4=p,{ 袍>o̾ WKd;6Ġ@Ca%uN=El"o|ݒu&p\J;ߌ7c8.`HC$y䓄㨧Xϲ{=02Ǡ;İ~N::xN:JйG/}A 40L2@H(g}5#m4F"GlҐB8&I18&Ǵn0C9< s` p[+! b`( H}s*`G\7~;p@~C ?*T2/x qH*ZeҶWCG` È2hL6pH:1X!' ,@YBc"F.QzOU p MaO1)D ? VVx' 'B/JN tc#90!pap'0j%Ȧ6Kf j%NAy(Ѐ֌U*z % !aNF8a0 *VvE-JQM<Hq oT8D^ R86Mq C9.XAQlB{ -MV1N8CP4JQjDU02!gH`PXYreF4 \X 81EDih'n9־&F3ŋ)#haQ `H-pr$ f`h!o5 ^x bt7Ͳ`w,]J/xXi-Ul+ 5  o5- s,$`,l[(bVa{)#QC"hu=Hu;0^W|Fjk2o3[^ʽp4A! u8 ," {l8~EZH¶~3-'@6LA@DVC! #{Y€< _c^a/iݓH)C8dAWP 8!Ԅ R0wcE! ;mYdKZBdc {Ha K g=fݭ`ی\(F1| [X *A[ A9bnlٴ? 1\E&ant"Hņ~bpWpǖOI{[0V9=T P^G|w8 h`9 z 3pHu @ y@s:L7p >`ci $mS@G'W<~p?CEiG BRI` S`@tB 1vi6 @"B-G PCN!,W!,W!,.] H*\ȰÇ#JHŋ3jȱ C I2Ǔ(S\ɲ˗0c" ? (f)P„ >*TѣH*]ʴӁ%M իXjݪC q͇*Fذv&|p ˷߿!_8|8q "K`̹^asD: QCEv A N\`Y ГouOpq^ËO>bU+҃\8ʽEҟ1wF/}AhJ&|ʃJ*i@!,?v6!|E(V2Wh2M4!}8cyP28H&$Ƅ"4"%/䢆.RfL0ϗ`=aYK(ԛ01:L K>AA;E% &VԢK(|hRr,β%-e7xO9C,jꩈFJ (PA^r3س{ԧ$ Ijv|4)X˳he**vx^s:sdc9n` =3)=$~,LZKWl-ؖ,\Yr*C < Up( v*Ĉ;:ӎ,'+3->PeÎ:cs=8G#(#d0΀F0&#Ә1DqCTH‰@؄&T@q΢!XV29`BA HĈ́ SC0ALd"X!WXъp(xW܂WHFd1@hd@A, Kݕx<pz|G?"dH@ã$'IJnPo&1y򓠴(GIRL*WV򕰌,gy3 @>`,o!@ &f:Ɍ&_HO⚤@CG 6A Q` @I€0@zsgN,s X@QҁH(:8A(CY A0@B!@G$(Mi LR00@S:a?iM#@Hhdq#4 }rH`T<|C}A!1(z*TxYZ N88*rb\ )*@ E(S*]v`!P" GP3AN#(0!fマG9DŏxqtB E&% vDN B n/d ^" . 2y>5I^"0\& DBK*' |9Xf0G:X1RQ#[rph^ 7X@^F, + 'n, Fr7?2SrV`)XJł]iZi+a@&cY_C!Q bn[Xg8n.pf dET1*H@}ᆉ޹@:_!W Wa,j Y:ǐ6s$~@=1<(d ؃\xׂ2z Fl쵶Uj[Úk0y$!,a Q3p {Bv<v#P1]ఌ{HbsA7liؒ%\PIƸ=rHz8ep&Q}lb.HȣO/g#8C.0DtX pEaQCDz? A=C$~NK9$HNmd !&v"NSu{$4 t^8;&Os&AP>3G̪?=WӧgOϽwOO;ЏO[Ͼ{OOOϿ8Xx ؀8Xx؁ "8$X&x(*,؂.8o|7U/X^opVP4h] 0LV1s`1# i?Y _|p% BPWa?P D{[NQ !N@P`UP F06G P+pp /0M!s3@ P`l $ͶH @[06(|p 8#D>$ @ t"xpuv\@P|߰ ? 8L( =@Pe` @ B$\p m5e=0 2|@ p 4 K8 4 @ 1  p'P0pCL  Xp @@ l@Bi|".LPe0`!`p Uy5`FC\ ``x|` Q `)&Q 3`(5wKB` &%58p 6|Bp VNG!U$p Qi$^􆟹f1< p@%ݰ @8q4"swcVp>I 0i9HPp|smXe;1_bp/`}sa8@e??)[Pps :Uf : 4pXaC@ :m:6=9" R[KhF8z^z!,W!,.] H*\ȰÇ#JHŋ3jȱG!B(d&?\ɲ˗0cʜi ,tX :|h_PH*]ʴӧP+0o(DNZe|Ê:8{p)>6i ll: `'M6U`P)Q" 0P=0P&:޹KZ/֎nx133`D<SxCh5**5h~(,[xǻ7N:"u@!SFd;lBZW38M0 WÈ*M(@~2Hӌ7Ҧjl&!8A,qt ЄVP:%s VpD,xA, dWD*``H: !8;>1bzZ.@A Iq@, `',\ (rB:#HpNi+#!,t*=Th%fx(!TIKZ=,A05*P @KB,[# !D bT +3AUB*Un n[T2(3xB93W9@49$^c 9IQ2x ``_ǿPAu9V1$4xE9>hN/=AG:)nK]P  bȆ;Yۘp1%;DqYm*f ]bF@>ǵQ:ִ73}kvZ*}k(lt8A" x:lP~XU{/nk{8*V0%g:dA( ׌L6Tb >0jHF򱒌dNx"-x0?0l#3TNP)<%3x%Ù$z q f5W?kŃ@ @+ j pP$`Q*rGI11 c)C yAC `{7 37!,W!,|.] H*\ȰÇ#JHŋ3jȱǏ C)R ɒ S\ɲ˗0c A¿:ɳϟ@ Jɑ&O]ʴӧP 2XVU}όTʵׯ`Ê  Ҫ5[Vm 2LKwcw%l8sD 1{#KL@.O` !Tt%MRUװcCemY+Vp:po ϏУK0W(5Zi*W[^?~օ'>B:_`Ay9 |A| 6@҆0Tha0dK-"-#xc(|"B8g?b" HD`(*)D&$0$$ؒ, -!(7?\v ( A#9ю<("A #C x'ld4dhY/-=휃 '(tN/uȘCc:蓅Kg-LVX C%&ꮼFT!J9ȱu)/D"&`N eZ焯NYXvn@ûEs 98o 3'n-d:Ik璘3MD6qAxsQGl9>|`4(?Ąz6Md>qأNU`D4 t d'9%\ pγFi.:p9O4GHp0A!<E9ۈ'3T GL2!3O.4k8`ȡU&p=P$lq?ex`;\1qo<'G߲bL1$՛ؗo~T@@@<о#A]tI  \D>L}:'H Z̠7rE  , # #0t!JVHÌTM 8A. 4pH40%:щM|",P o.+p$nP޸:avB:`@v1/ڑw. J>{]`#,ᐈtD C@WH Gc6ep92A$W?`,+Hrp5qRx9~KrCW mX>܋ 0 x^cL8k&\H:cCh4zCG{ƩA݅09 L?La l\A؂Qdتb#"#b67= l'T $pl¤@U1MgVᄠV68%vx%a^$=dQs$PS׺OEuRjT#m*[#6vExL>qh9.A.µPJS1  V+ IGx9oj Uѷ* mhPRkH͑6sDa}<0DB皗AÅzg@ QhI4BP a. Ǡ kGZ/,k` Z(h@r`esjd࢝43J,lR(؂< 6aT!H}q%+x=΃rz1Db\Lf< }gNs-f56 hgtsπE.@B@ }(ѐ'Miҗ7i`.촨GMRԨNWVհgMZָεw^MbNf;ЎMj[ζn{MrNvMzη~NO;'N[ϸ7{ 9؁ Ԥ&Og-ψ\l2ٛ%_#v*h!7m)4w*!|Ahݨ{^ 8N)C ĚC4P,;BX:pgG8E,n;})r89k2[# ':H`@ "J`Ȑl@Q!Ag@ u"<ں@hYo8aK͗ l O$%S3` 0c6jg}p p ?swAFRPu :## ~@P~ A@ Pv,Ejg 0{ ! F `P&3[`W 4 p"'tb'$`P0p_ a ?w& [p j p@ hk @#*R*|`p ` 7 PoFH NP^f  9݀  h zh}2]< @ "R-  peE]B` p Eop @7j20~ ph pM(Ì0CϨNO:0 FÐ1`G`=`tbPN&35PBt @3 sB24`b3b`pr`!Ȏ  &FI/@`` oxhl10p5Y1E@PD9xjY`'vFI@j ` /p 6"gpQЀEQٚ 83f- :0 q)倚} F  Ii0 ` lr qaik-Kѝ9"0d`YI` a b@5۹I *UjpB pAXk Br:*vB@!,W!,W!,.` H*\ȰÇ#JHŋ3jȱǏC)R I S\ɲ˗0c0AvifN@ JѣH*<9$JJJXw-z#{=P$ʝKݻx3BpB`o +P!ǐ#_Ƒ-Ӡ"9Eo/Yv %c˞Mm.<Ⴅwo.NpK94)_niУpbŊS9'fPW$u/X˟&T]1b +XRL?h>ḩ &A28bpwB82A@7$L(T4G)"LH3 /xϏ@+idlb(7p`80?uTds`P-.Y,56bL /!|NaIH{}а9t$ ~C}Vj1ml6-kIw骬r83%\28ZkkTinzifs̝xyd҃^hB= 0l8Bi+oGr)#19g1P+$![1$8Lw@y/Ζ*g0LecŇjAD 42ۀD_r©$-*JpT+8nuH$?Ā ,hl-f˒lNϲc}P0EP9N9,O'8Ϥ|Wn9 33L2d 4>4h#?xq"8lCX&!Vn!q4OlQG bEȇ? Gg (ZM/\P<# ,F1 :D8B 7z GH(L WB YX,X 0`}  w8B@ XQ ~A <!8 !!@@hX 0-z`bh2Y$aN tcB\@:VѨ4(( Y. d!B@K$1=dqR2 k8! ByV $f9J %$*,䒖%!be+;ct @x ,QHұ,󛯫T aӜD&qNa4CD9ٌf!ƮV⦦i,T&x8:nS| XqTX Ij[U/˱iZk) Rvjj/l<038η抉"tg! QU`kQk2uz+,!C9@4 |Nq֝n-{_^x7oЎn!:zKXGx'l$;/*: ih- a4>0>@!@C:b{U 5t" r. "C, \"PAC8C2PH+`49lvl4mЃp̀B pNthCш2HA[җ7 DL{Ӟ洨R=WհgMZָεw^MbNf;ЎMj[ζn{MrNvMzη~NO;'N[ϸ7{ GNBr5RP-| NLzb (~G9D$ص HP  5(н 8;юO~ρZJ5zƐ t!hdžy|OA <@;aM DX%:# dzV+Pj9BzQ`=nevvw8)AH@ 堄].#k>10=z<#8Rv$l$ ۀS+#4:͝#qn ЂB@ bk˰P{s ` @p |k@}$ [Y&@ ^78 c{o` #R+ 7 c 7V` aqj#p BkjzI@ H LN3 `S }J pPwZ{@ @T8 v`6$@ RP@` X! 9uS@ }f(WPhk4 p! Pp Qc 1`wQ% S  0^p H PgWP0S B yI2`B`>hz(46}We6/ ~Mϰ ` R@1 ʥrP~  `xxuH@ 'x2R eH01q$`]zF 0 Pi5tj ~p 0,9  + ` p9 pГ @ rݤؐ uP)%Z(8ޠq%BplhnЎ5pE| 1py =0C*0V@(߰ A3rP` /=@ G`瀚'ُwp  + 9Pp1+8Yym0 k` @/08{wkωj(r~p @w c } p p 'Wy wz6~G 03W2u` @\>pn "3g<k@ k @p$0, LC>H!p `I!l P  X0 Y~C)KP}!x`  ,R"W`jC~ 5MYjpWpa l"P&~pRR8`P Pr!<x>p3F  r7P`F~`Пrn`0TWrE`s 7B n@1&`c kp@F< `> ab  GpRt  1Qݰt @3(D ^f<NJ,ʵy-gG+Α$ `O': u`  p0lk^ %Dgs`~ 0nzQKP"Pp^"x%yxxp[p P!I b aM n@ :43D( k>Єk> v9Zw.hiCV% XBHc" HW> QIP *EPRX܆Jrp8A|qU*td 'F{3 K=rx7Z||S@g]+!,W!,q H*\ȰÇ#JHŋ3jȱǏ CIɓ(Sd@@ $ *\ɳϟ@ JѣH*]! DDE ͉zGMÊK6,(-F,עo*뒊٫õ\ s簧 .xw[/l?#J 0`gLpL4H0Sg:ݖzQSZGͮv޼v敭Y^ja%c Q{^Ȑ"(\p+a@ {F}[޺RjbD+ [~ł `Ǯẻ_V@9ހ~H:&$ v" 27j<vL8Dv%MtG,k&݂]N3% <, Nw `ش.a ؄&VjPd!qLa ?c DКAtbWt"h Uhn+\Q?n!fOA " &"ЁzG>@hsƠMa;^" Ȉ^xT?aJP7A #{ }{"[Wn Dob0E-_x4\V`Dac<: -[=.cNkGu7˨g ~=1SL8=pIW쀿Q >@ps2^1>=𸵀! IX@U<Ѹ P0 I@H!4`4 +H?@xuKxLp _RepPnw6H8 | Ѓ= 2 ɐ ? @ x rp4V!,$ `>3`%4> *${d SYC7C;ԉ Vp0`BǙry@*Qɟ9ANr7\6PHc @>bd0O-9H*qJZH>@ &oƄLLX*+ Q (ZgiڦkOIIj"PJ P 0Q ` Pp?zdS8 -`- PJ`\UVTXnp F`@0J k z nPe :5k0p; 8`6?`vHp۱;(a*۲.,2;4[6{8:<۳>@B;D[F{HJL۴NPR;T[V{XZ\۵^`b;d[f{hjl۶npr;t[v{xz|۷~;[{۸;[{۹+&)IG6Qr3fx yx`Y z +PW }'b¢ǰ8J  Q` /A{ .8FZ AF 4W Đ3 0` @ @ې pPr01 b@}!`h  IbɺKYi#Pn Cp%G`OM`  $̒ln'2C C9%Э(-01N@C0i5RQf4۰F"QvpN2g!` t@ X.ӪJRB`iJ3X_` H1R!B7dE 8!уL :9`BPRW+5Yt#M|p9zQɺٲB=#"ZaD 瀇)^p>qd>qMmbl7ڶny]_؂~GT0"vZp(@ |LA, /d+<>`[=C?vr"P@<>AyHVcWXxov{N-`( VP8h*D@ ˘A PCP[GY¨,bLVDP 8@>La,n1Y $fMAut _sҌL#4Wk ^3L D_[X=[`N+h];@zS:Xp@"у(׺Nbp p-AsP %ξ;p{% |hLjԿuTc 9Rp 8p 6luw@ 'ݰ 1ԀSgz p  p fHi6umRJ$ &n 0GhuSjbp6xUtS Xo!,W!,W!,.] H*\ȰÇ#JHŋ3jȱǏ CZ A$I&O\ɲ˗0cja$HAϟ@ JѣH1,2eҧPJ tVLI{0ٳhӪ]# ʍ ۹d0_|Èg*քPcSr'&̹ϠrX .[,iMDhgE~&8TNة&VSi |+\^Ǿ}?lg[_7}%WP/BD#5FQ3-bK.j! sK?h?s!0& h)!DST@)DioI!䓰t,x ?+؍=YSG٠Q)9p=t tiP6d-D˔U3b[v坈69DpfT92Mf馜.O)T" -XfYhvY8B!A~S4TPثkyɧz%\ 53=M`8akV~Ij튭oK,,CN$68cIVF,F%g ꊫ;- gA 49 'BQ&? 18ѻ3;kA﬒YB-N:NHgUSJ#0#$#M5t8tNrpm@k~0!W,dhIe,݈'\1qD9@@.y{Qƿt-"SrS("0А (  (?1@VWorO/ؗo觯/.جc` 6 (`Kc:tU0Oڒȃ4K^6CmF/{T-K-c]Ss q1oA:^-,S-r]3C@\![TM$8_^؀6Adbפ Q$RBߗ$*PC+DlBE*B%n `O39Vс|r0_CGЈNYVL[-@Ă G:@?ЃӪ^Oד PH> 1f'E |pBuQZ5e@ĐGf lP'qիE*t~6Qu888<G>ƚz~jy6BE/0Z+X D [蠨dnA}p.r 8]A>πMBЈNF;ѐ'MJ[Ҙδ7N{ӠGMRԨNWVհgMZָεw^MbNf;ЎMj[ζn{MrNvMzηdجe  U@ؠ K@g ((9;o >19*yT`E9@!'oCeA9V \?|1l`=:pnC€89?,yV!HȽ1_3rB-v2eDcj91A @B7 ~FB@؏?%:CPD6% ҅rbY0*$s/SoۀG: ̾x06N };!t,"=06ҡt( `Ef}tqK0Bl   N`pp 05 1BЦp p p 'Wp <0"(ˇ 4@ ?0p0Mp &l c@ ExP p5=P0 $ @ ݐ V_L` `m3R2 PF h 3p @[ `P [@+P= Pm4a @30>Df@1  b:8O`@y3`S[p P6I ~ @s8 7" D& ?17w@;HE!,W!,|.] H*\ȰÇ#JHŋ3jȱǏ CɒOɲ˗0c_&H(aB@~IѣH*]ʴS*MTիX12 d#a=| M#vfKݻx   Wp*LXxB Lr Vܭ7=n!rPlIPٲװc˞M Cp›7#OveO' 'mԞNeIԳr*XqBɋB˟O?!\dJ?WL}h?&1& x'1 =rXAw9"t bw'Ա =M@,/cR-8L!-C9i?k4M0#;3$3 #@#{LS ;M:@&aBCX?»'ȁ$pPs1\́x-_,B˒W,7q"D@ 'HA` @ n@"5" c^p 0̶DP "ʡwC@ H"HL&:jl؁(x8x` WEA)@[ D%R~haǼ` iBp19XȢG9# !71 d@89O$LS\.p ZJW` T@u8@6bPŐ@7QT0F*fLpIG dpUh*Q0FT h4J ѬR^HƏьL0B92VB -u0 Z ؃<~ p׾l_/aUaN@/U :+^$@ YT@El}sMmMb=?}5U-]eX# @2Te`puK]qT .nVҁ +A$ r,ch! px=&Z:{ͭ B`4>ҲDĂGJcHwS-F8Emt)傛:dW@Q8+` PDP~.U$Fy)QT?゜ `C=|09Č/y*T&^UlvO k8|Wpt >@xP{[` X EWIBեv,pe4A-k~6 8 `;;@mjWӮv{24iq 4v.@oy{m~Hp xń;'N[ϸ7{ GN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOpNxϻOOKĬ-s 2B;r쵗7<*clC,q|ȃln{q\"tg@Q{zd  Vю+X!R@)4;ZIGD4{P&`qD͓z^@G80'"@4 10Qp=0uvr` ` ;$0'@ 4W Đ3 0`ePq %ihw2(" z`  0V =/6" 0<9 ]m0 6A z RlfCj "pe@ 00T0E`0[" ` P R`=@ߧЈk!eG^`PLWE[! F ``(]P"pPp PB ' D& 0 `.gPx%p@ !ހ @ rbr ("0Pn!q0? dޠ1ye7p҅pb ː݀  *wp0W 4+  Q 0pf+@ pZ0 < !W` - 4~-1@n =\I@ e9z$p σ @ 6԰ F@4`Pdp $E00[P )@P[4 Syp>z! @gC0@WF e4l @Or ؀E 7؞6pGP@@C@FC ~Pu$0HIp"  f 8*ԀĀ ` @" " }jIޠ lp a>p<`+We@ ~Scx pxY"$pB{ `1R춠/('Rd@k)dE h!:^vP@&n!,W!,.] H*\ȰÇ#JHŋ3jȱǏ CɓOɲ˗0cZPa¿ ?t0aBSѣH*]ʴӧPTjԫXj5(6zCؽE#vdݻx˷U B`*dɘ{Z귲+';1fxa1V)plޑL۸s,sy2wp.\ĀsOlmrTkT91"z3A Y0Rc80((DidVdˆ0-1OB6ϖ\>]rGiPxAAX@Sԃ9"AC efj衈&ZP-䒵D)/L9K-RK1h?ր7?(jjT)~HQU1@3X`A,a;B©k챗1ꨒH:i\-i~v DO:,SD} N;D`8RC ?6,SL1Yj,qFq9O )`< A0, \ l`ym-D/,޸by\ +:H;WL@1:L@x-dlJ9R\e-CQ" 0P=0P&֭v6i?k)9/rq4K <M07䰁P3R!]b.w-,kSKϗc+=s8ÎC@H9;d@gKJ#$ 3$3M,|LC~83n!K0CXpBDA:Hh+́ O@8! IC+.BaoSP" )4[rN"|`5t w p!pD 2O"#>̢řq` (thL6pH:x̣>*FX@D ҏ@@JZ򒗔҃@x?Jb#lHe$` &gI`.u](P d"CZV'E:1b` tWXtVc248lj.0:]s.@z:ABpB=!{!+CH^P#>@7c"A޳H(MӨS2 ׯ~l1[yě&GlvmiOXqB"=#`Lfńe)l,' >?ӫ_Ͼ\:tu>QπC28`u5hVZp 84du  D1!@*<'80(#i bˎ; #H*#2ĴϑH6I"3F YD(+Al9!?D`"-Ti(R)t֙Q-xc,䘣-Q.~L*4yd>L*vFq5|I%BbsHȃ"J=b( K>H*무g~*(2?ڪ,C~7e%tN/u A=O",춻dRګ-h6R*A<@ibPKu)/D"&`N  \꫽rM("JB4; 98Y 3qL7tڲ,/벿0?m'" Z%fO DAT"؀.8QOkWgg,'u.'XB:HpJ$耠 =@⁊ [&(M)j ,iL+a5T1)!PQ($H(юd` $eD 9,J܈F$cTv7g-(?l Ą hG>J6nƏa#Løƨ 9BJR$aC@!R jQ*sHV zܣ$*э :bX(HC~X/B%p}ld q Gx3} PE~}>c8 `[$ p}`Np!H4 M8 :Cr`!'t @ ֐/ `}{  0@' G '<`f3؄MR" V *b@ hcd! eDxOhF#4J`PDtikXI$~  p}H* @ fln 6r@ l N US@" ppȋ"N ޢ& RKPٳ{hƀz g` `}R P~#l   1N01j@ G`ЏTV @{I@灖1o6p Nt:qp 8 ` ԃp 33@] <A$p=@ πw@Wp|p@& @ 田 ې( `@ ExP wS pI ROr(xYQ7rHpRp :tGpK &:hc7P7:K`f EZ$7p >oLp SDI:L 0l@=փ=ap`  Px> $IHr~ 0t"Њ"A)x`f-"APAI ~F ) Rpx/@ Tr t Pn[:"F A Wb Х x!,W!,.] H*\ȰÇ#JHŋ3jȱǏ C1ɓ&LI˗0cN 3L;k)ѣH*]ʴӧPESFSď\xDLOIMyF: =T@d@4lcg=4(骬j{ Ih*Yh-&3*K> #BT3%2tv$"CLZrq!X3@=$cL:?T?`g<+*p6~'?!3#+O\Sintx-E"{XbN Ho \HiJ6Uz4J[E>nb.PeA9@1NJլbgKj2r[0I~F*"7%qQ@gU#R- EPb"{2 ))ak\0:jYz:Ԉ ak*Dr "@*1A˲KX:UdՎ!0;r Vly*2Ѓh9Q(vۑjj,fQ[\Igo;:zᄱ Db lj0a9=@G;`7d~׶6*b 1q|c0B 4+G; 5P]2LtxwXa@.y W@B`vb =Hhp-\ul`Ch23XP:r ”p,`$&6!q Sc@$#V ^Ey5 SIkSː@7ZkC.`k^C +\ VBv?qd[hB(GuD &A*0 ,2r8'36p t[ F~>MGLx8'NqG 7{ GN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOpNxϻOO;񐏼'O[?VSkٰ@qɯhN&6r?!Z_QQN(|,~~Xa9BSH-`PcǶ DdT^POW;`Rho3E`KIgC~Uu&P`xT/R@h xPGU0 "v&SKP&`@$~Gp p56/be=x<Z)K$,K'!h @,  Eȅ!,W!,W!,.] H*\ȰÇ#JHŋ3jȱǏ CI2a(O L˗0cƔ BrvP9-L)ѣH*]ʴӧPDeԫXd޽=3R! >eSCJݻx% 4L_ (<7>쳯e 23n JcU+=u8tO c˞M۷o,\<[ 8iZ"9/Ml[VDĊ$"=R~2X˟O}Tiſ\d:𣠂M *Uv`ywWM@u05桅,0XQ0aˍ8LS =i?xs!aa[ASFm>X0dihL.jR˛p*9, s:Q"$P,4¨>Gk3nH@P4E$8A"ICw`3:/j> 4maF(( @5F:2 <`#! <AW8DС +A%WJPU $f JT" v%-!G0q ` 3ȂZy <9 Ȃp11c8j!Hg:QItL'2Q=w(9"oCYA$xBc)3vb|xaĩ"(/ьBdB-p#fQ 8 HD3?6!Ѣu6 c0ٺ/paRYPp>% u( `+xc[ :D8Cu8>@xKRj>UQ2^OYz|p{c?BUȚҚ)oYLN(U RѨG/ |p"QGb각:+NK$fc1 aM-Vm;N`#4`Λ4P@SX)[w 0/xU%a}F@ݸ D [蠇@ bHMpp jM5-]x脁 \֪* >3`stۨPhRHG71ɊLdP20Qd4(/H*@*AD5 {c1XHtẓFO؀ʀB`Q4"&4.rI"ѮpE1R YZqf@ҁ P VD1+@X2p`<}-l`N\vjK-'̶ nSYNbKMr+;vMzη~NO;'N[ϸ7{ GN(OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhO4 4RN㫵؃717y3Iu<vI!=d@SdP[vI?p)fP3 a Lw7A ("h +DJ`ԠA@ 6OCY ~KkB#=dqa߂WX;э-hy5{C~=-G]~wW  pez`` ` hZ@ }@ (hTb`p@eRP G5C@'v@ Y` Ãw>C(v  Q`WNxT`fjG{@K`)_d kQ!,W!,|.] H*\ȰÇ#JHŋ3jȱǏ CId(SRɗ0c„ : A̟@ JѣH*]JeJNC0Jg w bڿU&> ,,J?$`?죠R}"DZ08btPxl!5E0(4S000أ1!M-> 6JK2}`xse EPG9&=TpI_-0Y,R.'/s? {WVD;T˘ #Gp 83ڣꪬE?v8| (zhv$AucO;hGS v +#2}bLZ8(ue҃Z b|*8 |3$aG,kL!垛g|h›輎R$!t`d sPA=9䃎$&-DSӚZ̮JrT}@8VAp7"`8(rL>lMJi̬ ? wQV ,:AVf@%t"` B g1{bދ Ahs#lX` oo|4 33L2Ӕ4<4 o4R3`B:e^I$~/9OP1G?D_W9L, Y$0e  vWRY,H>8@x [8 8QHJ1NNH"EdaP"H*ZX̢.z` HƌX  ,@IޘF(`ẹ>J p֖t@c` R⒚肶N (1QbwM,P8,qyBk<@v0qs'(pA 9MԤ5q%xNMoF @Y\pR!<~7qG8Q6[%J@#PBp7ю GG9ZT8F7tpr9>u N# PVRbH0a`AJm4-m!#}0B. ?c ܡF 8Ժڵ[G0:."+^ ȸ {,'%cN!-0X Z l@"a?CDٻ1P.e.#)vQ}Șpx9#+:PNzFtJ.Uv욅oˤq{#CkA> !,h90dw`ItSy[Z7]{2N긅Lϱu<ы$J݂Lvp;u{Kg $ c69 4)ҡ5[ʱх6+[B \nW0~{ 3dt@/[ xE^v0qeXγe>Dr4 +KB+v9fG6&W7iS3 bF*,boCy5BqT6Q~t@(Di䑀墆0-F.O>L!R%/c?`>W Y|C;4PLQOy)蠄jR?.(j餶G6ibu%Q( 3س4T @?Tk|)*vj.^ꩦJ$I*S@36vj;E",э;# >:sXU9`_% ,o7 $8 3TÈ*Ï?5H?P=Qp&T@؄#~; ؄%@.z ԗ08s W8"` %D!qWІ00*k `R  PP<8El1Z<0F@ɌbpIp:q(|>Vp IBL"F:MQ!d,XP IR*L*WJE !f0ʛCB*0BA&ǒ,Xb,L2 hЌ41'?8`爇)7Bu(\@:Vt ؆=~>@ J@P((B]!8a (xЈF?!<&+K }A< k8! 2,=?wATx$:JT" zQJP&ժF!(2 C%aR X%D#czx1d01W g6xF:X*tG1 <`VM^;K\/ ^\NKO14}DñA D w<2o8!уL@(txǛE=xRXlPVP&Px< Qua5R`6y)0jzo|}HrE P* U!+ 1(@8쁏zcwl&LUfm 'A5[\1Nwt[09[G yA 򠄈3xa)5PEX`/Xue C!‹f;3VE MzթCj"81 ,p%,`8)ȡ<K0>?KCνy lG*l&“!=B``=aE.ΐ W? 2sЇDc,}#H D`w@8chOS(`xO: lw};i0[O 8y'{_7xVzGOϛOWֻgOϽwOO;ЏO[Ͼ{OOOϿ8Xx ؀8Xx؁ "8$X&xUGFgF1px(xi 4frA FPg6rf C؁  1Hvq?#' b" W#>p =@v4r!`8jxiC!P-ȁ HЄ}c` r0RjV`Ł 0fl` Q #`+W@(<3`Fp;"@%8 q( @ 0M`(?G p"x ݀CP;?@=cp 4 R8^ C#X[ @$c3H@LJH ؖ#@3 X=76dbY GA2P6!Ht eX :TKrXX|_QZ:*ը\Mu2U 8D?ЃS=kz kf7KKDa "8H W@8a  oݦ.4T B,AA{#xL`$BM˜C` S0.Αg/8n"1TSж0n38ւn$zz a K"h,P# cŌ \30f8jǀe0"b7G/@(t`2LBp,& \ qqMoG? 4jP ,?dZZ/v]C׽w h!̀pζ l ADrTL7= @xzN7 {XNO£.o9tDq 21QӰ+wgN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOpNxϻOO;񐏼'O[~/Ng@tyFSxG/|aKs>RFɝ9$q>F7.J_!P,pdS-&ݠ#{K C]ψȏ|`+  -l G< 0'BBH`1h@! c 0 k1s 6 6"0 ljH`@ l@ɧ|p+P `@k9(^ `sY V @ P4G =AuH? ;?&;A| 8rV5AĈTB4B 1 1EGpKC3K` 色h, 08X 怋 b8x) K~1bp R 6ܸ)"?(V!,W!,.] H*\ȰÇ#JHŋ3jȱǏ CII!Re0cz ;X04?dSѣH*]ʴӧPJVSR*f+>TݢfصJݻx˷T @h@ SPcT}VEᅩ{X{- jN^ͺװc˞=*\ܻBȓ=L;*M ?ڠ,ވeA f' ӫ_ϾaQժ~}Xڄo(s#s|IutOf8[CHO'Ա =$h(hK0dbˋ/ Sˋ .#E0&2dQ& #=%=G\v`TK-1dF)/Ĩ8GpR2BB,cN'lCs$Y2y磐F*&Y&CccL/r^49D'suPAӎ>`ޢY뮼V ɦ ;+D8B$ b|*8=k覫Cf"ͲֹnB X9S+?XM5D[G,[,2-)K@,,CN$6ePPp7"(##@,i-9*NA 49`B` 9](L?R<h6R)c ,PX;!A8EN ;+sN4G3H03L2TSH+k6Ӭ>]sPL@$\}MY'ȁ$ /(H` Qh2vxb}錨Dȋư/6ʋfLb2n4lg/l B7d! ,Nh" + P*Licn2$aa,IS.NvlD ﰄ#ыRAp][-,j)HQ0HjRQt];axܢ| |#Wxzq{Af7;125cMG]1}_Օ"X 'DP#<7X  px=xF[ڧvډ"dKB{ "|: IE/TLm//iAB"q[ ^0!LҨ9zo|:_`HկT]n1Yb x!,a SCp+W-5 G;fx@FIO u<Z&)yD.`Ae*rq^ 2p%6yDy\ P`xp`>ALB!h(Zь^F0J'ИtiNkӠ>tGPԨNWVհgMZָεw^MbNf;ЎMj[ζn{MrNvMzη~NO;'N[ϸ7{ GN|'A { 0;r#DuG+(AT+X@<\F61\~pBS83ɥ(Npj Ё׮ @RA~/(`wuIP!%D9pH!G*]ʴӧPJJիXju #Ndȏc"6n8׉!mWluy cH_'E|]m%<mٳʹϠCMӨ!yFNknjuMkN8rMիܮE58Ko^-3\\K]VԲ첳G˟O}Rǿz?2~Nt?&HM? SO33NN> cNtqߊ,0(!s= 3L ZOh`>O/2(8$h7(Vh1$؍=9h>V(dih/;^  D) .dPJIX)2@ e 棐F*餔V-⦁:BHv9Er~&㎠UNHL.i9j鮼 驏'zǧLj8e b%6f+䖻Q-l+Ή읣 Xꑧ X$۟v& 7\-Ŵh lϢ-*J0r[0郰0,4rV߼{߽kb{%ZTWmق̰oãsNB128Q&]&荺X߀c(?#~7'x>x?H~8ۯ۠}><gK@u'H Z̠7> GH(L W0 gHڐ\!ȡjX0$GPY94{rE@B"nсXH:ڑ4#}(lp" eMp8AN|d GIRZ N V 7`e+])1h▸'`sq+$&D ѐDDuA:6f+c46ME+ Xj'9[ \r$021@ЉlvhAlafLE-( c H %1V*o @2Ё p*  ER$JԢn0EQ΂(S: d¤D/U $@JLWAAsc QJ:"Uj-U6uU8؂zN *f@AaSZzEUa\+@qXyQ8)A:ы#X r.E_UkU BX+Ep!@,{ L0gKu*T: Vw%x@ qXaE`K$b n9^1 0}77/H X6K!W~(&~$!*A.F谐ܷ`O:U w_|H:!pG%iJ2fD' f$#Ә, dҐF z+H@ҫa& ؄%%%v ЄVpJ[asx08x:siOanqV[ /+*' P1X?pb[\lf#@mg# 9Y xn{@lqZ vM﹮t赭 `MOu#s D+![,5G h(O9Or[7Y(tcXnYH PP;.`ӡ^ -Qf|5?CW =SJIụOޑ?`{+*]y#!ppnC$DPJ;0W+]#GOHC|0VzcC `0af#l~e3PTRX84?T!p/~O}CHEj`" ~0 $(:UN!-嘬 robgWXH%b@b{ @XO5s~>+ p Fyڥyvq&8wZT  ZP1!lw C zg:@ ^+%wHH}M]N 6Q20vGP F#wNj(wK؄ /6+ @  C0V dbF^Ft_Z j` _ + p+^@ PqP'^ՈHp x _0{ E PpFp XW PU ෍sH~@4R Q gT?k 0`Xo  g @t]z6@ 3 ?4Bm,&042I1lTA-ٓ>9&  E?YF BDyLٔNPR9TYVyXZ\ٕ^`b9dYfyhjlٖnpr9tYvyxz|ٗ~9Yy٘9Yyٙ9Yyٚ9Yyh(!D:eD_vl{ @l@! E +0pFu P `` ~h$[ @ %TPR@?0ɖ$L'@ |x@ 9& 0` `%(4$@ 4` >k A  6[alD `Fy[,ZG @p 5 'i P R`=@ AS ް *tdP0p-fi)p PB ' ) @ r p wS$`p n ` UrP6`[O  jSd& pepiUN  A + 3wp DJG6&2E1p =\I@ G o  p>7 $PR "Pq2Up>0p!{ S" E cq6pGPԠDE@`)N> 7> ږ `  'q/&[P1V۰ *ɶ66(@z pjDY "0+:nlQdD"l l1k9V+%[C!,W!,|q H*\ȰÇ#JHŋ3jȱǏ CIɓ(S<)^F谲͛8sɳϟ@ JD $_AB+?bȿ.X-Z!F(Qj0Rȁ2"X$@Ѡ@0E&2X!$R |HitbZH2BR!6((c% 1H&TP?P WmCT0|`)fc@R4ͤf5LR# .y 1&x6S H'dI5|9' f0i&L2JRjJJ*M:Zf*Ng'ng1pikL22c`Aj&+R3|,00"Ld*m-CF,kLd 14mbK)|x -b|+m-0ڂL' .,jRn0,p4,Sʖl؀OӋ l4l͵4#H(:,m2|-ƍ4b R  ς/B;GL(^<  /PⱵKς$xpXI;+0O7"7~^:;oZ;y-}9ibtv421;_L4Fח| 7|-h_N1,]];A8Х=RİwDA"X [`>g.ŰtNZ-TGk-&0ʁ w `.*p ]AMX܈z>tIlDYLؐ.VCq[+ٕÁP?|ݷH|]Pcn%+aR8dqb`+x?L*WʽcAc 6Etڼj + 6V#(ֆpV<YV,TV 6 &Q70 L'\•ƌ1 V-ilZ.m!5^l?HgB E!!\Z F3e)hn,[t+ufI$D9,"bp*#2 \æE%*ዛXCOqS=G-F1TcŨOGsOTlUx)rbPDE*PvW: *.Z["03L  0d';De)kYŬf7[rV,iO+Zb%mlWZƶ @Z LMr.׹ˍ.rC*ͮv K xK^lMz.|Kͯ~IB 8A-M ;H& ]qI 6, q\`X$mУB"wU2s@(Z&"Ȏ.[*yɪu22`1Z{ʀQQ#epx b "=s$a  x3Qp.`X@YY8'4hF{+0VLa1xUP`G P4xWV,ZAV C)jMkX?=~ b5 $( ¤X{[` &kw r F, ! ~ȃ*tn[Pv,!F jaΕP:T W]%Dat[7Hxȁpdd0nA~G0V0OV%`?@?܃θЇw| Inr}XIa~ @wb э/hdzѱBq%oӍe,Bq9mD԰3qMൿ. (tXLы$ \$' ~Ѓֻ8!Cʛ|@Bd`v$a~vfm믏{}poz/o  ҁ Ipѥ @0Ogi}a,p ?0~e@PvzMBӐ 0 |P " 8 Ӱ }ru4|1 ` l8^BY`7fxHhJq70Kq0DPsp [x 0ep$B F xI؆nBpPrxqXp V7noXx2(ֈ8Xxae8^6 a: Q8habd-C A ?@n>WqXx[(P؍%0N蘎I瘎vV  ! GX11XP :Ґz @␑ R  Jd ˀጽNsy8 Ӑ > > PϰP 0 p` g2adY9?R?c;n  } KV4 ` 1VeP GP2I 6i!qG@?=rI6]`' ˀ ~Pe'@wb ٙuИN ` `0 HP X`P9\C>i0#D tsy$C@l` $ р4 Vp0`X?PMA,@yN˰+@ ` @0b1PPS &^q1p E@]p@!+ Xj3jPRQ`M9jI`<+Z08  ` | ` 6 m%P=0"'e%R (ڧj iP Z lVl ګPWaFuF(a/К P@Ja"6@6j>ڮq:@ [Eb[b ۰;[{۱ ";$[&{(*,۲.02;4[6{8:<۳>@B;D[F{HJL۴NPR;T[V{XZ\۵^`b;d[f{hjl۶npr;t[v{x'm%JmW!cXqN"Ǫ$< = !XhSP0+ `c9RB ~h  _G @ f @$ $ :pz$+0ր1` q~ ` ͆+_p@ j  *0`EA" ƾZd4 'bP~V08 WR apr u@W ` W  pP;X{  N0Qk0 `  |_ ` RPx Նp#V) p3~@b@ Λ0KE,^e @" pa$n@T̆)x p 'P Ơ@ J`Ăb M\ p`: cؐ ` p |_{ٰ {zl @  (l |'N`pp،  ` 3ſ :W @mj `   W{pp plǠ"ppA 6԰ xP<F@. ( >P)ʨ `@ EUM o\0 DV ݐ WwR _0@nP\'= |P~0) Yp4>F h'P`j,f ϿmP c0% ) @3`l0qj4  bpN_pe` @ ^`~ ՝+xpWp e 8 @{ 0M_g1 [,`(A," ,~{}&d $>[!,W!,W!,.] H*\ȰÇ#JHŋ3jȱǏ CIɓ#C\)p(c6_ ?P:uZSѣH*]ʴӧPJJK:C0H(0hDz)JS)sAʝKݻx$ 7WNXxB gknF Hs3A4VsCܷװc˞M6$D`٭ S79()3#Gщ0Wg:8G;P) k˟OiX&jşL|?&-$?)6Y0*aB\A6ܴ ֨3,v" #cˍ76J!Ԃ<ϐDyL?EyuӋ@ăJ 7eXhlɦ3X˜s" tIM4II ?}s u]b'/4dyA 5$e` : $@njꩨTpcȄǫy?G:h]8c:Æ:=I8C)=X۴CN98QӪv[Prz'z蟺:MIX"%PWb˞A=,O4P>, 6GyIhe  8+w9G:J;`N4G F= @-HQ`Yx^`$%t8Ѧ:"F* aXUjf4 G>e,n57вH c@;HP]-xGE.` ^86@->n#Px\Db'B2@W+tg`gKbx˘bӠEv,fd1x@L B8Cu8>3zعt 5Ve$>"S` A9`9lxdt]/ڛ}($`1 $rG9A8F D6dXl^F8u/|g (@IG@ys/sXv0[ E\c+x& 2qp jtx3ya-_Y[2Kd&`Ѿೲ`AtrĀzδ ±u0pP?mZq̶U. P)1G(-h'@ 4W `9@SP6 [_ P6  ݠ~WF  $gc$ p S{"h[@` 7xA! ` P R`fC Z >hBNIH[@ p Q(x"P Yp `>pPr01`|b@ ݅]PY`Gq]w ` r1+Y g``ЉeP4Gx^ p?+  BQ &Rl fN%ހ@0!w4qE4 =I@ &:Q PH7 4@ e @ @֧w3p  ׹˟O"U `1? 2؏9, ?Tv6 QBA# L;LA/PN>]'4h8ΧK.jbˏ?c,l>4?\7AQ&Ia!=}p=Ep)tigEX˞{ H9,٤rϔM3cT$"8RY 9= =r\-kwꪬ^ }F+͒*ʨOI 9 =!AucO;ÃOGS vFyk+" SA+# } b|*8 |3$!A 78J .@2waIPCI>䔓:D<3]Z9(6N63"p7"`8(rL>hlOt= 61 3ՁGD$h` 9]D40r>nTWnyo'rcC vRr: [ X ]g0y@K3M L2ɀ>sT{` '!BE tP?W ɴ/'sġsf@AWЀ%nq@S(( C>0-XRGOA8Z2 M+#t|!˚LD9Bm+T5Pݏ\B5ha-j)n"QnONwU _xa(6`\S~H;4!i@x$ۘޓH3j;H9@1K3. Ӣ1p<0`CIXCP =y q  YPdg2sg~` p @ :/e @` ݠ  CK"@ehX0} -H@ p @P P0GeHa8vvE`p%~  vu"S K@ τ^G W `ݰhph))WIx@ cL  '܀ >p6@P8]pl!NP ` /  '` v'@ eD :@ზ`" pS 07wY9W060 9 ? 0;Θp phmW FiQ ' {PI r=`ؐ   ər,  P B` p `$/^?yP   umI4 Ah ` 90?@]X v U4npp  ``v5` T#$l `P#NZ*@ G`awPpأ///WdwP4@@"Sys 8p @ "p M 2P 3!s5`@ ?P`  U' 8p 6 N؆%P u  W{'Wp <0"p`K fP0?8Gx;10Ю p'{  ˀǰ8D'A15Sh# k CkUڈ `!ݰ v+ hp$!l @ ExN@~ F k P5Z5s| `'4`|*@P)d / [v ` : $p`N !0d( > 2eFF!,W!,W!,.] H*\ȰÇ#JHŋ3jȱǏ CIɓ(?XR ˗!RB@3ÿ+H ;'TIѣH*]ʴӧPJJfVWF!sl$|PI{9|eCBݻx˷߿ BB F "Kad!iNn cV8B&Nʝ#Gލ ۸sͻ\<Ⴅ8.\!n.l8iN]LէBBc$NqXPL4IDQg+bt'ϿI, ,4RK*τ#K #1 =rXyu/dA82P0&g8 ̣JJ"9:嘓 %Te*3 =zkYK3硙4lC,BlH ,:Nn؁ !29kg K:KV/6aÖPA 4=pllH'^Ǽ"Z&#c/ZM M 99 P4̳Kmx}Q u"L(c5-4!r 9G9F E\*$褗n:|RSMՔ>%*݅+Ӑh:!ި;R7VH)0L2HM(To= ` rdVlҐB8&I18&a 9L s8,< P?+ b2c< P t+tHx9<|~"?,3,28A **ZXWp7HF1fhL6pH:+,HR6*ю 5BD$R RB,`MB(@:K\RDD0R€*WJUjd8 T3dQs V A0@p&?LDTZ PPnzf7]` LЃ#Q)1EH9P1E7H!G@>ʰl:ōP HXԢ*zJTbG8ARGIIZOlp9!e- za:e 'AňH4:1 gOF.0 .ƍ}P [>^d# <% vb\I8dA&xkQ"Iy0/`Cv>( N0)R$i0@4T h&DjpPE,'HŤ X@ ԁ(L3Ȃ,юg `' :F:=HӜF͂e^ R޸-FeX#  |cp@\_@W$/~D*E-leKb)o2qpCDogp_zyi xb+JC$N KnSlM^rm%-Ę- \%'& /<4y#"\d]4"JfR\KJT/R& +UBKX6Y|\!l߈$F62Tfd@Fwlqt pC;qy1'!Io$ WJErcݏgc5ܠD&`g(v`ۭS؀ x|< ܹp(\ _8F2Z܌ϸAȎ#R+#OW0gN8Ϲw@ЇNHOҗ;PԧN[XϺַ{`NhOpNxϻOO;񐏼'O[ϼ7{GOg= }exWޠ9@:q^(>!"̀!PR=# sp+ZVBA߁^XQZp ʯ~^%cIEry);W @ :YW&>w@wy\1R}W0`3m `YV%| K#`p$ &8W CpP;cz @z#"rZxM` 0z p ݀CI'PyWV @HH5FW[  7%HEp'BBM!,W!,<8 H*\ȰÇ#JHŋ3jX18Iɓ(S\ɲ˗0cʤZp`A3xT (8a!f` aN6+,qAT>ͰW+Ԅ*uhсS?2CMEEp bDhD N?M6*Ao5,q3MBKlǐ)QEKthwor6hո` @VHx-x]NسkOH(MLyѡ?"j!Mp""p8DhT~BP(Id!"<4&J =Xf_{|"B(fAxr ^|(Y$ qBul£)!?DḚ[CɐtX l!AP6\$'{0Љ)XP4(R 9|Y!:4]D=p|>h$H c?~@Q:QFɐM\iD@ #Bz"Y,puB'hcB`b{&6lϘ?C  : Ee9S ȣA=`9r-; ~S94A/C-:HI9S>laAю:C 2GKh8"-tC;lN:Ġ7C0;%[;eD PBtsO:@/s2Q3NExS1:XB ϰsN+@ C9[J*\᫳ )p$ÀPs?H:PDArgWs̻up=c- ,p9NDMO/B$| GCWx>PL}_6&0%pSX*1 Ġ8@0 M43t~G:(ݨ{Y5U GC4A`0|=PL X 2 ҝ+20I'qfs2SJ:Hd@#8C K DI F }"p*ѯJ<" 2tKJ8 xdYЕ&8 W7Tҁq K$?lXFe&|f@hl#B2UNm"I=I.KN0SQ+漌,9K=Ǟp@P 6dV*AZϟ Ɣ#-#JԢ5%*0Bl !4uA:=wt(ȓL@C(x5LdJ` WdAc ` T:T8`ξ@%K0D.r E ;E8W Ap2R8B pĀ6(b l @Cp '#Q E*$'! ~8.dEe" n䣘Gk VQ L@!!@Qp"Ѓ3tbB8Eq.DN/,B`? '(*Ca`E! p9Ё^V e#@eKSZѓ3@ 3{b/;"CPLyb `bC4<+ynpeHo?L[MM l@C (X P^N?أDQ3aӂR&!u sNg`YUH)!)C"@|TI(*G \r('9-5rQ&nT&P08"* Bc4 ! 8qh [` `m(2'x%rd ︅cT'@xBDn(b^$Wc渇z2# 'xȉȱyBD:PG<!%`^Si*p{h"'.xgi 'Ġ8p)?x7A~Z,EWP;7Bb2G#@ ` wl#0MP0IU p` EHR 4u` &@Qpp0 x0R55pQth -7lb7.'P_I@T }  nG Ϡpp/ pV!Pr@"0Re Pf불؈nk(ovX RP~0`Oe <Ip AXF 0Z/?"p@ `!87hH,- [S 0 e0 P ;hR`Dc! 6` 3% ʕ[X:p PX*Uf3p+0` y |'˰ Ȑ SNgP~+ @ P/ 0@W0{ >e`  |FF 9I  @u6 h h VX` Pذ )h`KX}Ӑ?IEH$tE0h  b@ A9CG0P U pNJH @ h x  =P^3J'0p( @p s PPFD e QXP n Kb 0Nr9Ce `irP  `~0.~0W`pˆ R3@ ` u@ǀSC `{ڤNPR:TZVzXZ\ڥ^S* ɣ P@0 p&¸ pPh ؋~Pe1p 5Gb  q V0}r @ b` 5hD~ p1` np4 `੊ +`TzF` h B`(pHV 0t ˠ7Đ 4 w@ 5:# @ QyP`OpJ 0 $ @ c I@; u` [GYYyvQIڣ Z(N 0@` p <  K` NѰp= Px ۀj߰ d] PnJI '5_` NP䰋I!'0FX {[PеNI:  + p @U;yF p8ѫJ `Hx$P( PYWG KP`<\| |H@Q)p .,  @0V堰@W`*  QEk [@tb`/.+ 0C 4[6~Pp@  Dt4z }M;EXHP` Mx`@YPz@ ">@;e рb0aHFe0}p `P)0ǯ 'H p pP ]%` hX938 HОa ɉ{Ӄ WQ@zF SЊ]`XPDNP J&R p"P ګ lx Wbˣ tW]ٖ}٘ٚb p V v:0~ ,* @R`Wp+= ` Hx f ǀp(x 5K?H `5*$ "@hVPX,PV`[ VI*  Bp wW W ; $PW@1Pɿl?@x ``{ݶ0E x޼\ԯ-ce"`לɏ\p0\`ΓU0  ޫl`GVY ru  M ܬVs' )3!=79ǰm-ҥ  ի3 W yԱ WR]|^ZMBN ce Mٔ^~]`nQ 0 a 4` GDBIH@>V<I 1P_S˔`;u:h:|+l4@sh`X@ 0Sp Rb<4 `Z!`@G`N[<W%| 09GDs@? л8\ 3mO@SP9ݐϴWpR;8VM&60: гP@  4 ަ3.SCpr 3G @ Ge9Уկ(#H4X 2?ăXD|`iq%Np6>A L 9 Et[W8,q k\A]Ȁ/+_H@/ x!~1!\A R#Y@6 L qTʡJ$=are_SoD$pEB9bZs%+@ b>(C9P"T Q\" $@ JDzȂ"ܖ76թOjT:Ut<쁏W 7C-X ;-# q@b F =A #2#l#S"p q-FXul=0H!WJ0s "3V21=zx8/h0`970݊ʀ3ߢG` &$ l8r#sؘq*{c fH4ױ|= zb 5Zbe!&e  ?CːnoC^as`8&ak8;F"Ї)AG6"bA&૭IFx=ڱ LBH>"|@"N!ʋdlXȆ#Xq )% u  Ћ^[x"cFO#pE*>@8<CU.s C@D@ NH vCO:]))Scэ+ èbD;TDFI|JGCL4MNOxD9,EQC:R4E=Ex҈:b"b!$Vb&f+.L/6a%,6b34=1v=b0 4Vb-&c)c+6c46c.06c02 zP-`Xs8H^ [[6X1E[FSE^I HE 9X\=x_](!fS e\8e9cv8Ёcf+e@ <;af0Hav#p^f`bdfs fjfl'n6HdYTX G>XJCK$} NCJhnXs @ea+U2evHZ:`F` @"X?JX> ,C!&Ӯ z@(`S@y"@9kyxWЎG6o0oH\miCَCnkn.n~jjF$`ip8pn΀n8k6ӿV'ervro(x ؃~Ce;L0} |8i*rΨ~n ?k,nk ߃!^qC~sss@.tF7E_t4H$OoRNUVe?18wa o,P1s 7{`z@Xrxzs˨p^s@OnC!7"kF7Gof;vwNH'wB77k"'F=UrVFkiЁzXA{r7pu cPx{S7wdKu[vuFٷsU딝zNB:VYqj?qM]q:Zo$?Ygi'zUB>Wzz>z!8%T6pb){؇xye`N((`.`tz@W@WHY)x&a聑*}ӏ؃`}&!6Hg}.}_!g=p?9(ݗ.叁@4 }ǃ 2?(ԧoٯ}}gO~g~' „ 2l!Ĉ'.g"ƌ7r#Ȑ"G,i$J.*l1G.ʄ",Yvϝ'o|? 8  * : JԁZx!jRz!!8"%x")"-"18#5x#9HCTC"$dBD6L%dEK*$O"RQB9YIZ#a2eRidfeelb&hIgn 'pM Z`QHL0?Z%XG;?dСIm)'{>d)w*F*^jj:sv+ʫ*,"\1s̚Ӭ>aG ;Bå`I9sD}-qi#q8/NtH4 <0|0ۻ0 kDel-(/, 8EI38Q1r1L>XD >X 3EA }3p4GӀCH/68@LC4+3aA' rSD lL"MHO 3:` \aC'A`rϻ8'6䑿O+k+.u>:{>:p~zx:뮗κǮzaRXd#CCZ}Q~ 3!P)X,<Q@ȗl{e&qXB زHA2H`@$DF ъ$ _E@'aӽv$A <!#C(H! Rh" (<G:2R(p6L2h?^Qz$S3q l8<;(9 (<"X= -܃D"< |>'L@ >Hfn&g./C/ЏN!2Gt_N:^H8)0 *1cH@V9BS"a=!E`SV"D#^D)@=q1t0@nA>6ցEՃL@>X 5P?p6tC/x=ܗ)1'H-h<,:Ȃdg>(FI/A-̂NMQ0fjjdk,3AmU&:B|E$gmP?'BL@rVsE+t; =\ Hbx%y:)l8B;8t):hB[~C4X9,R9&H8C=Ȃ:)?P(`h†FdDC4 AO=C8՝g$V>El<=܃=! p<܃:B$^RA8̃j9g47@>8Nt=BUCa;( iC=PC *?ԅ"*.6?<*HL!-)(!jD":؀jSچL](*WCt\X)!ĕUg\$N܃%X5eNb~BYک,`bf+Ik&j뼺&0b3@6<l'X&&B ")858h\}fD~zDA;t)lC=ЙOH¦nԁ:LZ8BR&E2y B<0 <, =8ARc6`ʭ;BN/)=C/$QCd l;5A98l,/wq!CA,i>&G0 ="Āz ¦(l\[C^Ģ$.TUDRiLXE2:UD!X@G{1(r,)/=\??8C?2?r-C9H"_I ~r(2/P<34?s Ѐ`s72`3:Fp;  (MI:QN=W=33]>?t`s??/4Z#BDKEL4UE_?d4FsEL4FxtGSFgtߕ4HEFIJkHIKtLߴG4NGOKPM4P4M]B5O?5I;5PO+uPQtT4T 5PRtV[T{uWkXRtU4RTYLU[VgA[c][u[^u^5`tZFnFdGSGncua6[Q5hXgu!Dg5WD6bbu] PJ\+b׶kmvjIvbvnjUuSs#rtSu` hG7mDw#7IgsyE7{zq3iKx{vy`t;vEۢs&@ C$~Dtë2>Bl&V()`T(5|!>D?Ӹx8x8:۸83︒'98?9?T9?yxS9xOµ˹y[/yyC9{9#s߹9yOz9_:z3s7:yc9{zKzz;:yzcs;9Ѓ7HJ@CGTC6H@]|gD86P[HxMGD@(HW(*xxT(@7hK~͗|}g}g~sW >g+J5؊@C7|B;eCC5TO ӭ.BzȓYÂ:jٳZ>a \v¥*wkݰ7lu{m\bZxaG/ÊF{Yz%6槓 XŠ%} fz]4Ӻj9̾m䲩**ܨ]Mm[Sl{\gt([ӾΙU.[yҴM8^)AL1G x82, 'bbH r)'y戕0PF`WV^ysc'{ &bNNg-2m>1Pb(LgsʡDƏ*@$Сca lډF^dg)'&s1e,xrKQӆ2\p 5TXp1CS}#\He\ėSqe?f5;rEuU[%՗np'9஋y~I^) ye jYƈ d _+٦4ufi{4Ogܔ"_y!SrCQ@ tXc~{ :rB2+ڗ5ځDPAGDG6Vpl(HHG8P!} T8F=^1Ѐ'<+菋)A%1 1qdh<H#2! [(Dɨ WAg#/tXQh at@.Tc1dh,AR 2h'1Y :CdE,P6 (1NR($GPbR xCQ`)Hd aal'ɨRa RF4&qDcH]P8Xa’c$qH[T򒙬&=i1 Cc0PyI5Ҕ`,iKNB#_11*Fedg4U6QTBgiN:1#O:`5$'YG.3ڨHM%({P2 mCi)l%Fqq#PM̔s-51YZacMPT:TcDgOVHc# թ[ͦ?Z gA Zu y T؆ j|&Qd Y",;(V]8P( X9x:QTq B4N`!D a@6)398~ n,XFEr ,VS# @B8d1ƠDTM30 ;rk4)XL/-1ۈOJ:m(Fga 9FePŨVY7~d(Y,U q2'kJ2Ăp2K!f27Ε˦q-YS>U Ff*yE%I'CYؤ(L ҄.Zi[챍fbJڔq>sRr3ۚ-|v㣥 .a9 &yx"1|U> @PH@B8 8 ֹG=ԑx5 bQ@qHCηE!`CB02vF/h`p7PG4AqB:#:$MY-`KYOX>v,gh^Ou-X/pa~d>rY<4#N /<wuQ[Z~AUxl uOs- aLJHqJbE,aԯ/ (n,jǾO ͐)iT!& *o/]@h/R),/Ki^$podP@PFImAT$// l/tAxO O!- Ui  m z m!X0 )- p> H*N Vm0i QQ A N% zoθ0 A8.<"p AaLVHBjK@L, p`ځʡ` 2 ^zNP  a & 6 QL"#6$D" !4a# H!DHl!&Pa V BL 0\ϕ 0vLu|OpȀ qʯ)r! ^Ҏ*}q ,+POR L 5)*ǐQ̭0aO>H/q2-QORoǏ~0Մ  0)0io1Iy/j Uo51*'s/0 װJp5o_s/@9+-809i91%pw y!X 0WjH9͍ q҂!M0Q pO7<+?ks" ,A,|},A$@.aa2ґС8aȱDFdfKR!2"EP#:HAT44pO-]*u;/YU}\]-ʏ.5|10AWQU 5)4]I*\ӓ>7/%^U>%`` aDUb.MC'c=gs񝪰d%TAEA0 yFF'HB*@z! D "Xxܡ $!@Ġ` ̡qtX"@r!!# `g2!2zD@*ȡ 8 .V%ad"r&$(W`:H'@^ aR)cU_\7tV<q/V^ [3`fgXi_OSVg?a/ޏDZ977M[\*3w3i~od-ݎ15X Xe6fU`W#3dg-Z;q2/-bATӌ.'4i#3jcO`=Cv]dGgk6wXi gy#+ш0haX;X:vBB3je80dCtD۵DAD_XlM*AFvmI", +C ȁo * hPA`!K¡AHЇԴ p@JwMt, $ vg!N7 V!vWHha2` ́ AXzc> 00*r` ҮN)Vc}[SY g RIdpopx2gAQ SXIa /ӧx0+Zjz1_:2G2I:OX>S M0 POU֓WІ5]>5rQY? _-kM4̓z_BxOm>Tt K5S,ewQW-_մZcO7K/[>ڐY۳k ǒuAM[ϱ/ַ19{pE6mimc$P!,d@*2`Na J. 2 $@ ^J(L a *! q~@ʠ6܁a(`'h! TW{P%aʋa!P$ 2 A .f*Aڊo\K%+ꌾ]Ӫ2;Qsho-cV@_}[vϯ/7zZ[73X Uӷz7{20'b;ɺ9zz3O3Z58Xu;U۰`۱%9){22u=;2m=3c{$XN Wۼii{ksv[ ?;{}OOU-Mۺ O7KUޱ i᰼1=Z}mf4G ,D@QO@F2@Nn2$@&kC  ԁb$T8W6a= P ؀:v倖@>a`x @(2AwM .F2$A "%' :GaʠK a N@ .a86A`9LAx_zArA&!yR}?&v\!}apa2AyBj 6)AҪTmaCiH@sV'v~x=9>@J5"c9qନG>k0c jM+!Kz VЇ~a6D#Hp'W-FA9@Yb?0Ìd#ӅtY8dǸ^Pk_9k2("a hBY ,zXaCyz1&Th~m'``lp!|'0^է&'O9REf{BH {bp%#RDh y(Rd29?0(gFpE0C|% pN.p~ ˇ.% İi˰ '7:ij% Є0G +J;" @>uR, W0! 4{%[9g`0e'Vw w_`?6p(0ux#Ozȇ E 8~P0C$E>P%R86AG7y7_wא &BpG@ir#E$hAU"> XZD$`". [~G8F'V0ȏS`Qp"zzRh@(EW! p&<`x``p~042|ŷ/PxF4~pbu]P'g0}'PC)u0h @ p p~  &R7vwL } y { x~U2? `0`[7$RЀr01]&C : ubxh ppx!.Ebpper&4(7PT0 ) UbPTVB1R`0nRF[G@#TX [:R.Y+ Shx" iI[q6epJ"`]P }c4<@# ~hr`0$EVb@R)w"Hy"duW @rƟ:z :Pv/ Q dW\1 E@#IP+s~P(7#|w N`$0%e!Ihhpp[e&"tV``P hʩbDa ` b@yzzH @@ S`5  =p L0`@  R@>  $#K CBp )}'r @J@ A&[)sL CY@"e CP:ٗ9@@%36pn4Rbr0@P}07@ 4 `5&10`k"a9 ʀk SG 5F `pY͊ ` Y #uZoB1*Ŋr97@ !@pyp`Zd`i#pPe p:VSI @/7jw"` #'ۺ/k" K$Vl`` CX[ Ip%@``Kl'zP>%7`ߵߠj P:NK@ rH ]y! @~O,j=1i~YJ PvP w ʅ e 0*p~&b`ۀpp%ćE ްt`0@l b p J\;&j 0 @ʤl d  @E7^[mv , G` 8}~yPo iS  8Fb { W&C@ A *U@ l` 0M|'I$L0 Л5Y$' PptI9"W0* #P`@pP t<| 9T{+~9Z=  rH N`U@`,ƩDS 0mq<?- Dm'`(򽟰YПO P5ՑPW[_a=egaQUhش@i@p OϖUpJ-7Z|'zq:E`P0CPG ` 6Q@Pӫ|!bS0$l@ }i""&"z G7  P` 'FP=`WR@  Q 0(#Z% ѰP᷀ S }J S? ^uPn,"$ e0  `|=@inp>Cݹ/H5ߠWpYin pu ;%Ԁ 8 p0 pbߞH0nݿ>  ̮)ޛ@ we" ]>~! 1>~{`0bwq%6  0`eE RR0P<Vmwop5Lِ i { `*.ݏ%l @}ͪh I~~P8>p{u"| x V@ p 0hR/0PiRK`J㠱p|ѷ|ʩ샫sGuRn"n˽Ou\Pe0ذ '6 V#Z9496pepF3@ OO 4:(FLȀ[ئLP[ $N!'w(n &v$Ir$J={dXX2]ر!qS*LTD$CP W9-18ߙ:!-aa >m+pl[ͥK׏ ,ٻg5$1jB, =q aA Wq@84-5. >ʁ2+W`*vbڵmm:tsbn|L0bŌC(e̚9K y/!u9Sf9k"SP CT޺p4. "`2Þ#pTP_P'2 0(0f2XA2pDE&IQp" NJ?oPpH"4H$TrI&tI(̀7HȐ"8KcJ @0d4 h :)!sH骛{RHF754}9G hG<{ԉyGnt"9F.@ ^j1ZlA 2LTW!f)"sGF` Ȧ (+݊t-hdi&}BFNYFʑDh &(g@w(@Q/})# ؆΢Ɖņ ?jwm#p B*4p0sLY '< # %{5"7;p`M&"E"F 6,l@-K+/NƑc!dd%9Ĭlc* wnD#mΛб3.ڭs#~@kQv6&V4X-+MoQ",d;y)P*0 y$ o\ak]$E,`+B/PP,;YF4P t!XB#@Io:Rb}\K|c%kYzVJJVҩ6W 'A mP1LhHq@ 0B6iGDXY.8D6 (&aXx? +,#CA q 6-[`XJU7q$JCyE8 @֐*]"#6Pb5d-r( f@E@F҅Q[ ` HB^@ o 8OGwIDDppj V JPKB Gĺm6'AM(@cVxnЀ?Y$a h9:X͆3l+1+' ˨npSнYs)} %BO/\*!b1 \OӠ&f @\(E\ۍxo E.*išیayusy4XEҍt" , $ٴi=pJ$!{`Z{ FEP{ %A fgӪ#Z\FOVt[fg%HŻHpoaQQˑ X(q R 9d ',2Ġ6{1O$؃d1 G/,a IDA"W5H}Vɖ tY]fXX; sOz7SZ }8,*?R|&XE@$8:??s$?@4@T@t@@4=@^ps#H S) P=r0h`T#r(3w{ykH$ !B^M[jXGo8(؆Uh;JG둈'(||k7FlDG|8h-Kp2{0?P' h0"a>SyؚpN r|ʐ%x{8G0n0lMpL+{uXY?zQ:[P%X{pO@o-؀]D"/Ӝ{xJJP ~V ab aha݀aPbvX$^~b+#+)Nb"#ab*b"!cV_)bPc c6&ncNa9b nc:P;&`8dc-'$abJ6d2cvc'c;fd%d@25@aSUcQXcdZ.b[cO&d4nWAeRe$AbPcTXfb.\>dhFNf]vGfK>fneYf_n4TPc>&gVg~_=f.fi^+ev^7Ic|~ateuacoexzgynvyNdoE?/e#yFa~,6c&&kn6nc^^~N^b [{Pm~j]恮lȖvi>f˾l/社k.iflfFn5l6nlVNlvkVleĖin6FVfmm6(!lN}mnd߮jm~mnІQnn&쿞ljVpnk?ߦnj^p6j giqV _nj7bh/oNp!l >v>gVm1f^O+ .l^rkjVj.rvh`.n ?gf .4?5.1_op$sg7oi:odls9w,j37pVpԮdFA?oC$p^fHfa+6ynea 4(ƒ &TĂ /b̘bDJPFK|1bŐ*IBLQ%J.7NygA/9 *t(ѢF"%:cҦN&m U͠Rbͪu+׮Z2*UbϢM;,ם5s+w.ݺvu:3/ߢf6 x0M.61ˆ>n,y2ʖwῚ?Z4Ц\4֮_ k ;X0{7o Y-huO|e 30Ԃ "v@AA+8TDUP }41a;w(/oH H`FЃM&B)@5PB f"%8pQ8~54yYj@, 8$*dd옑0 Au0 z%ߏ X } xԊi MYUx" ( CrŕtepBhP`'B,d%GwϔAnD! )B&"C!~F'ط h )zHBI6"#F[A$HH%M(B (~?pHk/zI uAB4"BD}C]ԱI NfAx§1%YdlP梫n4%S@_N81j4(eWHqI KbD{&xbm: !38Uh*hp}4 )hMup=3+3嘢]AN;CH%!H:s6# cHB-p]/s [C/ld Ps; }Q+r(1J9S>HyCю&U*(BB 84ցv㭷AHt;4+lC u 'o%¦xx3;^!U`Gz9t҄9ntayg<H sHxZQ's#uc !l0B7M)2qp9 /F8z'bЍ$ٯ  ȏ$ i=pq6*{^a!a"Hd{89617 1r/Syg 8kpO2 X60O#2"d@LH@g"c* rHEfC*sNXF:Bp"$rP;XlTSA? !`711aX&)lc2 Y&B̚ . HA O?Z 99dORBV)({(JRB88rl#Z+IE/H("ұ j$K)]iK_"KW\aHW q(BV )E@ @qM\BR*0ExduL&HD@귨 [lz)q3"4܃W8{dv(SN+ %괎2Eu=Fjҁqb h(f00uBh "8տ%]ЏG@ %2ېP<" r 8;! G(A3HmL^BKq]n *PGoȂҙpGDa1|X iYb8 0HAR@nCn!xe@\.X] 0]?b @ p@ a= e!' ˝vPyAsmoVcx n$^"ځzcl#w# y"f@RܑHA"|67t8_P@=8BIU5l@9-Ѓ8D9 !\I9;7AH+x4=C4 1PL@9ă2܃7LbFHݜC;F`B9Rj%d< CW+t@=D V@ D 88X܂j8@/h8udC/AIxC/# dc>1f87|B6[P]D'+]ŀ)9)$ *-tr9!5A8A|]܂8$ *@H8Ã6X $A&(-܃1`D )bPD]9'5" W!=<\[/؀9((:=B 9F B=`BU7];A8:,C$A AO$l! >Ђ=^e2(QTVB $ "69,͉*>D,6 t8@k+rT@<6V0]D)$)ԑCL.gsv*JD7PgD=OV@Ni^i:p|ڸX9!C6L+ȃ$2̃Q6+I@1'zC$@7 =܃<ԃ\P>%P:Tm8t89x96#Ѓ#t=I$(XB;Bz&39$hE8G"k0|' zL@)@=8a 1toD%><$P,&C4C/<&8Btzf'Gd:Al~.49™8A,v B;h-78BjYC8\@8:8]M8@ 8B <lC1Z)؃"@D!Y9-$J7P I?PBV xBEAި9l'o)cFN-3(+=C;kPL9|Mu&f+0n^H>b7\G5G ,@C/@$BhxC< @ $Y81}&2,PkCX+(P9l8Aj0(*t't9xC1$,_?q;X)2#;re@ H,'bL,2ȁ;ĤXBǔҮR ""$ ,H<=8B$X: (1pC(7H`/=,AMlX=A  $)xB9dA8P*Tjv~C4@B@Ȃ7HA @F+ê1]kF.l y}+t;\B+Ѓ)qp6$ڵkC<7=`1nx:8FAD8؀}d= D~ "l9boLj!p 7BAԋ"ȃ](?Cp o%(@P;XBDA9)\Geπ##,;uM!8Tm<ê9t7̃W@ݤT@8'1W}ȭ=xB@7ص.V"!*w9lSCL~Jvpgn`m4T&=6 "C'4eC C9\y=|fĵ=&D6* Rwq2&"B0/:j82=C/oTcMz48_D'<:'z~XB318\dA:8B};A~y߻ !,ʌ=C+@Y5d7ث-vT)=C50,P,%ǫK8_=|#E)2l™s7Io> tVy1#͛Bσ5\$Y'i? Ti%WLs vQ+QЭៅ ,49BN8Zp eu$WMG8!C Nm:O:{qp$xHIPQ{ `1% FM3*pC-rS[6rv#c-lS&(@G,f\ qYvvlٳ]We%Bsh$XF q TMbnB6@,b>B'۠!"v p]{G ZQ@ 2p4h8up[ԙB'p NPor &xqE&բrN42M<9ȱ2s:cHp,xa!$*Ҋ+߀`^!DHqBŸ_ҋ>p B=,ԤrS2n9-V jPM?b`<#Gl%>WVpz)SYk"TzM+y%X'E(^8Sh$s@?<7bqBcJ^(&@G@3T$C'aȑ`|"p)ȑX^8gD8 8 @񎖿 8D^@ o 8#VW '8q-} -'^86~W y"8ntTGPg1><(p`k$ rPb8ERZS+CX0jTpC8dq4eA:0`R2aHXu8"U>$<Q! tCL3+\7H3Є@)xC lȎUHZNt?@g c29A lF\7f /PԣFH{,FXw#b O {("p0>u@ů(rDu|ˀ, Wx#h)!Wc,ރ=qq0g>N u‹`rKQDj1 y a=F:Xd#v0x*"U9׹w[:0lh-NSXkH R"걏@7$48{TP4F_6EbXX;< ,Izj;3:@\ kYX)C N!"ߏi^ǶrA2`cK$.J`8 X!襃 a^B @6Z`u5!D06[_mn@+Gq=V7 Pe#@C G4iMAqn I <ю| )q{4=޾U ] Nx9-! (L`n!(Qi ,<^Qxƃ HX:p'oB Gޙ}bo*| ]he^<1:26^ puԸB:sQ8Z{``H!, *L+$aSx\BolAJ &2@!.:@ܡ>)H؀v<$&` azh`aL3aJ"P7@<AbBp$`A* Ρ'a@!0+A wi 0H  XAh؁t@E,hA8Xa`ZL֚zd V<pM*pE Q+a8ka /!Q,o@(50 B(' $xJhV֭A6|Vʸ(cΖ`$da.&XA( " a[8!<Ɓ da/zCƬnj&'rr"D Ȃn*"}F''sR#"b$,Zl\$Xr .& 'Ӓ*-trAb 2NRZdi !h&ac^rR'(2*֮r/E`*+M(s2o1o}҈z3**)--K5, Az 2D33a(O3BY#`6A*b3 Xt@5 1)(S95Ob72Hr3RH-5S4W*+0s;6hS 3u31If(<]5l6a L`Kq hv$0Ρ MNOM``"QOkha^t@APR#tЩ86!5^aHR;(RT(,Q=USUUW5aU9VkUWk5W;WwXkMmRAXYYզYYZUZZZQ' !,;D9}`ÿ*\x0Ä -L +PThAcŏB&x<",`IB*7"pH&<43迂DmgR`қ('cSSwn*SY~q K-˵BE v("a̕ |;#ϘYf萢Ͻ3mX5Έ"L٦ʘ3klO潼I.ͺӫ[^ٵDc[}1Mvȓo ՋJ) ; w^=oSDɛ#G(B+EAlH$6ЁC̃Oyxsx e'=5bD 5s | (/dBQCzcs'<'+d2Mh Zȃ$'@D< q,1G89PmL/ ,1t rPchCX׀s>P+3BaN$%SV%ވAd>AOs)&pllN9{YfԪ-e^۶RmrZ[ds Cw*%3#ǯ #pN94@(X^Q"Tp:DЃ98;-61B @ 9,%xP C )^@6?d`c74C-#v,8D>b@"IH$,J=xL\'BH8ꠒM=½CKƦDSFeUp5,n#ECI~sNH R+b1b)B#JhCNZ-0Ppdh* lb+K#UP: A,"mڴvھ~۟r?n+Ձq̫(@ю2aBĐ`/=@ `0 8690tt0b|AxCʈѲ[d# Z[ S  @ j eH SLA9|8@r$&DE "쑁 >h,PA0 @7`?NHBH@$zr35zP( qSD (r:;,ăD` hx^`G A 1$;6pA1rsHDɀr$'PG9@ ~yN@A9s6rC-t@$LX )u-5@6ɯqKSqY8_+El[+ z@6L@"ե= MJ@μoYhZSJ R*X8$=XG4 MH8P XUs">(;^!*aB:^DRh<*5&bc TaI PP+@R2 p6Rd=XA,0g#0Al0tHLv,(P 0Irl4p"Dv\y#-]+D PI< 87>!L0 t` S XF T +aOW8ǃaaGΊ`, X5QB(G9(A`E<17R7 dwD3@C9$F;slX+k|FC0XoǰtJ+4c6XtM? O+),ֱ$QӰ֋ Wz]ꬳuk&֝8M;fM6D(*8! @ -эUq?qwts )<> `(C]E"pYD8`a9H$@M fp@BxpƁNDG9HE8>x*)2@@Jzxp9 AoG{mM G? +5AmioCQQ$1ā)^丅@̼s!̋Ή zb)!L0v6<αH9fꠅ/:XF9`}ZS*z\P-B\^,]@&0lbpG$8G7qzFAqvK%kTDkS4k 8Vv@ A @ P/1"  `b Htbl$@"G p 8@ 31CqPgw24 PZaG Bh0~ qgq VsAwD{ϰ Qm` Rr0jI1`b`#' P  h404IttPgO1@ uH1 PD\ v FQ@\d +PΖǐ a2 @fs: pQ `@b "fAsie #/p  0<`l =0xep{Dq ϰ 7 ~'PPd t pE@F YMcQCdIp .钤P{1a?HgFV0ukPE78&4lFlkJ,4R.USlI(@ s  u/10ʸ1pcPp @0e1=p(OoYP r28 ' 6h 1 ik9op $EH PĐ Si40 |cA=0@ ĥtt(p|pؐOəҊ`zY EBN kw`@—{` aCd"Pa Hpb b  `" 59h{'wZ:KsP# `Lp u6BF@ Ae Pdx@hp)i>^*L>[S?^i6j&?UNF6VkFVWE@zxJ` h9G03`7 T/a"p p9^0 w#27^ 10@  -# u ( `qog Қ$b c })_`PP"` u IOgPx< Ed*)p r4 ` QD䀤bph'0p~`G0d?dՓ"^`OWYR:+{b7Xvi" ;7IEF T@ILzN ɪ?( ` @,vz@U9pJxzU{ʧnZjpTT\qz&z;;j:14/KVqn&1E8``(%B 0v B0@0 u@th: H F`DtKblx}4ֺ pb3 v l"0< c@\K* ␿`!`c EܫԐ d< p# ! Ġ "`#M- p~c 0T `'f1l u |4 Ao!L؏7S0| p0=F Cf0"4p` ' ɒ\'j<ٔkl{CD iEiyzej9MոlF} ƹvUYIjl˜L¼vF4$` p倻@ Rq`  `p UM7 0 5}p)?vذh 2  G sX 3P p 0 ǰ H0gS` CY3mv @ : H'YќP/V$qp{  rQ`F[@w˰!pbs /(HEP6"Bp %)~[̯h oaRGK-/3 fm, ` FKЫ`l@)zLePrl\t4 ` 1B+@FʪE~/ɐS|j?\j jutɃJMsZl;ݏˊ;iAiO`_{q‡ "!rb[5*'*slp"}4CDrxT2S!S(FEE*'QMuJPӧHF͙tU[V4:*V_*,Yc6M,FdׂխW}ZկVVF\m_m?Ydʓ?B?b^LpHSDd0 lH`$hnB:DLp%P{hHȂGHlt / aڤY!61$t{'BN,9`NH zPD+In (lQx졢~p CPbBbx( :CG*dʰ?p@5ʲGh .ڋ0+쪣Rvٿk1:W)g=\-j፷]wle^8`#;AP` a0aa,*a(9:fxbpXJNxax1vyΊ`,M h"#h>2xV)@7'a"Aj2P@hTN0"虁 mz830|jA(>`\͈yG &遖 nd{q<ϓ-(+|Ȃudxͳ*fv[`ӝϫy_WwZw_lM?:?|w?z߿>h]^@jA~ X?u`>% _Ǣ0*-9  -(a)P2䠵C#{Ob@)kQ9qC(\a 7QjAGQ*P2<|G[sc7&"9HBOK^ha+U܂GFrmp,A C61De*GU"򁭄e!YJ 5#Pd솢\hX4$1xLd6әT3hGiV`U9`6IИԬCmrb&~ů^ 󽓕 eot' O{.%Y>P6ϊ^?S^N0*>q!{(UQvs(!4 A(?)ϙЉdMhQ4  B=SըGSc~JC(uGIK85=RbN5+dHy-WߺCJu}%RW*ħSՌt+bĦ`ITs) 5V&sT-f Zl:wOvUhEl׾ֶ]L`֞%WI;؂+3p ь>VKrZ=(و&w.mI[ZRY[\x1g[['+b׵1{6\xK o6nhe;d>uĦ/a(3,s˜dnrc {PvwCs;y9+=2D)d+-0o{e,UE[1WHE3RO4ױښ- R"W4lchEOψP%\C;xlDj{_~Wm𥕽lgf͢*cō6狫\ڽjp `+*VdzeZMyg{_=ӃTe53PoftL/oXM2!iZ!}qo-[<+\-us&j3'Mq;|Othż)WwmgX}9ߺHﶯIu[Ỹˣ<{J/1N3CpWd5-]i=tZV}7=Qc򪋞o^g>~>>z9{E kT!-K=Q&\&1vo=Mv-.ǍxP~c?5s=G?}iaWrth9}9*8~ſ?Yb]Shjz}&j8l0 ?i "ۿ \(/S)# @ =<$tD? ;.#A $@ $"4#D%d&lӘ )6:T h! B*d 8 C4ҩ25 ` )DPCڱ7!.B+\.\ûD-*B@/Ԙ8(p=Ԡ=(? ſ94D,EdOdFo |GU=4Fv ZPt$}DA4!\ĵ9{D@BHclG@LRǁD.LHȍm=sYI|{ȇraN)zI{q8E}{pRk|ps4P X攀 Hs^U!p& c`ˢP ʙt-Mx8 -SEňo4?KLQ 0 u PtpNYshj=ƨ%8O p0@Hnыr5DsC0J^;p, qx;bY zXЋr(!1"HrG4 lxWNM@ mTI{pG0 Q|$} XsDztxr؋Xs`DpI"@z@i Pr0p7 vp?0 t(#hs98B_0u I؂!(s0o?8zx Y>5 p| w2(y@ ]/"=lw pPn8! ?I[t4XZ>>bzmq@= 5ZGȀ)@{8LXZ &]GdRp֕|.K$ȆeXKΙ`"! 0g9M#(W`qiPgrx10!ꠚs,,0t?Sxs0ܰ ChIXЌrcO0kx`9i{؇t l[uq OP%X?pXa`KO<:v} ]ZW 08SϢHNgQk #, ^ߜ 5ɀxsl8\an8PӼx 0sc rY]r0Ķ8ZvLi? GjJ"0e-|`n 0jK^k))hp]^nX _I Ѱ%_bґ0g_,cȀ)[n|(^?o݇8?URhxMX8!\#bq oXB($`^xW4qĶr+Zopن!kds' l@`Ru p$y47Gơ+WZ6d]'r)'!QsSP_F@=gp9Gs(n<0s!XFe $phhB U`(rVxeA츑! zh[!jt$y=#Vb#kl$x9v7S &)G)ɕ$I`-]86:,Y9sDCт [ m"C!ChHh N @HߑǼY;Պ Ƚ‘`5$d¿ E-+rrt8!ʞ|3_1؅sChhͰMeұzׇ9k=1zT@ 5s3su8)ɠ uzZkG9`i~zBst2 p |PGРEP$HG@ r5Dsۘ%\á~@JDUCiQG=qJM` 4ɤ@<)Ml_.#DCX+!d⬣$"`#cWR?p`E>n!g@U:ỸX HR36d|# T UMH4?oEAxPXF)r^*~d5$@J(b q 18I,` rtP$./&q ]c@LpBdM#'6!{\ 6= cq@P 8 Af 1$) :&^*y|Bz*! D` Xǂm4ztM) 89phMBBھqqcp| mqK r( ( !H=" 4NY(Bz p `-b0DAArX-t8 PwĠ#G:NaDX WTz@=(f'I^!2xp o,x`,dxXa=0G6z@("D="d@Dx !A'@:s[Dg7r`CJ}*8 G~88A>-(jH|a{>"D@R%0Hqh=A0J.Ǒ@tC\?WkgPg ǟN1XƊ"S1tR}Z'PYT8`j?% a QP_ js> ]ō& ̠ Q'\p(1B (ޙ,: ܙ A8"C8Y K| @&HXτ8BQuHB<<84VL ^ p NC,1C v&bd- W t2T80l (=UIDG8NX=:1,҄;\iR,*UNLa7DARB9,BYA;N_؀;*,W^M=m99x""f<& Ȃ8@u1C;96D"pO>qމ$A;JtC `B9C<Q_ 9؃:>dTPCI@XI8&At@ QBb/R䰢)T?Ȏ !È-CLPC47&D@^;C;@9C:#)73P EV(} M1cTe[@ڛё-C8t PEbFA+P\EA9PbX;[2e;P5C:P<@e*MPC;;h3C=CLރЍ+=@8dMXHB6%9؃9@Jd % WfJEfMtd#>"IN86B>ٝ> P=C;<%ɒ? UB"8P:EOAC=T=@f;%l4f %"<(ٟ%b"TjTet* !̃<;PXnS]$wd:u~臚b:DbYIHC<9bɁ=9DK8tܯ, U"9@ 

̛X-)CY*l!%:`*XG{D&& JDLDL#N)B9)\Ω?2%ڿ,D`B9 8 m"9J$X@D f7$Sꡁ|' |7* Q7>B{FX8\}a8D)BMĀ'؃'CޞКA- U1ЃE+PfmBqH,L]I#(T ):BC-`7tAc%B܂XC7*I'73\);t*Ddk -'`=X(~0iD)-%];BsUj:lAGޮGn(B+HB_< T\D::</qW?*%֙(vA+)\@+;#p8LB}(HAej8,Wx*p;9+l#L! ?h1-$O;pi5" ( ?@>8BuD, *tC'Z6vDmDB&I>8=*~eC,9,`%_2,r@f=9r X X:l1+ò10+X(a c=$I^(MZ@P6-$S@37ԙ=k_ЁX T@/Ɂ&"c@2ăC$*d""lZ:BquhjL2x $5Ls5cupŕt?PJ󰚫YCr \B<6d!@2@fv3<.&@ HAElolÆǦ$(&ԟ4L5 ֞M-X70rKCCv9\ l,lӂ#1ỳWuO@)D@rU@(XGl:8Px)e+Lpԁ97,@q!A=0QjDv@, @>7'sPfm:l, `/D@YCNִ>jVm @?F6,L\X C*Ȃ8;( v1Lz0AH@ct*zܩc@C=S4% B.]L@i:|%t<`UreC;x*#@;,A9BEDkFJDo|!8dC9(J8 }6"܄A;***`!l(!,o9t51-p9'PjT |*Pҁu2d@,9<8}RV@J^+^sI-7<a(˔|- .(T64J88-D@3=*l%e: (1t(hHJ @=?=/TXC67<Tn. we:V@887W{&8@XSXG#, amH|džA?f{5aB X˦cn֎0E΄/N8a6Z&O~| H%igj G Kʡ - U4 > U(mYĔ`u*T?X+#oʡS, $@x&ਞ-g%?YI`-  hQP'˞#q3DXp4!4iyѧW}{Ǐ~}`+q#!0Bqd,(cH`$y%*0"sp!pD@0!p^ˆNj?7#"P0Ee VX'"菇[ڑDFKNP/Ĩ‘vH HG%N>`D: "Gf,AW"G#A8,DE~@"!^l{nYYe %h$;1 }Xaa kAd,Oe Bf2T |F?x3plH g)"m&e:QjEG\l‰!* \WYg6D 8' @"Փ+44OOWOC5K S塈'R< @K>5s>Z`c~ h$`nĐ@MA# D0R>Iohx0(@y$~sn!w yr!%F%)@$WEϰc1G.Y $A ǔ+(GZBx,pg'"E"rE@:).p lLa @7Vu؆9N0'ctނ0! 8F=Xe@”)XAu{6X [ $"`JV0PHX]hN% *lW 6 ( >_`s!Y{H,'B$xS9@c؀gK$@Q#  nz#7Y 9Ƒ(C;q {Cf6gQ/L`F)J"fl $8ux6ґMs$D9Z f,- @ATǐ$oCARqjL 9ܑwhQXwY@)u! *Eu$>Uj1@)b 8p%T줈r㭔0aNX+Aau ځ\x@> }<#HP# lc nwϻR(Y]RWEEe䰇C F `ܰFn.A_*  @(C sPW !ޕf9AX#H-Q Ȃ8lCUI \PG;22 `ft@ sdp`yCO ϼ@! ̸)*"xŖ%PiƒX@R< AqE <t0LQ"nDxf,pm:ыce>M8GGf`G;, !X;G6A"" OucD*}\79axx# 9AHG>z1DѬ0ug]lg 42W'8a )<[' B *?ta\y.vJ,1p'NI%8C=hP"wJ/'>" )LʀQ) pxK>Qhzn:8rybTKPR}&7 Eo,]?! Mc@ÔOW p&fJ4vKlZJ(L+]P @H!૤$@f0& (𮙺V@J*@KnHnt`|eD $` hFh2  6a daZ#D`"'aQa 봏ϓ`6 At@\P` ?2`{0a&o d`$ 2tŔPb `0O 0, 8 O, &`%nI t$0 .A a - 7a 6@(a® H 4#O z$`(a ngAa$8r #&P8`2ON#`#b P.2%s2H/W^ȣ7N#@D02$C @Ve$`D3; 4}΃DSr*0\\ $n*0\$ CC„6 `3'3b`Sz>%$  B4}C˓NMh6ߓl,$$Pl7-0#D7M @a;C `<5yS>@ප0 $5-a\36{#R"^>Ql8/07A/"D vdA$#@3#d4> ; UU#44TU ED73S{QyUQ+0NtQg},(42@7Lͣ@`4I!2%vb)b-b16c5V=^%7vbE~ !%=bi@+@iZ#cݣcσ~@H@X*fvh3ad6 Fb2 z.NX~zaYk}'ư΀la6kg{lvnnn6BȁAOi6Vp5o7.zakewrb| rCa j'sơ=D@z@ՔsAV8=P0qrewxxxzn ԃp7hWc;^8a&zw2'2{b[pn}*TWh38 nD>,7{w~x 7Zzw6@4kր"^R2ǁ)x_D3R2E`%S.8v+r|9xh-@Tؗ5 [XnT;M3[ E9`W5Jx@#SYS>C7G3sD@q9։u]}իz5\O"wu^UY)XBF3N55mp)WMXw4^i@V,a֘+`W{54\N_#{lGJ),XX;U@ECx(2xr{@89e"Aq#@.! `L`8f@r@q@AD4! h7 !AAOHAA!zF@V"YLÁP'ځڤAHiCj{ V@!P&OT{!K^ ha#z脲! b# "2>ol6(; Jz=AF x`.ća&~r!u2lv-!:c=vccb 9$   @I܁/f`8c -cΥP}P2 VՁ\y{ =؅6x{ 6a'qـ́Qaʡj>4ݮ .h-hANC44N+|` V%p`}ΣS! DFġiݵ=@VVz݅@[ ` P@ZaDP7㮮A A!:!nIZ~`hgayT 術:A`~g[VA%@cѷ>CLISE d 76p :8W\:CW&:S<$&@ٝB` գ56;Xb:>0`X+C?NFU @ġpGz7aQl3 #ܢ]FW U-2 5-e5EL$Ԟ/`T @DA7Ws#ѕ#(_\=/… :dn7nPܭ#YQaE#Glkw M4]uo"T cBHQ0ҥL:} 5ԩTZ5V zy3lF 8"G(@JH 䘽r >|3&vFMߩv#d4dVP ce#C T@ uhׂ:O:c%bB *M` u,#ݽn A^< xG@SLֻ-*ɂN _ ;H Q7X@Bk1?G,ҖLY8$T3$'H/  QB #BBqH BLep!Bd~" UB  `3>$ ZjYҡBLq-5p(H$e ː)eId4TA[,R I!KH,U`2dQ!rаǨJ/VDHeG`P)M@%+T  D Q]QL9ځs?"*4hg8T1#]0 _*F` t!B"tH1X! 48W$Q>Q8/ŀ#^B B" X`` H ɗNsImrS108fC]C"@I9eD0"Uy u@l@N+qw@O'l uXn)"@B7wȃ1`@/yC)Ia{`88rl3E<I Dp"L `;D B=@@B(=qPNTY \:LB@%H:A) H߀;ءE@BV u# Pxw!=PaD.w_ W=gp;6xc89&x\gu\Rюv#8Sjv{X L@hT*"F n8a7 @*y->x#IUFtDHqzmoR"(4]׼u+LmH~)`V @b` t%!+XF8z$20;W1h)*  EjMg 0ҁ c]#AiNшrϔ ^LڅX];/B >3,xj/"p@tw k$Aoibtp n @p0 3;A{M` 07dAA؇9 ܅t` 1fPr"0!q PDF؋ e= xcGQML ( !y1`ţP$F/.Rf@ĔEtP2E zA@$XNh!W [Rr\8a 1ex" NQE aQ% Bd#A c9,)$$#'X6NF#H):2aȁFǾZۂ=zrcB,av@ T@73IP 8~ a`0:> XH0* z@ epPK{ERId W@4\ pi U'Pp @`4VVo O` e`Ƈ6О_"否=bys=( 0C C& tp@ `*=4Hb e0bIfi_o\ !W0@UC5Ƞe;FBI   V[K@q#  ¸T9aX3& FlrmFm  u0. qnw" x S@ Y0I}Kq WmF80bb`b@T2p 2  sWZ !砑?s@#@_  DYCF K#Ԁc EoERPJ ` p@ `JWipp MV 9x:0~ 2Q4 Wp)v1x:R#q 15JF + \fb Dݒ.S`W` 26 AM<[ T$W  T?`e+ |Da AEp{` p1pj0 >|Sp p FP{ 0ehC ; n6cz`@שs<JґaZQ0 0~p ="!% `dLQq QA:@}hX\k_ NpR7=: )Zx,php l㋅V02v0P8 y <  ` e&*T1` p ?0:c ![lЏPހź ǚǰ`zʭYߪ;c TXbYŎP 03*aWLt$ 5 Np{Ԁ@׋@˽TI q@M0 Q0* 'g*k ҅,b'7EaI `:1p;wkI )aH`XD0 4@@Yp`.myS C_{PIJR1 pUǰ a1 8P`8V֡E`V 0K h IR 6@ 䠳W"ZI6 PKI: ݢ3@F` İ I@4pr[5:б3Ц @f !ژ:`9{`G| Y]i|6`d* DYm7;\{0 j҈@ yY ^ Ip 8Pu+B  M#\:{x <:p,v CNjǦ=$Tp~T<OR!}A9|@`ԇs P]-PY[s'' $ܰ <;Qܰ s:OXS4y ـτ; k/S}&D'+'+[ȜaN1  FE$/@`Sf s`"Xu@:k߀ "pU!4]%S +QPPCF[0 Ql< " KA['0) ݐp%  _C:"@蠑Jo] ` < DE pQprP4@ p< Ѝh% _xЊp"j. ~0ڭ ذ$ k C nQp 0"{@K?= /S 3@ 6!1, 6 G`: n{'Nt' ?q? `IAR_ m/s/\ )^2{۰ 30Wps'p # ו7N 0JQ8 3> PAr`O1>b@r5@ bH[` K"^쯠D|A .dC%NXE5n8я$ZDYp WɒeQ~\J-Bm3nsge=(qV +-+pt"Y$IA@b?WdEb`7^K0Le:Zៈ2qqՃ1L)zXUѲ$JT7GΡ E|O3zH;Bm |u"< bVRCRZf[ VF zdG[˒xL9Aph!!9F ~"Q fxW^q@"8{z1p }&㜔_&`{tDz6!"X^0(Gꀄ=9P1E9nr" .FpW^qh!$W0O::X{nA{$ rDq! @Rǚ#@6! nˍwQdNĞ)3pm<2b I ={2M;4PGUT\"^ygK"ygئBܹbh8i$yŠ0 P HMSɇbqHEY%[a~!!<s^B~FW1gIpĞe SH .YFSz;h)ŵV{mvm>og{y)Il`Z,({aGʟDB$^BأỄ!OPe*ʞ{)'<@ pq))hǓL3sDŽ˺Y<{rsqHiVz cs >G#AqIusi5y)+ztXH h]8x$uT@BRk͑@zaxbSZH^ c|A}, 5#; 1 `ڳ=zu0NrR!t 8gR%0 RXna7" x+4Z'a$E |8'GB${P4GK#d^!=hA7( h=/mp&4p|b (C`tO P3wo\n8q^Q !8v~2qL?8q\#'[O9Wr\3ym T@y}s}0.Ps+A^ȢhbKUnP"D.fpcvhuَ:=S߱![D\\/W3]@nGúSzIp |d~|q\?$瞯ޑŋ=WO ]y{co5aSDH;ڑ@BAzxCEO: T GюC4=!FA6lI=86$ "GXސ+>ڠ pzp`T(c8чg!P`?A0ts@yh?}$Hh??'YrX @xt )x=~˘r0+;!x[ Ȃvh{p[E}tW7?s%̽#B7L C` rp O( {p$$ @sH W؆,s1`7!{DHG{Hr8Szh!ܘU(Aиnv0[>Q?UsDpH8# G s`) at%?,!JxЁptP!C9RTH8xKu(lHFaXlNHFJ()FBȆr!Hw`r0EpJ4b,ТxCGo(-*q$JAW z8ŌR +}+LD6`v 8tQ@89߰mBmdM;pʋ`lMrx1(3tژT+0GA)[h ˀ"Xn"{P.aԦ}9,RO4PBt PN8"Z 9UA\=]E\voki^jI 8M!dAq@Ww놺q1 V9o8Btp8P;+`!ukhK(S@'g1No^+3Fxe #)X'm "sD{@|y ApM?`ҹyS8?NJK=V XhW K#0xBPS#3[njX{X-RHx ^8PՋl8M '[zxHg! cf5a^/U0ph{`e+Z s8wtKc8,  o2@p8Mz/4woxBpzh{putXo!H`k6!KêݨPB8C_-dϦ%#)[0.%+=xl@݆2HBp:T@@1?8tB p ' z؂gnrP{p Пn M4(<DkȆ(҆>H8wWgqie{|gBX!\uzU@Up^bx][!XڡgYo{*qmșn(tbIbp;tpu}pűI==5!SrP Єr@|جC8 0n 8{' yn{@ʆ`Jvrp4q/+ ysH1c0{h-B#p$GH$zPH[x Ex@et9Q{L.BN ɚ֋ 6Δrx"˨1c- BD"?QPaF v&`@NVy?baFR@ BvJvp] 'dI E-+@Ղl'U 4#5!&HFK =roZ [DHH )pW8 ')I(zҽCB3jG$-ryQsLCr2N(jj؝× p~-R Czcd)'ddXQAF ܒ;v}O2e Fm2а-MAS')(|#[o7b0P&QN"@XWb[r٥_cYga|R/l3 (I)(@($a(RJ)xIJU`( <8>!^l:DA?mDCh%HNf M6"@Pe6z%": +S@U2p$! MTƀ#@pCNI4 K6qoŵߕU,!jd`UYAN8SlsKP)YPs9T ȑMQBZoᰲ+E<3HT-yNA|gbCɈsx⑇@# |9DP8A䓛z΁.sK3n98 ABKV*\>0 4b#mG׽M)M"! Kh0*\a &7DC;=ըF BC!R &p VĠBc0)`4A(2[FV0[&Td#(Dh# H[?BM3€C8X9#lGFH\XAGH"<x^98БpoZ ,jp@Ѐp2l$ -tb#Ⰷ=nqlGQ=L7'()X!,1lxP6d QH,@+^ zH)ZծfJk\@gK[e 'ns ^Ƒ`hv x:dA L ې20>:z+0XӾӅ#uXn8u`uhNwS78! OL)ülcHȁ`Al|oSJu'sў,cT # /,+Q0G[#CAP#5U!z֛O #,-mkk6, <أ,;F7cl=zq08?# Z$`oby`y(.B2 DE"PQ,sB@8c p`CuA`0(0@;N 7؈,pDqH)^! nCW| _BWMtm@=$Qp,8+lpɂhA4A< PGZW ,+<>pBNP򞞩^ VtSjTT 9G PD9!^Iw@{D% {Sa/inGL$x9 D!^Ja~a oX7u5"𿤲Bڿv 0Ű+`n5̯1 _0S @4=Dȕbi[q9ϯ, 4-n)doQmI sD@ O.ؒ ^>C9,!anC=R`HC G+8nVV'DPE lV0">PR$8ᢠHHюƷ#HPDP4D .)d_S Ff\de:0#b8S; Fڇ +" ofzNG{ HG/v+PE z1%A+l30_ xfQ!#;C$ D9 AC8AbD>-HtC;`Ll8^7 8B<B=G: +;# l7`hL;*u`ؚ \7 -Am8=pB:P-T۵ Q BC<"9ë]t*9C:<,8H:BC/l8=:d98ZYީ=Ct `9< ,#ރ=:!X6dK;-KC< 8Bf0Ha,(:C< ],6|3D::2;*0fX1@d"C<qFtXf `I xDB9<Ђ,ClU8ă:CF:*@1(B>8DX@?:¬C:9FDA7bJd&=@D!?&[H@tGBe΀)FPaL_8 ~3C;I@;Lu-;+19C9P&,7dA\A';3HAA9@2qB͐(ġp,C4DpC8T?*c;QIL6eD&m*(møcꪲj !l&P&$ ?8(ݝA(JЀ*%$lvqW"\&%YX'p%vd$%Fd h&p" 9!i@H+"`&ث&\F@$De"Eр`%]"Zd (n!&`"9Z Pad,B}a: !h&B #pB%,& š&XBUH,E %BJ@`n%`+%,FY&X,BAy ,s & B4FID%`?#%8%|і_Ы&rG2:@tvA{r?#xZ\ \!'ެ$8[P0X~AbQM%|N@X&9GArǪ &LD&"-G,lr'„6 j 8/Hت~!XAl!.\!¬'X"`VAyr%8#A](CddD&DT~PHV1@nDq811e@c,qVVOqV`MlOU jld8\hF]818@d@eS%K1I@s0pPUs1T[I"# Q$_F0i#oˊ0 I4XJV$E1.sw/1w`lF{Ƥ05 *:rp1Uq6A6I{$_?sxCwT:{yV49t$rpiH~@G(HsG#:Gб~/tExS|w+q "ǣԕH)XBWmz&,\wx+}FmR^h9 RP< W?u?"& Bĩ# L˗ ';)ЏXI;x\A HŌB8%pkcUNj%LAe, %M!B3c[Хݭܫgܹp{*4JrBaargn;NݿvNpݹyfd8j_C_0ڡCHcvK)Fҩ%dFLԽs3(eQ&|p$\,-v:v*,ڶ3+q^G>d`$V;w8Foc:5wg) +nӎ0n|5g+;9Hχ` <mA6&00OGrY83c9E<:L 3Ɖf3@ jf},.&ăNXZ jǚND49g,uy":`̙癌6#p8g :0{+ $aUT !0<Xv fh:(cZ)gFk`"iSGC-ʱ&/G0燗d3٘ĜC`s ~sD2g~_XLsfЬp1A "fM!e( B&=_pdrMٿZ9%(G8b#Xb{Hj"o& ~&,.rQx6>n D&l&[!m!ard9#ha? Rq 1Q2OgSnhNHf:a##zh8{~4~{gY $"56)g7CH2y E`^#/ :`\cM{ { p(s~{Ba'EeWYkD'ʱP2vH@#X"?pU0 *g% r ^GA/K!WVv(Hs Mz\}0|+e$JCx! bܷ8Z:e3ꀮ["q x 9\ XZYQ HSxA #H 8Pڏp@@N t&~ =:ן l#rÀEmxvc  9q` ` RR? +@PGB pׁV 7X4Cr=$![jt 1I@C2`!ȡ=i+\\ G41QgH|2Og$GCIFыQh#WCKqA+535mk= )BHo0gF(`c%bOV>I a eq 0%!߬$^YD!2f@!4$xa q2a륊h /@P$^^'z!`8P zt.pPD@*aa "@|Aa9 8@Rb". * aNb*2/-pN.R@ph!BL~/`  A+!h06o* ``* 6bzP24H@.$@  (aB F,a!.Ah aH "֏lI!@DTGD1!2h <@g$!&zkA ڡ T,b{qP$Rk\^k&D !$¯&f<ak !$za ~<3gB|P & &UI1hd+NA^I!$2A!C! $xHz!"AhV&` A(1@aAz&"@` tG(Apg|fFB P LruW9b?@ n&,\ DH(đg&V Sy@•%&F1eaVV#e(> $PN! RHa `V"AL4D$c$Q2D)S^c?6dGdO6e}m_6ffmgc^eggOg-2*3XA$ ap $LA ^!+U`R|Nz`A @Oh H!LF `TD`WC:` |aN  >a-^@tW! BzUW zVaZLdL\7&4`*a)0 z!Q^-Aa* /!5jx7yB @* @:  L!)qځ8Jx`H`&$z aWM!B#*Co_ ID`B O:`&La `5 4ˡ`>.(H#aH6p wAOa 0Aʀ`-ˇLaJam¡ ND7#3U`Aʡ^%(2NRSu aA9& aTY/Ёʠ tR ! T% 0$ ơ`;`!\:  e:2>Y:$^&`h$2@!9^ !N.u蕢P, nxju& !4A".L!!(Z |@ (|٨l `Q-aә/a"h X!jbV^A܀ gsA#psv:D  m  `Hf @*ӊu<;T@( !na`B*.79.sO2%A4G7A0[waQśP{T@$x)Ixݨ@I| ΁zc$tuHwRSw!߲%3"N4BTl:3"@v;E1C렧ڛaN).@Z)$@tiOZ;A *2  &FU9   A @M!)F8ʁ#ihu@-Z 7@Pi<LFġ!|"e` RrY;qO *>|%")TT9'jTzH`۱DDn]$ C?E!B >S;S'*J:8 ,CHtKS,D h㴕{B2D0"x'DK8jZ:L+һ^xA˘3kFC[;ID--"v >m+0&4ya$!׵o~xg) |ַK M휣-K^wBR3'b93eYEE&s<\SD8=|3D`=ct&T=CT cH?pEW2dH\#P(bp)؅6VL 51D9,`Z6Uڌ)=؃9r,`5X҅#uC9t2lggxDX -g)9 Wiؐ0+8`8`e8@;Kl+1  cN=Ԑ6R:DcxH:騳 XRV[17IE8Ps\Y)bs-3E$1Z&H[eV\s+1p6SH)>X) ()\0Fs)4A༒@DiIqÎ6 tЃ`r$$U6 B$+/X1+D86qF\M8,R6֋:M;Zh,S-pb#D 8! :4#3oVXq(BMP{XCO S%, NP3vFu'Ld-2QDC fABij eh;E=ы>{PPCE,b hNs- \> ;*aK e/C8A⓽;4,pG/ xF"1tpH .K70QuC@OzK*l{ _8)P5)lhrlUF/ވ6:ҡc Zl`6Cma{XT QBhHa1{%ыn7`9 Y0E=o`G/"IQj.@)(.OTF x" p7nq  hjN.[dc a F 0`lmOsZT^@ wi8B,0mpr43^F& Ԥ#'Ha[@,VhG?03)=Ğ&f(`BЋmDa&%HQH,'BH[r3U`P@A&M1H,pYC:0`PJT k+c?`$0F* r88 &MR rXB =1J 8Š 0*N-(GДp42 e`[O@W@>l8AD yѼ,:X$.6x+xN=Xƃ*6Z ,odFȘ['C , va@dKB6in;N}- o)^q} @+@q *q88CرI(1qU<43-@/94)l 8xBI PM', r TLIE + ?@5ː hIFBˀ B 3 B$gH\]eh+PG '  GP8x + `ۀ &(/@"p i8TE ЌRCqi i@3 4Xxݰ ( H~0t1xPlRppo8/Pp $ @ xC ︐&nHANhFn!"@ <÷Ž#("8|P`4  x%Mq!1p[#gBFH Hf l0 #*wU۰ 30Cp37aMx0}BR6y8b 10JgDZIQSB# > Ѕ(ePE (k*)N0` 4P.Ud_FP =x>gXrٓ?F~p0|wBP0@ Y )F ޠ*f~p @ P nť 5tZ0` /@i~? ge`V ~ < `l C +- aW PX + ePu`B U@>{@ PW1$ 6ˀ:@i P!`Ipe 8 e;P2$@u* 4_n  0`@ s91` @:0kPPp@ j@ 6w Qp P@5 p Pap b|z< pry%㨐JFI{ %W0 "06Qq5WF@ ؐp@ ˰[P@ O3 rp uj `@" 5 C `0Nvsւ\w 젪D$c[$T#԰ 7Q0;# p:PV`0"p  {(n  /` >0H@"pn;Pau|l'@ ǰP?&&?LE Sp@+[I^-&O ;ZB@ 4Lˠ |\5`a<@ mdpplI#p5`X =q6 Pϰ&twp;МkY` m ,eA!$N;hp!T`mQq3` 6sa` ye\ ؐP!t`5Hm= p|!pѐ0p@ C[ - <|R VmX'DPwe h@`6lQ& '\CBp![/ @QAdA U|0Ƃ wlV- =!@z PM 1# mU  `ޢ10lӴ}NJ B ]6mPߢ 9> PPe e QM W"1 0 `.pP BY@ 0)|h@ ` (۞c @p `xL} [a~l'^ İ u  30!?` F EpV : pE&`AEP`  +Ȯ@ 5C4a6<I7V3EVP>W*/ ^󅱀':I9%`P;1K/"<$3h^\cS!On~J0!eRYr3RD_J4I Y-/hA! / h!1:CA" JQ" tQB #DXᣄ7Np@1vPbB]BaЌ>^̈PBK!*d_N>5zSX#$ CL;~zTƽQtIBgJXa͡+NUרوLQȡZ ;=<[r넾f=&頂"z~ZhҥA@i֭9zkڱUƝ[h$D@gwܾ}lH=]"2'2u{ ͟G^zs?{I'+P]/@d3N@ d0 rb;3 <ЮA[ˀ7ŞW s)gFo4o_ h(SF3GF& LGu8!(l/63q8.|9 M7߄32LsM'}y4۶,0PCEκC.QQH'R%SK746;5TQG%u>-5UU?@Wjm,' zjY} IF2W3ɬ5%\Gi?5Vݭ?؈XT=vgfK@X2V`µj%ֳh5KqWݎ &AKVMq ^5 Ɨւo w^"1ab;ֈ]}$Wc|/.-`7Xmidc. =^s%])cs}U|'W2g`9vje?6vUS{'.mm-qā.q6#)c@p QVa1 `' x瓁Ic?|'t` rzq$2̑z9i?eza ±{AHK`gItGh} v~ ~# F@S|mKcMA{HyAbZk]^|@  K,(=50Qzث+܁uX"{E4P,$0 o+n=;)G=z U؆WF?F Ϙ:$qL@t+fMzC|@ xNL[,W=1Hd$B2c8ۈD&GH FavW8DdC(#P8NAt\.|F@AKB {PK@AvH]| E2lCp11sN `8. )68jׁ'OrC+e2^cg<5G8@ P @F@䰂,p],P!,c)> pN $w/E><?C P3 XFnr`HFCx#IHPW@# X>St#LoX {Pgh&`U/4 9J LЧTlЊэKԓbxA&`qD|9"akY 9"(GpmL48=^Az *uCGBRrCx1,p `YH[YlcT@P*1G( 3=nAzܢ^XjN $ &c(@BDP{t0p2[W^Z2xxOzwc =񱈰؄9XGH>H+X7pQawsZ?F7hXBmD78#-_Z9A Ila6,9LyaB8a SH" @A0HLdq0$ A<5]G"0@J" 0Vt aSE qA*9s!tp;` b0Qk ]-QCXF7pV'aA-9PN-`Uv%<X O{zCA9TF& {# 1 !" f%ȇcD9`ə:;iPJv `KcZ&O `ۘA `J <rУ~< Kށ^B' d -! `0S:i\#9y] rA8(+hH<)QS-_lpD} ճuP6A'Ա|PJL! ?-as{ ^{ o#/4 @[J݉x a$ȵN;X{86pS>q>): 0rs09[P@h&?8û:Ӻ8<؆h H> 97xpO0=c g ȀK(0@?r0sq©U2O#H?Ãp{MDS99PYHvH2X?6P0 K0{ ɀ@v$xM#cccrP(PBЕ0jvxg@I0spD9PX[$0nSxsX{؃G7$H7 OPthU@vXsz0#@CӊZ@n<@Ȅu0Ȁ+`{*(h'r@n  sxSkI1@ c`p!`toЄrPZBvXx$ upp$up2sPy6SHP8vx—S1Ip0+Xv؆r)pCP7!{H$JDJ\lXl#x(c+ n >"Tp`O8KpHK$yD܆+ s0'IZ&qxS=L ʫʥP0t` l8` Pv膵;+Xt+{3eȀSxN0DLO7OM(st Ds ڣ`%[ChS@)sr uXaT˝ ) <(K8ɃPT PcJb" I.|@p@ ʆQĆ#P$ȧªn Q AR {lE@OPh*ķ0h z"`9u`QptZЇӡ݊j0vhA{؆UHp8 y_-ox@sVҢeβ% Vzp|R("ȈxkH PMpq prsLGICDÚPM`RX{ #4#< voC08zxe,E@J:Yy0w`{:8Ilz#AyT0}9 KPTvg`Zx| <KHNhS[@p!/rmu `Y%`1S ##Hn Q4yH9 PEZ0PZuXm8WjpB(L| ZD˶W W'x$qwx@Xmكx={{yzRƎ@mX+I$(x@\]эhZZ[a-VD#@rxrx)/{,8Pxzp(M0筀 E\p$| Z{W{ [`PxUnhR]pH=r9yZ `zW`rP ܴ]]R8X]B^lx5YoaXo#ҕ U+hC:У4l0T8r(؃U __Պ&CޔH|}p.Kx Q79uxs ۛXg #.E#. XBneMj0p{:4 ,)H0LgV%692u)lgx!82pЄX * 'L @4$( PQ#P`?0QgQ0GzO7XmpO [`0M  r7EK.QX8wp(j8@1pg""):x0sxDqx[@v1}b?h[spy |2U6h&P>F-菮&0x ZeS?p8\O7wrKJm$xwep(#FM"xkp<zDJ (kr`8GIBpz0@Xθi8rz#j`[0- 1L=Ӟ{@|>h꧎N0M lec q^X-jUx{hց>se9DCvnk?xlo5TP*c $7 (8h(2pg=ZNp: b5[dܳ2D?Qh" &jKn>!(D 6XB;"lTPF/ܠ*0hWpqn8AH!)4qF09L!<9)""@G7@' Fm'&Ё $X Tc)1 K8@ REv9qz(M%:vE8>iE01XEG@C2#4AMqSx7El8!mBJc 6Nb9 T)X4ȡCG> <(ވHCJ!AdQEؠ؆CHxɫ |f4 ya5mU8`<$Nv^l2P)8j <<%Q@UX)6 8AD>98ivt&4!#d"d2َ3RC`Z`? P nSÃ:QWB 3*(gɂ:Z*2$Ci:A@8sqYb [N' % zr̮w ^:jPc :NYx 69^SM"AtːEHDA@q `/N8'(`R@$b!88!ّqy|\s*qthW )HI1#PXx'q GRwQLdP7EB9"AB3@sx^@wT9@= R x N T9E !: H`$TT<؃N"7t489 uP@2@!G/vHh(C saYx,qY\az]MXufr8z჈vc A8$#c@LmmlP˨Lc[wG4A< PG÷F Û-~c٠g aP fN0B8(18,qE [a(!04y1896yQ[ bt$ 6u@Ksx/(:dW(@7 L,!"78$=0 AY1en,+8dF9yBl7hA@;LFc>F4ׁC@%(@9BLjxB8LQ[;ԁ J\^H1p%p9؍k&#LLe;B HE†X`^^1 >m!v~! xB>JʔyB9F7@G9D  B= B;C$@L@4t 9`܏9D5G8+`ʻ8A-lCUڥ%&t@m&Z%Њs( d9<-2 tLA>@s "ԃ9  \H6CA4xh@ +4K$CWA;9p]A8 t,:AF"d)+9@65l<1\!ԃD)!l*\= N9B:-P8qV:c;B#]B$A;,B;xC(?7t\A9PC<]C>dDNB<% &* a L<9A$(:BOd86l8:#;A\-ABM^Px5ȈAcB=LXY9,A\ҥ]*G$90A407TK;K.(B=BHB;2a#1d! , "0&AH<Bt6r1UfVFAAA8\$;ȁ=H<<$)'99=hB86C7-* `DD'W;- A:6R:Y^@;B*ltxE)0d^X8lf)8 lgN=f9B$)% 21C;T;V1GX;jbi~ &?LY|';;<dP:!0@d::d(??C4Ld@ă;;4Jl8ăe?X;>ЂR6X9 E9>آ0d;G-12C3ZRA¤C:+:4>,Sʃ7t ,e;&Ҭ,@+;-$*Cn-6dӰռCJ,GmPDV*lB,A7l;1;Ef-,S=< 59˜ 9] 8Cΰ",ԮNl9 f h'CH:N h̺)Fl:C'0%Nh5W6bQ#F )'/Icn Btk!YLr31c3s=  go9;,8GEEcHFPP|y5ry@Ipk8z337:Pks֝ q9RP(KDzww A=BZ(@+@%sޗ;ͼPϗzzǺFz/@;fz Jx۹(q Z NzO"q{y݀39t{/S]xH';z/ů3;嗻;umsdS;ۻۺ뺷zk |0{{{My'0@zw4/2y33Ǽ {:<:A;ɗ9:S{|;33AqT@ATΣ<󻱫{<1:׽=wpm=>>'>)?>Gw `BL>go>w>u3 _y>闾>Ն.>뷾>{Gk_׾>~z?SO[͓|A|(v Fͼy:3k;$7stU?OGk{ }h?hR;W0c;?@@:A0D-!`G0v@t 0"J5#yE<{^@?)l 4>U Y@2%DĊ?0\bKME:ɥJӧ>-,T-ԯǕQ%0w2E.eÕ/ xc}/;,i¡IG.jҩ'[Y5eӮgwh`ا]>xq )8/:TS$# ?XFӥ,uMv8eHy"4҉h3JD") H ExЉ PH@zI DP? @ēClD’M0ʠCԥ*SNw#HR.X8M!&=}L'42Uj H@^x 3aD@(0HB8=,tD<@D(a?#rBpl>PԱ(9Xa:VY'n'(DQ,O.ְ@TdU0,q(, @E"{A;8cP(`X, L` G r\" B=Fp^&`YFh3] 2u̖%p8JI\~@.)1$!C!pى!C"4@e!Xp@0R DlMy(pю!@"7K l) apC Lb0E؃6!bhqE!ʰFsDQ8> @=hBkD@@ PH!ԥICA'K!#$dUE8"eY*a TU *H5M[>UkObOغ^MeTi:I굱 B #D`p&8Gr"$ K (oS|+$@\ځ $ 8z&@ptf(q<.8q`Z8p?KȤ()QPG D r5f z'5ݻ-aT$5ZdDB} \qwt?аBVvtY(,-%?^qCx8}p9!gx<iGetF8CH:юMteG=a{8B`=%jLЁ,\!P`$fz# DWm#C8dh:8Az*A` ,`LA$`i*f42`@!R*@.:"bAf8g Fg@aA0t^A`l "s.ʡQD`!f bMrbZ" Ld( 6@A h@a¢ H%$ Ё!% AeA!!‚ N@NzaA*#a+."D `  MʠWpڈ*nMmѧ\%{6Pp2g^'f{0Bx*Y~ރۼm"`A "0@N p a4}|"aЁʠ d ! d"0v$ ơAa!a0An΁( "  a^!\2@ xhN3(I, m|`M¡j4!cP a a<1f&^uQ`G!V @kdX!`ֳ=B($2*CĠ2JTH!`ndEa`! &OPzn*4A"A dAO @!)xF  pIjVa A@p(b`'a &"`؁L.Xٜn~22T ST;زg4s-`M5)XphFVa < > ځa! m0!NdmDa~.Lmp\FA`,B8AA +("!Bf岣 !$M^`H|!d܀ҁa@!`*Fa/gfeA!c&!IH`&0!a AJkH ":|` ` ]i@L(+%h:@`.da,i{0"s /Q$& KgR{ aSWJ4AT)|nA &ġ` 6af)u"*w d b_fCanȈ؁ |`F+@z!ժPSH`E"5bi%^5<0`+4HS5bmJiX$_:+C5!6mb nd'`#>`NT8/töRX*: ā4K"&@w{@ @".q@"D:,zM\Af= !! } %a+D` )] YԁF"  l1 @*(se`ab`!ġ(i!c~E Z4)pv$^!rV8ysyozzILSe!|-2U/ppn"~wS9U/Mzs0؁!"n!`4 GeAhA! VXb)qj\a9W@:@.ʆf2WEb1xxw Vzv`42WЍE;8`sf HF" >Lz! ":H#,x˒|l 2w"v)e'OF!!ر)*@:NhMB   Afb4-L{U  Y-jcP!"jt2[d!^n:7%q wuwbڔxar[Ei "H!u912 /5\/},f'w~w%Q`A Jը$zn!@&1Bd ! `!]XgenA`Xںv[ Ɂ@{$: t` $_ qJ4NjXu~`aS1)*2ۋ' 4v#{ͽTN N@eT>6aIPlI4^G;D`VV ċq/*NVrd* ́V᡺wI.N@@-Da x+^^@ A@_ p@,9'.^XA@?! ij͂a4eAc>!p^(Ab ` L1 N` >D3Awb! xBy/b! VbFmcc+ !^Aaz7J!COVDD"kA ^q%.0ET6-ֶ)<rxLwL ?(UxA ":~p  -RCKLL2gysM<{ӧΚ5*thRIE4T>q2TYZ} а[vIvҟ\{" {Nq:7޽| 8o?heI̊ ?ȅ37-\&KBb6)K]ςG,Bm3nsׁ?Jջ"aw^1,pEenVX.mKA\7(/dSꖻ[2`e*s| AHm |!aAp\%*d)9$Vg5P2!=8829h8 vJ9D PC8PX`Ar4*cS1 "`*DI;HH9 QI 8G98:ÛoᔁdC67O48tQ _ 4/S+\b-Ca2S 0M/s2g @ >堲 ;آS$q1fu 9`R M9H8L+"<X7u@+/SsK$|B (b|*c.ImYPX,hBBȑ?\hZX|% r-2^c,rET]f<3>WVZꦥ2ZtJ/mUc< D7sY0-iS;Gp*$qrOQ`Als=cs=W9b3Y-xBYlFm?hc\USc3JU`+t8P$p&9s?혂8R=\WV*'x AxH"q=ԃ}PAl` +_^8t;o" 8Hqs FT#`@ BDt aM@G4ы'DbS9ypBrFKad .7nЀG/z Wi p qxDUtC8ٌ${2Q|y5q|t9=$GDx)V/$>r7K` p@  X`2a"I7)eԣo fgl"4_*tQϊ`s62 As{n!fYLk%h!0ƴpsLyΔ$ q2|eR8N}(B$ɛL {K8G'gP/WW 4' ظs6n%W*޳8@{ᤥTiNn7)Iȣ|z"C9OzS8FҜR0*Mع-Ut# Bм /X ,Zld2a@٢NJG(@W(C RuQjΩ3>]=#qh J9PX@k]pk :nu1Y\rLeC%#Ol!ʈn̬()KYn3ykjӶ8_[afĥ7{M-:fɉwm{ >WխDc,}smcuC o_ul~ k8i@۰8ϛ^dNF_ x4{]>䠇K!~c^ 43{1`:1e7G1z|sy+K1yln fLЇ:p9i V];ncD+Z^&'3ɓ4/LkˍJG:i͝CENӪUծo\ϚҺ3lP.v|w#DoN6kQwf`FԖ˲kؼo wڅi7;`eܐ7ٽg]QW0ͶVhm+2"z%4CpSʶPlPȁ'&᠀^Ygեh&2pdvsmmnyw.28#"Jy1~ͅK}J Nl$,4X01uqp @Ihљ\&n`mwۍf,t}p +㵋دxkD)XـLʀDHpP$Ԉe0E8\"{y78^3I=_,}m[=n}rn }{r=ꇼ[~n'l:ܼu00`z-P@ SP 5e ^4yV@lPXe}|G|}wUxngy'G3%+uDWfg|ւ?M0NxOrot/GqvSG?N P ,#  AP b@G7Ph:p PG>eeإt+Hk}}6MvVXH&x=SLjȉ0hsqW.b<ӈ;S>|FW|.xG8bi%bH?ehz@ 4``b`/ 31pG!ܰ BXPG *dx b*w|O'f(}7s'2̧|L׊;tXxwG(O4ؗMX~X}T(x,%R0  UIK 2Y`CA~R-gFX8̔׃9OBwxGgwXGrSg}Cqj1Y dht`P"  hg FVOx`ְMRW'`e`Gxifw3x}։ahPnmLx2Ś7O莍wwx mifI(MLMm`4\@)ՙjtixzizq pCte@ "'P`$pV?,d9@|R)n'h yEZIp^X)ɡjoI%G ~BW}bm I]`+  9+ 1RhuN p PWW8`Cw_ډI_oyh)diǦ$jujj$V-S8^7nfFB6:uy:<>0%nPF0L*CFP@G @ p=BI +# n}Ztܔ_龭r.#.^#̪t^>^ݸѪyv_aF#$D10o莰` n bg} ꏍӵءM#\┨}ԍo)NܦsNft V8A8-~,|# h!P.8{ysĸ<\ d.evݷ8a9gWp)/^iՑTV_bP@ ,B0p0_11 "DB0LDZ6+ٚ.^&d2oZo,.挏sCiq p M$`1n0<=aְKbXS=ד  PР?cP!C6bDjtłEƉ'=\ʔ(CRYSfC1-lH/9LI3ѕH7҅?YT(R8}ZWaŎ%[Yiծe[qfx53_Vb핂+MpP8 ܉@~PA -80*P./뙧iZNӴkU &t].:5$Y&M8msJҨI^>it,vM{f^\կg{fXeO>B':90 K-ܑE SpAtP.A B 0C;CCl$cLHEF1r|pGmC!4H$lt'rJzRD)rK.K0,+81$ DsM6tM8sN:NL&8̱ $ӭ 2EtQH#tRJ+pǞ{y&Dh(4ч`Uh8#A0KsuW^{8p{j1BBn(3  F(!Cj(aĨ .}x’f7Mp:),!%zHb48aJt` :QCn~C)H}+UA \ShE-ʞ 3hRt`gQ$3OX؄% 0:#M όqM!YZA(^TKejSj4!`@R;::F (^Ā), d% U I:Usk]:{k_WV%la {X&V rTX`ple-{Yu 8*@\:@^ Y*CutLZVE1धիxd[mooJ -athHa?%01N`H!mt@C ԡxh?2SP 69Ѱ(rHZ' }rZoF7V$rW^5\ rXc yF^ YLu0.Ep`GR[|F?QGuGpT&IHiã֬&cg{K3j۞~}R)_3D0G=AUlc׈1 ! G(bqT Gϑy|Zw߳u<ԩovݶxll{ݿ~N&;ywkzxE p JF:k W&b&$0gɁ rg=qk[;Y;#>K;*?@Ga 99!l`{.xp{|TX\wpK rp+`Dr01GPA37 ܻC>*B >s L-\@@=;@@؋77`;E1)؂-@3LcxKhYy6QY>Ht @5 BR$ÒP>PE.DC+BV:qTL?SvK1(~Ɋ|pc -8)tR W8eh03u8xsDZd&?[{9 ytGxLx4\5Dxu 0-{HCic؇<x`G[ O-v Cjz^ p @z{=Gu2A9t1lkC}t9) pPJP *,J4 2t/bɜ }J1 E0 7Q yJ\*ɻ+1iKj"KLL[L|LȌLɜLʬL˼LLI@ \ B-ˠ`L͞kMsMدՔtz{`M|٬M N޼͗?| \7K-yI( z '\GN( O4NOM4GX@jiV(O:0qN(,7B%UʎMKDwذ,L>4Y,99݊)Yfs*j@ eP2&mJc.v= h-WE{؆5Zu( sy؆,pr(JWP#NZYZ}.Ǘ [)M)(*I@mњŻ}XYZ[[WMM (: #ڱ5T׼[˽\쭶ɅV1+ ]m*UՑ5 ]Uhӽ]]uz,]^,B܌z$bA + Q&J"yxz_h%^}9HB.zb±$pO @RR<LP`Q0G @TP5OX @?!<`?84 Q}aaYYvE͆/ 0C0$PlhsSr8s(}#Uw0!u+vy%u8yNe,?`WؾT؝a p Ȁ$p>Kn !ecMN߆.j>j /'LO 85^8c n(z`xFz 8+HC [8n %[`#Yu(0f5Hj>lNlFE? FVe9vjz`8q 놐@P60,%N(1(3 xPh[xQW}>nNaF惾1\l3<> |蔅R%t{j <9?(9 kxtV3tap/pZekQgh#>[58PRpIv~AQ )+wCy Ԇ~&hl( r!em3ul@뇙g)ecٷ V6l 9H lBd@!ވcۀ1XV+آ/3X7☣;#בNg 22E9D=2NdȠgk٦os)#d`C٧g ls$`͒D,8ZZ,|)4]`Zdi Uvꧬ-1?d@7  ϰC3SH0iu&[FZbr휀٘mNh/'=L@$HN E`=# 30}~[1G PSO; D&K/ `3\ͩFl- 9rfS_=ÆGY*nN;PRbNP%( ! 8`  T@ R.oZ"8=,gTLnxh3^\#Ƙ1r#h;13 1[ )HYl9\$#GB2~$+iKb$'1) &"}Se*kJ4ni,V>r%h:PPbR A,,@ DDt@D0iGb(2h6 $S%;g:Zmt 5+X}D@4>XPRbܥn*P>/$(d?&bC7Ϟ"@šhV Rd0Y0)u$8A*PBh,`B0#` PG*CxbXd>Ѻpuk5Ҍk[VYM _W赢'=G8Y VgcQ qLI *ЁyD|6ڡ>SxE9arG*sCԸ¥z vlA&1qzBH2hYO -d/X zX6lbKofVwD\ݝH 61Rd9&+N&`"$I1(*?XBppXc#ꁊ+n%Ⱥ)+_6w/! X /}Q!վd&[^e0)M[),vs]|]O7*\QŹ2 j^x 3aV_x`CJOQfYP@&PJ+H`X@Z`PaʗTd OKuWLodƒ;Y~n<琢'">mis=i酽߶ ?w9y'gNbEplQ3a ސB;Mrb @)bP"$W@Š\H xE$@"-:nC^'[ە7]0|[5g#oQ\s7%Wu~cߗhwzsg{.W8z d7Bım4+ F7l"ԃea(ѼWHDl"DAP@7>ZlCވI}=uB l Z|q|Aם[&]T^1d!*:*ѝ ?,!bs )]ZhyD=B -a5q <؊1(@d@L\'5!"썠1DC:,…I$Qbi_udƠe_BdZ ) __ՊLa/ d)C6uJNIHjJCyy2PD\u=C-:%L`B9B\`;C@ 7 ܍YQClC7# F_iTFN'Za,6_GVE!ݠ]:&n=$-]L`~$2eM~PQB $(v_> 65fG(#9 7,BW. l5@ \`N§L֧G*.MVQ_"ڐ ~b$d%(&zfeTgzJ&+2}"i\19)`;C?B| 8=<)<@0:p8\A$X%,C<DD7Bu;TC68C|4^9'#]$^ȼ (-y֧D.៙֍TͤȂi]g"҈&fui"#jRZJDl7C:U>zDh9ãA9}<=<%D?LCR"29P2DA78}i#EY^d`]f~丕f*jH&f%>hME]r+ j~f޽)Ha)_ᷖ&5g@ED4QjJ5=~8D]8@FxC6Dyl8l4ky"J0E~IS۽ 2C$h"z_d,,{]^ Zcb+ %U4-ц+ATߠ &E؟UUb!*؃>YD)8D+CeZJndai'!1X=VR$@RX3 ,fSj Zn .mؗ!Qn.G<*&o+&a.'5EFbojr/auoooooooop p#p+3p;CpKSp[;Rhsp{pp p p p p ppq q?O+3q;CqKSq[cqksq{qqqqq?qqr r!!#r"+"3r#;#Cr$K$Sr%[%cr&k&sr'{'r((#)r**r++r,,r--r..r//s0 0s11#s2+23s3;3Cs4C3Ss5[5cs6k6ss7{7s88s99s::s;;s<>s??wL@A/Bk@W0g$SD3IDX(HRTFs-~F'Q,zػ4d;mҹԾqU++AϲB4r[* onon3%DJ ݚSl,(&)B]XPU8)Ƭ@$GDD%BH$p&&%81)Ŝ*Lt.)jNnfsvg{gvhhviivjjviGE՝C-91v;tCF A7l;h18<L "$7`*h|=A@}@d7J+L@Ş,A86M0 9|E+"3p-!܃#GYvm,6N׬:odӐ@ 8@ @?{ @W{kkw{c {W;[;;;{w;| @'[bQb}LS5@=D<=H7"2do *|=ّ%Y F9-@HCp$!BЏP7Bg D!0 ,D< @FDC$ =,BXBT@@8B n=qH``ڮ 鱟)mT$>W`>$TB%<~%`~Ծ~#P~~%A~%>><WB'>%@gc H؁m]e%bëŶ#x ."Ć2cF9j$ݽ2 ޖ`J\D"acd%Q;@9ZE{6c8*t(0{B2 \?D V(mYV9"8! W vP\wDc[ t#ÊW[e RʁR Na=O$D0ŹW8fn6@-Vwncl׵ m=;89<20ڥSm0cƚ  \W˔){xg@;߬¤%-fgΛBibq&`3FJN&/8^Rcy%s.Z UXF'EH"*$J5[q{^7Bmrf:@}rDy{!Ppht!(1Gp$Q ugN? t!u *]zY Ǐ 2+H8ƚ zXOͬiW1c>{Vy ܹq D\@kpu0!ЁM6Z|pP19b#A!†dK-bA` `E[HBtr)^~s4hA5p".e!&e`"02愊nx8H 1C:z!ш(LaE+\!jy[\=[# :4A|xȱ} @qw`B?Hy'(ɊDDJ50=1cD 0:!(bh-{psC 8/Q{ ,Pl#怇%, .&As+£5S1C yG9ׁ4ȇ"r*G'RX H@ $ . >R5`W_w7x^F=o# u pJe^ ST5 GғAFj[mE&52XFku!X>qc!Va:U"!5} 5oۯ2iD,5E(32zNCuYDtva$! 0aqp`Ȅ[idLo. C9L+\a 4 IAa v,!(!Y WO$ 3h& rXb߈ڵgLwE`'vbǪ$O8n!?( 6'fbϽb !Z2jnAzmȼˊdkmr[9egGNճ+/s2%'ynHʡ5aYP811'ArKR+ XPыTUAA)xȆRhto?Su:WT@y Qb "HB^f^(B7Dr9^A 9|/BA[< C|9y"⊿lRn^TT/ DO#N`%da`D'l@,"!rg* X`'!t`ab`D`zAN @RNS8e@"A-< .U2Hd,+ 2b>0 I!Xzn,.A p4)%7TN@ >/Т>.j~Oo!caJAx/@p:!Bm@QOPȇJlȌOGy̎j9,q#D z[fA,Pa؁g!\HLƁ$: !H!  "F.@~5 b` `n!)L! +Pa a1$&tF# 0I#h&F!" "$F Β"->=-ܴdjijk4 HKKbڴMs!M} TN:!ؔN]aOp M Ms&R4RaN S5 RT2!Tu T#5N;NURq!O45Ss@Pt pQw~U(Qa mIHEl/yӞ4!ѣQ :SgFaF#"5F'G!HE4UC=_j_[` `\.aabVb'2,*vb)c!,6Vc7c#dG6e2b=Vdab' dE6fvdb+NgW6esVdghw. `iij_vi`jVk%j%VlllmvmlvlVmnVnÖlٶmmvnnnnVo7omvqvvk#Wr'r+-rM`vs3s?w:<JtOuSWuWu[u_vcWvgvkv_7B@0t{wxwxxWrGvyyz7vsyz{W{{1wWw!|W|!{|}}}w}7|~7~}}7~xW7"׀+8 X#x~-3KX-+@R8o.E'@X2exhX#m]{Ԅ8j5;8|?x'X7X|XX8=؈xŸ3"*@Eѵxl#x؇Nu8:+a5zgAB ]eA!*AAl+L2[`ɖF ayeYaٖ`jw9Gyq9wyYٖy9golYYyٜٚy{ٜ9ٝY9ƕz?݀聹p-Ԥ~V@',!-.$`];@Ee"a .l -@&|ԁ(9,`4C O^I~baue>{>e~!W_P`}aXT}!qsמ3Au>>EW>^hGV{~^ ?G&Aa ^qAOs 9^РԌ*`18tLa`NNQ@zdP81`m3N[y^.N@ M?m IHqBpH̰h*ЁƫrD_$ȡ:nd9|"Ȟ ?JJիXjʵׯYA0ƖYhgƧh5j6-aԅ[L0hixVBTH,]15 +JN֥1f)Wݼmږp'͸ȓ3~͝Uf hEZ o͹\ѦY)tn\a4!%$h(b#ĠCM HbO9bD U>#=)'H-.138Ž>s NA5bUP6 QQ|C 6(b1=C 'XeF#sD "R Lp9]ĐD;UNT8Q&VU-P>D"x$ldp6"P&H! 8}0Q H X54}oCN8WpnsC`x#h夒B9@/ =7 tC,>!ۑ'=hxIzWX/Ŷ4;U"|u>dhkE`z>?FȐ{blRqtZ\08*:Eq% 5=WC! bs1Y Ԉ"E> >Sd2TrhU ЌyR V/,huh{`KOAh40#`qAl?@7#Xj>!p??q ox|W`) RpQr9.cx+Lq `8ؑa"Gl^ ul4! VNN%ޝcKA:>O,e?>^@Җ*Pmoe}}E~2E-uT x kK'R |$8GJ;UO[`A~Rw j9zW/:UjԯJ[T$+ɥ'v 9|1# JSQbɱ|2169W! 1dm$*ʱ`⸬D,ٺ2 a&}VN+b()h!)dmm.S]0)"X?VyI37Wsx~Dbc"HjL$w썊PTrR*{owի5N0M,X|K&7E 0;\/Xv-1bv, Ӱ5.Wr\"|nqSٰ@d-Vk܊iٝ./Dx l3_jo% ^J,X` qAD9p HH  8 PJT*:kn^ E0f! E >MFx9!@";q ZDp8ά ^?AhDl {!w|:~ ؚuo/^A2~-׭%~ϏaR?l^T;"Viy,/gG?.}fB3cB6cT;F?!w}=Xn<Qirg2UVOQEKTWBjRg n~_v(5bdGatt! wG`S &@@\0 @ [ g> L3 @[ `@p`~ =B@ JW eF0[P'ېh*K@~ZE0 f(V{Q%. S،&+j0QSPoA.}0~NF?F~f~2@UhF$tnčpA?8 x<0TЀq-=FxUHDsd1!`jU%Q( *׏9?;74x168W ǃySH?0s4mF@[7@0`  YVP(a `+j00H:@Hn0pPu"` ` R$ @{u[ K &$pC`x 3`0H xl$ tP ҐY Ӡ9 P ȹՀ P ϩ P) 9 @Щ ɝ)y Y Y 0 Ր Y  ỷ ˉ y 9 p ۩yj)ڟ 0 * )p0 yiZ Հ ڞ PZ >Z /y JI)Q ypCʤ6n:O" 0֗? z+IxI +0&>p<|$b Px @ P X LZZ8& @ `M{S 4 no_DfDkz O kz fʭfך*Z kz :P@g: ʭ:JOP:Zغ׺ߚ+ъ kаת!K%J2.ʭg@ :+ ˭ f`C@̺}$ n`QN >1 `)ʴb;d[+" j@jpukyuw+~[{y+t˶[yp+p+0l˶[`p йy۶j{yKۻv˼PpkJ;{[ljkZ{蛾껾۾;[{ۿ<\SQK"hKjlJ^[`k "<$̿{\Tk,O) ` rkƘiEP" [P'' RRR@{@p6P^`bG` Yj={&4لUAxN 0.skw@P'%[ Ĉ)h "%;V16 p  ˴L ˳˶L\˼˾<̼\̵,llǜ̹L͸Ҽ֬|ì\||\ο,οלθ| l ] }}||,)] *`,0];4Tz'@ QK ]T@ Ǡ@`~ILr"UA|'E`63(Y'LsLb0*uYhc`hINTa:0|ʴppɄS15P۶]/p۶۵=4P9p9@10ܺ9ܵܽ1@-ō-6ӽܼ۸=3p5`Mۼm۝M1`MֽM/-m=] ޝ]֝-3pm/ } ⻝.=m.ܽԍ⻭7>24@NGN&ͭR,0ם6EnZZnk,QDNoNfu]x"P gbly`Ux@Z^ԮxІoXppP%Y @x^am=b2mp>npj$KiNWpQ!F $`W epF7pxK@<F @g @h[(P/O|pOi? P_j/g`y PP"0ip:? _@O k`  -? P'g"7O`, f _ U?MO?`I8L/4cOb?|Do/zOQT{ ;p kOo?='_8838??EGOs?/OhaP-|! O"?"?L_P!^1 ޖ`J\D"ƍzU B{HHFhY8?ĎQNacD!ZBl lp[oץ KPRKmN`௹f0j&;2@ړV ADrۭل#9픃BRj:B. oC\F[/rO[?ҹP5KSE4 g; G-$j@QL\0`ҰQZsbPXξ:0Rrys %CCtnI;DD11gL9=PՆUD0TO\)BqmTRGqMSKmTvLӴ:QxA{:h^xr9BUق1r@91  Hgƛh٦D# ^أEm) [Dp!h^d 'A2 *wcsHE@EDE Z#[xd,XM^{8lVf}hd,(afc!# oF UtYag%N хgE ]lyQ E>B fYl&Fg]U0fm3 &TǼIfqKC[z[\}uIoFP,'?fwKbA:m(.'FL}SA}S$G C7:f.C,C-Tyxd(]w)yE*Ǻ]/sg "{x1B ]BvCBX=~.s+,0g (&8XT&c-FH`pca Cb5CI.i`5("PV} /B9lA,/ˢ0,#ШFsyQA٩o{dP?N0\2 穐\<9G򮕮$C,iGLB0`(Շ $WAE F$`c0&X A !  s"P@֓ A:Qϥ)@A $@b bt` Hł$u0&0+ a'E 6Őm[)q uЭ㸅& uBB- aZQqto:9^8Z `/YpbG"Q%`W5TEqv_7'91F/,47X!ðhBc3sMpN+z=ULg Njrq(EPvdb'JV&}kN [so'3ne! €m,9/Tt 7О(1щ:z!ш`"'A*ť9X1 XAS 6Q=[# ̅:4A~x(ȱ !1eMsPe: rC ];m @p`jьQ6>73!3{XNK @v o (n Jq0qpp@DPR4 Z%+( P"SB8 r0$J) =K`Аoh,8䉳JȀ$ Krx4)fz빅}{Hs/4: E)Ą 8GK#GS1N8:8-;ý<@GAB 㭺[D#GHd"δDûŢC;FHT|$ٴ:ęCYD,j@<;F8tC#.;;Is0+jDQ:nL1y['Ԣts1{\份GtЎ;5%W>'h g0@oPN1Ј8X}JTPP8 om)WXl0xJvS@l@SJ^`ԍX iQ**1WF5{(b qHOTL"hX{YxMTث̄y{QYL,#01 FdO y|CόMsOMCZ9\|9dDbνsN<2H-QQC0Dl,LM4Od|EoUGWEr={ԥ`,ζY9dפ{ΑWLdVW5;:w]4GEGM"弘]"e=4OLX$3{pUm;-2} ]>,+UNg"G&ՉX{}Y.X PK H0 6X?сmȆ!?S'x@dsnPwp?0L<P,PGp"ȆQp[Kp% إe2XUKTɀ s=}8pw؆OL(@$2^8sýzRs@]7y7 e.yG7򞡽.r.NAMqQ2|_%}_$+ J+_ e_{_\` [ۢ_1Z2V&.E0]&_{A:-`.2U!R&&~HY`}a)xD`a_`_Z#`M`A-q`a,_%LH!sa[(Fd0"Rh$FH1V)/a 3:#D>%_2Fa=&Q6*_^arz:n%=d4ka0Fa}2腹v3tRus@e`Nx XomP ?c0rRtp!(A# Tvuq P !X[P xAxp xq = :l0@v0 R4 9·^ro؄wk}?({-hh+Kb:h,IBU!Ifa( ~,ߤ{_z߻k!r^#Ic n2 aj*0E`E|je]`Pr`E"1`?bxlA63dFd>`cGry+`5e!-Lx cO.cgn5ZP7^!~V:vbN U31Bz+kbN'FoHbbBm] J`NVns_xSAQbs\TЃb@t`(;FQ`t;sS:GPwt;h:WȅF/JCs_PtIu`0D?tAGxIG;bXPt_b8bQ@vTwuZbat=pPwvhgiYot<=t?t\'tSX>/E>IKwgNqZmTojwhY_IDž[otbJ`Z';0=_vRWg~wmg7V|q'ruJOP_wOt8yuK7jQJPybSt=ouF0xwbgxV|/Ggyb7Tv7y7H8Tx'yn{wwN?x\pyNT^ub{HzgIxQnw7z` 0׉ r +W rU}yr X6s Wgw߈ ڧ}hۧ}ݯ}77?G~_~}o'W}_~~'~~~}@W?G~~l B \СBhpň/bXF(VQF$;j ć%cfYPÓ6=ɰʄ@ %ˏ=/9PGw$:QCZ|u"ғ-k,ڴjײm-ܸrҭkZz/10b&3fl1d!L,v\oȚ1?D羙?iICzd֒S&,[qՇu\zy%5ևRn:ڷ5;Ǔ/o<׳o=ӯo>? 8 x * : J8!Zx!j!U!h[TA}TvX?L vdX;= B?$8&cP.JY%CNZɥ_.e^: &i[)`bI%Yfqy'}Y{9(j塀ʩ~2'Jjgz&gRi?%#ZT05e"$%+e*fA7ܳ>`Y& p)l!Av Xs8p:DbzJVފimvhm‹n㊋nh蹠;׫<)L/G,V ? =AHqLx.{䦇:듳63zN: ү.k^7O>w{-U11X ((288;Vz|"ky Pdɀ!PRXQGː WqȂ'X HDXƲC`H ,&PLigQ*q,t80 XlU`?*pr|l u@ Px+$ eF]t*AGZRE:TQARKt0ݩN?"`32 ^( GDr 9$?"Z:llc1= BN8AA ,6 6H.D{:"@9,=a L 8E< t P 0-*9A qX~@@H@[;H hG4 8N@ 吚z`sL^rЃ h3" J`Ǣ8s >` +`X"nE, W8D!:R!'TqX`#ya -jE[XE&` 'F$<\!D~q 5Y92,r3 fq'|%Gc!+gxm+,?XŊ&q#RaI1a=f:ʵ0,Rg|c:ɹh-c>+lS'?υfUâo1aNpZD^أ 2|P6F@NjA9L1i$7zA @G8n! oأ]N)(+<,K: ?W]=q rcA9V 30Y@{Ch& ?cQ;#H8T$#>d Vae@h@ {D3`TB=yA`ȃ*tQ|[} >CׅjpoBt02__yhm 2" Ja7c`-1 _0B-^92_(44`ə.A._3B(ޟE 2BD :1A)D11 B!_6%C `}__(46C ..-_*_ F`0ğ-*"U`v i! ja3A "  "aF_Zr"Rr,(a/la Fb__04#tBڂ1!+A/aa$(b F b .b#r$va%_Fbf`B~`r( C'_)!/xa/ *0@Z"06L,5B7d_CE!8)-l 1 8;-D ] *8@)2@ xB(T l=@< V9ߩ% 89 ]A:H^C6 8Add̀ 2^YD,:DAA8Ȃca,ԂbB-)&d&fd Ff0tB!\&dB)*_g/|cV&B0D& e.&FbCf-CF&?¦./,$*ld#fMB}&l" sf f0_xC(bhu*iBl^(fCgah#(/y§~)Bh-DCmCvbiA~gi*ߗ)e m"*dZAA<;C68EE(8A CG~,6= A/d@$iC=:<5@/lCL7L*$ST?`7&Wz"L^H(xd-${],8@07(f &l2'FjlN*iujkzlgJv|x:fӒqe2.(t(iq}~j)Zv_m{-|jgj֬*ܬ6Nx&vΩ1kjZ(ަ>-‡z'"ղ)-)F׺(v^iٮ m'&'R2j"'2jZjlN)ih.n)v&n:bw首>`'Bk'*&vj~V&ւl**gm>>*nR~wNvBC'*nᩦ2«ȪaC696<\8:h)9`IA:,7`9|C/A7,5( 8++A=C4 A$-l+vlǀA9P°5%YLJe(&(lW~eXB,)ԗG tּ%0!;DW<̃!8 8B>C9UuM @B../vh*D.f(К2.߮.hzRg.m%g-g2yjm#_ro!'~ne2%B4"/'[3B6>^n!')/rƲsN3f6-):0K?;nHj&4v"40Mg$*p2 $58Aq98Xk:DX9,k77Ap1?`1x71  VR;@|،41WޙBrI@;Wua8v]:e9B+ZoѪ"5+',@,9 Qe4}3dN0hMCg2*Jzf.F&'94og(FqrB2ي3sIrr~rB./mu.qEhB2A%4Gbfh^jrsdqctwzyohrR"h/$s?w+t+&A'4|'Q7o{2}?D'Zo:wwv/Hs7`;bT2t 2D9D=(?{GV#P`@[¿ RʁRϠm 1"EEArIA9ͪre,2XlGʘeNvƴK.^1yROa|BdiKX&FAӌg`ibu2'5ºRW):ή 2.KcjT NB1&thѮ[PZ]SU^:ʒ6e64x-mJ#5f3o朌,0jfcSʔ1WvӖVl YlTѪ[*`tu7oncm ̵ml8U*_n0Ϡck^^@%֐Vkڈ3/`Z̷o8#lxN WrMXNfAP 9.)B/䚁 /d >˺A\ɘjye@P G l~= 鰑 䖔.J^Pa`$0g yLy( 1ʡ @Y&,AxdYf@BX#JrOĞ@"COzW?{I'wLlY my1(Mhq{VX%{ڙg, qQ{$qvP)HyV9nQǝwF&S utyeX1)RiJBQf JokJYtSg ⶪA#rnaYRy+.әdZ3bfKnKȥ+0[PePbm0z8fn-d'm3yµu3)DvGƄhM*ff٘BtB{ƩW.uI[slll;>>JG1fu36m* BPd-f\k4W9LdPZbMHBBTЃ؂ FW&p0K#Q!dHH x?Ŏw6VL`SDaHKl@ &6РpmIAX&@, !ɀ2#[I@D\BwbHHB!H qmW@aC8 1Vl)T#8. N8#8 L!X?2J@#/2_|0 h4"p/jf/AgF3ӘxLh*&7PLÄF,qaJ:d4  k2{ B8s39Nh݄$_BD/<\t2Y FX>)\)[MoS)F'2dS3"D2SL~V(A]:6袦фF9ГJԗC5 g8jWRfdFOMiO㜆1y)eFTiFQBC]HNv"+h2ʪTa?WX@4:j"gg#R4|j\!+U]ժ#ͫVnƂ%[Κ| v3`!ε ghc2ʬhVM^СC0j*ڑVjm,[VJh5Ynfwh7AТwEgWb|~C G>+?_xso O/oxom$0#POAPI/=0//{p-ar/@^"$A %n/ɴL4M  `ze;]%RK) 4uQUC;́ A"!~3a,b~ !U "@ AСd0aE`a$́ 2f @A: @VFT, [U[QȁPNER`a~Ws6`V !5A #N`!-3&daʀXD _$<`aPV~SEB@`vavY!Vb1b-c@\aH3 A4T@ @h^8eE/ZY- āF"^Ctm O`6nH@$`   *ʨR,z !ԑ,zAʬ $*,@d(VJu]u].n?\ˁ@AQ a A6A@mC,(`z Z N@ L`Js!-VD uurs?R""`}^@DCq$ ?AgRP@d!X`@ \|.!*q,W<*`#&a|X @ &Ev# A~l tʡ 2 a ʀt2XG`FRaR"RXZr(` $ VI KTA@' L?ᙡšrx4wHA  ʠ-`[h H~,EP$l@A z^@ m 6$EDX@ q{`D@ۻ[ۼ[园gϵ1 0 h  cBy1FiA5*H`Ġ C z & 0A"tR<StٙAA  Ā9o-`oVXAX3 !Y Ρ\!!V@^l3!!+HO@ff]F>+@\$fLؑ] ΐ0AР@@bc ?AX6 ``"8X"D`-Mc !ށ!f^!OܡVŌZ.#@a= RdvɡΡh1m;$8_%I vAlo@jP$u`ڡށz]`a aXbځ(Vօ3>FL=a PHLnDԅX+ a_37Ϟ!S>=dE!Fae ^@ _ iA:=ui!!!3!! Lځ́#Z9|r!^* O"zQaj߿'$ QbOY CLޡ-,[| 3̙4kڼ3Ν<{Y36&T8Ԃ a!AhHs 9Αn2Xަ2 g=YbhAsh:gJGm 6Xٻ<-*-cV(X. f+uj^Rnb$(4M=伵#8>aÆ*v<0;{C4D9eퟱ- xX+'Cr$N(j bY F8;]e@Δ`q#5 L"bO ?2@=!J=X0R"D+ !䀩E%"AWTBMtF4BAB.RSOE5UUSpRN Q%0p#FTЃ9$ X= 7tPCcE\ 8#3$Q+(= 1pf Th,B: Zlv[n1>gSr,gMPow]v;lg_8ه~p!(]K 2,u #D,7Ѭݷ-7S$Uc sPq@/9P Eg RXlP#kH!x=Cl pB= P@ =:ى&Sp eNЁ t@`CFQ2j` 0\M ZBWUHضz`nbqDT@2`u=BL0*^1[8[PH8α]K WqA1dU G"H,A<^B +H,!"Q"걊=^K v0kd#B~dbX6FR2=x@"bPCK&XA%m mC Ѥ-i.X"5H*XE7`M !"b jz`NP?j V 20[`c A|ĀE#0DD , Sa8dmj4Iiѕ@ `xuMq@4]@aw%cbm[( &4^K>^ӟd6B j ۸\OcIA bT<7PD)z'@6Nζ|B(K2 `~" qLjn}Y?B })`Z*`"Y'>OJ%/|P* B 9(Ome *@htZ>f,KI־"w%#`u~woNRkwb0 @SWB@ @ ``1`$B p fK Np#HpU'`r@ 9mdUV7{'!%/@l[r;-Q;2&( (hB`"p F @fe!`PB B=Yp Kp 7u x12~\e'7[ ~QrA\5 s:CH$@ #8=T 0>w68pOG&uTgup R <3 V05rxx'gvEnhCQBsW`_$p C`4@"wap!x"VxCh cm,,*7P` #X`0ܸ 8B `撎-aw X W0Aa0rF,_PyD@1/yG5}ـ р !]ѰQp6l!'PXr'SIU{<4&J'  6PR`+ p6y "Hvt@ ` ` >bEPH&! 019 :ٙV@ T @6 06E+P֐20)VY+0+ǀ QXσpR @/`Đ=/@ ޠ12 l0 V5 "+TOx+F ?H@ԐPQ@2Wµ s9p@$ИA'@$` :PWUۀ t)QPHRev{Ōne$$b@1a!FxRw(+Т0a?cp JA$(68 :< > BJDFw.&O$$.H  @8g!0*m +6+y A@ TwpPiE`ijHb pj lEh+ p `ߐ#E-N e_b 0 f{ b0' ` Q W@@ ق9陞 Iip 1V!)0 0 0p 0E}v  aXA P"P `]r"@ q)@ G@kx&'B < X  Us-2tв:-J p Y4\ 3P4 p PO'Qw p V& ץ3PTp`@ =?b0$`Cp `u<Ь x@衭ح(H p zD+_z w@Q< ;{=ѻ[Ǜ h0{GH۪glK  KR4Ѩ3uv `3A` pK | h0 G / iF ,r0 r˫ElGPB410e`~ @ Ѡ Ѹ@ ! -% \ &KP$Nbl܀ ` ؗ+P "Hq0 %g] j f9` r 4`7&2 0 {EQ @ k Kv㭑td?2 Dϼ#A.h~^ E.N *.M,"]]3 ,,Q`DحµЦy-.,&IFȏ__-vf.HBl~n~?&*v-ݎ9Q0 *ɏ"0XnO`yQމ.fׄ=Rȭ뭢R95Aɮn4v-1@сo>C#@* 4[" N`מT}96`4.1Kv11&`4N _0 PP]3q\E/'CP~q,v70sB `G@pC_q(P#N_a/Si*6gi El7"@c_ *5%+С.<7p_r` z(=/ji R/y4 `#n^O`oF3/OoǏɯ/Oo׏ٯ/Oo/Oo$XA .dC%NXE5nG!E$YI)UdK1eΤYM9uOA%ZQI.eSQNZUYnWaŎ%[Yiծe[qΥ[]y_&\aĉ/fcȑ%O\m ;;X`DA{8:ΤUe5 -8 Be DHB 3Tn8AE0C"Bb',hkf`Pap &xϸg>[H ! ^H$/{Mʻ@ Р.4oSOB6J)VOD`@ NMh d%B `g ! pXd #@+,DH8, T(I DQ"1BN: aHDh<@!ER@%CB{1 VKς8D:pOXa! ذUIp!"ѡׁ:RSQ5ɂ"K/c@CK`UEؤY{[mb00Mh)XQd'@KpJhNR>لO,Ol 2L صah dY4,ҤιsaeS@Z8iGsQzna؁p$^} GDP{":0it-vܙ22xSNׂzag tR{$2*ngk@s)gp hG!*zQpJX7nغ=̑覝s@^lȽ;ⱾtdC 8a}sլ54@'<44Kp+!H< LE 0@{H?`A,z !"(@ 0=Am_B,[ȃe<)BzM [[8ore` CD=LAK(T \;C*V@$Oo:EڄaD%A9Ǝ rp;'E89n1I@ o|ٱ ؀xAlh|:Bp 3@=$c>pwtLdUوF N Ht"0 Ӛ\K ƥD0aQ\PSB$Oy,7Ġt,#A PtS1tW1xB8Щa AwQCW>0 ~78k2`)N ]J3[S_.D_!"@=Xq#)qtX2H(¦ZHBh&}c4{D!XIj$@$} n5pR+R\mpITK9mȨ "2XyS$S"(LΌS-ѬH?@-MQdOPe:k0C0pȆPVI@e.q%1`{(ܨ}*Mor9@'0?{y1 X$sIPi-nG8c9lPGGM {`18r]2N6vx8LNt98 ?(qHHD$#U#Nᘀ.0R{hb ЈJ}(y8+ WLph@^xr^ab@\qqxnZBx\VH zthN$Tp ΈCPQ_o ,0Es H{q(N.#s(j`K(8<Xk^^$T1$Cx٪0[`2x=PY+Ψ#Xo(`_>DY1Qۆw^r1kr}07820|p X8VN~\8w11?@ُ}(KEԞCP 8=pEqDl,Os,-%TpctQLHx+prh(S@y|4Hhxo pv@9o pw&E۠*h|(-VĤp)kjnQ6;BٖWmܶNpp0PZG*MHOfV9N ;͆)P2`C؂#X[\e[F-TPBq?5hc[(;l;?C(= =-ڭEi x?s8naH, '^ @/nxVUjjkHJx@fh@MP{m@ D9 Xʆ@ ~~RhƓ-Xj{ M(r@#!]Md0!!D tv{w WYxw# ꈷ"kf}P+gnOˢ%'>rpBupGvH'PUp@C@kX{o7,H]P9$r vyKAJC{#]q;e}o bĄ|}Nnq+1GR[+t Do/q'ws?z7z~hh_ *8)iBbL7>[]7CP0998Y u RQ)A Đ!`NusuQVr@q ,9rdXdPH 2gP _k}1RC'G)(wXP-AiR֕lW c1$@ 8DqEHoH,Q'&MTE VO1 B~c%M6Ox# pBQH)ŔSqZUYmV8$P㝷{%,4;̠)! |sV +0 CJ  *eؼLC#dEУTn #w2ѩ^UOhT#Hي|d,qp \:`: T.9>aKR PحXp,1 D ClqdLQ!tL" )T" aH <HE@ GY|#Ox*SqN$, Tc3^! xoDqKDAd*$q C`}Dj$,h~t%ʁu+&!$vNˀlƄfz0UPD&;c(9"e锫U'Ȁ ~,Ït"5H!s>$P*$R5΁e~mt9ܒ`vT+ )%W8aYt fO2bշdq`<( ?`/`:CB]HPdvMD@HLAT@GkL$EntD܅Ƞ[Xx$KBJ<[t܃hH^L̢L I}D)t0QGV$dxC68dD[PRG@<~L:Y^Y-UZAP$YR$ԔHEd+ huJ]0Neia(XdH%GeeKDeM+Cㇺ](,ȃֹhj$h+Ձ=|)Th~qo1Ci:Bi|JiYl=BNiDY9jX3vi)Y7pri2,ȚکW雮C*Q4Xi ZB<7wg׾;̓O6x瑥|NheѢ'nr':\vJ@xG lpt[ { &=kefi76UXF ge/ ~tʾ9+x["8Vzꐊ^Khݮ؊g^G=T ^S|FR % *=W'NlB47 4/{,egiG4m I-y?{.nn1Oӓ#NyO> TumH\m̭TkwT;va|]c{2]G0?Ϭ+[30Na[,gN3X .uh:7ɕ.h21\j*ANiΎU;-3x@SG!UTЋ6yEn!ۈ*3Aӕ!4iֳg?*ꭒd5-'EC7Z:JPtM)hͮSh|[:slAe !ŕ\ܼKxCoy2ʾrorVX`7Li}KR"E ux,/1U!\^UΰP!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,M ?H*\ȰÇ#JHbŀ!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,M aEDP yCV 叟ŋ ( _ u#dȑ='J- zyJ(KfNyisϛB5ZgӚ)> :2 !,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,M ?H*\ȰÇ#JHbŀ!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,M aEDP yCV 叟ŋ ( _ u#dȑ='J- zyJ(KfNyisϛB5ZgӚ)> :2 !,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,M ?H*\ȰÇ#JHbŀ!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,M aEDP yCV 叟ŋ ( _ u#dȑ='J- zyJ(KfNyisϛB5ZgӚ)> :2 !,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,W!,M ?H*\ȰÇ#JHbŀ!,W!,W;transparency-dev-tessera-3cb22ee/cmd/fsck/tui/000077500000000000000000000000001511600621500214215ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/cmd/fsck/tui/fsck_panel.go000066400000000000000000000101651511600621500240600ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tui import ( "fmt" "github.com/transparency-dev/tessera/fsck" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) // NewFsckPanel creates a new TUI panel showing information about an ongoing fsck operation. func NewFsckPanel() *FsckPanel { r := &FsckPanel{ statsView: NewStatsView(), } return r } // FsckPanel represents the UI model for the FSCK TUI. type FsckPanel struct { // entriesBar is the status/progress bar representing progress through the entry bundles. entriesBar *LayerProgressModel // titlesBars is the list status/progress bars representing progress through the various levels of tiles in the log. // The zeroth entry corresponds to the tiles on level zero. tilesBars []*LayerProgressModel statsView *StatsViewModel // width is the width of the app window width int } // Init is called by Bubbleteam early on to set up the app. func (m *FsckPanel) Init() tea.Cmd { return nil } // Update is called by Bubbletea to handle events. func (m *FsckPanel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width var cmd tea.Cmd cmds := []tea.Cmd{} if m.entriesBar != nil { _, cmd = m.entriesBar.Update(msg) cmds = append(cmds, cmd) } for _, t := range m.tilesBars { _, cmd = t.Update(msg) cmds = append(cmds, cmd) } return m, tea.Batch(cmds...) case FsckPanelUpdateMsg: // Ignore empty updates if len(msg.Status.TileRanges) == 0 { return m, nil } // Create the range progress bars now that we know how details about the tree. if len(m.tilesBars) != len(msg.Status.TileRanges) { m.entriesBar = NewLayerProgressBar("Entry bundles", m.width, 0) bs := make([]*LayerProgressModel, 0, len(msg.Status.TileRanges)) for i := range msg.Status.TileRanges { bs = append(bs, NewLayerProgressBar(fmt.Sprintf("Tiles level %02d", i), m.width, i)) } m.tilesBars = bs } // Update all the range progress bars with the latest state. _, cmd := m.entriesBar.Update(LayerUpdateMsg{Ranges: msg.Status.EntryRanges}) cmds := []tea.Cmd{cmd} for i := range m.tilesBars { _, cmd = m.tilesBars[i].Update(LayerUpdateMsg{Ranges: msg.Status.TileRanges[i]}) cmds = append(cmds, cmd) } _, cmd = m.statsView.Update(msg) cmds = append(cmds, cmd) return m, tea.Batch(cmds...) default: return m, nil } } // View is called by Bubbletea to render the UI components. func (m *FsckPanel) View() string { // Build the progress bars, we'll use this below. bars := []string{} for i := len(m.tilesBars) - 1; i >= 0; i-- { t := m.tilesBars[i] bars = append(bars, t.View()) } if m.entriesBar != nil { bars = append(bars, m.entriesBar.View()) } bars = append(bars, LayerProgressKey()) barsView := lipgloss.JoinVertical(lipgloss.Bottom, bars...) progress := lipgloss.NewStyle(). Width(m.width). Height(lipgloss.Height(barsView)+1). Border(lipgloss.NormalBorder(), true, false, false, false). Render(barsView) stats := lipgloss.NewStyle().Align(lipgloss.Left).Render(m.statsView.View()) return lipgloss.NewStyle().Align(lipgloss.Center).Render( lipgloss.JoinVertical(lipgloss.Top, progress, stats), ) } // FsckPanelUpdateMsg is used to tell the FsckPanel about updated status from the fsck operation. type FsckPanelUpdateMsg struct { Status fsck.Status } // FsckPanelUpdateCmd returns a Cmd which Bubbletea can execute in order to retrieve and updateMsg. func FsckPanelUpdateCmd(status fsck.Status) tea.Cmd { return func() tea.Msg { return FsckPanelUpdateMsg{Status: status} } } transparency-dev-tessera-3cb22ee/cmd/fsck/tui/layerbar.go000066400000000000000000000173141511600621500235570ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package tui provides a Bubbletea-based TUI for the fsck command. package tui import ( "fmt" "math" "strings" "github.com/muesli/termenv" "github.com/transparency-dev/tessera/fsck" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" ) var ( // percentageStyle defines the layout for the progress bar percentage. percentageStyle = lipgloss.NewStyle().Width(8).MaxWidth(8) // labelStyle defines the layout for the progress bar label. labelStyle = lipgloss.NewStyle().Width(16).MaxWidth(16) // stateStyle defines how the different fsck.State types show up in the progress bar. stateStyles = []stateStyle{ { state: fsck.Unchecked, style: lipgloss.NewStyle().Foreground(lipgloss.Color("#313244")), bar: "◻", priority: 1, }, { state: fsck.OK, style: lipgloss.NewStyle().Foreground(lipgloss.Color("#86a381")), bar: "◼", priority: 2, }, { state: fsck.Calculating, style: lipgloss.NewStyle().Foreground(lipgloss.Color("#89dceb")), bar: "C", priority: 3, }, { state: fsck.Fetched, style: lipgloss.NewStyle().Foreground(lipgloss.Color("#94e2d5")), bar: "◼", priority: 4, }, { state: fsck.Fetching, style: lipgloss.NewStyle().Foreground(lipgloss.Color("#eeeeee")), bar: "◼", priority: 5, }, { state: fsck.FetchError, style: lipgloss.NewStyle().Foreground(lipgloss.Color("#fab387")), bar: "E", priority: 9, }, { state: fsck.Invalid, style: lipgloss.NewStyle().Foreground(lipgloss.Color("#f38b38")), bar: "!", priority: 10, }, } // stateStylesByState maps styles to their corresponding fsck.State. stateStylesByState = func() map[fsck.State]stateStyle { r := make(map[fsck.State]stateStyle) for _, v := range stateStyles { r[v.state] = v } return r }() ) // LayerProgressKey returns a rendered "key" which can be used in the UI to visually explain // to the user which fsck state is associated with the various styles. func LayerProgressKey() string { r := make([]string, 0, len(stateStyles)) for _, s := range stateStyles { r = append(r, fmt.Sprintf("%s %s", s.Render(), s.state.String())) } return strings.Join(r, " | ") } type stateStyle struct { state fsck.State // style is the style to use for this state in the progress bar. style lipgloss.Style // bar is the character(s) used for representing this state in the progress bar. bar string // priority maps each possible state to a relative importance of that priority. // This is used when displaying the overall state of a range of resources where multiple // states are present. // The highest priority state represented in the range "wins". priority int } // Render returns a string representing a single segment of a LayerProgressBar, styled // appropriately for the state. func (s stateStyle) Render() string { return s.style.Inline(true).Render(s.bar) } // LayerUpdateMsg is a message which carries information for a specific layer in the tree (e.g. tiles for a specific level). type LayerUpdateMsg struct { Ranges []fsck.Range } // NewLayerProgressBar returns a progress bar which can render information about the states of fsck ranges. // // Label is the name of the range, and will be shown in the UI. // width is the total width available to this model. func NewLayerProgressBar(label string, width int, level int) *LayerProgressModel { m := &LayerProgressModel{ label: label, width: width, level: level, colorProfile: termenv.ColorProfile(), } return m } // LayerProgressModel is the UI model for a progress bar which represents fsck status through a paricular level of the tree. type LayerProgressModel struct { // label is a human readable name associated with this progress bar, and is shown in the UI. label string // PercentageStyle controls how the progress percentage is rendered. PercentageStyle lipgloss.Style // LabelStyle controls how the label string is rendered. LabelStyle lipgloss.Style // Color profile for the progress bar. colorProfile termenv.Profile // width available to this component. width int // level is the tile-space level this progress bar represents. // used to scale the bar portion of the component. level int // Current state this progress bar is representing. state []fsck.Range } func (m *LayerProgressModel) Init() tea.Cmd { return nil } // Update is used to animate the progress bar during transitions. Use // SetPercent to create the command you'll need to trigger the animation. // // If you're rendering with ViewAs you won't need this. func (m *LayerProgressModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width return m, nil case LayerUpdateMsg: m.state = msg.Ranges return m, nil default: return m, nil } } func (m *LayerProgressModel) View() string { if m.state == nil { return "" } return m.ViewAs(m.state) } func (m *LayerProgressModel) ViewAs(rs []fsck.Range) string { // Don't bother trying to show empty information. if len(rs) == 0 { return "" } extent := rs[len(rs)-1].First + rs[len(rs)-1].N byState := make(map[fsck.State]uint64) for _, r := range rs { byState[r.State] += r.N } // Render the pieces percentView := m.percentageView(float64(byState[fsck.OK]) / float64(extent)) labelView := labelStyle.Inline(true).Render(m.label) barWidth := m.width - lipgloss.Width(labelView) - 1 - lipgloss.Width(percentView) - 1 // Squash higher levels so the bars look a bit more tree-like. levelSize := max(1, barWidth>>m.level) barView := lipgloss.NewStyle().Width(barWidth).MaxWidth(barWidth).Align(lipgloss.Center).Inline(true).Render(renderBar(rs, levelSize)) return lipgloss.JoinHorizontal( lipgloss.Left, labelView, barView, percentView) } // stateForRange figures out the right state style to use for the progress bar section covering range [f, f+n), // using the provided fsck status ranges. func stateForRange(rs []fsck.Range, f, n uint64) stateStyle { ret := stateStylesByState[fsck.Unchecked] found := false for _, r := range rs { if r.First <= f && f < r.First+r.N { found = true } if found { s := stateStylesByState[r.State] if s.priority > ret.priority { ret = s } } if r.First+r.N > f+n { break } } return ret } // renderBar builds a complete width-sized rendering for a progress bar representing the provided fsck ranges. func renderBar(r []fsck.Range, width int) string { if len(r) == 0 { return strings.Repeat(" ", int(width)) } sb := strings.Builder{} rangeExtent := r[len(r)-1].First + r[len(r)-1].N rFirst := float64(0) for i := range width { chunk := (float64(rangeExtent) - rFirst - 1) / float64(width-i) _, _ = sb.WriteString(stateForRange(r, uint64(math.Round(rFirst)), uint64(math.Round(chunk))).Render()) rFirst += chunk } return sb.String() } // percentageView returns a rendering of the provided percentage value. func (m *LayerProgressModel) percentageView(percent float64) string { percent = math.Max(0, math.Min(1, percent)) percentage := fmt.Sprintf(" %03.2f%%", percent*100) return percentageStyle.Inline(true).Render(percentage) } transparency-dev-tessera-3cb22ee/cmd/fsck/tui/layerbar_test.go000066400000000000000000000057731511600621500246240ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package tui provides a Bubbletea-based TUI for the fsck command. package tui import ( "testing" "github.com/transparency-dev/tessera/fsck" ) func TestStateForRange(t *testing.T) { for _, test := range []struct { name string rs []fsck.Range f, n uint64 want stateStyle }{ { name: "single range, all contained from LHS", rs: []fsck.Range{{First: 0, N: 10, State: fsck.Calculating}}, f: 0, n: 3, want: stateStylesByState[fsck.Calculating], }, { name: "single range, all contained internal", rs: []fsck.Range{{First: 0, N: 10, State: fsck.Calculating}}, f: 1, n: 3, want: stateStylesByState[fsck.Calculating], }, { name: "single range, all contained RHS", rs: []fsck.Range{{First: 0, N: 10, State: fsck.Calculating}}, f: 7, n: 3, want: stateStylesByState[fsck.Calculating], }, { name: "single range, all contained full overlap", rs: []fsck.Range{{First: 0, N: 10, State: fsck.Calculating}}, f: 0, n: 10, want: stateStylesByState[fsck.Calculating], }, { name: "multiple ranges, contained in first", rs: []fsck.Range{ {First: 0, N: 10, State: fsck.Calculating}, {First: 10, N: 1, State: fsck.Invalid}, {First: 11, N: 10, State: fsck.OK}, }, f: 1, n: 3, want: stateStylesByState[fsck.Calculating], }, { name: "multiple ranges, contained in last", rs: []fsck.Range{ {First: 0, N: 10, State: fsck.Calculating}, {First: 10, N: 1, State: fsck.Invalid}, {First: 11, N: 10, State: fsck.OK}, }, f: 11, n: 3, want: stateStylesByState[fsck.OK], }, { name: "multiple ranges, spans two", rs: []fsck.Range{ {First: 0, N: 10, State: fsck.Calculating}, {First: 10, N: 10, State: fsck.Invalid}, {First: 20, N: 10, State: fsck.OK}, }, f: 8, n: 10, want: stateStylesByState[fsck.Invalid], }, { name: "multiple ranges, spans three", rs: []fsck.Range{ {First: 0, N: 10, State: fsck.Calculating}, {First: 10, N: 10, State: fsck.Invalid}, {First: 20, N: 10, State: fsck.OK}, {First: 30, N: 10, State: fsck.Fetching}, }, f: 18, n: 20, want: stateStylesByState[fsck.Invalid], }, } { t.Run(test.name, func(t *testing.T) { got := stateForRange(test.rs, test.f, test.n) if got.state != test.want.state { t.Fatalf("Got %v, want %v", got.state, test.want.state) } }) } } transparency-dev-tessera-3cb22ee/cmd/fsck/tui/stats_view.go000066400000000000000000000074251511600621500241500ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tui import ( "fmt" "time" "github.com/dustin/go-humanize" "github.com/transparency-dev/tessera/fsck" tea "github.com/charmbracelet/bubbletea" "github.com/charmbracelet/lipgloss" movingaverage "github.com/RobinUS2/golang-moving-average" ) // NewStatsView returns a widet which displays fsck statistics. func NewStatsView() *StatsViewModel { m := &StatsViewModel{ bytesAvg: movingaverage.New(10), resourcesAvg: movingaverage.New(10), errorsAvg: movingaverage.New(10), } return m } // StatsViewModel is the UI model for a stats widget. type StatsViewModel struct { // width available to this component. width int totalBytes uint64 totalResources uint64 totalErrors uint64 bytesAvg *movingaverage.MovingAverage resourcesAvg *movingaverage.MovingAverage errorsAvg *movingaverage.MovingAverage startTime time.Time lastUpdate time.Time eta time.Time } func (m *StatsViewModel) Init() tea.Cmd { m.startTime = time.Now() return nil } // Update is used to update the widget. func (m *StatsViewModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { switch msg := msg.(type) { case tea.WindowSizeMsg: m.width = msg.Width return m, nil case FsckPanelUpdateMsg: now := time.Now() d := now.Sub(m.lastUpdate) m.lastUpdate = now b := msg.Status.BytesFetched m.totalBytes += b bytesPerSecond := float64(b) / d.Seconds() m.bytesAvg.Add(bytesPerSecond) r := msg.Status.ResourcesFetched m.totalResources += r resourcesPerSecond := float64(r) / d.Seconds() m.resourcesAvg.Add(resourcesPerSecond) e := msg.Status.ErrorsEncountered m.totalErrors += e errorsPerSecond := float64(e) / d.Seconds() m.errorsAvg.Add(errorsPerSecond) totalResources := totalSize(msg.Status.EntryRanges) for _, rs := range msg.Status.TileRanges { totalResources += totalSize(rs) } if totalResources > 0 { elapsedTime := float64(now.Sub(m.startTime).Seconds()) complete := float64(m.totalResources) / float64(totalResources) m.eta = now.Add(time.Duration(elapsedTime / complete)) } return m, nil default: return m, nil } } // totalSize returns the total number of elements covered by the provided ranges. func totalSize(rs []fsck.Range) uint64 { tot := uint64(0) for _, r := range rs { tot += r.N } return tot } func (m *StatsViewModel) View() string { bytesFetched := lipgloss.NewStyle().Width(27).Render(formatTotalAndAverage("Bytes", m.totalBytes, m.bytesAvg.Avg())) resourcesFetched := lipgloss.NewStyle().Width(30).Render(formatTotalAndAverage("Resources", m.totalResources, m.resourcesAvg.Avg())) errorsEncountered := lipgloss.NewStyle().Width(23).Render(formatTotalAndAverage("Errors", m.totalErrors, m.errorsAvg.Avg())) eta := lipgloss.NewStyle().Width(55).Render(fmt.Sprintf("ETA: %s", humanize.Time(m.eta))) return lipgloss.NewStyle(). Width(m.width).Render( lipgloss.JoinHorizontal( lipgloss.Right, bytesFetched, resourcesFetched, errorsEncountered, eta), ) } func formatTotalAndAverage(label string, total uint64, avg float64) string { si, p := humanize.ComputeSI(float64(total)) avgSI, avgP := humanize.ComputeSI(avg) return fmt.Sprintf("%s: %0.1f %s (%03.1f%s/s)", label, si, p, avgSI, avgP) } transparency-dev-tessera-3cb22ee/ct_only.go000066400000000000000000000175741511600621500211430ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "context" "crypto/sha256" "fmt" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/ctonly" "golang.org/x/crypto/cryptobyte" ) // NewCertificateTransparencyAppender returns a function which knows how to add a CT-specific entry type to the log. // // This entry point MUST ONLY be used for CT logs participating in the CT ecosystem. // It should not be used as the basis for any other/new transparency application as this protocol: // a) embodies some techniques which are not considered to be best practice (it does this to retain backawards-compatibility with RFC6962) // b) is not compatible with the https://c2sp.org/tlog-tiles API which we _very strongly_ encourage you to use instead. // // Users of this MUST NOT call `Add` on the underlying Appender directly. // // Returns a future, which resolves to the assigned index in the log, or an error. func NewCertificateTransparencyAppender(a *Appender) func(context.Context, *ctonly.Entry) IndexFuture { return func(ctx context.Context, e *ctonly.Entry) IndexFuture { return a.Add(ctx, convertCTEntry(e)) } } // convertCTEntry returns an Entry struct which will do the right thing for CT Static API logs. // // This MUST NOT be used for any other purpose. func convertCTEntry(e *ctonly.Entry) *Entry { r := &Entry{} r.internal.Identity = e.Identity() r.marshalForBundle = func(idx uint64) []byte { r.internal.LeafHash = e.MerkleLeafHash(idx) r.internal.Data = e.LeafData(idx) return r.internal.Data } return r } // WithCTLayout instructs the underlying storage to use a Static CT API compatible scheme for layout. func (o *AppendOptions) WithCTLayout() *AppendOptions { o.entriesPath = ctEntriesPath o.bundleIDHasher = ctBundleIDHasher return o } // WithCTLayout instructs the underlying storage to use a Static CT API compatible scheme for layout. func (o *MigrationOptions) WithCTLayout() *MigrationOptions { o.entriesPath = ctEntriesPath o.bundleIDHasher = ctBundleIDHasher o.bundleLeafHasher = ctMerkleLeafHasher return o } func ctEntriesPath(n uint64, p uint8) string { return fmt.Sprintf("tile/data/%s", layout.NWithSuffix(0, n, p)) } // ctBundleIDHasher knows how to calculate antispam identity hashes for entries in a Static-CT formatted entry bundle. func ctBundleIDHasher(bundle []byte) ([][]byte, error) { r := make([][]byte, 0, layout.EntryBundleWidth) b := cryptobyte.String(bundle) for i := 0; i < layout.EntryBundleWidth && !b.Empty(); i++ { // Timestamp if !b.Skip(8) { return nil, fmt.Errorf("failed to read timestamp of entry index %d of bundle", i) } var entryType uint16 if !b.ReadUint16(&entryType) { return nil, fmt.Errorf("failed to read entry type of entry index %d of bundle", i) } switch entryType { case 0: // X509 entry cert := cryptobyte.String{} if !b.ReadUint24LengthPrefixed(&cert) { return nil, fmt.Errorf("failed to read certificate at entry index %d of bundle", i) } // For x509 entries we hash (just) the x509 certificate for identity. r = append(r, identityHash(cert)) // Must continue below to consume all the remaining bytes in the entry. case 1: // Precert entry // IssuerKeyHash if !b.Skip(sha256.Size) { return nil, fmt.Errorf("failed to read issuer key hash at entry index %d of bundle", i) } tbs := cryptobyte.String{} if !b.ReadUint24LengthPrefixed(&tbs) { return nil, fmt.Errorf("failed to read precert tbs at entry index %d of bundle", i) } default: return nil, fmt.Errorf("unknown entry type at entry index %d of bundle", i) } ignore := cryptobyte.String{} if !b.ReadUint16LengthPrefixed(&ignore) { return nil, fmt.Errorf("failed to read SCT extensions at entry index %d of bundle", i) } if entryType == 1 { precert := cryptobyte.String{} if !b.ReadUint24LengthPrefixed(&precert) { return nil, fmt.Errorf("failed to read precert at entry index %d of bundle", i) } // For Precert entries we hash (just) the full precertificate for identity. r = append(r, identityHash(precert)) } if !b.ReadUint16LengthPrefixed(&ignore) { return nil, fmt.Errorf("failed to read chain fingerprints at entry index %d of bundle", i) } } if !b.Empty() { return nil, fmt.Errorf("unexpected %d bytes of trailing data in entry bundle", len(b)) } return r, nil } // copyBytes copies N bytes between from and to. func copyBytes(from *cryptobyte.String, to *cryptobyte.Builder, N int) bool { b := make([]byte, N) if !from.ReadBytes(&b, N) { return false } to.AddBytes(b) return true } // copyUint16LengthPrefixed copies a uint16 length and value between from and to. func copyUint16LengthPrefixed(from *cryptobyte.String, to *cryptobyte.Builder) bool { b := cryptobyte.String{} if !from.ReadUint16LengthPrefixed(&b) { return false } to.AddUint16LengthPrefixed(func(c *cryptobyte.Builder) { c.AddBytes(b) }) return true } // copyUint24LengthPrefixed copies a uint24 length and value between from and to. func copyUint24LengthPrefixed(from *cryptobyte.String, to *cryptobyte.Builder) bool { b := cryptobyte.String{} if !from.ReadUint24LengthPrefixed(&b) { return false } to.AddUint24LengthPrefixed(func(c *cryptobyte.Builder) { c.AddBytes(b) }) return true } // ctMerkleLeafHasher knows how to calculate RFC6962 Merkle leaf hashes for entries in a Static-CT formatted entry bundle. func ctMerkleLeafHasher(bundle []byte) ([][]byte, error) { r := make([][]byte, 0, layout.EntryBundleWidth) b := cryptobyte.String(bundle) for i := 0; i < layout.EntryBundleWidth && !b.Empty(); i++ { preimage := &cryptobyte.Builder{} preimage.AddUint8(0 /* version = v1 */) preimage.AddUint8(0 /* leaf_type = timestamped_entry */) // Timestamp if !copyBytes(&b, preimage, 8) { return nil, fmt.Errorf("failed to copy timestamp of entry index %d of bundle", i) } var entryType uint16 if !b.ReadUint16(&entryType) { return nil, fmt.Errorf("failed to read entry type of entry index %d of bundle", i) } preimage.AddUint16(entryType) switch entryType { case 0: // X509 entry if !copyUint24LengthPrefixed(&b, preimage) { return nil, fmt.Errorf("failed to copy certificate at entry index %d of bundle", i) } case 1: // Precert entry // IssuerKeyHash if !copyBytes(&b, preimage, sha256.Size) { return nil, fmt.Errorf("failed to copy issuer key hash at entry index %d of bundle", i) } if !copyUint24LengthPrefixed(&b, preimage) { return nil, fmt.Errorf("failed to copy precert tbs at entry index %d of bundle", i) } default: return nil, fmt.Errorf("unknown entry type 0x%x at entry index %d of bundle", entryType, i) } if !copyUint16LengthPrefixed(&b, preimage) { return nil, fmt.Errorf("failed to copy SCT extensions at entry index %d of bundle", i) } ignore := cryptobyte.String{} if entryType == 1 { if !b.ReadUint24LengthPrefixed(&ignore) { return nil, fmt.Errorf("failed to read precert at entry index %d of bundle", i) } } if !b.ReadUint16LengthPrefixed(&ignore) { return nil, fmt.Errorf("failed to read chain fingerprints at entry index %d of bundle", i) } h := rfc6962.DefaultHasher.HashLeaf(preimage.BytesOrPanic()) r = append(r, h) } if !b.Empty() { return nil, fmt.Errorf("unexpected %d bytes of trailing data in entry bundle", len(b)) } return r, nil } transparency-dev-tessera-3cb22ee/ct_only_test.go000066400000000000000000000123361511600621500221710ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "bytes" "crypto/sha256" "fmt" "testing" "github.com/transparency-dev/tessera/ctonly" ) func TestCTEntriesPath(t *testing.T) { for _, test := range []struct { N uint64 p uint8 wantPath string }{ { N: 0, wantPath: "tile/data/000", }, { N: 0, p: 8, wantPath: "tile/data/000.p/8", }, { N: 255, wantPath: "tile/data/255", }, { N: 255, p: 253, wantPath: "tile/data/255.p/253", }, { N: 256, wantPath: "tile/data/256", }, { N: 123456789000, wantPath: "tile/data/x123/x456/x789/000", }, } { desc := fmt.Sprintf("N %d", test.N) t.Run(desc, func(t *testing.T) { gotPath := ctEntriesPath(test.N, test.p) if gotPath != test.wantPath { t.Errorf("got file %q want %q", gotPath, test.wantPath) } }) } } var ( testCert = []byte("I am a Certificate") testPrecert = []byte("I am a Precertificate") testPrecertTBS = []byte("I am a Precertificate TBS") testIssuerKeyHash = sha256.Sum256([]byte("I'm an IssuerKey")) testFingerprintsChain = [][32]byte{ sha256.Sum256([]byte("one")), sha256.Sum256([]byte("two")), } ) func TestCTIdentityHasher(t *testing.T) { for _, test := range []struct { name string entries []ctonly.Entry }{ { name: "Single Certificate", entries: []ctonly.Entry{ { Timestamp: 1234, IsPrecert: false, Certificate: testCert, FingerprintsChain: testFingerprintsChain, }, }, }, { name: "Single Preertificate", entries: []ctonly.Entry{ { Timestamp: 1234, IsPrecert: true, Certificate: testPrecertTBS, Precertificate: testPrecert, IssuerKeyHash: testIssuerKeyHash[:], FingerprintsChain: testFingerprintsChain, }, }, }, { name: "Mixed bag", entries: []ctonly.Entry{ { Timestamp: 1234, IsPrecert: true, Certificate: testPrecertTBS, Precertificate: testPrecert, IssuerKeyHash: testIssuerKeyHash[:], FingerprintsChain: testFingerprintsChain, }, { Timestamp: 1234, IsPrecert: false, Certificate: testCert, FingerprintsChain: testFingerprintsChain, }, }, }, } { t.Run(test.name, func(t *testing.T) { bundle := []byte{} wantIDs := [][]byte{} for _, e := range test.entries { bundle = append(bundle, e.LeafData(123)...) wantIDs = append(wantIDs, e.Identity()) } gotIDs, gotErr := ctBundleIDHasher(bundle) if gotErr != nil { t.Fatalf("ctBundleIDHasher: %v", gotErr) } if lg, lw := len(gotIDs), len(wantIDs); lg != lw { t.Fatalf("got %d hashes, want %d", lg, lw) } for i := range gotIDs { if !bytes.Equal(gotIDs[i], wantIDs[i]) { t.Fatalf("%d: got ID hash %x, want %x", i, gotIDs[i], wantIDs[i]) } } }) } } func TestCTMerkleLeafHasher(t *testing.T) { for _, test := range []struct { name string entries []ctonly.Entry }{ { name: "Single Certificate", entries: []ctonly.Entry{ { Timestamp: 1234, IsPrecert: false, Certificate: testCert, FingerprintsChain: testFingerprintsChain, }, }, }, { name: "Single Preertificate", entries: []ctonly.Entry{ { Timestamp: 1234, IsPrecert: true, Certificate: testPrecertTBS, Precertificate: testPrecert, IssuerKeyHash: testIssuerKeyHash[:], FingerprintsChain: testFingerprintsChain, }, }, }, { name: "Mixed bag", entries: []ctonly.Entry{ { Timestamp: 1234, IsPrecert: true, Certificate: testPrecertTBS, Precertificate: testPrecert, IssuerKeyHash: testIssuerKeyHash[:], FingerprintsChain: testFingerprintsChain, }, { Timestamp: 1234, IsPrecert: false, Certificate: testCert, FingerprintsChain: testFingerprintsChain, }, }, }, } { t.Run(test.name, func(t *testing.T) { bundle := []byte{} wantIDs := [][]byte{} for _, e := range test.entries { bundle = append(bundle, e.LeafData(123)...) wantIDs = append(wantIDs, e.MerkleLeafHash(123)) } gotIDs, gotErr := ctMerkleLeafHasher(bundle) if gotErr != nil { t.Fatalf("ctMerkleLeafHasher: %v", gotErr) } if lg, lw := len(gotIDs), len(wantIDs); lg != lw { t.Fatalf("got %d hashes, want %d", lg, lw) } for i := range gotIDs { if !bytes.Equal(gotIDs[i], wantIDs[i]) { t.Fatalf("%d: got ID hash %x, want %x", i, gotIDs[i], wantIDs[i]) } } }) } } transparency-dev-tessera-3cb22ee/ctonly/000077500000000000000000000000001511600621500204375ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/ctonly/ct.go000066400000000000000000000137661511600621500214110ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // // // Original source: https://github.com/FiloSottile/sunlight/blob/main/tile.go // // # Copyright 2023 The Sunlight Authors // // Permission to use, copy, modify, and/or distribute this software for any // purpose with or without fee is hereby granted, provided that the above // copyright notice and this permission notice appear in all copies. // // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES // WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF // MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR // ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES // WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. // Package ctonly has support for CT Tiles API. // // This code should not be reused outside of CT. // Most of this code came from Filippo's Sunlight implementation of https://c2sp.org/ct-static-api. package ctonly import ( "crypto/sha256" "errors" "github.com/transparency-dev/merkle/rfc6962" "golang.org/x/crypto/cryptobyte" ) // Entry represents a CT log entry. type Entry struct { Timestamp uint64 IsPrecert bool // Certificate holds different things depending on whether the entry represents a Certificate or a Precertificate submission: // - IsPrecert == false: the bytes here are the x509 certificate submitted for logging. // - IsPrecert == true: the bytes here are the TBS certificate extracted from the submitted precert. Certificate []byte // Precertificate holds the precertificate to be logged, only used when IsPrecert is true. Precertificate []byte IssuerKeyHash []byte FingerprintsChain [][32]byte } // LeafData returns the data which should be added to an entry bundle for this entry. // // Note that this will include data which IS NOT directly committed to by the entry's // MerkleLeafHash. func (c Entry) LeafData(idx uint64) []byte { b := cryptobyte.NewBuilder([]byte{}) b.AddUint64(uint64(c.Timestamp)) if !c.IsPrecert { b.AddUint16(0 /* entry_type = x509_entry */) b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(c.Certificate) }) } else { b.AddUint16(1 /* entry_type = precert_entry */) b.AddBytes(c.IssuerKeyHash[:]) b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { // Note that this is really the TBS extracted from the submitted precertificate. b.AddBytes(c.Certificate) }) } addExtensions(b, idx) if c.IsPrecert { b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(c.Precertificate) }) } b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { for _, f := range c.FingerprintsChain { b.AddBytes(f[:]) } }) return b.BytesOrPanic() } // MerkleTreeLeaf returns a RFC 6962 MerkleTreeLeaf. // // Note that we embed an SCT extension which captures the index of the entry in the log according to // the mechanism specified in https://c2sp.org/ct-static-api. func (e *Entry) MerkleTreeLeaf(idx uint64) []byte { b := &cryptobyte.Builder{} b.AddUint8(0 /* version = v1 */) b.AddUint8(0 /* leaf_type = timestamped_entry */) b.AddUint64(uint64(e.Timestamp)) if !e.IsPrecert { b.AddUint16(0 /* entry_type = x509_entry */) b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { b.AddBytes(e.Certificate) }) } else { b.AddUint16(1 /* entry_type = precert_entry */) b.AddBytes(e.IssuerKeyHash[:]) b.AddUint24LengthPrefixed(func(b *cryptobyte.Builder) { // Note that this is really the TBS extracted from the submitted precertificate. b.AddBytes(e.Certificate) }) } addExtensions(b, idx) return b.BytesOrPanic() } // MerkleLeafHash returns the RFC6962 leaf hash for this entry. // // Note that we embed an SCT extension which captures the index of the entry in the log according to // the mechanism specified in https://c2sp.org/ct-static-api. func (c Entry) MerkleLeafHash(leafIndex uint64) []byte { return rfc6962.DefaultHasher.HashLeaf(c.MerkleTreeLeaf(leafIndex)) } func (c Entry) Identity() []byte { var r [sha256.Size]byte if c.IsPrecert { r = sha256.Sum256(c.Precertificate) } else { r = sha256.Sum256(c.Certificate) } return r[:] } func addExtensions(b *cryptobyte.Builder, leafIndex uint64) { b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { ext, err := extensions{LeafIndex: leafIndex}.Marshal() if err != nil { b.SetError(err) return } b.AddBytes(ext) }) } // extensions is the CTExtensions field of SignedCertificateTimestamp and // TimestampedEntry, according to c2sp.org/static-ct-api. type extensions struct { LeafIndex uint64 } func (c extensions) Marshal() ([]byte, error) { // enum { // leaf_index(0), (255) // } ExtensionType; // // struct { // ExtensionType extension_type; // opaque extension_data<0..2^16-1>; // } Extension; // // Extension CTExtensions<0..2^16-1>; // // uint8 uint40[5]; // uint40 LeafIndex; b := &cryptobyte.Builder{} b.AddUint8(0 /* extension_type = leaf_index */) b.AddUint16LengthPrefixed(func(b *cryptobyte.Builder) { if c.LeafIndex >= 1<<40 { b.SetError(errors.New("leaf_index out of range")) return } addUint40(b, uint64(c.LeafIndex)) }) return b.Bytes() } // addUint40 appends a big-endian, 40-bit value to the byte string. func addUint40(b *cryptobyte.Builder, v uint64) { b.AddBytes([]byte{byte(v >> 32), byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)}) } transparency-dev-tessera-3cb22ee/deployment/000077500000000000000000000000001511600621500213075ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/README.md000066400000000000000000000013471511600621500225730ustar00rootroot00000000000000# Deployment This directory contains configuration-as-code to deploy Tessera to supported infrastructure: - `modules`: terraform modules to configure infrastructure for running a Tessera log. + `gcp`: a Tessera GCP specific terraform module. + `aws`: a Tessera AWS specific terraform module. - `live`: example terragrunt configurations for deploying to different environments which use the modules. ## Prerequisites Deploying these examples requires installation of: - [`terraform`](https://developer.hashicorp.com/terraform/install) or [`opentofu`](https://opentofu.org/docs/intro/install/) - [`terragrunt`](https://terragrunt.gruntwork.io/docs/getting-started/install/) ## Deploying See individual `live` subdirectories. transparency-dev-tessera-3cb22ee/deployment/live/000077500000000000000000000000001511600621500222465ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/live/aws/000077500000000000000000000000001511600621500230405ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/live/aws/codelab/000077500000000000000000000000001511600621500244315ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/live/aws/codelab/README.md000066400000000000000000000204541511600621500257150ustar00rootroot00000000000000# AWS codelab deployment This codelab helps you bring a test Tessera infrastructure on AWS, and to use it by running a test personality server on an EC2 VM. The infrastructure will be comprised of an [Aurora](https://aws.amazon.com/rds/aurora/) MySQL database and a private [S3](https://aws.amazon.com/s3/) bucket. > [!CAUTION] > > This example creates real Amazon Web Services resources running in your > project. They will cost you real money. For the purposes of this demo > it is strongly recommended that you create a new project so that you > can easily clean up at the end. ## Prerequisites For the remainder of this codelab, you'll need to have an AWS account, with a running EC2 Amazon Linux VM, and the following software installed: - [golang](https://go.dev/doc/install), which we'll use to compile and run the test personality on the VM - [terraform](https://developer.hashicorp.com/terraform/tutorials/aws-get-started/install-cli) and [terragrunt](https://terragrunt.gruntwork.io/docs/getting-started/install/) in order to deploy the Tessera infrastructure from the VM. - `git` to clone the repo - a terminal multiplexer of your choice for convenience Follow [these instructions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EC2_GetStarted.html) to set up a VM. A free-tier `t2.micro` VM is enough for this codelab. Leave all the defaults settings, including for the default VPC. Don't forget to run `chmod 400` on your SSH key. ## Instructions ### Prepare your environment 1. [SSH to your VM](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EC2_GetStarted.html#ec2-connect-to-instance). 1. Authenticate with a role that has sufficient access to create resources. For the purpose of this codelab, and for ease of demonstration, we'll use the `AdministratorAccess` role, and authenticate with `aws configure sso`. **DO NOT** use this role to run any production infrastructure, or if there are *other services running on your AWS account. Here's an example run: ``` [ec2-user@ip-172-31-21-186 tessera]$ aws configure sso SSO session name (Recommended): greenfield-session SSO start URL [None]: https://console.aws.amazon.com/ // unless you use a custom signin console SSO region [None]: us-east-1 SSO registration scopes [sso:account:access]: Attempting to automatically open the SSO authorization page in your default browser. If the browser does not open or you wish to use a different device to authorize this request, open the following URL: https://device.sso.us-east-1.amazonaws.com/ Then enter the code: There are 4 AWS accounts available to you. Using the account ID The only role available to you is: AdministratorAccess Using the role name "AdministratorAccess" CLI default client Region [None]: us-east-1 CLI default output format [None]: CLI profile name [AdministratorAccess-]: To use this profile, specify the profile name using --profile, as shown: aws s3 ls --profile AdministratorAccess- ``` 1. Set these environment variables according to the ones you chose when configuring your AWS profile: ``` export AWS_REGION=us-east-1 export AWS_PROFILE=AdministratorAccess- ``` 1. Fetch the Tessera repo, and go to its root: ``` git clone https://github.com/transparency-dev/tessera cd tessera/ ``` ### Deploy a Tessera storage infrastructure In this section, we'll bring up a [S3](https://aws.amazon.com/s3/) bucket, an [Aurora](https://aws.amazon.com/rds/aurora/) MySQL, and we'll connect them to the VM. 1. From the root of the tessera repo, initialize terragrunt: ``` terragrunt init --working-dir=deployment/live/aws/codelab/ ``` 1. Deploy the infrastructure: ``` terragrunt apply --working-dir=deployment/live/aws/codelab/ ``` This brings up the Terraform infrastructure (S3 bucket + DynamoDB table for terraform state locking only) and the Tessera infrastructure: an RDS Aurora instance, a private S3 bucket, and connects this bucket to the default VPC that your VM should be connected to. 1. Save the RDS instance URI and S3 bucket name for later: ``` export LOG_RDS_DB=$(terragrunt output --working-dir=deployment/live/aws/codelab/ --raw log_rds_db) export LOG_BUCKET=$(terragrunt output --working-dir=deployment/live/aws/codelab/ --raw log_bucket_id) export LOG_NAME=$(terragrunt output --working-dir=deployment/live/aws/codelab/ --raw log_name) ``` 1. Connect the VM and Aurora database following [these instructions](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/tutorial-ec2-rds-option1.html#option1-task3-connect-ec2-instance-to-rds-database), it takes a few clicks in the UI. ### Start a Tessera personality A personality is a server that interacts with Tessera's storage infrastructure. In this codelab, it accepts POST requests on a `add/` HTTP endpoint. 1. Generate the key pair used to sign and verify checkpoints: ``` mkdir -p /home/ec2-user/tessera-keys go run github.com/transparency-dev/serverless-log/cmd/generate_keys@80334bc9dc573e8f6c5b3694efad6358da50abd4 \ --key_name=$LOG_NAME \ --out_priv=/home/ec2-user/tessera-keys/$LOG_NAME.sec \ --out_pub=/home/ec2-user/tessera-keys/$LOG_NAME.pub ``` 1. Running the commands below will print some easily copy-and-pasteable exports which you can use to set up the environment in a second terminal ready to be able to send requests: ``` echo "export WRITE_URL=http://localhost:2024/" echo "export READ_URL=https://$LOG_BUCKET.s3.$AWS_REGION.amazonaws.com/" echo "export LOG_PUBLIC_KEY=$(cat /home/ec2-user/tessera-keys/$LOG_NAME.pub)" ``` 1. Run the Conformance personality binary. ``` go run ./cmd/conformance/aws \ --bucket=$LOG_BUCKET \ --db_user=root \ --db_password=password \ --db_name=tessera \ --db_host=$LOG_RDS_DB \ --signer=$(cat /home/ec2-user/tessera-keys/$LOG_NAME.sec) \ --v=3 ``` 1. 🎉 **Congratulations** 🎉 You have successfully brought up Tessera's AWS infrastructure, and started a personality server that can add entries to it. Use the environment variables from above to interact with the personality in a different terminal. This personality accepts `POST` requests to the `/add` endpoint under `WRITE_URL`. Log entries can be read directly from S3 without going through the server, at `READ_URL`, and checkpoint signatures can be verified with `LOG_PUBLIC_KEY`. 1. Head over to the [remainder of this codelab](https://github.com/transparency-dev/tessera/tree/main/cmd/conformance#codelab) to add leaves to the log and inspect its contents. > [!IMPORTANT] > Do not forget to delete all the resources to avoid incuring any further cost > when you're done using the log. The easiest way to do this, is to [close the account](https://docs.aws.amazon.com/accounts/latest/reference/manage-acct-closing.html). > If you prefer to delete the resources with `terragrunt destroy`, bear in mind > that this command might not destroy all the resources that were created (like > the S3 bucket or DynamoDB instance Terraform created to store its state for > instance). If `terragrunt destroy` shows no output, run > `terragrunt destroy --terragrunt-log-level debug --terragrunt-debug`. ## Trying with Antispam The instructions above deploy the conformance binary without antispam. This means that duplicate entries can be written to the log. For logs that are publicly writable, it may be beneficial to deploy antispam, which is a weak form of deduplication. The instructions to do this for the codelab are largely the same, except: 1. When applying the terraform, instruct it to create an additional DB for the antispam tables: ``` terragrunt apply --working-dir=deployment/live/aws/codelab/ -var="create_antispam_db=true" ``` 1. When running the conformance binary, pass in two additional flags to configure antispam: ``` go run ./cmd/conformance/aws \ --bucket=$LOG_BUCKET \ --db_user=root \ --db_password=password \ --db_name=tessera \ --db_host=$LOG_RDS_DB \ --signer=$(cat /home/ec2-user/tessera-keys/$LOG_NAME.sec) \ --v=3 \ --antispam=true \ --antispam_db_name=antispam_db ``` transparency-dev-tessera-3cb22ee/deployment/live/aws/codelab/terragrunt.hcl000066400000000000000000000011501511600621500273130ustar00rootroot00000000000000terraform { source = "${get_repo_root()}/deployment/modules/aws//codelab" } locals { region = get_env("AWS_REGION", "us-east-1") base_name = "trillian-tessera" prefix_name = "codelab-${get_aws_account_id()}" ephemeral = true } remote_state { backend = "s3" config = { region = local.region bucket = "${local.prefix_name}-${local.base_name}-terraform-state" key = "terraform.tfstate" dynamodb_table = "${local.prefix_name}-${local.base_name}-terraform-lock" s3_bucket_tags = { name = "terraform_state_storage" } } } inputs = local transparency-dev-tessera-3cb22ee/deployment/live/aws/conformance/000077500000000000000000000000001511600621500253325ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/live/aws/conformance/README.md000066400000000000000000000027621511600621500266200ustar00rootroot00000000000000# AWS Conformance Configs This config uses the [aws/conformance](/deployment/modules/aws/conformance) module to define a conformance testing environment, actuated by the [AWS Conformance Test](/.github/workflows/aws_integration_test.yml) GitHub action. At a high level, this environment consists of: - Aurora MySQL database - S3 Bucket - ECS+Fargate service running the AWS-specific conformance binary and hammer ## Prequisites You'll need to have configured the right IAM permissions to create S3 buckets and RDS databases, and configured a local AWS profile that can make use of these permissions. TODO(phboneff): establish what's the minimum set of permissions we need, and list them here. ## Manual deployment Configure an AWS profile on your workstation using your prefered method, (e.g [sso](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sso.html) or [credential files](https://docs.aws.amazon.com/cli/v1/userguide/cli-configure-files.html)) Set the required environment variables: ```bash export AWS_PROFILE={VALUE} ``` Optionally, customize the AWS region (defaults to "us-east-1"), prefix, and base name for resources (defaults to "tessera" and "conformance"): ```bash export TESSERA_BASE_NAME={VALUE} export TESSERA_PREFIX_NAME={VALUE} ``` Resources will be named using a `${TESSERA_PREFIX_NAME}-${TESSERA_BASE_NAME}` convention. Terraforming the project can be done by: 1. `cd` to the relevant directory for the environment to deploy/change (e.g. `ci`) 2. Run `terragrunt apply` transparency-dev-tessera-3cb22ee/deployment/live/aws/conformance/ci/000077500000000000000000000000001511600621500257255ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/live/aws/conformance/ci/README.md000066400000000000000000000002321511600621500272010ustar00rootroot00000000000000# AWS Conformance CI config See the [README in the parent directory](../README.md) for detailed instructions on how to use the config in this directory. transparency-dev-tessera-3cb22ee/deployment/live/aws/conformance/ci/terragrunt.hcl000066400000000000000000000011051511600621500306070ustar00rootroot00000000000000include "root" { path = find_in_parent_folders() expose = true } inputs = merge( include.root.locals, { # This hack makes it so that the antispam tables are created in the main # tessera DB. We strongly recommend that the antispam DB is separate, but # creating a second DB from Terraform is too difficult without a large # rewrite. For CI purposes, testing antispam, even if in the same DB, is # preferred compared to not testing antispam at all. antispam = true antispam_db_name = "tessera" create_antispam_db = false } ) transparency-dev-tessera-3cb22ee/deployment/live/aws/conformance/terragrunt.hcl000066400000000000000000000026071511600621500302240ustar00rootroot00000000000000terraform { source = "${get_repo_root()}/deployment/modules/aws//conformance" } locals { env = path_relative_to_include() account_id = "${get_aws_account_id()}" region = get_env("AWS_REGION", "us-east-1") base_name = get_env("TESSERA_BASE_NAME", "${local.env}-conformance") prefix_name = get_env("TESSERA_PREFIX_NAME", "trillian-tessera") ecr_registry = get_env("ECR_REGISTRY", "${local.account_id}.dkr.ecr.${local.region}.amazonaws.com") ecr_repository_conformance = get_env("ECR_REPOSITORY_CONFORMANCE", "trillian-tessera/conformance:latest") ecr_repository_hammer = get_env("ECR_REPOSITORY_HAMMER", "trillian-tessera/hammer:latest") signer = get_env("TESSERA_SIGNER") verifier = get_env("TESSERA_VERIFIER") # Roles are defined externally ecs_execution_role = "arn:aws:iam::864981736166:role/ecsTaskExecutionRole" ecs_conformance_task_role = "arn:aws:iam::864981736166:role/ConformanceECSTaskRolePolicy" ephemeral = true } remote_state { backend = "s3" config = { region = local.region bucket = "${local.prefix_name}-${local.base_name}-terraform-state" key = "${local.env}/terraform.tfstate" s3_bucket_tags = { name = "terraform_state_storage" } use_lockfile = true } } transparency-dev-tessera-3cb22ee/deployment/live/gcp/000077500000000000000000000000001511600621500230175ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/live/gcp/cloudbuild/000077500000000000000000000000001511600621500251455ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/live/gcp/cloudbuild/README.md000066400000000000000000000017111511600621500264240ustar00rootroot00000000000000# Cloudbuild Triggers and Steps This directory contains terragrunt files to configure our CloudBuild pipeline(s). See links in the [deployment dir](/deployment/README.md) to install necessary tools. The CloudBuild pipeline is triggered on commits to the `main` branch of the repo, and is responsible for: 1. Building the `cmd/gcp/conformance` docker image from the `main` branch, 2. Creating a fresh conformance testing environment, 3. Running the conformance test against the newly build conformance docker image, 4. Turning-down the conformance testing environment. ## Initial setup The first time this is run for a pair of {GCP Project, GitHub Repo} you will get an error message such as the following: ``` Error: Error creating Trigger: googleapi: Error 400: Repository mapping does not exist. Please visit $URL to connect a repository to your project ``` This is a manual one-time step that needs to be followed to integrate GCB and the GitHub project. transparency-dev-tessera-3cb22ee/deployment/live/gcp/cloudbuild/prod/000077500000000000000000000000001511600621500261115ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/live/gcp/cloudbuild/prod/.terraform.lock.hcl000066400000000000000000000022021511600621500316030ustar00rootroot00000000000000# This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/google" { version = "6.9.0" hashes = [ "h1:MsKAs34RhnHsJ2xFtbRiwvDYWZgEPaZwqK0Hm/G8UH0=", "zh:53e9d7ffed63e2accff949ac7ca348d03be3e404e0b18c93ec596bcb52bac97a", "zh:6cbaf7e40fba2cff3d3fe4b3213de81c6157e327e996febad6949949b104b6ae", "zh:74562331eae7c88a8f934eb05971c361081c6e23d7a4564d1b11206558c037ed", "zh:ac65f1507886d92858ddeeff710c7dab942437c7421c63c1c7aeb139f2bb44af", "zh:b4a562b7c497661cd6c972097fea12449f183bb7e11bf6c62a750cc97bd3407e", "zh:b9cdc22e59c47604492bdf3e4d123037e2f5dd0f8a0ec0cf0b81fda165dd8581", "zh:c3f146d739b88de32339fb0091898bc9ad0aa3b257c8db4526076476c78e467a", "zh:c7567999d7563913360598bca4c9fce59f43e3a45726da4fa3f6d2752469c486", "zh:f3451c149844709713301641a5a2cfbf8add4abda927a457c5a2d67fa565887b", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", "zh:f7df50451a534f59c472a190fc2e49caaed9ebdb22bd0b6e4fe2be423ce3a7a5", "zh:fd41e513b9546f2ab6b2cc36de36f4cd8ae3eb04ba3fca9b308ec6e387837366", ] } transparency-dev-tessera-3cb22ee/deployment/live/gcp/cloudbuild/prod/terragrunt.hcl000066400000000000000000000004221511600621500307740ustar00rootroot00000000000000include "root" { path = find_in_parent_folders() expose = true } inputs = merge( include.root.locals, { # Service accounts are managed externally. service_account = "cloudbuild-${include.root.locals.env}-sa@trillian-tessera.iam.gserviceaccount.com" } ) transparency-dev-tessera-3cb22ee/deployment/live/gcp/cloudbuild/terragrunt.hcl000066400000000000000000000010161511600621500300300ustar00rootroot00000000000000terraform { source = "${get_repo_root()}/deployment/modules/gcp//cloudbuild" } locals { project_id = "trillian-tessera" region = "us-central1" env = path_relative_to_include() } remote_state { backend = "gcs" config = { project = local.project_id location = local.region bucket = "${local.project_id}-cloudbuild-${local.env}-terraform-state" prefix = "${path_relative_to_include()}-terraform.tfstate" gcs_bucket_labels = { name = "terraform_state_storage" } } } transparency-dev-tessera-3cb22ee/deployment/live/gcp/conformance/000077500000000000000000000000001511600621500253115ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/live/gcp/conformance/README.md000066400000000000000000000127111511600621500265720ustar00rootroot00000000000000# GCP Conformance Configs This config uses the [gcp/conformance](/deployment/modules/gcp/conformance) module to define a conformance testing environment. At a high level, this environment consists of: - Spanner DB, - GCS Bucket, - CloudRun service running the [GCP-specific conformance binary](/cmd/conformance/gcp). The config allows identities (e.g. service accounts) to be provided to allow access to reading from, and writing to, the log. ## Automatic deployment For the most part, this terragrunt config is automatically used as part of conformance testing by the [Cloud Build](/deployment/live/gcp/cloudbuild) pipeline, so doesn't generally need to be manually applied. ## Manual deployment ### Prerequisites You'll need the following tools installed: - [`golang`](https://go.dev/doc/install) - [`docker`](https://docs.docker.com/engine/install/) - [`gcloud`](https://cloud.google.com/sdk/docs/install) - One of: + [`terraform`](https://developer.hashicorp.com/terraform/install) or + [`opentofu`](https://opentofu.org/docs/intro/install/) - [`terragrunt`](https://terragrunt.gruntwork.io/docs/getting-started/install/) #### Google Cloud tooling > [!CAUTION] > This example creates real Google Cloud resources running in your project. They will almost certainly > cost you real money if left running. For the purposes of this demo it is strongly recommended that > you create a new project so that you can easily clean up at the end. Once you've got a Google Cloud project you want to use, have configured your local `gcloud` tool use use it, and authenticated as a principle with sufficient ACLs for the project: ```bash gcloud config set project {YOUR PROJECT} gcloud auth application-default login ``` #### Set environment variables Set the required environment variables: ```bash # The ID of the Google Cloud Project you're deploying into export GOOGLE_PROJECT=$(gcloud config get project) # This should be a note signer string. # You can use the generate_keys tool to create a new signer & verifier pair: go run github.com/transparency-dev/serverless-log/cmd/generate_keys@HEAD --key_name="TestTessera" --out_priv=tessera.sec --out_pub=tessera.pub export TESSERA_SIGNER=$(cat tessera.sec) # This is the name of the artifact registry docker repo to create/use. export DOCKER_REPO_NAME=tessera-docker ``` Optionally, set the environment variables below to customize the deployment: ```bash # GCP region to deploy into (defaults to us-central1) export GOOGLE_REGION={VALUE} # This is used as part of resource names, using this variable will allow you to have multiple deployments in a single project. export TESSERA_BASE_NAME={VALUE} # This allows you to specify the email of an existing service account which should be used by Cloud Run. # By default, the project's default service account will be used. export TESSERA_CLOUD_RUN_SERVICE_ACCOUNT={VALUE} # This allows configuration of which users are allowed to read from the GCS bucket containing the t-log tiles. # To make the bucket public, set this to "allUsers". export TESSERA_READER={VALUE} # This allows configuration of which users are allowed to make HTTP requests to the Cloud Run instance, e.g. to add entries to the t-log. # By default, only the project's default service account is permitted. export TESSERA_WRITER={VALUE} ``` #### Set up artifact registry First, create a new artifact registry based Docker repo: ```bash gcloud artifacts repositories create ${DOCKER_REPO_NAME} \ --repository-format=docker \ --location=us-central1 \ --description="My Tessera docker repo" \ --immutable-tags ``` Then authorize your local `docker` command to be able to interact with it: ```bash gcloud auth configure-docker us-central1-docker.pkg.dev ``` ### Process #### Build & push docker image You will need to build and push the image created by the [the /cmd/conformance/gcp/Dockerfile](/cmd/conformance/gcp/Dockerfile) somewhere. Google Artifact Registry is one option: https://cloud.google.com/build/docs/build-push-docker-image Note that it's not currently possible to _build_ the docker image with Google Cloud Build (this is because building from the root directory but referencing Dockerfile in a subdirectory isn't supported), but you can configure your local `docker` to get access to Artifact Registry using the gcloud CLI credential helper, and then build locally and push to Artifact Registry. Details on this can be found in the link above. ```bash docker build . -f ./cmd/conformance/gcp/Dockerfile --tag us-central1-docker.pkg.dev/${GOOGLE_PROJECT}/${DOCKER_REPO_NAME}/conformance:latest docker push us-central1-docker.pkg.dev/${GOOGLE_PROJECT}/${DOCKER_REPO_NAME}/conformance:latest # The docker image:tag for the image you just built. export TESSERA_CLOUD_RUN_DOCKER_IMAGE=us-central1-docker.pkg.dev/${GOOGLE_PROJECT}/${DOCKER_REPO_NAME}/conformance:latest ``` #### Terragrunt apply Finally, apply the config using `terragrunt`: 1. `cd` to the relevant directory for the environment to deploy/change (e.g. `ci`) 2. Run `terragrunt apply` This should create all necessary infrastructure, and spin up a Cloud Run instance with the docker image you created above. ### Clean up > [!IMPORTANT] > You need to run this step on your project if you want to ensure you don't get charged into perpetuity > for the resources we've setup. **This will delete your project!** Do not do this on a project that you didn't create expressly and exclusively to run this demo. ```bash gcloud projects delete ${GOOGLE_PROJECT} ``` transparency-dev-tessera-3cb22ee/deployment/live/gcp/conformance/ci/000077500000000000000000000000001511600621500257045ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/live/gcp/conformance/ci/.terraform.lock.hcl000066400000000000000000000022361511600621500314050ustar00rootroot00000000000000# This file is maintained automatically by "terraform init". # Manual edits may be lost in future updates. provider "registry.terraform.io/hashicorp/google" { version = "6.1.0" constraints = "6.1.0" hashes = [ "h1:okppWOAoIPz45VkydzAA74HRLgEKvP4CFXypPU228j8=", "zh:2463510438c97c59e06ab1fb1ef76221c844abd1bc404c439401fc256e9928ab", "zh:2afd9b76a81c51632bd54d3cc3bdc2685e8d89b8ace8ca7578b1ae42880228b5", "zh:51e2fb64c7c8258ac0ec7315d488e5c655b392bf565f9bee2922ee72f6abfb90", "zh:85aa39bad51132810ee6cd369f426614abff59cb0274fc737d087c17afa9b5ee", "zh:989669bfed5ca7bf4d960eb9f27a62cbe2578ca2907da7c74fc93edae9a497fa", "zh:a26665782e90ef3fd322d6a23a1de383c81ae93395e7c2bd9648a1aa85c69876", "zh:d5e1b785b4c8569b91153eeba89280ffbbe7a0aaabb708833ada67544aeed057", "zh:d748c69eab6acc4ab7ec369b3bd3ddd5d2e4120d99570743dafde74934959a20", "zh:eb853ab5c4c0d3e536b8c77abf844b7893ac355967c95b6e0d39b12526e67989", "zh:f4b50f0ae082412ba189041b6ac540523b7d6463905fed63be67eec03e1539b9", "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", "zh:f6e7adcfafe267d9c657a6c087388f7e0c1e3be4dc179a9a823f75c830a499b7", ] } transparency-dev-tessera-3cb22ee/deployment/live/gcp/conformance/ci/README.md000066400000000000000000000002321511600621500271600ustar00rootroot00000000000000# GCP Conformance CI config See the [README in the parent directory](../README.md) for detailed instructions on how to use the config in this directory. transparency-dev-tessera-3cb22ee/deployment/live/gcp/conformance/ci/terragrunt.hcl000066400000000000000000000005041511600621500305700ustar00rootroot00000000000000terraform { source = "${get_repo_root()}/deployment/modules/gcp//conformance" } include "root" { path = find_in_parent_folders() expose = true } inputs = merge( include.root.locals, { base_name = get_env("TESSERA_BASE_NAME", "ci-conformance-${substr(uuid(), 0, 4)}") enable_antispam = true } ) transparency-dev-tessera-3cb22ee/deployment/live/gcp/conformance/terragrunt.hcl000066400000000000000000000023201511600621500301730ustar00rootroot00000000000000terraform { source = "${get_repo_root()}/deployment/modules/gcp//conformance" } locals { env = path_relative_to_include() project_id = get_env("GOOGLE_PROJECT") location = get_env("GOOGLE_REGION", "us-central1") base_name = get_env("TESSERA_BASE_NAME", "${local.env}-conformance") state_bucket_name = "${local.base_name}" server_docker_image = get_env("TESSERA_CLOUD_RUN_DOCKER_IMAGE") signer = get_env("TESSERA_SIGNER") tessera_reader = get_env("TESSERA_READER", "") tessera_writer = get_env("TESSERA_WRITER", "") conformance_readers = length(local.tessera_reader) > 0 ? [local.tessera_reader] : [] conformance_writers = length(local.tessera_writer) > 0 ? [local.tessera_writer] : [] cloudrun_service_account = get_env("TESSERA_CLOUD_RUN_SERVICE_ACCOUNT", "") } remote_state { backend = "gcs" config = { project = local.project_id location = local.location bucket = "${local.project_id}-${local.state_bucket_name}-terraform-state" prefix = "${local.env}/terraform.tfstate" gcs_bucket_labels = { name = "terraform_state_storage" } } } transparency-dev-tessera-3cb22ee/deployment/modules/000077500000000000000000000000001511600621500227575ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/modules/aws/000077500000000000000000000000001511600621500235515ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/modules/aws/codelab/000077500000000000000000000000001511600621500251425ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/modules/aws/codelab/main.tf000066400000000000000000000036671511600621500264350ustar00rootroot00000000000000# Header ###################################################################### terraform { backend "s3" {} required_providers { aws = { source = "hashicorp/aws" version = "5.76.0" } } } locals { name = "${var.prefix_name}-${var.base_name}" port = 2024 } provider "aws" { region = var.region } module "storage" { source = "../storage" prefix_name = var.prefix_name base_name = var.base_name region = var.region create_antispam_db = var.create_antispam_db ephemeral = var.ephemeral } # Resources #################################################################### ## Virtual private network ##################################################### # This will be used for the containers to communicate between themselves, and # the S3 bucket. resource "aws_default_vpc" "default" { tags = { Name = "Default VPC" } } ## Connect S3 bucket to VPC #################################################### # This allows the hammer to talk to a non public S3 bucket over HTTP. resource "aws_vpc_endpoint" "s3" { vpc_id = aws_default_vpc.default.id service_name = "com.amazonaws.${var.region}.s3" } resource "aws_vpc_endpoint_route_table_association" "private_s3" { vpc_endpoint_id = aws_vpc_endpoint.s3.id route_table_id = aws_default_vpc.default.default_route_table_id } resource "aws_s3_bucket_policy" "allow_access_from_vpce" { bucket = module.storage.log_bucket.id policy = data.aws_iam_policy_document.allow_access_from_vpce.json } data "aws_iam_policy_document" "allow_access_from_vpce" { statement { principals { type = "*" identifiers = ["*"] } actions = [ "s3:GetObject", ] resources = [ "${module.storage.log_bucket.arn}/*", ] condition { test = "StringEquals" variable = "aws:sourceVpce" values = [aws_vpc_endpoint.s3.id] } } depends_on = [aws_vpc_endpoint.s3] } transparency-dev-tessera-3cb22ee/deployment/modules/aws/codelab/outputs.tf000066400000000000000000000007171511600621500272250ustar00rootroot00000000000000output "log_bucket_id" { description = "Log S3 bucket name" value = module.storage.log_bucket.id } output "log_bucket_http" { description = "Log S3 bucket http access" value = "https://${module.storage.log_bucket.bucket_regional_domain_name}" } output "log_rds_db" { description = "Log RDS database endpoint" value = module.storage.log_rds_db.endpoint } output "log_name" { description = "Log name" value = local.name } transparency-dev-tessera-3cb22ee/deployment/modules/aws/codelab/variables.tf000066400000000000000000000014071511600621500274470ustar00rootroot00000000000000variable "prefix_name" { description = "Common prefix to use when naming resources, ensures unicity of the s3 bucket name." type = string } variable "base_name" { description = "Common name to use when naming resources." type = string } variable "region" { description = "Region in which to create resources." type = string } variable "create_antispam_db" { description = "Set to true to create another database to be used by the antispam implementation." type = bool default = false } variable "ephemeral" { description = "Set to true if this is a throwaway/temporary log instance. Will set attributes on created resources to allow them to be disabled/deleted more easily." type = bool default = true } transparency-dev-tessera-3cb22ee/deployment/modules/aws/conformance/000077500000000000000000000000001511600621500260435ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/modules/aws/conformance/main.tf000066400000000000000000000207131511600621500273250ustar00rootroot00000000000000# Header ###################################################################### terraform { backend "s3" {} required_providers { aws = { source = "hashicorp/aws" version = "5.76.0" } } } locals { name = "${var.prefix_name}-${var.base_name}" port = 2024 } provider "aws" { region = var.region } module "storage" { source = "../storage" prefix_name = var.prefix_name base_name = var.base_name region = var.region ephemeral = true create_antispam_db = var.create_antispam_db } # Resources #################################################################### ## ECS cluster ################################################################# # This will be used to run the conformance and hammer binaries on Fargate. resource "aws_ecs_cluster" "ecs_cluster" { name = local.name } resource "aws_ecs_cluster_capacity_providers" "ecs_capacity" { cluster_name = aws_ecs_cluster.ecs_cluster.name capacity_providers = ["FARGATE"] } ## Virtual private network ##################################################### # This will be used for the containers to communicate between themselves, and # the S3 bucket. resource "aws_default_vpc" "default" { tags = { Name = "Default VPC" } } data "aws_subnets" "subnets" { filter { name = "vpc-id" values = [aws_default_vpc.default.id] } } ## Service discovery ########################################################### # This will by the hammer to contact multiple conformance tasks with a single # dns name. resource "aws_service_discovery_private_dns_namespace" "internal" { name = "internal" vpc = aws_default_vpc.default.id } resource "aws_service_discovery_service" "conformance_discovery" { name = "conformance-discovery" dns_config { namespace_id = aws_service_discovery_private_dns_namespace.internal.id dns_records { ttl = 10 type = "A" } // TODO(phboneff): make sure that the hammer uses multiple IPs // otherwise, set a low TTL and use WEIGHTED. routing_policy = "MULTIVALUE" } health_check_custom_config { failure_threshold = 1 } } ## Connect S3 bucket to VPC #################################################### # This allows the hammer to talk to a non public S3 bucket over HTTP. resource "aws_vpc_endpoint" "s3" { vpc_id = aws_default_vpc.default.id service_name = "com.amazonaws.${var.region}.s3" } resource "aws_vpc_endpoint_route_table_association" "private_s3" { vpc_endpoint_id = aws_vpc_endpoint.s3.id route_table_id = aws_default_vpc.default.default_route_table_id } resource "aws_s3_bucket_policy" "allow_access_from_vpce" { bucket = module.storage.log_bucket.id policy = data.aws_iam_policy_document.allow_access_from_vpce.json } data "aws_iam_policy_document" "allow_access_from_vpce" { statement { principals { type = "*" identifiers = ["*"] } actions = [ "s3:GetObject", ] resources = [ "${module.storage.log_bucket.arn}/*", ] condition { test = "StringEquals" variable = "aws:sourceVpce" values = [aws_vpc_endpoint.s3.id] } } depends_on = [aws_vpc_endpoint.s3] } ## Conformance task and service ################################################ # This will start multiple conformance tasks on Fargate within a service. resource "aws_ecs_task_definition" "conformance" { family = "conformance" requires_compatibilities = ["FARGATE"] # Required network_mode for tasks running on Fargate. network_mode = "awsvpc" cpu = 1024 memory = 2048 execution_role_arn = var.ecs_execution_role # We need a special role that has access to S3. task_role_arn = var.ecs_conformance_task_role container_definitions = jsonencode([{ "name" : "${local.name}-conformance", "image" : "${var.ecr_registry}/${var.ecr_repository_conformance}", "cpu" : 0, "portMappings" : [{ "name" : "conformance-${local.port}-tcp", "containerPort" : local.port, "hostPort" : local.port, "protocol" : "tcp", "appProtocol" : "http" }], "essential" : true, "command" : [ "--signer=${var.signer}", "--bucket=${module.storage.log_bucket.id}", "--db_user=root", "--db_password=password", "--db_name=tessera", "--db_host=${module.storage.log_rds_db.endpoint}", "--antispam=${var.antispam}", "--antispam_db_name=${var.antispam_db_name}", "-v=2" ], "logConfiguration" : { "logDriver" : "awslogs", "options" : { "awslogs-group" : "/ecs/${local.name}", "mode" : "non-blocking", "awslogs-create-group" : "true", "max-buffer-size" : "25m", "awslogs-region" : "us-east-1", "awslogs-stream-prefix" : "ecs" }, }, }, { "name" : "aws-otel-collector-${local.name}", "image" : "public.ecr.aws/aws-observability/aws-otel-collector:v0.43.1", "command" : ["--config=/etc/ecs/ecs-cloudwatch-xray.yaml"], "essential" : true, "logConfiguration" : { "logDriver" : "awslogs", "options" : { "awslogs-group" : "/ecs/${local.name}", "awslogs-region" : "us-east-1", "awslogs-stream-prefix" : "ecs", "awslogs-create-group" : "true" } }, "healthCheck" : { "command" : ["/healthcheck"], "interval" : 5, "timeout" : 6, "retries" : 5, "startPeriod" : 1 }, "portMappings" : [], }]) runtime_platform { operating_system_family = "LINUX" cpu_architecture = "X86_64" } depends_on = [module.storage] } resource "aws_ecs_service" "conformance_service" { name = local.name task_definition = aws_ecs_task_definition.conformance.arn cluster = aws_ecs_cluster.ecs_cluster.arn launch_type = "FARGATE" desired_count = 3 wait_for_steady_state = true network_configuration { subnets = data.aws_subnets.subnets.ids # required to access container registry assign_public_ip = true } # connect the service with the service discovery defined above service_registries { registry_arn = aws_service_discovery_service.conformance_discovery.arn } depends_on = [ aws_service_discovery_private_dns_namespace.internal, aws_service_discovery_service.conformance_discovery, aws_ecs_cluster.ecs_cluster, aws_ecs_task_definition.conformance, ] } ## Hammer task definition and execution ######################################## # The hammer can also be launched manually with the following command: # aws ecs run-task \ # --cluster="$(terragrunt output -raw ecs_cluster)" \ # --task-definition=hammer \ # --count=1 \ # --launch-type=FARGATE \ # --network-configuration='{"awsvpcConfiguration": {"assignPublicIp":"ENABLED","subnets": '$(terragrunt output -json vpc_subnets)'}}' resource "aws_ecs_task_definition" "hammer" { family = "hammer" requires_compatibilities = ["FARGATE"] # Required network_mode for tasks running on Fargate network_mode = "awsvpc" cpu = 1024 memory = 2048 execution_role_arn = var.ecs_execution_role container_definitions = jsonencode([{ "name" : "${local.name}-hammer", "image" : "${var.ecr_registry}/${var.ecr_repository_hammer}", "cpu" : 0, "portMappings" : [{ "name" : "hammer-80-tcp", "containerPort" : 80, "hostPort" : 80, "protocol" : "tcp", "appProtocol" : "http" }], "essential" : true, "command" : [ "--log_public_key=${var.verifier}", "--log_url=https://${module.storage.log_bucket.bucket_regional_domain_name}", "--write_log_url=http://${aws_service_discovery_service.conformance_discovery.name}.${aws_service_discovery_private_dns_namespace.internal.name}:${local.port}", "-v=3", "--show_ui=false", "--logtostderr", "--num_writers=1100", "--max_write_ops=1500", "--leaf_min_size=1024", "--leaf_write_goal=50000" ], "logConfiguration" : { "logDriver" : "awslogs", "options" : { "awslogs-group" : "/ecs/${local.name}-hammer", "mode" : "non-blocking", "awslogs-create-group" : "true", "max-buffer-size" : "25m", "awslogs-region" : "us-east-1", "awslogs-stream-prefix" : "ecs" }, }, }]) runtime_platform { operating_system_family = "LINUX" cpu_architecture = "X86_64" } depends_on = [ module.storage, aws_ecs_cluster.ecs_cluster, ] } transparency-dev-tessera-3cb22ee/deployment/modules/aws/conformance/outputs.tf000066400000000000000000000003251511600621500301210ustar00rootroot00000000000000output "ecs_cluster" { description = "ECS cluster name" value = aws_ecs_cluster.ecs_cluster.id } output "vpc_subnets" { description = "VPC subnets list" value = data.aws_subnets.subnets.ids } transparency-dev-tessera-3cb22ee/deployment/modules/aws/conformance/variables.tf000066400000000000000000000037061511600621500303540ustar00rootroot00000000000000variable "prefix_name" { description = "Common prefix to use when naming resources, ensures unicity of the s3 bucket name." type = string } variable "base_name" { description = "Common name to use when naming resources." type = string } variable "region" { description = "Region in which to create resources." type = string } variable "ephemeral" { description = "Set to true if this is a throwaway/temporary log instance. Will set attributes on created resources to allow them to be disabled/deleted more easily." type = bool } variable "ecr_registry" { description = "Container registry address, with the conformance and hammer repositories." type = string } variable "ecr_repository_conformance" { description = "Container repository for the conformance binary, with the tag." type = string } variable "ecr_repository_hammer" { description = "Container repository for the hammer binary, with the tag." type = string } variable "signer" { description = "The note signer which used to sign checkpoints." type = string } variable "verifier" { description = "The note verifier used to verify checkpoints." type = string } variable "ecs_execution_role" { description = "Role used to run the ECS task." type = string } variable "ecs_conformance_task_role" { description = "Role assumed by conformance containers when they run." type = string } variable "antispam" { description = "Set to true to enable antispam for this conformance log. If enabled, antispam_db_name must also be provided." type = bool default = false } variable "create_antispam_db" { description = "Set to true to create a separate DB for the antispam data. This will not work from github actions." type = bool default = false } variable "antispam_db_name" { description = "The name of the antispam database." type = string default = "" } transparency-dev-tessera-3cb22ee/deployment/modules/aws/storage/000077500000000000000000000000001511600621500252155ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/modules/aws/storage/main.tf000066400000000000000000000036021511600621500264750ustar00rootroot00000000000000locals { name = "${var.prefix_name}-${var.base_name}" } terraform { required_providers { mysql = { source = "petoju/mysql" version = "3.0.71" } } } # Configure the AWS Provider provider "aws" { region = var.region } # Resources ## S3 Bucket resource "aws_s3_bucket" "log_bucket" { bucket = "${local.name}-bucket" force_destroy = var.ephemeral } ## Aurora MySQL RDS database resource "aws_rds_cluster" "log_rds" { apply_immediately = true cluster_identifier = "${local.name}-cluster" engine = "aurora-mysql" # TODO(phboneff): make sure that we want to pin this engine_version = "8.0" database_name = "tessera" master_username = "root" # TODO(phboneff): move to either random strings / Secret Manager / IAM master_password = "password" skip_final_snapshot = true backup_retention_period = 1 } resource "aws_rds_cluster_instance" "cluster_instances" { # TODO(phboneff): make some of these variables and/or # tweak some of these. count = 1 identifier = "${local.name}-writer-${count.index}" cluster_identifier = aws_rds_cluster.log_rds.id instance_class = "db.r5.large" engine = aws_rds_cluster.log_rds.engine engine_version = aws_rds_cluster.log_rds.engine_version } # Configure the MySQL provider based on the outcome of # creating the aws_db_instance. # This requires that the machine running terraform has access # to the DB instance created above. This is _NOT_ the case when # github actions are applying the terraform. provider "mysql" { endpoint = aws_rds_cluster_instance.cluster_instances[0].endpoint username = aws_rds_cluster.log_rds.master_username password = aws_rds_cluster.log_rds.master_password } # Create a second database for antispam. resource "mysql_database" "antispam_db" { name = "antispam_db" count = var.create_antispam_db ? 1 : 0 } transparency-dev-tessera-3cb22ee/deployment/modules/aws/storage/outputs.tf000066400000000000000000000003051511600621500272710ustar00rootroot00000000000000output "log_bucket" { description = "Log S3 bucket" value = aws_s3_bucket.log_bucket } output "log_rds_db" { description = "Log RDS database" value = aws_rds_cluster.log_rds } transparency-dev-tessera-3cb22ee/deployment/modules/aws/storage/variables.tf000066400000000000000000000013601511600621500275200ustar00rootroot00000000000000variable "prefix_name" { description = "Common prefix to use when naming resources, ensures unicity of the s3 bucket name." type = string } variable "base_name" { description = "Common name to use when naming resources" type = string } variable "region" { description = "Region in which to create resources" type = string } variable "create_antispam_db" { description = "Set to true to create another database to be used by the antispam implementation." type = bool default = false } variable "ephemeral" { description = "Set to true if this is a throwaway/temporary log instance. Will set attributes on created resources to allow them to be disabled/deleted more easily." type = bool } transparency-dev-tessera-3cb22ee/deployment/modules/gcp/000077500000000000000000000000001511600621500235305ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/modules/gcp/cloudbuild/000077500000000000000000000000001511600621500256565ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/modules/gcp/cloudbuild/main.tf000066400000000000000000000147421511600621500271450ustar00rootroot00000000000000terraform { backend "gcs" {} } provider "google" { project = var.project_id region = var.region } resource "google_artifact_registry_repository" "docker" { repository_id = "docker-${var.env}" location = var.region description = "Tessera conformance docker images" format = "DOCKER" } locals { artifact_repo = "${var.region}-docker.pkg.dev/${var.project_id}/${google_artifact_registry_repository.docker.name}" conformance_gcp_docker_image = "${local.artifact_repo}/conformance-gcp" } resource "google_cloudbuild_trigger" "docker" { name = "GCP-integration-test-${var.env}" service_account = "projects/${var.project_id}/serviceAccounts/${var.service_account}" location = var.region github { owner = "transparency-dev" name = "tessera" push { branch = "^main$" } } build { ## Destroy any pre-existing deployment/live/gcp/conformance/ci environment. ## This might happen if a previous cloud build failed for some reason. step { id = "preclean_env" name = "alpine/terragrunt:1.9.8" script = <&1 EOT dir = "deployment/live/gcp/conformance/ci" env = [ "TESSERA_SIGNER=unused", "TESSERA_CLOUD_RUN_DOCKER_IMAGE=${local.conformance_gcp_docker_image}:latest", "TESSERA_CLOUD_RUN_SERVICE_ACCOUNT=cloudrun-ci-sa@trillian-tessera.iam.gserviceaccount.com", "TESSERA_READER=serviceAccount:cloudbuild-prod-sa@trillian-tessera.iam.gserviceaccount.com", "TESSERA_WRITER=serviceAccount:cloudbuild-prod-sa@trillian-tessera.iam.gserviceaccount.com", "GOOGLE_PROJECT=${var.project_id}", "TF_IN_AUTOMATION=1", "TF_INPUT=false", "TF_VAR_project_id=${var.project_id}" ] } ## Build the GCP conformance server docker image. ## This will be used by the conformance terragrunt config step further down. step { id = "docker_build_conformance_gcp" name = "gcr.io/cloud-builders/docker" args = [ "build", "-t", "${local.conformance_gcp_docker_image}:$SHORT_SHA", "-t", "${local.conformance_gcp_docker_image}:latest", "-f", "./cmd/conformance/gcp/Dockerfile", "." ] } ## Push the image. step { id = "docker_push_conformance_gcp" name = "gcr.io/cloud-builders/docker" args = [ "push", "--all-tags", local.conformance_gcp_docker_image ] wait_for = ["docker_build_conformance_gcp"] } step { id = "generate_keys" name = "golang" script = <&1 EOT dir = "deployment/live/gcp/conformance/ci" env = [ "GOOGLE_PROJECT=${var.project_id}", "TESSERA_CLOUD_RUN_DOCKER_IMAGE=${local.conformance_gcp_docker_image}:latest", "TESSERA_CLOUD_RUN_SERVICE_ACCOUNT=cloudrun-ci-sa@trillian-tessera.iam.gserviceaccount.com", "TESSERA_READER=serviceAccount:cloudbuild-prod-sa@trillian-tessera.iam.gserviceaccount.com", "TESSERA_WRITER=serviceAccount:cloudbuild-prod-sa@trillian-tessera.iam.gserviceaccount.com", "TF_IN_AUTOMATION=1", "TF_INPUT=false", "TF_VAR_project_id=${var.project_id}" ] wait_for = ["docker_push_conformance_gcp", "generate_keys"] } ## Grab some outputs from the terragrunt apply above (e.g. conformance server URL) and store ## them in files under /workspace. These are needed for later steps. step { id = "terraform_outputs" name = "alpine/terragrunt:1.9.8" script = < /workspace/conformance_url terragrunt output --raw conformance_bucket_name > /workspace/conformance_bucket_name EOT wait_for = ["terraform_apply_conformance_ci"] } ## Since the conformance infrastructure is not publicly accessible, we need to use bearer tokens ## for the hammer to access them. ## This step creates those, and stores them for later use. step { id = "access" name = "gcr.io/cloud-builders/gcloud" script = < /workspace/cb_access curl -H "Metadata-Flavor: Google" "http://metadata/computeMetadata/v1/instance/service-accounts/${var.service_account}/identity?audience=$(cat /workspace/conformance_url)" > /workspace/cb_identity EOT wait_for = ["terraform_outputs"] } ## Run the hammer against the conformance server. ## We're using it in "target throughput" mode. step { id = "hammer" name = "golang" script = < 0 ? var.conformance_readers : ["serviceAccount:${data.google_compute_default_service_account.default.email}"] writers = length(var.conformance_writers) > 0 ? var.conformance_writers : ["serviceAccount:${data.google_compute_default_service_account.default.email}"] cloudrun_service_account = length(var.cloudrun_service_account) > 0 ? var.cloudrun_service_account : data.google_compute_default_service_account.default.email } ## Call the Tessera GCP module ## ## This will configure all the storage infrastructure required to run a Tessera log on GCP. module "gcs" { source = "..//gcs" base_name = var.base_name env = var.env location = var.location project_id = var.project_id bucket_readers = local.readers log_writer_members = ["serviceAccount:${local.cloudrun_service_account}"] create_antispam_db = var.enable_antispam ephemeral = true } ## ## Resources ## # Enable Cloud Run API resource "google_project_service" "cloudrun_api" { service = "run.googleapis.com" disable_on_destroy = false } resource "google_project_service" "compute_engine" { service = "compute.googleapis.com" disable_on_destroy = false } locals { spanner_db_full = "projects/${var.project_id}/instances/${module.gcs.log_spanner_instance.name}/databases/${module.gcs.log_spanner_db.name}" } resource "google_cloud_run_v2_service" "default" { name = var.base_name location = var.location launch_stage = "GA" template { service_account = local.cloudrun_service_account max_instance_request_concurrency = 700 timeout = "5s" scaling { max_instance_count = 3 } containers { image = var.server_docker_image name = "conformance" args = [ "--logtostderr", "--v=1", "--bucket=${module.gcs.log_bucket.id}", "--spanner=${local.spanner_db_full}", "--listen=:8080", "--signer=${var.signer}", "--antispam=${var.enable_antispam}", ] ports { name = "h2c" container_port = 8080 } resources { limits = { cpu = "2" memory = "1024Mi" } } startup_probe { initial_delay_seconds = 10 timeout_seconds = 10 period_seconds = 10 failure_threshold = 10 tcp_socket { port = 8080 } } } } deletion_protection = false client = "terraform" depends_on = [ module.gcs, google_project_service.cloudrun_api, ] } resource "google_cloud_run_v2_service_iam_binding" "cloudrun_invoker" { location = google_cloud_run_v2_service.default.location name = google_cloud_run_v2_service.default.name role = "roles/run.invoker" members = local.writers } transparency-dev-tessera-3cb22ee/deployment/modules/gcp/conformance/outputs.tf000066400000000000000000000004331511600621500301000ustar00rootroot00000000000000output "conformance_url" { description = "The URL of the running conformance server" value = google_cloud_run_v2_service.default.uri } output "conformance_bucket_name" { description = "The name of the conformance log bucket" value = module.gcs.log_bucket.name } transparency-dev-tessera-3cb22ee/deployment/modules/gcp/conformance/variables.tf000066400000000000000000000027641511600621500303360ustar00rootroot00000000000000variable "project_id" { description = "GCP project ID where the log is hosted" type = string } variable "base_name" { description = "Base name to use when naming resources" type = string } variable "location" { description = "Location in which to create resources" type = string } variable "env" { description = "Environment name, e.g ci, prod, etc." type = string } variable "server_docker_image" { description = "The full image URL (path & tag) for the Docker image to deploy in Cloud Run" type = string } variable "signer" { description = "The note signer which should be used to sign checkpoints" type = string } variable "cloudrun_service_account" { description = "The service account email to use for the CloudRun instance. If unset, uses the project default service account." type = string } variable "conformance_writers" { description = "The list of users allowed to invoke HTTP calls to the conformance Cloud Run instance. If unset, only the project default service account will be able to send requests." type = list(any) } variable "conformance_readers" { description = "The list of users allowed to read the conformance t-log resources from GCS. If unset, only the project default service account will be able to read the t-log contents." type = list(any) } variable "enable_antispam" { description = "Set to true to enable persistent antispam feature on conformance instance." type = bool } transparency-dev-tessera-3cb22ee/deployment/modules/gcp/gcs/000077500000000000000000000000001511600621500243045ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/deployment/modules/gcp/gcs/main.tf000066400000000000000000000052351511600621500255700ustar00rootroot00000000000000# Services resource "google_project_service" "serviceusage_googleapis_com" { service = "serviceusage.googleapis.com" disable_on_destroy = false } resource "google_project_service" "storage_api_googleapis_com" { service = "storage-api.googleapis.com" disable_on_destroy = false } resource "google_project_service" "storage_component_googleapis_com" { service = "storage-component.googleapis.com" disable_on_destroy = false } resource "google_project_service" "storage_googleapis_com" { service = "storage.googleapis.com" disable_on_destroy = false } resource "google_project_service" "spanner_api" { service = "spanner.googleapis.com" disable_on_destroy = false } ## Resources # Buckets resource "google_storage_bucket" "log_bucket" { name = "${var.project_id}-${var.base_name}-bucket" location = var.location storage_class = "STANDARD" uniform_bucket_level_access = true force_destroy = var.ephemeral } resource "google_storage_bucket_iam_binding" "log_bucket_reader" { bucket = google_storage_bucket.log_bucket.name role = "roles/storage.objectViewer" members = var.bucket_readers } resource "google_storage_bucket_iam_binding" "log_bucket_writer" { bucket = google_storage_bucket.log_bucket.name role = "roles/storage.legacyBucketWriter" members = var.log_writer_members } # Spanner resource "google_spanner_instance" "log_spanner" { name = var.base_name config = "regional-${var.location}" display_name = var.base_name processing_units = 100 force_destroy = var.ephemeral depends_on = [ google_project_service.spanner_api, ] } resource "google_spanner_database" "log_db" { instance = google_spanner_instance.log_spanner.name name = var.base_name deletion_protection = !var.ephemeral } resource "google_spanner_database" "log_antispam_db" { count = var.create_antispam_db ? 1 : 0 instance = google_spanner_instance.log_spanner.name name = "${var.base_name}-antispam" deletion_protection = !var.ephemeral } resource "google_spanner_database_iam_binding" "database" { instance = google_spanner_instance.log_spanner.name database = google_spanner_database.log_db.name role = "roles/spanner.databaseAdmin" members = var.log_writer_members } resource "google_spanner_database_iam_binding" "database_antispam" { count = var.create_antispam_db ? 1 : 0 instance = google_spanner_instance.log_spanner.name database = google_spanner_database.log_antispam_db[count.index].name role = "roles/spanner.databaseAdmin" members = var.log_writer_members } transparency-dev-tessera-3cb22ee/deployment/modules/gcp/gcs/outputs.tf000066400000000000000000000005341511600621500263640ustar00rootroot00000000000000output "log_bucket" { description = "Log GCS bucket" value = google_storage_bucket.log_bucket } output "log_spanner_db" { description = "Log Spanner database" value = google_spanner_database.log_db } output "log_spanner_instance" { description = "Log Spanner instance" value = google_spanner_instance.log_spanner } transparency-dev-tessera-3cb22ee/deployment/modules/gcp/gcs/variables.tf000066400000000000000000000021101511600621500266010ustar00rootroot00000000000000variable "project_id" { description = "GCP project ID where the log is hosted" type = string } variable "base_name" { description = "Base name to use when naming resources" type = string } variable "location" { description = "Location in which to create resources" type = string } variable "env" { description = "Unique identifier for the env, e.g. ci or prod" type = string } variable "bucket_readers" { description = "List of identities allowed to read the log bucket" type = list(any) default = ["allUsers"] } variable "log_writer_members" { description = "List of identities in member format allowed to write to the log" type = list(any) } variable "create_antispam_db" { description = "Set to true to create the infrastructure required by the GCP antispam implementation." type = bool } variable "ephemeral" { description = "Set to true if this is a throwaway/temporary log instance. Will set attributes on created resources to allow them to be disabled/deleted more easily." type = bool } transparency-dev-tessera-3cb22ee/docs/000077500000000000000000000000001511600621500200575ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/docs/design/000077500000000000000000000000001511600621500213305ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/docs/design/README.md000066400000000000000000000152031511600621500226100ustar00rootroot00000000000000# Design Docs This directory contains design documentation for Tessera. It's probably wise to start with the [philosophy](philosophy.md) doc first in order to establish the context around the approach and design trade-offs made herein. The following documents are available: - [philosophy.md](./philosophy.md): The guiding principles and rationale behind Tessera's design. - [lifecycle.md](./lifecycle.md): The various states a Tessera log can be in. - [antispam.md](./antispam.md): How Tessera handles duplicate entries. - [performance.md](../performance.md): A high-level overview of the performance characteristics of the different storage backends. ## Objective Tessera is a library for managing Merkle trees & building transparency applications. It is designed to embody and guide users towards the current best practices developed over time through the implementation and operation of transparency systems at scale. ### Goals Primary goals are to: 1. Take a simpler MVP approach compared to Trillian v1. 1. Correctly, reliably, and cheaply manage Merkle trees. 1. Enable users of the library to build safe transparency systems, conforming to the [tlog-tiles][] spec (or the proposed new version of CT described by the [static-ct-api][] spec). + Tessera means _"An individual tile, used in creating a mosaic"_!) 1. Reduce TCO of operating logs compared to Trillian v1. ### Non-goals: 1. Multi-tenancy. 1. Global "strong" deduplication of entries. ### Notable differences from Trillian v1 While there are many similarities with Trillian v1 at a high level (e.g. they both manage t-logs, they both utilise tile structures for storing t-log data, etc.), Tessera approaches the problem in quite a different want. The major changes from Trillian v1 are that Tessera: * Is a library rather than a collection of microservices. * Is strongly opinionated on how t-logs are to be exposed and served. + For example, Tessera will only support building t-logs which conform to [tlog-tiles][] or [static-ct-api][] specs, and enforces the use of [tlog-checkpoint][] spec commitments to the state of the log. * Has a clear scope of responsibility: Tessera is only concerned with managing t-logs, anything beyond that is the responsibility of the application. * Implements its storage layer much more "natively"; a much simpler abstraction which pushes more responsibility (and thus flexibility) down into the storage implementations. Details on the rationale behind the trade-offs and principles which shape Tessera are provided in the [philosophy](philosophy.md) doc. ## Overview Tessera is intended to be used _to build_ other, transparency related, applications, and as such does not provide production binaries to run, but rather: * Data storage and management libraries * including support libraries for authors of Tessera storage implementations * Application support libraries ("PersonalityKit") * e.g. to help authors of applications which manage t-log instances, or to help authors whose applications need to fetch data from t-logs, or build & verify proofs, etc. ```mermaid block-beta columns 1 block:Application["Application"]:1 columns 2 space space space space PersonalityKit("Tessera PersonalityKit") end TLog["Tessera Log API"] block:StorageImpl["Storage"]:1 columns 2 space:4 tlibs("Tessera StorageKit") ilibs("Infra libs") end Infra["Storage infra"]:1 style Application fill:#ed7, stroke:#000 style PersonalityKit fill:#7eb, stroke:#000 style TLog fill:#7eb, stroke:#000 style StorageImpl fill:#e88, stroke:#000 style tlibs fill:#7eb, stroke:#000 style ilibs fill:#89e, stroke:#000 style Infra fill:#89e, stroke:#000 ``` ## Storage The data managed by Tessera which represents the t-log state is persisted using one of the concrete storage implementations are provided by this library. These particular implementations were selected due to expressed demand from existing Trillian users we interviewed, but others can easily be constructed by third parties. Each of the provided storage implementations has an associated "one-pager" design doc which offers a detailed explanation of how it works: * [GCP](/storage/gcp/README.md) * [MySQL](/storage/mysql/DESIGN.md) * [POSIX filesystem](/storage/posix/README.md) Every storage implementation is required to expose a small number of fairly high-level APIs which support the modes listed in the [lifecycle doc](./lifecycle.md). These storage APIs are intended to be high-level enough to allow implementations flexibility to act in the most native way for the infrastructure they're targeting, rather than assuming e.g. a transactional model which can rule out entire classes of storage or make optimisation difficult/impossible. The individual lifecycle APIs are covered below, but these more broad general guidelines apply to all storage implementations: * If the storage infrastructure is capable of being directly exposed to client (e.g. S3/GCS via HTTP, etc.), the Merkle tree internal node data and leaves MUST be stored according to the [tlog-tiles][] or [static-ct-api][] specs. * The small number of "With Options" defined by Tessera (e.g. `WithBatching`, `WithCheckpointSigner`, etc.) SHOULD be supported where possible. ### [`Appender`](./lifecycle.md#appender) Lifecycle This lifecycle mode is the typical "left-dense, right-hand append only" mode we typically think of as append-only logs. #### [`Add`](https://pkg.go.dev/github.com/transparency-dev/tessera#AddFn) *Durably* assigns an entry to an index in the log, and returns an [`IndexFuture`](https://pkg.go.dev/github.com/transparency-dev/tessera#IndexFuture) which will resolve either to the assigned index or an error. Implementations MUST NOT return a future which resolves to an index value which is not yet durably flushed/committed to the storage. To give some freedom for performance trade-offs, implementations MAY delay and group concurrent calls in this method in order to take advantage of batching strategies. While implementations can chose to integrate entries into the log by the time the future resolves, implementations MAY perform integration asynchronously but SHOULD target integrating such entries into the log within low single-digit seconds. #### [`LogReader`](https://pkg.go.dev/github.com/transparency-dev/tessera#LogReader) This interface is intended to provide the application with access to static log resources, e.g. for building proofs to be returned to the caller, using entries committed to by the log to derive other data structures (e.g. a verifiable index), etc. [tlog-tiles]: https://c2sp.org/tlog-tiles [tlog-checkpoint]: https://c2sp.org/tlog-checkpoint [static-ct-api]: https://c2sp.org/static-ct-api transparency-dev-tessera-3cb22ee/docs/design/antispam.md000066400000000000000000000161531511600621500234740ustar00rootroot00000000000000# Tessera Anti-spam Tessera will undoubtedly be used to build transparency systems which have publicly available write endpoints, e.g. Certificate Transparency. With such systems, there is usually an accompanying risk that, whether by accident or malice, the write endpoints will be misused somehow - likely by resubmitting entries which are already present in the log. From a transparency perspective, this is unlikely to be a problem - an entry is _discoverable_ if it's present in a log; this property doesn't diminish if there are further copies of it the log. However, particularly for large scale logs, there is a potentially significant cost to both the log operator (in terms of egress & storage), and to monitors/verifiers within the ecosystem (in terms of ingress, and possibly compute & storage, too) to allowing the unfetter addition of duplicate entries to a log. It would, therefore, be preferable to try to minimise the number of duplicate entries, and this is what Tessera's anti-spam mechanism is intended to do. Tessera anti-spam provides _best effort_ support for minimising duplicate entries submitted to a log. ## Non-goals Tessera's anti-spam mechanisms are _explicitly not_ intended to provide strong/atomic deduplication, nor any guarantee about uniqueness of entries in a log. These properties are not generally required, are expensive, and have a profound impact on log performance. If your transparency application needs such properties you will need to handle this in the personality. ## Overview The anti-spam support is optional, and needs to be explicitly provisioned in the infrastructure and enabled by the personality application (via the [`WithAntispam`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#AppendOptions.WithAntispam)) option). The following diagram gives a high-level overview of how it works. Across the top, we see the chain of _decorators_ ultimately leading to the storage implementation's `Append` func. The anti-spam implementation is part of this chain, but only performs _lookups_ here, short-circuiting and returning a previously assigned index if match is found. The anti-spam index data is populated asynchronously, by "following" (or "tailing") the contents of the log, and adding index information for new entries as they appear. ```mermaid architecture-beta group dedup[Antispam implementation] group appender[Storage Appender] group follower[Log Follower] service app(server)[Application] service inmem(server)[In memory dedup] in dedup service persistent(server)[Persistent dedup] in dedup service dedupdata(disk)[Persistent index] in dedup service populater(server)[Persistent index populater] in dedup service append(server)[Append] in appender service integrate(server)[Integrate] in appender service bundles(disk)[Entry bundles] in appender junction f1 in follower junction f2 in follower app:R --> L:inmem inmem:R --> L:persistent persistent:R --> L:append append:R --> L:integrate persistent:B -[Lookup]-> T:dedupdata populater:T -[Insert]-> B:dedupdata integrate:B -[Write]-> T:bundles bundles:B -[Read]- T:f2 f2:L -- R:f1 f1:L --> R:populater ``` If the process of following the log falls too far behind a configurable threshold (e.g. on [GCP's antispam](https://pkg.go.dev/github.com/transparency-dev/tessera@main#AppendOptions.WithPushback) implementation), the decorator will start returning `ErrPushback` to the application until such time as the follower has caught up. This helps to prevent the anti-spam mechanism getting so far behind as to become ineffective in preventing abuse. ## Threats / failure scenarios ### Threats: 1. Stuttering but non-malicious submission client 2. Malicious bot (e.g. feeding log contents back into the log) ### Failure scenarios: 1. Normal operation 2. Degraded anti-spam 3. Degraded integration As currently implemented, the Tessera anti-spam implementation consists of a layered defense: 1. An in-memory deduplication cache, followed by: 2. A persistent deduplication index. This gives fast and cheap protection against (T1) in all failure scenarios, and falls back to the more costly interaction with the persistent storage where necessary. (T2) is expected to be rarer, but given that the approach is to offer best effort protection, it's important to think through how this threat will be handled in various failure cases; we would prefer that anti-spam does not fail to block malicious submissions exactly when we're currently overwhelmed, for example. > [!Note] > Note that, if running multiple personality frontents for redundancy purposes, a rapid round-robin submission of > duplicate entries can result in a limited number of these duplicates (1 per frontend) being permitted into the log until > the persistent deduplication index has caught up. ## Storage considerations Tessera offers a couple of infrastructure-specific anti-spam modules (initially for AWS and GCP, but others may also be made available in response to demand or contributions). It's expected that most anti-spam persistence implementations are going to use some sort of transactional storage (whether KV or relational). While the exact behaviour of individual storage infrastructure will naturally differ, in general, the performance of these storage engines tends to degrade in the face of many competing transactions/updates, and the overhead of attempting single-row inserts tends to push towards batching. However, the nature of the anti-spam storage is mapping uniformly distributed keys (`SHA256` identity hashes) to indices in the log (`uint64` values), which means that any two batches are likely to contend for range locks. Given these constraints, a _Follower_ approach, where we closely tail the log as it grows, using entries we find to populate the anti-spam index will perform better than, e.g., attempting to update the index during highly concurrent calls to `Add()` or `SetEntryBundle()`. This is borne out by experimental evidence; early tests on GCP show that this approach delivers twice the throughput (tested with 10% dupe traffic) compared with the "competing batched updates" approach briefly described above. ## Tuning ### In-memory cache size Ideally, the in-memory anti-spam decorator should have a sufficiently large cache to cover the window before newly added entries are seen by the follower and added to the persistent anti-spam storage. Fortunately, anti-spam index entries are `32+8` bytes plus overhead, so having even a very large cache depth of 100's of 1000's of entries is not expected to be a problem. The `tessera.WithAntispam` option allows the capacity of the in-memory cache to be configured, and internally ensures that the in-memory cache and persistent anti-spam index are applied in the correct order. ### Persistent index Applications should configure the push-back threshold according to their expected throughput & log performance numbers. A reasonable starting point is probably a few seconds' worth of the peak expected log growth rate. The in-memory cache should also be configured to be _at least_ this size. Individual implementations may have other tunable options to help tailor the behaviour to the expected load. transparency-dev-tessera-3cb22ee/docs/design/lifecycle.md000066400000000000000000000133361511600621500236170ustar00rootroot00000000000000# Log Lifecycle Log lifecycle is a useful concept for outlining expected modes the log can be in, and state transitions between these states. The lifecycle states outlined below exist either as explicit API constructs or as internal states, but there is no explicit state stored with the log data. The requirements for each state are documented, and the log operator takes responsibility to ensure that they only migrate between these states in the supported directions. [![](https://mermaid.ink/img/pako:eNptkj9vwyAQxb8KYqzMkpFKlSp5TIa2W0sHCuf4VHxYcG4aRfnuJXYcJ1WYuPd7B48_B-miB6mlUsoQIwfQYo0NuL0LYGiUM1uGGu022U79rAyJMjwmcIyRxPr1cZJcsDnX0AgkhkQ2iCYSq8z7siqyDej-O22Pk2kHuG1Zf8Xgq8wpfoPaoedWr_rf0jS1fTx8CqWexHPfA3lIt-oGS8BTotl-EUb8MiBkB8QTnNe4xy7lnd1uWQ0BGPyEzsUISqg5xnjYq775dq7p3HqPXZKWy7rWl-ONwJCsZAeps-jLgx5OViO5hQ6M1GXqobFDYCMNHYvVDhzf9uSk5jRAJYfeL88sdWNDLip45Jg20ycZ_0ole0vvMS6eFIdte66Of4uAxnk?type=png)](https://mermaid.live/edit#pako:eNptkj9vwyAQxb8KYqzMkpFKlSp5TIa2W0sHCuf4VHxYcG4aRfnuJXYcJ1WYuPd7B48_B-miB6mlUsoQIwfQYo0NuL0LYGiUM1uGGu022U79rAyJMjwmcIyRxPr1cZJcsDnX0AgkhkQ2iCYSq8z7siqyDej-O22Pk2kHuG1Zf8Xgq8wpfoPaoedWr_rf0jS1fTx8CqWexHPfA3lIt-oGS8BTotl-EUb8MiBkB8QTnNe4xy7lnd1uWQ0BGPyEzsUISqg5xnjYq775dq7p3HqPXZKWy7rWl-ONwJCsZAeps-jLgx5OViO5hQ6M1GXqobFDYCMNHYvVDhzf9uSk5jRAJYfeL88sdWNDLip45Jg20ycZ_0ole0vvMS6eFIdte66Of4uAxnk) ## Terminology The definitions below will use terms that we'll define here: - pending: an entry which has been submitted to the log and durably assigned a sequence number, but which has not yet been integrated. - integrated: a sequenced entry that has been included in the Merkle tree, and a checkpoint committing to it has been created. ## Modes ### `Appender` The purpose of this mode is to allow entries to be assigned indices by, and integrated into, the log. This is the "normal" state of most active logs, and is characterized by the writer personality using only the [`tessera.NewAppender`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#NewAppender) lifecycle API. In this mode, storage drivers: - durably assign and return sequence numbers for entries passed via the `Add` method on the `Appender` struct. - integrate entries with sequence numbers into the tree, possibly asynchronously. - periodically publish a new signed `checkpoint` file which commits to the contents of the tree. This state can start from an empty tree, or from the `Quiescent` state, in which case there can be any number of entries already in the tree, but they must all be integrated. The only valid transition outwards is to [`Quiescent`](#Quiescent), and the operator should ensure that new entries are not being accepted and all pending entries have been integrated before transitioning. ### `Migration` This mode is used to "import" [tlog-tiles] (or [static-ct]) compliant logs into a Tessera instance. The most common use case for this mode is migrating a log you operate between different supported Tessera infrastructure. This state must start from an empty tree. It is characterized by the personality that uses only the [`tessera.NewMigrationTarget`](https://pkg.go.dev/github.com/transparency-dev/tessera@main#MigrationTarget) lifecycle API. In this mode, the lifecycle struct will copy entry bundle resources from the source log into the target log, and locally re-create the merkle tree which commits to them. The resulting root hash should match the root hash of the source log at the same tree size. > [!Note] > This lifecycle mode _does not_, by design, create or publish `checkpoint` files. > > For logs being migrated between infrastructure, it is of paramount importance that only one of two logs is canonical > at any given time in order to avoid inadvertently creating a fork. > Consequently, Tessera will only create a new `checkpoint` file once the operator has completed the migration of the > source data into the target log, verified its correctness, and manually switched over to `Appender` mode on the target > log. The only valid transition outwards is to [`Quiescent`](#Quiescent), and the operator should ensure that the migration operation has completed successfully before transitioning. ### `Quiescent` The purpose of this conceptual mode is to provide a safe intermediate state where the tree and all associated metadata is up-to-date and self-consistent. This state may be considered as terminal, e.g. if the log is being frozen or retired. This state requires [`Migration`](#Migration) or [`Appender`](#Appender) first. No calls which can modify state should be made while in this mode. The only valid transitions are to [`Appending`](#Appending) (e.g. when a log migration is complete and the log should become operational in its new location), or [`Deleted`](#Deleted). ### `Deleted` Each storage implementation will define instructions for deleting the contents of the log when no longer required. It can only be reached from the [`Quiescent`](#Quiescent) state. The concept of a soft-delete is not supported by Tessera, though the deployer may be able to realize this via their own infrastructure (e.g. by deleting URL mappings to the log handlers). ## Lifecycle in Trillian v1 This lifecycle proposal was inspired by Trillian v1, but simplified as much as possible. For comparative purposes, the states possible in Trillian are documented below. Using Trillian log [TreeState](https://github.com/google/trillian/blob/master/trillian.proto#L66) and [TreeType](https://github.com/google/trillian/blob/master/trillian.proto#L92) as inspiration, the largest conceivable lifecycle is: - No log / unknown - Empty log - Pre-ordered - Active - Draining (this is the only state that allows a back-transition. This can move back to Active) - Frozen - No log (deleted) --- [tlog-tiles]: https://c2sp.org/tlog-tiles [static-ct]: https://c2sp.org/static-ct transparency-dev-tessera-3cb22ee/docs/design/philosophy.md000066400000000000000000000145541511600621500240610ustar00rootroot00000000000000 ## Objective This document explains the rationale behind some of the philosophy and design choices underpinning Tessera. ## Simplicity Tessera is intended to be: simple to use, adopt, and maintain; and cheaper/easier to operate than Trillian v1. There are many tensions and trade-offs here, and while there is no guarantee that a single "right answer" exists, we are shooting for a MVP, and must hold ourselves accountable whenever we're adding cost, complexity, or [speculative abstractions](https://100go.co/#interface-pollution-5) - _"is the driver for this something we *really need now*?", or otherwise restricting our ability to make large internal changes in the future. ## Multi-implementation storage Each storage implementation for Tessera is independently implemented, and takes the most "native" approach for the infrastructure it targets. Trillian v1 defined `LogStorage` and embedded `TreeStorage` interfaces which all storage implementations had to implement. These interfaces were created early, reflected implementation details of a small sampling of largely similar storage implementations, and consequently turned out not to be a clean abstraction of what was _actually_ desired by higher levels in the stack. In turn, this made it hard to: 1. Support non-single-domain/non-transactional storage implementations, and 2. Refactor storage internals to improve performance. With Tessera, we are learning from these mistakes, and acknowledging that: 1. The different storage implementations we are building now, and those which will come in the future, have their own unique properties which stem from the infrastructure they're built upon - e.g. _some_ infrastructure offers rich transactional semantics over multiple entities, others offer only check-and-set semantics. 2. We don't _necessarily_ need to use the more expensive transactional storage to serve reads. 3. Prematurely binding different storage implementations together (e.g. through inappropriate code reuse, interfaces, structures, etc.) which _appear_ similar today can lead to headaches later if we find we need to make structural changes. For at least the early versions of Tessera, it is an explicit non-goal to try to reuse code between storage implementations. Attempting to do this so early in the project lifecycle opens us up to the same pitfalls described above, and any perceived benefits from this approach are unlikely to be worth the risk; storage implementations are expected to be relatively small in terms of LoC and complexity. ## Asynchronous integration in storage implementation In Trillian v1, the only supported mechanism for adding entries to a log was via a fully decoupled queue: the caller requesting the addition of the entry was given nothing more than a timestamp and a promise that the entry would be integrated at some point (note that 24h is the CT _policy_, but there's no specific parameter or deadline in Trillian itself - it's _"as soon as possible"_). With Tessera, we're tightening the storage contract up so that calls to add entries to the log will return with a durably assigned sequence number, or an error. It's not a requirement that the Merkle tree has already been extended to cryptographically commit to the new leaf by the time the call to add returns, although it _is_ expected that this process will take place within a short window (e.g. seconds). This API represents a reasonable set of tradeoffs: 1. Keeping sequencing and integration separate enables: 1. Storage to be implemented in the way which works best given the particular constraints of that infrastructure 2. Higher write-throughput * E.g. A Bucket-type storage typically has roundtrip read/write latency which is far higher than DBMS. From our experiments, typically, a transactional DBMS is used for coordination and sequencing, and the slower, cheaper, bucket storage is used for serving the tree. Sequencing, which has typically been done within the DBMS only, is fast. Integration, however, must update the buckets with new tree structure, leaves, checkpoint, etc., and is by far the slower operation of the two. Coupling <sequence>-<integrate> within a call to add entries to the log (even if batched via a local pool in a single frontend), requires blocking updates to the tree for the long-pole integration duration. Allowing <sequence> operations to happen asynchronously to <integration> enables sequencing to proceed from multiple frontend pools concurrently with any integration operation (at least until some back-pressure is applied), which, in turn, allows the <integration> operation to potentially amortise the long-pole cost over a larger number of sequenced entries. 2. Limiting the intended window between <sequence> and <integrate> operations to a low single-digit-seconds target enables a synchronous add API to be constructed at the layer above (i.e within the "personality" that is built using Tessera). This approach enables: 1. synchronous "personalities" to benefit from improved write-throughput (compared with a naive <sequence>-<integrate> approach) at the cost of a small increase of latency 2. Other "personalities" are not forced to pay the cost of the synchronous operation they do not require. Back-pressure for `Writer` requests to the storage implementation will be important to maintain the very short window between sequence numbers being assigned to entries, and those entries being committed to by the Merkle tree. ## Resilience and availability Storage implementations should be implemented in such a way that multiple instances of a Tessera-backed personality can *safely* be run concurrently for any given log storage instance. The manner in which this is achieved is left for each storage implementation to decide, allowing for the simplest/most infrastructure-native mechanism to be used in each case. Having this property offers two benefits: * Tessera-backed logs can offer comparable availability as a similar log being managed by Trillian v1 on the same infrastructure would have. * Safety guard rails are in place against "silly" mistakes, such as "<up><up><enter>" and _copy-n-paste_ errors, resulting in accidentally launching multiple instances pointing at the same storage configuration. transparency-dev-tessera-3cb22ee/docs/performance.md000066400000000000000000000237701511600621500227130ustar00rootroot00000000000000# Tessera Storage Performance Tessera is designed to scale to meet the needs of most currently envisioned workloads in a cost-effective manner. All storage backends have been tested to meet the write-throughput of CT-scale loads without issue. The read API of Tessera based logs scales extremely well due to the immutable resource based approach used, which allows for: 1. Aggressive caching to be applied, e.g. via CDN 2. Horizontal scaling of read infrastructure (e.g. object storage)[^1] [^1]: The MySQL storage backend is different to the others in that reads must be served via the personality rather than directly, however, due to changes in how MySQL is used compared to Trillian v1, read performance should be far better, and _could_ still be scaled horizontally with additional MySQL read replicas & read-only personality instances. Below are some indicative figures which show the rough scale of performance we've seen from deploying Tessera conformance binaries in various environments. ## Performance factors ### Resources Exact performance numbers are highly dependent on the exact infrastructure being used (e.g. storage type & locality, host resources of the machine(s) running the personality binary, network speed and weather, etc.) If in doubt, you should run your own performance tests on infrastructure which is as close as possible to that which will ultimately be used to run the log in production. The [conformance binaries](/cmd/conformance) and [hammer tool](/internal/hammer) are designed for this kind of performance testing. ### Antispam Antispam is a feature which does best effort deduplication of incoming entries. While cheaper than _strong atomic_ deduplication would be, it is still a somewhat expensive operation in terms of both storage and throughput. Not all personality designs will require it, so Tessera is built such that you only incur these costs if they are necessary for your design. Leaving antispam disabled will greatly increase the throughput of the log, and decrease CPU and storage costs. ## Backends The currently supported storage backends are listed below, with a rough idea of the expected performance figures. Individual storage implementations may have more detailed information about performance in their respective directories. ### GCP The main lever for cost vs performance on GCP is Spanner, in the form of "Performance Units" (PUs). PUs can be allocated in blocks of 100, and 1000 PUs is equivalent to 1 Spanner Server. The table below shows some rough numbers of measured performance: | Spanner PUs | Num FEs | QPS no-antispam | QPS antispam | |-------------|---------|--------------|-----------| | 100 | 1 | > 3,000 | > 800 | | 200 | 1 | not done | > 1500 | | 300 | 1 | not done | > 3000 | | 300 | 2 | not done | > 5000 | ### POSIX Performance of the POSIX storage backend is highly dependent on the underlying infrastructure, some representative examples of the performance on different types of infratructure are given below. #### Local storage ##### NVMe The log and hammer were both run in the same VM, with the log using a ZFS subvolume from the NVMe mirror. With antispam enabled, it was able to sustain around 10,000 write qps, using up to 7 cores for the server. ``` ┌───────────────────────────────────────────────────────────────────────────┐ │Read (8 workers): Current max: 20/s. Oversupply in last second: 0 │ │Write (30000 workers): Current max: 10000/s. Oversupply in last second: 0 │ │TreeSize: 5042936 (Δ 10567qps over 30s) │ │Time-in-queue: 1889ms/2990ms/3514ms (min/avg/max) │ │Observed-time-to-integrate: 2255ms/3103ms/3607ms (min/avg/max) │ ├───────────────────────────────────────────────────────────────────────────┤ ``` ##### SAS 12Gb HDD A single local instance on an 12-core VM with 8GB of RAM writing to local filesystem stored on a mirrored pair of SAS disks. Without antispam, it was able to sustain around 2900 writes/s. ``` ┌────────────────────────────────────────────────────────────────────────────────────┐ │Read (8 workers): Current max: 20/s. Oversupply in last second: 0 │ │Write (3000 workers): Current max: 3000/s. Oversupply in last second: 0 │ │TreeSize: 1470460 (Δ 2927qps over 30s) │ │Time-in-queue: 136ms/1110ms/1356ms (min/avg/max) │ │Observed-time-to-integrate: 583ms/6019ms/6594ms (min/avg/max) │ ├────────────────────────────────────────────────────────────────────────────────────┤ ``` With antispam enabled (badger), it was able to sustain around 1600 writes/s. ``` ┌────────────────────────────────────────────────────────────────────────────────────┐ │Read (8 workers): Current max: 20/s. Oversupply in last second: 0 │ │Write (1800 workers): Current max: 1800/s. Oversupply in last second: 0 │ │TreeSize: 2041087 (Δ 1664qps over 30s) │ │Time-in-queue: 0ms/112ms/448ms (min/avg/max) │ │Observed-time-to-integrate: 593ms/3232ms/5754ms (min/avg/max) │ ├────────────────────────────────────────────────────────────────────────────────────┤ ``` #### Network storage A 4 node CephFS cluster (1 admin, 3x storage nodes) running on E2 nodes sustained > 1000qps of writes. #### GCP Free Tier VM Instance A small `e2-micro` free-tier VM is able to sustain > 1500 writes/s using a mounted PersistentDisk to store the log. > [!NOTE] > Virtual CPUs (vCPUs) in virtualized environments often share physical CPU cores with other vCPUs and introduce variability > and potential performance impacts. ``` ┌───────────────────────────────────────────────────────────────────────┐ │Read (184 workers): Current max: 0/s. Oversupply in last second: 0 │ │Write (600 workers): Current max: 1758/s. Oversupply in last second: 0 │ │TreeSize: 1882477 (Δ 1587qps over 30s) │ │Time-in-queue: 149ms/371ms/692ms (min/avg/max) │ │Observed-time-to-integrate: 569ms/1191ms/1878ms (min/avg/max) │ └───────────────────────────────────────────────────────────────────────┘ ``` More details on Tessera POSIX performance can be found [here](/storage/posix/PERFORMANCE.md). ## MySQL Figures below were measured using VMs on GCP in order to provide an idea of size of machine required to achieve the results. > [!NOTE] > Note that for Tessera on GCP deployments, we **strongly recommended* using the Tessera GCP storage implementation instead. ### GCP free-tier + CloudSQL Tessera running on an `e2-micro` free tier VM instance on GCP, using CloudSQL for storage can sustain around 2000 write/s. ``` ┌───────────────────────────────────────────────────────────────────────┐ │Read (8 workers): Current max: 0/s. Oversupply in last second: 0 │ │Write (512 workers): Current max: 2571/s. Oversupply in last second: 0 │ │TreeSize: 2530480 (Δ 2047qps over 30s) │ │Time-in-queue: 41ms/120ms/288ms (min/avg/max) │ │Observed-time-to-integrate: 568ms/636ms/782ms (min/avg/max) │ └───────────────────────────────────────────────────────────────────────┘ ``` ### GCP free-tier VM only Tessera + MySQL both running on an `e2-micro` free tier VM instance on GCP can sustain around 300 writes/s. ``` ┌──────────────────────────────────────────────────────────────────────┐ │Read (8 workers): Current max: 0/s. Oversupply in last second: 0 │ │Write (256 workers): Current max: 409/s. Oversupply in last second: 0 │ │TreeSize: 240921 (Δ 307qps over 30s) │ │Time-in-queue: 86ms/566ms/2172ms (min/avg/max) │ │Observed-time-to-integrate: 516ms/1056ms/2531ms (min/avg/max) │ └──────────────────────────────────────────────────────────────────────┘ ``` More details on Tessera MySQL performance can be found [here](/storage/mysql/PERFORMANCE.md). ## AWS Coming soon. transparency-dev-tessera-3cb22ee/entry.go000066400000000000000000000063271511600621500206270ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package tessera provides an implementation of a tile-based logging framework. package tessera import ( "encoding/binary" "github.com/transparency-dev/merkle/rfc6962" ) // Entry represents an entry in a log. type Entry struct { // We keep the all data in exported fields inside an unexported interal struct. // This allows us to use gob to serialise the entry data (relying on the backwards-compatibility // it provides), while also keeping these fields private which allows us to deter bad practice // by forcing use of the API to set these values to safe values. internal struct { Data []byte Identity []byte LeafHash []byte Index *uint64 } // marshalForBundle knows how to convert this entry's Data into a marshalled bundle entry. marshalForBundle func(index uint64) []byte } // Data returns the raw entry bytes which will form the entry in the log. func (e Entry) Data() []byte { return e.internal.Data } // Identity returns an identity which may be used to de-duplicate entries and they are being added to the log. func (e Entry) Identity() []byte { return e.internal.Identity } // LeafHash is the Merkle leaf hash which will be used for this entry in the log. // Note that in almost all cases, this should be the RFC6962 definition of a leaf hash. func (e Entry) LeafHash() []byte { return e.internal.LeafHash } // Index returns the index assigned to the entry in the log, or nil if no index has been assigned. func (e Entry) Index() *uint64 { return e.internal.Index } // MarshalBundleData returns this entry's data in a format ready to be appended to an EntryBundle. // // Note that MarshalBundleData _may_ be called multiple times, potentially with different values for index // (e.g. if there's a failure in the storage when trying to persist the assignment), so index should not // be considered final until the storage Add method has returned successfully with the durably assigned index. func (e *Entry) MarshalBundleData(index uint64) []byte { e.internal.Index = &index return e.marshalForBundle(index) } // NewEntry creates a new Entry object with leaf data. func NewEntry(data []byte) *Entry { e := &Entry{} e.internal.Data = data h := identityHash(e.internal.Data) e.internal.Identity = h[:] e.internal.LeafHash = rfc6962.DefaultHasher.HashLeaf(e.internal.Data) // By default we will marshal ourselves into a bundle using the mechanism described // by https://c2sp.org/tlog-tiles: e.marshalForBundle = func(_ uint64) []byte { r := make([]byte, 0, 2+len(e.internal.Data)) r = binary.BigEndian.AppendUint16(r, uint16(len(e.internal.Data))) r = append(r, e.internal.Data...) return r } return e } transparency-dev-tessera-3cb22ee/entry_test.go000066400000000000000000000021571511600621500216630ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "bytes" "fmt" "testing" ) func TestEntryMarshalBundleDelegates(t *testing.T) { const wantIdx = uint64(143) wantBundle := fmt.Appendf(nil, "Yes %d", wantIdx) e := NewEntry([]byte("this is data")) e.marshalForBundle = func(gotIdx uint64) []byte { if gotIdx != wantIdx { t.Fatalf("Got idx %d, want %d", gotIdx, wantIdx) } return wantBundle } if got, want := e.MarshalBundleData(wantIdx), wantBundle; !bytes.Equal(got, want) { t.Fatalf("Got %q, want %q", got, want) } } transparency-dev-tessera-3cb22ee/fsck/000077500000000000000000000000001511600621500200555ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/fsck/fsck.go000066400000000000000000000312431511600621500213350ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package fsck import ( "bytes" "context" "crypto/sha256" "encoding/base64" "fmt" "strings" "sync/atomic" "github.com/transparency-dev/merkle/compact" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/client" "golang.org/x/mod/sumdb/note" "golang.org/x/sync/errgroup" "k8s.io/klog/v2" ) // Fetcher describes a struct which knows how to retrieve tlog-tiles artifacts from a log. type Fetcher interface { ReadCheckpoint(ctx context.Context) ([]byte, error) ReadTile(ctx context.Context, l, i uint64, p uint8) ([]byte, error) ReadEntryBundle(ctx context.Context, i uint64, p uint8) ([]byte, error) } // Fsck knows how to check the integrity of tlog-tile logs. type Fsck struct { origin string verifier note.Verifier fetcher *countingFetcher bundleHasher func([]byte) ([][]byte, error) rangeTracker *rangeTracker opts Opts fsckTree *fsckTree } type Opts struct { N uint } // New creates a new Fsck instance configured for a particular log. // // The log resources will be retrieved via the provided fetcher, using the provided // bundleHasher to parse and convert entries from the log's entry bundles into leaf hashes. // // The leaf hashes are used to: // 1. re-construct the root hash of the log, and compare it against the value in the log's checkpoint // 2. re-construct the internal tiles of the log, and compare them against the log's tile resources. // // The checking will use the provided N parameter to control the number of concurrent workers undertaking // this process. func New(origin string, verifier note.Verifier, f Fetcher, bundleHasher func([]byte) ([][]byte, error), opts Opts) *Fsck { if opts.N == 0 { opts.N = 1 } return &Fsck{ origin: origin, verifier: verifier, fetcher: newCountingFetcher(f), opts: opts, bundleHasher: bundleHasher, } } // Check performs an integrity check against the log. // // Check may only be called once per instance of Fsck. func (f *Fsck) Check(ctx context.Context) error { cp, cpRaw, _, err := client.FetchCheckpoint(ctx, f.fetcher.ReadCheckpoint, f.verifier, f.origin) if err != nil { return fmt.Errorf("failed to fetch and verify checkpoint: %v", err) } klog.Infof("Fsck: checking log of size %d", cp.Size) klog.V(1).Infof("Fsck: checkpoint:\n%s", cpRaw) f.rangeTracker = newRangeTracker(cp.Size) f.fsckTree = &fsckTree{ fetcher: f.fetcher, bundleHasher: f.bundleHasher, tree: (&compact.RangeFactory{Hash: rfc6962.DefaultHasher.HashChildren}).NewEmptyRange(0), sourceSize: cp.Size, pendingTiles: make(map[compact.NodeID]*api.HashTile), expectedResources: make(chan resource, f.opts.N), rangeTracker: f.rangeTracker, } // Set up a stream of entry bundles from the log to be checked. eg := errgroup.Group{} // Kick off resource comparing workers for range f.opts.N { eg.Go(f.fsckTree.resourceCheckWorker(ctx)) } trackBundle := func(ctx context.Context, idx uint64, p uint8) ([]byte, error) { f.fsckTree.rangeTracker.Update(-1, idx, Fetching) r, err := f.fetcher.ReadEntryBundle(ctx, idx, p) if err != nil { f.fsckTree.rangeTracker.Update(-1, idx, FetchError) return nil, err } f.fsckTree.rangeTracker.Update(-1, idx, Fetched) return r, nil } getSize := func(_ context.Context) (uint64, error) { return cp.Size, nil } // Consume the stream of bundles to re-derive the other log resources. // TODO(al): consider chunking the log and doing each in parallel. for b, err := range client.EntryBundles(ctx, f.opts.N, getSize, trackBundle, 0, cp.Size) { if err != nil { return fmt.Errorf("error while streaming bundles: %v", err) } f.fsckTree.rangeTracker.Update(-1, b.RangeInfo.Index, OK) if err := f.fsckTree.AppendBundle(b.RangeInfo, b.Data); err != nil { return fmt.Errorf("failure calling AppendBundle(%v): %v", b.RangeInfo, err) } if f.fsckTree.tree.End() >= cp.Size { break } } // Ensure we see process any partial tiles too. f.fsckTree.flushPartialTiles() // Signal that there will be no more resource checking jobs coming so workers can exit when the channel is drained. close(f.fsckTree.expectedResources) // Wait for all the work to be done. if err := eg.Wait(); err != nil { return fmt.Errorf("failed: %v", err) } // Finally, check that the claimed root hash matches what we calculated. gotRoot, err := f.fsckTree.GetRootHash() switch { case err != nil: return fmt.Errorf("failed to calculate root: %v", err) case !bytes.Equal(gotRoot, cp.Hash): return fmt.Errorf("calculated root %x, but checkpoint claims %x", gotRoot, cp.Hash) default: klog.Infof("Successfully fsck'd log with size %d and root %s (%x)", cp.Size, base64.StdEncoding.EncodeToString(gotRoot), gotRoot) } return nil } // Status represents the current status of an ongoing fsck check. type Status struct { // EntryRanges describes the status of the entrybundles in the target log. EntryRanges []Range // TileRanges describes the status of the tiles in the target log. // The zeroth entry in the slice represents the lower-most tile level, just above the entry bundles. TileRanges [][]Range // BytesFetched is the total number of bytes fetched from the target log since the last time Status() was called. BytesFetched uint64 // ResourcesFetched is the total number of resources fetched from the target log since the last time Status() was called. ResourcesFetched uint64 // ErrorsEncountered is the total number of errors encountered since the last time Status() was called. ErrorsEncountered uint64 } // Status returns a struct representing the current status of the fsck operation. func (f *Fsck) Status() Status { if f.rangeTracker == nil { return Status{} } e, t := f.rangeTracker.Ranges() r := Status{ EntryRanges: e, TileRanges: t, BytesFetched: f.fsckTree.fetcher.bytesFetched.Swap(0), ResourcesFetched: f.fsckTree.fetcher.resourcesFetched.Swap(0), ErrorsEncountered: f.fsckTree.fetcher.errorsEncountered.Swap(0), } return r } func (s Status) String() string { ret := []string{} for i := len(s.TileRanges) - 1; i >= 0; i-- { l := []string{} for _, tr := range s.TileRanges[i] { l = append(l, tr.String()) } ret = append(ret, fmt.Sprintf("Tiles/%d: ", i)) ret = append(ret, strings.Join(l, ", ")) } l := []string{} for _, tr := range s.EntryRanges { l = append(l, tr.String()) } ret = append(ret, "EntryBdl: ") ret = append(ret, strings.Join(l, ", ")) return strings.Join(ret, "\n") } // resource represents a single static tile resource on the log, and the derived content we expect it to contain. type resource struct { level, index uint64 partial uint8 content []byte } // fsckTree represents the tree we're currently checking. type fsckTree struct { // fetcher knows how to retrieve static tlog-tile resources. fetcher *countingFetcher // bundleHasher knows how to convert entry bundles into leaf hashes. bundleHasher func([]byte) ([][]byte, error) // tree contains the running state of the leaves we've appended so far. tree *compact.Range // sourceSize is the size of the source log we're checking. sourceSize uint64 // pendingTiles holds tlog-tile structs which we are currently populating, but which we haven't yet // verified. // Entries are removed from this map once they either a) become fully populated, or b) flushPartialTiles is called. pendingTiles map[compact.NodeID]*api.HashTile // expectedResources is a channel of derived tlog resources which need to be verified against the source log's static resources. // Entries in this channel are consumed by the resoruceCheckWorker functions. expectedResources chan resource rangeTracker *rangeTracker } // AppendBundle appends leaf hashes from the provided entry bundle. func (f *fsckTree) AppendBundle(ri layout.RangeInfo, data []byte) error { if impliedSeq := ri.Index*layout.EntryBundleWidth + uint64(ri.First); impliedSeq != f.tree.End() { return fmt.Errorf("bundle with implied sequence number %d but expected %d", impliedSeq, f.tree.End()) } hs, err := f.bundleHasher(data) if err != nil { return err } for i := ri.First; i < ri.First+ri.N; i++ { if err := f.tree.Append(hs[i], f.visit); err != nil { return err } } return nil } func (f *fsckTree) GetRootHash() ([]byte, error) { if f.tree.End() == 0 { return rfc6962.DefaultHasher.EmptyRoot(), nil } return f.tree.GetRootHash(nil) } // visit is used to populate the derived tiles as we consume entries from the log we're checking. func (f *fsckTree) visit(id compact.NodeID, h []byte) { // We're only storing the lowest level of hash in the tiles, so early-out in other cases. if id.Level%layout.TileHeight != 0 { return } tLevel, tIdx, hIdx := id.Level/layout.TileHeight, id.Index/layout.EntryBundleWidth, id.Index%layout.EntryBundleWidth k := compact.NodeID{Level: tLevel, Index: tIdx} t, ok := f.pendingTiles[k] if !ok { t = &api.HashTile{} f.pendingTiles[k] = t } if hIdx != uint64(len(t.Nodes)) { klog.Exitf("LOGIC ERROR: got tile (l: %d, idx: %d) node index %d, for tile with %d nodes", tLevel, tIdx, hIdx, len(t.Nodes)) } t.Nodes = append(t.Nodes, h) if len(t.Nodes) == layout.EntryBundleWidth { c, err := t.MarshalText() if err != nil { klog.Exitf("Failed to marshal tile: %v", err) } f.expectedResources <- resource{ level: uint64(tLevel), index: tIdx, partial: uint8(len(t.Nodes)), content: c, } delete(f.pendingTiles, k) } } // flushPartialTiles ensures that any remaining derived tiles, which due to the size of the tree are partial, are also flushed to the // expectedResources work queue. func (f *fsckTree) flushPartialTiles() { for k, t := range f.pendingTiles { c, err := t.MarshalText() if err != nil { klog.Exitf("Failed to marshal tile: %v", err) } f.expectedResources <- resource{ level: uint64(k.Level), index: k.Index, partial: uint8(len(t.Nodes)), content: c, } delete(f.pendingTiles, k) } } var resourceWorkerID atomic.Uint32 // resourceCheckWorker returns a func which will consume resource check jobs from the // expectedResources channel. func (f *fsckTree) resourceCheckWorker(ctx context.Context) func() error { id := fmt.Sprintf("rc-worker-%d", resourceWorkerID.Add(1)) return func() error { for r := range f.expectedResources { f.rangeTracker.Update(int(r.level), r.index, Fetching) data, err := f.fetcher.ReadTile(ctx, r.level, r.index, r.partial) if err != nil { f.rangeTracker.Update(int(r.level), r.index, FetchError) return err } f.rangeTracker.Update(int(r.level), r.index, Calculating) if l, e := uint(len(data)), uint(r.partial)*sha256.Size; r.partial != 0 && l > e { // We were likely given a full tile rather than a partial tile, so trim it to the expected size. data = data[:e] } p := layout.TilePath(r.level, r.index, r.partial) if !bytes.Equal(data, r.content) { f.rangeTracker.Update(int(r.level), r.index, Invalid) return fmt.Errorf("%s: log has:\n%x\nexpected:\n%x", p, data, r.content) } f.rangeTracker.Update(int(r.level), r.index, OK) klog.V(2).Infof("%s: %s ok", id, p) } return nil } } // countingFetcher is a Fetcher which keeps track of the total number of bytes and resources fetched. type countingFetcher struct { f Fetcher bytesFetched atomic.Uint64 resourcesFetched atomic.Uint64 errorsEncountered atomic.Uint64 } func newCountingFetcher(f Fetcher) *countingFetcher { return &countingFetcher{ f: f, } } func (c *countingFetcher) count(r []byte, err error) ([]byte, error) { if err != nil { c.errorsEncountered.Add(1) return nil, err } c.bytesFetched.Add(uint64(len(r))) c.resourcesFetched.Add(1) return r, nil } func (c *countingFetcher) ReadCheckpoint(ctx context.Context) ([]byte, error) { return c.count(c.f.ReadCheckpoint(ctx)) } func (c *countingFetcher) ReadEntryBundle(ctx context.Context, i uint64, p uint8) ([]byte, error) { return c.count(c.f.ReadEntryBundle(ctx, i, p)) } func (c *countingFetcher) ReadTile(ctx context.Context, l, i uint64, p uint8) ([]byte, error) { return c.count(c.f.ReadTile(ctx, l, i, p)) } transparency-dev-tessera-3cb22ee/fsck/fsck_test.go000066400000000000000000000047071511600621500224010ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package fsck import ( "context" "crypto/sha256" "errors" "fmt" "testing" ) func TestTrimFullToPartial(t *testing.T) { for _, test := range []struct { name string r resource storedTile []byte wantErr bool }{ { name: "partial request, partial exists", r: resource{ partial: 10, content: makeTile(10), }, storedTile: makeTile(10), }, { name: "partial request, full exists", r: resource{ partial: 10, content: makeTile(10), }, storedTile: makeTile(256), }, { name: "invalid stored data", r: resource{ content: makeTile(256), }, storedTile: makeTile(2), wantErr: true, }, { name: "full request, full exists", r: resource{ content: makeTile(256), }, storedTile: makeTile(256), }, } { t.Run(test.name, func(t *testing.T) { f := &fsckTree{ expectedResources: make(chan resource, 1), fetcher: newCountingFetcher(&fakeFetcher{ tile: test.storedTile, }), rangeTracker: newRangeTracker(1), } f.expectedResources <- test.r close(f.expectedResources) err := f.resourceCheckWorker(t.Context())() if gotErr := err != nil; gotErr != test.wantErr { t.Fatalf("resourceCheckWorker: %v want err %t", err, test.wantErr) } }) } } type fakeFetcher struct { tile []byte } func (f *fakeFetcher) ReadCheckpoint(_ context.Context) ([]byte, error) { return nil, errors.New("not implemented") } func (f *fakeFetcher) ReadTile(_ context.Context, _, _ uint64, _ uint8) ([]byte, error) { return f.tile, nil } func (f *fakeFetcher) ReadEntryBundle(_ context.Context, _ uint64, _ uint8) ([]byte, error) { return nil, errors.New("not implemented") } func makeTile(n int) []byte { r := make([]byte, 0, n*sha256.Size) for i := range n { h := sha256.Sum256(fmt.Appendf(nil, "%50d", i)) r = append(r, h[:]...) } return r } transparency-dev-tessera-3cb22ee/fsck/status.go000066400000000000000000000221701511600621500217310ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package fsck import ( "container/list" "fmt" "strings" "sync" "github.com/transparency-dev/tessera/api/layout" ) const ( // Unchecked represents the state of resources which hasn't yet been checked. Unchecked State = iota // Fetching is the state of a resource being retrieved from the target log. Fetching // FetchError is the state of a failed fetch. FetchError // Fetched represents a resource which has been fetched, but not yet processed. Fetched // Calculating represents a resource being used to calculate hashes. Calculating // OK represents a resource which was successfully verified. OK // Invalid represents a resource which was determined to be incorrect or invalid somehow. Invalid ) // State represents the state of a given static resource. type State uint8 // String returns a string representation of the state. func (s State) String() string { switch s { case Unchecked: return "Unchecked" case Fetching: return "Fetching" case FetchError: return "FetchError" case Fetched: return "Fetched" case Calculating: return "Calculating" case OK: return "OK" case Invalid: return "Invalid" } panic(fmt.Errorf("unknown state %d", s)) } // rangeTracker is a struct which knows how to maintain the state of all resources in a log. // // Resources in a given "level" (i.e. tile level or entry bundle) are represented as ranges with a given state. // Resources covered by a given range must, by definition, all share the same state. // Initially there is a single range at each level with covers the entire set of resources at that level, e.g. all entry bundles. // When individual resources in the range have their state updated, the range which contains that resource is split (into either // two or three ranges) such that we can continue to represent all elements while honouring the one-state-per-range invariant. // In some situations, updates to resources will mean that there are two adjacent ranges which have the same state, and any such // ranges are coalescent into a single larger range which covers the union. // // In this way, we can represent the state of all resources in the log in an efficient scheme. // Due to the way the fsck tool works, the number of ranges is expected to be relatively small: // - the entire tree starts in state Unknown // - As the checking progresses, the left hand side of the tree will mostly be in the OK state, // and the right hand side will mostly be Unknown, with some range fragmentation in the middle (Fetching, Calculating, etc.) // - Eventually, all the ranges will (hopefully) return to being OK. type rangeTracker struct { // mu guards access to the fields below. mu *sync.Mutex // tiles holds information about the tiles in each level of the log, with tiles[0] being the // lowest level tiles, just above the entries. // The elements in this list are all *Range structs. tiles []*list.List // entries holds information about the entrybundles in the log. The list elements are all // *Range structs. entries *list.List } // Range describes the common state of a range of bundles/tiles. // The range covers [First, First+N) in tile-space, and all resources in the range share the same State. type Range struct { // First is the index of the first resource covered by this range. First uint64 // N is the number of resources covered by this range. N uint64 // State is the state all resources covered by this range have. State State } // String returns a simple human readable representation of the range. func (r *Range) String() string { return fmt.Sprintf("[%d, %d):%s", r.First, r.First+r.N, r.State) } // containts returns true iff the provided index is covered by this range. func (r *Range) contains(idx uint64) bool { return idx >= r.First && idx < r.First+r.N } // newRangeTracker constructs a new tracker for a log of the given size. func newRangeTracker(logSize uint64) *rangeTracker { el := list.New() t := []*list.List{} for level, levelSize := 0, logSize; levelSize > 0; level, levelSize = level+1, levelSize>>layout.TileHeight { n := levelSize >> layout.TileHeight if levelSize%layout.TileWidth > 0 { n++ } if level == 0 { el.PushBack(&Range{ First: 0, N: n, State: Unchecked, }) } l := list.New() l.PushBack(&Range{ First: 0, N: n, State: Unchecked, }) t = append(t, l) } return &rangeTracker{ mu: &sync.Mutex{}, entries: el, tiles: t, } } // maybeMergeWithPrev merges the provided Range element with the Range element before it // in the list into a single larger range, iff they share the same state. func maybeMergeWithPrev(e *list.Element, l *list.List) { if e == nil { return } ev := e.Value.(*Range) if p := e.Prev(); p != nil { pv := p.Value.(*Range) if pv.State == ev.State { ev.N += pv.N ev.First = pv.First l.Remove(p) } } } // maybeMergeWithNext merges the provided Range element with the Range element after it // in the list into a single larger range, iff they share the same state. func maybeMergeWithNext(e *list.Element, l *list.List) { if e == nil { return } ev := e.Value.(*Range) if n := e.Next(); n != nil { nv := n.Value.(*Range) if nv.State == ev.State { ev.N += nv.N l.Remove(n) } } } // Update updates the range state representation with the new state for the static resource at the given index. // level is the tile level if it's >= 0, or an entry bundle if it's == -1. func (r *rangeTracker) Update(l int, idx uint64, state State) { r.mu.Lock() defer r.mu.Unlock() var list *list.List if l == -1 { list = r.entries } else if l < len(r.tiles) { list = r.tiles[l] } else { panic(fmt.Errorf("no such tile level %d", l)) } for p := list.Front(); p != nil; p = p.Next() { v := p.Value.(*Range) if !v.contains(idx) { continue } if v.State == state { return } r := &Range{ First: idx, N: 1, State: state, } switch { case v.N == 1: // If this range has 1 element, then just change the state of the range. // This may mean we can coalesce this range with the previous and/or next range. v.State = state maybeMergeWithPrev(p, list) maybeMergeWithNext(p, list) case v.First == idx: // If we're changing the state of the first element in a range, then cleave that off into its // own range and insert it into the list before this one. e := list.InsertBefore(r, p) v.First++ v.N-- maybeMergeWithPrev(e, list) case v.First+v.N-1 == idx: // If we're changing the state of the last element in a range, then cleave that last element off // into its own range and add it after this one. e := list.InsertAfter(r, p) v.N-- maybeMergeWithNext(e, list) default: // We're changing an element somewhere in the middle of the range, so we want to split this range // up into 3 parts: // - a prefix range // - a range consisting of one element whose state we're changing // - a suffix range suffix := &Range{ First: r.First + 1, N: v.N - (idx - v.First) - 1, State: v.State, } // Turn the current range into the prefix range: v.N = idx - v.First // Add the single element range: e := list.InsertAfter(r, p) // Add the suffix range: list.InsertAfter(suffix, e) } return } dump := r.dumpRanges() panic(fmt.Errorf("walked off end of range with idx %d. List:\n%s", idx, strings.Join(dump, "\n"))) } // Ranges returns a snapshot of the current status of resources in the log. // // Returns: // - a []Range which contains one or more contiguous Range structs which cover all the entry bundles // - a slice of []Range which perform the same function for the internal Merkle tree levels, with the // zeroth entry being the bottom-most level of the tree, just above the entry bundles. func (r *rangeTracker) Ranges() ([]Range, [][]Range) { r.mu.Lock() defer r.mu.Unlock() eRange := make([]Range, 0, r.entries.Len()) for p := r.entries.Front(); p != nil; p = p.Next() { v := p.Value.(*Range) eRange = append(eRange, *v) } tRanges := make([][]Range, 0, len(r.tiles)) for _, t := range r.tiles { tr := make([]Range, 0, t.Len()) for p := t.Front(); p != nil; p = p.Next() { v := p.Value.(*Range) tr = append(tr, *v) } tRanges = append(tRanges, tr) } return eRange, tRanges } // dumpRanges returns a string suitable for logging/printing to std out to help debug the internal // state of the tracked ranges. func (r *rangeTracker) dumpRanges() []string { d := []string{} for p := r.entries.Front(); p != nil; p = p.Next() { v := p.Value.(*Range) d = append(d, fmt.Sprintf("([%d:%d): %v", v.First, v.First+v.N, v.State)) } return d } transparency-dev-tessera-3cb22ee/fsck/status_test.go000066400000000000000000000076731511600621500230030ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package fsck import ( "reflect" "strings" "testing" "github.com/transparency-dev/tessera/api/layout" ) type update struct { idx uint64 s State } func TestUpdate(t *testing.T) { for _, test := range []struct { name string size uint64 updates []update wantRanges []Range }{ { name: "no updates", size: 3 * layout.EntryBundleWidth, wantRanges: []Range{ {First: 0, N: 3}, }, }, { name: "Split head", size: 5 * layout.EntryBundleWidth, updates: []update{ {idx: 0, s: OK}, }, wantRanges: []Range{ {First: 0, N: 1, State: OK}, {First: 1, N: 4}, }, }, { name: "Split middle", size: 11 * layout.EntryBundleWidth, updates: []update{ {idx: 3, s: OK}, }, wantRanges: []Range{ {First: 0, N: 3}, {First: 3, N: 1, State: OK}, {First: 4, N: 7}, }, }, { name: "Split tail", size: 13 * layout.EntryBundleWidth, updates: []update{ {idx: 12, s: OK}, }, wantRanges: []Range{ {First: 0, N: 12}, {First: 12, N: 1, State: OK}, }, }, { name: "Multi-split", size: 10 * layout.EntryBundleWidth, updates: []update{ {idx: 1, s: OK}, {idx: 5, s: OK}, {idx: 7, s: OK}, }, wantRanges: []Range{ {First: 0, N: 1}, {First: 1, N: 1, State: OK}, {First: 2, N: 3}, {First: 5, N: 1, State: OK}, {First: 6, N: 1}, {First: 7, N: 1, State: OK}, {First: 8, N: 2}, }, }, { name: "Coalesce before", size: 500 * layout.EntryBundleWidth, updates: []update{ {idx: 1, s: OK}, {idx: 0, s: OK}, }, wantRanges: []Range{ {First: 0, N: 2, State: OK}, {First: 2, N: 498}, }, }, { name: "Coalesce after", size: 1000 * layout.EntryBundleWidth, updates: []update{ {idx: 1, s: OK}, {idx: 2, s: OK}, }, wantRanges: []Range{ {First: 0, N: 1}, {First: 1, N: 2, State: OK}, {First: 3, N: 997}, }, }, { name: "Coalesce first", size: 10 * layout.EntryBundleWidth, updates: []update{ {idx: 0, s: OK}, {idx: 1, s: OK}, }, wantRanges: []Range{ {First: 0, N: 2, State: OK}, {First: 2, N: 8}, }, }, { name: "Coalesce last", size: 10 * layout.EntryBundleWidth, updates: []update{ {idx: 9, s: OK}, {idx: 8, s: OK}, }, wantRanges: []Range{ {First: 0, N: 8}, {First: 8, N: 2, State: OK}, }, }, { name: "Coalesce degenerate", size: 10 * layout.EntryBundleWidth, updates: []update{ {idx: 9, s: OK}, {idx: 7, s: OK}, {idx: 5, s: OK}, {idx: 3, s: OK}, {idx: 1, s: OK}, {idx: 8, s: OK}, {idx: 6, s: OK}, {idx: 4, s: OK}, {idx: 2, s: OK}, {idx: 0, s: OK}, }, wantRanges: []Range{ {First: 0, N: 10, State: OK}, }, }, } { t.Run(test.name, func(t *testing.T) { s := newRangeTracker(test.size) for _, u := range test.updates { s.Update(-1, u.idx, u.s) } p := s.entries.Front() for i, want := range test.wantRanges { if p == nil { t.Fatalf("got %d entry ranges, want %d", i-1, len(test.wantRanges)) } got := p.Value.(*Range) if !reflect.DeepEqual(*got, want) { t.Errorf("Got range at index %d:\n%+v\nwant:\n%+v", i, *got, want) t.Errorf("DUMP:\n%s", strings.Join(s.dumpRanges(), "\n")) } p = p.Next() } if p != nil { t.Errorf("got more ranges than expected") } }) } } transparency-dev-tessera-3cb22ee/go.mod000066400000000000000000000153141511600621500202410ustar00rootroot00000000000000module github.com/transparency-dev/tessera go 1.24.0 require ( cloud.google.com/go/spanner v1.86.1 cloud.google.com/go/storage v1.58.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.30.0 github.com/RobinUS2/golang-moving-average v1.0.0 github.com/avast/retry-go/v4 v4.7.0 github.com/aws/aws-sdk-go-v2 v1.41.0 github.com/aws/aws-sdk-go-v2/config v1.32.4 github.com/aws/aws-sdk-go-v2/credentials v1.19.4 github.com/aws/aws-sdk-go-v2/service/s3 v1.93.1 github.com/aws/smithy-go v1.24.0 github.com/bitfield/script v0.24.1 github.com/cenkalti/backoff/v5 v5.0.3 github.com/charmbracelet/bubbletea v1.3.10 github.com/charmbracelet/lipgloss v1.1.0 github.com/dgraph-io/badger/v4 v4.8.0 github.com/dustin/go-humanize v1.0.1 github.com/gdamore/tcell/v2 v2.13.2 github.com/google/go-cmp v0.7.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/muesli/termenv v0.16.0 github.com/rivo/tview v0.42.0 github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c github.com/transparency-dev/merkle v0.0.2 go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0 go.opentelemetry.io/contrib/detectors/aws/ecs v1.38.0 go.opentelemetry.io/contrib/detectors/gcp v1.38.0 go.opentelemetry.io/contrib/propagators/aws v1.38.0 go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 go.opentelemetry.io/otel/sdk v1.39.0 go.opentelemetry.io/otel/sdk/metric v1.39.0 golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 golang.org/x/mod v0.31.0 google.golang.org/api v0.257.0 google.golang.org/grpc v1.77.0 k8s.io/klog/v2 v2.130.1 ) require ( cloud.google.com/go/compute/metadata v0.9.0 // indirect cloud.google.com/go/monitoring v1.24.2 // indirect cloud.google.com/go/trace v1.11.6 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 // indirect github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20221221133751-67e37ae746cd // indirect github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect github.com/charmbracelet/x/ansi v0.10.2 // indirect github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect github.com/charmbracelet/x/term v0.2.1 // indirect github.com/dgraph-io/ristretto/v2 v2.2.0 // indirect github.com/envoyproxy/go-control-plane/envoy v1.35.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-jose/go-jose/v4 v4.1.3 // indirect github.com/google/flatbuffers v25.2.10+incompatible // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect github.com/itchyny/gojq v0.12.13 // indirect github.com/itchyny/timefmt-go v0.1.5 // indirect github.com/klauspost/compress v1.18.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/spiffe/go-spiffe/v2 v2.6.0 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 // indirect go.opentelemetry.io/proto/otlp v1.9.0 // indirect mvdan.cc/sh/v3 v3.7.0 // indirect ) require ( cel.dev/expr v0.24.0 // indirect cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/auth v0.17.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/iam v1.5.3 // indirect cloud.google.com/go/longrunning v0.7.0 // indirect filippo.io/edwards25519 v1.1.0 // indirect github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f // indirect github.com/envoyproxy/protoc-gen-validate v1.2.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-sql-driver/mysql v1.9.3 github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-runewidth v0.0.17 // indirect github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect github.com/rivo/uniseg v0.4.7 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect go.opentelemetry.io/otel v1.39.0 go.opentelemetry.io/otel/metric v1.39.0 go.opentelemetry.io/otel/trace v1.39.0 // indirect golang.org/x/crypto v0.46.0 golang.org/x/net v0.47.0 golang.org/x/oauth2 v0.33.0 // indirect golang.org/x/sync v0.19.0 golang.org/x/sys v0.39.0 // indirect golang.org/x/term v0.38.0 // indirect golang.org/x/text v0.32.0 // indirect golang.org/x/time v0.14.0 google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect google.golang.org/protobuf v1.36.10 // indirect ) transparency-dev-tessera-3cb22ee/go.sum000066400000000000000000005300011511600621500202610ustar00rootroot00000000000000cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY= cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE= cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU= cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4= cloud.google.com/go/auth v0.17.0/go.mod h1:6wv/t5/6rOPAX4fJiRjKkJCvswLwdet7G8+UGXt7nCQ= cloud.google.com/go/auth/oauth2adapt v0.2.8 h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10= cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= cloud.google.com/go/iam v1.5.3 h1:+vMINPiDF2ognBJ97ABAYYwRgsaqxPbQDlMnbHMjolc= cloud.google.com/go/iam v1.5.3/go.mod h1:MR3v9oLkZCTlaqljW6Eb2d3HGDGK5/bDv93jhfISFvU= cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= cloud.google.com/go/logging v1.13.0 h1:7j0HgAp0B94o1YRDqiqm26w4q1rDMH7XNRU34lJXHYc= cloud.google.com/go/logging v1.13.0/go.mod h1:36CoKh6KA/M0PbhPKMq6/qety2DCAErbhXT62TuXALA= cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= cloud.google.com/go/longrunning v0.7.0 h1:FV0+SYF1RIj59gyoWDRi45GiYUMM3K1qO51qoboQT1E= cloud.google.com/go/longrunning v0.7.0/go.mod h1:ySn2yXmjbK9Ba0zsQqunhDkYi0+9rlXIwnoAf+h+TPY= cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= cloud.google.com/go/monitoring v1.24.2 h1:5OTsoJ1dXYIiMiuL+sYscLc9BumrL3CarVLL7dd7lHM= cloud.google.com/go/monitoring v1.24.2/go.mod h1:x7yzPWcgDRnPEv3sI+jJGBkwl5qINf+6qY4eq0I9B4U= cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= cloud.google.com/go/spanner v1.86.1 h1:lSeVPwUotuKTpf8K6BPitzneQfGu73QcDFIca2lshG8= cloud.google.com/go/spanner v1.86.1/go.mod h1:bbwCXbM+zljwSPLZ44wZOdzcdmy89hbUGmM/r9sD0ws= cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= cloud.google.com/go/storage v1.58.0 h1:PflFXlmFJjG/nBeR9B7pKddLQWaFaRWx4uUi/LyNxxo= cloud.google.com/go/storage v1.58.0/go.mod h1:cMWbtM+anpC74gn6qjLh+exqYcfmB9Hqe5z6adx+CLI= cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= cloud.google.com/go/trace v1.11.6 h1:2O2zjPzqPYAHrn3OKl029qlqG6W8ZdYaOWRyr8NgMT4= cloud.google.com/go/trace v1.11.6/go.mod h1:GA855OeDEBiBMzcckLPE2kDunIpC72N+Pq8WFieFjnI= cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3 h1:2afWGsMzkIcN8Qm4mgPJKZWyroE5QBszMiDMYEBrnfw= github.com/GoogleCloudPlatform/grpc-gcp-go/grpcgcp v1.5.3/go.mod h1:dppbR7CwXD4pgtV9t3wD1812RaLDcBjtblcDF5f1vI0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0 h1:sBEjpZlNHzK1voKq9695PJSX2o5NEXl7/OL3coiIY0c= github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.30.0/go.mod h1:P4WPRUkOhJC13W//jWpyfJNDAIpvRbAUIYLX/4jtlE0= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 h1:lhhYARPUu3LmHysQ/igznQphfzynnqI3D75oUyw1HXk= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0/go.mod h1:l9rva3ApbBpEJxSNYnwT9N4CDLrWgtq3u8736C5hyJw= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.30.0 h1:5eCqTd9rTwMlE62z0xFdzPJ+3pji75hJrwq1jrCjo5w= github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.30.0/go.mod h1:4BcvJy7WxY8X2eX49z2VO1ByhO+CcQK8lKPCH/QlZvo= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0 h1:xfK3bbi6F2RDtaZFtUdKO3osOBIhNb+xTs8lFW6yx9o= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.54.0/go.mod h1:vB2GH9GAYYJTO3mEn8oYwzEdhlayZIdQz6zdzgUIRvA= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 h1:s0WlVbf9qpvkh1c/uDAPElam0WrL7fHRIidgZJ7UqZI= github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0/go.mod h1:Mf6O40IAyB9zR/1J8nGDDPirZQQPbYJni8Yisy7NTMc= github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/RobinUS2/golang-moving-average v1.0.0 h1:PD7DDZNt+UFb9XlsBbTIu/DtXqqaD/MD86DYnk3mwvA= github.com/RobinUS2/golang-moving-average v1.0.0/go.mod h1:MdzhY+KoEvi+OBygTPH0OSaKrOJzvILWN2SPQzaKVsY= github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= github.com/ajstarks/svgo v0.0.0-20180226025133-644b8db467af/go.mod h1:K08gAheRH3/J6wwsYMMT4xOr94bZjxIelGM0+d/wbFw= github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/avast/retry-go/v4 v4.7.0 h1:yjDs35SlGvKwRNSykujfjdMxMhMQQM0TnIjJaHB+Zio= github.com/avast/retry-go/v4 v4.7.0/go.mod h1:ZMPDa3sY2bKgpLtap9JRUgk2yTAba7cgiFhqxY2Sg6Q= github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4= github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= github.com/aws/aws-sdk-go-v2/config v1.32.4 h1:gl+DxVuadpkYoaDcWllZqLkhGEbvwyqgNVRTmlaf5PI= github.com/aws/aws-sdk-go-v2/config v1.32.4/go.mod h1:MBUp9Og/bzMmQHjMwace4aJfyvJeadzXjoTcR/SxLV0= github.com/aws/aws-sdk-go-v2/credentials v1.19.4 h1:KeIZxHVbGWRLhPvhdPbbi/DtFBHNKm6OsVDuiuFefdQ= github.com/aws/aws-sdk-go-v2/credentials v1.19.4/go.mod h1:Smw5n0nCZE9PeFEguofdXyt8kUC4JNrkDTfBOioPhFA= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 h1:80+uETIWS1BqjnN9uJ0dBUaETh+P1XwFy5vwHwK5r9k= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16/go.mod h1:wOOsYuxYuB/7FlnVtzeBYRcjSRtQpAW0hCP7tIULMwo= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 h1:rgGwPzb82iBYSvHMHXc8h9mRoOUBZIGFgKb9qniaZZc= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16/go.mod h1:L/UxsGeKpGoIj6DxfhOWHWQ/kGKcd4I1VncE4++IyKA= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 h1:1jtGzuV7c82xnqOVfx2F0xmJcOw5374L7N6juGW6x6U= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16/go.mod h1:M2E5OQf+XLe+SZGmmpaI2yy+J326aFf6/+54PoxSANc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16 h1:CjMzUs78RDDv4ROu3JnJn/Ig1r6ZD7/T2DXLLRpejic= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.16/go.mod h1:uVW4OLBqbJXSHJYA9svT9BluSvvwbzLQ2Crf6UPzR3c= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7 h1:DIBqIrJ7hv+e4CmIk2z3pyKT+3B6qVMgRsawHiR3qso= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.7/go.mod h1:vLm00xmBke75UmpNvOcZQ/Q30ZFjbczeLFqGx5urmGo= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 h1:oHjJHeUy0ImIV0bsrX0X91GkV5nJAyv1l1CC9lnO0TI= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16/go.mod h1:iRSNGgOYmiYwSCXxXaKb9HfOEj40+oTKn8pTxMlYkRM= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16 h1:NSbvS17MlI2lurYgXnCOLvCFX38sBW4eiVER7+kkgsU= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.16/go.mod h1:SwT8Tmqd4sA6G1qaGdzWCJN99bUmPGHfRwwq3G5Qb+A= github.com/aws/aws-sdk-go-v2/service/s3 v1.93.1 h1:5FhzzN6JmlGQF6c04kDIb5KNGm6KnNdLISNrfivIhHg= github.com/aws/aws-sdk-go-v2/service/s3 v1.93.1/go.mod h1:79S2BdqCJpScXZA2y+cpZuocWsjGjJINyXnOsf5DTz8= github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 h1:HpI7aMmJ+mm1wkSHIA2t5EaFFv5EFYXePW30p1EIrbQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.4/go.mod h1:C5RdGMYGlfM0gYq/tifqgn4EbyX99V15P2V3R+VHbQU= github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 h1:eYnlt6QxnFINKzwxP5/Ucs1vkG7VT3Iezmvfgc2waUw= github.com/aws/aws-sdk-go-v2/service/sso v1.30.7/go.mod h1:+fWt2UHSb4kS7Pu8y+BMBvJF0EWx+4H0hzNwtDNRTrg= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 h1:AHDr0DaHIAo8c9t1emrzAlVDFp+iMMKnPdYy6XO4MCE= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12/go.mod h1:GQ73XawFFiWxyWXMHWfhiomvP3tXtdNar/fi8z18sx0= github.com/aws/aws-sdk-go-v2/service/sts v1.41.4 h1:YCu/iAhQer8WZ66lldyKkpvMyv+HkPufMa4dyT6wils= github.com/aws/aws-sdk-go-v2/service/sts v1.41.4/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk= github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bitfield/script v0.24.1 h1:D4ZWu72qWL/at0rXFF+9xgs17VwyrpT6PkkBTdEz9xU= github.com/bitfield/script v0.24.1/go.mod h1:fv+6x4OzVsRs6qAlc7wiGq8fq1b5orhtQdtW0dwjUHI= github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20221221133751-67e37ae746cd h1:C0dfBzAdNMqxokqWUysk2KTJSMmqvh9cNW1opdy5+0Q= github.com/brunoscheufler/aws-ecs-metadata-go v0.0.0-20221221133751-67e37ae746cd/go.mod h1:CeKhh8xSs3WZAc50xABMxu+FlfAAd5PNumo7NfOv7EE= github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM= github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs= github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30= github.com/charmbracelet/x/ansi v0.10.2 h1:ith2ArZS0CJG30cIUfID1LXN7ZFXRCww6RUvAPA+Pzw= github.com/charmbracelet/x/ansi v0.10.2/go.mod h1:HbLdJjQH4UH4AqA2HpRWuWNluRE6zxJH/yteYEYCFa8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8= github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs= github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ= github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f h1:Y8xYupdHxryycyPlc9Y+bSQAYZnetRJ70VMVKm5CKI0= github.com/cncf/xds/go v0.0.0-20251022180443-0feb69152e9f/go.mod h1:HlzOvOjVBOfTGSRXRyY0OiCS/3J1akRGQQpRO/7zyF4= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgraph-io/badger/v4 v4.8.0 h1:JYph1ChBijCw8SLeybvPINizbDKWZ5n/GYbz2yhN/bs= github.com/dgraph-io/badger/v4 v4.8.0/go.mod h1:U6on6e8k/RTbUWxqKR0MvugJuVmkxSNc79ap4917h4w= github.com/dgraph-io/ristretto/v2 v2.2.0 h1:bkY3XzJcXoMuELV8F+vS8kzNgicwQFAaGINAEJdWGOM= github.com/dgraph-io/ristretto/v2 v2.2.0/go.mod h1:RZrm63UmcBAaYWC1DotLYBmTvgkrs0+XhBd7Npn7/zI= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da h1:aIftn67I1fkbMa512G+w+Pxci9hJPB8oMnkcP3iZF38= github.com/dgryski/go-farm v0.0.0-20240924180020-3414d57e47da/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329 h1:K+fnvUM0VZ7ZFJf0n4L/BRlnsb9pL/GuDG6FqaH+PwM= github.com/envoyproxy/go-control-plane v0.13.5-0.20251024222203-75eaa193e329/go.mod h1:Alz8LEClvR7xKsrq3qzoc4N0guvVNSS8KmSChGYr9hs= github.com/envoyproxy/go-control-plane/envoy v1.35.0 h1:ixjkELDE+ru6idPxcHLj8LBVc2bFP7iBytj353BoHUo= github.com/envoyproxy/go-control-plane/envoy v1.35.0/go.mod h1:09qwbGVuSWWAyN5t/b3iyVfz5+z8QWGrzkoqm/8SbEs= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0 h1:/G9QYbddjL25KvtKTv3an9lx6VBE2cnb8wp1vEGNYGI= github.com/envoyproxy/go-control-plane/ratelimit v0.1.0/go.mod h1:Wk+tMFAFbCXaJPzVVHnPgRKdUdwW/KdbRt94AzgRee4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fogleman/gg v1.2.1-0.20190220221249-0403632d5b90/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw= github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo= github.com/gdamore/tcell/v2 v2.13.2 h1:5j4srfF8ow3HICOv/61/sOhQtA25qxEB2XR3Q/Bhx2g= github.com/gdamore/tcell/v2 v2.13.2/go.mod h1:+Wfe208WDdB7INEtCsNrAN6O2m+wsTPk1RAovjaILlo= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= github.com/go-fonts/liberation v0.1.1/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= github.com/go-latex/latex v0.0.0-20210118124228-b3d85cf34e07/go.mod h1:CO1AlKB2CSIqUrmQPqA0gdRIlnLEY0gK5JGjh37zN5U= github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo= github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU= github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ= github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U= github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/flatbuffers v25.2.10+incompatible h1:F3vclr7C3HpB1k9mxCGRMXq6FdUalZ6H/pNX4FP1v0Q= github.com/google/flatbuffers v25.2.10+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/martian/v3 v3.3.3 h1:DIhPTQrbPkgs2yJYdXU/eNACCG5DVQjySNRNlflZ9Fc= github.com/google/martian/v3 v3.3.3/go.mod h1:iEPrYcgCF7jA9OtScMFQyAlZZ4YXTKEtJ1E6RWzmBA0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/enterprise-certificate-proxy v0.3.7 h1:zrn2Ee/nWmHulBx5sAVrGgAa0f2/R35S4DJwfFaUPFQ= github.com/googleapis/enterprise-certificate-proxy v0.3.7/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/itchyny/gojq v0.12.13 h1:IxyYlHYIlspQHHTE0f3cJF0NKDMfajxViuhBLnHd/QU= github.com/itchyny/gojq v0.12.13/go.mod h1:JzwzAqenfhrPUuwbmEz3nu3JQmFLlQTQMUcOdnu/Sf4= github.com/itchyny/timefmt-go v0.1.5 h1:G0INE2la8S6ru/ZI5JecgyzbbJNs5lG1RcBqa7Jm6GE= github.com/itchyny/timefmt-go v0.1.5/go.mod h1:nEP7L+2YmAbT2kZ2HfSs1d8Xtw9LY8D2stDBckWakZ8= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.3-0.20190309125859-24315acbbda5/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4= github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.17 h1:78v8ZlW0bP43XfmAfPsdXcoNCelfMHsDmd/pkENfrjQ= github.com/mattn/go-runewidth v0.0.17/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 h1:GFCKgmp0tecUJ0sJuv4pzYCqS9+RGSn52M3FUwPs+uo= github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10/go.mod h1:t/avpk3KcrXxUnYOhZhMXJlSEyie6gQbtLq5NM3loB8= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/rivo/tview v0.42.0 h1:b/ftp+RxtDsHSaynXTbJb+/n/BxDEi+W3UfF5jILK6c= github.com/rivo/tview v0.42.0/go.mod h1:cSfIYfhpSGCjp3r/ECJb+GKS7cGJnqV8vfjQPwoXyfY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spiffe/go-spiffe/v2 v2.6.0 h1:l+DolpxNWYgruGQVV0xsfeya3CsC7m8iBzDnMpsbLuo= github.com/spiffe/go-spiffe/v2 v2.6.0/go.mod h1:gm2SeUoMZEtpnzPNs2Csc0D/gX33k1xIx7lEzqblHEs= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c h1:5a2XDQ2LiAUV+/RjckMyq9sXudfrPSuCY4FuPC1NyAw= github.com/transparency-dev/formats v0.0.0-20251017110053-404c0d5b696c/go.mod h1:g85IafeFJZLxlzZCDRu4JLpfS7HKzR+Hw9qRh3bVzDI= github.com/transparency-dev/merkle v0.0.2 h1:Q9nBoQcZcgPamMkGn7ghV8XiTZ/kRxn1yCG81+twTK4= github.com/transparency-dev/merkle v0.0.2/go.mod h1:pqSy+OXefQ1EDUVmAJ8MUhHB9TXGuzVAT58PqBoHz1A= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0 h1:29ryzGOpNONSkgpOzJqYFOaUxAt7rmuvHRAPRCKe9Mw= go.opentelemetry.io/contrib/detectors/aws/ec2/v2 v2.0.0/go.mod h1:5NPwG/iuUWEeSVX8yPxuGQDXhdMuPte7PdaBTh/qLhY= go.opentelemetry.io/contrib/detectors/aws/ecs v1.38.0 h1:3k8Hm/2d06eFegWKjPgGqyrGBTa8xGWMXsV3EHmXqUY= go.opentelemetry.io/contrib/detectors/aws/ecs v1.38.0/go.mod h1:/RMzURa608/HpVWxCeTkkQVgasv/KG8SvFrLyic4le8= go.opentelemetry.io/contrib/detectors/gcp v1.38.0 h1:ZoYbqX7OaA/TAikspPl3ozPI6iY6LiIY9I8cUfm+pJs= go.opentelemetry.io/contrib/detectors/gcp v1.38.0/go.mod h1:SU+iU7nu5ud4oCb3LQOhIZ3nRLj6FNVrKgtflbaf2ts= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 h1:YH4g8lQroajqUwWbq/tr2QX1JFmEXaDLgG+ew9bLMWo= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0/go.mod h1:fvPi2qXDqFs8M4B4fmJhE92TyQs9Ydjlg3RvfUp+NbQ= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 h1:RbKq8BG0FI8OiXhBfcRtqqHcZcka+gU3cskNuf05R18= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg= go.opentelemetry.io/contrib/propagators/aws v1.38.0 h1:eRZ7asSbLc5dH7+TBzL6hFKb1dabz0IV51uUUwYRZts= go.opentelemetry.io/contrib/propagators/aws v1.38.0/go.mod h1:wXqc9NTGcXapBExHBDVLEZlByu6quiQL8w7Tjgv8TCg= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0 h1:cEf8jF6WbuGQWUVcqgyWtTR0kOOAWY1DYZ+UhvdmQPw= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.39.0/go.mod h1:k1lzV5n5U3HkGvTCJHraTAGJ7MqsgL1wrGwTj1Isfiw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0 h1:in9O8ESIOlwJAEGTkkf34DesGRAc/Pn8qJ7k3r/42LM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.39.0/go.mod h1:Rp0EXBm5tfnv0WL+ARyO/PHBEaEAT8UUHQ6AGJcSq6c= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0 h1:wm/Q0GAAykXv83wzcKzGGqAnnfLFyFe7RslekZuv+VI= go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v1.38.0/go.mod h1:ra3Pa40+oKjvYh+ZD3EdxFZZB0xdMfuileHAm4nNN7w= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210216034530-4410531fe030/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI= golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gonum.org/v1/gonum v0.0.0-20180816165407-929014505bf4/go.mod h1:Y+Yx5eoAFn32cQvJDxZx5Dpnq+c3wtXuadVZAcxbbBo= gonum.org/v1/gonum v0.8.2/go.mod h1:oe/vMfY3deqTw+1EZJhuvEW2iwGF1bW9wwu7XCu0+v0= gonum.org/v1/gonum v0.9.3/go.mod h1:TZumC3NeyVQskjXqmyWt4S3bINhy7B4eYwW69EbyX+0= gonum.org/v1/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/netlib v0.0.0-20190313105609-8cb42192e0e0/go.mod h1:wa6Ws7BG/ESfp6dHfk7C6KdzKA7wR7u/rKwOGE66zvw= gonum.org/v1/plot v0.0.0-20190515093506-e2840ee46a6b/go.mod h1:Wt8AAjI+ypCyYX3nZBvf6cAIx93T+c/OS2HFAYskSZc= gonum.org/v1/plot v0.9.0/go.mod h1:3Pcqqmp6RHvJI72kgb8fThyUnav364FOsdDo2aGW5lY= gonum.org/v1/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= google.golang.org/api v0.257.0 h1:8Y0lzvHlZps53PEaw+G29SsQIkuKrumGWs9puiexNAA= google.golang.org/api v0.257.0/go.mod h1:4eJrr+vbVaZSqs7vovFd1Jb/A6ml6iw2e6FBYf3GAO4= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9 h1:LvZVVaPE0JSqL+ZWb6ErZfnEOKIqqFWUJE2D0fObSmc= google.golang.org/genproto v0.0.0-20250922171735-9219d122eba9/go.mod h1:QFOrLhdAe2PsTp3vQY4quuLKTi9j3XG3r6JPPaw7MSc= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM= google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= mvdan.cc/sh/v3 v3.7.0 h1:lSTjdP/1xsddtaKfGg7Myu7DnlHItd3/M2tomOcNNBg= mvdan.cc/sh/v3 v3.7.0/go.mod h1:K2gwkaesF/D7av7Kxl0HbF5kGOd2ArupNTX3X44+8l8= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= transparency-dev-tessera-3cb22ee/integration/000077500000000000000000000000001511600621500214525ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/integration/fault/000077500000000000000000000000001511600621500225655ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/integration/fault/posix/000077500000000000000000000000001511600621500237275ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/integration/fault/posix/fault_test.go000066400000000000000000000270551511600621500264410ustar00rootroot00000000000000// Copyright 2025 The Tessera Authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. //go:build linux // Package fault_test contains a (probably Linux-specific) test which uses `strace` to inject faults // into the Tessera posix-oneshot binary to verify that this does not result in an inconsistent or corrupt // tree state stored on disk. package fault_test import ( "fmt" "io" "os" "path/filepath" "strings" "testing" "github.com/bitfield/script" "github.com/transparency-dev/formats/note" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/fsck" "k8s.io/klog/v2" ) const ( testSigner = "PRIVATE+KEY+example.com/fault_test+7f84ee58+AajLvPzbzMafNfiFvmRw1YlP7del7DYc1cr7Yntjf8yP" testVerifier = "example.com/fault_test+7f84ee58+AfMY9QKMQjVcokKlKSGRvcLL8YwvpDHua/r0e64EfFIX" treeStateFile = "/.state/treeState" ) var ( // signerPath will be set to a path to a file containing a note signer key. signerPath string // verifierPath will be set to a path to a file containing a note verifier key corresponding to the signer key above. verifierPath string // oneshotPath will be set to a path to a compiled posix-oneshot binary. oneshotPath string // interestingSyscalls lists the syscalls ultimately used by posix-oneshot to manipulate the state of the log on disk. interestingSyscalls = []string{"openat", "linkat", "mkdirat", "renameat", "fsync", "close", "read", "write"} ) // runFaultInjectionTest is a test which tries to cause tree corruption by injecting faults into syscalls which are // ultimately used by the posix-oneshot application. // // This test builds and execs the `/cmd/examples/posix-oneshot` tool in order to manage some logs-under-test, and // uses `strace` to inspect its operation at the syscall level and to inject failures for specific calls. // // Broadly, it works as follows: // 1. use the posix-oneshot tool to create a "base" log, which will be used as an ancestor for all logs created during the test. // 2. create a copy of the base log, and use `strace` to inspect posix-oneshot as it adds a new entry to this copy. // 3. analyse the output from `strace` to identify a list of "interesting" (i.e. file operation related) syscalls which occurred while // posix-oneshat was actively managing/updating the log on disk. // 4. for each individual interesting syscall: // 4.1. create a new copy of the base log // 4.2. invoke posix-oneshot using `strace`'s `-e inject=' flag to inject the provided fault for a single specific instance of a syscall. // We don't particularly care whether this fault causes posix-oneshot to exit or retry; what's important is that the on-disk // representation of the log state is never left in an inconsistent or corrupt state. // 4.3. run `fsck` against the log-under-test to assert that the on-disk state is, indeed, self-consistent and uncorrupted. func runFaultInjectionTest(t *testing.T, inject string) { tmp := t.TempDir() setup(t, tmp) baseDir := filepath.Join(tmp, "base") _ = os.MkdirAll(baseDir, 0o755) // Set up base log: { mustWrite(t, filepath.Join(baseDir, "base-0"), []byte("base first")) p := script.Exec(growLogCommand(baseDir, filepath.Join(baseDir, "base-0"))) if output, err := p.String(); err != nil { t.Fatalf("Failed to create base log: %v\n%s", err, output) } } // Create an isolated "descendent" log which we'll use to inspect the syscalls being used by posix-oneshot while // updating it: count, processFn := processStraceOutput(interestingSyscalls) { traceDir := filepath.Join(tmp, "trace") _ = os.MkdirAll(traceDir, 0o755) if output, err := script.Exec(fmt.Sprintf("cp -r %s/. %s/", baseDir, traceDir)).String(); err != nil { t.Fatalf("Failed to copy base log into trace directory %q: %v\n%s", traceDir, err, output) } mustWrite(t, filepath.Join(traceDir, "trace-0"), []byte("trace first")) traceRun := script.Exec(fmt.Sprintf("strace -f -e trace=%s -e quiet=all %s", strings.Join(interestingSyscalls, ","), growLogCommand(traceDir, filepath.Join(traceDir, "trace-0")))) traceRun.FilterScan(processFn) o, err := traceRun.String() if err != nil { t.Errorf("traceRun failed: %v\n%s", err, o) } t.Logf("Syscall counts: %v", count) } // Now perform the test proper. // For each individual invocation of each individual syscall used by posix-oneshot, create a new copy of the base tree and // use `strace` to inject a fault into an invocation of the posix-oneshot command which attempts to add an entry to that copy. for sc, c := range count { for i := range c.N { i := c.First + i t.Run(fmt.Sprintf("%s-on-%s#%d", inject, sc, i), func(t *testing.T) { t.Parallel() // Fork the base log into a new directory. injectDir := filepath.Join(tmp, fmt.Sprintf("inject-%s-%d", sc, i)) _ = os.MkdirAll(injectDir, 0o755) if output, err := script.Exec(fmt.Sprintf("cp -r %s/. %s/", baseDir, injectDir)).String(); err != nil { t.Fatalf("Failed to copy base log into directory %q: %v\n%s", injectDir, err, output) } // Now run posix-oneshot to add an entry, using strace to inject a fault on the ith call to the syscall named in sc: // We don't really care whether this results in an error or not, but it _MUST_ be the case that the on-disk tree is // left in a self-consistent state. mustWrite(t, filepath.Join(injectDir, "inject-0"), fmt.Appendf(nil, "inject-%d first", i)) cmd := fmt.Sprintf("strace -f -e inject=%s:%s:when=%d -e quiet=all %s", sc, inject, i, growLogCommand(injectDir, filepath.Join(injectDir, "inject-0"))) if _, err := script.Exec(cmd).String(); err != nil { t.Logf("Failed to growLog on %s: %v", injectDir, err) } // Verify that the tree on disk is self-consistent and uncorrupted by running fsck against it. // Any error here is bad news. if err := fsckLog(t, injectDir); err != nil { t.Errorf("Fsck on %s failed: %v", injectDir, err) } }) } } } func TestInjectIOError(t *testing.T) { runFaultInjectionTest(t, "error=EIO") } func TestInjectSigKill(t *testing.T) { runFaultInjectionTest(t, "signal=KILL") } // setup configures everything necessary for running the fault test. // // This includes building the oneshot-posix binary, writing out test keys, // and figuring out paths. func setup(t *testing.T, outDir string) { t.Helper() oneshotPath = filepath.Join(outDir, "posix-oneshot") signerPath = filepath.Join(outDir, "test.sec") verifierPath = filepath.Join(outDir, "test.pub") if o, err := script.Exec(fmt.Sprintf("go build -o %s ../../../cmd/examples/posix-oneshot", oneshotPath)).String(); err != nil { t.Fatalf("Failed to build posix-oneshot: %v\n%s", err, o) } if err := os.WriteFile(signerPath, []byte(testSigner), 0o644); err != nil { t.Fatalf("Failed to write %s: %v", signerPath, err) } if err := os.WriteFile(verifierPath, []byte(testVerifier), 0o644); err != nil { t.Fatalf("Failed to write %s: %v", verifierPath, err) } } // growLogCommand returns a formatted shell command for executing posix-oneshot on a log stored at the provided location // to integrate the provided new entry files. func growLogCommand(storagePath string, entriesGlob string) string { return fmt.Sprintf("%s --private_key=%s --storage_dir=%s --entries=%s", oneshotPath, signerPath, storagePath, entriesGlob) } // calls represents a range of invocations of a particular syscall. type calls struct { // First is the index of the first "interesting" use of the corresponding syscall - i.e. it occurred // while posix-oneshot was actually modifying/processing the log. First int // N is the number of invocations of the syscall which occurred during the period posix-oneshot was // modifying/processing the log. N int } // processStraceOutput reads the output from the strace command being used to inspect the goings-on // inside posix-oneshot. // // This function returns a map which contains information about calls to the list of provided // "interesting" syscalls, and a function which can be used to filter the output down to a heuristically // determined subset for human-inspection purposes. // // The syscalls we're interested in are pretty common (e.g. Go's runtime wants to read a bunch of // files/devices when we first start a compiled Go program, posix-oneshot wants to read keys from // disk, etc.), so we attempt to pare down the _actually_ interesting calls by ignoring anything // which happens before execution has reached the "actual" posix-oneshot application code. We do this by // looking for the very first syscall referencing the ".state/treeState" file - this is tessera opening the // tree. func processStraceOutput(syscalls []string) (map[string]calls, func(string, io.Writer)) { // m is a map of syscall name to information about its invocations. m := make(map[string]calls) // oneshotStarted will be false until we've seen the string "/.state/treeState" in one of the syscalls. // we use this to determine whether syscalls should be skipped or not. oneshotStarted := false return m, func(line string, w io.Writer) { // When we see the treeStateFile referenced by a syscall we know posix-oneshot has started up // and is now about to perform the work on the log. if strings.Contains(line, treeStateFile) { oneshotStarted = true } // If we're in the interesting part of the strace output, then pass through the log line, otherwise // just drop it. if oneshotStarted { _, _ = fmt.Fprintf(w, "%s\n", line) } // Check the stract log line against our list of interesting syscalls and update the info in // our map. for _, s := range syscalls { if strings.Contains(line, s) { c := m[s] if !oneshotStarted { // oneshot still hasn't fully started up, so we're going to skip all the early calls to this // syscall. c.First++ } else { // oneshot is working on the log, so count this syscall invocation as being a candidate for // fault injection. c.N++ } m[s] = c } } } } // fsckLog runs fsck on the log rooted at the provided path. func fsckLog(t *testing.T, dir string) error { t.Helper() src := &client.FileFetcher{ Root: dir, } v, err := note.NewVerifier(testVerifier) if err != nil { klog.Exitf("Invalid verifier in %q: %v", testVerifier, err) } f := fsck.New(v.Name(), v, src, defaultMerkleLeafHasher, fsck.Opts{N: 1}) return f.Check(t.Context()) } // defaultMerkleLeafHasher parses a C2SP tlog-tile bundle and returns the Merkle leaf hashes of each entry it contains. func defaultMerkleLeafHasher(bundle []byte) ([][]byte, error) { eb := &api.EntryBundle{} if err := eb.UnmarshalText(bundle); err != nil { return nil, fmt.Errorf("unmarshal: %v", err) } r := make([][]byte, 0, len(eb.Entries)) for _, e := range eb.Entries { h := rfc6962.DefaultHasher.HashLeaf(e) r = append(r, h[:]) } return r, nil } // mustWrite writes the provided contents to a file at the provided path, or causes a fatal test failure. func mustWrite(t *testing.T, path string, contents []byte) { t.Helper() if err := os.WriteFile(path, contents, 0o644); err != nil { t.Fatalf("Failed to write %q: %v", path, err) } } transparency-dev-tessera-3cb22ee/integration/integration_test.go000066400000000000000000000156161511600621500253740ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package integration_test contains some integration tests which are intended to // serve as a way of checking that example binary works as intended, // as well as providing a simple example of how to run and use it. package integration_test import ( "bytes" "context" "flag" "fmt" "io" "net/http" "net/url" "os" "strconv" "strings" "sync" "testing" "time" "github.com/transparency-dev/merkle/proof" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/client" "golang.org/x/mod/sumdb/note" "golang.org/x/sync/errgroup" "k8s.io/klog/v2" ) var ( runIntegrationTest = flag.Bool("run_integration_test", false, "If true, the integration tests in this package will not be skipped") logURL = flag.String("log_url", "http://localhost:2024", "Log storage read root URL, e.g. https://log.server/and/path/") writeLogURL = flag.String("write_log_url", "http://localhost:2024", "Log storage write root URL, e.g. https://log.server/and/path/") logPublicKey = flag.String("log_public_key", "", "The log's public key value for checkpoint note verification") testEntrySize = flag.Int("test_entry_size", 1024, "The number of entries to be tested in the live log integration") noteVerifier note.Verifier logReadBaseURL *url.URL logReadCP client.CheckpointFetcherFunc logReadTile client.TileFetcherFunc logReadEntryBundle client.EntryBundleFetcherFunc hc = &http.Client{ Transport: &http.Transport{ MaxIdleConns: 256, MaxIdleConnsPerHost: 256, }, Timeout: 60 * time.Second, } ) func TestMain(m *testing.M) { klog.InitFlags(nil) flag.Parse() if !*runIntegrationTest { klog.Warning("example binary integration tests are skipped") return } var err error noteVerifier, err = note.NewVerifier(*logPublicKey) if err != nil { klog.Fatalf("Failed to create new verifier: %v", err) } logReadBaseURL, err = url.Parse(*logURL) if err != nil { klog.Fatalf("failed to parse logURL: %v", err) } switch logReadBaseURL.Scheme { case "http", "https": hf, err := client.NewHTTPFetcher(logReadBaseURL, nil) if err != nil { klog.Fatalf("NewHTTPFetcher: %v", err) } logReadCP = hf.ReadCheckpoint logReadTile = hf.ReadTile logReadEntryBundle = hf.ReadEntryBundle case "file": ff := client.FileFetcher{Root: logReadBaseURL.Path} logReadCP = ff.ReadCheckpoint logReadTile = ff.ReadTile logReadEntryBundle = ff.ReadEntryBundle default: klog.Fatalf("unsupported url scheme: %s", logReadBaseURL.Scheme) } os.Exit(m.Run()) } func TestLiveLogIntegration(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) defer cancel() var entryIndexMap sync.Map // Step 1 - Get checkpoint initial size for increment validation. lst, err := client.NewLogStateTracker(ctx, logReadTile, nil, noteVerifier, noteVerifier.Name(), client.UnilateralConsensus(logReadCP)) if err != nil { t.Fatalf("client.NewLogStateTracker: %v", err) } checkpointInitSize := lst.Latest().Size // Step 2 - Add entries and get new checkpoints. The entry data comes from the int loop ranging from 0 to the test entry size - 1. addEntriesURL, err := url.JoinPath(*writeLogURL, "add") if err != nil { t.Errorf("url.JoinPath: %v", err) } entryWriter := entryWriter{ addURL: addEntriesURL, } var miMu sync.Mutex var maxIndex uint64 errG := errgroup.Group{} for i := range *testEntrySize { errG.Go(func() error { index, err := entryWriter.add(ctx, fmt.Appendf(nil, "%d", i)) if err != nil { return fmt.Errorf("entryWriter.add(%d): %v", i, err) } entryIndexMap.Store(i, index) miMu.Lock() defer miMu.Unlock() if maxIndex < index { maxIndex = index } return nil }) } if err := errG.Wait(); err != nil { t.Fatalf("addEntry: %v", err) } // All entries are queued. Wait for a checkpoint committing to maxIndex. for size := lst.Latest().Size; size <= maxIndex; { if _, _, _, err := lst.Update(ctx); err != nil { t.Errorf("lst.Update: %v", err) } size = lst.Latest().Size time.Sleep(50 * time.Millisecond) } gotIncrease := lst.Latest().Size - checkpointInitSize if gotIncrease < uint64(*testEntrySize) { t.Logf("checkpoint size increase (%d) is < %d, entries may have been deduplicated.", gotIncrease, *testEntrySize) } // Step 3 - Loop through the entry data index map to verify leaves and inclusion proofs. entryIndexMap.Range(func(k, v any) bool { data := k.(int) index := v.(uint64) // Step 4.1 - Get entry bundles to read back what was written, check leaves are correct. entryBundle, err := client.GetEntryBundle(ctx, logReadEntryBundle, index/layout.EntryBundleWidth, lst.Latest().Size) if err != nil { t.Fatalf("client.GetEntryBundle: %v", err) } got, want := entryBundle.Entries[index%layout.EntryBundleWidth], fmt.Appendf(nil, "%d", data) if !bytes.Equal(got, want) { t.Errorf("Entry bundle (index: %d) got %v want %v", index, got, want) } // Step 4.2 - Test inclusion proofs. pb, err := client.NewProofBuilder(ctx, lst.Latest().Size, logReadTile) if err != nil { t.Errorf("client.NewProofBuilder: %v", err) } ip, err := pb.InclusionProof(ctx, index) if err != nil { t.Errorf("pb.InclusionProof: %v", err) } leafHash := rfc6962.DefaultHasher.HashLeaf(fmt.Append(nil, data)) if err := proof.VerifyInclusion(rfc6962.DefaultHasher, index, lst.Latest().Size, leafHash, ip, lst.Latest().Hash); err != nil { t.Errorf("proof.VerifyInclusion: %v", err) } return true }) } type entryWriter struct { addURL string } func (w *entryWriter) add(ctx context.Context, entry []byte) (uint64, error) { req, err := http.NewRequestWithContext(ctx, http.MethodPost, w.addURL, bytes.NewReader(entry)) if err != nil { return 0, err } resp, err := hc.Do(req) if err != nil { return 0, err } body, err := io.ReadAll(resp.Body) defer func() { if err := resp.Body.Close(); err != nil { klog.Warningf("resp.Body.Close(): %v", err) } }() if err != nil { return 0, fmt.Errorf("failed to read response from %s: %w", w.addURL, err) } if resp.StatusCode != http.StatusOK { return 0, fmt.Errorf("code: %s, path: %s, body: %s", resp.Status, w.addURL, strings.TrimSpace(string(body))) } index, err := strconv.ParseUint(string(body), 10, 64) if err != nil { return 0, err } return index, nil } transparency-dev-tessera-3cb22ee/internal/000077500000000000000000000000001511600621500207435ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/internal/fetcher/000077500000000000000000000000001511600621500223635ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/internal/fetcher/fallback.go000066400000000000000000000032361511600621500244550ustar00rootroot00000000000000// Copyright 2025 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package fetcher import ( "context" "errors" "fmt" "os" ) // PartialOrFullResource calls the provided function with the provided partial resource size value in order to fetch and return a static resource. // If p is non-zero, and f returns os.ErrNotExist, this function will try to fetch the corresponding full resource by calling f a second time passing // zero. func PartialOrFullResource(ctx context.Context, p uint8, f func(context.Context, uint8) ([]byte, error)) ([]byte, error) { sRaw, err := f(ctx, p) switch { case errors.Is(err, os.ErrNotExist) && p == 0: return sRaw, fmt.Errorf("resource not found: %w", err) case errors.Is(err, os.ErrNotExist) && p > 0: // It could be that the partial resource was removed as the tree has grown and a full resource is now present, so try // falling back to that. sRaw, err = f(ctx, 0) if err != nil { return sRaw, fmt.Errorf("neither partial nor full resource found: %w", err) } return sRaw, nil case err != nil: return sRaw, fmt.Errorf("failed to fetch resource: %v", err) default: return sRaw, nil } } transparency-dev-tessera-3cb22ee/internal/fetcher/fallback_test.go000066400000000000000000000034331511600621500255130ustar00rootroot00000000000000// Copyright 2025 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package fetcher import ( "context" "os" "testing" ) func TestFetchPartialOrFullResource(t *testing.T) { for _, test := range []struct { name string p uint8 responses []error expectCalls int wantErr bool }{ { name: "partial resource found", p: 23, responses: []error{nil}, }, { name: "partial resource missing, full resource found", p: 23, responses: []error{os.ErrNotExist, nil}, }, { name: "partial resource missing, full resource missing", p: 23, responses: []error{os.ErrNotExist, os.ErrNotExist}, wantErr: true, }, { name: "full resource found", responses: []error{nil}, }, { name: "full resource missing", responses: []error{os.ErrNotExist}, wantErr: true, }, } { t.Run(test.name, func(t *testing.T) { i := 0 _, gotE := PartialOrFullResource(t.Context(), test.p, func(ctx context.Context, u uint8) ([]byte, error) { defer func() { i++ }() return []byte("ret"), test.responses[i] }) if gotErr := gotE != nil; gotErr != test.wantErr { t.Fatalf("got error %v, want err %t", gotErr, test.wantErr) } }) } } transparency-dev-tessera-3cb22ee/internal/future/000077500000000000000000000000001511600621500222555ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/internal/future/future.go000066400000000000000000000032331511600621500241170ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package future import "sync" // FutureErr is a future which resolves to a value or an error. type FutureErr[T any] struct { // done is used to block/unblock calls to resolve the future. // // This could be done with a channel, but that turns out to be heavier in terms of // memory alloc than using a waitgroup. done *sync.WaitGroup val T err error } // NewFutureErr creates a new future which resolves to a T or an error. // // Returns the future, and a function which is used to set the future's value/error. // Calls to the future's Get function will block until this function is called. func NewFutureErr[T any]() (*FutureErr[T], func(T, error)) { f := &FutureErr[T]{ done: &sync.WaitGroup{}, } f.done.Add(1) var o sync.Once return f, func(t T, err error) { o.Do(func() { f.val = t f.err = err f.done.Done() }) } } // Get resolves the future, returning either a valid T or an error. // // This function will block until the future has had its value set. func (f *FutureErr[T]) Get() (T, error) { f.done.Wait() return f.val, f.err } transparency-dev-tessera-3cb22ee/internal/hammer/000077500000000000000000000000001511600621500222145ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/internal/hammer/Dockerfile000066400000000000000000000012011511600621500242000ustar00rootroot00000000000000FROM golang:1.24.1-alpine3.21@sha256:43c094ad24b6ac0546c62193baeb3e6e49ce14d3250845d166c77c25f64b0386 AS builder ARG GOFLAGS="-trimpath -buildvcs=false -buildmode=exe" ENV GOFLAGS=$GOFLAGS # Move to working directory /build WORKDIR /build # Copy and download dependency using go mod COPY go.mod . COPY go.sum . RUN go mod download # Copy the code into the container COPY . . # Build the application RUN go build -o bin/hammer ./internal/hammer # Build release image FROM alpine:3.20.2@sha256:0a4eaa0eecf5f8c050e5bba433f58c052be7587ee8af3e8b3910ef9ab5fbe9f5 COPY --from=builder /build/bin/hammer /bin/hammer ENTRYPOINT ["/bin/hammer"] transparency-dev-tessera-3cb22ee/internal/hammer/README.md000066400000000000000000000107601511600621500234770ustar00rootroot00000000000000# Hammer: A load testing tool for Tessera logs This hammer sets up read and (optionally) write traffic to a log to test correctness and performance under load. The read traffic is sent according to the [tlog-tiles](https://github.com/C2SP/C2SP/blob/main/tlog-tiles.md) spec, and thus could be used to load test any tiles-based log, not just Tessera logs. If write traffic is enabled, then the target log must support `POST` requests to a `/add` path. A successful request MUST return an ASCII decimal number representing the index that has been assigned to the new value. ## UI The hammer runs using a text-based UI in the terminal that shows the current status, logs, and supports increasing/decreasing read and write traffic. The process can be killed with ``. This TUI allows for a level of interactivity when probing a new configuration of a log in order to find any cliffs where performance degrades. For real load-testing applications, especially headless runs as part of a CI pipeline, it is recommended to run the tool with `show_ui=false` in order to disable the UI. ## Usage Example usage to test a deployment of `cmd/conformance/mysql`: ```shell go run ./internal/hammer \ --log_public_key=transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW \ --log_url=http://localhost:2024 \ --max_read_ops=1024 \ --num_readers_random=128 \ --num_readers_full=128 \ --num_writers=256 \ --max_write_ops=42 ``` For a headless write-only example that could be used for integration tests, this command attempts to write 2500 leaves within 1 minute. If the target number of leaves is reached then it exits successfully. If the timeout of 1 minute is reached first, then it exits with an exit code of 1. ```shell go run ./internal/hammer \ --log_public_key=transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW \ --log_url=http://localhost:2024 \ --max_read_ops=0 \ --num_writers=512 \ --max_write_ops=512 \ --max_runtime=1m \ --leaf_write_goal=2500 \ --show_ui=false ``` # Design ## Objective Write a tool that can send write and read requests to a Tessera log in order to check the performance of writes and reads, and ensure that these logs are behaving correctly. ## Architecture ### Components Interactions with the log are performed by different implementations of worker, that are managed by separate pools: - writer: adds new leaves to the tree using a `POST` request to an `/add` endpoint - full reader: reads all leaves from the tree, starting at 0 and fetching them all - random reader: reads leaves randomly within the size of the tree All readers verify inclusion proofs against a common checkpoint, so it is cryptographically assured that they all see consistent views of the data. An important point here is that readers exercise the Tessera log via standard tlog-tiles endpoints so will work for any deployment of that spec. However, the writers exercise a `/add` endpoint that is not defined in any spec, and is simply a convenient endpoint added to the example applications to allow for this kind of testing. A production log would be very unlikely to have such an endpoint. The number of each of these workers, and the rate at which they work is configurable (both via flags and through the UI). The number of workers is configured by increasing the size of the pool, which increases concurrency. The amount of work to be performed in a given duration is controlled by a pair of throttles: one for read operations, and one for write operations. Higher level components are: - Hammer: orchestrates the work and owns the worker pools - Hammer Analyser: consumes the results of the hammer and workers to determine throughput and system health - UI: displays the current state of the system and allows some control over the number of workers The TUI is only intended to be used to interactively explore the capabilities of a log; actual load testing should be done headlessly. ## Deployment There are 2 main modes that the hammer has been designed to run in: 1. Headless, which has 2 main sub-modes: 1. Goal-oriented: given both a timeout and a target number of entries to write, process exits successfully if enough entries can be _written_ to the log before the time expires, or otherwise an error code is returned 1. Infinite: keeps running until killed in order to perform long-lived performance tests 1. TUI: runs in the console with a Text UI that allows some interactivity to tune the load characteristics and see the results transparency-dev-tessera-3cb22ee/internal/hammer/hammer.go000066400000000000000000000264311511600621500240220ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // hammer is a tool to load test a Tessera log. package main import ( "bytes" "context" "crypto/tls" "flag" "fmt" "io" "math/rand/v2" "net" "net/http" "net/http/httptrace" "net/url" "os" "strconv" "strings" "sync" "time" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/internal/hammer/loadtest" "golang.org/x/mod/sumdb/note" "golang.org/x/net/http2" "k8s.io/klog/v2" ) func init() { flag.Var(&logURL, "log_url", "Log storage root URL (can be specified multiple times), e.g. https://log.server/and/path/") flag.Var(&writeLogURL, "write_log_url", "Root URL for writing to a log (can be specified multiple times), e.g. https://log.server/and/path/ (optional, defaults to log_url)") } var ( logURL multiStringFlag writeLogURL multiStringFlag logPubKey = flag.String("log_public_key", os.Getenv("TILES_LOG_PUBLIC_KEY"), "Public key for the log. This is defaulted to the environment variable TILES_LOG_PUBLIC_KEY") maxReadOpsPerSecond = flag.Int("max_read_ops", 20, "The maximum number of read operations per second") numReadersRandom = flag.Int("num_readers_random", 4, "The number of readers looking for random leaves") numReadersFull = flag.Int("num_readers_full", 4, "The number of readers downloading the whole log") maxWriteOpsPerSecond = flag.Int("max_write_ops", 0, "The maximum number of write operations per second") numWriters = flag.Int("num_writers", 0, "The number of independent write tasks to run") leafMinSize = flag.Int("leaf_min_size", 0, "Minimum size in bytes of individual leaves") dupChance = flag.Float64("dup_chance", 0.1, "The probability of a generated leaf being a duplicate of a previous value") leafWriteGoal = flag.Int64("leaf_write_goal", 0, "Exit after writing this number of leaves, or 0 to keep going indefinitely") maxRunTime = flag.Duration("max_runtime", 0, "Fail after this amount of time has passed, or 0 to keep going indefinitely") showUI = flag.Bool("show_ui", true, "Set to false to disable the text-based UI") bearerToken = flag.String("bearer_token", "", "The bearer token for auth. For GCP this is the result of `gcloud auth print-access-token`") bearerTokenWrite = flag.String("bearer_token_write", "", "The bearer token for auth to write. For GCP this is the result of `gcloud auth print-identity-token`. If unset will default to --bearer_token.") httpTimeout = flag.Duration("http_timeout", 30*time.Second, "Timeout for HTTP requests") forceHTTP2 = flag.Bool("force_http2", false, "Use HTTP/2 connections *only*") hc *http.Client ) func main() { klog.InitFlags(nil) flag.Parse() hc = &http.Client{ Transport: &http.Transport{ MaxIdleConns: *numWriters + *numReadersFull + *numReadersRandom, MaxIdleConnsPerHost: *numWriters + *numReadersFull + *numReadersRandom, DisableKeepAlives: false, }, Timeout: *httpTimeout, } if *forceHTTP2 { hc.Transport = &http2.Transport{ // So http2.Transport doesn't complain the URL scheme isn't 'https' AllowHTTP: true, // Pretend we are dialing a TLS endpoint. (Note, we ignore the passed tls.Config) DialTLSContext: func(ctx context.Context, network, addr string, cfg *tls.Config) (net.Conn, error) { var d net.Dialer return d.DialContext(ctx, network, addr) }, } } // If bearerTokenWrite is unset, default it to whatever bearerToken has (which may too be unset). if *bearerTokenWrite == "" { *bearerTokenWrite = *bearerToken } ctx, cancel := context.WithCancel(context.Background()) logSigV, err := note.NewVerifier(*logPubKey) if err != nil { klog.Exitf("failed to create verifier: %v", err) } r := mustCreateReaders(logURL) if len(writeLogURL) == 0 { writeLogURL = logURL } w := mustCreateWriters(writeLogURL) var cpRaw []byte cons := client.UnilateralConsensus(r.ReadCheckpoint) tracker, err := client.NewLogStateTracker(ctx, r.ReadTile, cpRaw, logSigV, logSigV.Name(), cons) if err != nil { klog.Exitf("Failed to create LogStateTracker: %v", err) } // Fetch initial state of log _, _, _, err = tracker.Update(ctx) if err != nil { klog.Exitf("Failed to get initial state of the log: %v", err) } ha := loadtest.NewHammerAnalyser(func() uint64 { return tracker.Latest().Size }) ha.Run(ctx) gen := newLeafGenerator(tracker.Latest().Size, *leafMinSize, *dupChance) opts := loadtest.HammerOpts{ MaxReadOpsPerSecond: *maxReadOpsPerSecond, MaxWriteOpsPerSecond: *maxWriteOpsPerSecond, NumReadersRandom: *numReadersRandom, NumReadersFull: *numReadersFull, NumWriters: *numWriters, } hammer := loadtest.NewHammer(tracker, r.ReadEntryBundle, w, gen, ha.SeqLeafChan, ha.ErrChan, opts) exitCode := 0 if *leafWriteGoal > 0 { go func() { startTime := time.Now() goal := tracker.Latest().Size + uint64(*leafWriteGoal) klog.Infof("Will exit once tree size is at least %d", goal) tick := time.NewTicker(1 * time.Second) for { select { case <-ctx.Done(): return case <-tick.C: if tracker.Latest().Size >= goal { elapsed := time.Since(startTime) klog.Infof("Reached tree size goal of %d after %s; exiting", goal, elapsed) cancel() return } } } }() } if *maxRunTime > 0 { go func() { klog.Infof("Will fail after %s", *maxRunTime) for { select { case <-ctx.Done(): return case <-time.After(*maxRunTime): klog.Infof("Max runtime reached; exiting") exitCode = 1 cancel() return } } }() } hammer.Run(ctx) if *showUI { c := loadtest.NewController(hammer, ha) c.Run(ctx) } else { <-ctx.Done() } os.Exit(exitCode) } // newLeafGenerator returns a function that generates values to append to a log. // The leaves are constructed to be at least minLeafSize bytes long. // The generator can be used by concurrent threads. // // dupChance provides the probability that a new leaf will be a duplicate of a previous entry. // Leaves will be unique if dupChance is 0, and if set to 1 then all values will be duplicates. // startSize should be set to the initial size of the log so that repeated runs of the // hammer can start seeding leaves to avoid duplicates with previous runs. func newLeafGenerator(startSize uint64, minLeafSize int, dupChance float64) func() []byte { // genLeaf MUST be determinstic given n genLeaf := func(n uint64) []byte { // Make a slice with half the number of requested bytes since we'll // hex-encode them below which gets us back up to the full amount. filler := make([]byte, minLeafSize/2) source := rand.New(rand.NewPCG(0, n)) for i := range filler { // This throws away a lot of the generated data. An exercise to a future // coder is to fill in multiple bytes at a time. filler[i] = byte(source.Int()) } return fmt.Appendf(nil, "%x %d", filler, n) } sizeLocked := startSize var mu sync.Mutex return func() []byte { mu.Lock() thisSize := sizeLocked if thisSize > 0 && rand.Float64() <= dupChance { thisSize = rand.Uint64N(thisSize) } else { sizeLocked++ } mu.Unlock() // Do this outside of the protected block so that writers don't block on leaf generation (especially for larger leaves). return genLeaf(thisSize) } } func mustCreateReaders(us []string) loadtest.LogReader { r := []loadtest.LogReader{} for _, u := range us { if !strings.HasSuffix(u, "/") { u += "/" } rURL, err := url.Parse(u) if err != nil { klog.Exitf("Invalid log reader URL %q: %v", u, err) } switch rURL.Scheme { case "http", "https": c, err := client.NewHTTPFetcher(rURL, hc) if err != nil { klog.Exitf("Failed to create HTTP fetcher for %q: %v", u, err) } if *bearerToken != "" { c.SetAuthorizationHeader(fmt.Sprintf("Bearer %s", *bearerToken)) } r = append(r, c) case "file": r = append(r, client.FileFetcher{Root: rURL.Path}) default: klog.Exitf("Unsupported scheme %s on log URL", rURL.Scheme) } } return loadtest.NewRoundRobinReader(r) } func mustCreateWriters(us []string) loadtest.LeafWriter { w := []loadtest.LeafWriter{} for _, u := range us { if !strings.HasSuffix(u, "/") { u += "/" } u += "add" wURL, err := url.Parse(u) if err != nil { klog.Exitf("Invalid log writer URL %q: %v", u, err) } w = append(w, httpWriter(wURL, hc, *bearerTokenWrite)) } return loadtest.NewRoundRobinWriter(w) } func httpWriter(u *url.URL, hc *http.Client, bearerToken string) loadtest.LeafWriter { cTrace := &httptrace.ClientTrace{ GotConn: func(info httptrace.GotConnInfo) { klog.Infof("connection established %#v", info) }, } return func(ctx context.Context, newLeaf []byte) (uint64, error) { req, err := http.NewRequest(http.MethodPost, u.String(), bytes.NewReader(newLeaf)) if err != nil { return 0, fmt.Errorf("failed to create request: %v", err) } if bearerToken != "" { req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", bearerToken)) } reqCtx := req.Context() if klog.V(2).Enabled() { reqCtx = httptrace.WithClientTrace(req.Context(), cTrace) } resp, err := hc.Do(req.WithContext(reqCtx)) if err != nil { return 0, fmt.Errorf("failed to write leaf: %v", err) } body, err := io.ReadAll(resp.Body) _ = resp.Body.Close() if err != nil { return 0, fmt.Errorf("failed to read body: %v", err) } switch resp.StatusCode { case http.StatusOK: if resp.Request.Method != http.MethodPost { return 0, fmt.Errorf("write leaf was redirected to %s", resp.Request.URL) } // Continue below case http.StatusServiceUnavailable, http.StatusBadGateway, http.StatusGatewayTimeout, http.StatusTooManyRequests: // These status codes may indicate a delay before retrying, so handle that here: time.Sleep(retryDelay(resp.Header.Get("Retry-After"), time.Second)) return 0, fmt.Errorf("log not available. Status code: %d. Body: %q %w", resp.StatusCode, body, loadtest.ErrRetry) default: return 0, fmt.Errorf("write leaf was not OK. Status code: %d. Body: %q", resp.StatusCode, body) } parts := bytes.Split(body, []byte("\n")) index, err := strconv.ParseUint(string(parts[0]), 10, 64) if err != nil { return 0, fmt.Errorf("write leaf failed to parse response: %v", body) } return index, nil } } func retryDelay(retryAfter string, defaultDur time.Duration) time.Duration { if retryAfter == "" { return defaultDur } d, err := time.Parse(http.TimeFormat, retryAfter) if err == nil { return time.Until(d) } s, err := strconv.Atoi(retryAfter) if err == nil { return time.Duration(s) * time.Second } return defaultDur } // multiStringFlag allows a flag to be specified multiple times on the command // line, and stores all of these values. type multiStringFlag []string func (ms *multiStringFlag) String() string { return strings.Join(*ms, ",") } func (ms *multiStringFlag) Set(w string) error { *ms = append(*ms, w) return nil } transparency-dev-tessera-3cb22ee/internal/hammer/hammer_test.go000066400000000000000000000017471511600621500250640ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package main import ( "testing" ) func TestLeafGenerator(t *testing.T) { // Always generate new values gN := newLeafGenerator(0, 100, 0) vs := make(map[string]bool) for range 256 { v := string(gN()) vs[v] = true } // Always generate duplicate gD := newLeafGenerator(256, 100, 1.0) for range 256 { if !vs[string(gD())] { t.Error("Expected duplicate") } } } transparency-dev-tessera-3cb22ee/internal/hammer/loadtest/000077500000000000000000000000001511600621500240335ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/internal/hammer/loadtest/analysis.go000066400000000000000000000077761511600621500262260ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package loadtest import ( "context" "errors" "time" movingaverage "github.com/RobinUS2/golang-moving-average" "k8s.io/klog/v2" ) func NewHammerAnalyser(treeSizeFn func() uint64) *HammerAnalyser { leafSampleChan := make(chan LeafTime, 100) errChan := make(chan error, 20) return &HammerAnalyser{ treeSizeFn: treeSizeFn, SeqLeafChan: leafSampleChan, ErrChan: errChan, IntegrationTime: movingaverage.Concurrent(movingaverage.New(30)), QueueTime: movingaverage.Concurrent(movingaverage.New(30)), } } // HammerAnalyser is responsible for measuring and interpreting the result of hammering. type HammerAnalyser struct { treeSizeFn func() uint64 SeqLeafChan chan LeafTime ErrChan chan error QueueTime *movingaverage.ConcurrentMovingAverage IntegrationTime *movingaverage.ConcurrentMovingAverage } func (a *HammerAnalyser) Run(ctx context.Context) { go a.updateStatsLoop(ctx) go a.errorLoop(ctx) } func (a *HammerAnalyser) updateStatsLoop(ctx context.Context) { tick := time.NewTicker(100 * time.Millisecond) size := a.treeSizeFn() for { select { case <-ctx.Done(): return case <-tick.C: } newSize := a.treeSizeFn() if newSize <= size { continue } now := time.Now() totalLatency := time.Duration(0) queueLatency := time.Duration(0) numLeaves := 0 var sample *LeafTime ReadLoop: for { if sample == nil { select { case l, ok := <-a.SeqLeafChan: if !ok { break ReadLoop } sample = &l default: break ReadLoop } } // Stop considering leaf times once we've caught up with that cross // either the current checkpoint or "now": // - leaves with indices beyond the tree size we're considering are not integrated yet, so we can't calculate their TTI // - leaves which were queued before "now", but not assigned by "now" should also be ignored as they don't fall into this epoch (and would contribute a -ve latency if they were included). if sample.Index >= newSize || sample.AssignedAt.After(now) { break } queueLatency += sample.AssignedAt.Sub(sample.QueuedAt) // totalLatency is skewed towards being higher than perhaps it may technically be by: // - the tick interval of this goroutine, // - the tick interval of the goroutine which updates the LogStateTracker, // - any latency in writes to the log becoming visible for reads. // But it's probably good enough for now. totalLatency += now.Sub(sample.QueuedAt) numLeaves++ sample = nil } if numLeaves > 0 { a.IntegrationTime.Add(float64(totalLatency/time.Millisecond) / float64(numLeaves)) a.QueueTime.Add(float64(queueLatency/time.Millisecond) / float64(numLeaves)) } } } func (a *HammerAnalyser) errorLoop(ctx context.Context) { tick := time.NewTicker(time.Second) pbCount := 0 lastErr := "" lastErrCount := 0 for { select { case <-ctx.Done(): //context cancelled return case <-tick.C: if pbCount > 0 { klog.Warningf("%d requests received pushback from log", pbCount) pbCount = 0 } if lastErrCount > 0 { klog.Warningf("(%d x) %s", lastErrCount, lastErr) lastErrCount = 0 } case err := <-a.ErrChan: if errors.Is(err, ErrRetry) { pbCount++ continue } es := err.Error() if es != lastErr && lastErrCount > 0 { klog.Warningf("(%d x) %s", lastErrCount, lastErr) lastErr = es lastErrCount = 0 continue } lastErr = es lastErrCount++ } } } transparency-dev-tessera-3cb22ee/internal/hammer/loadtest/analysis_test.go000066400000000000000000000030321511600621500272420ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package loadtest import ( "sync" "testing" "time" ) func TestHammerAnalyser_Stats(t *testing.T) { ctx := t.Context() var treeSize treeSizeState ha := NewHammerAnalyser(treeSize.getSize) go ha.updateStatsLoop(ctx) time.Sleep(100 * time.Millisecond) baseTime := time.Now().Add(-1 * time.Minute) for i := range 10 { ha.SeqLeafChan <- LeafTime{ Index: uint64(i), QueuedAt: baseTime, AssignedAt: baseTime.Add(time.Duration(i) * time.Second), } } treeSize.setSize(10) time.Sleep(500 * time.Millisecond) avg := ha.QueueTime.Avg() if want := float64(4500); avg != want { t.Errorf("integration time avg: got != want (%f != %f)", avg, want) } } type treeSizeState struct { size uint64 mux sync.RWMutex } func (s *treeSizeState) getSize() uint64 { s.mux.RLock() defer s.mux.RUnlock() return s.size } func (s *treeSizeState) setSize(size uint64) { s.mux.Lock() defer s.mux.Unlock() s.size = size } transparency-dev-tessera-3cb22ee/internal/hammer/loadtest/client.go000066400000000000000000000045231511600621500256440ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package loadtest import ( "context" "errors" "sync" ) var ErrRetry = errors.New("retry") // NewRoundRobinReader creates a new LogReader which will spread read requests over the passed-in LogReaders. func NewRoundRobinReader(r []LogReader) LogReader { return &roundRobinReader{r: r} } // NewRoundRobinWriter creates a new LeafWriter which will spread write requests over the passed-in LeafWriters. func NewRoundRobinWriter(w []LeafWriter) LeafWriter { return (&roundRobinLeafWriter{ws: w}).Write } // roundRobinReader ensures that read requests are sent to all configured readers // using a round-robin strategy. type roundRobinReader struct { sync.Mutex idx int r []LogReader } func (rr *roundRobinReader) ReadCheckpoint(ctx context.Context) ([]byte, error) { r := rr.next() return r.ReadCheckpoint(ctx) } func (rr *roundRobinReader) ReadTile(ctx context.Context, l, i uint64, p uint8) ([]byte, error) { r := rr.next() return r.ReadTile(ctx, l, i, p) } func (rr *roundRobinReader) ReadEntryBundle(ctx context.Context, i uint64, p uint8) ([]byte, error) { r := rr.next() return r.ReadEntryBundle(ctx, i, p) } func (rr *roundRobinReader) next() LogReader { rr.Lock() defer rr.Unlock() r := rr.r[rr.idx] rr.idx = (rr.idx + 1) % len(rr.r) return r } // roundRobinLeafWriter ensures that write requests are sent to all configured // LeafWriters using a round-robin strategy. type roundRobinLeafWriter struct { sync.Mutex idx int ws []LeafWriter } func (rr *roundRobinLeafWriter) Write(ctx context.Context, newLeaf []byte) (uint64, error) { w := rr.next() return w(ctx, newLeaf) } func (rr *roundRobinLeafWriter) next() LeafWriter { rr.Lock() defer rr.Unlock() w := rr.ws[rr.idx] rr.idx = (rr.idx + 1) % len(rr.ws) return w } transparency-dev-tessera-3cb22ee/internal/hammer/loadtest/hammer.go000066400000000000000000000065021511600621500256360ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package loadtest import ( "context" "errors" "time" "github.com/transparency-dev/tessera/client" "k8s.io/klog/v2" ) type HammerOpts struct { MaxReadOpsPerSecond int MaxWriteOpsPerSecond int NumReadersRandom int NumReadersFull int NumWriters int } func NewHammer(tracker *client.LogStateTracker, f client.EntryBundleFetcherFunc, w LeafWriter, gen func() []byte, seqLeafChan chan<- LeafTime, errChan chan<- error, opts HammerOpts) *Hammer { readThrottle := NewThrottle(opts.MaxReadOpsPerSecond) writeThrottle := NewThrottle(opts.MaxWriteOpsPerSecond) randomReaders := NewWorkerPool(func() Worker { return NewLeafReader(tracker, f, RandomNextLeaf(), readThrottle.TokenChan, errChan) }) fullReaders := NewWorkerPool(func() Worker { return NewLeafReader(tracker, f, MonotonicallyIncreasingNextLeaf(), readThrottle.TokenChan, errChan) }) writers := NewWorkerPool(func() Worker { return NewLogWriter(w, gen, writeThrottle.TokenChan, errChan, seqLeafChan) }) return &Hammer{ opts: opts, randomReaders: randomReaders, fullReaders: fullReaders, writers: writers, readThrottle: readThrottle, writeThrottle: writeThrottle, tracker: tracker, } } // Hammer is responsible for coordinating the operations against the log in the form // of write and read operations. The work of analysing the results of hammering should // live outside of this class. type Hammer struct { opts HammerOpts randomReaders WorkerPool fullReaders WorkerPool writers WorkerPool readThrottle *Throttle writeThrottle *Throttle tracker *client.LogStateTracker } func (h *Hammer) Run(ctx context.Context) { // Kick off readers & writers for range h.opts.NumReadersRandom { h.randomReaders.Grow(ctx) } for range h.opts.NumReadersFull { h.fullReaders.Grow(ctx) } for range h.opts.NumWriters { h.writers.Grow(ctx) } go h.readThrottle.Run(ctx) go h.writeThrottle.Run(ctx) go h.updateCheckpointLoop(ctx) } func (h *Hammer) updateCheckpointLoop(ctx context.Context) { tick := time.NewTicker(500 * time.Millisecond) for { select { case <-ctx.Done(): return case <-tick.C: size := h.tracker.Latest().Size _, _, _, err := h.tracker.Update(ctx) if err != nil { klog.Warning(err) inconsistentErr := client.ErrInconsistency{} if errors.As(err, &inconsistentErr) { klog.Fatalf("Last Good Checkpoint:\n%s\n\nFirst Bad Checkpoint:\n%s\n\n%v", string(inconsistentErr.SmallerRaw), string(inconsistentErr.LargerRaw), inconsistentErr) } } newSize := h.tracker.Latest().Size if newSize > size { klog.V(1).Infof("Updated checkpoint from %d to %d", size, newSize) } else { klog.V(2).Infof("Checkpoint size unchanged: %d", newSize) } } } } transparency-dev-tessera-3cb22ee/internal/hammer/loadtest/throttle.go000066400000000000000000000040221511600621500262250ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package loadtest import ( "context" "fmt" "sync" "time" ) func NewThrottle(opsPerSecond int) *Throttle { return &Throttle{ opsPerSecond: opsPerSecond, TokenChan: make(chan bool, opsPerSecond), } } type Throttle struct { TokenChan chan bool mu sync.Mutex opsPerSecond int oversupply int } func (t *Throttle) Increase() { t.mu.Lock() defer t.mu.Unlock() tokenCount := t.opsPerSecond delta := max(float64(tokenCount)*0.1, 1) t.opsPerSecond = tokenCount + int(delta) } func (t *Throttle) Decrease() { t.mu.Lock() defer t.mu.Unlock() tokenCount := t.opsPerSecond if tokenCount <= 1 { return } delta := max(float64(tokenCount)*0.1, 1) t.opsPerSecond = tokenCount - int(delta) } func (t *Throttle) Run(ctx context.Context) { interval := time.Second ticker := time.NewTicker(interval) for { select { case <-ctx.Done(): //context cancelled return case <-ticker.C: ctx, cancel := context.WithTimeout(ctx, interval) t.supplyTokens(ctx) cancel() } } } func (t *Throttle) supplyTokens(ctx context.Context) { t.mu.Lock() defer t.mu.Unlock() tokenCount := t.opsPerSecond for range t.opsPerSecond { select { case t.TokenChan <- true: tokenCount-- case <-ctx.Done(): t.oversupply = tokenCount return } } t.oversupply = 0 } func (t *Throttle) String() string { return fmt.Sprintf("Current max: %d/s. Oversupply in last second: %d", t.opsPerSecond, t.oversupply) } transparency-dev-tessera-3cb22ee/internal/hammer/loadtest/tui.go000066400000000000000000000110751511600621500251670ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package loadtest import ( "context" "flag" "fmt" "strings" "time" movingaverage "github.com/RobinUS2/golang-moving-average" "github.com/gdamore/tcell/v2" "github.com/rivo/tview" "k8s.io/klog/v2" ) type tuiController struct { hammer *Hammer analyser *HammerAnalyser app *tview.Application statusView *tview.TextView logView *tview.TextView helpView *tview.TextView } func NewController(h *Hammer, a *HammerAnalyser) *tuiController { c := tuiController{ hammer: h, analyser: a, app: tview.NewApplication(), } grid := tview.NewGrid() grid.SetRows(5, 0, 10).SetColumns(0).SetBorders(true) // Top: status box statusView := tview.NewTextView() grid.AddItem(statusView, 0, 0, 1, 1, 0, 0, false) c.statusView = statusView // Middle: log view box logView := tview.NewTextView() logView.ScrollToEnd() logView.SetMaxLines(10000) grid.AddItem(logView, 1, 0, 1, 1, 0, 0, false) c.logView = logView // Bottom: help text helpView := tview.NewTextView() helpView.SetText("+/- to increase/decrease read load\n>/< to increase/decrease write load\nw/W to increase/decrease workers") grid.AddItem(helpView, 2, 0, 1, 1, 0, 0, false) c.helpView = helpView c.app.SetRoot(grid, true) return &c } func (c *tuiController) Run(ctx context.Context) { // Redirect logs to the view if err := flag.Set("logtostderr", "false"); err != nil { klog.Exitf("Failed to set flag: %v", err) } if err := flag.Set("alsologtostderr", "false"); err != nil { klog.Exitf("Failed to set flag: %v", err) } klog.SetOutput(c.logView) go c.updateStatsLoop(ctx, 500*time.Millisecond) c.app.SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey { switch event.Rune() { case '+': klog.Info("Increasing the read operations per second") c.hammer.readThrottle.Increase() case '-': klog.Info("Decreasing the read operations per second") c.hammer.readThrottle.Decrease() case '>': klog.Info("Increasing the write operations per second") c.hammer.writeThrottle.Increase() case '<': klog.Info("Decreasing the write operations per second") c.hammer.writeThrottle.Decrease() case 'w': klog.Info("Increasing the number of workers") c.hammer.randomReaders.Grow(ctx) c.hammer.fullReaders.Grow(ctx) c.hammer.writers.Grow(ctx) case 'W': klog.Info("Decreasing the number of workers") c.hammer.randomReaders.Shrink(ctx) c.hammer.fullReaders.Shrink(ctx) c.hammer.writers.Shrink(ctx) } return event }) if err := c.app.Run(); err != nil { panic(err) } } func (c *tuiController) updateStatsLoop(ctx context.Context, interval time.Duration) { formatMovingAverage := func(ma *movingaverage.ConcurrentMovingAverage) string { aMin, _ := ma.Min() aMax, _ := ma.Max() aAvg := ma.Avg() return fmt.Sprintf("%.0fms/%.0fms/%.0fms (min/avg/max)", aMin, aAvg, aMax) } ticker := time.NewTicker(interval) lastSize := c.hammer.tracker.Latest().Size maSlots := int((30 * time.Second) / interval) growth := movingaverage.New(maSlots) for { select { case <-ctx.Done(): return case <-ticker.C: s := c.hammer.tracker.Latest().Size growth.Add(float64(s - lastSize)) lastSize = s qps := growth.Avg() * float64(time.Second/interval) readWorkersLine := fmt.Sprintf("Read (%d workers): %s", c.hammer.fullReaders.Size()+c.hammer.randomReaders.Size(), c.hammer.readThrottle.String()) writeWorkersLine := fmt.Sprintf("Write (%d workers): %s", c.hammer.writers.Size(), c.hammer.writeThrottle.String()) treeSizeLine := fmt.Sprintf("TreeSize: %d (Δ %.0fqps over %ds)", s, qps, time.Duration(maSlots*int(interval))/time.Second) queueLine := fmt.Sprintf("Time-in-queue: %s", formatMovingAverage(c.analyser.QueueTime)) integrateLine := fmt.Sprintf("Observed-time-to-integrate: %s", formatMovingAverage(c.analyser.IntegrationTime)) text := strings.Join([]string{readWorkersLine, writeWorkersLine, treeSizeLine, queueLine, integrateLine}, "\n") c.statusView.SetText(text) c.app.Draw() } } } transparency-dev-tessera-3cb22ee/internal/hammer/loadtest/workerpool.go000066400000000000000000000031301511600621500265620ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package loadtest import "context" type Worker interface { Run(ctx context.Context) Kill() } // NewWorkerPool creates a simple pool of workers. // // This works well enough for the simple task we ask of it at the moment. // If we find ourselves adding more features to this, consider swapping it // for a library such as https://github.com/alitto/pond. func NewWorkerPool(factory func() Worker) WorkerPool { workers := make([]Worker, 0) pool := WorkerPool{ workers: workers, factory: factory, } return pool } // WorkerPool contains a collection of _running_ workers. type WorkerPool struct { workers []Worker factory func() Worker } func (p *WorkerPool) Grow(ctx context.Context) { w := p.factory() p.workers = append(p.workers, w) go w.Run(ctx) } func (p *WorkerPool) Shrink(ctx context.Context) { if len(p.workers) == 0 { return } w := p.workers[len(p.workers)-1] p.workers = p.workers[:len(p.workers)-1] w.Kill() } func (p *WorkerPool) Size() int { return len(p.workers) } transparency-dev-tessera-3cb22ee/internal/hammer/loadtest/workers.go000066400000000000000000000150061511600621500260600ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package loadtest import ( "context" "encoding/base64" "errors" "fmt" "math/rand/v2" "time" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/client" "k8s.io/klog/v2" ) // LeafWriter is the signature of a function which can write arbitrary data to a log. // The data to be written is provided, and the implementation must return the sequence // number at which this data will be found in the log, or an error. type LeafWriter func(ctx context.Context, data []byte) (uint64, error) type LogReader interface { ReadCheckpoint(ctx context.Context) ([]byte, error) ReadTile(ctx context.Context, l, i uint64, p uint8) ([]byte, error) ReadEntryBundle(ctx context.Context, i uint64, p uint8) ([]byte, error) } // NewLeafReader creates a LeafReader. // The next function provides a strategy for which leaves will be read. // Custom implementations can be passed, or use RandomNextLeaf or MonotonicallyIncreasingNextLeaf. func NewLeafReader(tracker *client.LogStateTracker, f client.EntryBundleFetcherFunc, next func(uint64) uint64, throttle <-chan bool, errChan chan<- error) *LeafReader { return &LeafReader{ tracker: tracker, f: f, next: next, throttle: throttle, errChan: errChan, } } // LeafReader reads leaves from the tree. // This class is not thread safe. type LeafReader struct { tracker *client.LogStateTracker f client.EntryBundleFetcherFunc next func(uint64) uint64 throttle <-chan bool errChan chan<- error cancel func() c leafBundleCache } // Run runs the log reader. This should be called in a goroutine. func (r *LeafReader) Run(ctx context.Context) { if r.cancel != nil { panic("LeafReader was ran multiple times") } ctx, r.cancel = context.WithCancel(ctx) for { select { case <-ctx.Done(): return case <-r.throttle: } size := r.tracker.Latest().Size if size == 0 { continue } i := r.next(size) if i >= size { continue } klog.V(2).Infof("LeafReader getting %d", i) _, err := r.getLeaf(ctx, i, size) if err != nil { r.errChan <- fmt.Errorf("failed to get leaf %d: %v", i, err) } } } // getLeaf fetches the raw contents committed to at a given leaf index. func (r *LeafReader) getLeaf(ctx context.Context, i uint64, logSize uint64) ([]byte, error) { if i >= logSize { return nil, fmt.Errorf("requested leaf %d >= log size %d", i, logSize) } if cached, _ := r.c.get(i); cached != nil { klog.V(2).Infof("Using cached result for index %d", i) return cached, nil } bundle, err := client.GetEntryBundle(ctx, r.f, i/layout.EntryBundleWidth, logSize) if err != nil { return nil, fmt.Errorf("failed to get entry bundle: %v", err) } ti := i % layout.EntryBundleWidth r.c = leafBundleCache{ start: i - ti, leaves: bundle.Entries, } return r.c.leaves[ti], nil } // Kill kills this leaf reader at the next opportune moment. // This function may return before the reader is dead. func (r *LeafReader) Kill() { if r.cancel != nil { r.cancel() } } // leafBundleCache stores the results of the last fetched tile. This allows // readers that read contiguous blocks of leaves to act more like real // clients and fetch a tile of 256 leaves once, instead of 256 times. type leafBundleCache struct { start uint64 leaves [][]byte } func (tc leafBundleCache) get(i uint64) ([]byte, error) { end := tc.start + uint64(len(tc.leaves)) if i >= tc.start && i < end { leaf := tc.leaves[i-tc.start] return base64.StdEncoding.DecodeString(string(leaf)) } return nil, errors.New("not found") } // RandomNextLeaf returns a function that fetches a random leaf available in the tree. func RandomNextLeaf() func(uint64) uint64 { return func(size uint64) uint64 { return rand.Uint64N(size) } } // MonotonicallyIncreasingNextLeaf returns a function that always wants the next available // leaf after the one it previously fetched. It starts at leaf 0. func MonotonicallyIncreasingNextLeaf() func(uint64) uint64 { var i uint64 return func(size uint64) uint64 { if i < size { r := i i++ return r } return size } } // LeafTime records the time at which a leaf was assigned the given index. // // This is used when sampling leaves which are added in order to later calculate // how long it took to for them to become integrated. type LeafTime struct { Index uint64 QueuedAt time.Time AssignedAt time.Time } // NewLogWriter creates a LogWriter. // u is the URL of the write endpoint for the log. // gen is a function that generates new leaves to add. func NewLogWriter(writer LeafWriter, gen func() []byte, throttle <-chan bool, errChan chan<- error, leafSampleChan chan<- LeafTime) *LogWriter { return &LogWriter{ writer: writer, gen: gen, throttle: throttle, errChan: errChan, leafChan: leafSampleChan, } } // LogWriter writes new leaves to the log that are generated by `gen`. type LogWriter struct { writer LeafWriter gen func() []byte throttle <-chan bool errChan chan<- error leafChan chan<- LeafTime cancel func() } // Run runs the log writer. This should be called in a goroutine. func (w *LogWriter) Run(ctx context.Context) { if w.cancel != nil { panic("LogWriter was run multiple times") } ctx, w.cancel = context.WithCancel(ctx) newLeaf := w.gen() for { select { case <-ctx.Done(): return case <-w.throttle: } lt := LeafTime{QueuedAt: time.Now()} index, err := w.writer(ctx, newLeaf) if err != nil { w.errChan <- fmt.Errorf("failed to create request: %w", err) continue } lt.Index, lt.AssignedAt = index, time.Now() // See if we can send a leaf sample select { // TODO: we might want to count dropped samples, and/or make sampling a bit more statistical. case w.leafChan <- lt: default: } klog.V(2).Infof("Wrote leaf at index %d", index) newLeaf = w.gen() } } // Kill kills this writer at the next opportune moment. // This function may return before the writer is dead. func (w *LogWriter) Kill() { if w.cancel != nil { w.cancel() } } transparency-dev-tessera-3cb22ee/internal/migrate/000077500000000000000000000000001511600621500223735ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/internal/migrate/migrate.go000066400000000000000000000033021511600621500243500ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package migrate contains internal implementations for migration. package migrate import "context" type MigrationWriter interface { // SetEntryBundle stores the provided serialised entry bundle at the location implied by the provided // entry bundle index and partial size. // // Bundles may be set in any order (not just consecutively), and the implementation should integrate // them into the local tree in the most efficient way possible. // // Writes should be idempotent; repeated calls to set the same bundle with the same data should not // return an error. SetEntryBundle(ctx context.Context, idx uint64, partial uint8, bundle []byte) error // AwaitIntegration should block until the local integrated tree has grown to the provided size, // and should return the locally calculated root hash derived from the integration of the contents of // entry bundles set using SetEntryBundle above. AwaitIntegration(ctx context.Context, size uint64) ([]byte, error) // IntegratedSize returns the current size of the locally integrated log. IntegratedSize(ctx context.Context) (uint64, error) } transparency-dev-tessera-3cb22ee/internal/otel/000077500000000000000000000000001511600621500217065ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/internal/otel/cast.go000066400000000000000000000016651511600621500231770ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package otel import "math" // Clamp64 casts a uint64 to an int64, clamping it at MaxInt64 if the value is above. // // Intended only for converting Tessera uint64 internal values to int64 for use with // open telemetry metrics. func Clamp64(u uint64) int64 { if u > math.MaxInt64 { return math.MaxInt64 } return int64(u) } transparency-dev-tessera-3cb22ee/internal/parse/000077500000000000000000000000001511600621500220555ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/internal/parse/parse.go000066400000000000000000000042151511600621500235200ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package parse contains internal methods for parsing data structures quickly, // if unsafely. This is a bit of a utility package which is an anti-pattern, but // this code is critical enough that it should be reused, tested, and benchmarked // rather than copied around willy nilly. // If a better home becomes available, feel free to move the contents elsewhere. package parse import ( "bytes" "encoding/base64" "fmt" "strconv" ) // CheckpointUnsafe parses a checkpoint without performing any signature verification. // This is intended to be as fast as possible, but sacrifices safety because it skips verifying // the note signature. // // Parsing a checkpoint like this is only acceptable in the same binary as the // log implementation that generated it and thus we can safely assume it's a well formed and // validly signed checkpoint. Anyone copying similar logic into client code will get hurt. func CheckpointUnsafe(rawCp []byte) (string, uint64, []byte, error) { parts := bytes.SplitN(rawCp, []byte{'\n'}, 4) if want, got := 4, len(parts); want != got { return "", 0, nil, fmt.Errorf("invalid checkpoint: %q", rawCp) } origin := string(parts[0]) sizeStr := string(parts[1]) hashStr := string(parts[2]) size, err := strconv.ParseUint(sizeStr, 10, 64) if err != nil { return "", 0, nil, fmt.Errorf("failed to turn checkpoint size of %q into uint64: %v", sizeStr, err) } hash, err := base64.StdEncoding.DecodeString(hashStr) if err != nil { return "", 0, nil, fmt.Errorf("failed to decode hash: %v", err) } return origin, size, hash, nil } transparency-dev-tessera-3cb22ee/internal/parse/parse_test.go000066400000000000000000000056411511600621500245630ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package parse_test import ( "bytes" "encoding/base64" "testing" "github.com/transparency-dev/tessera/internal/parse" ) func TestCheckpointUnsafe(t *testing.T) { testCases := []struct { desc string cp string wantOrigin string wantSize uint64 wantHash []byte wantErr bool }{ { desc: "happy checkpoint", cp: "original.example.com\n42\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n", wantOrigin: "original.example.com", wantSize: 42, wantHash: mustDecodeB64(t, "qINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs="), }, { desc: "Negative size", cp: "original.example.com\n-42\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n", wantErr: true, }, { desc: "Bad hash", cp: "original.example.com\n42\nthisisnotright\n", wantErr: true, }, { desc: "Empty origin", cp: "\n42\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n", wantOrigin: "", wantSize: 42, wantHash: mustDecodeB64(t, "qINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs="), }, { desc: "No origin", cp: "42\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n", wantErr: true, }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { origin, size, hash, err := parse.CheckpointUnsafe([]byte(tC.cp)) if gotErr := err != nil; gotErr != tC.wantErr { t.Fatalf("gotErr != wantErr (%t != %t): %v", gotErr, tC.wantErr, err) } if tC.wantErr { return } if tC.wantOrigin != origin { t.Errorf("origin: got != want (%v != %v)", origin, tC.wantOrigin) } if tC.wantSize != size { t.Errorf("size : got != want (%v != %v)", size, tC.wantSize) } if !bytes.Equal(tC.wantHash, hash) { t.Errorf("hash : got != want (%v != %v)", hash, tC.wantHash) } }) } } func mustDecodeB64(t *testing.T, encoded string) []byte { t.Helper() res, err := base64.StdEncoding.DecodeString(encoded) if err != nil { t.Fatal(err) } return res } func BenchmarkCheckpointUnsafe(b *testing.B) { cpRaw := []byte("go.sum database tree\n31700353\nqINS1GRFhWHwdkUeqLEoP4yEMkTBBzxBkGwGQlVlVcs=\n\n— sum.golang.org Az3grnmrIUEDFqHzAElIQCPNoRFRAAdFo47fooyWKMHb89k11GJh5zHIfNCOBmwn/C3YI8oW9/C8DJ87F61QqspBYwM=") for b.Loop() { _, _, _, err := parse.CheckpointUnsafe(cpRaw) if err != nil { b.Error(err) } } } transparency-dev-tessera-3cb22ee/internal/witness/000077500000000000000000000000001511600621500224375ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/internal/witness/otel.go000066400000000000000000000016601511600621500237340ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package witness import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) const name = "github.com/transparency-dev/tessera/internal/witness" var ( meter = otel.Meter(name) ) var ( witnessNameKey = attribute.Key("tessera.witness.name") witnessStatusKey = attribute.Key("tessera.witness.status") ) transparency-dev-tessera-3cb22ee/internal/witness/witness.go000066400000000000000000000331271511600621500244700ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package witness contains the implementation for sending out a checkpoint to witnesses // and retrieving sufficient signatures to satisfy a policy. package witness import ( "bytes" "context" "encoding/base64" "errors" "fmt" "io" "net/http" "strconv" "strings" "sync" "time" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/internal/parse" "go.opentelemetry.io/otel/metric" "golang.org/x/mod/sumdb/note" "k8s.io/klog/v2" ) var ( witnessReqsTotal metric.Int64Counter witnessReqHistogram metric.Int64Histogram witnessRespsTotal metric.Int64Counter // custom histogram buckets as we're interested in 10-100s of millis. witnessHistogramBuckets = []float64{0, 10, 20, 30, 40, 60, 80, 100, 120, 140, 160, 180, 200, 250, 300, 350, 400, 450, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000, 4000, 5000, 6000, 8000, 10000} ) func init() { var err error witnessReqsTotal, err = meter.Int64Counter( "tessera.witness.request", metric.WithDescription("Number of requests to the witnesses' submit endpoint"), metric.WithUnit("{call}")) if err != nil { klog.Exitf("Failed to create witnessReqsTotal metric: %v", err) } witnessReqHistogram, err = meter.Int64Histogram( "tessera.witness.duration", metric.WithDescription("Duration of calls to the witnesses' submit endpoint"), metric.WithUnit("ms"), metric.WithExplicitBucketBoundaries(witnessHistogramBuckets...)) if err != nil { klog.Exitf("Failed to create witnessReqHistogram metric: %v", err) } witnessRespsTotal, err = meter.Int64Counter( "tessera.witness.response", metric.WithDescription("Number of responses from the witnesses' submit endpoint"), metric.WithUnit("{call}")) if err != nil { klog.Exitf("Failed to create witnessRespsTotal metric: %v", err) } } var ErrPolicyNotSatisfied = errors.New("witness policy was not satisfied") // WitnessGroup defines a group of witnesses, and a threshold of // signatures that must be met for this group to be satisfied. // Witnesses within a group should be fungible, e.g. all of the Armored // Witness devices form a logical group, and N should be picked to // represent a threshold of the quorum. For some users this will be a // simple majority, but other strategies are available. // N must be <= len(WitnessKeys). type WitnessGroup interface { // Satisfied returns true if the checkpoint provided is signed by this witness. // This will return false if there is no signature, and also if the // checkpoint cannot be read as a valid note. It is up to the caller to ensure // that the input value represents a valid note. Satisfied(cp []byte) bool // Endpoints returns the details required for updating a witness and checking the // response. The returned result is a map from the URL that should be used to update // the witness with a new checkpoint, to the value which is the verifier to check // the response is well formed. Endpoints() map[string]note.Verifier } // NewWitnessGateway returns a WitnessGateway that will send out new checkpoints to witnesses // in the group, and will ensure that the policy is satisfied before returning. All outbound // requests will be done using the given client. The tile fetcher is used for constructing // consistency proofs for the witnesses. func NewWitnessGateway(group WitnessGroup, client *http.Client, oldSize uint64, fetchTiles client.TileFetcherFunc) WitnessGateway { endpoints := group.Endpoints() witnesses := make([]*witness, 0, len(endpoints)) for u, v := range endpoints { witnesses = append(witnesses, &witness{ client: client, url: u, verifier: v, size: oldSize, }) } return WitnessGateway{ group: group, witnesses: witnesses, fetchTile: fetchTiles, } } // WitnessGateway allows a log implementation to send out a checkpoint to witnesses. type WitnessGateway struct { group WitnessGroup witnesses []*witness fetchTile client.TileFetcherFunc } // Witness sends out a new checkpoint (which must be signed by the log), to all witnesses // and returns the checkpoint as soon as the policy the WitnessGateway was constructed with // is Satisfied. func (wg *WitnessGateway) Witness(ctx context.Context, cp []byte) ([]byte, error) { if len(wg.witnesses) == 0 { return cp, nil } ctx, cancel := context.WithCancel(ctx) defer cancel() var waitGroup sync.WaitGroup _, size, _, err := parse.CheckpointUnsafe(cp) if err != nil { return nil, fmt.Errorf("failed to parse checkpoint from log: %v", err) } pb, err := client.NewProofBuilder(ctx, size, wg.fetchTile) if err != nil { return nil, fmt.Errorf("failed to build proof builder: %v", err) } pf := sharedConsistencyProofFetcher{ pb: pb, toSize: size, results: make(map[uint64]consistencyFuture), } type sigOrErr struct { sig []byte err error } results := make(chan sigOrErr) // Kick off a goroutine for each witness and send result to results chan for _, w := range wg.witnesses { waitGroup.Add(1) go func() { defer waitGroup.Done() sig, err := w.update(ctx, cp, size, pf.ConsistencyProof) results <- sigOrErr{ sig: sig, err: err, } }() } go func() { waitGroup.Wait() close(results) }() // Consume the results coming back from each witness var sigBlock bytes.Buffer sigBlock.Write(cp) for r := range results { if r.err != nil { err = errors.Join(err, r.err) continue } // Some basic validation, which can be extended if needed. if !bytes.HasSuffix(r.sig, []byte("\n")) { err = errors.Join(err, fmt.Errorf("invalid signature from witness: %q", r.sig)) continue } // Add new signature to the new note we're building sigBlock.Write(r.sig) // See whether the group is satisfied now if newCp := sigBlock.Bytes(); wg.group.Satisfied(newCp) { return newCp, nil } } // We can only get here if all witnesses have returned and we're still not satisfied. return sigBlock.Bytes(), errors.Join(ErrPolicyNotSatisfied, err) } type consistencyFuture func() ([][]byte, error) // sharedConsistencyProofFetcher is a thread-safe caching wrapper around a proof builder. // This is an optimization for the common case where multiple witnesses are used, and all // of the witnesses are of the same size, and thus require the same proof. type sharedConsistencyProofFetcher struct { pb *client.ProofBuilder toSize uint64 mu sync.Mutex results map[uint64]consistencyFuture } // ConsistencyProof constructs a consistency proof, reusing any results from parallel requests. func (pf *sharedConsistencyProofFetcher) ConsistencyProof(ctx context.Context, smaller, larger uint64) ([][]byte, error) { if larger != pf.toSize { return nil, fmt.Errorf("required larger size to be %d but was given %d", pf.toSize, larger) } var f consistencyFuture var ok bool pf.mu.Lock() if f, ok = pf.results[smaller]; !ok { f = sync.OnceValues(func() ([][]byte, error) { return pf.pb.ConsistencyProof(ctx, smaller, larger) }) pf.results[smaller] = f } pf.mu.Unlock() return f() } // witness is the log's model of a witness's view of this log. // It has a URL which is the address to which updates to this log's state can be posted to the witness, // using the https://github.com/C2SP/C2SP/blob/main/tlog-witness.md spec. // It also has the size of the checkpoint that the log thinks that the witness last signed. // This is important for sending update proofs. // This is defaulted to zero on startup and calibrated after the first request, which is expected by the spec: // `If a client doesn't have information on the latest cosigned checkpoint, it MAY initially make a request with a old size of zero to obtain it` type witness struct { client *http.Client url string verifier note.Verifier size uint64 } func (w *witness) update(ctx context.Context, cp []byte, size uint64, fetchProof func(ctx context.Context, from, to uint64) ([][]byte, error)) ([]byte, error) { var proof [][]byte if w.size > 0 { var err error proof, err = fetchProof(ctx, w.size, size) if err != nil { return nil, fmt.Errorf("fetchProof: %v", err) } } // The request body MUST be a sequence of // - a previous size line, // - zero or more consistency proof lines, // - and an empty line, // - followed by a [checkpoint][]. body := fmt.Sprintf("old %d\n", w.size) for _, p := range proof { body += base64.StdEncoding.EncodeToString(p) + "\n" } body += "\n" body += string(cp) start := time.Now() nameAttr := witnessNameKey.String(w.verifier.Name()) witnessReqsTotal.Add(ctx, 1, metric.WithAttributes(nameAttr)) req, err := http.NewRequestWithContext(ctx, http.MethodPost, w.url, strings.NewReader(body)) if err != nil { return nil, fmt.Errorf("failed to construct request to %q: %v", w.url, err) } httpResp, err := w.client.Do(req) if err != nil { return nil, fmt.Errorf("failed to post to witness at %q: %v", w.url, err) } defer func() { _ = httpResp.Body.Close() }() rb, err := io.ReadAll(httpResp.Body) if err != nil { return nil, fmt.Errorf("failed to read body from witness at %q: %v", w.url, err) } statusAttr := witnessStatusKey.Int(httpResp.StatusCode) witnessRespsTotal.Add(ctx, 1, metric.WithAttributes(nameAttr, statusAttr)) d := time.Since(start) witnessReqHistogram.Record(ctx, d.Milliseconds(), metric.WithAttributes(nameAttr, statusAttr)) switch httpResp.StatusCode { case http.StatusOK: // Concatenate the signature to the checkpoint passed in and verify it is valid. // append is tempting here but is dangerous because it can modify `cp` and race with other // witnesses, causing signatures to be swapped. cp must not be modified. signed := make([]byte, len(cp)+len(rb)) copy(signed, cp) copy(signed[len(cp):], rb) if n, err := note.Open(signed, note.VerifierList(w.verifier)); err != nil { return nil, fmt.Errorf("witness %q at %q replied with invalid signature: %q\nconstructed note: %q\nerror: %v", w.verifier.Name(), w.url, rb, string(signed), err) } else { w.size = uint64(size) return fmt.Appendf(nil, "— %s %s\n", n.Sigs[0].Name, n.Sigs[0].Base64), nil } case http.StatusConflict: // Two cases here: the first is a situation we can recover from, the second isn't. // The witness MUST check that the old size matches the size of the latest checkpoint it cosigned // for the checkpoint's origin (or zero if it never cosigned a checkpoint for that origin). // If it doesn't match, the witness MUST respond with a "409 Conflict" HTTP status code. // The response body MUST consist of the tree size of the latest cosigned checkpoint in decimal, // followed by a newline (U+000A). The response MUST have a Content-Type of text/x.tlog.size ct := httpResp.Header["Content-Type"] if len(ct) == 1 && ct[0] == "text/x.tlog.size" { bodyStr := strings.TrimSpace(string(rb)) newWitSize, err := strconv.ParseUint(bodyStr, 10, 64) if err != nil { return nil, fmt.Errorf("witness at %q replied with x.tlog.size but body %q could not be parsed as decimal", w.url, bodyStr) } // This should _never_ happen - the witness has a larger tree size than the log knows about! if newWitSize > size { return nil, fmt.Errorf("witness at %q replied with x.tlog.size %d, larger than log size %d", w.url, newWitSize, size) } klog.Infof("Witness at %q replied with x.tlog.size %d != our hint %d, retrying", w.url, newWitSize, w.size) w.size = newWitSize // Witnesses could cause this recursion to go on for longer than expected if the value they kept returning // this case with slightly larger values. Consider putting a max recursion cap if context timeout isn't enough. return w.update(ctx, cp, size, fetchProof) } // If the old size matches the checkpoint size, the witness MUST check that the root hashes are also identical. // If they don't match, the witness MUST respond with a "409 Conflict" HTTP status code. return nil, fmt.Errorf("witness at %q says old root hash did not match previous for size %d: %d", w.url, w.size, httpResp.StatusCode) case http.StatusNotFound: // If the checkpoint origin is unknown, the witness MUST respond with a "404 Not Found" HTTP status code. return nil, fmt.Errorf("witness at %q says checkpoint origin is unknown: %d", w.url, httpResp.StatusCode) case http.StatusForbidden: // If none of the signatures verify against a trusted public key, the witness MUST respond with a "403 Forbidden" HTTP status code. return nil, fmt.Errorf("witness at %q says no signatures verify against trusted public key: %d", w.url, httpResp.StatusCode) case http.StatusBadRequest: // The old size MUST be equal to or lower than the checkpoint size. // Otherwise, the witness MUST respond with a "400 Bad Request" HTTP status code. return nil, fmt.Errorf("witness at %q says old checkpoint size of %d is too large: %d", w.url, w.size, httpResp.StatusCode) case http.StatusUnprocessableEntity: // If the Merkle Consistency Proof doesn't verify, the witness MUST respond with a "422 Unprocessable Entity" HTTP status code. return nil, fmt.Errorf("witness at %q says that the consistency proof is bad: %d", w.url, httpResp.StatusCode) default: return nil, fmt.Errorf("got bad status code: %v", httpResp.StatusCode) } } transparency-dev-tessera-3cb22ee/internal/witness/witness_test.go000066400000000000000000000412001511600621500255160ustar00rootroot00000000000000// Copyright 2025 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package witness_test import ( "bytes" "context" "fmt" "io" "net/http" "net/http/httptest" "net/url" "os" "path/filepath" "strconv" "strings" "sync/atomic" "testing" "github.com/transparency-dev/formats/log" f_note "github.com/transparency-dev/formats/note" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/internal/witness" "github.com/transparency-dev/tessera/storage/posix" "golang.org/x/mod/sumdb/note" ) const ( logVkey = "example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx" wit1Vkey = "Wit1+55ee4561+AVhZSmQj9+SoL+p/nN0Hh76xXmF7QcHfytUrI1XfSClk" wit1Skey = "PRIVATE+KEY+Wit1+55ee4561+AeadRiG7XM4XiieCHzD8lxysXMwcViy5nYsoXURWGrlE" wit2Vkey = "Wit2+85ecc407+AWVbwFJte9wMQIPSnEnj4KibeO6vSIOEDUTDp3o63c2x" wit2Skey = "PRIVATE+KEY+Wit2+85ecc407+AfPTvxw5eUcqSgivo2vaiC7JPOMUZ/9baHPSDrWqgdGm" witBadVkey = "WitBad+b82b4b16+AY5FLOcqxs5lD+OpC6cVTrxsyNJktaCGYHNfnE5vKBQX" witBadSkey = "PRIVATE+KEY+WitBad+b82b4b16+AYSil2PKfSN1a0LhdbzmK1uXqDFZbp+P1OyR54k3gdJY" ) var ( logVerifier = mustCreateVerifier(logVkey) ) func TestWitnessGateway_Update(t *testing.T) { logSignedCheckpoint, cp := loadCheckpoint(t, 9) // Set up a fake server hosting the witnesses. // The witnesses just sign the checkpoint with whatever key is requested, they don't check the body at all. // An improvement on this would be to make the fake witnesses more realistic, but it's a non-trivial // amount of code to add to this already long test! var wit1 tessera.Witness var wit2 tessera.Witness var witBad tessera.Witness ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w1u, err := url.Parse(wit1.URL) if err != nil { t.Fatal(err) } w2u, err := url.Parse(wit2.URL) if err != nil { t.Fatal(err) } wbu, err := url.Parse(witBad.URL) if err != nil { t.Fatal(err) } switch r.URL.String() { case w1u.Path: _, _ = w.Write(sigForSigner(t, cp, wit1Skey)) case w2u.Path: _, _ = w.Write(sigForSigner(t, cp, wit2Skey)) case wbu.Path: _, _ = w.Write([]byte("this is not a signature\n")) default: t.Fatalf("Unknown case: %s", r.URL.String()) } })) baseURL, err := url.Parse(ts.URL) if err != nil { t.Fatal(err) } wit1, err = tessera.NewWitness(wit1Vkey, baseURL.JoinPath("wit1")) if err != nil { t.Fatal(err) } wit2, err = tessera.NewWitness(wit2Vkey, baseURL.JoinPath("wit2")) if err != nil { t.Fatal(err) } witBad, err = tessera.NewWitness(witBadVkey, baseURL) if err != nil { t.Fatal(err) } testCases := []struct { desc string group tessera.WitnessGroup wantSigs int wantErr bool }{ { desc: "no witnesses", group: tessera.WitnessGroup{}, wantSigs: 0, }, { desc: "one optional witness", group: tessera.NewWitnessGroup(0, wit1), wantSigs: 0, }, { desc: "two optional witnesses", group: tessera.NewWitnessGroup(0, wit1, wit2), wantSigs: 0, }, { desc: "one required witness", group: tessera.NewWitnessGroup(1, wit1), wantSigs: 1, }, { desc: "one required witness out of 2", group: tessera.NewWitnessGroup(1, wit1, wit2), wantSigs: 1, }, { desc: "two required witnesses", group: tessera.NewWitnessGroup(2, wit1, wit2), wantSigs: 2, }, { desc: "one required witness twice", group: tessera.NewWitnessGroup(2, wit1, wit1), wantSigs: 1, }, { desc: "bad witness", group: tessera.NewWitnessGroup(1, witBad), wantErr: true, }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { ctx := context.Background() g := witness.NewWitnessGateway(tC.group, ts.Client(), 0, testLogTileFetcher) witnessedCP, err := g.Witness(ctx, logSignedCheckpoint) if got, want := err != nil, tC.wantErr; got != want { t.Fatalf("got != want (%t != %t): %v", got, want, err) } if tC.wantErr { return } n, err := note.Open(witnessedCP, note.VerifierList(logVerifier, wit1.Key, wit2.Key)) if err != nil { t.Fatalf("failed to open note %q: %v", witnessedCP, err) } if len(n.Sigs)-1 < tC.wantSigs { t.Errorf("wanted %d sigs but got %d", tC.wantSigs, len(n.Sigs)-1) } }) } } func TestWitness_UpdateRequest(t *testing.T) { logSignedCheckpoint, _ := loadCheckpoint(t, 9) d, err := posix.New(context.Background(), posix.Config{Path: "../../testdata/log/"}) if err != nil { t.Fatal(err) } _, _, reader, err := tessera.NewAppender(context.Background(), d, tessera.NewAppendOptions().WithCheckpointSigner(mustCreateCoSigSigner(t, wit1Skey))) if err != nil { t.Fatal(err) } testCases := []struct { desc string proof [][]byte witSize uint64 wantErr bool wantBody string }{ { desc: "size 0 no proof needed", witSize: 0, wantBody: fmt.Sprintf("old 0\n\n%s", logSignedCheckpoint), }, { desc: "non zero size requires proof", witSize: 6, wantBody: fmt.Sprintf("old 6\nycRkkNklus5eMVRUvkD1pK321vMrA+jjOiZKU8aOcY4=\nnk9gCR+floFqznAPtqjjcnnV64dge2jQB95D5t164Hg=\nzY1lN35vrXYAPixXSd59LsU29xUJtuW4o2dNNg5Y2Co=\n91HQqaPzWlbBsUDk3JvSpOTK7Bc4ifZGxXZzfABOmuU=\n\n%s", logSignedCheckpoint), }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { ctx := context.Background() var gotBody string var initDone bool ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !initDone { w.Header().Add("Content-Type", "text/x.tlog.size") w.WriteHeader(409) _, _ = fmt.Fprintf(w, "%d", tC.witSize) initDone = true return } body, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } gotBody = string(body) _, checkpoint, ok := bytes.Cut(body, []byte("\n\n")) if !ok { t.Fatalf("expected two newlines in body, got: %q", body) } _, _, n, err := log.ParseCheckpoint(checkpoint, logVerifier.Name(), logVerifier) if err != nil { t.Fatal(err) } _, _ = w.Write(sigForSigner(t, n.Text, wit1Skey)) })) baseURL := mustURL(t, ts.URL) var err error wit1, err := tessera.NewWitness(wit1Vkey, baseURL) if err != nil { t.Fatal(err) } group := tessera.NewWitnessGroup(1, wit1) wg := witness.NewWitnessGateway(group, ts.Client(), 0, reader.ReadTile) _, err = wg.Witness(ctx, logSignedCheckpoint) if got, want := err != nil, tC.wantErr; got != want { t.Fatalf("got != want (%t != %t): %v", got, want, err) } if tC.wantErr { return } if gotBody != tC.wantBody { t.Errorf("body does not match expected (want vs got):\n%q\n%q", tC.wantBody, gotBody) } }) } } func TestWitness_UpdateResponse(t *testing.T) { logSignedCheckpoint, cp := loadCheckpoint(t, 9) sig1 := sigForSigner(t, cp, wit1Skey) sig2 := sigForSigner(t, cp, wit2Skey) testCases := []struct { desc string statusCode int body []byte pre error wantErr bool wantResult []byte }{ { desc: "all good", statusCode: 200, body: sig1, wantResult: sig1, }, { desc: "all good, two sigs", statusCode: 200, body: append(sig1, sig2...), wantResult: sig1, }, { desc: "404 is an error", statusCode: 404, wantErr: true, }, { desc: "403 is an error", statusCode: 403, wantErr: true, }, { desc: "422 is an error", statusCode: 422, wantErr: true, }, { desc: "409 with no headers is error", statusCode: 409, wantErr: true, }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { ctx := context.Background() ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(tC.statusCode) _, _ = w.Write(tC.body) })) baseURL := mustURL(t, ts.URL) wit1, err := tessera.NewWitness(wit1Vkey, baseURL) if err != nil { t.Fatal(err) } g := witness.NewWitnessGateway(tessera.NewWitnessGroup(1, wit1), ts.Client(), 0, testLogTileFetcher) witnessed, err := g.Witness(ctx, logSignedCheckpoint) if got, want := err != nil, tC.wantErr; got != want { t.Fatalf("got != want (%t != %t): %v", got, want, err) } if tC.wantErr { return } sigs := witnessed[len(logSignedCheckpoint):] if !bytes.Equal(sigs, tC.wantResult) { t.Errorf("expected result %q but got %q", tC.body, sigs) } }) } } func TestWitnessConflict(t *testing.T) { for _, test := range []struct { name string witnessSeen uint64 oldSizeHint uint64 wantErr bool }{ { name: "nothing seen before", }, { name: "correct hint", witnessSeen: 8, oldSizeHint: 8, }, { name: "hint stale - witness missed an update", witnessSeen: 4, oldSizeHint: 8, }, { name: "log rolled back - witness is ahead of log", witnessSeen: 20, wantErr: true, }, } { t.Run(test.name, func(t *testing.T) { logSignedCheckpoint, cp := loadCheckpoint(t, 9) var wit1 tessera.Witness ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w1u := mustURL(t, wit1.URL) if got, want := r.URL.String(), w1u.Path; got != want { t.Fatalf("got request to URL %q but expected %q", got, want) } body, err := io.ReadAll(r.Body) if err != nil { t.Fatalf("error reading body: %v", err) } lines := bytes.Split(body, []byte("\n")) if len(lines) == 0 { t.Fatal("empty body") } bits := strings.Split(string(lines[0]), " ") if len(bits) != 2 || bits[0] != "old" { t.Fatal("invalid old line") } oldSize, err := strconv.ParseUint(bits[1], 10, 64) if err != nil { t.Fatalf("Invalid old size %v", err) } if oldSize != test.witnessSeen { t.Logf("Saw stale %d != %d", oldSize, test.witnessSeen) w.Header().Add("Content-Type", "text/x.tlog.size") w.WriteHeader(409) _, _ = fmt.Fprintf(w, "%d\n", test.witnessSeen) return } _, _ = w.Write(sigForSigner(t, cp, wit1Skey)) test.witnessSeen = oldSize })) baseURL := mustURL(t, ts.URL) var err error wit1, err = tessera.NewWitness(wit1Vkey, baseURL) if err != nil { t.Fatal(err) } group := tessera.NewWitnessGroup(1, wit1) g := witness.NewWitnessGateway(group, ts.Client(), test.oldSizeHint, testLogTileFetcher) _, err = g.Witness(t.Context(), logSignedCheckpoint) if gotErr := err != nil; gotErr != test.wantErr { t.Fatalf("Got err %v, want err %t", err, test.wantErr) } }) } } func TestWitnessStateEvolution(t *testing.T) { logSignedCheckpoint, cp := loadCheckpoint(t, 9) // Set up a fake server hosting the witnesses. // The witnesses just sign the checkpoint with whatever key is requested, they don't check the body at all. // An improvement on this would be to make the fake witnesses more realistic, but it's a non-trivial // amount of code to add to this already long test! var wit1 tessera.Witness var count int ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w1u := mustURL(t, wit1.URL) if got, want := r.URL.String(), w1u.Path; got != want { t.Fatalf("got request to URL %q but expected %q", got, want) } switch count { case 0: w.Header().Add("Content-Type", "text/x.tlog.size") w.WriteHeader(409) _, _ = w.Write([]byte("8")) case 1: body, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } if !bytes.HasPrefix(body, []byte("old 8")) { t.Fatalf("expected body to start with old 8 but got\n%v", body) } _, _ = w.Write(sigForSigner(t, cp, wit1Skey)) case 2: body, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } if !bytes.HasPrefix(body, []byte("old 9")) { t.Fatalf("expected body to start with old 9 but got\n%v", string(body)) } // End of test; we don't even bother constructing a valid response here } count++ })) baseURL := mustURL(t, ts.URL) var err error wit1, err = tessera.NewWitness(wit1Vkey, baseURL) if err != nil { t.Fatal(err) } group := tessera.NewWitnessGroup(1, wit1) ctx := context.Background() g := witness.NewWitnessGateway(group, ts.Client(), 0, testLogTileFetcher) // This call will trigger case 0 and then case 1 in the witness handler above. // case 0 will return a response that notifies the log that its view of the witness size is wrong. // This method will then update its size and make a second request with a consistency proof, triggering case 1. _, err = g.Witness(ctx, logSignedCheckpoint) if err != nil { t.Fatal(err) } // This triggers case 2 in the witness, which isn't implemented so we don't care about any error, // we just invoke this to cause the validation in that witness body to trigger. _, _ = g.Witness(ctx, logSignedCheckpoint) } func TestWitnessReusesProofs(t *testing.T) { var wit1, wit2 tessera.Witness ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { t.Fatal(err) } _, checkpoint, ok := bytes.Cut(body, []byte("\n\n")) if !ok { t.Fatalf("expected two newlines in body, got: %q", body) } _, _, n, err := log.ParseCheckpoint(checkpoint, logVerifier.Name(), logVerifier) if err != nil { t.Fatal(err) } w1u := mustURL(t, wit1.URL) w2u := mustURL(t, wit2.URL) switch r.URL.String() { case w1u.Path: _, _ = w.Write(sigForSigner(t, n.Text, wit1Skey)) case w2u.Path: _, _ = w.Write(sigForSigner(t, n.Text, wit2Skey)) default: t.Fatalf("Unknown case: %s", r.URL.String()) } })) baseURL := mustURL(t, ts.URL) var err error wit1, err = tessera.NewWitness(wit1Vkey, baseURL.JoinPath("wit1")) if err != nil { t.Fatal(err) } wit2, err = tessera.NewWitness(wit2Vkey, baseURL.JoinPath("wit2")) if err != nil { t.Fatal(err) } ctx := context.Background() var tf1 atomic.Int32 var tf2 atomic.Int32 cf1 := func(ctx context.Context, level, index uint64, p uint8) ([]byte, error) { tf1.Add(1) return testLogTileFetcher(ctx, level, index, p) } cf2 := func(ctx context.Context, level, index uint64, p uint8) ([]byte, error) { tf2.Add(1) return testLogTileFetcher(ctx, level, index, p) } g1 := witness.NewWitnessGateway(tessera.NewWitnessGroup(1, wit1), ts.Client(), 0, cf1) g2 := witness.NewWitnessGateway(tessera.NewWitnessGroup(2, wit1, wit2), ts.Client(), 0, cf2) for i := range 10 { logSignedCheckpoint, _ := loadCheckpoint(t, i) _, err = g1.Witness(ctx, logSignedCheckpoint) if err != nil { t.Fatal(err) } _, err = g2.Witness(ctx, logSignedCheckpoint) if err != nil { t.Fatal(err) } } if got1, got2 := tf1.Load(), tf2.Load(); got1 != got2 { t.Errorf("expected same number of tiles loaded for 1 witness or 2 witnesses but got (%d != %d)", got1, got2) } } func loadCheckpoint(t *testing.T, size int) (signed []byte, unsigned string) { t.Helper() path := fmt.Sprintf("../../testdata/log/checkpoint.%d", size) cp, err := os.ReadFile(path) if err != nil { t.Fatal(err) } _, _, n, err := log.ParseCheckpoint(cp, logVerifier.Name(), logVerifier) if err != nil { t.Fatal(err) } return cp, n.Text } // testLogTileFetcher is a fetcher which reads tiles from the checked-in golden test log // data stored in $REPO_ROOT/testdata/log func testLogTileFetcher(ctx context.Context, l, i uint64, p uint8) ([]byte, error) { path := filepath.Join("../../testdata/log", layout.TilePath(l, i, p)) return os.ReadFile(path) } func mustURL(t *testing.T, u string) *url.URL { t.Helper() parsed, err := url.Parse(u) if err != nil { t.Fatal(err) } return parsed } func sigForSigner(t *testing.T, cp, skey string) []byte { t.Helper() s, err := f_note.NewSignerForCosignatureV1(skey) if err != nil { t.Fatal(err) } witSignedCheckpoint, err := note.Sign(¬e.Note{Text: cp}, s) if err != nil { t.Fatal(err) } return append(bytes.Trim(witSignedCheckpoint[len(cp):], "\n"), '\n') } func mustCreateVerifier(vkey string) note.Verifier { verifier, err := note.NewVerifier(vkey) if err != nil { panic(err) } return verifier } func mustCreateCoSigSigner(t *testing.T, skey string) note.Signer { t.Helper() signer, err := f_note.NewSignerForCosignatureV1(skey) if err != nil { t.Fatal(err) } return signer } transparency-dev-tessera-3cb22ee/lifecycle.go000066400000000000000000000132711511600621500214210ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "context" "crypto/sha256" "fmt" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api" ) // LogReader provides read-only access to the log. type LogReader interface { // ReadCheckpoint returns the latest checkpoint available. // If no checkpoint is available then os.ErrNotExist should be returned. ReadCheckpoint(ctx context.Context) ([]byte, error) // ReadTile returns the raw marshalled tile at the given coordinates, if it exists. // The expected usage for this method is to derive the parameters from a tree size // that has been committed to by a checkpoint returned by this log. Whenever such a // tree size is used, this method will behave as per the https://c2sp.org/tlog-tiles // spec for the /tile/ path. // // If callers pass in parameters that are not implied by a published tree size, then // implementations _may_ act differently from one another, but all will act in ways // that are allowed by the spec. For example, if the only published tree size has been // for size 2, then asking for a partial tile of 1 may lead to some implementations // returning not found, some may return a tile with 1 leaf, and some may return a tile // with more leaves. ReadTile(ctx context.Context, level, index uint64, p uint8) ([]byte, error) // ReadEntryBundle returns the raw marshalled leaf bundle at the given coordinates, if // it exists. // The expected usage and corresponding behaviours are similar to ReadTile. ReadEntryBundle(ctx context.Context, index uint64, p uint8) ([]byte, error) // NextIndex returns the first as-yet unassigned index. // // In a quiescent log, this will be the same as the checkpoint size. In a log with entries actively // being added, this number will be higher since it will take sequenced but not-yet-integrated/not-yet-published // entries into account. NextIndex(ctx context.Context) (uint64, error) // IntegratedSize returns the current size of the integrated tree. // // This tree will have in place all the static resources the returned size implies, but // there may not yet be a checkpoint for this size signed, witnessed, or published. // // It's ONLY safe to use this value for processes internal to the operation of the log (e.g. // populating antispam data structures); it MUST NOT not be used as a substitute for // reading the checkpoint when only data which has been publicly committed to by the // log should be used. If in doubt, use ReadCheckpoint instead. IntegratedSize(ctx context.Context) (uint64, error) } // Follower describes the contract of an entity which tracks the contents of the local log. // // Currently, this is only used by anti-spam. type Follower interface { // Name returns a human readable name for this follower. Name() string // Follow should be implemented so as to visit entries in the log in order, using the provided // LogReader to access the entry bundles which contain them. // // Implementations should keep track of their progress such that they can pick-up where they left off // if e.g. the binary is restarted. Follow(context.Context, LogReader) // EntriesProcessed reports the progress of the follower, returning the total number of log entries // successfully seen/processed. EntriesProcessed(context.Context) (uint64, error) } // Antispam describes the contract that an antispam implementation must meet in order to be used via the // WithAntispam option below. type Antispam interface { // Decorator must return a function which knows how to decorate an Appender's Add function in order // to return an index previously assigned to an entry with the same identity hash, if one exists, or // delegate to the next Add function in the chain otherwise. Decorator() func(AddFn) AddFn // Follower should return a structure which will populate the anti-spam index by tailing the contents // of the log, using the provided function to turn entry bundles into identity hashes. Follower(func(entryBundle []byte) ([][]byte, error)) Follower } // identityHash calculates the antispam identity hash for the provided (single) leaf entry data. func identityHash(data []byte) []byte { h := sha256.Sum256(data) return h[:] } // defaultIDHasher returns a list of identity hashes corresponding to entries in the provided bundle. // Currently, these are simply SHA256 hashes of the raw byte of each entry. func defaultIDHasher(bundle []byte) ([][]byte, error) { eb := &api.EntryBundle{} if err := eb.UnmarshalText(bundle); err != nil { return nil, fmt.Errorf("unmarshal: %v", err) } r := make([][]byte, 0, len(eb.Entries)) for _, e := range eb.Entries { h := identityHash(e) r = append(r, h[:]) } return r, nil } // defaultMerkleLeafHasher parses a C2SP tlog-tile bundle and returns the Merkle leaf hashes of each entry it contains. func defaultMerkleLeafHasher(bundle []byte) ([][]byte, error) { eb := &api.EntryBundle{} if err := eb.UnmarshalText(bundle); err != nil { return nil, fmt.Errorf("unmarshal: %v", err) } r := make([][]byte, 0, len(eb.Entries)) for _, e := range eb.Entries { h := rfc6962.DefaultHasher.HashLeaf(e) r = append(r, h[:]) } return r, nil } transparency-dev-tessera-3cb22ee/log.go000066400000000000000000000043161511600621500202430ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "errors" "fmt" ) var ( // ErrPushback is returned by underlying storage implementations when a new entry cannot be accepted // due to overload in the system. This could be because there are too many entries with indices assigned // but which have not yet been integrated into the tree, or it could be because the antispam mechanism // is not able to keep up with recently added entries. It should always be wrapped with a more // specific error to provide context to clients. // // Personalities encountering this error should apply back-pressure to the source of new entries // in an appropriate manner (e.g. for HTTP services, return a 503 with a Retry-After header). // // Personalities should check for this error (wrapped or not) using `errors.Is(e, ErrPushback)`. ErrPushback = errors.New("pushback") // ErrPushbackAntispam is a wrapped ErrPushback. It is returned by underlying storage implementations // when an entry cannot be accepted becasue the antispam follower has fallen too far behind the size // of the integrated tree. ErrPushbackAntispam = fmt.Errorf("antispam %w", ErrPushback) // ErrPushbackIntegration is a wrapped ErrPushback. It is returned by underlying storage implementations // when an entry cannot be accepted becasue there are too many "in-flight" add requests - i.e. entries // with sequence numbers assigned, but which are not yet integrated into the log. ErrPushbackIntegration = fmt.Errorf("integration %w", ErrPushback) ) // Driver is the implementation-specific parts of Tessera. No methods are on here as this is not for public use. type Driver any transparency-dev-tessera-3cb22ee/migrate.go000066400000000000000000000100661511600621500211110ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "context" "fmt" "sync/atomic" "github.com/cenkalti/backoff/v5" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/client" "golang.org/x/sync/errgroup" "k8s.io/klog/v2" ) type setEntryBundleFunc func(ctx context.Context, index uint64, partial uint8, bundle []byte) error func newCopier(numWorkers uint, setEntryBundle setEntryBundleFunc, getEntryBundle client.EntryBundleFetcherFunc) *copier { return &copier{ setEntryBundle: setEntryBundle, getEntryBundle: getEntryBundle, todo: make(chan bundle, numWorkers), } } // copier controls the migration work. type copier struct { setEntryBundle setEntryBundleFunc getEntryBundle client.EntryBundleFetcherFunc // todo contains work items to be completed. todo chan bundle // bundlesCopied is the number of entry bundles copied so far. bundlesCopied atomic.Uint64 } // bundle represents the address of an individual entry bundle. type bundle struct { Index uint64 Partial uint8 } // Copy starts the work of copying sourceSize entries from the source to the target log. // // Only the entry bundles are copied as the target storage is expected to integrate them and recalculate the root. // This is done to ensure the correctness of both the source log as well as the copy process itself. // // A call to this function will block until either the copying is done, or an error has occurred. func (c *copier) Copy(ctx context.Context, fromSize uint64, sourceSize uint64) error { klog.Infof("Starting copy from %d to source size %d", fromSize, sourceSize) if fromSize > sourceSize { return fmt.Errorf("from size %d > source size %d", fromSize, sourceSize) } go c.populateWork(fromSize, sourceSize) // Do the copying eg := errgroup.Group{} for range cap(c.todo) { eg.Go(func() error { return c.worker(ctx) }) } if err := eg.Wait(); err != nil { return fmt.Errorf("copy failed: %v", err) } return nil } // Progress returns the number of bundles from the source present in the target. func (c *copier) BundlesCopied() uint64 { return c.bundlesCopied.Load() } // populateWork sends entries to the `todo` work channel. // Each entry corresponds to an individual entryBundle which needs to be copied. func (m *copier) populateWork(from, treeSize uint64) { klog.Infof("Spans for entry range [%d, %d)", from, treeSize) defer close(m.todo) for ri := range layout.Range(from, treeSize-from, treeSize) { m.todo <- bundle{Index: ri.Index, Partial: ri.Partial} } } // worker undertakes work items from the `todo` channel. // // It will attempt to retry failed operations several times before giving up, this should help // deal with any transient errors which may occur. func (m *copier) worker(ctx context.Context) error { for b := range m.todo { n, err := backoff.Retry(ctx, func() (uint64, error) { d, err := m.getEntryBundle(ctx, b.Index, uint8(b.Partial)) if err != nil { wErr := fmt.Errorf("failed to fetch entrybundle %d (p=%d): %v", b.Index, b.Partial, err) klog.Infof("%v", wErr) return 0, wErr } if err := m.setEntryBundle(ctx, b.Index, b.Partial, d); err != nil { wErr := fmt.Errorf("failed to store entrybundle %d (p=%d): %v", b.Index, b.Partial, err) klog.Infof("%v", wErr) return 0, wErr } return 1, nil }, backoff.WithMaxTries(10), backoff.WithBackOff(backoff.NewExponentialBackOff())) if err != nil { klog.Infof("retry: %v", err) return err } m.bundlesCopied.Add(n) } return nil } transparency-dev-tessera-3cb22ee/migrate_lifecycle.go000066400000000000000000000155701511600621500231350ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "bytes" "context" "fmt" "strings" "time" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/internal/migrate" "golang.org/x/sync/errgroup" "k8s.io/klog/v2" ) // NewMigrationTarget returns a MigrationTarget, which allows a personality to "import" a C2SP // tlog-tiles or static-ct compliant log into a Tessera instance. func NewMigrationTarget(ctx context.Context, d Driver, opts *MigrationOptions) (*MigrationTarget, error) { type migrateLifecycle interface { MigrationWriter(context.Context, *MigrationOptions) (migrate.MigrationWriter, LogReader, error) } lc, ok := d.(migrateLifecycle) if !ok { return nil, fmt.Errorf("driver %T does not implement MigrationTarget lifecycle", d) } mw, r, err := lc.MigrationWriter(ctx, opts) if err != nil { return nil, fmt.Errorf("failed to init MigrationTarget lifecycle: %v", err) } return &MigrationTarget{ writer: mw, reader: r, followers: opts.followers, }, nil } func NewMigrationOptions() *MigrationOptions { return &MigrationOptions{ entriesPath: layout.EntriesPath, bundleIDHasher: defaultIDHasher, bundleLeafHasher: defaultMerkleLeafHasher, } } // MigrationOptions holds migration lifecycle settings for all storage implementations. type MigrationOptions struct { // entriesPath knows how to format entry bundle paths. entriesPath func(n uint64, p uint8) string // bundleIDHasher knows how to create antispam leaf identities for entries in a serialised bundle. // This field's value must not be updated once configured or weird and probably unwanted antispam behaviour is likely to occur. bundleIDHasher func([]byte) ([][]byte, error) // bundleLeafHasher knows how to create Merkle leaf hashes for the entries in a serialised bundle. // This field's value must not be updated once configured or weird and probably unwanted integration behaviour is likely to occur. bundleLeafHasher func([]byte) ([][]byte, error) followers []Follower } func (o MigrationOptions) EntriesPath() func(uint64, uint8) string { return o.entriesPath } func (o *MigrationOptions) LeafHasher() func([]byte) ([][]byte, error) { return o.bundleLeafHasher } // WithAntispam configures the migration target to *populate* the provided antispam storage using // the data being migrated into the target tree. // // Note that since the tree is being _migrated_, the resulting target tree must match the structure // of the source tree and so no attempt is made to reject/deduplicate entries. func (o *MigrationOptions) WithAntispam(as Antispam) *MigrationOptions { if as != nil { o.followers = append(o.followers, as.Follower(o.bundleIDHasher)) } return o } // MigrationTarget handles the process of migrating/importing a source log into a Tessera instance. type MigrationTarget struct { writer migrate.MigrationWriter reader LogReader followers []Follower } // Migrate performs the work of importing a source log into the local Tessera instance. // // Any entry bundles implied by the provided source log size which are not already present in the local log // will be fetched using the provided getEntries function, and stored by the underlying driver. // A background process will continuously attempt to integrate these bundles into the local tree. // // An error will be returned if there is an unrecoverable problem encountered during the migration // process, or if, once all entries have been copied and integrated into the local tree, the local // root hash does not match the provided sourceRoot. func (mt *MigrationTarget) Migrate(ctx context.Context, numWorkers uint, sourceSize uint64, sourceRoot []byte, getEntries client.EntryBundleFetcherFunc) error { cctx, cancel := context.WithCancel(ctx) defer cancel() c := newCopier(numWorkers, mt.writer.SetEntryBundle, getEntries) fromSize, err := mt.writer.IntegratedSize(ctx) if err != nil { return fmt.Errorf("fetching integrated size failed: %v", err) } c.bundlesCopied.Store(fromSize / layout.EntryBundleWidth) // Print stats go func() { bundlesToCopy := (sourceSize / layout.EntryBundleWidth) if bundlesToCopy == 0 { return } for { select { case <-cctx.Done(): return case <-time.After(time.Second): } s, err := mt.writer.IntegratedSize(ctx) if err != nil { klog.Warningf("Size: %v", err) } info := []string{} bn := c.BundlesCopied() info = append(info, progress("copy", bn, bundlesToCopy)) info = append(info, progress("integration", s, sourceSize)) for _, f := range mt.followers { p, err := f.EntriesProcessed(ctx) if err != nil { klog.Infof("%s EntriesProcessed(): %v", f.Name(), err) continue } info = append(info, progress(f.Name(), p, sourceSize)) } klog.Infof("Progress: %s", strings.Join(info, ", ")) } }() // go integrate errG := errgroup.Group{} errG.Go(func() error { return c.Copy(cctx, fromSize, sourceSize) }) var calculatedRoot []byte errG.Go(func() error { r, err := mt.writer.AwaitIntegration(cctx, sourceSize) if err != nil { return fmt.Errorf("awaiting integration failed: %v", err) } calculatedRoot = r return nil }) for _, f := range mt.followers { klog.Infof("Starting %s follower", f.Name()) go f.Follow(cctx, mt.reader) errG.Go(awaitFollower(cctx, f, sourceSize)) } if err := errG.Wait(); err != nil { return fmt.Errorf("migrate failed: %v", err) } if !bytes.Equal(calculatedRoot, sourceRoot) { return fmt.Errorf("migration completed, but local root hash %x != source root hash %x", calculatedRoot, sourceRoot) } klog.Infof("Migration successful.") return nil } // awaitFollower returns a function which will block until the provided follower has processed // at least as far as the provided index. func awaitFollower(ctx context.Context, f Follower, i uint64) func() error { return func() error { for { select { case <-ctx.Done(): return nil case <-time.After(time.Second): } pos, err := f.EntriesProcessed(ctx) if err != nil { klog.Infof("%s EntriesProcessed(): %v", f.Name(), err) continue } if pos >= i { klog.Infof("%s follower complete", f.Name()) return nil } } } } func progress(n string, p, total uint64) string { return fmt.Sprintf("%s: %d (%.2f%%)", n, p, (float64(p*100) / float64(total))) } transparency-dev-tessera-3cb22ee/otel.go000066400000000000000000000016001511600621500204160ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) const name = "github.com/transparency-dev/tessera" var ( tracer = otel.Tracer(name) meter = otel.Meter(name) ) var ( followerNameKey = attribute.Key("tessera.follower.name") ) transparency-dev-tessera-3cb22ee/storage/000077500000000000000000000000001511600621500205735ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/storage/aws/000077500000000000000000000000001511600621500213655ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/storage/aws/README.md000066400000000000000000000126051511600621500226500ustar00rootroot00000000000000# Tessera on Amazon Web Services This document describes the storage implementation for running Tessera on Amazon Web Services (AWS). ## Overview This design takes advantage of Amazon S3 for long-term storage and low-cost, low-complexity serving of read traffic. It uses Amazon Aurora (MySQL) for coordinating writes. New entries flow in from the binary built with Tessera into transactional storage, where they're held temporarily to batch them up, and then assigned sequence numbers as each batch is flushed. This allows the `Add` API call to quickly return with *durably assigned* sequence numbers. From there, an async process derives the entry bundles and Merkle tree structure from the sequenced batches, writes these to GCS for serving, before finally removing integrated bundles from the transactional storage. Since entries are all sequenced by the time they're stored, and sequencing is done in "chunks", it's worth noting that all tree derivations are therefore idempotent. ## Transactional storage The transactional storage is implemented with Aurora MySQL, and uses a schema with the following tables: * `Tessera`: This table is used to identify the current schema version. * `SeqCoord`: A table with a single row which is used to keep track of the next assignable sequence number. * `Seq`: This holds batches of entries keyed by the sequence number assigned to the first entry in the batch. * `IntCoord`: This table is used to coordinate integration of sequenced batches in the `Seq` table, and keeps track of the current tree state. * `PubCoord`: This table is used to coordinate publication of new checkpoints, ensuring that checkpoints are not published more frequently than configured. * `GCCoord`: This table is used to coordinate garbage collection of partial tiles and entry bundles which have been made obsolete by the continued growth of the log. ## Life of a leaf 1. Leaves are submitted by the binary built using Tessera via a call the storage's `Add` func. 1. The storage library batches these entries up, and, after a configurable period of time has elapsed or the batch reaches a configurable size threshold, the batch is written to the `Seq` table which effectively assigns a sequence numbers to the entries using the following algorithm: In a transaction: 1. selects next from `SeqCoord` with for update ← this blocks other FE from writing their pools, but only for a short duration. 1. Inserts batch of entries into `Seq` with key `SeqCoord.next` 1. Update `SeqCoord` with `next+=len(batch)` 1. Newly sequenced entries are periodically appended to the tree: In a transaction: 1. select `seq` from `IntCoord` with for update ← this blocks other integrators from proceeding. 1. Select one or more consecutive batches from `Seq` for update, starting at `IntCoord.seq` 1. Write leaf bundles to S3 using batched entries 1. Integrate in Merkle tree and write tiles to S3 1. Update checkpoint in S3 1. Delete consumed batches from `Seq` 1. Update `IntCoord` with `seq+=num_entries_integrated` and the latest `rootHash` 1. Checkpoints representing the latest state of the tree are published at the configured interval. ## Antispam Two experimental implementations have been tested which uses either Aurora MySQL, or a local bbolt database to store the `` --> `sequence` mapping. They work well, but call for further stress testing and cost analysis. ## Compatibility This storage implementation is intended to be used with AWS services. However, given that it's based on services which are compatible with MySQL and S3 protocols, it's possible that it will work with other non-AWS-based backends which are compatible with these protocols. Given the vast array of combinations of backend implementations and versions, using this storage implementation outside of AWS isn't officially supported, although there may be folks who can help with issues in the Transparency-Dev slack. Similarly, PRs raised against it relating to its use outside of AWS are unlikely to be accepted unless it's shown that they have no detremental effect to the implementation's performance on AWS. ### Alternatives considered Other transactional storage systems are available on AWS, e.g. Redshift, RDS or DynamoDB. Experiments were run using Aurora (MySQL, Serverless v2), RDS (MySQL), and DynamoDB. Aurora (MySQL) worked out to be a good compromise between cost, performance, operational overhead, code complexity, and so was selected. The alpha implementation was tested with entries of size 1KB each, at a write rate of 1500/s. This was done using the smallest possible Aurora instance available, `db.r5.large`, running `8.0.mysql_aurora.3.05.2`. Aurora (Serverless v2) worked out well, but seems less cost effective than provisioned Aurora for sustained traffic. For now, we decided not to explore this option further. RDS (MySQL) worked out well, but requires more administrative overhead than Aurora. For now, we decided not to explore this option further. DynamoDB worked out to be less cost efficient than Aurora and RDS. It also has constraints that introduced a non trivial amount of complexity: max object size is 400KB, max transaction size is {4MB OR 25 rows for write OR 100 rows for reads}, binary values must be base64 encoded, arrays of bytes are marshaled as sets by default (as of Dec. 2024). We decided not to explore this option further. transparency-dev-tessera-3cb22ee/storage/aws/antispam/000077500000000000000000000000001511600621500232015ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/storage/aws/antispam/aws.go000066400000000000000000000320721511600621500243260ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package aws contains an AWS-based antispam implementation for Tessera. // // A MySQL database provides a mechanism for maintaining an index of // hash --> log position for detecting duplicate submissions. package aws import ( "context" "database/sql" "errors" "fmt" "iter" "strings" "sync/atomic" "time" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/client" "k8s.io/klog/v2" _ "github.com/go-sql-driver/mysql" ) const ( DefaultMaxBatchSize = 64 DefaultPushbackThreshold = 2048 // SchemaCompatibilityVersion represents the expected version (e.g. layout & serialisation) of stored data. // // A binary built with a given version of the Tessera library is compatible with stored data created by a different version // of the library if and only if this value is the same as the compatibilityVersion stored in the Tessera table. // // NOTE: if changing this version, you need to consider whether end-users are going to update their schema instances to be // compatible with the new format, and provide a means to do it if so. SchemaCompatibilityVersion = 1 ) // AntispamOpts allows configuration of some tunable options. type AntispamOpts struct { // MaxBatchSize is the largest number of mutations permitted in a single write operation when // updating the antispam index. // // Larger batches can enable (up to a point) higher throughput, but care should be taken not to // overload the database instance. MaxBatchSize uint // PushbackThreshold allows configuration of when to start responding to Add requests with pushback due to // the antispam follower falling too far behind. // // When the antispam follower is at least this many entries behind the size of the locally integrated tree, // the antispam decorator will return a wrapped tessera.ErrPushback for every Add request. PushbackThreshold uint PushbackMaxOutstanding uint64 MaxOpenConns int MaxIdleConns int } type AntispamStorage struct { opts AntispamOpts dbPool *sql.DB // pushBack is used to prevent the follower from getting too far underwater. // Populate dynamically will set this to true/false based on how far behind the follower is from the // currently integrated tree size. // When pushBack is true, the decorator will start returning a wrapped ErrPushback to all calls. pushBack atomic.Bool numLookups atomic.Uint64 numWrites atomic.Uint64 numHits atomic.Uint64 } // NewAntispam returns an antispam driver which uses a MySQL table to maintain a mapping of // previously seen entries and their assigned indices. // // Note that the storage for this mapping is entirely separate and unconnected to the storage used for // maintaining the Merkle tree. // // This functionality is experimental! func NewAntispam(ctx context.Context, dsn string, opts AntispamOpts) (*AntispamStorage, error) { if opts.MaxBatchSize == 0 { opts.MaxBatchSize = DefaultMaxBatchSize } if opts.PushbackThreshold == 0 { opts.PushbackThreshold = DefaultPushbackThreshold } dbPool, err := sql.Open("mysql", dsn) if err != nil { return nil, fmt.Errorf("failed to connect to MySQL db: %v", err) } if opts.MaxOpenConns > 0 { dbPool.SetMaxOpenConns(opts.MaxOpenConns) } if opts.MaxIdleConns >= 0 { dbPool.SetMaxIdleConns(opts.MaxIdleConns) } if err := dbPool.Ping(); err != nil { return nil, fmt.Errorf("failed to ping MySQL db: %v", err) } r := &AntispamStorage{ opts: opts, dbPool: dbPool, } if err := r.initDB(ctx); err != nil { return nil, fmt.Errorf("failed to initDB: %v", err) } if err := r.checkDataCompatibility(ctx); err != nil { return nil, fmt.Errorf("schema is not compatible with this version of the Tessera library: %v", err) } return r, nil } func (s *AntispamStorage) initDB(ctx context.Context) error { if _, err := s.dbPool.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS AntispamMeta ( id INT UNSIGNED NOT NULL, compatibilityVersion BIGINT UNSIGNED NOT NULL, PRIMARY KEY (id) )`); err != nil { return err } if _, err := s.dbPool.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS AntispamIDSeq ( h TINYBLOB NOT NULL, idx BIGINT UNSIGNED NOT NULL, PRIMARY KEY (h(32)) )`); err != nil { return err } if _, err := s.dbPool.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS AntispamFollowCoord ( id INT UNSIGNED NOT NULL, nextIdx BIGINT UNSIGNED NOT NULL, PRIMARY KEY (id) )`); err != nil { return err } // Set default values for a newly initialised schema - these rows being present are a precondition for // following and population of mapping data to occur. // Note that this will only succeed if no row exists, so there's no danger // of "resetting" an existing antispam database. if _, err := s.dbPool.ExecContext(ctx, `INSERT IGNORE INTO AntispamMeta (id, compatibilityVersion) VALUES (0, ?)`, SchemaCompatibilityVersion); err != nil { return err } if _, err := s.dbPool.ExecContext(ctx, `INSERT IGNORE INTO AntispamFollowCoord (id, nextIdx) VALUES (0, 0)`); err != nil { return err } return nil } // checkDataCompatibility compares the Tessera library SchemaCompatibilityVersion with the one stored in the // database, and returns an error if they are not identical. func (s *AntispamStorage) checkDataCompatibility(ctx context.Context) error { row := s.dbPool.QueryRowContext(ctx, "SELECT compatibilityVersion FROM AntispamMeta WHERE id = 0") var gotVersion uint64 if err := row.Scan(&gotVersion); err != nil { return fmt.Errorf("failed to read schema compatibility version from DB: %v", err) } if gotVersion != SchemaCompatibilityVersion { return fmt.Errorf("schema compatibilityVersion (%d) != library compatibilityVersion (%d)", gotVersion, SchemaCompatibilityVersion) } return nil } // index returns the index (if any) previously associated with the provided hash func (d *AntispamStorage) index(ctx context.Context, h []byte) (*uint64, error) { d.numLookups.Add(1) row := d.dbPool.QueryRowContext(ctx, "SELECT idx FROM AntispamIDSeq WHERE h = ?", h) var idx uint64 if err := row.Scan(&idx); err == sql.ErrNoRows { return nil, nil } d.numHits.Add(1) return &idx, nil } // Decorator returns a function which will wrap an underlying Add delegate with // code to dedup against the stored data. func (d *AntispamStorage) Decorator() func(f tessera.AddFn) tessera.AddFn { return func(delegate tessera.AddFn) tessera.AddFn { return func(ctx context.Context, e *tessera.Entry) tessera.IndexFuture { if d.pushBack.Load() { // The follower is too far behind the currently integrated tree, so we're going to push back against // the incoming requests. // This should have two effects: // 1. The tree will cease growing, giving the follower a chance to catch up, and // 2. We'll stop doing lookups for each submission, freeing up the DB to catch up. // // We may decide in the future that serving duplicate reads is more important than catching up as quickly // as possible, in which case we'd move this check down below the call to index. return func() (tessera.Index, error) { return tessera.Index{}, tessera.ErrPushbackAntispam } } idx, err := d.index(ctx, e.Identity()) if err != nil { return func() (tessera.Index, error) { return tessera.Index{}, err } } if idx != nil { return func() (tessera.Index, error) { return tessera.Index{Index: *idx, IsDup: true}, nil } } return delegate(ctx, e) } } } // Follower returns a follower which knows how to populate the antispam index. // // This implements tessera.Antispam. func (d *AntispamStorage) Follower(b func([]byte) ([][]byte, error)) tessera.Follower { return &follower{ as: d, bundleHasher: b, } } // follower is a struct which knows how to populate the antispam storage with identity hashes // for entries in a log. type follower struct { as *AntispamStorage bundleHasher func([]byte) ([][]byte, error) } func (f *follower) Name() string { return "AWS antispam" } // Follow uses entry data from the log to populate the antispam storage. func (f *follower) Follow(ctx context.Context, lr tessera.LogReader) { errOutOfSync := errors.New("out-of-sync") t := time.NewTicker(time.Second) var ( next func() (client.Entry[[]byte], error, bool) stop func() ) for { select { case <-ctx.Done(): return case <-t.C: } // logSize is the latest known size of the log we're following. // This will get initialised below, inside the loop. var logSize uint64 // Busy loop while there's work to be done for streamDone := false; !streamDone; { select { case <-ctx.Done(): return default: } err := func() error { tx, err := f.as.dbPool.BeginTx(ctx, &sql.TxOptions{ ReadOnly: false, }) if err != nil { return err } defer func() { if tx != nil { _ = tx.Rollback() } }() row := tx.QueryRowContext(ctx, "SELECT nextIdx FROM AntispamFollowCoord WHERE id = 0 FOR UPDATE") var followFrom uint64 if err := row.Scan(&followFrom); err != nil { return err } if followFrom >= logSize { // Our view of the log is out of date, update it logSize, err = lr.IntegratedSize(ctx) if err != nil { streamDone = true return fmt.Errorf("populate: IntegratedSize(): %v", err) } switch { case followFrom > logSize: streamDone = true return fmt.Errorf("followFrom %d > size %d", followFrom, logSize) case followFrom == logSize: // We're caught up, so unblock pushback and go back to sleep streamDone = true f.as.pushBack.Store(false) return nil default: // size > followFrom, so there's more work to be done! } } f.as.pushBack.Store(logSize-followFrom > uint64(f.as.opts.PushbackThreshold)) // If this is the first time around the loop we need to start the stream of entries now that we know where we want to // start reading from: if next == nil { sizeFn := func(_ context.Context) (uint64, error) { return logSize, nil } numFetchers := uint(10) next, stop = iter.Pull2(client.Entries(client.EntryBundles(ctx, numFetchers, sizeFn, lr.ReadEntryBundle, followFrom, logSize-followFrom), f.bundleHasher)) } bs := uint64(f.as.opts.MaxBatchSize) if r := logSize - followFrom; r < bs { bs = r } curEntries := make([][]byte, 0, bs) for i := range int(bs) { e, err, ok := next() if !ok { // The entry stream has ended so we'll need to start a new stream next time around the loop: stop() next = nil break } if err != nil { return fmt.Errorf("entryReader.next: %v", err) } if wantIdx := followFrom + uint64(i); e.Index != wantIdx { // We're out of sync return errOutOfSync } curEntries = append(curEntries, e.Entry) } if len(curEntries) == 0 { return nil } klog.V(1).Infof("Inserting %d entries into antispam database (follow from %d of size %d)", len(curEntries), followFrom, logSize) args := make([]string, 0, len(curEntries)) vals := make([]any, 0, 2*len(curEntries)) for i, e := range curEntries { args = append(args, "(?, ?)") vals = append(vals, e, followFrom+uint64(i)) } sqlStr := fmt.Sprintf("INSERT IGNORE INTO AntispamIDSeq (h, idx) VALUES %s", strings.Join(args, ",")) _, err = tx.ExecContext(ctx, sqlStr, vals...) if err != nil { return fmt.Errorf("failed to insert into AntispamIDSeq with query %q: %v", sqlStr, err) } numAdded := uint64(len(curEntries)) f.as.numWrites.Add(numAdded) nextIdx := uint64(followFrom + numAdded) // Insertion of dupe entries was successful, so update our follow coordination row: _, err = tx.ExecContext(ctx, "UPDATE AntispamFollowCoord SET nextIdx=? WHERE id=0", nextIdx) if err != nil { return fmt.Errorf("error updating AntispamFollowCoord: %v", err) } if err := tx.Commit(); err != nil { return err } tx = nil return nil }() if err != nil { if err != errOutOfSync { klog.Errorf("Failed to commit antispam population tx: %v", err) } if next != nil { stop() next = nil stop = nil } streamDone = true continue } } } } // EntriesProcessed returns the total number of log entries processed. func (f *follower) EntriesProcessed(ctx context.Context) (uint64, error) { row := f.as.dbPool.QueryRowContext(ctx, "SELECT nextIdx FROM AntispamFollowCoord WHERE id = 0") var idx uint64 if err := row.Scan(&idx); err != nil { return 0, fmt.Errorf("failed to read follow coordination info: %v", err) } return idx, nil } transparency-dev-tessera-3cb22ee/storage/aws/antispam/aws_test.go000066400000000000000000000146021511600621500253640ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package aws import ( "context" "crypto/sha256" "database/sql" "flag" "os" "testing" "time" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/testonly" "k8s.io/klog/v2" ) var ( mySQLURI = flag.String("mysql_uri", "root:root@tcp(localhost:3306)/test_tessera", "Connection string for a MySQL database") isMySQLTestOptional = flag.Bool("is_mysql_test_optional", true, "Boolean value to control whether the MySQL test is optional") ) // TestMain inits flags and runs tests. func TestMain(m *testing.M) { klog.InitFlags(nil) // m.Run() will parse flags os.Exit(m.Run()) } func TestAntispam(t *testing.T) { ctx := t.Context() if canSkipMySQLTest(t, ctx) { klog.Warningf("MySQL not available, skipping %s", t.Name()) t.Skip("MySQL not available, skipping test") } mustDropTables(t, ctx) as, err := NewAntispam(ctx, *mySQLURI, AntispamOpts{}) if err != nil { t.Fatal(err) } fl, shutdown := testonly.NewTestLog(t, tessera.NewAppendOptions().WithCheckpointInterval(time.Second)) defer func() { if err := shutdown(t.Context()); err != nil { t.Logf("shutdown: %v", err) } }() addFn := as.Decorator()(fl.Appender.Add) follower := as.Follower(testBundleHasher) go follower.Follow(ctx, fl.LogReader) pos, err := follower.EntriesProcessed(ctx) if err != nil { t.Fatal(err) } if pos != 0 { t.Error("expected initial position to be 0") } a := tessera.NewPublicationAwaiter(t.Context(), fl.LogReader.ReadCheckpoint, time.Second) var idx1 tessera.Index idxf1 := addFn(ctx, tessera.NewEntry([]byte("one"))) if _, _, err := a.Await(t.Context(), idxf1); err != nil { t.Fatalf("Await(1): %v", err) } idxf2 := addFn(ctx, tessera.NewEntry([]byte("two"))) if _, _, err := a.Await(t.Context(), idxf2); err != nil { t.Fatalf("Await(2): %v", err) } if idx1, err = idxf1(); err != nil { t.Fatal(err) } if _, err := idxf2(); err != nil { t.Fatal(err) } for { if idx, err := follower.EntriesProcessed(ctx); err != nil { t.Fatal(err) } else if idx == 2 { break } } dupIdx, err := addFn(ctx, tessera.NewEntry([]byte("one")))() if err != nil { t.Error(err) } if !dupIdx.IsDup { t.Error("expected dupe but it wasn't marked as such") } if dupIdx.Index != idx1.Index { t.Errorf("expected idx %d but got %d", idx1.Index, dupIdx.Index) } } func TestAntispamPushbackRecovers(t *testing.T) { ctx := t.Context() if canSkipMySQLTest(t, ctx) { klog.Warningf("MySQL not available, skipping %s", t.Name()) t.Skip("MySQL not available, skipping test") } mustDropTables(t, ctx) as, err := NewAntispam(ctx, *mySQLURI, AntispamOpts{ PushbackThreshold: 1, }) if err != nil { t.Fatal(err) } fl, shutdown := testonly.NewTestLog(t, tessera.NewAppendOptions().WithCheckpointInterval(time.Second)) defer func() { if err := shutdown(t.Context()); err != nil { t.Logf("shutdown: %v", err) } }() addFn := as.Decorator()(fl.Appender.Add) follower := as.Follower(testBundleHasher) a := tessera.NewPublicationAwaiter(t.Context(), fl.LogReader.ReadCheckpoint, time.Second) idxf1 := addFn(ctx, tessera.NewEntry([]byte("one"))) if _, _, err := a.Await(t.Context(), idxf1); err != nil { t.Fatalf("Await(1): %v", err) } idxf2 := addFn(ctx, tessera.NewEntry([]byte("two"))) if _, _, err := a.Await(t.Context(), idxf2); err != nil { t.Fatalf("Await(2): %v", err) } go follower.Follow(ctx, fl.LogReader) for { time.Sleep(time.Second) if idx, err := follower.EntriesProcessed(ctx); err != nil { t.Fatal(err) } else if idx == 2 { break } } // Ensure that the follower gets itself _out_ of pushback mode once it's caught up. // We'll give the follower some time to do its thing and notice. // It runs onces a second, so this should be plenty of time. for i := range 5 { time.Sleep(time.Second) if !as.pushBack.Load() { t.Logf("Antispam caught up and out of pushback in %ds", i) return } } t.Fatalf("pushBack remains true after 5 seconds despite being caught up!") } // canSkipMySQLTest checks if the test MySQL db is available and, if not, if the test can be skipped. // // Use this method before every MySQL test, and if it returns true, skip the test. // // If is_mysql_test_optional is set to true and MySQL database cannot be opened or pinged, // the test will fail immediately. Otherwise, the test will be skipped if the test is optional // and the database is not available. func canSkipMySQLTest(t *testing.T, ctx context.Context) bool { t.Helper() db, err := sql.Open("mysql", *mySQLURI) if err != nil { if *isMySQLTestOptional { return true } t.Fatalf("failed to open MySQL test db: %v", err) } defer func() { if err := db.Close(); err != nil { t.Fatalf("failed to close MySQL database: %v", err) } }() if err := db.PingContext(ctx); err != nil { if *isMySQLTestOptional { return true } t.Fatalf("failed to ping MySQL test db: %v", err) } return false } // mustDropTables drops the `Seq`, `SeqCoord` and `IntCoord` tables. // Call this function before every MySQL test. func mustDropTables(t *testing.T, ctx context.Context) { t.Helper() db, err := sql.Open("mysql", *mySQLURI) if err != nil { t.Fatalf("failed to connect to db: %v", *mySQLURI) } defer func() { if err := db.Close(); err != nil { t.Fatalf("failed to close db: %v", err) } }() if _, err := db.ExecContext(ctx, "DROP TABLE IF EXISTS `AntispamMeta`, `AntispamIDSeq`, `AntispamFollowCoord`"); err != nil { t.Fatalf("failed to drop all tables: %v", err) } } func testIDHash(d []byte) []byte { r := sha256.Sum256(d) return r[:] } func testBundleHasher(b []byte) ([][]byte, error) { bun := &api.EntryBundle{} err := bun.UnmarshalText(b) if err != nil { return nil, err } r := make([][]byte, len(bun.Entries)) for i, e := range bun.Entries { r[i] = testIDHash(e) } return r, err } transparency-dev-tessera-3cb22ee/storage/aws/aws.go000066400000000000000000001464601511600621500225210ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package aws contains an AWS-based storage implementation for Tessera. // // TODO: decide whether to rename this package. // // This storage implementation uses S3 for long-term storage and serving of // entry bundles and log tiles, and MySQL for coordinating updates to AWS // when multiple instances of a personality binary are running. // // A single S3 bucket is used to hold entry bundles and log internal tiles. // The object keys for the bucket are selected so as to conform to the // expected layout of a tile-based log. // // A MySQL database provides a transactional mechanism to allow multiple // frontends to safely update the contents of the log. package aws import ( "bytes" "compress/gzip" "context" "database/sql" "encoding/base64" "encoding/gob" "errors" "fmt" "io" "net/http" "os" "path/filepath" "strings" "sync" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/smithy-go" "github.com/google/go-cmp/cmp" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/internal/fetcher" "github.com/transparency-dev/tessera/internal/migrate" "github.com/transparency-dev/tessera/internal/parse" storage "github.com/transparency-dev/tessera/storage/internal" "golang.org/x/sync/errgroup" "k8s.io/klog/v2" "github.com/go-sql-driver/mysql" ) const ( logContType = "application/octet-stream" ckptContType = "text/plain; charset=utf-8" logCacheControl = "max-age=604800,immutable" ckptCacheControl = "no-cache" minCheckpointInterval = time.Second DefaultPushbackMaxOutstanding = 4096 DefaultIntegrationSizeLimit = 5 * 4096 // SchemaCompatibilityVersion represents the expected version (e.g. layout & serialisation) of stored data. // // A binary built with a given version of the Tessera library is compatible with stored data created by a different version // of the library if and only if this value is the same as the compatibilityVersion stored in the Tessera table. // // NOTE: if changing this version, you need to consider whether end-users are going to update their schema instances to be // compatible with the new format, and provide a means to do it if so. SchemaCompatibilityVersion = 1 ) // Storage is an AWS based storage implementation for Tessera. type Storage struct { cfg Config } // objStore describes a type which can store and retrieve objects. type objStore interface { getObject(ctx context.Context, obj string) ([]byte, error) setObject(ctx context.Context, obj string, data []byte, contType string, cacheControl string) error setObjectIfNoneMatch(ctx context.Context, obj string, data []byte, contType string, cacheControl string) error deleteObjectsWithPrefix(ctx context.Context, prefix string) error } // sequencer describes a type which knows how to sequence entries. type sequencer interface { // assignEntries should durably allocate contiguous index numbers to the provided entries. assignEntries(ctx context.Context, entries []*tessera.Entry) error // consumeEntries should call the provided function with up to limit previously sequenced entries. // If the call to consumeFunc returns no error, the entries should be considered to have been consumed. // If any entries were successfully consumed, the implementation should also return true; this // serves as a weak hint that there may be more entries to be consumed. // If forceUpdate is true, then the consumeFunc should be called, with an empty slice of entries if // necessary. This allows the log self-initialise in a transactionally safe manner. consumeEntries(ctx context.Context, limit uint64, f consumeFunc, forceUpdate bool) (bool, error) // currentTree returns the sequencer's view of the current tree state. currentTree(ctx context.Context) (uint64, []byte, error) // nextIndex returns the next available index in the log. nextIndex(ctx context.Context) (uint64, error) // publishCheckpoint coordinates the publication of new checkpoints based on the current integrated tree. publishCheckpoint(ctx context.Context, minStaleActive, minStaleRepub time.Duration, f func(ctx context.Context, size uint64, root []byte) error) error // garbageCollect coordinates the removal of unneeded partial tiles/entry bundles for the provided tree size, up to a maximum number of deletes per invocation. garbageCollect(ctx context.Context, treeSize uint64, maxDeletes uint, removePrefix func(ctx context.Context, prefix string) error, entriesPath func(uint64, uint8) string) error } // consumeFunc is the signature of a function which can consume entries from the sequencer. // Returns the updated root hash of the tree with the consumed entries integrated. type consumeFunc func(ctx context.Context, from uint64, entries []storage.SequencedEntry) ([]byte, error) // Config holds AWS project and resource configuration for a storage instance. type Config struct { // SDKConfig is an optional AWS config to use when configuring service clients, e.g. to // use non-AWS S3 or MySQL services. // // If nil, the value from config.LoadDefaultConfig() will be used - this is the only // supported configuration. SDKConfig *aws.Config // S3Options is an optional function which can be used to configure the S3 library. // This is primarily useful when configuring the use of non-AWS S3 or MySQL services. // // If nil, the default options will be used - this is the only supported configuration. S3Options func(*s3.Options) // Bucket is the name of the S3 bucket to use for storing log state. Bucket string // BucketPrefix is an optional prefix to prepend to all log resource paths. // This can be used e.g. to store multiple logs in the same bucket. BucketPrefix string // DSN is the DSN of the MySQL instance to use. DSN string // Maximum connections to the MySQL database. MaxOpenConns int // Maximum idle database connections in the connection pool. MaxIdleConns int // HTTPClient will be used for other HTTP requests. If unset, Tessera will use the net/http DefaultClient. HTTPClient *http.Client } // New creates a new instance of the AWS based Storage. // // Storage instances created via this c'tor will participate in integrating newly sequenced entries into the log // and periodically publishing a new checkpoint which commits to the state of the tree. func New(ctx context.Context, cfg Config) (tessera.Driver, error) { if cfg.SDKConfig == nil { // We're running on AWS so use the SDK's default config which will will handle credentials etc. sdkConfig, err := config.LoadDefaultConfig(ctx) if err != nil { return nil, fmt.Errorf("failed to load default AWS configuration: %v", err) } cfg.SDKConfig = &sdkConfig // We need a non-nil options func to pass in to s3.NewFromConfig below or it'll panic, so // we'll use a "do nothing" placeholder. cfg.S3Options = func(_ *s3.Options) {} } else { printDragonsWarning() } return &Storage{ cfg: cfg, }, nil } // Appender creates a new tessera.Appender lifecycle object. func (s *Storage) Appender(ctx context.Context, opts *tessera.AppendOptions) (*tessera.Appender, tessera.LogReader, error) { seq, err := newMySQLSequencer(ctx, s.cfg.DSN, uint64(opts.PushbackMaxOutstanding()), s.cfg.MaxOpenConns, s.cfg.MaxIdleConns) if err != nil { return nil, nil, fmt.Errorf("failed to create MySQL sequencer: %v", err) } s3Store := &s3Storage{ s3Client: s3.NewFromConfig(*s.cfg.SDKConfig, s.cfg.S3Options), bucket: s.cfg.Bucket, bucketPrefix: s.cfg.BucketPrefix, } a, lr, err := s.newAppender(ctx, s3Store, seq, opts) if err != nil { return nil, nil, err } return &tessera.Appender{ Add: a.Add, }, lr, nil } // newAppender creates and initialises an Appender struct with the provided underlying storage implementations. func (s *Storage) newAppender(ctx context.Context, o objStore, seq sequencer, opts *tessera.AppendOptions) (*Appender, tessera.LogReader, error) { if opts.CheckpointInterval() < minCheckpointInterval { return nil, nil, fmt.Errorf("requested CheckpointInterval (%v) is less than minimum permitted %v", opts.CheckpointInterval(), minCheckpointInterval) } logStore := &logResourceStore{ objStore: o, entriesPath: opts.EntriesPath(), integratedSize: func(context.Context) (uint64, error) { s, _, err := seq.currentTree(ctx) return s, err }, nextIndex: func(context.Context) (uint64, error) { return seq.nextIndex(ctx) }, } r := &Appender{ logStore: logStore, sequencer: seq, queue: storage.NewQueue(ctx, opts.BatchMaxAge(), opts.BatchMaxSize(), seq.assignEntries), newCP: opts.CheckpointPublisher(logStore, s.cfg.HTTPClient), treeUpdated: make(chan struct{}), } if err := r.init(ctx); err != nil { return nil, nil, fmt.Errorf("failed to initialise log storage: %v", err) } // Kick off go-routine which handles the integration of entries. go r.integrateEntriesJob(ctx) // Kick off go-routine which handles the publication of checkpoints. go r.publishCheckpointJob(ctx, opts.CheckpointInterval(), opts.CheckpointRepublishInterval()) if i := opts.GarbageCollectionInterval(); i > 0 { go r.garbageCollectorJob(ctx, i) } return r, r.logStore, nil } // Appender is an implementation of the Tessera appender lifecycle contract. type Appender struct { newCP func(context.Context, uint64, []byte) ([]byte, error) sequencer sequencer logStore *logResourceStore queue *storage.Queue treeUpdated chan struct{} } // integrateEntriesJob periodically appends newly sequenced entries to the log. // // This function does not return until the passed context is done. func (a *Appender) integrateEntriesJob(ctx context.Context) { t := time.NewTicker(1 * time.Second) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: } func() { // Don't quickloop for now, it causes issues updating checkpoint too frequently. cctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() if _, err := a.sequencer.consumeEntries(cctx, DefaultIntegrationSizeLimit, a.integrateEntries, false); err != nil { klog.Errorf("integrateEntries: %v", err) return } select { case a.treeUpdated <- struct{}{}: default: } }() } } // publishCheckpointJob periodically attempts to publish a new checkpoint representing the current state // of the tree, once per interval. // // This function does not return until the passed in context is done. func (a *Appender) publishCheckpointJob(ctx context.Context, pubInterval, republishInterval time.Duration) { t := time.NewTicker(pubInterval) defer t.Stop() for { select { case <-ctx.Done(): return case <-a.treeUpdated: case <-t.C: } if err := a.sequencer.publishCheckpoint(ctx, pubInterval, republishInterval, a.publishCheckpoint); err != nil { klog.Warningf("publishCheckpoint: %v", err) } } } // garbageCollectorJob is a long-running function which handles the removal of obsolete partial tiles // and entry bundles. // Blocks until ctx is done. func (a *Appender) garbageCollectorJob(ctx context.Context, i time.Duration) { t := time.NewTicker(i) defer t.Stop() // Entirely arbitrary number. maxBundlesPerRun := uint(100) for { select { case <-ctx.Done(): return case <-t.C: } func() { ctx, span := tracer.Start(ctx, "tessera.storage.aws.garbageCollectJob") defer span.End() // Figure out the size of the latest published checkpoint - we can't be removing partial tiles implied by // that checkpoint just because we've done an integration and know about a larger (but as yet unpublished) // checkpoint! cp, err := a.logStore.ReadCheckpoint(ctx) if err != nil { klog.Warningf("Failed to get published checkpoint: %v", err) return } _, pubSize, _, err := parse.CheckpointUnsafe(cp) if err != nil { klog.Warningf("Failed to parse published checkpoint: %v", err) return } if err := a.sequencer.garbageCollect(ctx, pubSize, maxBundlesPerRun, a.logStore.objStore.deleteObjectsWithPrefix, a.logStore.entriesPath); err != nil { klog.Warningf("GarbageCollect failed: %v", err) return } }() } } // Add is the entrypoint for adding entries to a sequencing log. func (a *Appender) Add(ctx context.Context, e *tessera.Entry) tessera.IndexFuture { return a.queue.Add(ctx, e) } // init ensures that the storage represents a log in a valid state. func (a *Appender) init(ctx context.Context) error { _, err := a.logStore.ReadCheckpoint(ctx) if err != nil { if errors.Is(err, os.ErrNotExist) { // No checkpoint exists, do a forced (possibly empty) integration to create one in a safe // way (calling updateCP directly here would not be safe as it's outside the transactional // framework which prevents the tree from rolling backwards or otherwise forking). cctx, c := context.WithTimeout(ctx, 10*time.Second) defer c() if _, err := a.sequencer.consumeEntries(cctx, DefaultIntegrationSizeLimit, a.integrateEntries, true); err != nil { return fmt.Errorf("forced integrate: %v", err) } select { case a.treeUpdated <- struct{}{}: default: } return nil } return fmt.Errorf("failed to read checkpoint: %v", err) } return nil } func (a *Appender) publishCheckpoint(ctx context.Context, size uint64, root []byte) error { cpRaw, err := a.newCP(ctx, size, root) if err != nil { return fmt.Errorf("newCP: %v", err) } if err := a.logStore.setCheckpoint(ctx, cpRaw); err != nil { return fmt.Errorf("writeCheckpoint: %v", err) } klog.V(2).Infof("Published latest checkpoint: %d, %x", size, root) return nil } // integrateEntries appends the provided entries into the log starting at fromSeq. // // Returns the new root hash of the log with the entries added. func (a *Appender) integrateEntries(ctx context.Context, fromSeq uint64, entries []storage.SequencedEntry) ([]byte, error) { var newRoot []byte errG := errgroup.Group{} errG.Go(func() error { if err := a.updateEntryBundles(ctx, fromSeq, entries); err != nil { return fmt.Errorf("updateEntryBundles: %v", err) } return nil }) errG.Go(func() error { lh := make([][]byte, len(entries)) for i, e := range entries { lh[i] = e.LeafHash } r, err := integrate(ctx, fromSeq, lh, a.logStore) if err != nil { return fmt.Errorf("integrate: %v", err) } newRoot = r return nil }) err := errG.Wait() return newRoot, err } // updateEntryBundles adds the entries being integrated into the entry bundles. // // The right-most bundle will be grown, if it's partial, and/or new bundles will be created as required. func (a *Appender) updateEntryBundles(ctx context.Context, fromSeq uint64, entries []storage.SequencedEntry) error { if len(entries) == 0 { return nil } numAdded := uint64(0) bundleIndex, entriesInBundle := fromSeq/layout.EntryBundleWidth, fromSeq%layout.EntryBundleWidth bundleWriter := &bytes.Buffer{} if entriesInBundle > 0 { // If the latest bundle is partial, we need to read the data it contains in for our newer, larger, bundle. part, err := a.logStore.getEntryBundle(ctx, uint64(bundleIndex), uint8(entriesInBundle)) if err != nil { return err } if _, err := bundleWriter.Write(part); err != nil { return fmt.Errorf("bundleWriter: %v", err) } } seqErr := errgroup.Group{} // goSetEntryBundle is a function which uses seqErr to spin off a go-routine to write out an entry bundle. // It's used in the for loop below. goSetEntryBundle := func(ctx context.Context, bundleIndex uint64, p uint8, bundleRaw []byte) { seqErr.Go(func() error { if err := a.logStore.setEntryBundle(ctx, bundleIndex, p, bundleRaw); err != nil { return err } return nil }) } // Add new entries to the bundle for _, e := range entries { if _, err := bundleWriter.Write(e.BundleData); err != nil { return fmt.Errorf("bundlewriter.Write: %v", err) } entriesInBundle++ fromSeq++ numAdded++ if entriesInBundle == layout.EntryBundleWidth { // This bundle is full, so we need to write it out... klog.V(1).Infof("In-memory bundle idx %d is full, attempting write to S3", bundleIndex) goSetEntryBundle(ctx, bundleIndex, 0, bundleWriter.Bytes()) // ... and prepare the next entry bundle for any remaining entries in the batch bundleIndex++ entriesInBundle = 0 // Don't use Reset/Truncate here - the backing []bytes is still being used by goSetEntryBundle above. bundleWriter = &bytes.Buffer{} klog.V(1).Infof("Starting to fill in-memory bundle idx %d", bundleIndex) } } // If we have a partial bundle remaining once we've added all the entries from the batch, // this needs writing out too. if entriesInBundle > 0 { klog.V(1).Infof("Attempting to write in-memory partial bundle idx %d.%d to S3", bundleIndex, entriesInBundle) goSetEntryBundle(ctx, bundleIndex, uint8(entriesInBundle), bundleWriter.Bytes()) } return seqErr.Wait() } // MigrationWriter creates a new AWS storage for the MigrationWriter lifecycle mode. func (s *Storage) MigrationWriter(ctx context.Context, opts *tessera.MigrationOptions) (migrate.MigrationWriter, tessera.LogReader, error) { logStore := &logResourceStore{ objStore: &s3Storage{ s3Client: s3.NewFromConfig(*s.cfg.SDKConfig, s.cfg.S3Options), bucket: s.cfg.Bucket, bucketPrefix: s.cfg.BucketPrefix, }, entriesPath: opts.EntriesPath(), } seq, err := newMySQLSequencer(ctx, s.cfg.DSN, DefaultPushbackMaxOutstanding, s.cfg.MaxOpenConns, s.cfg.MaxIdleConns) if err != nil { return nil, nil, fmt.Errorf("failed to create MySQL sequencer: %v", err) } m := &MigrationStorage{ s: s, dbPool: seq.dbPool, bundleHasher: opts.LeafHasher(), sequencer: seq, logStore: logStore, } return m, logStore, nil } // MigrationStorage implements the tessera.MigrationStorage lifecycle contract. type MigrationStorage struct { s *Storage dbPool *sql.DB bundleHasher func([]byte) ([][]byte, error) sequencer sequencer logStore *logResourceStore } var _ migrate.MigrationWriter = &MigrationStorage{} func (m *MigrationStorage) AwaitIntegration(ctx context.Context, sourceSize uint64) ([]byte, error) { t := time.NewTicker(time.Second) defer t.Stop() for { select { case <-ctx.Done(): return nil, ctx.Err() case <-t.C: from, _, err := m.sequencer.currentTree(ctx) if err != nil && !errors.Is(err, os.ErrNotExist) { klog.Warningf("readTreeState: %v", err) continue } klog.Infof("Integrate from %d (Target %d)", from, sourceSize) newSize, newRoot, err := m.buildTree(ctx, sourceSize) if err != nil { klog.Warningf("integrate: %v", err) } if newSize == sourceSize { klog.Infof("Integrated to %d with roothash %x", newSize, newRoot) return newRoot, nil } } } } func (m *MigrationStorage) SetEntryBundle(ctx context.Context, index uint64, partial uint8, bundle []byte) error { return m.logStore.setEntryBundle(ctx, index, partial, bundle) } func (m *MigrationStorage) IntegratedSize(ctx context.Context) (uint64, error) { sz, _, err := m.sequencer.currentTree(ctx) return sz, err } func (m *MigrationStorage) fetchLeafHashes(ctx context.Context, from, to, sourceSize uint64) ([][]byte, error) { // TODO(al): Make this configurable. const maxBundles = 300 toBeAdded := sync.Map{} eg := errgroup.Group{} n := 0 for ri := range layout.Range(from, to, sourceSize) { eg.Go(func() error { b, err := m.logStore.getEntryBundle(ctx, ri.Index, ri.Partial) if err != nil { return fmt.Errorf("getEntryBundle(%d.%d): %v", ri.Index, ri.Partial, err) } bh, err := m.bundleHasher(b) if err != nil { return fmt.Errorf("bundleHasherFunc for bundle index %d: %v", ri.Index, err) } toBeAdded.Store(ri.Index, bh[ri.First:ri.First+ri.N]) return nil }) n++ if n >= maxBundles { break } } if err := eg.Wait(); err != nil { return nil, err } lh := make([][]byte, 0, maxBundles) for i := from / layout.EntryBundleWidth; ; i++ { v, ok := toBeAdded.LoadAndDelete(i) if !ok { break } bh := v.([][]byte) lh = append(lh, bh...) } return lh, nil } func (m *MigrationStorage) buildTree(ctx context.Context, sourceSize uint64) (uint64, []byte, error) { var newSize uint64 var newRoot []byte tx, err := m.dbPool.BeginTx(ctx, nil) if err != nil { return 0, nil, fmt.Errorf("failed to begin Tx: %v", err) } defer func() { if tx != nil { if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { klog.Errorf("failed to rollback Tx: %v", err) } } }() // Figure out which is the starting index of sequenced entries to start consuming from. row := tx.QueryRowContext(ctx, "SELECT seq, rootHash FROM IntCoord WHERE id = ? FOR UPDATE", 0) var from uint64 var rootHash []byte if err := row.Scan(&from, &rootHash); err != nil { return 0, nil, fmt.Errorf("failed to read IntCoord: %v", err) } klog.V(1).Infof("Integrating from %d", from) lh, err := m.fetchLeafHashes(ctx, from, sourceSize, sourceSize) if err != nil { return 0, nil, fmt.Errorf("fetchLeafHashes(%d, %d, %d): %v", from, sourceSize, sourceSize, err) } if len(lh) == 0 { klog.Infof("Integrate: nothing to do, nothing done") return from, rootHash, nil } added := uint64(len(lh)) klog.Infof("Integrate: adding %d entries to existing tree size %d", len(lh), from) newRoot, err = integrate(ctx, from, lh, m.logStore) if err != nil { klog.Warningf("integrate failed: %v", err) return 0, nil, fmt.Errorf("integrate failed: %v", err) } newSize = from + added klog.Infof("Integrate: added %d entries", added) if _, err := tx.ExecContext(ctx, "UPDATE IntCoord SET seq=?, rootHash=? WHERE id=?", newSize, newRoot, 0); err != nil { return 0, nil, fmt.Errorf("update intcoord: %v", err) } if err := tx.Commit(); err != nil { return 0, nil, fmt.Errorf("failed to commit Tx: %v", err) } tx = nil return newSize, newRoot, nil } // logResourceStore knows how to read and write entries which represent a tiles log inside an objStore. type logResourceStore struct { objStore objStore entriesPath func(uint64, uint8) string integratedSize func(context.Context) (uint64, error) nextIndex func(context.Context) (uint64, error) } func (lr *logResourceStore) ReadCheckpoint(ctx context.Context) ([]byte, error) { r, err := lr.get(ctx, layout.CheckpointPath) if err != nil { var nske *types.NoSuchKey if errors.As(err, &nske) { return r, os.ErrNotExist } } return r, err } func (lr *logResourceStore) ReadTile(ctx context.Context, l, i uint64, p uint8) ([]byte, error) { return fetcher.PartialOrFullResource(ctx, p, func(ctx context.Context, p uint8) ([]byte, error) { return lr.get(ctx, layout.TilePath(l, i, p)) }) } func (lr *logResourceStore) ReadEntryBundle(ctx context.Context, i uint64, p uint8) ([]byte, error) { return fetcher.PartialOrFullResource(ctx, p, func(ctx context.Context, p uint8) ([]byte, error) { return lr.get(ctx, lr.entriesPath(i, p)) }) } func (lr *logResourceStore) IntegratedSize(ctx context.Context) (uint64, error) { return lr.integratedSize(ctx) } func (lr *logResourceStore) NextIndex(ctx context.Context) (uint64, error) { return lr.nextIndex(ctx) } // get returns the requested object. // // This is indended to be used to proxy read requests through the personality for debug/testing purposes. func (s *logResourceStore) get(ctx context.Context, path string) ([]byte, error) { d, err := s.objStore.getObject(ctx, path) return d, err } func (lrs *logResourceStore) setCheckpoint(ctx context.Context, cpRaw []byte) error { return lrs.objStore.setObject(ctx, layout.CheckpointPath, cpRaw, ckptContType, ckptCacheControl) } // setTile idempotently stores the provided tile at the location implied by the given level, index, and treeSize. // // The location to which the tile is written is defined by the tile layout spec. func (lrs *logResourceStore) setTile(ctx context.Context, level, index, logSize uint64, tile *api.HashTile) error { data, err := tile.MarshalText() if err != nil { return err } tPath := layout.TilePath(level, index, layout.PartialTileSize(level, index, logSize)) klog.V(2).Infof("StoreTile: %s (%d entries)", tPath, len(tile.Nodes)) return lrs.objStore.setObjectIfNoneMatch(ctx, tPath, data, logContType, logCacheControl) } // getTiles returns the tiles with the given tile-coords for the specified log size. // // Tiles are returned in the same order as they're requested, nils represent tiles which were not found. func (lrs *logResourceStore) getTiles(ctx context.Context, tileIDs []storage.TileID, logSize uint64) ([]*api.HashTile, error) { r := make([]*api.HashTile, len(tileIDs)) errG := errgroup.Group{} for i, id := range tileIDs { i := i id := id errG.Go(func() error { objName := layout.TilePath(id.Level, id.Index, layout.PartialTileSize(id.Level, id.Index, logSize)) data, err := lrs.objStore.getObject(ctx, objName) if err != nil { // Do not use errors.Is. Keep errors.As to compare by type and not by value. var nske *types.NoSuchKey if errors.As(err, &nske) { // Depending on context, this may be ok. // We'll signal to higher levels that it wasn't found by retuning a nil for this tile. return nil } return err } t := &api.HashTile{} if err := t.UnmarshalText(data); err != nil { return fmt.Errorf("unmarshal(%q): %v", objName, err) } r[i] = t return nil }) } if err := errG.Wait(); err != nil { return nil, err } return r, nil } // getEntryBundle returns the serialised entry bundle at the location implied by the given index and treeSize. // // Returns a wrapped os.ErrNotExist if the bundle does not exist. func (lrs *logResourceStore) getEntryBundle(ctx context.Context, bundleIndex uint64, p uint8) ([]byte, error) { objName := lrs.entriesPath(bundleIndex, p) data, err := lrs.objStore.getObject(ctx, objName) if err != nil { // Do not use errors.Is. Keep errors.As to compare by type and not by value. var nske *types.NoSuchKey if errors.As(err, &nske) { // Return the generic NotExist error so that higher levels can differentiate // between this and other errors. return nil, fmt.Errorf("%v: %w", objName, os.ErrNotExist) } return nil, err } return data, nil } // setEntryBundle idempotently stores the serialised entry bundle at the location implied by the bundleIndex and treeSize. func (lrs *logResourceStore) setEntryBundle(ctx context.Context, bundleIndex uint64, p uint8, bundleRaw []byte) error { objName := lrs.entriesPath(bundleIndex, p) // Note that setObject does an idempotent interpretation of IfNoneMatch - it only // returns an error if the named object exists _and_ contains different data to what's // passed in here. if err := lrs.objStore.setObjectIfNoneMatch(ctx, objName, bundleRaw, logContType, logCacheControl); err != nil { return fmt.Errorf("setObjectIfNoneMatch(%q): %v", objName, err) } return nil } // integrate adds the provided leaf hashes to the merkle tree, starting at the provided location. func integrate(ctx context.Context, fromSeq uint64, lh [][]byte, lrs *logResourceStore) ([]byte, error) { getTiles := func(ctx context.Context, tileIDs []storage.TileID, treeSize uint64) ([]*api.HashTile, error) { n, err := lrs.getTiles(ctx, tileIDs, treeSize) if err != nil { return nil, fmt.Errorf("getTiles: %w", err) } return n, nil } newSize, newRoot, tiles, err := storage.Integrate(ctx, getTiles, fromSeq, lh) if err != nil { return nil, fmt.Errorf("storage.Integrate: %v", err) } errG := errgroup.Group{} for k, v := range tiles { func(ctx context.Context, k storage.TileID, v *api.HashTile) { errG.Go(func() error { return lrs.setTile(ctx, uint64(k.Level), k.Index, newSize, v) }) }(ctx, k, v) } if err := errG.Wait(); err != nil { return nil, err } klog.V(1).Infof("New tree: %d, %x", newSize, newRoot) return newRoot, nil } // mySQLSequencer uses MySQL to provide // a durable and thread/multi-process safe sequencer. type mySQLSequencer struct { dbPool *sql.DB maxOutstanding uint64 } // newMySQLSequencer returns a new mysqlSequencer struct which uses the provided // DSN for its MySQL connection. func newMySQLSequencer(ctx context.Context, dsn string, maxOutstanding uint64, maxOpenConns, maxIdleConns int) (*mySQLSequencer, error) { dbPool, err := sql.Open("mysql", dsn) if err != nil { return nil, fmt.Errorf("failed to connect to MySQL db: %v", err) } if maxOpenConns > 0 { dbPool.SetMaxOpenConns(maxOpenConns) } if maxIdleConns >= 0 { dbPool.SetMaxIdleConns(maxIdleConns) } if err := dbPool.Ping(); err != nil { return nil, fmt.Errorf("failed to ping MySQL db: %v", err) } r := &mySQLSequencer{ dbPool: dbPool, maxOutstanding: maxOutstanding, } if err := r.initDB(ctx); err != nil { return nil, fmt.Errorf("failed to initDB: %v", err) } if err := r.checkDataCompatibility(ctx); err != nil { return nil, fmt.Errorf("schema is not compatible with this version of the Tessera library: %v", err) } return r, nil } // checkDataCompatibility compares the Tessera library SchemaCompatibilityVersion with the one stored in the // database, and returns an error if they are not identical. func (s *mySQLSequencer) checkDataCompatibility(ctx context.Context) error { row := s.dbPool.QueryRowContext(ctx, "SELECT compatibilityVersion FROM Tessera WHERE id = 0") var gotVersion uint64 if err := row.Scan(&gotVersion); err != nil { return fmt.Errorf("failed to read schema compatibility version from DB: %v", err) } if gotVersion != SchemaCompatibilityVersion { return fmt.Errorf("schema compatibilityVersion (%d) != library compatibilityVersion (%d)", gotVersion, SchemaCompatibilityVersion) } return nil } // initDB ensures that the coordination DB is initialised correctly. // // It creates tables if they don't exist already, and inserts zero values. // // The database schema consists of 4 tables: // - Tessera // This table only ever contains a single row which tracks the compatibility // version of the DB schema and data formats. // - SeqCoord // This table only ever contains a single row which tracks the next available // sequence number. // - Seq // This table holds sequenced "batches" of entries. The batches are keyed // by the sequence number assigned to the first entry in the batch, and // each subsequent entry in the batch takes the numerically next sequence number. // - IntCoord // This table coordinates integration of the batches of entries stored in // Seq into the committed tree state. func (s *mySQLSequencer) initDB(ctx context.Context) error { if _, err := s.dbPool.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS Tessera ( id INT UNSIGNED NOT NULL, compatibilityVersion BIGINT UNSIGNED NOT NULL, PRIMARY KEY (id) )`); err != nil { return err } if _, err := s.dbPool.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS SeqCoord( id INT UNSIGNED NOT NULL, next BIGINT UNSIGNED NOT NULL, PRIMARY KEY (id) )`); err != nil { return err } // TODO(phboneff): test this with very large leaves, consider downgrading to MEDIUMBLOB. // Keep in mind that CT leaves can be large, as large as: https://crt.sh/?id=10751627. if _, err := s.dbPool.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS Seq( id INT UNSIGNED NOT NULL, seq BIGINT UNSIGNED NOT NULL, v LONGBLOB, PRIMARY KEY (id, seq) )`); err != nil { return err } if _, err := s.dbPool.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS IntCoord( id INT UNSIGNED NOT NULL, seq BIGINT UNSIGNED NOT NULL, rootHash TINYBLOB NOT NULL, PRIMARY KEY (id) )`); err != nil { return err } if _, err := s.dbPool.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS PubCoord( id INT UNSIGNED NOT NULL, publishedAt BIGINT NOT NULL, size BIGINT UNSIGNED, PRIMARY KEY (id) )`); err != nil { return err } // Attempt to migrate the schema for existing tables. // // Of course MySQL doesn't support IF NOT EXISTS for ADD COLUMN. MariaDB does, but we can't rely on that // here unless we try to divine which DBMS we're _actually_ talking to. // // In theory MariaDB and AuroraDB are MySQL compatible, and that should extend to error codes (MariaDB explicitly // says that error codes between 1000 and 1800 are identical // (https://mariadb.com/docs/server/reference/error-codes/mariadb-error-code-reference). // // So we'll just do it the MySQL way, and ignore any "already exists" error we get back. if _, err := s.dbPool.ExecContext(ctx, `ALTER TABLE PubCoord ADD COLUMN size BIGINT UNSIGNED`); err != nil { if e, ok := err.(*mysql.MySQLError); ok { // If this is anything other than ER_DUP_FIELDNAME (1060), then fail. if e.Number != 1060 { return fmt.Errorf("failed to add column to PubCoord: %v", err) } } else { // Also fail if it wasn't a MySQL error. return fmt.Errorf("failed to add column to PubCoord: %v", err) } } if _, err := s.dbPool.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS GCCoord( id INT UNSIGNED NOT NULL, fromSize BIGINT NOT NULL, PRIMARY KEY (id) )`); err != nil { return err } // Set default values for a newly initialised schema - these rows being present are a precondition for // sequencing and integration to occur. // Note that this will only succeed if no row exists, so there's no danger // of "resetting" an existing log. if _, err := s.dbPool.ExecContext(ctx, `INSERT IGNORE INTO Tessera (id, compatibilityVersion) VALUES (0, ?)`, SchemaCompatibilityVersion); err != nil { return err } if _, err := s.dbPool.ExecContext(ctx, `INSERT IGNORE INTO SeqCoord (id, next) VALUES (0, 0)`); err != nil { return err } if _, err := s.dbPool.ExecContext(ctx, `INSERT IGNORE INTO IntCoord (id, seq, rootHash) VALUES (0, 0, ?)`, rfc6962.DefaultHasher.EmptyRoot()); err != nil { return err } if _, err := s.dbPool.ExecContext(ctx, `INSERT IGNORE INTO PubCoord (id, publishedAt, size) VALUES (0, 0, 0)`); err != nil { return err } if _, err := s.dbPool.ExecContext(ctx, `INSERT IGNORE INTO GCCoord (id, fromSize) VALUES (0, 0)`); err != nil { return err } return nil } // assignEntries durably assigns each of the passed-in entries an index in the log. // // Entries are allocated contiguous indices, in the order in which they appear in the entries parameter. // This is achieved by storing the passed-in entries in the Seq table in MySQL, keyed by the // index assigned to the first entry in the batch. func (s *mySQLSequencer) assignEntries(ctx context.Context, entries []*tessera.Entry) error { // First grab the treeSize in a non-locking read-only fashion (we don't want to block/collide with integration). // We'll use this value to determine whether we need to apply back-pressure. var treeSize uint64 row := s.dbPool.QueryRowContext(ctx, "SELECT seq FROM IntCoord WHERE id = ?", 0) if err := row.Scan(&treeSize); err == sql.ErrNoRows { return nil } else if err != nil { return fmt.Errorf("failed to read integration coordination info: %v", err) } // Now move on with sequencing in a single transaction tx, err := s.dbPool.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to begin Tx: %v", err) } defer func() { if tx != nil { if err := tx.Rollback(); err != nil { klog.Errorf("failed to rollback Tx: %v", err) } } }() // First we need to grab the next available sequence number from the SeqCoord table. var next, id uint64 r := tx.QueryRowContext(ctx, "SELECT id, next FROM SeqCoord WHERE id = ? FOR UPDATE", 0) if err := r.Scan(&id, &next); err != nil { return fmt.Errorf("failed to read seqcoord: %v", err) } // Check whether there are too many outstanding entries and we should apply // back-pressure. if outstanding := next - treeSize; outstanding > s.maxOutstanding { return tessera.ErrPushbackIntegration } sequencedEntries := make([]storage.SequencedEntry, len(entries)) // Assign provisional sequence numbers to entries. // We need to do this here in order to support serialisations which include the log position. for i, e := range entries { sequencedEntries[i] = storage.SequencedEntry{ BundleData: e.MarshalBundleData(next + uint64(i)), LeafHash: e.LeafHash(), } } // Flatten the entries into a single slice of bytes which we can store in the Seq.v column. b := &bytes.Buffer{} e := gob.NewEncoder(b) if err := e.Encode(sequencedEntries); err != nil { return fmt.Errorf("failed to serialise batch: %v", err) } data := b.Bytes() num := uint64(len(entries)) // Insert our newly sequenced batch of entries into Seq, if _, err := tx.ExecContext(ctx, "INSERT INTO Seq(id, seq, v) VALUES(?, ?, ?)", 0, next, data); err != nil { return fmt.Errorf("insert into seq: %v", err) } // and update the next-available sequence number row in SeqCoord. if _, err := tx.ExecContext(ctx, "UPDATE SeqCoord SET next = ? WHERE ID = ?", next+num, 0); err != nil { return fmt.Errorf("update seqcoord: %v", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit Tx: %v", err) } tx = nil return nil } // consumeEntries calls f with previously sequenced entries. // // Once f returns without error, the entries it was called with are considered to have been consumed and are // removed from the Seq table. // // Returns true if some entries were consumed as a weak signal that there may be further entries waiting to be consumed. func (s *mySQLSequencer) consumeEntries(ctx context.Context, limit uint64, f consumeFunc, forceUpdate bool) (bool, error) { tx, err := s.dbPool.BeginTx(ctx, nil) if err != nil { return false, fmt.Errorf("failed to begin Tx: %v", err) } defer func() { if tx != nil { if err := tx.Rollback(); err != nil { klog.Errorf("failed to rollback Tx: %v", err) } } }() // Figure out which is the starting index of sequenced entries to start consuming from. row := tx.QueryRowContext(ctx, "SELECT seq, rootHash FROM IntCoord WHERE id = ? FOR UPDATE", 0) var fromSeq uint64 var rootHash []byte if err := row.Scan(&fromSeq, &rootHash); err == sql.ErrNoRows { return false, nil } else if err != nil { return false, fmt.Errorf("failed to read IntCoord: %v", err) } klog.V(1).Infof("Consuming from %d", fromSeq) // Now read the sequenced starting at the index we got above. rows, err := tx.QueryContext(ctx, "SELECT seq, v FROM Seq WHERE id = ? AND seq >= ? ORDER BY seq LIMIT ? FOR UPDATE", 0, fromSeq, limit) if err != nil { return false, fmt.Errorf("failed to read Seq: %v", err) } defer func() { if err := rows.Close(); err != nil { klog.Warningf("rows.Close: %v", err) } }() // This needs to be of type `any`, to be passed to ExecContext. Only uint64s will be stored. seqsConsumed := []any{} entries := make([]storage.SequencedEntry, 0, limit) orderCheck := fromSeq for rows.Next() { var vGob []byte var seq uint64 if err := rows.Scan(&seq, &vGob); err != nil { return false, fmt.Errorf("failed to scan Seq row: %v", err) } if orderCheck != seq { return false, fmt.Errorf("integrity fail - expected seq %d, but found %d", orderCheck, seq) } g := gob.NewDecoder(bytes.NewReader(vGob)) b := []storage.SequencedEntry{} if err := g.Decode(&b); err != nil { return false, fmt.Errorf("failed to deserialise v from Seq: %v", err) } entries = append(entries, b...) seqsConsumed = append(seqsConsumed, seq) orderCheck += uint64(len(b)) } if len(seqsConsumed) == 0 && !forceUpdate { klog.V(1).Info("Found no rows to sequence") return false, nil } // Call consumeFunc with the entries we've found newRoot, err := f(ctx, uint64(fromSeq), entries) if err != nil { return false, err } // consumeFunc was successful, so we can update our coordination row, and delete the row(s) for // the then consumed entries. if _, err := tx.ExecContext(ctx, "UPDATE IntCoord SET seq=?, rootHash=? WHERE id=?", orderCheck, newRoot, 0); err != nil { return false, fmt.Errorf("update intcoord: %v", err) } if len(seqsConsumed) > 0 { // TODO(phboneff): evaluate if seq BETWEEN ? AND ? is more efficient q := "DELETE FROM Seq WHERE id=? AND seq IN ( " + placeholder(len(seqsConsumed)) + " )" if _, err := tx.ExecContext(ctx, q, append([]any{0}, seqsConsumed...)...); err != nil { return false, fmt.Errorf("update intcoord: %v", err) } } if err := tx.Commit(); err != nil { return false, fmt.Errorf("failed to commit Tx: %v", err) } tx = nil return true, nil } // currentTree returns the size and root hash of the currently integrated tree. func (s *mySQLSequencer) currentTree(ctx context.Context) (uint64, []byte, error) { row := s.dbPool.QueryRowContext(ctx, "SELECT seq, rootHash FROM IntCoord WHERE id = ?", 0) var fromSeq uint64 var rootHash []byte if err := row.Scan(&fromSeq, &rootHash); err != nil { return 0, nil, fmt.Errorf("failed to read IntCoord: %v", err) } return fromSeq, rootHash, nil } // nextIndex returns the next available index in the log. func (s *mySQLSequencer) nextIndex(ctx context.Context) (uint64, error) { row := s.dbPool.QueryRowContext(ctx, "SELECT next FROM SeqCoord WHERE ID = ?", 0) var nextSeq uint64 if err := row.Scan(&nextSeq); err != nil { return 0, fmt.Errorf("failed to read DB: %v", err) } return nextSeq, nil } // publishCheckpoint checks when the last checkpoint was published, and if it was more than minAge ago, calls the provided // function to publish a new one. // // This function uses PubCoord with an exclusive lock to guarantee that only one tessera instance can attempt to publish // a checkpoint at any given time. func (s *mySQLSequencer) publishCheckpoint(ctx context.Context, minStaleActive, minStaleRepub time.Duration, f func(context.Context, uint64, []byte) error) error { tx, err := s.dbPool.Begin() if err != nil { return err } defer func() { if tx != nil { _ = tx.Rollback() } }() pRow := tx.QueryRowContext(ctx, "SELECT publishedAt, size FROM PubCoord WHERE id = ? FOR UPDATE", 0) var pubAt int64 var lastSize sql.NullInt64 if err := pRow.Scan(&pubAt, &lastSize); err != nil { return fmt.Errorf("failed to parse PubCoord: %v", err) } cpAge := time.Since(time.Unix(pubAt, 0)) if cpAge < minStaleActive { klog.V(1).Infof("publishCheckpoint: last checkpoint published %s ago (< required %s), not publishing new checkpoint", cpAge, minStaleActive) return nil } row := tx.QueryRowContext(ctx, "SELECT seq, rootHash FROM IntCoord WHERE id = ?", 0) var fromSeq uint64 var rootHash []byte if err := row.Scan(&fromSeq, &rootHash); err != nil { return fmt.Errorf("failed to read IntCoord: %v", err) } currentSize := fromSeq shouldPublish := minStaleRepub > 0 && cpAge >= minStaleRepub if !shouldPublish { if !lastSize.Valid { // If we don't know the last published size, we should probably publish to be safe/self-heal. shouldPublish = true } else if currentSize > uint64(lastSize.Int64) { shouldPublish = true } } if !shouldPublish { klog.V(1).Infof("publishCheckpoint: skipping publish because tree hasn't grown and previous checkpoint is too recent") return nil } klog.V(1).Infof("publishCheckpoint: updating checkpoint (replacing %s old checkpoint)", cpAge) if err := f(ctx, fromSeq, rootHash); err != nil { return err } if _, err := tx.ExecContext(ctx, "UPDATE PubCoord SET publishedAt=?, size=? WHERE id=?", time.Now().Unix(), currentSize, 0); err != nil { return err } if err := tx.Commit(); err != nil { return err } tx = nil return nil } // garbageCollect will identify up to maxBundles unneeded partial entry bundles (and any unneeded partial tiles which sit above them in the tree) and // call the provided function to remove them. // // Uses the `GCCoord` table to ensure that only one binary is actively garbage collecting at any given time, and to track progress so that we don't // needlessly attempt to GC over regions which have already been cleaned. func (s *mySQLSequencer) garbageCollect(ctx context.Context, treeSize uint64, maxBundles uint, deleteWithPrefix func(ctx context.Context, prefix string) error, entriesPath func(uint64, uint8) string) error { tx, err := s.dbPool.Begin() if err != nil { return err } defer func() { if tx != nil { _ = tx.Rollback() } }() pRow := tx.QueryRowContext(ctx, "SELECT fromSize FROM GCCoord WHERE id = ? FOR UPDATE", 0) var fromSize uint64 if err := pRow.Scan(&fromSize); err != nil { return fmt.Errorf("failed to parse publishedAt: %v", err) } if fromSize == treeSize { return nil } d := uint(0) eg := errgroup.Group{} // GC the tree in "vertical" chunks defined by entry bundles. for ri := range layout.Range(fromSize, treeSize-fromSize, treeSize) { // Only known-full bundles are in-scope for for GC, so exit if the current bundle is partial or // we've reached our limit of chunks. if ri.Partial > 0 || d > maxBundles { break } // GC any partial versions of the entry bundle itself and the tile which sits immediately above it. eg.Go(func() error { return deleteWithPrefix(ctx, entriesPath(ri.Index, 0)+".p/") }) eg.Go(func() error { return deleteWithPrefix(ctx, layout.TilePath(0, ri.Index, 0)+".p/") }) fromSize += uint64(ri.N) d++ // Now consider (only) the part of the tree which sits above the bundle. // We'll walk up the parent tiles for as a long as we're tracing the right-hand // edge of a perfect subtree. // This gives the property we'll only visit each parent tile once, rather than up to 256 times. pL, pIdx := uint64(0), ri.Index for isLastLeafInParent(pIdx) { // Move our coordinates up to the parent pL, pIdx = pL+1, pIdx>>layout.TileHeight // GC any partial versions of the parent tile. eg.Go(func() error { return deleteWithPrefix(ctx, layout.TilePath(pL, pIdx, 0)+".p/") }) } } if err := eg.Wait(); err != nil { return fmt.Errorf("failed to delete one or more objects: %v", err) } if _, err := tx.ExecContext(ctx, "UPDATE GCCoord SET fromSize=? WHERE id=?", fromSize, 0); err != nil { return err } if err := tx.Commit(); err != nil { return err } tx = nil return nil } // isLastLeafInParent returns true if a tile with the provided index is the final child node of a // (hypothetical) full parent tile. func isLastLeafInParent(i uint64) bool { return i%layout.TileWidth == layout.TileWidth-1 } func placeholder(n int) string { places := make([]string, n) for i := range n { places[i] = "?" } return strings.Join(places, ",") } // s3Storage knows how to store and retrieve objects from S3. type s3Storage struct { bucket string bucketPrefix string s3Client *s3.Client } // getObject returns the data of the specified object, or an error. func (s *s3Storage) getObject(ctx context.Context, obj string) ([]byte, error) { if s.bucketPrefix != "" { obj = filepath.Join(s.bucketPrefix, obj) } r, err := s.s3Client.GetObject(ctx, &s3.GetObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(obj), }) if err != nil { return nil, fmt.Errorf("getObject: failed to create reader for object %q in bucket %q: %w", obj, s.bucket, err) } d, err := io.ReadAll(r.Body) if err != nil { return nil, fmt.Errorf("getObject: failed to read %q: %v", obj, err) } return d, r.Body.Close() } // setObject stores the provided data in the specified object. func (s *s3Storage) setObject(ctx context.Context, objName string, data []byte, contType string, cacheControl string) error { if s.bucketPrefix != "" { objName = filepath.Join(s.bucketPrefix, objName) } put := &s3.PutObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(objName), Body: bytes.NewReader(data), ContentType: aws.String(contType), CacheControl: aws.String(cacheControl), } if _, err := s.s3Client.PutObject(ctx, put); err != nil { return fmt.Errorf("failed to write object %q to bucket %q: %w", objName, s.bucket, err) } return nil } // setObjectIfNoneMatch stores data in the specified object gated by a IfNoneMatch condition. // // ifNoneMatch can be used to specify the IfNoneMatch preconditions for the write, i.e write // iff no object exists under this key already. If an object already exists under the same key, // an error will be returned *unless* the currently stored data is bit-for-bit identical to the // data to-be-written. This is intended to provide idempotentency for writes. func (s *s3Storage) setObjectIfNoneMatch(ctx context.Context, objName string, data []byte, contType string, cacheControl string) error { if s.bucketPrefix != "" { objName = filepath.Join(s.bucketPrefix, objName) } put := &s3.PutObjectInput{ Bucket: aws.String(s.bucket), Key: aws.String(objName), Body: bytes.NewReader(data), ContentType: aws.String(contType), CacheControl: aws.String(cacheControl), // "*" is the expected character for this condition IfNoneMatch: aws.String("*"), } if _, err := s.s3Client.PutObject(ctx, put); err != nil { // If we run into a precondition failure error, check that the object // which exists contains the same content that we want to write. // If so, we can consider this write to be idempotently successful. var apiErr smithy.APIError if errors.As(err, &apiErr) && apiErr.ErrorCode() == "PreconditionFailed" { existing, err := s.getObject(ctx, objName) if err != nil { return fmt.Errorf("failed to fetch existing content for %q: %v", objName, err) } if !bytes.Equal(existing, data) { klog.Errorf("Resource %q non-idempotent write:\n%s", objName, cmp.Diff(existing, data)) return fmt.Errorf("precondition failed: resource content for %q differs from data to-be-written", objName) } klog.V(2).Infof("setObjectIfNoneMatch: identical resource already exists for %q, continuing", objName) return nil } return fmt.Errorf("failed to write object %q to bucket %q: %w", objName, s.bucket, err) } return nil } // deleteObjectsWithPrefix removes any objects with the provided prefix from S3. func (s *s3Storage) deleteObjectsWithPrefix(ctx context.Context, objPrefix string) error { ctx, span := tracer.Start(ctx, "tessera.storage.aws.deleteObject") defer span.End() if s.bucketPrefix != "" { objPrefix = filepath.Join(s.bucketPrefix, objPrefix) } span.SetAttributes(objectPathKey.String(objPrefix)) l, err := s.s3Client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{ Bucket: aws.String(s.bucket), Prefix: aws.String(objPrefix), }) if err != nil { return fmt.Errorf("failed to list objects with prefix %q: %v", objPrefix, err) } di := &s3.DeleteObjectsInput{ Bucket: aws.String(s.bucket), Delete: &types.Delete{ Objects: make([]types.ObjectIdentifier, 0, len(l.Contents)), }, } for _, k := range l.Contents { klog.V(2).Infof("Deleting object %s", *k.Key) di.Delete.Objects = append(di.Delete.Objects, types.ObjectIdentifier{Key: k.Key}) } if _, err := s.s3Client.DeleteObjects(ctx, di); err != nil { return fmt.Errorf("failed to delete objects: %v", err) } return nil } func printDragonsWarning() { d := `H4sIAFZYZGcAA01QMQ7EIAzbeYXV5UCqkq1bf2IFtpNuPalj334hFQdkwLGNAwBzyXnKitOiqTYj B7ZGplWEwZhZqxZ1aKuswcD0AA4GXPUhI0MEpSd5Ow09vJ+m6rVtF6m0GDccYXDZEdp9N/g1H9Pf Qu80vNj7tiOe0lkdc8hwZK9YxavT0+FTP++vU6DUKvpEOr1+VGTk3IBXKSX9AHz5xXRgAQAA` g, _ := base64.StdEncoding.DecodeString(d) r, _ := gzip.NewReader(bytes.NewReader(g)) t, _ := io.ReadAll(r) klog.Infof("Running in non-AWS mode - see storage/aws/README.md for more details.") klog.Infof("Here be dragons!\n%s", t) } transparency-dev-tessera-3cb22ee/storage/aws/aws_test.go000066400000000000000000000573451511600621500235630ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // This the tests for a MySQL+S3 AWS Tessera implementation. It requires a // MySQL database to successfully run the MySQL tests, otherwise they are // skipped. Run tests with `-parallel=1` to avoid concurent tests on the same // database, and specifically runs of `mustDropTables`. // // Sample command to start a local MySQL database using Docker: // $ docker run --name test-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=test_tessera -d mysql package aws import ( "bytes" "context" "crypto/sha256" "database/sql" "errors" "flag" "fmt" "os" "reflect" "strings" "sync" "testing" "time" "github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/smithy-go" "github.com/google/go-cmp/cmp" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/fsck" storage "github.com/transparency-dev/tessera/storage/internal" "golang.org/x/mod/sumdb/note" "k8s.io/klog/v2" ) var ( mySQLURI = flag.String("mysql_uri", "root:root@tcp(localhost:3306)/test_tessera", "Connection string for a MySQL database") isMySQLTestOptional = flag.Bool("is_mysql_test_optional", true, "Boolean value to control whether the MySQL test is optional") ) // TestMain inits flags and runs tests. func TestMain(m *testing.M) { klog.InitFlags(nil) // m.Run() will parse flags os.Exit(m.Run()) } // canSkipMySQLTest checks if the test MySQL db is available and, if not, if the test can be skipped. // // Use this method before every MySQL test, and if it returns true, skip the test. // // If is_mysql_test_optional is set to true and MySQL database cannot be opened or pinged, // the test will fail immediately. Otherwise, the test will be skipped if the test is optional // and the database is not available. func canSkipMySQLTest(t *testing.T, ctx context.Context) bool { t.Helper() db, err := sql.Open("mysql", *mySQLURI) if err != nil { if *isMySQLTestOptional { return true } t.Fatalf("failed to open MySQL test db: %v", err) } defer func() { if err := db.Close(); err != nil { t.Fatalf("failed to close MySQL database: %v", err) } }() if err := db.PingContext(ctx); err != nil { if *isMySQLTestOptional { return true } t.Fatalf("failed to ping MySQL test db: %v", err) } return false } // mustDropTables drops the `Seq`, `SeqCoord` and `IntCoord` tables. // Call this function before every MySQL test. func mustDropTables(t *testing.T, ctx context.Context) { t.Helper() db, err := sql.Open("mysql", *mySQLURI) if err != nil { t.Fatalf("failed to connect to db: %v", *mySQLURI) } defer func() { if err := db.Close(); err != nil { t.Fatalf("failed to close db: %v", err) } }() if _, err := db.ExecContext(ctx, "DROP TABLE IF EXISTS `Seq`, `SeqCoord`, `IntCoord`, `PubCoord`, `GCCoord`"); err != nil { t.Fatalf("failed to drop all tables: %v", err) } } func TestMySQLSequencerAssignEntries(t *testing.T) { ctx := context.Background() if canSkipMySQLTest(t, ctx) { klog.Warningf("MySQL not available, skipping %s", t.Name()) t.Skip("MySQL not available, skipping test") } // Clean tables in case there's already something in there. mustDropTables(t, ctx) seq, err := newMySQLSequencer(ctx, *mySQLURI, 1000, 0, 0) if err != nil { t.Fatalf("newMySQLSequencer: %v", err) } want := uint64(0) for chunks := range 10 { entries := []*tessera.Entry{} for i := range 10 + chunks { entries = append(entries, tessera.NewEntry(fmt.Appendf(nil, "item %d/%d", chunks, i))) } if err := seq.assignEntries(ctx, entries); err != nil { t.Fatalf("assignEntries: %v", err) } for i, e := range entries { if got := *e.Index(); got != want { t.Errorf("Chunk %d entry %d got seq %d, want %d", chunks, i, got, want) } want++ } } } func TestMySQLSequencerPushback(t *testing.T) { ctx := context.Background() if canSkipMySQLTest(t, ctx) { klog.Warningf("MySQL not available, skipping %s", t.Name()) t.Skip("MySQL not available, skipping test") } // Clean tables in case there's already something in there. mustDropTables(t, ctx) for _, test := range []struct { name string threshold uint64 initialEntries int wantPushback bool }{ { name: "no pushback: num < threshold", threshold: 10, initialEntries: 5, }, { name: "no pushback: num = threshold", threshold: 10, initialEntries: 10, }, { name: "pushback: initial > threshold", threshold: 10, initialEntries: 15, wantPushback: true, }, } { t.Run(test.name, func(t *testing.T) { mustDropTables(t, ctx) seq, err := newMySQLSequencer(ctx, *mySQLURI, test.threshold, 0, 0) if err != nil { t.Fatalf("newMySQLSequencer: %v", err) } // Set up the test scenario with the configured number of initial outstanding entries entries := []*tessera.Entry{} for i := range test.initialEntries { entries = append(entries, tessera.NewEntry(fmt.Appendf(nil, "initial item %d", i))) } if err := seq.assignEntries(ctx, entries); err != nil { t.Fatalf("initial assignEntries: %v", err) } // Now perform the test with a single additional entry to check for pushback entries = []*tessera.Entry{tessera.NewEntry([]byte("additional"))} err = seq.assignEntries(ctx, entries) if gotPushback := errors.Is(err, tessera.ErrPushback); gotPushback != test.wantPushback { t.Fatalf("assignEntries: got pushback %t (%v), want pushback: %t", gotPushback, err, test.wantPushback) } else if !gotPushback && err != nil { t.Fatalf("assignEntries: %v", err) } }) } } func TestMySQLSequencerRoundTrip(t *testing.T) { ctx := context.Background() if canSkipMySQLTest(t, ctx) { klog.Warningf("MySQL not available, skipping %s", t.Name()) t.Skip("MySQL not available, skipping test") } // Clean tables in case there's already something in there. mustDropTables(t, ctx) s, err := newMySQLSequencer(ctx, *mySQLURI, 1000, 0, 0) if err != nil { t.Fatalf("newMySQLSequencer: %v", err) } seq := 0 wantEntries := []storage.SequencedEntry{} for chunks := range 10 { entries := []*tessera.Entry{} for range 10 + chunks { e := tessera.NewEntry(fmt.Appendf(nil, "item %d", seq)) entries = append(entries, e) wantEntries = append(wantEntries, storage.SequencedEntry{ BundleData: e.MarshalBundleData(uint64(seq)), LeafHash: e.LeafHash(), }) seq++ } if err := s.assignEntries(ctx, entries); err != nil { t.Fatalf("assignEntries: %v", err) } } seenIdx := uint64(0) f := func(_ context.Context, fromSeq uint64, entries []storage.SequencedEntry) ([]byte, error) { if fromSeq != seenIdx { return nil, fmt.Errorf("f called with fromSeq %d, want %d", fromSeq, seenIdx) } for i, e := range entries { if got, want := e, wantEntries[i]; !reflect.DeepEqual(got, want) { return nil, fmt.Errorf("entry %d+%d != %d", fromSeq, i, seenIdx) } seenIdx++ } return []byte("newroot"), nil } more, err := s.consumeEntries(ctx, 7, f, false) if err != nil { t.Errorf("consumeEntries: %v", err) } if !more { t.Errorf("more: false, expected true") } } func makeTile(t *testing.T, size uint64) *api.HashTile { t.Helper() r := &api.HashTile{Nodes: make([][]byte, size)} for i := uint64(0); i < size; i++ { h := sha256.Sum256(fmt.Appendf(nil, "%d", i)) r.Nodes[i] = h[:] } return r } func TestTileRoundtrip(t *testing.T) { ctx := context.Background() m := newMemObjStore() s := &logResourceStore{objStore: m} for _, test := range []struct { name string level uint64 index uint64 logSize uint64 tileSize uint64 }{ { name: "ok", level: 0, index: 3 * layout.TileWidth, logSize: 3*layout.TileWidth + 20, tileSize: 20, }, } { t.Run(test.name, func(t *testing.T) { wantTile := makeTile(t, test.tileSize) if err := s.setTile(ctx, test.level, test.index, test.logSize, wantTile); err != nil { t.Fatalf("setTile: %v", err) } expPath := layout.TilePath(test.level, test.index, layout.PartialTileSize(test.level, test.index, test.logSize)) _, ok := m.mem[expPath] if !ok { t.Fatalf("want tile at %v but found none", expPath) } got, err := s.getTiles(ctx, []storage.TileID{{Level: test.level, Index: test.index}}, test.logSize) if err != nil { t.Fatalf("getTile: %v", err) } if !cmp.Equal(got[0], wantTile) { t.Fatal("roundtrip returned different data") } }) } } func makeBundle(t *testing.T, idx uint64, size int) []byte { t.Helper() r := &bytes.Buffer{} if size == 0 { size = layout.EntryBundleWidth } for i := range size { e := tessera.NewEntry(fmt.Appendf(nil, "%d:%d", idx, i)) if _, err := r.Write(e.MarshalBundleData(uint64(i))); err != nil { t.Fatalf("MarshalBundleEntry: %v", err) } } return r.Bytes() } func TestBundleRoundtrip(t *testing.T) { ctx := context.Background() m := newMemObjStore() s := &logResourceStore{ objStore: m, entriesPath: layout.EntriesPath, } for _, test := range []struct { name string index uint64 p uint8 bundleSize int }{ { name: "ok", index: 3 * layout.EntryBundleWidth, p: 20, bundleSize: 20, }, } { t.Run(test.name, func(t *testing.T) { wantBundle := makeBundle(t, 0, test.bundleSize) if err := s.setEntryBundle(ctx, test.index, test.p, wantBundle); err != nil { t.Fatalf("setEntryBundle: %v", err) } expPath := layout.EntriesPath(test.index, test.p) _, ok := m.mem[expPath] if !ok { t.Fatalf("want bundle at %v but found none", expPath) } got, err := s.getEntryBundle(ctx, test.index, test.p) if err != nil { t.Fatalf("getEntryBundle: %v", err) } if !cmp.Equal(got, wantBundle) { t.Fatal("roundtrip returned different data") } }) } } func TestPublishTree(t *testing.T) { ctx := context.Background() if canSkipMySQLTest(t, ctx) { klog.Warningf("MySQL not available, skipping %s", t.Name()) t.Skip("MySQL not available, skipping test") } for _, test := range []struct { name string publishInterval time.Duration republishInterval time.Duration attempts []time.Duration wantUpdates int }{ { name: "works ok", publishInterval: 100 * time.Millisecond, republishInterval: 100 * time.Millisecond, attempts: []time.Duration{1 * time.Second}, wantUpdates: 1, }, { name: "too soon, skip update", publishInterval: 10 * time.Second, republishInterval: 10 * time.Second, attempts: []time.Duration{100 * time.Millisecond}, wantUpdates: 0, }, { name: "too soon, skip update, but recovers", publishInterval: 2 * time.Second, republishInterval: 2 * time.Second, attempts: []time.Duration{100 * time.Millisecond, 2 * time.Second}, wantUpdates: 1, }, { name: "many attempts, eventually one succeeds", publishInterval: 1 * time.Second, republishInterval: 1 * time.Second, attempts: []time.Duration{300 * time.Millisecond, 300 * time.Millisecond, 300 * time.Millisecond, 300 * time.Millisecond}, wantUpdates: 1, }, { name: "republish needed", publishInterval: 1 * time.Second, republishInterval: 2 * time.Second, attempts: []time.Duration{1500 * time.Millisecond, 2500 * time.Millisecond}, wantUpdates: 1, }, } { t.Run(test.name, func(t *testing.T) { // Clean tables in case there's already something in there. mustDropTables(t, ctx) s, err := newMySQLSequencer(ctx, *mySQLURI, 1000, 0, 0) if err != nil { t.Fatalf("newMySQLSequencer: %v", err) } m := newMemObjStore() storage := &Appender{ logStore: &logResourceStore{ objStore: m, entriesPath: layout.EntriesPath, }, sequencer: s, newCP: func(_ context.Context, size uint64, hash []byte) ([]byte, error) { return fmt.Appendf(nil, "%d/%x,", size, hash), nil }, } // Call init so we've got a zero-sized checkpoint to work with. if err := storage.init(ctx); err != nil { t.Fatalf("storage.init: %v", err) } if err := s.publishCheckpoint(ctx, test.publishInterval, test.republishInterval, storage.publishCheckpoint); err != nil { t.Fatalf("publishTree: %v", err) } cpOld := []byte("bananas") if err := m.setObject(ctx, layout.CheckpointPath, cpOld, "", ""); err != nil { t.Fatalf("setObject(bananas): %v", err) } updatesSeen := 0 for _, d := range test.attempts { time.Sleep(d) if err := s.publishCheckpoint(ctx, test.publishInterval, test.republishInterval, storage.publishCheckpoint); err != nil { t.Fatalf("publishTree: %v", err) } cpNew, err := m.getObject(ctx, layout.CheckpointPath) if err != nil { t.Fatalf("getObject: %v", err) } if !bytes.Equal(cpOld, cpNew) { updatesSeen++ cpOld = cpNew } } if updatesSeen != test.wantUpdates { t.Fatalf("Saw %d updates, want %d", updatesSeen, test.wantUpdates) } }) } } func TestGarbageCollect(t *testing.T) { ctx := t.Context() if canSkipMySQLTest(t, ctx) { klog.Warningf("MySQL not available, skipping %s", t.Name()) t.Skip("MySQL not available, skipping test") } // Clean tables in case there's already something in there. mustDropTables(t, ctx) batchSize := uint64(60000) integrateEvery := uint64(31234) s, err := newMySQLSequencer(ctx, *mySQLURI, batchSize, 0, 0) if err != nil { t.Fatalf("newMySQLSequencer: %v", err) } defer func() { if err := s.dbPool.Close(); err != nil { t.Fatalf("Close: %v", err) } }() sk, vk := mustGenerateKeys(t) m := newMemObjStore() storage := &Storage{} opts := tessera.NewAppendOptions(). WithCheckpointInterval(1200*time.Millisecond). WithBatching(uint(batchSize), 100*time.Millisecond). // Disable GC so we can manually invoke below. WithGarbageCollectionInterval(time.Duration(0)). WithCheckpointSigner(sk) appender, lr, err := storage.newAppender(ctx, m, s, opts) if err != nil { t.Fatalf("newAppender: %v", err) } if err := appender.publishCheckpoint(ctx, 0, []byte("")); err != nil { t.Fatalf("publishCheckpoint: %v", err) } // Build a reasonably-sized tree with a bunch of partial resouces present, and wait for // it to be published. treeSize := uint64(256 * 384) a := tessera.NewPublicationAwaiter(ctx, lr.ReadCheckpoint, 100*time.Millisecond) // grow and garbage collect the tree several times to check continued correct operation over lifetime of the log for size := uint64(0); size < treeSize; { t.Logf("Adding entries from %d", size) for range batchSize { f := appender.Add(ctx, tessera.NewEntry(fmt.Appendf(nil, "entry %d", size))) if size%integrateEvery == 0 { t.Logf("Awaiting entry %d", size) if _, _, err := a.Await(ctx, f); err != nil { t.Fatalf("Await: %v", err) } } size++ } t.Logf("Awaiting tree at size %d", size) if _, _, err := a.Await(ctx, func() (tessera.Index, error) { return tessera.Index{Index: size - 1}, nil }); err != nil { t.Fatalf("Await final tree: %v", err) } t.Logf("Running GC at size %d", size) if err := s.garbageCollect(ctx, size, 1000, m.deleteObjectsWithPrefix, appender.logStore.entriesPath); err != nil { t.Fatalf("garbageCollect: %v", err) } t.Logf("GC complete at size %d", size) // Compare any remaining partial resources to the list of places // we'd expect them to be, given the tree size. wantPartialPrefixes := make(map[string]struct{}) for _, p := range expectedPartialPrefixes(size, appender.logStore.entriesPath) { wantPartialPrefixes[p] = struct{}{} } for k := range m.mem { if strings.Contains(k, ".p/") { p := strings.SplitAfter(k, ".p/")[0] if _, ok := wantPartialPrefixes[p]; !ok { t.Errorf("Found unwanted partial: %s", k) } } } } // And finally, for good measure, assert that all the resources implied by the log's checkpoint // are present. f := fsck.New(vk.Name(), vk, lr, defaultMerkleLeafHasher, fsck.Opts{N: 1}) if err := f.Check(ctx); err != nil { t.Fatalf("FSCK failed: %v", err) } } func TestGarbageCollectOption(t *testing.T) { batchSize := uint64(60000) integrateEvery := uint64(31234) garbageCollectionInterval := 100 * time.Millisecond for _, test := range []struct { name string withCTLayout bool withGarbageCollectionInterval time.Duration }{ { name: "on", withGarbageCollectionInterval: garbageCollectionInterval, withCTLayout: false, }, { name: "on-ct", withGarbageCollectionInterval: garbageCollectionInterval, withCTLayout: true, }, { name: "off", withGarbageCollectionInterval: time.Duration(0), withCTLayout: false, }, } { t.Run(test.name, func(t *testing.T) { ctx := t.Context() if canSkipMySQLTest(t, ctx) { klog.Warningf("MySQL not available, skipping %s", t.Name()) t.Skip("MySQL not available, skipping test") } // Clean tables in case there's already something in there. mustDropTables(t, ctx) s, err := newMySQLSequencer(ctx, *mySQLURI, batchSize, 0, 0) if err != nil { t.Fatalf("newMySQLSequencer: %v", err) } defer func() { if err := s.dbPool.Close(); err != nil { t.Fatalf("Close: %v", err) } }() sk, vk := mustGenerateKeys(t) m := newMemObjStore() storage := &Storage{} opts := tessera.NewAppendOptions(). WithCheckpointInterval(1200*time.Millisecond). WithBatching(uint(batchSize), 100*time.Millisecond). // Disable GC so we can manually invoke below. WithGarbageCollectionInterval(test.withGarbageCollectionInterval). WithCheckpointSigner(sk) if test.withCTLayout { opts.WithCTLayout() } appender, lr, err := storage.newAppender(ctx, m, s, opts) if err != nil { t.Fatalf("newAppender: %v", err) } if err := appender.publishCheckpoint(ctx, 0, []byte("")); err != nil { t.Fatalf("publishCheckpoint: %v", err) } // Build a reasonably-sized tree with a bunch of partial resouces present, and wait for // it to be published. treeSize := uint64(256 * 384) a := tessera.NewPublicationAwaiter(ctx, lr.ReadCheckpoint, 100*time.Millisecond) wantPartialPrefixes := make(map[string]struct{}) // Grow the tree several times to check continued correct operation over lifetime of the log. // Let garbage collection happen in the background. for size := uint64(0); size < treeSize; { t.Logf("Adding entries from %d", size) for range batchSize { f := appender.Add(ctx, tessera.NewEntry(fmt.Appendf(nil, "entry %d", size))) if size%integrateEvery == 0 { t.Logf("Awaiting entry %d", size) if _, _, err := a.Await(ctx, f); err != nil { t.Fatalf("Await: %v", err) } // If garbage collection is off, we want partial tiles and bundles to stick around. if test.withGarbageCollectionInterval == time.Duration(0) { for _, p := range expectedPartialPrefixes(size, appender.logStore.entriesPath) { wantPartialPrefixes[p] = struct{}{} } } } size++ } t.Logf("Awaiting tree at size %d", size) if _, _, err := a.Await(ctx, func() (tessera.Index, error) { return tessera.Index{Index: size - 1}, nil }); err != nil { t.Fatalf("Await final tree: %v", err) } // Leave a bit of time for Garbage Collection to run. time.Sleep(3 * garbageCollectionInterval) // Compare any remaining partial resources to the list of places // we'd expect them to be, given the tree size. // Regardless of whether garbage collection is on, partial tiles corresponding to the last // checkpoint should alway be here. for _, p := range expectedPartialPrefixes(size, appender.logStore.entriesPath) { wantPartialPrefixes[p] = struct{}{} } allPartialDirs := make(map[string]struct{}) for k := range m.mem { if strings.Contains(k, ".p/") { allPartialDirs[strings.SplitAfter(k, ".p/")[0]] = struct{}{} } } // If gargabe collection is on, no partial tiles other than the ones we expect should be // present. for p := range allPartialDirs { if _, ok := wantPartialPrefixes[p]; !ok && test.withGarbageCollectionInterval > 0 { t.Errorf("Found unwanted partial: %s", p) } delete(wantPartialPrefixes, p) } for p := range wantPartialPrefixes { t.Errorf("Did not find expected partial: %s", p) } } // And finally, for good measure, assert that all the resources implied by the log's checkpoint // are present. f := fsck.New(vk.Name(), vk, lr, defaultMerkleLeafHasher, fsck.Opts{N: 1}) if err := f.Check(ctx); err != nil { t.Fatalf("FSCK failed: %v", err) } }) } } // expectedPartialPrefixes returns a slice containing resource prefixes where it's acceptable for a // tree of the provided size to have partial resources. // // These are really just the right-hand tiles/entry bundle in the tree. func expectedPartialPrefixes(size uint64, entriesPath func(uint64, uint8) string) []string { r := []string{} for l, c := uint64(0), size; c > 0; l, c = l+1, c>>8 { idx, p := c/256, c%256 if p != 0 { if l == 0 { r = append(r, entriesPath(idx, 0)+".p/") } r = append(r, layout.TilePath(l, idx, 0)+".p/") } } return r } type memObjStore struct { sync.RWMutex mem map[string][]byte } func newMemObjStore() *memObjStore { return &memObjStore{ mem: make(map[string][]byte), } } func (m *memObjStore) getObject(_ context.Context, obj string) ([]byte, error) { m.RLock() defer m.RUnlock() d, ok := m.mem[obj] if !ok { return nil, fmt.Errorf("obj %q not found: %w", obj, &types.NoSuchKey{}) } return d, nil } // TODO(phboneff): add content type tests func (m *memObjStore) setObject(_ context.Context, obj string, data []byte, _, _ string) error { m.Lock() defer m.Unlock() m.mem[obj] = data return nil } // TODO(phboneff): add content type tests func (m *memObjStore) setObjectIfNoneMatch(_ context.Context, obj string, data []byte, _, _ string) error { m.Lock() defer m.Unlock() d, ok := m.mem[obj] if ok && !bytes.Equal(d, data) { return &smithy.GenericAPIError{Code: "PreconditionFailed"} } m.mem[obj] = data return nil } func (m *memObjStore) deleteObjectsWithPrefix(_ context.Context, prefix string) error { m.Lock() defer m.Unlock() for k := range m.mem { if strings.HasPrefix(k, prefix) { delete(m.mem, k) } } return nil } func mustGenerateKeys(t *testing.T) (note.Signer, note.Verifier) { sk, vk, err := note.GenerateKey(nil, "testlog") if err != nil { t.Fatalf("GenerateKey: %v", err) } s, err := note.NewSigner(sk) if err != nil { t.Fatalf("NewSigner: %v", err) } v, err := note.NewVerifier(vk) if err != nil { t.Fatalf("NewVerifier: %v", err) } return s, v } // defaultMerkleLeafHasher parses a C2SP tlog-tile bundle and returns the Merkle leaf hashes of each entry it contains. func defaultMerkleLeafHasher(bundle []byte) ([][]byte, error) { eb := &api.EntryBundle{} if err := eb.UnmarshalText(bundle); err != nil { return nil, fmt.Errorf("unmarshal: %v", err) } r := make([][]byte, 0, len(eb.Entries)) for _, e := range eb.Entries { h := rfc6962.DefaultHasher.HashLeaf(e) r = append(r, h[:]) } return r, nil } transparency-dev-tessera-3cb22ee/storage/aws/otel.go000066400000000000000000000015501511600621500226600ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package aws import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) const name = "github.com/transparency-dev/tessera/storage/aws" var ( tracer = otel.Tracer(name) ) var ( objectPathKey = attribute.Key("tessera.objectPath") ) transparency-dev-tessera-3cb22ee/storage/gcp/000077500000000000000000000000001511600621500213445ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/storage/gcp/README.md000066400000000000000000000076321511600621500226330ustar00rootroot00000000000000# Tessera on Google Cloud Platform This document describes the storage implementation for running Tessera on Google Cloud Platform (GCP). ## Overview This design takes advantage of GCS for long term storage and low cost & complexity serving of read traffic, but leverages something more transactional for coordinating the cluster. New entries flow in from the binary built with Tessera into transactional storage, where they're held temporarily to batch them up, and then assigned sequence numbers as each batch is flushed. This allows the `Add` API call to quickly return with *durably assigned* sequence numbers. From there, an async process derives the entry bundles and Merkle tree structure from the sequenced batches, writes these to GCS for serving, before finally removing integrated bundles from the transactional storage. Since entries are all sequenced by the time they're stored, and sequencing is done in "chunks", it's worth noting that all tree derivations are therefore idempotent. ## Transactional storage The transactional storage is implemented with Cloud Spanner, and uses a schema with the following tables: ### `Tessera` This table is used to identify the current schema version. ### `SeqCoord` A table with a single row which is used to keep track of the next assignable sequence number. ### `Seq` This holds batches of entries keyed by the sequence number assigned to the first entry in the batch. ### `IntCoord` This table is used to coordinate integration of sequenced batches in the `Seq` table. ### `PubCoord` This table is used to coordinate publication of new checkpoints, ensuring that checkpoints are not published more frequently than configured. ### `GCCoord` This table is used to coordinate garbage collection of partial tiles and entry bundles which have been made obsolete by the continued growth of the log. ## Life of a leaf 1. Leaves are submitted by the binary built using Tessera via a call the storage's `Add` func. 1. The storage library batches these entries up, and, after a configurable period of time has elapsed or the batch reaches a configurable size threshold, the batch is written to the `Seq` table which effectively assigns a sequence numbers to the entries using the following algorithm: In a transaction: 1. selects next from `SeqCoord` with for update ← this blocks other FE from writing their pools, but only for a short duration. 1. Inserts batch of entries into `Seq` with key `SeqCoord.next` 1. Update `SeqCoord` with `next+=len(batch)` 1. Newly sequenced entries are periodically appended to the tree: In a transaction: 1. select `seq` from `IntCoord` with for update ← this blocks other integrators from proceeding. 1. Select one or more consecutive batches from `Seq` for update, starting at `IntCoord.seq` 1. Write leaf bundles to GCS using batched entries 1. Integrate in Merkle tree and write tiles to GCS 1. Delete consumed batches from `Seq` 1. Update `IntCoord` with `seq+=num_entries_integrated` and the latest `rootHash` 1. Checkpoints representing the latest state of the tree are published at the configured interval. ## Antispam An experimental implementation has been tested which uses Spanner to store the `` --> `sequence` mapping. This works well using "slack" Spanner CPU available in the smallest possible footprint, and consequently is comparably cheap requiring only extra Spanner storage costs. ### Alternatives Considered Other transactional storage systems are available on GCP, e.g. CloudSQL or AlloyDB. Experiments were run using CloudSQL (MySQL), AlloyDB, and Spanner. Spanner worked out to be the cheapest while also removing much of the administrative overhead which would come from even a managed MySQL instance, and so was selected. The experimental implementation was tested to around 1B entries of 1KB each at a write rate of 1500/s. This was done using the smallest possible Spanner alloc of 100 Processing Units. transparency-dev-tessera-3cb22ee/storage/gcp/antispam/000077500000000000000000000000001511600621500231605ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/storage/gcp/antispam/gcp.go000066400000000000000000000422361511600621500242670ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package gcp contains a GCP-based antispam implementation for Tessera. // // A Spanner database provides a mechanism for maintaining an index of // hash --> log position for detecting duplicate submissions. package gcp import ( "bytes" "compress/gzip" "context" "encoding/base64" "errors" "fmt" "io" "iter" "os" "sync/atomic" "time" "cloud.google.com/go/spanner" database "cloud.google.com/go/spanner/admin/database/apiv1" adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" "cloud.google.com/go/spanner/apiv1/spannerpb" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/client" "google.golang.org/grpc/codes" "k8s.io/klog/v2" ) const ( DefaultMaxBatchSize = 1500 DefaultPushbackThreshold = 2048 ) // AntispamOpts allows configuration of some tunable options. type AntispamOpts struct { // MaxBatchSize is the largest number of mutations permitted in a single BatchWrite operation when // updating the antispam index. // // Larger batches can enable (up to a point) higher throughput, but care should be taken not to // overload the Spanner instance. // // During testing, we've found that 1500 appears to offer maximum throughput when using Spanner instances // with 300 or more PU. Smaller deployments (e.g. 100 PU) will likely perform better with smaller batch // sizes of around 64. MaxBatchSize uint // PushbackThreshold allows configuration of when to start responding to Add requests with pushback due to // the antispam follower falling too far behind. // // When the antispam follower is at least this many entries behind the size of the locally integrated tree, // the antispam decorator will return a wrapped tessera.ErrPushback for every Add request. PushbackThreshold uint } // NewAntispam returns an antispam driver which uses Spanner to maintain a mapping of // previously seen entries and their assigned indices. // // Note that the storage for this mapping is entirely separate and unconnected to the storage used for // maintaining the Merkle tree. // // This functionality is experimental! func NewAntispam(ctx context.Context, spannerDB string, opts AntispamOpts) (*AntispamStorage, error) { if opts.MaxBatchSize == 0 { opts.MaxBatchSize = DefaultMaxBatchSize } if opts.PushbackThreshold == 0 { opts.PushbackThreshold = DefaultPushbackThreshold } if err := createAndPrepareTables( ctx, spannerDB, []string{ "CREATE TABLE IF NOT EXISTS FollowCoord (id INT64 NOT NULL, nextIdx INT64 NOT NULL) PRIMARY KEY (id)", "CREATE TABLE IF NOT EXISTS IDSeq (h BYTES(32) NOT NULL, idx INT64 NOT NULL) PRIMARY KEY (h)", }, [][]*spanner.Mutation{ {spanner.Insert("FollowCoord", []string{"id", "nextIdx"}, []any{0, 0})}, }, ); err != nil { return nil, fmt.Errorf("failed to create tables: %v", err) } db, err := spanner.NewClient(ctx, spannerDB) if err != nil { return nil, fmt.Errorf("failed to connect to Spanner: %v", err) } r := &AntispamStorage{ opts: opts, dbPool: db, } return r, nil } type AntispamStorage struct { opts AntispamOpts dbPool *spanner.Client // pushBack is used to prevent the follower from getting too far underwater. // Populate dynamically will set this to true/false based on how far behind the follower is from the // currently integrated tree size. // When pushBack is true, the decorator will start returning a wrapped ErrPushback to all calls. pushBack atomic.Bool numLookups atomic.Uint64 numWrites atomic.Uint64 numHits atomic.Uint64 } // index returns the index (if any) previously associated with the provided hash func (d *AntispamStorage) index(ctx context.Context, h []byte) (*uint64, error) { ctx, span := tracer.Start(ctx, "tessera.antispam.gcp.index") defer span.End() d.numLookups.Add(1) var idx int64 if row, err := d.dbPool.Single().ReadRow(ctx, "IDSeq", spanner.Key{h}, []string{"idx"}); err != nil { if c := spanner.ErrCode(err); c == codes.NotFound { span.AddEvent("tessera.miss") return nil, nil } return nil, err } else { if err := row.Column(0, &idx); err != nil { return nil, fmt.Errorf("failed to read antispam index: %v", err) } idx := uint64(idx) span.AddEvent("tessera.hit") d.numHits.Add(1) return &idx, nil } } // Decorator returns a function which will wrap an underlying Add delegate with // code to dedup against the stored data. func (d *AntispamStorage) Decorator() func(f tessera.AddFn) tessera.AddFn { return func(delegate tessera.AddFn) tessera.AddFn { return func(ctx context.Context, e *tessera.Entry) tessera.IndexFuture { ctx, span := tracer.Start(ctx, "tessera.antispam.gcp.Add") defer span.End() if d.pushBack.Load() { span.AddEvent("tessera.pushback") // The follower is too far behind the currently integrated tree, so we're going to push back against // the incoming requests. // This should have two effects: // 1. The tree will cease growing, giving the follower a chance to catch up, and // 2. We'll stop doing lookups for each submission, freeing up Spanner CPU to focus on catching up. // // We may decide in the future that serving duplicate reads is more important than catching up as quickly // as possible, in which case we'd move this check down below the call to index. return func() (tessera.Index, error) { return tessera.Index{}, tessera.ErrPushbackAntispam } } idx, err := d.index(ctx, e.Identity()) if err != nil { return func() (tessera.Index, error) { return tessera.Index{}, err } } if idx != nil { return func() (tessera.Index, error) { return tessera.Index{Index: *idx, IsDup: true}, nil } } return delegate(ctx, e) } } } // Follower returns a follower which knows how to populate the antispam index. // // This implements tessera.Antispam. func (d *AntispamStorage) Follower(b func([]byte) ([][]byte, error)) tessera.Follower { f := &follower{ as: d, bundleHasher: b, } // Use the "normal" BatchWrite mechanism to update the antispam index. // This will be overriden by the test to use an "inline" mechanism since spannertest // does not support BatchWrite :( f.updateIndex = f.batchUpdateIndex if r := os.Getenv("SPANNER_EMULATOR_HOST"); r != "" { const warn = `H4sIAAAAAAAAA83VwRGAIAwEwH+qoFwrsEAr8eEDPZO7gxkcGV6G7IAJ2tr8iDp07Fs6J7BnImcK5J3EmHVIT2Dvp2YTVJMu/y1+X+jiFQ84LtK9mLHr0aqh+K15PwkWRDaPrcbU5WdMKILtCDMF5hSgQEdJlw/36D7eRYqPfsVNVBcMsNH2QQKq/p957Yr8RfWIE22t7L7ABwAA` r, _ := base64.StdEncoding.DecodeString(warn) gzr, _ := gzip.NewReader(bytes.NewReader([]byte(r))) w, _ := io.ReadAll(gzr) klog.Warningf("%s\nWarning: you're running under the Spanner emulator - this is not a supported environment!\n\n", string(w)) // Hack in a workaround for spannertest not supporting BatchWrites f.updateIndex = emulatorWorkaroundUpdateIndexTx } return f } // follower is a struct which knows how to populate the antispam storage with identity hashes // for entries in a log. type follower struct { as *AntispamStorage // updateIndex knows how to apply the provided slice of mutations to the underlying Spanner DB. // // In normal operation this simply points to the batchUpdateIndex func below, but spannertest // does not support either: // - BatchWrite operations, or // - nested transactions // so we use this member as a hook to fallback to // a regular transaction for tests. updateIndex func(context.Context, *spanner.ReadWriteTransaction, []*spanner.Mutation) error bundleHasher func([]byte) ([][]byte, error) } func (f *follower) Name() string { return "GCP antispam" } // Follow uses entry data from the log to populate the antispam storage. func (f *follower) Follow(ctx context.Context, lr tessera.LogReader) { errOutOfSync := errors.New("out-of-sync") t := time.NewTicker(time.Second) var ( next func() (client.Entry[[]byte], error, bool) stop func() curEntries [][]byte curIndex uint64 ) for { select { case <-ctx.Done(): return case <-t.C: } // logSize is the latest known size of the log we're following. // This will get initialised below, inside the loop. var logSize uint64 // Busy loop while there are entries to be consumed from the stream for streamDone := false; !streamDone; { _, err := f.as.dbPool.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { ctx, span := tracer.Start(ctx, "tessera.antispam.gcp.FollowTxn") defer span.End() // Figure out the last entry we used to populate our antispam storage. row, err := txn.ReadRowWithOptions(ctx, "FollowCoord", spanner.Key{0}, []string{"nextIdx"}, &spanner.ReadOptions{LockHint: spannerpb.ReadRequest_LOCK_HINT_EXCLUSIVE}) if err != nil { return err } var nextIdx int64 // Spanner doesn't support uint64 if err := row.Columns(&nextIdx); err != nil { return fmt.Errorf("failed to read follow coordination info: %v", err) } span.SetAttributes(followFromKey.Int64(nextIdx)) followFrom := uint64(nextIdx) if followFrom >= logSize { // Our view of the log is out of date, update it logSize, err = lr.IntegratedSize(ctx) if err != nil { streamDone = true return fmt.Errorf("populate: IntegratedSize(): %v", err) } switch { case followFrom > logSize: streamDone = true return fmt.Errorf("followFrom %d > size %d", followFrom, logSize) case followFrom == logSize: // We're caught up, so unblock pushback and go back to sleep streamDone = true f.as.pushBack.Store(false) return nil default: // size > followFrom, so there's more work to be done! } } pushback := logSize-followFrom > uint64(f.as.opts.PushbackThreshold) span.SetAttributes(pushbackKey.Bool(pushback)) f.as.pushBack.Store(pushback) // If this is the first time around the loop we need to start the stream of entries now that we know where we want to // start reading from: if next == nil { span.AddEvent("Start streaming entries") sizeFn := func(_ context.Context) (uint64, error) { return logSize, nil } numFetchers := uint(10) next, stop = iter.Pull2(client.Entries(client.EntryBundles(ctx, numFetchers, sizeFn, lr.ReadEntryBundle, followFrom, logSize-followFrom), f.bundleHasher)) } if curIndex == followFrom && curEntries != nil { // Note that it's possible for Spanner to automatically retry transactions in some circumstances, when it does // it'll call this function again. // If the above condition holds, then we're in a retry situation and we must use the same data again rather // than continue reading entries which will take us out of sync. } else { bs := uint64(f.as.opts.MaxBatchSize) if r := logSize - followFrom; r < bs { bs = r } batch := make([][]byte, 0, bs) for i := range int(bs) { e, err, ok := next() if !ok { // The entry stream has ended so we'll need to start a new stream next time around the loop: stop() next = nil break } if err != nil { return fmt.Errorf("entryReader.next: %v", err) } if wantIdx := followFrom + uint64(i); e.Index != wantIdx { // We're out of sync return errOutOfSync } batch = append(batch, e.Entry) } curEntries = batch curIndex = followFrom } if len(curEntries) == 0 { return nil } // Now update the index. { ms := make([]*spanner.Mutation, 0, len(curEntries)) for i, e := range curEntries { ms = append(ms, spanner.Insert("IDSeq", []string{"h", "idx"}, []any{e, int64(curIndex + uint64(i))})) } if err := f.updateIndex(ctx, txn, ms); err != nil { return err } } numAdded := uint64(len(curEntries)) f.as.numWrites.Add(numAdded) // Insertion of dupe entries was successful, so update our follow coordination row: m := make([]*spanner.Mutation, 0) m = append(m, spanner.Update("FollowCoord", []string{"id", "nextIdx"}, []any{0, int64(followFrom + numAdded)})) return txn.BufferWrite(m) }) if err != nil { if err != errOutOfSync { klog.Errorf("Failed to commit antispam population tx: %v", err) } stop() next = nil streamDone = true continue } curEntries = nil } } } // batchUpdateIndex applies the provided mutations using Spanner's BatchWrite support. // // Note that we _do not_ use the passed in txn here - we're writing the antispam entries outside of the transaction. // The reason is because we absolutely do not want the larger transaction to fail if there's already an entry for the // same hash in the IDSeq table - this would cause us to get stuck retrying forever, so we use BatchWrite and ignore // any AlreadyExists errors we encounter. // // It looks unusual, but is ok because: // - individual antispam entries failing to insert because there's already an entry for that hash is perfectly ok, // - we'll only continue on to update FollowCoord if no errors (other than AlreadyExists) occur while inserting entries, // - similarly, if we manage to insert antispam entries here, but then fail to update FollowCoord, we'll end up // retrying over the same set of log entries, and then ignoring the AlreadyExists which will occur. // // Alternative approaches are: // - Use InsertOrUpdate, but that will keep updating the index associated with the ID hash, and we'd rather keep serving // the earliest index known for that entry. // - Perform reads for each of the hashes we're about to write, and use that to filter writes. // This would work, but would also incur an extra round-trip of data which isn't really necessary but would // slow the process down considerably and add extra load to Spanner for no benefit. func (f *follower) batchUpdateIndex(ctx context.Context, _ *spanner.ReadWriteTransaction, ms []*spanner.Mutation) error { ctx, span := tracer.Start(ctx, "tessera.antispam.gcp.batchUpdateIndex") defer span.End() mgs := make([]*spanner.MutationGroup, 0, len(ms)) for _, m := range ms { mgs = append(mgs, &spanner.MutationGroup{ Mutations: []*spanner.Mutation{m}, }) } i := f.as.dbPool.BatchWrite(ctx, mgs) return i.Do(func(r *spannerpb.BatchWriteResponse) error { s := r.GetStatus() if c := codes.Code(s.Code); c != codes.OK && c != codes.AlreadyExists { return fmt.Errorf("failed to write antispam record: %v (%v)", s.GetMessage(), c) } return nil }) } // EntriesProcessed returns the total number of log entries processed. func (f *follower) EntriesProcessed(ctx context.Context) (uint64, error) { row, err := f.as.dbPool.Single().ReadRow(ctx, "FollowCoord", spanner.Key{0}, []string{"nextIdx"}) if err != nil { return 0, err } var nextIdx int64 // Spanner doesn't support uint64 if err := row.Columns(&nextIdx); err != nil { return 0, fmt.Errorf("failed to read follow coordination info: %v", err) } return uint64(nextIdx), nil } // createAndPrepareTables applies the passed in list of DDL statements and groups of mutations. // // This is intended to be used to create and initialise Spanner instances on first use. // DDL should likely be of the form "CREATE TABLE IF NOT EXISTS". // Mutation groups should likey be one or more spanner.Insert operations - AlreadyExists errors will be silently ignored. func createAndPrepareTables(ctx context.Context, spannerDB string, ddl []string, mutations [][]*spanner.Mutation) error { adminClient, err := database.NewDatabaseAdminClient(ctx) if err != nil { return err } defer func() { if err := adminClient.Close(); err != nil { klog.Warningf("adminClient.Close(): %v", err) } }() op, err := adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{ Database: spannerDB, Statements: ddl, }) if err != nil { return fmt.Errorf("failed to create tables: %v", err) } if err := op.Wait(ctx); err != nil { return err } dbPool, err := spanner.NewClient(ctx, spannerDB) if err != nil { return fmt.Errorf("failed to connect to Spanner: %v", err) } defer dbPool.Close() // Set default values for a newly initialised schema using passed in mutation groups. // Note that this will only succeed if no row exists, so there's no danger of "resetting" an existing log. for _, mg := range mutations { if _, err := dbPool.Apply(ctx, mg); err != nil && spanner.ErrCode(err) != codes.AlreadyExists { return err } } return nil } // emulatorWorkaroundUpdateIndexTx is a workaround for spannertest not supporting BatchWrites. // We use this func as a replacement for follower's updateIndex hook, and simply commit the index // updates inline with the larger transaction. func emulatorWorkaroundUpdateIndexTx(_ context.Context, txn *spanner.ReadWriteTransaction, ms []*spanner.Mutation) error { return txn.BufferWrite(ms) } transparency-dev-tessera-3cb22ee/storage/gcp/antispam/gcp_test.go000066400000000000000000000146021511600621500253220ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gcp import ( "crypto/sha256" "os" "testing" "time" "cloud.google.com/go/spanner/spannertest" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/testonly" "k8s.io/klog/v2" ) type testLookup struct { entryHash []byte wantNotFound bool } func TestAntispamStorage(t *testing.T) { for _, test := range []struct { name string opts AntispamOpts logEntries [][]byte lookupEntries []testLookup }{ { name: "roundtrip", logEntries: [][]byte{ []byte("one"), []byte("two"), []byte("three"), }, lookupEntries: []testLookup{ { entryHash: testIDHash([]byte("one")), }, { entryHash: testIDHash([]byte("two")), }, { entryHash: testIDHash([]byte("three")), }, { entryHash: testIDHash([]byte("nowhere to be found")), wantNotFound: true, }, }, }, } { t.Run(test.name, func(t *testing.T) { closeDB := newSpannerDB(t) defer closeDB() as, err := NewAntispam(t.Context(), "projects/p/instances/i/databases/d", test.opts) if err != nil { t.Fatalf("NewAntispam: %v", err) } fl, shutdown := testonly.NewTestLog(t, tessera.NewAppendOptions().WithCheckpointInterval(time.Second)) defer func() { if err := shutdown(t.Context()); err != nil { t.Logf("shutdown: %v", err) } }() f := as.Follower(testBundleHasher) go f.Follow(t.Context(), fl.LogReader) entryIndex := make(map[string]uint64) a := tessera.NewPublicationAwaiter(t.Context(), fl.LogReader.ReadCheckpoint, 100*time.Millisecond) for i, e := range test.logEntries { entry := tessera.NewEntry(e) f := fl.Appender.Add(t.Context(), entry) idx, _, err := a.Await(t.Context(), f) if err != nil { t.Fatalf("Await(%d): %v", i, err) } klog.Infof("%d == %x", i, entry.Identity()) entryIndex[string(testIDHash(e))] = idx.Index } for { time.Sleep(time.Second) pos, err := f.EntriesProcessed(t.Context()) if err != nil { t.Logf("EntriesProcessed: %v", err) continue } sz, err := fl.LogReader.IntegratedSize(t.Context()) if err != nil { t.Logf("IntegratedSize: %v", err) continue } klog.Infof("Wait for follower (%d) to catch up with tree (%d)", pos, sz) if pos >= sz { break } } for _, e := range test.lookupEntries { gotIndex, err := as.index(t.Context(), e.entryHash) if err != nil { t.Errorf("error looking up hash %x: %v", e.entryHash, err) } wantIndex := entryIndex[string(e.entryHash)] if gotIndex == nil { if !e.wantNotFound { t.Errorf("no index for hash %x, but expected index %d", e.entryHash, wantIndex) } continue } if *gotIndex != wantIndex { t.Errorf("got index %d, want %d from looking up hash %x", gotIndex, wantIndex, e.entryHash) } } }) } } func TestAntispamPushbackRecovers(t *testing.T) { for _, test := range []struct { name string opts AntispamOpts logEntries [][]byte }{ { name: "pushback", opts: AntispamOpts{ PushbackThreshold: 1, }, logEntries: [][]byte{ []byte("one"), []byte("two"), []byte("three"), }, }, } { t.Run(test.name, func(t *testing.T) { closeDB := newSpannerDB(t) defer closeDB() as, err := NewAntispam(t.Context(), "projects/p/instances/i/databases/d", test.opts) if err != nil { t.Fatalf("NewAntispam: %v", err) } fl, shutdown := testonly.NewTestLog(t, tessera.NewAppendOptions().WithCheckpointInterval(time.Second)) defer func() { if err := shutdown(t.Context()); err != nil { t.Logf("shutdown: %v", err) } }() f := as.Follower(testBundleHasher) entryIndex := make(map[string]uint64) a := tessera.NewPublicationAwaiter(t.Context(), fl.LogReader.ReadCheckpoint, 100*time.Millisecond) for i, e := range test.logEntries { entry := tessera.NewEntry(e) f := fl.Appender.Add(t.Context(), entry) idx, _, err := a.Await(t.Context(), f) if err != nil { t.Fatalf("Await(%d): %v", i, err) } klog.Infof("%d == %x", i, entry.Identity()) entryIndex[string(testIDHash(e))] = idx.Index } // Wait for entries te be integrated before we start the follower, so we know we'll hit the pushback condition go f.Follow(t.Context(), fl.LogReader) for { time.Sleep(time.Second) pos, err := f.EntriesProcessed(t.Context()) if err != nil { t.Logf("EntriesProcessed: %v", err) continue } sz, err := fl.LogReader.IntegratedSize(t.Context()) if err != nil { t.Logf("IntegratedSize: %v", err) continue } klog.Infof("Wait for follower (%d) to catch up with tree (%d)", pos, sz) if pos >= sz { break } } // Ensure that the follower gets itself _out_ of pushback mode once it's caught up. // We'll give the follower some time to do its thing and notice. // It runs onces a second, so this should be plenty of time. for i := range 5 { time.Sleep(time.Second) if !as.pushBack.Load() { t.Logf("Antispam caught up and out of pushback in %ds", i) return } } t.Fatalf("pushBack remains true after 5 seconds despite being caught up!") }) } } func newSpannerDB(t *testing.T) func() { t.Helper() srv, err := spannertest.NewServer("localhost:0") if err != nil { t.Fatalf("Failed to set up test spanner: %v", err) } if err := os.Setenv("SPANNER_EMULATOR_HOST", srv.Addr); err != nil { t.Fatalf("Setenv: %v", err) } return srv.Close } func testIDHash(d []byte) []byte { r := sha256.Sum256(d) return r[:] } func testBundleHasher(b []byte) ([][]byte, error) { bun := &api.EntryBundle{} err := bun.UnmarshalText(b) if err != nil { return nil, err } r := make([][]byte, len(bun.Entries)) for i, e := range bun.Entries { r[i] = testIDHash(e) } return r, err } transparency-dev-tessera-3cb22ee/storage/gcp/antispam/otel.go000066400000000000000000000016441511600621500244570ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gcp import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) const name = "github.com/transparency-dev/tessera/storage/gcp/antispam" var ( tracer = otel.Tracer(name) ) var ( followFromKey = attribute.Key("tessera.followFrom") pushbackKey = attribute.Key("tessera.pushback") ) transparency-dev-tessera-3cb22ee/storage/gcp/gcp.go000066400000000000000000001540501511600621500224510ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package gcp contains a GCP-based storage implementation for Tessera. // // TODO: decide whether to rename this package. // // This storage implementation uses GCS for long-term storage and serving of // entry bundles and log tiles, and Spanner for coordinating updates to GCS // when multiple instances of a personality binary are running. // // A single GCS bucket is used to hold entry bundles and log internal tiles. // The object keys for the bucket are selected so as to conform to the // expected layout of a tile-based log. // // A Spanner database provides a transactional mechanism to allow multiple // frontends to safely update the contents of the log. package gcp import ( "bytes" "context" "encoding/gob" "errors" "fmt" "io" "net/http" "os" "path/filepath" "sync" "time" "cloud.google.com/go/spanner" database "cloud.google.com/go/spanner/admin/database/apiv1" adminpb "cloud.google.com/go/spanner/admin/database/apiv1/databasepb" "cloud.google.com/go/spanner/apiv1/spannerpb" gcs "cloud.google.com/go/storage" "github.com/google/go-cmp/cmp" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/internal/fetcher" "github.com/transparency-dev/tessera/internal/migrate" "github.com/transparency-dev/tessera/internal/otel" "github.com/transparency-dev/tessera/internal/parse" storage "github.com/transparency-dev/tessera/storage/internal" "golang.org/x/sync/errgroup" "google.golang.org/api/googleapi" "google.golang.org/api/iterator" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" "k8s.io/klog/v2" ) const ( // minCheckpointInterval is the shortest permitted interval between updating published checkpoints. // GCS has a rate limit 1 update per second for individual objects, but we've observed that attempting // to update at exactly that rate still results in the occasional refusal, so bake in a little wiggle // room. minCheckpointInterval = 1200 * time.Millisecond logContType = "application/octet-stream" ckptContType = "text/plain; charset=utf-8" logCacheControl = "max-age=604800,immutable" ckptCacheControl = "no-cache" DefaultIntegrationSizeLimit = 5 * 4096 // SchemaCompatibilityVersion represents the expected version (e.g. layout & serialisation) of stored data. // // A binary built with a given version of the Tessera library is compatible with stored data created by a different version // of the library if and only if this value is the same as the compatibilityVersion stored in the Tessera table. // // NOTE: if changing this version, you need to consider whether end-users are going to update their schema instances to be // compatible with the new format, and provide a means to do it if so. SchemaCompatibilityVersion = 1 ) // Storage is a GCP based storage implementation for Tessera. type Storage struct { cfg Config } // sequencer describes a type which knows how to sequence entries. // // TODO(al): rename this as it's really more of a coordination for the log. type sequencer interface { // assignEntries should durably allocate contiguous index numbers to the provided entries. assignEntries(ctx context.Context, entries []*tessera.Entry) error // consumeEntries should call the provided function with up to limit previously sequenced entries. // If the call to consumeFunc returns no error, the entries should be considered to have been consumed. // If any entries were successfully consumed, the implementation should also return true; this // serves as a weak hint that there may be more entries to be consumed. // If forceUpdate is true, then the consumeFunc should be called, with an empty slice of entries if // necessary. This allows the log self-initialise in a transactionally safe manner. consumeEntries(ctx context.Context, limit uint64, f consumeFunc, forceUpdate bool) (bool, error) // currentTree returns the tree state of the currently integrated tree according to the IntCoord table. currentTree(ctx context.Context) (uint64, []byte, error) // nextIndex returns the next available index in the log. nextIndex(ctx context.Context) (uint64, error) // publishCheckpoint coordinates the publication of new checkpoints based on the current integrated tree. publishCheckpoint(ctx context.Context, minStaleActive, minStaleRepub time.Duration, f func(ctx context.Context, size uint64, root []byte) error) error // garbageCollect coordinates the removal of unneeded partial tiles/entry bundles for the provided tree size, up to a maximum number of deletes per invocation. garbageCollect(ctx context.Context, treeSize uint64, maxDeletes uint, removePrefix func(ctx context.Context, prefix string) error, entriesPath func(uint64, uint8) string) error } // consumeFunc is the signature of a function which can consume entries from the sequencer and integrate // them into the log. // Returns the new rootHash once all passed entries have been integrated. type consumeFunc func(ctx context.Context, from uint64, entries []storage.SequencedEntry) ([]byte, error) // Config holds GCP project and resource configuration for a storage instance. type Config struct { // GCSClient will be used to interact with GCS. If unset, Tessera will create one. GCSClient *gcs.Client // SpannerClient will be used to interact with Spanner. If unset, Tessera will create one. SpannerClient *spanner.Client // HTTPClient will be used for other HTTP requests. If unset, Tessera will use the net/http DefaultClient. HTTPClient *http.Client // Bucket is the name of the GCS bucket to use for storing log state. Bucket string // BucketPrefix is an optional prefix to prepend to all log resource paths. // This can be used e.g. to store multiple logs in the same bucket. BucketPrefix string // Spanner is the GCP resource URI of the spanner database instance to use. Spanner string } // New creates a new instance of the GCP based Storage. func New(ctx context.Context, cfg Config) (tessera.Driver, error) { if cfg.HTTPClient == nil { cfg.HTTPClient = http.DefaultClient } return &Storage{ cfg: cfg, }, nil } type LogReader struct { lrs logResourceStore integratedSize func(context.Context) (uint64, error) nextIndex func(context.Context) (uint64, error) } func (lr *LogReader) ReadCheckpoint(ctx context.Context) ([]byte, error) { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.ReadCheckpoint") defer span.End() r, err := lr.lrs.getCheckpoint(ctx) if err != nil { if errors.Is(err, gcs.ErrObjectNotExist) { return r, os.ErrNotExist } } return r, err } func (lr *LogReader) ReadTile(ctx context.Context, l, i uint64, p uint8) ([]byte, error) { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.ReadTile") defer span.End() return fetcher.PartialOrFullResource(ctx, p, func(ctx context.Context, p uint8) ([]byte, error) { return lr.lrs.getTile(ctx, l, i, p) }) } func (lr *LogReader) ReadEntryBundle(ctx context.Context, i uint64, p uint8) ([]byte, error) { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.ReadEntryBundle") defer span.End() return fetcher.PartialOrFullResource(ctx, p, func(ctx context.Context, p uint8) ([]byte, error) { return lr.lrs.getEntryBundle(ctx, i, p) }) } func (lr *LogReader) IntegratedSize(ctx context.Context) (uint64, error) { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.IntegratedSize") defer span.End() return lr.integratedSize(ctx) } func (lr *LogReader) NextIndex(ctx context.Context) (uint64, error) { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.NextIndex") defer span.End() return lr.nextIndex(ctx) } // Appender creates a new tessera.Appender lifecycle object. func (s *Storage) Appender(ctx context.Context, opts *tessera.AppendOptions) (*tessera.Appender, tessera.LogReader, error) { if s.cfg.GCSClient == nil { var err error s.cfg.GCSClient, err = gcs.NewClient(ctx, gcs.WithJSONReads()) if err != nil { return nil, nil, fmt.Errorf("failed to create GCS client: %v", err) } } gs := &gcsStorage{ gcsClient: s.cfg.GCSClient, bucket: s.cfg.Bucket, bucketPrefix: s.cfg.BucketPrefix, } var err error if s.cfg.SpannerClient == nil { s.cfg.SpannerClient, err = spanner.NewClient(ctx, s.cfg.Spanner) if err != nil { return nil, nil, fmt.Errorf("failed to connect to Spanner: %v", err) } } if err := initDB(ctx, s.cfg.Spanner); err != nil { return nil, nil, fmt.Errorf("failed to verify/init Spanner schema: %v", err) } seq, err := newSpannerCoordinator(ctx, s.cfg.SpannerClient, uint64(opts.PushbackMaxOutstanding())) if err != nil { return nil, nil, fmt.Errorf("failed to create Spanner coordinator: %v", err) } a, lr, err := s.newAppender(ctx, gs, seq, opts) if err != nil { return nil, nil, err } return &tessera.Appender{ Add: a.Add, }, lr, nil } // newAppender creates and initialises a tessera.Appender struct with the provided underlying storage implementations. func (s *Storage) newAppender(ctx context.Context, o objStore, seq *spannerCoordinator, opts *tessera.AppendOptions) (*Appender, tessera.LogReader, error) { if opts.CheckpointInterval() < minCheckpointInterval { return nil, nil, fmt.Errorf("requested CheckpointInterval (%v) is less than minimum permitted %v", opts.CheckpointInterval(), minCheckpointInterval) } a := &Appender{ logStore: &logResourceStore{ objStore: o, entriesPath: opts.EntriesPath(), }, sequencer: seq, cpUpdated: make(chan struct{}), } a.queue = storage.NewQueue(ctx, opts.BatchMaxAge(), opts.BatchMaxSize(), a.sequencer.assignEntries) reader := &LogReader{ lrs: *a.logStore, integratedSize: func(context.Context) (uint64, error) { s, _, err := a.sequencer.currentTree(ctx) return s, err }, nextIndex: func(context.Context) (uint64, error) { return a.sequencer.nextIndex(ctx) }, } a.newCP = opts.CheckpointPublisher(reader, s.cfg.HTTPClient) if err := a.init(ctx); err != nil { return nil, nil, fmt.Errorf("failed to initialise log storage: %v", err) } go a.integrateEntriesJob(ctx) go a.publishCheckpointJob(ctx, opts.CheckpointInterval(), opts.CheckpointRepublishInterval()) if i := opts.GarbageCollectionInterval(); i > 0 { go a.garbageCollectorJob(ctx, i) } return a, reader, nil } // Appender is an implementation of the Tessera appender lifecycle contract. type Appender struct { newCP func(context.Context, uint64, []byte) ([]byte, error) sequencer sequencer logStore *logResourceStore queue *storage.Queue cpUpdated chan struct{} } // Add is the entrypoint for adding entries to a sequencing log. func (a *Appender) Add(ctx context.Context, e *tessera.Entry) tessera.IndexFuture { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.Add") defer span.End() return a.queue.Add(ctx, e) } // integrateEntriesJob periodically append newly sequenced entries. // // Blocks until ctx is done. func (a *Appender) integrateEntriesJob(ctx context.Context) { t := time.NewTicker(1 * time.Second) defer t.Stop() for { select { case <-ctx.Done(): return case <-t.C: } func() { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.integrateEntriesJob") defer span.End() // Don't quickloop for now, it causes issues updating checkpoint too frequently. cctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() if _, err := a.sequencer.consumeEntries(cctx, DefaultIntegrationSizeLimit, a.integrateEntries, false); err != nil { klog.Errorf("integrateEntriesJob: %v", err) return } select { case a.cpUpdated <- struct{}{}: default: } }() } } // publishCheckpointJob periodically attempts to publish a new checkpoint representing the current state // of the tree, once per interval. // // Blocks until ctx is done. func (a *Appender) publishCheckpointJob(ctx context.Context, pubInterval, republishInterval time.Duration) { t := time.NewTicker(pubInterval) defer t.Stop() for { select { case <-ctx.Done(): return case <-a.cpUpdated: case <-t.C: } func() { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.publishCheckpointJob") defer span.End() if err := a.sequencer.publishCheckpoint(ctx, pubInterval, republishInterval, a.publishCheckpoint); err != nil { klog.Warningf("publishCheckpoint failed: %v", err) } }() } } // garbageCollectorJob is a long-running function which handles the removal of obsolete partial tiles // and entry bundles. // Blocks until ctx is done. func (a *Appender) garbageCollectorJob(ctx context.Context, i time.Duration) { t := time.NewTicker(i) defer t.Stop() // Entirely arbitrary number. maxBundlesPerRun := uint(100) for { select { case <-ctx.Done(): return case <-t.C: } func() { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.garbageCollectTask") defer span.End() // Figure out the size of the latest published checkpoint - we can't be removing partial tiles implied by // that checkpoint just because we've done an integration and know about a larger (but as yet unpublished) // checkpoint! cp, err := a.logStore.getCheckpoint(ctx) if err != nil { klog.Warningf("Failed to get published checkpoint: %v", err) return } _, pubSize, _, err := parse.CheckpointUnsafe(cp) if err != nil { klog.Warningf("Failed to parse published checkpoint: %v", err) return } if err := a.sequencer.garbageCollect(ctx, pubSize, maxBundlesPerRun, a.logStore.objStore.deleteObjectsWithPrefix, a.logStore.entriesPath); err != nil { klog.Warningf("GarbageCollect failed: %v", err) return } }() } } // init ensures that the storage represents a log in a valid state. func (a *Appender) init(ctx context.Context) error { if _, err := a.logStore.getCheckpoint(ctx); err != nil { if errors.Is(err, gcs.ErrObjectNotExist) { // No checkpoint exists, do a forced (possibly empty) integration to create one in a safe // way (setting the checkpoint directly here would not be safe as it's outside the transactional // framework which prevents the tree from rolling backwards or otherwise forking). cctx, c := context.WithTimeout(ctx, 10*time.Second) defer c() if _, err := a.sequencer.consumeEntries(cctx, DefaultIntegrationSizeLimit, a.integrateEntries, true); err != nil { return fmt.Errorf("forced integrate: %v", err) } select { case a.cpUpdated <- struct{}{}: default: } return nil } return fmt.Errorf("failed to read checkpoint: %v", err) } return nil } func (a *Appender) publishCheckpoint(ctx context.Context, size uint64, root []byte) error { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.publishCheckpoint") defer span.End() span.SetAttributes(treeSizeKey.Int64(otel.Clamp64(size))) cpRaw, err := a.newCP(ctx, size, root) if err != nil { return fmt.Errorf("newCP: %v", err) } if err := a.logStore.setCheckpoint(ctx, cpRaw); err != nil { return fmt.Errorf("writeCheckpoint: %v", err) } klog.V(2).Infof("Published latest checkpoint: %d, %x", size, root) return nil } // objStore describes a type which can store and retrieve objects. type objStore interface { getObject(ctx context.Context, obj string) ([]byte, int64, error) setObject(ctx context.Context, obj string, data []byte, cond *gcs.Conditions, contType string, cacheCtl string) error deleteObjectsWithPrefix(ctx context.Context, prefix string) error } // logResourceStore knows how to read and write entries which represent a tiles log inside an objStore. type logResourceStore struct { objStore objStore entriesPath func(uint64, uint8) string } func (lrs *logResourceStore) setCheckpoint(ctx context.Context, cpRaw []byte) error { return lrs.objStore.setObject(ctx, layout.CheckpointPath, cpRaw, nil, ckptContType, ckptCacheControl) } func (lrs *logResourceStore) getCheckpoint(ctx context.Context) ([]byte, error) { r, _, err := lrs.objStore.getObject(ctx, layout.CheckpointPath) return r, err } // setTile idempotently stores the provided tile at the location implied by the given level, index, and treeSize. // // The location to which the tile is written is defined by the tile layout spec. func (s *logResourceStore) setTile(ctx context.Context, level, index uint64, partial uint8, data []byte) error { tPath := layout.TilePath(level, index, partial) return s.objStore.setObject(ctx, tPath, data, &gcs.Conditions{DoesNotExist: true}, logContType, logCacheControl) } // getTile retrieves the raw tile from the provided location. // // The location to which the tile is written is defined by the tile layout spec. func (s *logResourceStore) getTile(ctx context.Context, level, index uint64, partial uint8) ([]byte, error) { tPath := layout.TilePath(level, index, partial) d, _, err := s.objStore.getObject(ctx, tPath) return d, err } // getTiles returns the tiles with the given tile-coords for the specified log size. // // Tiles are returned in the same order as they're requested, nils represent tiles which were not found. func (s *logResourceStore) getTiles(ctx context.Context, tileIDs []storage.TileID, logSize uint64) ([]*api.HashTile, error) { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.getTiles") defer span.End() r := make([]*api.HashTile, len(tileIDs)) errG := errgroup.Group{} for i, id := range tileIDs { i := i id := id errG.Go(func() error { objName := layout.TilePath(id.Level, id.Index, layout.PartialTileSize(id.Level, id.Index, logSize)) data, _, err := s.objStore.getObject(ctx, objName) if err != nil { if errors.Is(err, gcs.ErrObjectNotExist) { // Depending on context, this may be ok. // We'll signal to higher levels that it wasn't found by retuning a nil for this tile. return nil } return err } t := &api.HashTile{} if err := t.UnmarshalText(data); err != nil { return fmt.Errorf("unmarshal(%q): %v", objName, err) } r[i] = t return nil }) } if err := errG.Wait(); err != nil { return nil, err } return r, nil } // getEntryBundle returns the serialised entry bundle at the location described by the given index and partial size. // A partial size of zero implies a full tile. // // Returns a wrapped os.ErrNotExist if the bundle does not exist. func (s *logResourceStore) getEntryBundle(ctx context.Context, bundleIndex uint64, p uint8) ([]byte, error) { objName := s.entriesPath(bundleIndex, p) data, _, err := s.objStore.getObject(ctx, objName) if err != nil { if errors.Is(err, gcs.ErrObjectNotExist) { // Return the generic NotExist error so that higher levels can differentiate // between this and other errors. return nil, fmt.Errorf("%v: %w", objName, os.ErrNotExist) } return nil, err } return data, nil } // setEntryBundle idempotently stores the serialised entry bundle at the location implied by the bundleIndex and treeSize. func (s *logResourceStore) setEntryBundle(ctx context.Context, bundleIndex uint64, p uint8, bundleRaw []byte) error { objName := s.entriesPath(bundleIndex, p) // Note that setObject does an idempotent interpretation of DoesNotExist - it only // returns an error if the named object exists _and_ contains different data to what's // passed in here. if err := s.objStore.setObject(ctx, objName, bundleRaw, &gcs.Conditions{DoesNotExist: true}, logContType, logCacheControl); err != nil { return fmt.Errorf("setObject(%q): %v", objName, err) } return nil } // integrateEntries appends the provided entries into the log starting at fromSeq. // // Returns the new root hash of the log with the entries added. func (a *Appender) integrateEntries(ctx context.Context, fromSeq uint64, entries []storage.SequencedEntry) ([]byte, error) { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.integrateEntries") defer span.End() var newRoot []byte errG := errgroup.Group{} errG.Go(func() error { if err := a.updateEntryBundles(ctx, fromSeq, entries); err != nil { return fmt.Errorf("updateEntryBundles: %v", err) } return nil }) errG.Go(func() error { lh := make([][]byte, len(entries)) for i, e := range entries { lh[i] = e.LeafHash } r, err := integrate(ctx, fromSeq, lh, a.logStore) if err != nil { return fmt.Errorf("integrate: %v", err) } newRoot = r return nil }) if err := errG.Wait(); err != nil { return nil, err } return newRoot, nil } // integrate adds the provided leaf hashes to the merkle tree, starting at the provided location. func integrate(ctx context.Context, fromSeq uint64, lh [][]byte, logStore *logResourceStore) ([]byte, error) { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.integrate") defer span.End() span.SetAttributes(fromSizeKey.Int64(otel.Clamp64(fromSeq)), numEntriesKey.Int(len(lh))) errG := errgroup.Group{} getTiles := func(ctx context.Context, tileIDs []storage.TileID, treeSize uint64) ([]*api.HashTile, error) { n, err := logStore.getTiles(ctx, tileIDs, treeSize) if err != nil { return nil, fmt.Errorf("getTiles: %w", err) } return n, nil } newSize, newRoot, tiles, err := storage.Integrate(ctx, getTiles, fromSeq, lh) if err != nil { return nil, fmt.Errorf("storage.Integrate: %v", err) } for k, v := range tiles { func(ctx context.Context, k storage.TileID, v *api.HashTile) { errG.Go(func() error { data, err := v.MarshalText() if err != nil { return err } return logStore.setTile(ctx, k.Level, k.Index, layout.PartialTileSize(k.Level, k.Index, newSize), data) }) }(ctx, k, v) } if err := errG.Wait(); err != nil { return nil, err } klog.V(1).Infof("New tree: %d, %x", newSize, newRoot) return newRoot, nil } // updateEntryBundles adds the entries being integrated into the entry bundles. // // The right-most bundle will be grown, if it's partial, and/or new bundles will be created as required. func (a *Appender) updateEntryBundles(ctx context.Context, fromSeq uint64, entries []storage.SequencedEntry) error { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.updateEntryBundles") defer span.End() if len(entries) == 0 { return nil } numAdded := uint64(0) bundleIndex, entriesInBundle := fromSeq/layout.EntryBundleWidth, fromSeq%layout.EntryBundleWidth bundleWriter := &bytes.Buffer{} if entriesInBundle > 0 { // If the latest bundle is partial, we need to read the data it contains in for our newer, larger, bundle. part, err := a.logStore.getEntryBundle(ctx, uint64(bundleIndex), uint8(entriesInBundle)) if err != nil { return err } if _, err := bundleWriter.Write(part); err != nil { return fmt.Errorf("bundleWriter: %v", err) } } seqErr := errgroup.Group{} // goSetEntryBundle is a function which uses seqErr to spin off a go-routine to write out an entry bundle. // It's used in the for loop below. goSetEntryBundle := func(ctx context.Context, bundleIndex uint64, p uint8, bundleRaw []byte) { seqErr.Go(func() error { if err := a.logStore.setEntryBundle(ctx, bundleIndex, p, bundleRaw); err != nil { return err } return nil }) } // Add new entries to the bundle for _, e := range entries { if _, err := bundleWriter.Write(e.BundleData); err != nil { return fmt.Errorf("bundleWriter.Write: %v", err) } entriesInBundle++ fromSeq++ numAdded++ if entriesInBundle == layout.EntryBundleWidth { // This bundle is full, so we need to write it out... klog.V(1).Infof("In-memory bundle idx %d is full, attempting write to GCS", bundleIndex) goSetEntryBundle(ctx, bundleIndex, 0, bundleWriter.Bytes()) // ... and prepare the next entry bundle for any remaining entries in the batch bundleIndex++ entriesInBundle = 0 // Don't use Reset/Truncate here - the backing []bytes is still being used by goSetEntryBundle above. bundleWriter = &bytes.Buffer{} klog.V(1).Infof("Starting to fill in-memory bundle idx %d", bundleIndex) } } // If we have a partial bundle remaining once we've added all the entries from the batch, // this needs writing out too. if entriesInBundle > 0 { klog.V(1).Infof("Attempting to write in-memory partial bundle idx %d.%d to GCS", bundleIndex, entriesInBundle) goSetEntryBundle(ctx, bundleIndex, uint8(entriesInBundle), bundleWriter.Bytes()) } return seqErr.Wait() } // spannerCoordinator uses Cloud Spanner to provide // a durable and thread/multi-process safe sequencer. type spannerCoordinator struct { dbPool *spanner.Client maxOutstanding uint64 } // newSpannerCoordinator returns a new spannerSequencer struct which uses the provided // spanner resource name for its spanner connection. func newSpannerCoordinator(ctx context.Context, dbPool *spanner.Client, maxOutstanding uint64) (*spannerCoordinator, error) { r := &spannerCoordinator{ dbPool: dbPool, maxOutstanding: maxOutstanding, } if err := r.checkDataCompatibility(ctx); err != nil { return nil, fmt.Errorf("schema is not compatible with this version of the Tessera library: %v", err) } return r, nil } // initDB ensures that the coordination DB is initialised correctly. // // The database schema consists of 3 tables: // - SeqCoord // This table only ever contains a single row which tracks the next available // sequence number. // - Seq // This table holds sequenced "batches" of entries. The batches are keyed // by the sequence number assigned to the first entry in the batch, and // each subsequent entry in the batch takes the numerically next sequence number. // - IntCoord // This table coordinates integration of the batches of entries stored in // Seq into the committed tree state. func initDB(ctx context.Context, spannerDB string) error { return createAndPrepareTables(ctx, spannerDB, []string{ "CREATE TABLE IF NOT EXISTS Tessera (id INT64 NOT NULL, compatibilityVersion INT64 NOT NULL) PRIMARY KEY (id)", "CREATE TABLE IF NOT EXISTS SeqCoord (id INT64 NOT NULL, next INT64 NOT NULL,) PRIMARY KEY (id)", "CREATE TABLE IF NOT EXISTS Seq (id INT64 NOT NULL, seq INT64 NOT NULL, v BYTES(MAX),) PRIMARY KEY (id, seq)", "CREATE TABLE IF NOT EXISTS IntCoord (id INT64 NOT NULL, seq INT64 NOT NULL, rootHash BYTES(32)) PRIMARY KEY (id)", "CREATE TABLE IF NOT EXISTS PubCoord (id INT64 NOT NULL, publishedAt TIMESTAMP NOT NULL, size INT64) PRIMARY KEY (id)", "CREATE TABLE IF NOT EXISTS GCCoord (id INT64 NOT NULL, fromSize INT64 NOT NULL) PRIMARY KEY (id)", }, []string{ "ALTER TABLE PubCoord ADD COLUMN IF NOT EXISTS size INT64", }, [][]*spanner.Mutation{ {spanner.Insert("Tessera", []string{"id", "compatibilityVersion"}, []any{0, SchemaCompatibilityVersion})}, {spanner.Insert("SeqCoord", []string{"id", "next"}, []any{0, 0})}, {spanner.Insert("IntCoord", []string{"id", "seq", "rootHash"}, []any{0, 0, rfc6962.DefaultHasher.EmptyRoot()})}, {spanner.Insert("PubCoord", []string{"id", "publishedAt", "size"}, []any{0, time.Unix(0, 0), 0})}, {spanner.Insert("GCCoord", []string{"id", "fromSize"}, []any{0, 0})}, }) } // checkDataCompatibility compares the Tessera library SchemaCompatibilityVersion with the one stored in the // database, and returns an error if they are not identical. func (s *spannerCoordinator) checkDataCompatibility(ctx context.Context) error { row, err := s.dbPool.Single().ReadRow(ctx, "Tessera", spanner.Key{0}, []string{"compatibilityVersion"}) if err != nil { return fmt.Errorf("failed to read schema compatibilityVersion: %v", err) } var compat int64 if err := row.Columns(&compat); err != nil { return fmt.Errorf("failed to scan schema compatibilityVersion: %v", err) } if compat != SchemaCompatibilityVersion { return fmt.Errorf("schema compatibilityVersion (%d) != library compatibilityVersion (%d)", compat, SchemaCompatibilityVersion) } return nil } // assignEntries durably assigns each of the passed-in entries an index in the log. // // Entries are allocated contiguous indices, in the order in which they appear in the entries parameter. // This is achieved by storing the passed-in entries in the Seq table in Spanner, keyed by the // index assigned to the first entry in the batch. func (s *spannerCoordinator) assignEntries(ctx context.Context, entries []*tessera.Entry) error { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.assignEntries") defer span.End() span.SetAttributes(numEntriesKey.Int(len(entries))) // First grab the treeSize in a non-locking read-only fashion (we don't want to block/collide with integration). // We'll use this value to determine whether we need to apply back-pressure. var treeSize int64 if row, err := s.dbPool.Single().ReadRow(ctx, "IntCoord", spanner.Key{0}, []string{"seq"}); err != nil { return err } else { if err := row.Column(0, &treeSize); err != nil { return fmt.Errorf("failed to read integration coordination info: %v", err) } } span.SetAttributes(treeSizeKey.Int64(treeSize)) var next int64 // Unfortunately, Spanner doesn't support uint64 so we'll have to cast around a bit. _, err := s.dbPool.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { // First we need to grab the next available sequence number from the SeqCoord table. row, err := txn.ReadRowWithOptions(ctx, "SeqCoord", spanner.Key{0}, []string{"id", "next"}, &spanner.ReadOptions{LockHint: spannerpb.ReadRequest_LOCK_HINT_EXCLUSIVE}) if err != nil { return fmt.Errorf("failed to read SeqCoord: %w", err) } var id int64 if err := row.Columns(&id, &next); err != nil { return fmt.Errorf("failed to parse id column: %v", err) } // Check whether there are too many outstanding entries and we should apply // back-pressure. if outstanding := next - treeSize; outstanding > int64(s.maxOutstanding) { return tessera.ErrPushbackIntegration } next := uint64(next) // Shadow next with a uint64 version of the same value to save on casts. sequencedEntries := make([]storage.SequencedEntry, len(entries)) // Assign provisional sequence numbers to entries. // We need to do this here in order to support serialisations which include the log position. for i, e := range entries { sequencedEntries[i] = storage.SequencedEntry{ BundleData: e.MarshalBundleData(next + uint64(i)), LeafHash: e.LeafHash(), } } // Flatten the entries into a single slice of bytes which we can store in the Seq.v column. b := &bytes.Buffer{} e := gob.NewEncoder(b) if err := e.Encode(sequencedEntries); err != nil { return fmt.Errorf("failed to serialise batch: %v", err) } data := b.Bytes() num := len(entries) // TODO(al): think about whether aligning bundles to tile boundaries would be a good idea or not. m := []*spanner.Mutation{ // Insert our newly sequenced batch of entries into Seq, spanner.Insert("Seq", []string{"id", "seq", "v"}, []any{0, int64(next), data}), // and update the next-available sequence number row in SeqCoord. spanner.Update("SeqCoord", []string{"id", "next"}, []any{0, int64(next) + int64(num)}), } if err := txn.BufferWrite(m); err != nil { return fmt.Errorf("failed to apply TX: %v", err) } return nil }) if err != nil { return fmt.Errorf("failed to flush batch: %w", err) } return nil } // consumeEntries calls f with previously sequenced entries. // // Once f returns without error, the entries it was called with are considered to have been consumed and are // removed from the Seq table. // // Returns true if some entries were consumed as a weak signal that there may be further entries waiting to be consumed. func (s *spannerCoordinator) consumeEntries(ctx context.Context, limit uint64, f consumeFunc, forceUpdate bool) (bool, error) { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.consumeEntries") defer span.End() didWork := false _, err := s.dbPool.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { // Figure out which is the starting index of sequenced entries to start consuming from. row, err := txn.ReadRowWithOptions(ctx, "IntCoord", spanner.Key{0}, []string{"seq", "rootHash"}, &spanner.ReadOptions{LockHint: spannerpb.ReadRequest_LOCK_HINT_EXCLUSIVE}) if err != nil { return err } var fromSeq int64 // Spanner doesn't support uint64 var rootHash []byte if err := row.Columns(&fromSeq, &rootHash); err != nil { return fmt.Errorf("failed to read integration coordination info: %v", err) } // See how much potential work there is to do and trim our limit accordingly. row, err = txn.ReadRow(ctx, "SeqCoord", spanner.Key{0}, []string{"next"}) if err != nil { return err } var endSeq int64 // Spanner doesn't support uint64 if err := row.Columns(&endSeq); err != nil { return fmt.Errorf("failed to read sequence coordination info: %v", err) } if endSeq == fromSeq { return nil } if l := fromSeq + int64(limit); l < endSeq { endSeq = l } klog.V(1).Infof("Consuming bundles start from %d to at most %d", fromSeq, endSeq) // Now read the sequenced starting at the index we got above. rows := txn.ReadWithOptions(ctx, "Seq", spanner.KeyRange{Start: spanner.Key{0, fromSeq}, End: spanner.Key{0, endSeq}}, []string{"seq", "v"}, &spanner.ReadOptions{LockHint: spannerpb.ReadRequest_LOCK_HINT_EXCLUSIVE}) defer rows.Stop() seqsConsumed := []int64{} entries := make([]storage.SequencedEntry, 0, endSeq-fromSeq) orderCheck := fromSeq for { row, err := rows.Next() if row == nil || err == iterator.Done { break } var vGob []byte var seq int64 // spanner doesn't have uint64 if err := row.Columns(&seq, &vGob); err != nil { return fmt.Errorf("failed to scan seq row: %v", err) } if orderCheck != seq { return fmt.Errorf("integrity fail - expected seq %d, but found %d", orderCheck, seq) } g := gob.NewDecoder(bytes.NewReader(vGob)) b := []storage.SequencedEntry{} if err := g.Decode(&b); err != nil { return fmt.Errorf("failed to deserialise v: %v", err) } entries = append(entries, b...) seqsConsumed = append(seqsConsumed, seq) orderCheck += int64(len(b)) } if len(seqsConsumed) == 0 && !forceUpdate { klog.V(1).Info("Found no rows to sequence") return nil } // Call consumeFunc with the entries we've found newRoot, err := f(ctx, uint64(fromSeq), entries) if err != nil { return err } // consumeFunc was successful, so we can update our coordination row, and delete the row(s) for // the then consumed entries. m := make([]*spanner.Mutation, 0) m = append(m, spanner.Update("IntCoord", []string{"id", "seq", "rootHash"}, []any{0, int64(orderCheck), newRoot})) for _, c := range seqsConsumed { m = append(m, spanner.Delete("Seq", spanner.Key{0, c})) } if len(m) > 0 { if err := txn.BufferWrite(m); err != nil { return err } } didWork = true return nil }) if err != nil { return false, err } return didWork, nil } // currentTree returns the size and root hash of the currently integrated tree. func (s *spannerCoordinator) currentTree(ctx context.Context) (uint64, []byte, error) { row, err := s.dbPool.Single().ReadRow(ctx, "IntCoord", spanner.Key{0}, []string{"seq", "rootHash"}) if err != nil { return 0, nil, fmt.Errorf("failed to read IntCoord: %v", err) } var fromSeq int64 // Spanner doesn't support uint64 var rootHash []byte if err := row.Columns(&fromSeq, &rootHash); err != nil { return 0, nil, fmt.Errorf("failed to read integration coordination info: %v", err) } return uint64(fromSeq), rootHash, nil } // nextIndex returns the next available index in the log. func (s *spannerCoordinator) nextIndex(ctx context.Context) (uint64, error) { txn := s.dbPool.ReadOnlyTransaction() defer txn.Close() var nextSeq int64 // Spanner doesn't support uint64 row, err := txn.ReadRow(ctx, "SeqCoord", spanner.Key{0}, []string{"next"}) if err != nil { return 0, fmt.Errorf("failed to read sequence coordination row: %v", err) } if err := row.Columns(&nextSeq); err != nil { return 0, fmt.Errorf("failed to read sequence coordination info: %v", err) } return uint64(nextSeq), nil } // publishCheckpoint checks when the last checkpoint was published, and if appropriate, calls the provided // function to publish a new one. // // A checkpoint will not be published if either: // - the currently published checkpoint was published less than minStaleActive ago // - the new checkpoint is the same size as the currently published one, AND the currently published checkpoint // was published less than minStaleRepub ago. // // This function uses PubCoord with an exclusive lock to guarantee that only one tessera instance can attempt to publish // a checkpoint at any given time. func (s *spannerCoordinator) publishCheckpoint(ctx context.Context, minStaleActive, minStaleRepub time.Duration, f func(context.Context, uint64, []byte) error) error { if _, err := s.dbPool.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { pRow, err := txn.ReadRowWithOptions(ctx, "PubCoord", spanner.Key{0}, []string{"publishedAt", "size"}, &spanner.ReadOptions{LockHint: spannerpb.ReadRequest_LOCK_HINT_EXCLUSIVE}) if err != nil { return fmt.Errorf("failed to read PubCoord: %w", err) } var pubAt time.Time var lastSize spanner.NullInt64 if err := pRow.Columns(&pubAt, &lastSize); err != nil { return fmt.Errorf("failed to parse PubCoord: %v", err) } cpAge := time.Since(pubAt) if cpAge < minStaleActive { klog.V(1).Infof("publishCheckpoint: last checkpoint published %s ago (< required %s), not publishing new checkpoint", cpAge, minStaleActive) return nil } // Can't just use currentTree() here as the spanner emulator doesn't do nested transactions, so do it manually: row, err := txn.ReadRow(ctx, "IntCoord", spanner.Key{0}, []string{"seq", "rootHash"}) if err != nil { return fmt.Errorf("failed to read IntCoord: %w", err) } var fromSeq int64 // Spanner doesn't support uint64 var rootHash []byte if err := row.Columns(&fromSeq, &rootHash); err != nil { return fmt.Errorf("failed to parse integration coordination info: %v", err) } currentSize := uint64(fromSeq) shouldPublish := minStaleRepub > 0 && cpAge >= minStaleRepub if !shouldPublish { if !lastSize.Valid { // If we don't know the last published size, we should probably publish to be safe/self-heal. shouldPublish = true } else if currentSize > uint64(lastSize.Int64) { shouldPublish = true } } if !shouldPublish { klog.V(1).Infof("publishCheckpoint: skipping publish because tree hasn't grown and previous checkpoint is too recent") return nil } klog.V(1).Infof("publishCheckpoint: updating checkpoint (replacing %s old checkpoint)", cpAge) if err := f(ctx, currentSize, rootHash); err != nil { return err } if err := txn.BufferWrite([]*spanner.Mutation{spanner.Update("PubCoord", []string{"id", "publishedAt", "size"}, []any{0, time.Now(), int64(currentSize)})}); err != nil { return err } return nil }); err != nil { return err } return nil } // garbageCollect will identify up to maxBundles unneeded partial entry bundles (and any unneeded partial tiles which sit above them in the tree) and // call the provided function to remove them. // // Uses the `GCCoord` table to ensure that only one binary is actively garbage collecting at any given time, and to track progress so that we don't // needlessly attempt to GC over regions which have already been cleaned. func (s *spannerCoordinator) garbageCollect(ctx context.Context, treeSize uint64, maxBundles uint, deleteWithPrefix func(ctx context.Context, prefix string) error, entriesPath func(uint64, uint8) string) error { _, err := s.dbPool.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { row, err := txn.ReadRowWithOptions(ctx, "GCCoord", spanner.Key{0}, []string{"fromSize"}, &spanner.ReadOptions{LockHint: spannerpb.ReadRequest_LOCK_HINT_EXCLUSIVE}) if err != nil { return fmt.Errorf("failed to read GCCoord: %w", err) } var fs int64 if err := row.Columns(&fs); err != nil { return fmt.Errorf("failed to parse row contents: %v", err) } fromSize := uint64(fs) if fromSize == treeSize { return nil } d := uint(0) eg := errgroup.Group{} // GC the tree in "vertical" chunks defined by entry bundles. for ri := range layout.Range(fromSize, treeSize-fromSize, treeSize) { // Only known-full bundles are in-scope for for GC, so exit if the current bundle is partial or // we've reached our limit of chunks. if ri.Partial > 0 || d > maxBundles { break } // GC any partial versions of the entry bundle itself and the tile which sits immediately above it. eg.Go(func() error { return deleteWithPrefix(ctx, entriesPath(ri.Index, 0)+".p/") }) eg.Go(func() error { return deleteWithPrefix(ctx, layout.TilePath(0, ri.Index, 0)+".p/") }) fromSize += uint64(ri.N) d++ // Now consider (only) the part of the tree which sits above the bundle. // We'll walk up the parent tiles for as a long as we're tracing the right-hand // edge of a perfect subtree. // This gives the property we'll only visit each parent tile once, rather than up to 256 times. pL, pIdx := uint64(0), ri.Index for isLastLeafInParent(pIdx) { // Move our coordinates up to the parent pL, pIdx = pL+1, pIdx>>layout.TileHeight // GC any partial versions of the parent tile. eg.Go(func() error { return deleteWithPrefix(ctx, layout.TilePath(pL, pIdx, 0)+".p/") }) } } if err := eg.Wait(); err != nil { return fmt.Errorf("failed to delete one or more objects: %v", err) } if err := txn.BufferWrite([]*spanner.Mutation{spanner.Update("GCCoord", []string{"id", "fromSize"}, []any{0, int64(fromSize)})}); err != nil { return err } return nil }) return err } // isLastLeafInParent returns true if a tile with the provided index is the final child node of a // (hypothetical) full parent tile. func isLastLeafInParent(i uint64) bool { return i%layout.TileWidth == layout.TileWidth-1 } // gcsStorage knows how to store and retrieve objects from GCS. type gcsStorage struct { bucket string bucketPrefix string gcsClient *gcs.Client } // getObject returns the data and generation of the specified object, or an error. func (s *gcsStorage) getObject(ctx context.Context, obj string) ([]byte, int64, error) { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.getObject") defer span.End() if s.bucketPrefix != "" { obj = filepath.Join(s.bucketPrefix, obj) } span.SetAttributes(objectPathKey.String(obj)) r, err := s.gcsClient.Bucket(s.bucket).Object(obj).NewReader(ctx) if err != nil { return nil, -1, fmt.Errorf("getObject: failed to create reader for object %q in bucket %q: %w", obj, s.bucket, err) } d, err := io.ReadAll(r) if err != nil { return nil, -1, fmt.Errorf("failed to read %q: %v", obj, err) } return d, r.Attrs.Generation, r.Close() } // setObject stores the provided data in the specified object, optionally gated by a condition. // // cond can be used to specify preconditions for the write (e.g. write iff not exists, write iff // current generation is X, etc.), or nil can be passed if no preconditions are desired. // // Note that when preconditions are specified and are not met, an error will be returned *unless* // the currently stored data is bit-for-bit identical to the data to-be-written. // This is intended to provide idempotentency for writes. func (s *gcsStorage) setObject(ctx context.Context, objName string, data []byte, cond *gcs.Conditions, contType string, cacheCtl string) error { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.setObject") defer span.End() if s.bucketPrefix != "" { objName = filepath.Join(s.bucketPrefix, objName) } span.SetAttributes(objectPathKey.String(objName)) bkt := s.gcsClient.Bucket(s.bucket) obj := bkt.Object(objName) var w *gcs.Writer if cond == nil { w = obj.NewWriter(ctx) } else { w = obj.If(*cond).NewWriter(ctx) } w.ContentType = contType w.CacheControl = cacheCtl // Limit the amount of memory used for buffers, see https://pkg.go.dev/cloud.google.com/go/storage#Writer w.ChunkSize = len(data) + 1024 if _, err := w.Write(data); err != nil { return fmt.Errorf("failed to write object %q to bucket %q: %w", objName, s.bucket, err) } if err := w.Close(); err != nil { // If we run into a precondition failure error, check that the object // which exists contains the same content that we want to write. // If so, we can consider this write to be idempotently successful. preconditionFailed := false // Helpfully, the mechanism for detecting a failed precodition differs depending // on whether you're using the HTTP or gRPC GCS client, so test both. if ee, ok := err.(*googleapi.Error); ok && ee.Code == http.StatusPreconditionFailed { preconditionFailed = true } else if st, ok := status.FromError(err); ok && st.Code() == codes.FailedPrecondition { preconditionFailed = true } if preconditionFailed { existing, existingGen, err := s.getObject(ctx, objName) if err != nil { return fmt.Errorf("failed to fetch existing content for %q (@%d): %v", objName, existingGen, err) } if !bytes.Equal(existing, data) { span.AddEvent("Non-idempotent write") klog.Errorf("Resource %q non-idempotent write:\n%s", objName, cmp.Diff(existing, data)) return fmt.Errorf("precondition failed: resource content for %q differs from data to-be-written", objName) } span.AddEvent("Idempotent write") klog.V(2).Infof("setObject: identical resource already exists for %q, continuing", objName) return nil } return fmt.Errorf("failed to close write on %q: %v", objName, err) } return nil } // deleteObjectsWithPrefix removes any objects with the provided prefix from GCS. func (s *gcsStorage) deleteObjectsWithPrefix(ctx context.Context, objPrefix string) error { ctx, span := tracer.Start(ctx, "tessera.storage.gcp.deleteObject") defer span.End() if s.bucketPrefix != "" { objPrefix = filepath.Join(s.bucketPrefix, objPrefix) } span.SetAttributes(objectPathKey.String(objPrefix)) bkt := s.gcsClient.Bucket(s.bucket) errs := []error(nil) it := bkt.Objects(ctx, &gcs.Query{Prefix: objPrefix}) for { attr, err := it.Next() if err != nil { if err == iterator.Done { break } return err } klog.V(2).Infof("Deleting object %s", attr.Name) if err := bkt.Object(attr.Name).Delete(ctx); err != nil { errs = append(errs, err) } } return errors.Join(errs...) } // MigrationWriter creates a new GCP storage for the MigrationTarget lifecycle mode. func (s *Storage) MigrationWriter(ctx context.Context, opts *tessera.MigrationOptions) (migrate.MigrationWriter, tessera.LogReader, error) { var err error if s.cfg.GCSClient == nil { s.cfg.GCSClient, err = gcs.NewClient(ctx, gcs.WithJSONReads()) if err != nil { return nil, nil, fmt.Errorf("failed to create GCS client: %v", err) } } if s.cfg.SpannerClient == nil { s.cfg.SpannerClient, err = spanner.NewClient(ctx, s.cfg.Spanner) if err != nil { return nil, nil, fmt.Errorf("failed to connect to Spanner: %v", err) } } if err := initDB(ctx, s.cfg.Spanner); err != nil { return nil, nil, fmt.Errorf("failed to verify/init Spanner schema: %v", err) } seq, err := newSpannerCoordinator(ctx, s.cfg.SpannerClient, 0) if err != nil { return nil, nil, fmt.Errorf("failed to create Spanner sequencer: %v", err) } m := &MigrationStorage{ s: s, dbPool: seq.dbPool, bundleHasher: opts.LeafHasher(), sequencer: seq, logStore: &logResourceStore{ objStore: &gcsStorage{ gcsClient: s.cfg.GCSClient, bucket: s.cfg.Bucket, bucketPrefix: s.cfg.BucketPrefix, }, entriesPath: opts.EntriesPath(), }, } r := &LogReader{ lrs: *m.logStore, integratedSize: func(context.Context) (uint64, error) { s, _, err := m.sequencer.currentTree(ctx) return s, err }, nextIndex: func(context.Context) (uint64, error) { return 0, nil }, } return m, r, nil } // MigrationStorgage implements the tessera.MigrationTarget lifecycle contract. type MigrationStorage struct { s *Storage dbPool *spanner.Client bundleHasher func([]byte) ([][]byte, error) sequencer sequencer logStore *logResourceStore } var _ migrate.MigrationWriter = &MigrationStorage{} func (m *MigrationStorage) AwaitIntegration(ctx context.Context, sourceSize uint64) ([]byte, error) { t := time.NewTicker(time.Second) defer t.Stop() for { select { case <-ctx.Done(): return nil, ctx.Err() case <-t.C: from, _, err := m.sequencer.currentTree(ctx) if err != nil && !errors.Is(err, os.ErrNotExist) { klog.Warningf("readTreeState: %v", err) continue } klog.Infof("Integrate from %d (Target %d)", from, sourceSize) newSize, newRoot, err := m.buildTree(ctx, sourceSize) if err != nil { klog.Warningf("integrate: %v", err) } if newSize == sourceSize { klog.Infof("Integrated to %d with roothash %x", newSize, newRoot) return newRoot, nil } } } } func (m *MigrationStorage) SetEntryBundle(ctx context.Context, index uint64, partial uint8, bundle []byte) error { return m.logStore.setEntryBundle(ctx, index, partial, bundle) } func (m *MigrationStorage) IntegratedSize(ctx context.Context) (uint64, error) { sz, _, err := m.sequencer.currentTree(ctx) return sz, err } func (m *MigrationStorage) fetchLeafHashes(ctx context.Context, from, to, sourceSize uint64) ([][]byte, error) { // TODO(al): Make this configurable. const maxBundles = 300 toBeAdded := sync.Map{} eg := errgroup.Group{} n := 0 for ri := range layout.Range(from, to, sourceSize) { eg.Go(func() error { b, err := m.logStore.getEntryBundle(ctx, ri.Index, ri.Partial) if err != nil { return fmt.Errorf("getEntryBundle(%d.%d): %v", ri.Index, ri.Partial, err) } bh, err := m.bundleHasher(b) if err != nil { return fmt.Errorf("bundleHasherFunc for bundle index %d: %v", ri.Index, err) } toBeAdded.Store(ri.Index, bh[ri.First:ri.First+ri.N]) return nil }) n++ if n >= maxBundles { break } } if err := eg.Wait(); err != nil { return nil, err } lh := make([][]byte, 0, maxBundles) for i := from / layout.EntryBundleWidth; ; i++ { v, ok := toBeAdded.LoadAndDelete(i) if !ok { break } bh := v.([][]byte) lh = append(lh, bh...) } return lh, nil } func (m *MigrationStorage) buildTree(ctx context.Context, sourceSize uint64) (uint64, []byte, error) { var newSize uint64 var newRoot []byte _, err := m.dbPool.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error { // Figure out which is the starting index of sequenced entries to start consuming from. row, err := txn.ReadRowWithOptions(ctx, "IntCoord", spanner.Key{0}, []string{"seq", "rootHash"}, &spanner.ReadOptions{LockHint: spannerpb.ReadRequest_LOCK_HINT_EXCLUSIVE}) if err != nil { return err } var fromSeq int64 // Spanner doesn't support uint64 var rootHash []byte if err := row.Columns(&fromSeq, &rootHash); err != nil { return fmt.Errorf("failed to read integration coordination info: %v", err) } from := uint64(fromSeq) klog.V(1).Infof("Integrating from %d", from) lh, err := m.fetchLeafHashes(ctx, from, sourceSize, sourceSize) if err != nil { return fmt.Errorf("fetchLeafHashes(%d, %d, %d): %v", from, sourceSize, sourceSize, err) } if len(lh) == 0 { klog.Infof("Integrate: nothing to do, nothing done") // Set these to the current state of the tree so we reflect that in buildTree's return values. newSize, newRoot = from, rootHash return nil } added := uint64(len(lh)) klog.Infof("Integrate: adding %d entries to existing tree size %d", len(lh), from) newRoot, err = integrate(ctx, from, lh, m.logStore) if err != nil { klog.Warningf("integrate failed: %v", err) return fmt.Errorf("integrate failed: %v", err) } newSize = from + added klog.Infof("Integrate: added %d entries", added) // integration was successful, so we can update our coordination row m := make([]*spanner.Mutation, 0) m = append(m, spanner.Update("IntCoord", []string{"id", "seq", "rootHash"}, []any{0, int64(from + added), newRoot})) return txn.BufferWrite(m) }) if err != nil { return 0, nil, err } return newSize, newRoot, nil } // createAndPrepareTables applies the passed in list of DDL statements and groups of mutations. // // This is intended to be used to create and initialise Spanner instances on first use. // DDL should likely be of the form "CREATE TABLE IF NOT EXISTS". // Mutation groups should likey be one or more spanner.Insert operations - AlreadyExists errors will be silently ignored. func createAndPrepareTables(ctx context.Context, spannerDB string, ddl []string, alter []string, mutations [][]*spanner.Mutation) error { adminClient, err := database.NewDatabaseAdminClient(ctx) if err != nil { return err } defer func() { if err := adminClient.Close(); err != nil { klog.Warningf("adminClient.Close(): %v", err) } }() op, err := adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{ Database: spannerDB, Statements: ddl, }) if err != nil { return fmt.Errorf("failed to create tables: %v", err) } if err := op.Wait(ctx); err != nil { return err } if len(alter) > 0 { // The spannertest emulator appears to ignore IF NOT EXISTS in ALTER DATABASE statements, so // we'll apply each update individually and ignore any AlreadyExists errors we see. for _, a := range alter { op, err := adminClient.UpdateDatabaseDdl(ctx, &adminpb.UpdateDatabaseDdlRequest{ Database: spannerDB, Statements: []string{a}, }) if err != nil { // && spanner.ErrCode(err) != codes.AlreadyExists { return fmt.Errorf("updateDatabaseDdl: %v", err) } if err := op.Wait(ctx); err != nil && spanner.ErrCode(err) != codes.AlreadyExists { return fmt.Errorf("failed to alter table: %v", err) } } } dbPool, err := spanner.NewClient(ctx, spannerDB) if err != nil { return fmt.Errorf("failed to connect to Spanner: %v", err) } defer dbPool.Close() // Set default values for a newly initialised schema using passed in mutation groups. // Note that this will only succeed if no row exists, so there's no danger of "resetting" an existing log. for _, mg := range mutations { if _, err := dbPool.Apply(ctx, mg); err != nil && spanner.ErrCode(err) != codes.AlreadyExists { return err } } return nil } transparency-dev-tessera-3cb22ee/storage/gcp/gcp_test.go000066400000000000000000000536771511600621500235250ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gcp import ( "bytes" "context" "crypto/sha256" "errors" "fmt" "log" "os" "reflect" "strings" "sync" "testing" "time" "cloud.google.com/go/spanner" "cloud.google.com/go/spanner/spannertest" gcs "cloud.google.com/go/storage" "github.com/google/go-cmp/cmp" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/fsck" storage "github.com/transparency-dev/tessera/storage/internal" "golang.org/x/mod/sumdb/note" ) func newSpannerDB(t *testing.T) (*spanner.Client, func()) { t.Helper() srv, err := spannertest.NewServer("localhost:0") if err != nil { t.Fatalf("Failed to set up test spanner: %v", err) } if err := os.Setenv("SPANNER_EMULATOR_HOST", srv.Addr); err != nil { t.Fatalf("Setenv: %v", err) } id := "projects/p/instances/i/databases/d" if err := initDB(t.Context(), id); err != nil { t.Fatalf("initDB: %v", err) } c, err := spanner.NewClient(t.Context(), id) if err != nil { t.Fatalf("NewClient: %v", err) } return c, srv.Close } func TestSpannerSequencerAssignEntries(t *testing.T) { ctx := context.Background() db, close := newSpannerDB(t) defer close() seq, err := newSpannerCoordinator(ctx, db, 1000) if err != nil { t.Fatalf("newSpannerCoordinator: %v", err) } want := uint64(0) for chunks := range 10 { entries := []*tessera.Entry{} for i := range 10 + chunks { entries = append(entries, tessera.NewEntry(fmt.Appendf(nil, "item %d/%d", chunks, i))) } if err := seq.assignEntries(ctx, entries); err != nil { t.Fatalf("assignEntries: %v", err) } for i, e := range entries { if got := *e.Index(); got != want { t.Errorf("Chunk %d entry %d got seq %d, want %d", chunks, i, got, want) } want++ } } } func TestSpannerSequencerPushback(t *testing.T) { ctx := context.Background() for _, test := range []struct { name string threshold uint64 initialEntries int wantPushback bool }{ { name: "no pushback: num < threshold", threshold: 10, initialEntries: 5, }, { name: "no pushback: num = threshold", threshold: 10, initialEntries: 10, }, { name: "pushback: initial > threshold", threshold: 10, initialEntries: 15, wantPushback: true, }, } { t.Run(test.name, func(t *testing.T) { db, close := newSpannerDB(t) defer close() seq, err := newSpannerCoordinator(ctx, db, test.threshold) if err != nil { t.Fatalf("newSpannerCoordinator: %v", err) } // Set up the test scenario with the configured number of initial outstanding entries entries := []*tessera.Entry{} for i := range test.initialEntries { entries = append(entries, tessera.NewEntry(fmt.Appendf(nil, "initial item %d", i))) } if err := seq.assignEntries(ctx, entries); err != nil { t.Fatalf("initial assignEntries: %v", err) } // Now perform the test with a single additional entry to check for pushback entries = []*tessera.Entry{tessera.NewEntry([]byte("additional"))} err = seq.assignEntries(ctx, entries) if gotPushback := errors.Is(err, tessera.ErrPushback); gotPushback != test.wantPushback { t.Fatalf("assignEntries: got pushback %t (%v), want pushback: %t", gotPushback, err, test.wantPushback) } else if !gotPushback && err != nil { t.Fatalf("assignEntries: %v", err) } }) } } func TestSpannerSequencerRoundTrip(t *testing.T) { ctx := context.Background() db, close := newSpannerDB(t) defer close() s, err := newSpannerCoordinator(ctx, db, 1000) if err != nil { t.Fatalf("newSpannerCoordinator: %v", err) } seq := 0 wantEntries := []storage.SequencedEntry{} for chunks := range 10 { entries := []*tessera.Entry{} for range 10 + chunks { e := tessera.NewEntry(fmt.Appendf(nil, "item %d", seq)) entries = append(entries, e) wantEntries = append(wantEntries, storage.SequencedEntry{ BundleData: e.MarshalBundleData(uint64(seq)), LeafHash: e.LeafHash(), }) seq++ } if err := s.assignEntries(ctx, entries); err != nil { t.Fatalf("assignEntries: %v", err) } } seenIdx := uint64(0) f := func(_ context.Context, fromSeq uint64, entries []storage.SequencedEntry) ([]byte, error) { if fromSeq != seenIdx { return nil, fmt.Errorf("f called with fromSeq %d, want %d", fromSeq, seenIdx) } for i, e := range entries { if got, want := e, wantEntries[i]; !reflect.DeepEqual(got, want) { return nil, fmt.Errorf("entry %d+%d != %d", fromSeq, i, seenIdx) } seenIdx++ } return fmt.Appendf(nil, "root<%d>", seenIdx), nil } more, err := s.consumeEntries(ctx, 7, f, false) if err != nil { t.Errorf("consumeEntries: %v", err) } if !more { t.Errorf("more: false, expected true") } } func TestCheckDataCompatibility(t *testing.T) { ctx := context.Background() db, close := newSpannerDB(t) defer close() s, err := newSpannerCoordinator(ctx, db, 1000) if err != nil { t.Fatalf("newSpannerCoordinator: %v", err) } for _, test := range []struct { desc string dbV int64 wantErr bool }{ { desc: "versions match", dbV: SchemaCompatibilityVersion, }, { desc: "data < library", dbV: SchemaCompatibilityVersion + 1, wantErr: true, }, { desc: "data > library", dbV: SchemaCompatibilityVersion - 1, wantErr: true, }, } { t.Run(test.desc, func(t *testing.T) { if _, err := s.dbPool.Apply(ctx, []*spanner.Mutation{spanner.InsertOrUpdate("Tessera", []string{"id", "compatibilityVersion"}, []any{0, test.dbV})}); err != nil { t.Fatalf("Failed for force schema version to %d: %v", test.dbV, err) } err := s.checkDataCompatibility(ctx) if gotErr := err != nil; test.wantErr != gotErr { t.Fatalf("checkDataCompatibility: %v, wantErr %t", err, test.wantErr) } }) } } func makeTile(t *testing.T, size uint64) *api.HashTile { t.Helper() r := &api.HashTile{Nodes: make([][]byte, size)} for i := uint64(0); i < size; i++ { h := sha256.Sum256(fmt.Appendf(nil, "%d", i)) r.Nodes[i] = h[:] } return r } func TestTileRoundtrip(t *testing.T) { ctx := context.Background() m := newMemObjStore() s := &logResourceStore{ objStore: m, } for _, test := range []struct { name string level uint64 index uint64 logSize uint64 tileSize uint64 }{ { name: "ok", level: 0, index: 3 * layout.TileWidth, logSize: 3*layout.TileWidth + 20, tileSize: 20, }, } { t.Run(test.name, func(t *testing.T) { wantTile := makeTile(t, test.tileSize) tRaw, err := wantTile.MarshalText() if err != nil { t.Fatalf("Failed to marshal tile: %v", err) } if err := s.setTile(ctx, test.level, test.index, layout.PartialTileSize(test.level, test.index, test.logSize), tRaw); err != nil { t.Fatalf("setTile: %v", err) } expPath := layout.TilePath(test.level, test.index, layout.PartialTileSize(test.level, test.index, test.logSize)) _, ok := m.mem[expPath] if !ok { t.Fatalf("want tile at %v but found none", expPath) } got, err := s.getTiles(ctx, []storage.TileID{{Level: test.level, Index: test.index}}, test.logSize) if err != nil { t.Fatalf("getTile: %v", err) } if !cmp.Equal(got[0], wantTile) { t.Fatal("roundtrip returned different data") } }) } } func makeBundle(t *testing.T, idx uint64, size int) []byte { t.Helper() r := &bytes.Buffer{} if size == 0 { size = layout.EntryBundleWidth } for i := range size { e := tessera.NewEntry(fmt.Appendf(nil, "%d:%d", idx, i)) if _, err := r.Write(e.MarshalBundleData(uint64(i))); err != nil { t.Fatalf("MarshalBundleEntry: %v", err) } } return r.Bytes() } func TestBundleRoundtrip(t *testing.T) { ctx := context.Background() m := newMemObjStore() s := &logResourceStore{ objStore: m, entriesPath: layout.EntriesPath, } for _, test := range []struct { name string index uint64 logSize uint64 bundleSize int }{ { name: "ok", index: 3 * layout.EntryBundleWidth, logSize: 3*layout.EntryBundleWidth + 20, bundleSize: 20, }, } { t.Run(test.name, func(t *testing.T) { wantBundle := makeBundle(t, test.index, test.bundleSize) if err := s.setEntryBundle(ctx, test.index, uint8(test.bundleSize), wantBundle); err != nil { t.Fatalf("setEntryBundle: %v", err) } expPath := layout.EntriesPath(test.index, layout.PartialTileSize(0, test.index, test.logSize)) _, ok := m.mem[expPath] if !ok { t.Fatalf("want bundle at %v but found none", expPath) } got, err := s.getEntryBundle(ctx, test.index, layout.PartialTileSize(0, test.index, test.logSize)) if err != nil { t.Fatalf("getEntryBundle: %v", err) } if !cmp.Equal(got, wantBundle) { t.Fatal("roundtrip returned different data") } }) } } func TestPublishTree(t *testing.T) { ctx := context.Background() for _, test := range []struct { name string publishInterval time.Duration republishInterval time.Duration attempts []time.Duration wantUpdates int }{ { name: "works ok", publishInterval: 100 * time.Millisecond, republishInterval: 100 * time.Millisecond, attempts: []time.Duration{1 * time.Second}, wantUpdates: 1, }, { name: "too soon, skip update", publishInterval: 10 * time.Second, republishInterval: 10 * time.Second, attempts: []time.Duration{100 * time.Millisecond}, wantUpdates: 0, }, { name: "too soon, skip update, but recovers", publishInterval: 2 * time.Second, republishInterval: 2 * time.Second, attempts: []time.Duration{100 * time.Millisecond, 2 * time.Second}, wantUpdates: 1, }, { name: "many attempts, eventually one succeeds", publishInterval: 1 * time.Second, republishInterval: 1 * time.Second, attempts: []time.Duration{300 * time.Millisecond, 300 * time.Millisecond, 300 * time.Millisecond, 300 * time.Millisecond}, wantUpdates: 1, }, { name: "republish needed", publishInterval: 1 * time.Second, republishInterval: 2 * time.Second, attempts: []time.Duration{1500 * time.Millisecond, 2500 * time.Millisecond}, wantUpdates: 1, }, } { t.Run(test.name, func(t *testing.T) { db, closeDB := newSpannerDB(t) defer closeDB() s, err := newSpannerCoordinator(ctx, db, 1000) if err != nil { t.Fatalf("newSpannerCoordinator: %v", err) } defer s.dbPool.Close() m := newMemObjStore() storage := &Appender{ logStore: &logResourceStore{ objStore: m, entriesPath: layout.EntriesPath, }, sequencer: s, newCP: func(_ context.Context, size uint64, hash []byte) ([]byte, error) { return fmt.Appendf(nil, "%d/%x,", size, hash), nil }, } // Call init so we've got a zero-sized checkpoint to work with. if err := storage.init(ctx); err != nil { t.Fatalf("storage.init: %v", err) } if err := s.publishCheckpoint(ctx, test.publishInterval, test.republishInterval, storage.publishCheckpoint); err != nil { t.Fatalf("publishTree: %v", err) } cpOld := []byte("bananas") if err := m.setObject(ctx, layout.CheckpointPath, cpOld, nil, "", ""); err != nil { t.Fatalf("setObject(bananas): %v", err) } updatesSeen := 0 for _, d := range test.attempts { time.Sleep(d) if err := s.publishCheckpoint(ctx, test.publishInterval, test.republishInterval, storage.publishCheckpoint); err != nil { t.Fatalf("publishTree: %v", err) } cpNew, _, err := m.getObject(ctx, layout.CheckpointPath) if err != nil { t.Fatalf("getObject: %v", err) } if !bytes.Equal(cpOld, cpNew) { updatesSeen++ cpOld = cpNew } } if updatesSeen != test.wantUpdates { t.Fatalf("Saw %d updates, want %d", updatesSeen, test.wantUpdates) } }) } } func TestGarbageCollect(t *testing.T) { ctx := t.Context() batchSize := uint64(60000) integrateEvery := uint64(31234) db, closeDB := newSpannerDB(t) defer closeDB() s, err := newSpannerCoordinator(ctx, db, batchSize) if err != nil { t.Fatalf("newSpannerCoordinator: %v", err) } defer s.dbPool.Close() sk, vk := mustGenerateKeys(t) m := newMemObjStore() storage := &Storage{} opts := tessera.NewAppendOptions(). WithCheckpointInterval(1200*time.Millisecond). WithBatching(uint(batchSize), 100*time.Millisecond). // Disable GC so we can manually invoke below. WithGarbageCollectionInterval(time.Duration(0)). WithCheckpointSigner(sk) appender, lr, err := storage.newAppender(ctx, m, s, opts) if err != nil { t.Fatalf("newAppender: %v", err) } if err := appender.publishCheckpoint(ctx, 0, []byte("")); err != nil { t.Fatalf("publishCheckpoint: %v", err) } // Build a reasonably-sized tree with a bunch of partial resouces present, and wait for // it to be published. treeSize := uint64(256 * 384) a := tessera.NewPublicationAwaiter(ctx, lr.ReadCheckpoint, 100*time.Millisecond) // grow and garbage collect the tree several times to check continued correct operation over lifetime of the log for size := uint64(0); size < treeSize; { t.Logf("Adding entries from %d", size) for range batchSize { f := appender.Add(ctx, tessera.NewEntry(fmt.Appendf(nil, "entry %d", size))) if size%integrateEvery == 0 { t.Logf("Awaiting entry %d", size) if _, _, err := a.Await(ctx, f); err != nil { t.Fatalf("Await: %v", err) } } size++ } t.Logf("Awaiting tree at size %d", size) if _, _, err := a.Await(ctx, func() (tessera.Index, error) { return tessera.Index{Index: size - 1}, nil }); err != nil { t.Fatalf("Await final tree: %v", err) } t.Logf("Running GC at size %d", size) if err := s.garbageCollect(ctx, size, 1000, m.deleteObjectsWithPrefix, appender.logStore.entriesPath); err != nil { t.Fatalf("garbageCollect: %v", err) } // Compare any remaining partial resources to the list of places // we'd expect them to be, given the tree size. wantPartialPrefixes := make(map[string]struct{}) for _, p := range expectedPartialPrefixes(size, appender.logStore.entriesPath) { wantPartialPrefixes[p] = struct{}{} } for k := range m.mem { if strings.Contains(k, ".p/") { p := strings.SplitAfter(k, ".p/")[0] if _, ok := wantPartialPrefixes[p]; !ok { t.Errorf("Found unwanted partial: %s", k) } } } } // And finally, for good measure, assert that all the resources implied by the log's checkpoint // are present. f := fsck.New(vk.Name(), vk, lr, defaultMerkleLeafHasher, fsck.Opts{N: 1}) if err := f.Check(ctx); err != nil { t.Fatalf("FSCK failed: %v", err) } } func TestGarbageCollectOption(t *testing.T) { batchSize := uint64(60000) integrateEvery := uint64(31234) garbageCollectionInterval := 100 * time.Millisecond for _, test := range []struct { name string withCTLayout bool withGarbageCollectionInterval time.Duration }{ { name: "on", withGarbageCollectionInterval: garbageCollectionInterval, withCTLayout: false, }, { name: "on-ct", withGarbageCollectionInterval: garbageCollectionInterval, withCTLayout: true, }, { name: "off", withGarbageCollectionInterval: time.Duration(0), withCTLayout: false, }, } { t.Run(test.name, func(t *testing.T) { ctx := t.Context() db, closeDB := newSpannerDB(t) defer closeDB() s, err := newSpannerCoordinator(ctx, db, batchSize) if err != nil { t.Fatalf("newSpannerCoordinator: %v", err) } defer s.dbPool.Close() sk, vk := mustGenerateKeys(t) m := newMemObjStore() storage := &Storage{} opts := tessera.NewAppendOptions(). WithCheckpointInterval(1200*time.Millisecond). WithBatching(uint(batchSize), 100*time.Millisecond). // Disable GC so we can manually invoke below. WithGarbageCollectionInterval(test.withGarbageCollectionInterval). WithCheckpointSigner(sk) if test.withCTLayout { opts.WithCTLayout() } appender, lr, err := storage.newAppender(ctx, m, s, opts) if err != nil { t.Fatalf("newAppender: %v", err) } if err := appender.publishCheckpoint(ctx, 0, []byte("")); err != nil { t.Fatalf("publishCheckpoint: %v", err) } // Build a reasonably-sized tree with a bunch of partial resouces present, and wait for // it to be published. treeSize := uint64(256 * 384) a := tessera.NewPublicationAwaiter(ctx, lr.ReadCheckpoint, 100*time.Millisecond) wantPartialPrefixes := make(map[string]struct{}) // Grow the tree several times to check continued correct operation over lifetime of the log. // Let garbage collection happen in the background. for size := uint64(0); size < treeSize; { t.Logf("Adding entries from %d", size) for range batchSize { f := appender.Add(ctx, tessera.NewEntry(fmt.Appendf(nil, "entry %d", size))) if size%integrateEvery == 0 { t.Logf("Awaiting entry %d", size) if _, _, err := a.Await(ctx, f); err != nil { t.Fatalf("Await: %v", err) } // If garbage collection is off, we want partial tiles and bundles to stick around. if test.withGarbageCollectionInterval == time.Duration(0) { for _, p := range expectedPartialPrefixes(size, appender.logStore.entriesPath) { wantPartialPrefixes[p] = struct{}{} } } } size++ } t.Logf("Awaiting tree at size %d", size) if _, _, err := a.Await(ctx, func() (tessera.Index, error) { return tessera.Index{Index: size - 1}, nil }); err != nil { t.Fatalf("Await final tree: %v", err) } // Leave a bit of time for Garbage Collection to run. time.Sleep(3 * garbageCollectionInterval) // Compare any remaining partial resources to the list of places // we'd expect them to be, given the tree size. // Regardless of whether garbage collection is on, partial tiles corresponding to the last // checkpoint should alway be here. for _, p := range expectedPartialPrefixes(size, appender.logStore.entriesPath) { wantPartialPrefixes[p] = struct{}{} } allPartialDirs := make(map[string]struct{}) for k := range m.mem { if strings.Contains(k, ".p/") { allPartialDirs[strings.SplitAfter(k, ".p/")[0]] = struct{}{} } } // If gargabe collection is on, no partial tiles other than the ones we expect should be // present. for p := range allPartialDirs { if _, ok := wantPartialPrefixes[p]; !ok && test.withGarbageCollectionInterval > 0 { t.Errorf("Found unwanted partial: %s", p) } delete(wantPartialPrefixes, p) } for p := range wantPartialPrefixes { t.Errorf("Did not find expected partial: %s", p) } } // And finally, for good measure, assert that all the resources implied by the log's checkpoint // are present. f := fsck.New(vk.Name(), vk, lr, defaultMerkleLeafHasher, fsck.Opts{N: 1}) if err := f.Check(ctx); err != nil { t.Fatalf("FSCK failed: %v", err) } }) } } // expectedPartialPrefixes returns a slice containing resource prefixes where it's acceptable for a // tree of the provided size to have partial resources. // // These are really just the right-hand tiles/entry bundle in the tree. func expectedPartialPrefixes(size uint64, entriesPath func(uint64, uint8) string) []string { r := []string{} for l, c := uint64(0), size; c > 0; l, c = l+1, c>>8 { idx, p := c/256, c%256 if p != 0 { if l == 0 { r = append(r, entriesPath(idx, 0)+".p/") } r = append(r, layout.TilePath(l, idx, 0)+".p/") } } return r } type memObjStore struct { sync.RWMutex mem map[string][]byte } func newMemObjStore() *memObjStore { return &memObjStore{ mem: make(map[string][]byte), } } func (m *memObjStore) getObject(_ context.Context, obj string) ([]byte, int64, error) { m.RLock() defer m.RUnlock() d, ok := m.mem[obj] if !ok { return nil, -1, fmt.Errorf("obj %q not found: %w", obj, gcs.ErrObjectNotExist) } return d, 1, nil } // TODO(phboneff): add content type tests func (m *memObjStore) setObject(_ context.Context, obj string, data []byte, cond *gcs.Conditions, _, _ string) error { m.Lock() defer m.Unlock() d, ok := m.mem[obj] if cond != nil { if ok && cond.DoesNotExist { if !bytes.Equal(d, data) { return errors.New("precondition failed and data not identical") } return nil } } m.mem[obj] = data return nil } func (m *memObjStore) deleteObjectsWithPrefix(_ context.Context, prefix string) error { m.Lock() defer m.Unlock() for k := range m.mem { if strings.HasPrefix(k, prefix) { log.Printf("DELETE: %s", k) delete(m.mem, k) } } return nil } func mustGenerateKeys(t *testing.T) (note.Signer, note.Verifier) { sk, vk, err := note.GenerateKey(nil, "testlog") if err != nil { t.Fatalf("GenerateKey: %v", err) } s, err := note.NewSigner(sk) if err != nil { t.Fatalf("NewSigner: %v", err) } v, err := note.NewVerifier(vk) if err != nil { t.Fatalf("NewVerifier: %v", err) } return s, v } // defaultMerkleLeafHasher parses a C2SP tlog-tile bundle and returns the Merkle leaf hashes of each entry it contains. func defaultMerkleLeafHasher(bundle []byte) ([][]byte, error) { eb := &api.EntryBundle{} if err := eb.UnmarshalText(bundle); err != nil { return nil, fmt.Errorf("unmarshal: %v", err) } r := make([][]byte, 0, len(eb.Entries)) for _, e := range eb.Entries { h := rfc6962.DefaultHasher.HashLeaf(e) r = append(r, h[:]) } return r, nil } transparency-dev-tessera-3cb22ee/storage/gcp/otel.go000066400000000000000000000020031511600621500226310ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package gcp import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) const name = "github.com/transparency-dev/tessera/storage/gcp" var ( tracer = otel.Tracer(name) ) var ( treeSizeKey = attribute.Key("tessera.treeSize") fromSizeKey = attribute.Key("tessera.fromSize") numEntriesKey = attribute.Key("tessera.numEntries") objectPathKey = attribute.Key("tessera.objectPath") ) transparency-dev-tessera-3cb22ee/storage/internal/000077500000000000000000000000001511600621500224075ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/storage/internal/integrate.go000066400000000000000000000330521511600621500247230ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package storage import ( "context" "errors" "fmt" "reflect" "github.com/transparency-dev/merkle/compact" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/internal/otel" "golang.org/x/exp/maps" "k8s.io/klog/v2" ) // SequencedEntry represents a log entry which has already been sequenced. type SequencedEntry struct { // BundleData is the entry's data serialised into the correct format for appending to an entry bundle. BundleData []byte // LeafHash is the entry's Merkle leaf hash. LeafHash []byte } func Integrate(ctx context.Context, getTiles func(ctx context.Context, tileIDs []TileID, treeSize uint64) ([]*api.HashTile, error), fromSize uint64, leafHashes [][]byte) (newSize uint64, rootHash []byte, tiles map[TileID]*api.HashTile, err error) { tb := newTreeBuilder(getTiles) return tb.integrate(ctx, fromSize, leafHashes) } // getPopulatedTileFunc is the signature of a function which can return a fully populated tile for the given tile coords. type getPopulatedTileFunc func(ctx context.Context, tileID TileID, treeSize uint64) (*populatedTile, error) // treeBuilder constructs Merkle trees. // // This struct it indended to be used by storage implementations during the integration of entries into the log. // treeBuilder caches data from tiles to speed things up, but has no mechanism for evicting from its internal cache, // so while it _may_ be possible to use the same instance across a number of integration runs (e.g. if the same job // is responsible for integrating entries for a number of contiguous trees), the lifetime should be bounded so as not // to leak memory. type treeBuilder struct { readCache *tileReadCache rf *compact.RangeFactory } // newTreeBuilder creates a new instance of treeBuilder. // // The getTiles param must know how to fetch the specified tiles from storage. It must return tiles in the same order as the // provided tileIDs, substituing nil for any tiles which were not found. func newTreeBuilder(getTiles func(ctx context.Context, tileIDs []TileID, treeSize uint64) ([]*api.HashTile, error)) *treeBuilder { readCache := newTileReadCache(getTiles) r := &treeBuilder{ readCache: &readCache, rf: &compact.RangeFactory{Hash: rfc6962.DefaultHasher.HashChildren}, } return r } // newRange creates a new compact.Range for the specified treeSize, fetching tiles as necessary. func (t *treeBuilder) newRange(ctx context.Context, treeSize uint64) (*compact.Range, error) { rangeNodes := compact.RangeNodes(0, treeSize, nil) toFetch := make(map[TileID]struct{}) for _, id := range rangeNodes { tLevel, tIndex, _, _ := layout.NodeCoordsToTileAddress(uint64(id.Level), id.Index) toFetch[TileID{Level: tLevel, Index: tIndex}] = struct{}{} } if err := t.readCache.Prewarm(ctx, maps.Keys(toFetch), treeSize); err != nil { return nil, fmt.Errorf("Prewarm: %v", err) } hashes := make([][]byte, 0, len(rangeNodes)) for _, id := range rangeNodes { tLevel, tIndex, nLevel, nIndex := layout.NodeCoordsToTileAddress(uint64(id.Level), id.Index) ft, err := t.readCache.Get(ctx, TileID{Level: tLevel, Index: tIndex}, treeSize) if err != nil { return nil, err } h := ft.Get(compact.NodeID{Level: nLevel, Index: nIndex}) if h == nil { return nil, fmt.Errorf("missing node: [%d/%d@%d]", id.Level, id.Index, treeSize) } hashes = append(hashes, h) } return t.rf.NewRange(0, treeSize, hashes) } func (t *treeBuilder) integrate(ctx context.Context, fromSize uint64, leafHashes [][]byte) (newSize uint64, rootHash []byte, tiles map[TileID]*api.HashTile, err error) { ctx, span := tracer.Start(ctx, "tessera.storage.integrate") defer span.End() span.SetAttributes(fromSizeKey.Int64(otel.Clamp64(fromSize)), numEntriesKey.Int(len(leafHashes))) baseRange, err := t.newRange(ctx, fromSize) if err != nil { return 0, nil, nil, fmt.Errorf("failed to create range covering existing log: %w", err) } // Initialise a compact range representation, and verify the stored state. r, err := baseRange.GetRootHash(nil) if err != nil { return 0, nil, nil, fmt.Errorf("invalid log state, unable to recalculate root: %w", err) } if len(leafHashes) == 0 { klog.V(1).Infof("Nothing to do.") // C2SP.org/log-tiles says all Merkle operations are those from RFC6962, we need to override // the root of the empty tree to match (compact.Range will return an empty slice). if fromSize == 0 { r = rfc6962.DefaultHasher.EmptyRoot() } // Nothing to do, nothing done. return fromSize, r, nil, nil } span.AddEvent("Loaded state") klog.V(1).Infof("Loaded state with roothash %x", r) // Create a new compact range which represents the update to the tree newRange := t.rf.NewEmptyRange(fromSize) tc := newTileWriteCache(fromSize, t.readCache.Get) visitor := tc.Visitor(ctx) for _, e := range leafHashes { // Update range and set nodes if err := newRange.Append(e, visitor); err != nil { return 0, nil, nil, fmt.Errorf("newRange.Append(): %v", err) } } // Check whether the visitor had any problems building the update range if err := tc.Err(); err != nil { return 0, nil, nil, err } span.AddEvent("Updated tile cache") // Merge the update range into the old tree if err := baseRange.AppendRange(newRange, visitor); err != nil { return 0, nil, nil, fmt.Errorf("failed to merge new range onto existing log: %w", err) } // Check whether the visitor had any problems when merging the new range into the tree if err := tc.Err(); err != nil { return 0, nil, nil, err } // Calculate the new root hash - don't pass in the tileCache visitor here since // this will construct any ephemeral nodes and we do not want to store those. newRoot, err := baseRange.GetRootHash(nil) if err != nil { return 0, nil, nil, fmt.Errorf("failed to calculate new root hash: %w", err) } span.AddEvent("Calculated new root") // All calculation is now complete, all that remains is to store the new // tiles and updated log state. klog.V(1).Infof("New log state: size 0x%x hash: %x", baseRange.End(), newRoot) return baseRange.End(), newRoot, tc.Tiles(), nil } // tileReadCache is a structure which provides a very simple thread-safe read-through cache based on a map of tiles. type tileReadCache struct { entries map[string]*populatedTile getTiles func(ctx context.Context, tileIDs []TileID, treeSize uint64) ([]*api.HashTile, error) } func newTileReadCache(getTiles func(ctx context.Context, tileIDs []TileID, treeSize uint64) ([]*api.HashTile, error)) tileReadCache { return tileReadCache{ entries: make(map[string]*populatedTile), getTiles: getTiles, } } // Get returns a previously set tile and true, or, if no such tile is in the cache, attempt to fetch it. func (r *tileReadCache) Get(ctx context.Context, tileID TileID, treeSize uint64) (*populatedTile, error) { ctx, span := tracer.Start(ctx, "tessera.storage.readCache.Get") defer span.End() span.SetAttributes(indexKey.Int64(otel.Clamp64(tileID.Index)), levelKey.Int64(otel.Clamp64(tileID.Level)), treeSizeKey.Int64(otel.Clamp64(treeSize))) k := layout.TilePath(uint64(tileID.Level), tileID.Index, layout.PartialTileSize(tileID.Level, tileID.Index, treeSize)) e, ok := r.entries[k] if !ok { klog.V(1).Infof("Readcache miss: %q", k) span.AddEvent(fmt.Sprintf("Cache miss %q", k)) t, err := r.getTiles(ctx, []TileID{tileID}, treeSize) if err != nil { return nil, err } e, err = newPopulatedTile(t[0]) if err != nil { return nil, fmt.Errorf("failed to create fulltile: %v", err) } r.entries[k] = e } return e, nil } // Preward fills the cache by fetching the given tilesIDs. // // Returns an error if any of the tiles couldn't be fetched. func (r *tileReadCache) Prewarm(ctx context.Context, tileIDs []TileID, treeSize uint64) error { ctx, span := tracer.Start(ctx, "tessera.storage.readCache.Prewarm") defer span.End() t, err := r.getTiles(ctx, tileIDs, treeSize) if err != nil { return err } for i, tile := range t { e, err := newPopulatedTile(tile) if err != nil { return fmt.Errorf("failed to create fulltile: %v", err) } k := layout.TilePath(uint64(tileIDs[i].Level), tileIDs[i].Index, layout.PartialTileSize(tileIDs[i].Level, tileIDs[i].Index, treeSize)) r.entries[k] = e } return nil } // tileWriteCache is a simple cache for storing the newly created tiles produced by // the integration of new leaves into the tree. // // Calls to Visit will cause the map of tiles to become filled with the set of // `dirty` tiles which need to be flushed back to storage to preserve the updated // tree state. // // Note that by itself, this cache does not update any persisted state. type tileWriteCache struct { m map[TileID]*populatedTile err []error treeSize uint64 getTile getPopulatedTileFunc } // newtileWriteCache creates a new cache for the given treeSize, and uses the provided // function to fetch existing tiles which are being updated by the Visitor func. func newTileWriteCache(treeSize uint64, getTile getPopulatedTileFunc) *tileWriteCache { return &tileWriteCache{ m: make(map[TileID]*populatedTile), treeSize: treeSize, getTile: getTile, } } // Err returns an aggregated view of any errors seen by the visitor function. // // This can be used to check whether updates to the tile cache made by the visitor // were made correctly. Any errors returned here are most likely to be due to // the cache attempting to read an existing tile which is being updated. func (tc *tileWriteCache) Err() error { return errors.Join(tc.err...) } // minImpliedTreeSize returns the smallest possible tree size implied by the existence of a tile // with the given ID. func minImpliedTreeSize(id TileID) uint64 { return (id.Index * layout.TileWidth) << (id.Level * 8) } // Visitor returns a function suitable for use with the compact.Range visitor pattern. // // The returned function is expected to be called sequentially to set one or nodes // to their corresponding hash values. func (tc *tileWriteCache) Visitor(ctx context.Context) compact.VisitFn { return func(id compact.NodeID, hash []byte) { tileLevel, tileIndex, nodeLevel, nodeIndex := layout.NodeCoordsToTileAddress(uint64(id.Level), uint64(id.Index)) tileID := TileID{Level: tileLevel, Index: tileIndex} tile := tc.m[tileID] if tile == nil { var err error // If this tile implies a larger tree size than we started integrating at, we don't // need to try to fetch the tile since it probably doesn't exist. // If it _does_ exist, e.g. due to an earlier crash during integration, we'll discover // any non-idempotency issues when we come to flush these new tiles out. if iSize := minImpliedTreeSize(tileID); iSize <= tc.treeSize { tile, err = tc.getTile(ctx, tileID, tc.treeSize) if err != nil { tc.err = append(tc.err, err) return } } if tile == nil { // No tile found in storage: this is a brand new tile being created due to tree growth. tile, err = newPopulatedTile(nil) if err != nil { tc.err = append(tc.err, err) return } } } tc.m[tileID] = tile // Update the tile with the new node hash. idx := compact.NodeID{Level: nodeLevel, Index: nodeIndex} tile.Set(idx, hash) } } // Tiles returns all visited tiles. func (tc *tileWriteCache) Tiles() map[TileID]*api.HashTile { newTiles := make(map[TileID]*api.HashTile) for k, t := range tc.m { newTiles[k] = &api.HashTile{Nodes: t.leaves} } return newTiles } // populatedTile represents a "fully populated" tile, i.e. it has all non-ephemeral internal nodes // implied by the leaves. type populatedTile struct { inner map[compact.NodeID][]byte leaves [][]byte } // newPopulatedTile creates and populates a fullTile struct based on the passed in HashTile data. func newPopulatedTile(h *api.HashTile) (*populatedTile, error) { ft := &populatedTile{ inner: make(map[compact.NodeID][]byte), leaves: make([][]byte, 0, layout.TileWidth), } if h != nil { // TODO: it might be better if we calculate (and cache) nodes in get, so we don't do more work that necessary. r := (&compact.RangeFactory{Hash: rfc6962.DefaultHasher.HashChildren}).NewEmptyRange(0) for _, h := range h.Nodes { if err := r.Append(h, ft.Set); err != nil { return nil, fmt.Errorf("failed to append to range: %v", err) } } } return ft, nil } // Set allows setting of individual leaf/inner nodes. // It's intended to be used as a visitor for compact.Range. func (f *populatedTile) Set(id compact.NodeID, hash []byte) { if id.Level == 0 { if id.Index > 255 { panic(fmt.Sprintf("Weird node ID: %v", id)) } if l, idx := uint64(len(f.leaves)), id.Index; idx >= l { f.leaves = append(f.leaves, make([][]byte, idx-l+1)...) } f.leaves[id.Index] = hash } else { f.inner[id] = hash } } // Get allows access to individual leaf/inner nodes. func (f *populatedTile) Get(id compact.NodeID) []byte { if id.Level == 0 { if l := uint64(len(f.leaves)); id.Index >= l { return nil } return f.leaves[id.Index] } return f.inner[id] } func (f *populatedTile) Equals(other *populatedTile) bool { return reflect.DeepEqual(f, other) } transparency-dev-tessera-3cb22ee/storage/internal/integrate_test.go000066400000000000000000000146601511600621500257660ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package storage import ( "context" "fmt" "reflect" "sync" "testing" "github.com/google/go-cmp/cmp" "github.com/transparency-dev/merkle/compact" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "k8s.io/klog/v2" ) func TestNewRangeFetchesTiles(t *testing.T) { ctx := context.Background() m := newMemTileStore[api.HashTile]() tb := newTreeBuilder(m.getTiles) treeSize := uint64(0x102030) wantIDs := []TileID{ {Level: 0, Index: 0x1020}, {Level: 1, Index: 0x10}, {Level: 2, Index: 0x0}, } for _, id := range wantIDs { if err := m.setTile(ctx, id, treeSize, zeroTile(layout.TileWidth)); err != nil { t.Fatalf("setTile: %v", err) } } _, err := tb.newRange(ctx, treeSize) if err != nil { t.Fatalf("newRange(%d): %v", treeSize, err) } } func TestTileVisit(t *testing.T) { ctx := context.Background() m := newMemTileStore[populatedTile]() treeSize := uint64(0x102030) for _, test := range []struct { name string visits map[compact.NodeID][]byte wantTiles map[TileID]*api.HashTile }{ { name: "ok - single tile", visits: map[compact.NodeID][]byte{ {Level: 0, Index: 0}: {0}, {Level: 0, Index: 1}: {1}, {Level: 1, Index: 1}: {2}, }, wantTiles: map[TileID]*api.HashTile{ {Level: 0, Index: 0}: {Nodes: [][]byte{{0}, {1}}}, }, }, { name: "ok - multiple tiles", visits: map[compact.NodeID][]byte{ {Level: 0, Index: 0}: {0}, {Level: 0, Index: 1 * layout.TileWidth}: {1}, {Level: 8, Index: 2 * layout.TileWidth}: {2}, }, wantTiles: map[TileID]*api.HashTile{ {Level: 0, Index: 0}: {Nodes: [][]byte{{0}}}, {Level: 0, Index: 1}: {Nodes: [][]byte{{1}}}, {Level: 1, Index: 2}: {Nodes: [][]byte{{2}}}, }, }, } { twc := newTileWriteCache(treeSize, m.getTile) v := twc.Visitor(ctx) for id, k := range test.visits { v(id, k) } if err := twc.Err(); err != nil { t.Fatalf("Got err: %v", err) } gotTiles := twc.Tiles() for id, wantTile := range test.wantTiles { gotTile, ok := gotTiles[id] if !ok { t.Errorf("Missing tile %v", id) continue } if !reflect.DeepEqual(gotTile, wantTile) { t.Errorf("Got tile with unexpected data at %v:\ngot:\n%x\nwant:\n%x", id, gotTile, wantTile) } delete(gotTiles, id) delete(test.wantTiles, id) } if l := len(gotTiles); l > 0 { t.Errorf("got unexpected tiles: %v", gotTiles) } if l := len(test.wantTiles); l > 0 { t.Errorf("did not get expected tiles: %v", test.wantTiles) } } } func TestIntegrate(t *testing.T) { ctx := context.Background() m := newMemTileStore[api.HashTile]() cr := (&compact.RangeFactory{Hash: rfc6962.DefaultHasher.HashChildren}).NewEmptyRange(0) chunkSize := 200 numChunks := 1000 seq := uint64(0) for chunk := range numChunks { oldSeq := seq c := make([][]byte, chunkSize) for i := range c { leaf := []byte{byte(seq)} entry := tessera.NewEntry(leaf) c[i] = entry.LeafHash() if err := cr.Append(rfc6962.DefaultHasher.HashLeaf(leaf), nil); err != nil { t.Fatalf("compact Append: %v", err) } seq++ } wantRoot, err := cr.GetRootHash(nil) if err != nil { t.Fatalf("[%d] compactRange: %v", chunk, err) } gotSize, gotRoot, gotTiles, err := Integrate(ctx, m.getTiles, oldSeq, c) if err != nil { t.Fatalf("[%d] Integrate: %v", chunk, err) } if wantSize := seq; gotSize != wantSize { t.Errorf("[%d] Got size %d, want %d", chunk, gotSize, wantSize) } if !cmp.Equal(gotRoot, wantRoot) { t.Errorf("[%d] Got root %x, want %x", chunk, gotRoot, wantRoot) } for k, tile := range gotTiles { if err := m.setTile(ctx, k, seq, tile); err != nil { t.Fatalf("setTile: %v", err) } } } } func BenchmarkIntegrate(b *testing.B) { ctx := context.Background() m := newMemTileStore[api.HashTile]() chunkSize := 200 seq := uint64(0) for chunk := 0; b.Loop(); chunk++ { oldSeq := seq c := make([][]byte, chunkSize) for i := range c { leaf := []byte{byte(seq)} entry := tessera.NewEntry(leaf) c[i] = entry.LeafHash() seq++ } _, _, gotTiles, err := Integrate(ctx, m.getTiles, oldSeq, c) if err != nil { b.Fatalf("[%d] Integrate: %v", chunk, err) } for k, tile := range gotTiles { if err := m.setTile(ctx, k, seq, tile); err != nil { b.Fatalf("setTile: %v", err) } } } } // zerotile creates a new api.HashTile of the provided size, whose leaves are all a single zero byte. func zeroTile(size uint64) *api.HashTile { r := &api.HashTile{ Nodes: make([][]byte, size), } for i := range r.Nodes { r.Nodes[i] = []byte{0} } return r } type memTileStore[T any] struct { sync.RWMutex mem map[string]*T } func newMemTileStore[T any]() *memTileStore[T] { return &memTileStore[T]{ mem: make(map[string]*T), } } func (m *memTileStore[T]) getTile(_ context.Context, id TileID, treeSize uint64) (*T, error) { m.RLock() defer m.RUnlock() k := layout.TilePath(id.Level, id.Index, layout.PartialTileSize(id.Level, id.Index, treeSize)) d := m.mem[k] return d, nil } func (m *memTileStore[T]) getTiles(_ context.Context, ids []TileID, treeSize uint64) ([]*T, error) { m.RLock() defer m.RUnlock() r := make([]*T, len(ids)) for i, id := range ids { k := layout.TilePath(id.Level, id.Index, layout.PartialTileSize(id.Level, id.Index, treeSize)) klog.V(1).Infof("mem.getTile(%q, %d)", k, treeSize) d, ok := m.mem[k] if !ok { continue } r[i] = d } return r, nil } func (m *memTileStore[T]) setTile(_ context.Context, id TileID, treeSize uint64, t *T) error { m.Lock() defer m.Unlock() k := layout.TilePath(id.Level, id.Index, layout.PartialTileSize(id.Level, id.Index, treeSize)) klog.V(1).Infof("mem.setTile(%q, %d)", k, treeSize) _, ok := m.mem[k] if ok { return fmt.Errorf("%q is already present", k) } d := *t m.mem[k] = &d return nil } transparency-dev-tessera-3cb22ee/storage/internal/otel.go000066400000000000000000000020511511600621500236770ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package storage import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) const name = "github.com/transparency-dev/tessera/storage" var ( tracer = otel.Tracer(name) ) var ( fromSizeKey = attribute.Key("tessera.fromSize") numEntriesKey = attribute.Key("tessera.numEntries") treeSizeKey = attribute.Key("tessera.treeSize") indexKey = attribute.Key("tessera.index") levelKey = attribute.Key("tessera.level") ) transparency-dev-tessera-3cb22ee/storage/internal/queue.go000066400000000000000000000123041511600621500240620ustar00rootroot00000000000000// Copyright 2024 Google LLC. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package storage provides implementations and shared components for tessera storage backends. package storage import ( "context" "errors" "sync" "time" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/internal/future" ) // Queue knows how to queue up a number of entries in order. // // When the buffered queue grows past a defined size, or the age of the oldest entry in the // queue reaches a defined threshold, the queue will call a provided FlushFunc with // a slice containing all queued entries in the same order as they were added. type Queue struct { maxSize uint maxAge time.Duration timer *time.Timer work chan []queueItem mu sync.Mutex items []queueItem } // FlushFunc is the signature of a function which will receive the slice of queued entries. // Normally, this function would be provided by storage implementations. It's important to note // that the implementation MUST call each entry's MarshalBundleData function before attempting // to integrate it into the tree. // See the comment on Entry.MarshalBundleData for further info. type FlushFunc func(ctx context.Context, entries []*tessera.Entry) error // NewQueue creates a new queue with the specified maximum age and size. // // The provided FlushFunc will be called with a slice containing the contents of the queue, in // the same order as they were added, when either the oldest entry in the queue has been there // for maxAge, or the size of the queue reaches maxSize. func NewQueue(ctx context.Context, maxAge time.Duration, maxSize uint, f FlushFunc) *Queue { q := &Queue{ maxSize: maxSize, maxAge: maxAge, work: make(chan []queueItem, 1), items: make([]queueItem, 0, maxSize), } // Spin off a worker thread to write the queue flushes to storage. go func(ctx context.Context) { for { select { case <-ctx.Done(): return case entries := <-q.work: q.doFlush(ctx, f, entries) } } }(ctx) return q } // Add places e into the queue, and returns a func which should be called to retrieve the assigned index. func (q *Queue) Add(ctx context.Context, e *tessera.Entry) tessera.IndexFuture { qi := newEntry(e) q.mu.Lock() q.items = append(q.items, qi) // If this is the first item, start the timer. if len(q.items) == 1 { q.timer = time.AfterFunc(q.maxAge, q.flush) } // If we've reached max size, flush. var itemsToFlush []queueItem if len(q.items) >= int(q.maxSize) { itemsToFlush = q.flushLocked() } q.mu.Unlock() if itemsToFlush != nil { q.work <- itemsToFlush } return qi.f } // flush is called by the timer to flush the buffer. func (q *Queue) flush() { q.mu.Lock() itemsToFlush := q.flushLocked() q.mu.Unlock() if itemsToFlush != nil { q.work <- itemsToFlush } } // flushLocked must be called with q.mu held. // It prepares items for flushing and returns them. func (q *Queue) flushLocked() []queueItem { if len(q.items) == 0 { return nil } if q.timer != nil { q.timer.Stop() q.timer = nil } itemsToFlush := q.items q.items = make([]queueItem, 0, q.maxSize) return itemsToFlush } // doFlush handles the queue flush, and sending notifications of assigned log indices. func (q *Queue) doFlush(ctx context.Context, f FlushFunc, entries []queueItem) { ctx, span := tracer.Start(ctx, "tessera.storage.queue.doFlush") defer span.End() entriesData := make([]*tessera.Entry, 0, len(entries)) for _, e := range entries { entriesData = append(entriesData, e.entry) } err := f(ctx, entriesData) // Send assigned indices to all the waiting Add() requests for _, e := range entries { e.notify(err) } } // queueItem represents an in-flight queueItem in the queue. // // The f field acts as a future for the queueItem's assigned index/error, and will // hang until assign is called. type queueItem struct { entry *tessera.Entry f tessera.IndexFuture set func(tessera.Index, error) } // newEntry creates a new entry for the provided data. func newEntry(data *tessera.Entry) queueItem { f, set := future.NewFutureErr[tessera.Index]() e := queueItem{ entry: data, f: f.Get, set: set, } return e } // notify sets the assigned log index (or an error) to the entry. // // This func must only be called once, and will cause any current or future callers of index() // to be given the values provided here. func (e *queueItem) notify(err error) { if e.entry.Index() == nil && err == nil { panic(errors.New("logic error: flush complete without error, but entry was not assigned an index - did storage fail to call entry.MarshalBundleData?")) } var idx uint64 if e.entry.Index() != nil { idx = *e.entry.Index() } e.set(tessera.Index{Index: idx}, err) } transparency-dev-tessera-3cb22ee/storage/internal/queue_test.go000066400000000000000000000107461511600621500251310ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package storage_test import ( "context" "errors" "fmt" "reflect" "sync" "testing" "time" "github.com/transparency-dev/tessera" storage "github.com/transparency-dev/tessera/storage/internal" ) func TestQueue(t *testing.T) { for _, test := range []struct { name string numItems uint64 maxEntries int maxWait time.Duration }{ { name: "small", numItems: 100, maxEntries: 200, maxWait: time.Second, }, { name: "more items than queue space", numItems: 100, maxEntries: 20, maxWait: time.Second, }, { name: "much flushing", numItems: 100, maxEntries: 100, maxWait: time.Microsecond, }, } { t.Run(test.name, func(t *testing.T) { ctx := t.Context() assignMu := sync.Mutex{} assignedItems := make([]*tessera.Entry, test.numItems) assignedIndex := uint64(0) // flushFunc mimics sequencing storage - it takes entries, assigns them to // positions in assignedItems. flushFunc := func(_ context.Context, entries []*tessera.Entry) error { assignMu.Lock() defer assignMu.Unlock() for _, e := range entries { _ = e.MarshalBundleData(assignedIndex) assignedItems[assignedIndex] = e assignedIndex++ } return nil } // Create the Queue q := storage.NewQueue(ctx, test.maxWait, uint(test.maxEntries), flushFunc) // Now submit a bunch of entries adds := make([]tessera.IndexFuture, test.numItems) wantEntries := make([]*tessera.Entry, test.numItems) for i := uint64(0); i < test.numItems; i++ { d := fmt.Appendf(nil, "item %d", i) wantEntries[i] = tessera.NewEntry(d) adds[i] = q.Add(ctx, wantEntries[i]) } for i, r := range adds { N, err := r() if err != nil { t.Errorf("Add: %v", err) return } if got, want := assignedItems[N.Index].Data(), wantEntries[i].Data(); !reflect.DeepEqual(got, want) { t.Errorf("Got item@%d %v, want %v", N.Index, got, want) } } }) } } func TestNotify(t *testing.T) { for _, test := range []struct { name string setIdx int64 setErr error wantErr bool }{ { name: "just idx, no error", setIdx: 200, setErr: nil, wantErr: false, }, { name: "just error", setIdx: -1, setErr: errors.New("expected error"), wantErr: true, }, { name: "error and idx", setIdx: 200, setErr: errors.New("expected error"), wantErr: true, }, } { t.Run(test.name, func(t *testing.T) { ctx := t.Context() // flushFunc mimics sequencing storage - it takes entries, assigns them to // positions in assignedItems. flushFunc := func(_ context.Context, entries []*tessera.Entry) error { if got := len(entries); got != 1 { t.Fatalf("expected 1 entry but got %d", got) } if test.setIdx >= 0 { _ = entries[0].MarshalBundleData(uint64(test.setIdx)) } return test.setErr } // Create the Queue q := storage.NewQueue(ctx, time.Second, uint(1), flushFunc) // Now submit the entry added := q.Add(ctx, tessera.NewEntry([]byte(test.name))) _, err := added() if gotErr, wantErr := err != nil, test.wantErr; gotErr != wantErr { t.Errorf("gotErr != wantErr (%t != %t): %v", gotErr, wantErr, err) } }) } } func BenchmarkQueue(b *testing.B) { ctx := b.Context() const count = 1024 // Outer loop is for benchmark calibration, inside here is each individual run of the benchmark for b.Loop() { flushFn := func(_ context.Context, entries []*tessera.Entry) error { for _, e := range entries { _ = e.MarshalBundleData(0) } return nil } q := storage.NewQueue(ctx, time.Second, 256, flushFn) adds := make([]tessera.IndexFuture, 0, count) for leafIndex := range count { f := q.Add(ctx, tessera.NewEntry([]byte{byte(leafIndex)})) adds = append(adds, f) } for _, r := range adds { _, err := r() if err != nil { b.Errorf("Add: %v", err) return } } } } transparency-dev-tessera-3cb22ee/storage/internal/tileid.go000066400000000000000000000013421511600621500242100ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package storage // TileID represents a tile address in tile-space. type TileID struct { Level uint64 Index uint64 } transparency-dev-tessera-3cb22ee/storage/mysql/000077500000000000000000000000001511600621500217405ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/storage/mysql/DESIGN.md000066400000000000000000000070321511600621500232350ustar00rootroot00000000000000# MySQL Design Author: [Martin Hutchinson](https://github.com/mhutchinson) This document describes how the storage implementation for running Tessera on MySQL is intended to work. ## Intended Audience - Cloud neutral deployments - On-premise deployments ## Architecture The DB layout has been designed such that serving any read request is a point lookup. This can be framed as each row in the database maps to a single file that would be created in the file-based implementations of Tessera. ### Table Schema #### `Tessera` A single row that records the current version of the Tessera schema and data compatibility. #### `Checkpoint` A single row that records the current published checkpoint. #### `TreeState` A single row that records the current state of the tree. Updated after every integration. #### `Subtree` An internal tile consisting of hashes. There is one row for each internal tile, and this is updated until it is completed, at which point it is immutable. #### `TiledLeaves` The data committed to by the leaves of the tree. Follows the same evolution as Subtree. Reads can scale horizontally with very little overhead or contention between frontends. Writing is more complicated than reading for the following reasons: 1. Each write request updates multiple rows across all tables. 1. Write requests need to be globally coordinated to ensure every sequence number is allocated precisely once. There can be an arbitrary number of frontends each receiving write traffic. Each of these has the following processes: API Handlers: 1. Handle the /add request, and extract the data to be added 1. Add the data to a pool to be sequenced and block until this returns the index Sequence pool: 1. Accept requests from API handlers to add a new entry to the pool. 1. If the pool is now of the maximum size, flush the current batch. 1. If this is the first entry in the batch, then start a timer and flush the batch after a timeout. 1. Flushing: starts a sequence & integrate operation. Sequence & integrate (DB integration starts here): 1. Takes a batch of entries to sequence and integrate 1. Starts a transaction, which first takes a write lock on the `TreeState` row to ensure that: 1. No other processes will be competing with this work. 1. That the next index to sequence is known (this is the same as the current tree size) 1. Update the required TiledLeaves rows 1. Perform an integration operation to update the Merkle tree, updating/adding Subtree rows as needed, and eventually updating the `TreeState` row 1. Commit the transaction 1. Checkpoints representing the latest state of the tree are published at the configured interval. ## Costs Either all the money, or free. This could run as lightly as fitting inside a free-tier GCE VM, or scale up to a Cloud SQL instance that costs a hefty sum each month. These prices could be estimated based on QPS. It is a lot harder to estimate the price when physical machines are owned in an on-prem deployment. ## Alternative Considered For this implementation, MySQL 8 was picked as the RDBMS. Other options considered were PostgreSQL and CockroachDB. The choice was somewhat arbitrary, and any of these solutions could be reasonably justified. My justification for picking MySQL is that it is more ubiquitous than Cockroach, and [writes scale better than PostgreSQL](https://www.uber.com/en-GB/blog/postgres-to-mysql-migration/). The implementation uses pretty standard SQL, so it isn’t envisaged that switching implementation would be insurmountable. That said, testing has only been performed with MySQL 8. transparency-dev-tessera-3cb22ee/storage/mysql/PERFORMANCE.md000066400000000000000000000234471511600621500240350ustar00rootroot00000000000000# MySQL Performance > [!TIP] > The performance test result shows that Tessera can run on the free tier VM instance on GCP. ### GCP Free Tier VM Instance **tl;dr:** Tessera (MySQL) can run on the free tier VM instance on GCP with around **300 write QPS**. The bottleneck comes from the lack of memory which is consumed by the dockerized MySQL instance. **e2-micro** - vCPU: 0.25-2 vCPU (1 shared core) - Memory: 1 GB - OS: Debian GNU/Linux 12 (bookworm) > [!NOTE] > Virtual CPUs (vCPUs) in virtualized environments often share physical CPU cores with other vCPUs and introduce variability and potential performance impacts. #### Result ``` ┌──────────────────────────────────────────────────────────────────────┐ │Read (8 workers): Current max: 0/s. Oversupply in last second: 0 │ │Write (256 workers): Current max: 409/s. Oversupply in last second: 0 │ │TreeSize: 240921 (Δ 307qps over 30s) │ │Time-in-queue: 86ms/566ms/2172ms (min/avg/max) │ │Observed-time-to-integrate: 516ms/1056ms/2531ms (min/avg/max) │ └──────────────────────────────────────────────────────────────────────┘ ``` The bottleneck is at the dockerized MySQL instance, which consumes around 50% of the memory. ``` top - 20:07:16 up 9 min, 3 users, load average: 0.55, 0.56, 0.29 Tasks: 103 total, 1 running, 102 sleeping, 0 stopped, 0 zombie %Cpu(s): 3.5 us, 1.7 sy, 0.0 ni, 89.9 id, 2.9 wa, 0.0 hi, 2.0 si, 0.0 st MiB Mem : 970.0 total, 74.5 free, 932.7 used, 65.2 buff/cache MiB Swap: 0.0 total, 0.0 free, 0.0 used. 37.3 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 1770 root 20 0 1231828 22808 0 S 8.6 2.3 0:18.35 conformance-mys 1140 999 20 0 1842244 493652 0 S 4.0 49.7 0:13.93 mysqld ``` #### Steps 1. Create a [GCP free tier](https://cloud.google.com/free/docs/free-cloud-features#free-tier) e2-micro VM instance in us-central1 (iowa). 1. [Install Go](https://go.dev/doc/install) ```sh instance:~$ wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz instance:~$ sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz instance:~$ export PATH=$PATH:/usr/local/go/bin instance:~$ go version go version go1.23.0 linux/amd64 ``` 1. [Install Docker using the `apt` repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) 1. Install Git ```sh instance:~$ sudo apt-get install git -y -q ... instance:~$ git version git version 2.39.2 ``` 1. Clone the Tessera repository ```sh instance:~$ git clone https://github.com/transparency-dev/tessera.git Cloning into 'tessera'... ``` 1. Run `cmd/conformance/mysql` and MySQL database via Docker compose ```sh instance:~/tessera$ sudo docker compose -f ./cmd/conformance/mysql/docker/compose.yaml up ``` 1. Run `hammer` and get performance metrics ```sh hammer:~/tessera$ go run ./internal/hammer --log_public_key=transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW --log_url=http://10.128.0.3:2024 --max_read_ops=0 --num_writers=512 --max_write_ops=512 ``` ### GCP Free Tier VM Instance + Cloud SQL (MySQL) **tl;dr:** Tessera (MySQL) can run on the free tier VM instance on GCP with around **2000 write QPS** when the MySQL database is run on Cloud SQL. **e2-micro** - vCPU: 0.25-2 vCPU (1 shared core) - Memory: 1 GB - OS: Debian GNU/Linux 12 (bookworm) > [!NOTE] > Virtual CPUs (vCPUs) in virtualized environments often share physical CPU cores with other vCPUs and introduce variability and potential performance impacts. **Cloud SQL (MySQL 8.0.31)** - vCPUs: 4 - Memory: 7.5 GB - SSD storage: 10 GB #### Result ``` ┌───────────────────────────────────────────────────────────────────────┐ │Read (8 workers): Current max: 0/s. Oversupply in last second: 0 │ │Write (512 workers): Current max: 2571/s. Oversupply in last second: 0 │ │TreeSize: 2530480 (Δ 2047qps over 30s) │ │Time-in-queue: 41ms/120ms/288ms (min/avg/max) │ │Observed-time-to-integrate: 568ms/636ms/782ms (min/avg/max) │ └───────────────────────────────────────────────────────────────────────┘ ``` The bottleneck comes from CPU usage of the `cmd/conformance/mysql` binary on the free tier VM instance. The Cloud SQL (MySQL) CPU usage is lower than 10%. #### Steps 1. Create a MySQL instance on Cloud SQL. 1. Create a [GCP free tier](https://cloud.google.com/free/docs/free-cloud-features#free-tier) e2-micro VM instance in us-central1 (iowa). 1. Setup VPC peering. 1. [Install Go](https://go.dev/doc/install) ```sh instance:~$ wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz instance:~$ sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz instance:~$ export PATH=$PATH:/usr/local/go/bin instance:~$ go version go version go1.23.0 linux/amd64 ``` 1. Install Git ```sh instance:~$ sudo apt-get install git -y -q ... instance:~$ git version git version 2.39.2 ``` 1. Clone the Tessera repository ```sh instance:~$ git clone https://github.com/transparency-dev/tessera.git Cloning into 'tessera'... ``` 1. Run `cloud-sql-proxy` ```sh instance:~$ ./cloud-sql-proxy --port 3306 transparency-dev-playground:us-central1:mysql-dev-instance-1 ``` 1. Run `cmd/conformance/mysql` ```sh instance:~/tessera$ go run ./cmd/conformance/mysql --mysql_uri="root:root@tcp(127.0.0.1:3306)/test_tessera" --init_schema_path="./storage/mysql/schema.sql" --private_key_path="./cmd/conformance/mysql/docker/testdata/key" --db_max_open_conns=1024 --db_max_idle_conns=512 ``` 1. Run `hammer` and get performance metrics ```sh hammer:~/tessera$ go run ./internal/hammer --log_public_key=transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW --log_url=http://10.128.0.3:2024 --max_read_ops=0 --num_writers=512 --max_write_ops=512 ``` ## POSIX ### GCP Free Tier VM Instance **e2-micro** - vCPU: 0.25-2 vCPU (1 shared core) - Memory: 1 GB - OS: Debian GNU/Linux 12 (bookworm) > [!NOTE] > Virtual CPUs (vCPUs) in virtualized environments often share physical CPU cores with other vCPUs and introduce variability and potential performance impacts. #### Result ``` ┌───────────────────────────────────────────────────────────────────────┐ │Read (184 workers): Current max: 0/s. Oversupply in last second: 0 │ │Write (600 workers): Current max: 1758/s. Oversupply in last second: 0 │ │TreeSize: 1882477 (Δ 1587qps over 30s) │ │Time-in-queue: 149ms/371ms/692ms (min/avg/max) │ │Observed-time-to-integrate: 569ms/1191ms/1878ms (min/avg/max) │ └───────────────────────────────────────────────────────────────────────┘ ``` ``` top - 20:45:35 up 47 min, 3 users, load average: 1.89, 0.88, 1.03 Tasks: 97 total, 1 running, 96 sleeping, 0 stopped, 0 zombie %Cpu(s): 47.2 us, 24.7 sy, 0.0 ni, 0.0 id, 0.0 wa, 0.0 hi, 28.1 si, 0.0 st MiB Mem : 970.0 total, 158.7 free, 566.8 used, 409.3 buff/cache MiB Swap: 0.0 total, 0.0 free, 0.0 used. 403.2 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 8336 root 20 0 1231800 34784 5080 S 200.0 3.5 2:59.50 conformance-pos 409 root 20 0 2442648 79112 26836 S 1.0 8.0 0:49.10 dockerd 6848 root 20 0 1800176 34376 16940 S 0.7 3.5 0:12.57 docker-compose ``` #### Steps 1. Create a [GCP free tier](https://cloud.google.com/free/docs/free-cloud-features#free-tier) e2-micro VM instance in us-central1 (iowa). 1. [Install Go](https://go.dev/doc/install) ```sh instance:~$ wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz instance:~$ sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz instance:~$ export PATH=$PATH:/usr/local/go/bin instance:~$ go version go version go1.23.0 linux/amd64 ``` 1. [Install Docker using the `apt` repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) 1. Install Git ```sh instance:~$ sudo apt-get install git -y -q ... instance:~$ git version git version 2.39.2 ``` 1. Clone the Tessera repository ```sh instance:~$ git clone https://github.com/transparency-dev/tessera.git Cloning into 'tessera'... ``` 1. Run `cmd/conformance/posix` via Docker compose ```sh instance:~/tessera$ sudo docker compose -f ./cmd/conformance/posix/docker/compose.yaml up ``` 1. Run `hammer` and get performance metrics ```sh hammer:~/tessera$ go run ./internal/hammer --log_public_key=example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx --log_url=http://localhost:2025 --max_read_ops=0 --num_writers=512 --max_write_ops=512 ``` ## GCP Coming soon. transparency-dev-tessera-3cb22ee/storage/mysql/README.md000066400000000000000000000023061511600621500232200ustar00rootroot00000000000000# Tessera on MySQL This directory contains the implementation of a storage backend for Tessera using MySQL. This allows Tessera to leverage MySQL as its underlying database for storing checkpoint, entry hashes and data in tiles format. ## Design See [MySQL storage design documentation](./DESIGN.md). ### Requirements - A running MySQL server instance. This storage implementation has been tested against MySQL 8.4. ## Usage ### Constructing the Storage Object Here is an example code snippet to initialise the MySQL storage in Tessera. ```go import ( "context" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/storage/mysql" "k8s.io/klog/v2" ) func main() { mysqlURI := "user:password@tcp(db:3306)/tessera" db, err := sql.Open("mysql", mysqlURI) if err != nil { klog.Exitf("Failed to connect to DB: %v", err) } storage, err := mysql.New(ctx, db) if err != nil { klog.Exitf("Failed to create new MySQL storage: %v", err) } } ``` ### Example Personality See [MySQL conformance example](/cmd/conformance/mysql/). ## Future Work - [Separate sequencing and integration](https://github.com/transparency-dev/tessera/pull/282) transparency-dev-tessera-3cb22ee/storage/mysql/mysql.go000066400000000000000000000635371511600621500234520ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package mysql contains a MySQL-based storage implementation for Tessera. package mysql import ( "bytes" "context" "crypto/sha256" "database/sql" "errors" "fmt" "io/fs" "net/http" "os" "strings" "time" _ "github.com/go-sql-driver/mysql" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/internal/migrate" storage "github.com/transparency-dev/tessera/storage/internal" "k8s.io/klog/v2" ) const ( selectCompatibilityVersionSQL = "SELECT `compatibilityVersion` FROM `Tessera` WHERE `id` = 0" selectCheckpointByIDSQL = "SELECT `note`, `published_at` FROM `Checkpoint` WHERE `id` = ?" selectCheckpointByIDForUpdateSQL = selectCheckpointByIDSQL + " FOR UPDATE" replaceCheckpointSQL = "REPLACE INTO `Checkpoint` (`id`, `note`, `published_at`) VALUES (?, ?, ?)" selectTreeStateByIDSQL = "SELECT `size`, `root` FROM `TreeState` WHERE `id` = ?" selectTreeStateByIDForUpdateSQL = selectTreeStateByIDSQL + " FOR UPDATE" replaceTreeStateSQL = "REPLACE INTO `TreeState` (`id`, `size`, `root`) VALUES (?, ?, ?)" selectSubtreeByLevelAndIndexSQL = "SELECT `nodes` FROM `Subtree` WHERE `level` = ? AND `index` = ?" replaceSubtreeSQL = "REPLACE INTO `Subtree` (`level`, `index`, `nodes`) VALUES (?, ?, ?)" selectTiledLeavesSQL = "SELECT `size`, `data` FROM `TiledLeaves` WHERE `tile_index` = ?" streamTiledLeavesSQL = "SELECT `tile_index`, `size`, `data` FROM `TiledLeaves` WHERE `tile_index` >= ? ORDER BY `tile_index` ASC" replaceTiledLeavesSQL = "REPLACE INTO `TiledLeaves` (`tile_index`, `size`, `data`) VALUES (?, ?, ?)" checkpointID = 0 treeStateID = 0 schemaCompatibilityVersion = 1 minCheckpointInterval = time.Second ) // Storage is a MySQL-based storage implementation for Tessera. type Storage struct { db *sql.DB } // New creates a new instance of the MySQL-based Storage. func New(ctx context.Context, db *sql.DB) (*Storage, error) { s := &Storage{ db: db, } if err := s.db.Ping(); err != nil { klog.Errorf("Failed to ping database: %v", err) return nil, err } if err := s.ensureVersion(ctx, schemaCompatibilityVersion); err != nil { return nil, fmt.Errorf("incompatible schema version: %v", err) } return s, nil } // Note that `tessera.WithCheckpointSigner()` is mandatory in the `opts` argument. func (s *Storage) Appender(ctx context.Context, opts *tessera.AppendOptions) (*tessera.Appender, tessera.LogReader, error) { if opts.CheckpointInterval() < minCheckpointInterval { return nil, nil, fmt.Errorf("requested CheckpointInterval too low - %v < %v", opts.CheckpointInterval(), minCheckpointInterval) } a := &appender{ s: s, newCheckpoint: opts.CheckpointPublisher(s, http.DefaultClient), cpUpdated: make(chan struct{}, 1), } a.queue = storage.NewQueue(ctx, opts.BatchMaxAge(), opts.BatchMaxSize(), a.sequenceBatch) if err := s.maybeInitTree(ctx); err != nil { return nil, nil, fmt.Errorf("maybeInitTree: %v", err) } a.cpUpdated <- struct{}{} go func(ctx context.Context, i time.Duration) { t := time.NewTicker(i) defer t.Stop() for { select { case <-ctx.Done(): return case <-a.cpUpdated: case <-t.C: } if err := a.publishCheckpoint(ctx, i); err != nil { klog.Warningf("publishCheckpoint: %v", err) } } }(ctx, opts.CheckpointInterval()) return &tessera.Appender{ Add: a.Add, }, s, nil } func (s *Storage) ensureVersion(ctx context.Context, wantVersion uint8) error { row := s.db.QueryRowContext(ctx, selectCompatibilityVersionSQL) if row.Err() != nil { return row.Err() } var gotVersion uint8 if err := row.Scan(&gotVersion); err != nil { return fmt.Errorf("failed to read Tessera version from DB: %v", err) } if gotVersion != wantVersion { return fmt.Errorf("DB has Tessera compatibility version of %d, but version %d required", gotVersion, wantVersion) } return nil } // maybeInitTree will insert an initial "empty tree" row into the // TreeState table iff no row already exists. // // This method doesn't also publish this new empty tree as a Checkpoint, // rather, such a checkpoint will be published asynchronously by the // same mechanism used to publish future checkpoints. Although in _this_ // case it would be expected to happen in very short order given that it's // likely that no row currently exists in the Checkpoints table either. func (s *Storage) maybeInitTree(ctx context.Context) error { tx, err := s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("being tx init tree state: %v", err) } defer func() { if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { klog.Errorf("Failed to rollback in write initial tree state: %v", err) } }() treeState, err := s.readTreeStateForUpdate(ctx, tx) if err != nil && !errors.Is(err, fs.ErrNotExist) { klog.Errorf("Failed to read tree state: %v", err) return err } if treeState == nil { klog.Infof("Initializing tree state") if err := s.writeTreeState(ctx, tx, 0, rfc6962.DefaultHasher.EmptyRoot()); err != nil { klog.Errorf("Failed to write initial tree state: %v", err) return err } // Only need to commit if we've actually initialised the tree state, otherwise we'll // rely on the defer'd rollback to tidy up. if err := tx.Commit(); err != nil { return fmt.Errorf("commit init tree state: %v", err) } } return nil } // ReadCheckpoint returns the latest stored checkpoint. // If the checkpoint is not found, it returns os.ErrNotExist. func (s *Storage) ReadCheckpoint(ctx context.Context) ([]byte, error) { row := s.db.QueryRowContext(ctx, selectCheckpointByIDSQL, checkpointID) if err := row.Err(); err != nil { return nil, err } var checkpoint []byte var at int64 if err := row.Scan(&checkpoint, &at); err != nil { if err == sql.ErrNoRows { return nil, os.ErrNotExist } return nil, fmt.Errorf("scan checkpoint: %v", err) } return checkpoint, nil } type treeState struct { size uint64 root []byte } // readTreeState returns the currently stored state information. // If there is no stored tree state, it returns os.ErrNotExist. func (s *Storage) readTreeState(ctx context.Context) (*treeState, error) { row := s.db.QueryRowContext(ctx, selectTreeStateByIDSQL, treeStateID) if err := row.Err(); err != nil { return nil, err } r := &treeState{} if err := row.Scan(&r.size, &r.root); err != nil { if err == sql.ErrNoRows { return nil, os.ErrNotExist } return nil, fmt.Errorf("scan tree state: %v", err) } return r, nil } // readTreeStateForUpdate returns the currently stored tree state information, and locks the row for update using the provided transaction. // If there is no stored tree state, it returns os.ErrNotExist. func (s *Storage) readTreeStateForUpdate(ctx context.Context, tx *sql.Tx) (*treeState, error) { row := tx.QueryRowContext(ctx, selectTreeStateByIDForUpdateSQL, treeStateID) if err := row.Err(); err != nil { return nil, err } r := &treeState{} if err := row.Scan(&r.size, &r.root); err != nil { if err == sql.ErrNoRows { return nil, os.ErrNotExist } return nil, fmt.Errorf("scan tree state: %v", err) } return r, nil } // writeTreeState updates the TreeState table with the new tree state information. func (s *Storage) writeTreeState(ctx context.Context, tx *sql.Tx, size uint64, rootHash []byte) error { if _, err := tx.ExecContext(ctx, replaceTreeStateSQL, treeStateID, size, rootHash); err != nil { klog.Errorf("Failed to execute replaceTreeStateSQL: %v", err) return err } return nil } // ReadTile returns a full tile or a partial tile at the given level, index and treeSize. // If the tile is not found, it returns os.ErrNotExist. // // Note that if a partial tile is requested, but a larger tile is available, this // will return the largest tile available. This could be trimmed to return only the // number of entries specifically requested if this behaviour becomes problematic. func (s *Storage) ReadTile(ctx context.Context, level, index uint64, p uint8) ([]byte, error) { row := s.db.QueryRowContext(ctx, selectSubtreeByLevelAndIndexSQL, level, index) if err := row.Err(); err != nil { return nil, err } var tile []byte if err := row.Scan(&tile); err != nil { if err == sql.ErrNoRows { return nil, os.ErrNotExist } return nil, fmt.Errorf("scan tile: %v", err) } numEntries := uint64(len(tile) / sha256.Size) requestedEntries := uint64(p) if requestedEntries == 0 { requestedEntries = layout.TileWidth } if requestedEntries > numEntries { // If the user has requested a size larger than we have, they can't have it return nil, os.ErrNotExist } return tile, nil } // writeTile replaces the tile nodes at the given level and index. func (s *Storage) writeTile(ctx context.Context, tx *sql.Tx, level, index uint64, nodes []byte) error { if _, err := tx.ExecContext(ctx, replaceSubtreeSQL, level, index, nodes); err != nil { klog.Errorf("Failed to execute replaceSubtreeSQL: %v", err) return err } return nil } // ReadEntryBundle returns the log entries at the given index. // If the entry bundle is not found, it returns os.ErrNotExist. // // Note that if a partial tile is requested, but a larger tile is available, this // will return the largest tile available. This could be trimmed to return only the // number of entries specifically requested if this behaviour becomes problematic. func (s *Storage) ReadEntryBundle(ctx context.Context, index uint64, p uint8) ([]byte, error) { row := s.db.QueryRowContext(ctx, selectTiledLeavesSQL, index) if err := row.Err(); err != nil { return nil, err } var size uint32 var entryBundle []byte if err := row.Scan(&size, &entryBundle); err != nil { if err == sql.ErrNoRows { return nil, os.ErrNotExist } return nil, fmt.Errorf("scan entry bundle: %v", err) } requestedSize := uint32(p) if requestedSize == 0 { requestedSize = layout.EntryBundleWidth } if requestedSize > size { return nil, fmt.Errorf("bundle with %d entries requested, but only %d available: %w", requestedSize, size, os.ErrNotExist) } return entryBundle, nil } // IntegratedSize returns the current size of the integrated tree. // // This is part of the tessera LogReader contract. func (s *Storage) IntegratedSize(ctx context.Context) (uint64, error) { ts, err := s.readTreeState(ctx) if err != nil { return 0, fmt.Errorf("readTreeState: %v", err) } return ts.size, nil } // NextIndex returns the next available leaf index. // // Currently, this is the same as the integrated size since new leaves are integrated synchronously. // This is part of the tessera LogReader contract. func (s *Storage) NextIndex(ctx context.Context) (uint64, error) { return s.IntegratedSize(ctx) } // dbExecContext describes something which can support the sql ExecContext function. // this allows us to use either sql.Tx or sql.DB. type dbExecContext interface { ExecContext(context.Context, string, ...any) (sql.Result, error) } func (s *Storage) writeEntryBundle(ctx context.Context, tx dbExecContext, index uint64, size uint32, entryBundle []byte) error { if _, err := tx.ExecContext(ctx, replaceTiledLeavesSQL, index, size, entryBundle); err != nil { klog.Errorf("Failed to execute replaceTiledLeavesSQL: %v", err) return err } return nil } // appender implements the tessera Append lifecycle. type appender struct { s *Storage queue *storage.Queue newCheckpoint func(context.Context, uint64, []byte) ([]byte, error) cpUpdated chan struct{} } // publishCheckpoint creates a new checkpoint for the given size and root hash, and stores it in the // Checkpoint table. func (a *appender) publishCheckpoint(ctx context.Context, interval time.Duration) error { tx, err := a.s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin tx: %v", err) } defer func() { if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { klog.Warningf("publishCheckpoint rollback failed: %v", err) } }() var note string var at int64 if err := tx.QueryRowContext(ctx, selectCheckpointByIDForUpdateSQL, checkpointID).Scan(¬e, &at); err != nil && !errors.Is(err, sql.ErrNoRows) { return fmt.Errorf("scan checkpoint: %v", err) } if time.Since(time.UnixMilli(at)) < interval { // Too soon, try again later. klog.V(1).Info("skipping publish - too soon") return nil } treeState, err := a.s.readTreeStateForUpdate(ctx, tx) if err != nil { return fmt.Errorf("readTreeState: %v", err) } rawCheckpoint, err := a.newCheckpoint(ctx, treeState.size, treeState.root) if err != nil { return err } if _, err := tx.ExecContext(ctx, replaceCheckpointSQL, checkpointID, rawCheckpoint, time.Now().UnixMilli()); err != nil { return err } klog.V(2).Infof("Published latest checkpoint: %d, %x", treeState.size, treeState.root) return tx.Commit() } // Add is the entrypoint for adding entries to a sequencing log. func (a *appender) Add(ctx context.Context, entry *tessera.Entry) tessera.IndexFuture { return a.queue.Add(ctx, entry) } // sequenceBatch writes the entries from the provided batch into the entry bundle files of the log. // // This func starts filling entries bundles at the next available slot in the log, ensuring that the // sequenced entries are contiguous from the zeroth entry (i.e left-hand dense). // We try to minimise the number of partially complete entry bundles by writing entries in chunks rather // than one-by-one. // // TODO(#21): Separate sequencing and integration for better performance. func (a *appender) sequenceBatch(ctx context.Context, entries []*tessera.Entry) error { // Return when there is no entry to sequence. if len(entries) == 0 { return nil } // Get a Tx for making transaction requests. tx, err := a.s.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("begin tx: %v", err) } // Defer a rollback in case anything fails. defer func() { if err := tx.Rollback(); err != nil && err != sql.ErrTxDone { klog.Errorf("Failed to rollback in sequenceBatch: %v", err) } }() // Get tree size. Note that "SELECT ... FOR UPDATE" is used for row-level locking. row := tx.QueryRowContext(ctx, selectTreeStateByIDForUpdateSQL, treeStateID) if err := row.Err(); err != nil { return fmt.Errorf("select tree state: %v", err) } state := treeState{} if err := row.Scan(&state.size, &state.root); err != nil { return fmt.Errorf("failed to read tree state: %w", err) } // Integrate the new entries into the entry bundle (TiledLeaves table) and tile (Subtree table). if err := a.appendEntries(ctx, tx, state.size, entries); err != nil { return fmt.Errorf("failed to integrate: %w", err) } // Commit the transaction. err = tx.Commit() select { case a.cpUpdated <- struct{}{}: default: } return err } // appendEntries incorporates the provided entries into the log starting at fromSeq. func (a *appender) appendEntries(ctx context.Context, tx *sql.Tx, fromSeq uint64, entries []*tessera.Entry) error { sequencedEntries := make([]storage.SequencedEntry, len(entries)) // Assign provisional sequence numbers to entries. // We need to do this here in order to support serialisations which include the log position. for i, e := range entries { sequencedEntries[i] = storage.SequencedEntry{ BundleData: e.MarshalBundleData(fromSeq + uint64(i)), LeafHash: e.LeafHash(), } } // Add sequenced entries to entry bundles. bundleIndex, entriesInBundle := fromSeq/layout.EntryBundleWidth, fromSeq%layout.EntryBundleWidth bundleWriter := &bytes.Buffer{} // If the latest bundle is partial, we need to read the data it contains in for our newer, larger, bundle. if entriesInBundle > 0 { row := tx.QueryRowContext(ctx, selectTiledLeavesSQL, bundleIndex) if err := row.Err(); err != nil { return fmt.Errorf("query tiled leaves: %v", err) } var size uint32 var partialEntryBundle []byte if err := row.Scan(&size, &partialEntryBundle); err != nil { return fmt.Errorf("scan partial entry bundle: %w", err) } if size != uint32(entriesInBundle) { return fmt.Errorf("expected %d entries in storage but found %d", entriesInBundle, size) } if _, err := bundleWriter.Write(partialEntryBundle); err != nil { return fmt.Errorf("write partial entry bundle: %w", err) } } // Add new entries to the bundle. for _, e := range sequencedEntries { if _, err := bundleWriter.Write(e.BundleData); err != nil { return fmt.Errorf("write bundle data: %w", err) } entriesInBundle++ // This bundle is full, so we need to write it out. if entriesInBundle == layout.EntryBundleWidth { if err := a.s.writeEntryBundle(ctx, tx, bundleIndex, uint32(entriesInBundle), bundleWriter.Bytes()); err != nil { return fmt.Errorf("writeEntryBundle: %w", err) } // Prepare the next entry bundle for any remaining entries in the batch. bundleIndex++ entriesInBundle = 0 bundleWriter = &bytes.Buffer{} } } // If we have a partial bundle remaining once we've added all the entries from the batch, // this needs writing out too. if entriesInBundle > 0 { if err := a.s.writeEntryBundle(ctx, tx, bundleIndex, uint32(entriesInBundle), bundleWriter.Bytes()); err != nil { return fmt.Errorf("writeEntryBundle: %w", err) } } lh := make([][]byte, len(sequencedEntries)) for i, e := range sequencedEntries { lh[i] = e.LeafHash } newSize, newRoot, err := integrate(ctx, tx, fromSeq, lh, a.s.writeTile) if err != nil { return fmt.Errorf("integrate: %v", err) } // Write new tree state. if err := a.s.writeTreeState(ctx, tx, newSize, newRoot); err != nil { return fmt.Errorf("writeCheckpoint: %w", err) } klog.V(1).Infof("New tree: %d, %x", newSize, newRoot) return nil } func getTiles(ctx context.Context, tx *sql.Tx, tileIDs []storage.TileID, _ uint64) ([]*api.HashTile, error) { hashTiles := make([]*api.HashTile, len(tileIDs)) if len(tileIDs) == 0 { return hashTiles, nil } // Build the SQL and args to fetch the hash tiles. var sql strings.Builder args := make([]any, 0, len(tileIDs)*2) for i, id := range tileIDs { if i != 0 { sql.WriteString(" UNION ALL ") } _, err := sql.WriteString(selectSubtreeByLevelAndIndexSQL) if err != nil { return nil, err } args = append(args, id.Level, id.Index) } rows, err := tx.QueryContext(ctx, sql.String(), args...) if err != nil { return nil, fmt.Errorf("failed to query the hash tiles with SQL (%s): %w", sql.String(), err) } defer func() { if err := rows.Close(); err != nil { klog.Warningf("Failed to close the rows: %v", err) } }() i := 0 for rows.Next() { var tile []byte if err := rows.Scan(&tile); err != nil { return nil, fmt.Errorf("scan subtree tile: %w", err) } t := &api.HashTile{} if err := t.UnmarshalText(tile); err != nil { return nil, fmt.Errorf("unmarshal tile: %w", err) } hashTiles[i] = t i++ } if err = rows.Err(); err != nil { return nil, fmt.Errorf("rows error while fetching subtrees: %w", err) } return hashTiles, nil } // integrate adds the provided leaf hashes to the merkle tree, starting at the provided location. func integrate(ctx context.Context, tx *sql.Tx, fromSeq uint64, lh [][]byte, writeTile func(context.Context, *sql.Tx, uint64, uint64, []byte) error) (uint64, []byte, error) { getTiles := func(ctx context.Context, tileIDs []storage.TileID, treeSize uint64) ([]*api.HashTile, error) { return getTiles(ctx, tx, tileIDs, treeSize) } newSize, newRoot, tiles, err := storage.Integrate(ctx, getTiles, fromSeq, lh) if err != nil { return 0, nil, fmt.Errorf("storage.Integrate: %v", err) } for k, v := range tiles { nodes, err := v.MarshalText() if err != nil { return 0, nil, err } if err := writeTile(ctx, tx, uint64(k.Level), k.Index, nodes); err != nil { return 0, nil, fmt.Errorf("failed to set tile(%v): %w", k, err) } } return newSize, newRoot, nil } // MigrationWriter creates a new MySQL storage for the MigrationTarget lifecycle mode. func (s *Storage) MigrationWriter(ctx context.Context, opts *tessera.MigrationOptions) (migrate.MigrationWriter, tessera.LogReader, error) { if err := s.maybeInitTree(ctx); err != nil { return nil, nil, fmt.Errorf("maybeInitTree: %v", err) } return &MigrationStorage{ s: s, bundleHasher: opts.LeafHasher(), }, s, nil } // MigrationStorgage implements the tessera.MigrationTarget lifecycle contract. type MigrationStorage struct { s *Storage bundleHasher func([]byte) ([][]byte, error) } var _ migrate.MigrationWriter = &MigrationStorage{} // AwaitIntegration blocks until the local integrated tree has grown to the provided size. // // This implements part of the tessera MigrationTarget lifecycle contract. // // As well as waiting for the integration to reach the desired size, this method is where // the integration process itself actually happens. func (m *MigrationStorage) AwaitIntegration(ctx context.Context, sourceSize uint64) ([]byte, error) { // fromSeq keeps track of where we need to integrate from - i.e. the current local size of the integrated tree. var fromSeq uint64 // rows provides a stream of entry bundle rows which will be processed in the loop below. var rows *sql.Rows // The outer loop "tryAgain", will (re-) setup the streaming read of entry bundles from the DB. // The inner loop will go around attempting to process each of these rows in turn. If it encounters // a problem it'll break out to the outer loop to sort things out and retry. tryAgain: for { select { case <-ctx.Done(): return nil, ctx.Err() case <-time.After(time.Second): } // Release resources if we're going around and resetting the read. if rows != nil { _ = rows.Close() } // Figure out where we should be integration from. from, err := m.IntegratedSize(ctx) if err != nil && !errors.Is(err, os.ErrNotExist) { klog.Warningf("AwaitIntegration: readTreeState: %v", err) continue } fromSeq = from klog.Infof("AwaitIntegration: Integrate from %d (Target %d)", fromSeq, sourceSize) // Set up the streaming read of entry bundles from the DB. nextBundle := fromSeq / layout.EntryBundleWidth rows, err = m.s.db.QueryContext(ctx, streamTiledLeavesSQL, nextBundle) if err != nil { klog.Warningf("Failed to start streaming entry bundles @%d: %v", nextBundle, err) continue } // This is the inner loop which processes each of the entry bundle rows from the DB read in turn. for rows.Next() { // Parse the row. var idx, size uint64 var data []byte if err := rows.Scan(&idx, &size, &data); err != nil { klog.Warningf("AwaitIntegration: Scan: %v", err) continue tryAgain } // Check that we're seeing contiguous bundles, and go around if we've encountered a gap. // This isn't necessarily an unrecoverable error, it's probably just that we've either hit the end of all // available entry bundles, or whatever process is copying them over hasn't yet written this one. // We'll continue looping around in the outer loop (where we back off to avoid hammering the DB) until // this entry bundle turns up. if want := fromSeq / uint64(layout.EntryBundleWidth); idx != want { klog.V(1).Infof("AwaitIntegration: encountered gap, want idx %d (fromSeq %d) but found %d", want, fromSeq, idx) continue tryAgain } // Turn the entry bundle into leaf hashes. lh, err := m.bundleHasher(data) if err != nil { klog.Warningf("AwaitIntegration: bundleHasher: %v", err) continue tryAgain } // Trim the bundle if we've previously integrated some of it (e.g. because it was a [smaller] partial bundle last time // we saw it. f := fromSeq % layout.EntryBundleWidth lh = lh[f:] // And finally integrate the bundle into the tree. newSize, newRoot, err := m.integrateBatch(ctx, fromSeq, lh) if err != nil { klog.Warningf("AwaitIntegration: integrateBatch: %v", err) continue tryAgain } fromSeq = newSize if newSize == sourceSize { klog.Infof("AwaitIntegration: Integrated to %d with root hash %x", newSize, newRoot) return newRoot, nil } } } } // integrateBatch integrates the provided entries at the specified starting index. // // Returns the new size of the local tree and its new root hash. func (m *MigrationStorage) integrateBatch(ctx context.Context, fromSeq uint64, lh [][]byte) (uint64, []byte, error) { tx, err := m.s.db.BeginTx(ctx, nil) if err != nil { return 0, nil, err } defer func() { if tx != nil { if err := tx.Rollback(); err != nil { klog.Warningf("integrateBatch: Rollback: %v", err) } } }() newSize, newRoot, err := integrate(ctx, tx, fromSeq, lh, m.s.writeTile) if err != nil { return 0, nil, fmt.Errorf("integrate: %v", err) } if err := m.s.writeTreeState(ctx, tx, newSize, newRoot); err != nil { return 0, nil, fmt.Errorf("writeTreeState: %v", err) } if err := tx.Commit(); err != nil { return 0, nil, fmt.Errorf("commit: %v", err) } tx = nil return newSize, newRoot, err } // SetEntryBundle stores the provided serialised entry bundle at the location implied by the provided // entry bundle index and partial size. // // Implements the tessera MigrationTarget lifecycle contract. func (m *MigrationStorage) SetEntryBundle(ctx context.Context, index uint64, partial uint8, bundle []byte) error { return m.s.writeEntryBundle(ctx, m.s.db, index, uint32(partial), bundle) } // IntegratedSize returns the current size of the locally integrated log. // // Implements the tessera MigrationTarget lifecycle contract. func (m *MigrationStorage) IntegratedSize(ctx context.Context) (uint64, error) { return m.s.IntegratedSize(ctx) } transparency-dev-tessera-3cb22ee/storage/mysql/mysql_test.go000066400000000000000000000303321511600621500244740ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package mysql_test contains the tests for a MySQL-based storage implementation for Tessera. // It requires a MySQL database to successfully run the tests. Otherwise, the tests in this file will be skipped. // // Sample command to start a local MySQL database using Docker: // $ docker run --name test-mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=root -e MYSQL_DATABASE=test_tessera -d mysql package mysql import ( "bytes" "context" "crypto/sha256" "database/sql" "errors" "flag" "fmt" "io/fs" "os" "testing" "time" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "golang.org/x/mod/sumdb/note" "golang.org/x/sync/errgroup" "k8s.io/klog/v2" ) var ( mysqlURI = flag.String("mysql_uri", "root:root@tcp(localhost:3306)/test_tessera", "Connection string for a MySQL database") isMySQLTestOptional = flag.Bool("is_mysql_test_optional", true, "Boolean value to control whether the MySQL test is optional") testDB *sql.DB noteSigner note.Signer ) const ( testPrivateKey = "PRIVATE+KEY+transparency.dev/tessera/example+ae330e15+AXEwZQ2L6Ga3NX70ITObzyfEIketMr2o9Kc+ed/rt/QR" testPublicKey = "transparency.dev/tessera/example+ae330e15+ASf4/L1zE859VqlfQgGzKy34l91Gl8W6wfwp+vKP62DW" ) // TestMain checks whether the test MySQL database is available and starts the tests including database schema initialization. // If is_mysql_test_optional is set to true and MySQL database cannot be opened or pinged, the test will fail immediately. // Otherwise, the test will be skipped if the test is optional and the database is not available. func TestMain(m *testing.M) { klog.InitFlags(nil) flag.Parse() ctx := context.Background() db, err := sql.Open("mysql", *mysqlURI) if err != nil { if *isMySQLTestOptional { klog.Warning("MySQL not available, skipping all MySQL storage tests") return } klog.Fatalf("Failed to open MySQL test db: %v", err) } defer func() { if err := db.Close(); err != nil { klog.Warningf("Failed to close MySQL database: %v", err) } }() if err := db.PingContext(ctx); err != nil { if *isMySQLTestOptional { klog.Warning("MySQL not available, skipping all MySQL storage tests") return } klog.Fatalf("Failed to ping MySQL test db: %v", err) } testDB = db klog.Info("Successfully connected to MySQL test database") initDatabaseSchema(ctx) noteSigner, err = note.NewSigner(testPrivateKey) if err != nil { klog.Fatalf("Failed to create new signer: %v", err) } os.Exit(m.Run()) } // initDatabaseSchema drops the tables and then imports the schema. // A separate database connection is required since the schema file contains multiple statements. // `multiStatements=true` in the data source name allows multiple statements in one query. // This is not being used in the actual MySQL storage implementation. func initDatabaseSchema(ctx context.Context) { dropTablesSQL := "DROP TABLE IF EXISTS `Checkpoint`, `Subtree`, `TiledLeaves`, `TreeState`" rawSchema, err := os.ReadFile("schema.sql") if err != nil { klog.Fatalf("Failed to read schema.sql: %v", err) } db, err := sql.Open("mysql", *mysqlURI+"?multiStatements=true") if err != nil { klog.Fatalf("Failed to connect to DB: %v", err) } defer func() { if err := db.Close(); err != nil { klog.Warningf("Failed to close db: %v", err) } }() if _, err := db.ExecContext(ctx, dropTablesSQL); err != nil { klog.Fatalf("Failed to drop all tables: %v", err) } if _, err := db.ExecContext(ctx, string(rawSchema)); err != nil { klog.Fatalf("Failed to execute init database schema: %v", err) } } func TestAppend(t *testing.T) { ctx := context.Background() for _, test := range []struct { name string opts *tessera.AppendOptions wantErr bool }{ { name: "no tessera.AppendOptions", opts: tessera.NewAppendOptions(), wantErr: true, }, { name: "standard tessera.WithCheckpointSigner", opts: tessera.NewAppendOptions().WithCheckpointSigner(noteSigner), }, { name: "all tessera.AppendOptions", opts: tessera.NewAppendOptions(). WithCheckpointSigner(noteSigner). WithBatching(1, 1*time.Second). WithPushback(10), }, } { t.Run(test.name, func(t *testing.T) { drv, err := New(ctx, testDB) if err != nil { t.Fatalf("New: %v", err) } _, _, _, err = tessera.NewAppender(ctx, drv, test.opts) gotErr := err != nil if gotErr != test.wantErr { t.Errorf("got err: %v", err) } }) } } func TestGetTile(t *testing.T) { ctx := context.Background() addFn, r, _ := newTestMySQLStorage(t, ctx) awaiter := tessera.NewPublicationAwaiter(ctx, r.ReadCheckpoint, 1*time.Second) treeSize := 258 wg := errgroup.Group{} for i := range treeSize { wg.Go( func() error { _, _, err := awaiter.Await(ctx, addFn(ctx, tessera.NewEntry(fmt.Appendf(nil, "TestGetTile %d", i)))) if err != nil { return fmt.Errorf("failed to prep test with entry: %v", err) } return nil }) } if err := wg.Wait(); err != nil { t.Fatalf("Failed to set up database with required leaves: %v", err) } for _, test := range []struct { name string level, index uint64 p uint8 wantEntries int wantNotFound bool }{ { name: "requested partial tile for a complete tile", level: 0, index: 0, p: 10, wantEntries: layout.TileWidth, wantNotFound: false, }, { name: "too small but that's ok", level: 0, index: 1, p: layout.PartialTileSize(0, 1, uint64(treeSize-1)), wantEntries: 2, wantNotFound: false, }, { name: "just right", level: 0, index: 1, p: layout.PartialTileSize(0, 1, uint64(treeSize)), wantEntries: 2, wantNotFound: false, }, { name: "too big", level: 0, index: 1, p: layout.PartialTileSize(0, 1, uint64(treeSize+1)), wantNotFound: true, }, { name: "level 1 too small", level: 1, index: 0, p: layout.PartialTileSize(1, 0, uint64(treeSize-1)), wantEntries: 1, wantNotFound: false, }, { name: "level 1 just right", level: 1, index: 0, p: layout.PartialTileSize(1, 0, uint64(treeSize)), wantEntries: 1, wantNotFound: false, }, { name: "level 1 too big", level: 1, index: 0, p: layout.PartialTileSize(1, 0, 550), wantNotFound: true, }, } { t.Run(test.name, func(t *testing.T) { tile, err := r.ReadTile(ctx, test.level, test.index, test.p) if err != nil { if notFound, wantNotFound := errors.Is(err, fs.ErrNotExist), test.wantNotFound; notFound != wantNotFound { t.Errorf("wantNotFound %v but notFound %v", wantNotFound, notFound) } if test.wantNotFound { return } t.Errorf("got err: %v", err) } numEntries := len(tile) / sha256.Size if got, want := numEntries, test.wantEntries; got != want { t.Errorf("got %d entries, but want %d", got, want) } }) } } func TestReadMissingTile(t *testing.T) { ctx := context.Background() _, r, _ := newTestMySQLStorage(t, ctx) for _, test := range []struct { name string level, index uint64 p uint8 }{ { name: "0/0/0", level: 0, index: 0, p: 0, }, { name: "123/456/789", level: 123, index: 456, p: 789 % layout.TileWidth, }, } { t.Run(test.name, func(t *testing.T) { tile, err := r.ReadTile(ctx, test.level, test.index, test.p) if err != nil { if errors.Is(err, fs.ErrNotExist) { // this is success for this test return } t.Errorf("got err: %v", err) } if tile != nil { t.Error("tile is not nil") } }) } } func TestReadMissingEntryBundle(t *testing.T) { ctx := context.Background() _, r, _ := newTestMySQLStorage(t, ctx) for _, test := range []struct { name string index uint64 }{ { name: "0", index: 0, }, { name: "123456789", index: 123456789, }, } { t.Run(test.name, func(t *testing.T) { entryBundle, err := r.ReadEntryBundle(ctx, test.index, uint8(test.index%layout.TileWidth)) if err != nil { if errors.Is(err, fs.ErrNotExist) { // this is success for this test return } t.Errorf("got err: %v", err) } if entryBundle != nil { t.Error("entryBundle is not nil") } }) } } func TestParallelAdd(t *testing.T) { t.Parallel() ctx := context.Background() addFn, _, _ := newTestMySQLStorage(t, ctx) for _, test := range []struct { name string entry []byte }{ { name: "empty string entry", entry: []byte(""), }, { name: "123 string entry", entry: []byte("123"), }, { name: "empty byte", entry: []byte{}, }, } { t.Run(test.name, func(t *testing.T) { eG := errgroup.Group{} for range 1024 { eG.Go(func() error { _, err := addFn(ctx, tessera.NewEntry(test.entry))() return err }) } if err := eG.Wait(); err != nil { t.Errorf("got err: %v", err) } }) } } func TestTileRoundTrip(t *testing.T) { ctx := context.Background() addFn, r, _ := newTestMySQLStorage(t, ctx) for _, test := range []struct { name string entry []byte }{ { name: "empty string entry", entry: []byte(""), }, { name: "string entry", entry: []byte("I love Tessera"), }, { name: "empty byte", entry: []byte{}, }, } { t.Run(test.name, func(t *testing.T) { entryIndex, err := addFn(ctx, tessera.NewEntry(test.entry))() if err != nil { t.Errorf("Add got err: %v", err) } tileLevel, tileIndex, _, nodeIndex := layout.NodeCoordsToTileAddress(0, entryIndex.Index) tileRaw, err := r.ReadTile(ctx, tileLevel, tileIndex, uint8(nodeIndex+1)) if err != nil { t.Errorf("ReadTile got err: %v", err) } tile := api.HashTile{} if err := tile.UnmarshalText(tileRaw); err != nil { t.Errorf("failed to parse tile at index %d: %v", entryIndex.Index, err) } nodes := tile.Nodes if len(nodes) == 0 { t.Error("no node found") } else { gotHash := nodes[nodeIndex] wantHash := rfc6962.DefaultHasher.HashLeaf(test.entry) if !bytes.Equal(gotHash, wantHash[:]) { t.Errorf("got hash %v want %v", gotHash, wantHash) } } }) } } func TestEntryBundleRoundTrip(t *testing.T) { ctx := context.Background() addFn, r, _ := newTestMySQLStorage(t, ctx) for _, test := range []struct { name string entry []byte }{ { name: "empty string entry", entry: []byte(""), }, { name: "string entry", entry: []byte("I love Tessera"), }, { name: "empty byte", entry: []byte{}, }, } { t.Run(test.name, func(t *testing.T) { entryIndex, err := addFn(ctx, tessera.NewEntry(test.entry))() if err != nil { t.Errorf("Add got err: %v", err) } idx := entryIndex.Index entryBundleRaw, err := r.ReadEntryBundle(ctx, idx/layout.EntryBundleWidth, layout.PartialTileSize(0, idx, idx+1)) if err != nil { t.Fatalf("ReadEntryBundle got err: %v", err) } bundle := api.EntryBundle{} if err := bundle.UnmarshalText(entryBundleRaw); err != nil { t.Errorf("failed to parse EntryBundle at index %d: %v", entryIndex.Index, err) } gotEntries := bundle.Entries if len(gotEntries) == 0 { t.Error("no entry found") } else { if !bytes.Equal(bundle.Entries[idx%layout.EntryBundleWidth], test.entry) { t.Errorf("got entry %v want %v", bundle.Entries[0], test.entry) } } }) } } func newTestMySQLStorage(t *testing.T, ctx context.Context) (tessera.AddFn, tessera.LogReader, *Storage) { t.Helper() initDatabaseSchema(ctx) s, err := New(ctx, testDB) if err != nil { t.Fatalf("Failed to create mysql.Storage: %v", err) } a, _, r, err := tessera.NewAppender(ctx, s, tessera.NewAppendOptions(). WithCheckpointSigner(noteSigner). WithCheckpointInterval(time.Second). WithBatching(128, 100*time.Millisecond)) if err != nil { t.Fatal(err) } return a.Add, r, s } transparency-dev-tessera-3cb22ee/storage/mysql/schema.sql000066400000000000000000000064761511600621500237360ustar00rootroot00000000000000-- Copyright 2024 Google LLC -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- MySQL version of the Tessera database schema. -- "Tessera" table stores a single row that is the version of this schema -- and the data formats within it. This is read at startup to prevent Tessera -- running against a database with an incompatible format. CREATE TABLE IF NOT EXISTS Tessera ( -- id is expected to be always 0 to maintain a maximum of a single row. `id` TINYINT UNSIGNED NOT NULL, -- compatibilityVersion is the version of this schema and the data within it. `compatibilityVersion` BIGINT UNSIGNED NOT NULL, PRIMARY KEY (`id`) ); INSERT IGNORE INTO Tessera (`id`, `compatibilityVersion`) VALUES (0, 1); -- "Checkpoint" table stores a single row that records the latest _published_ checkpoint for the log. -- This is stored separately from the TreeState in order to enable publishing of commitments to updated tree states to happen -- on an indepentent timeframe to the internal updating of state. CREATE TABLE IF NOT EXISTS `Checkpoint` ( -- id is expected to be always 0 to maintain a maximum of a single row. `id` TINYINT UNSIGNED NOT NULL, -- note is the text signed by one or more keys in the checkpoint format. See https://c2sp.org/tlog-checkpoint and https://c2sp.org/signed-note. `note` MEDIUMBLOB NOT NULL, -- published_at is the millisecond UNIX timestamp of when this row was written. `published_at` BIGINT NOT NULL, PRIMARY KEY(`id`) ); -- "TreeState" table stores the current state of the integrated tree. -- This is not the same thing as a Checkpoint, which is a signed commitment to such a state. CREATE TABLE IF NOT EXISTS `TreeState` ( -- id is expected to be always 0 to maintain a maximum of a single row. `id` TINYINT UNSIGNED NOT NULL, -- size is the extent of the currently integrated tree. `size` BIGINT UNSIGNED NOT NULL, -- root is the root hash of the tree at the size stored in `size`. `root` TINYBLOB NOT NULL, PRIMARY KEY(`id`) ); -- "Subtree" table is an internal tile consisting of hashes. There is one row for each internal tile, and this is updated until it is completed, at which point it is immutable. CREATE TABLE IF NOT EXISTS `Subtree` ( -- level is the level of the tile. `level` TINYINT UNSIGNED NOT NULL, -- index is the index of the tile. `index` BIGINT UNSIGNED NOT NULL, -- nodes stores the hashes of the leaves. `nodes` MEDIUMBLOB NOT NULL, PRIMARY KEY(`level`, `index`) ); -- "TiledLeaves" table stores the data committed to by the leaves of the tree. Follows the same evolution as Subtree. CREATE TABLE IF NOT EXISTS `TiledLeaves` ( `tile_index` BIGINT UNSIGNED NOT NULL, -- size is the number of entries serialized into this leaf bundle. `size` SMALLINT UNSIGNED NOT NULL, `data` LONGBLOB NOT NULL, PRIMARY KEY(`tile_index`) ); transparency-dev-tessera-3cb22ee/storage/posix/000077500000000000000000000000001511600621500217355ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/storage/posix/PERFORMANCE.md000066400000000000000000000141231511600621500240210ustar00rootroot00000000000000# POSIX Performance ## Local system VM + local disk An LXC container with 12 cores and 8GB RAM, using a local ZFS filesystem built from two 6TB SAS disks configured as a mirrored device. The limiting factor is storage latency; writes are `fsync`'d for durability. NVME storage would likely improve throughput. Without antispam, it was able to sustain around 2900 writes/s. ``` ┌────────────────────────────────────────────────────────────────────────────────────┐ │Read (8 workers): Current max: 20/s. Oversupply in last second: 0 │ │Write (3000 workers): Current max: 3000/s. Oversupply in last second: 0 │ │TreeSize: 1470460 (Δ 2927qps over 30s) │ │Time-in-queue: 136ms/1110ms/1356ms (min/avg/max) │ │Observed-time-to-integrate: 583ms/6019ms/6594ms (min/avg/max) │ ├────────────────────────────────────────────────────────────────────────────────────┤ ``` With antispam enabled (badger), it was able to sustain around 1600 writes/s. ``` ┌────────────────────────────────────────────────────────────────────────────────────┐ │Read (8 workers): Current max: 20/s. Oversupply in last second: 0 │ │Write (1800 workers): Current max: 1800/s. Oversupply in last second: 0 │ │TreeSize: 2041087 (Δ 1664qps over 30s) │ │Time-in-queue: 0ms/112ms/448ms (min/avg/max) │ │Observed-time-to-integrate: 593ms/3232ms/5754ms (min/avg/max) │ ├────────────────────────────────────────────────────────────────────────────────────┤ ``` ``` top - 16:03:40 up 13 days, 7:14, 3 users, load average: 1.47, 1.94, 1.97 Tasks: 72 total, 1 running, 71 sleeping, 0 stopped, 0 zombie %Cpu(s): 1.7 us, 1.2 sy, 0.0 ni, 97.0 id, 0.0 wa, 0.0 hi, 0.1 si, 0.0 st MiB Mem : 8192.0 total, 5552.3 free, 1844.5 used, 795.1 buff/cache MiB Swap: 512.0 total, 512.0 free, 0.0 used. 28332.6 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 2141681 al 20 0 4258436 114576 5120 S 50.5 0.3 6:26.34 hammer 2140012 al 20 0 7480052 735092 143196 S 29.9 2.2 4:55.74 posix ``` ## GCP Free Tier VM Instance **e2-micro** - vCPU: 0.25-2 vCPU (1 shared core) - Memory: 1 GB - OS: Debian GNU/Linux 12 (bookworm) > [!NOTE] > Virtual CPUs (vCPUs) in virtualized environments often share physical CPU cores with other vCPUs and introduce variability and potential performance impacts. This is able to sustain around 600 write/s with antispam enabled. ``` ┌─────────────────────────────────────────────────────────────────────────┐ │Read (8 workers): Current max: 20/s. Oversupply in last second: 0 │ │Write (1000 workers): Current max: 1000/s. Oversupply in last second: 538│ │TreeSize: 96827 (Δ 683qps over 30s) │ │Time-in-queue: 167ms/1264ms/2000ms (min/avg/max) │ │Observed-time-to-integrate: 416ms/7877ms/10990ms (min/avg/max) │ ├─────────────────────────────────────────────────────────────────────────┤ ``` ``` top - 15:37:30 up 22 min, 4 users, load average: 6.56, 4.83, 2.68 Tasks: 101 total, 1 running, 100 sleeping, 0 stopped, 0 zombie Cpu(s): 56.8 us, 21.6 sy, 0.0 ni, 8.0 id, 9.1 wa, 0.0 hi, 4.5 si, 0.0 st MiB Mem : 970.0 total, 62.2 free, 839.5 used, 227.0 buff/cache 0.0 total, 0.0 free, 0.0 used. 130.5 avail Mem PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND 4716 al 20 0 3671328 196420 15700 S 96.0 19.8 5:01.70 posix 5244 al 20 0 1300984 85548 6160 S 74.4 8.6 3:44.09 hammer ``` ### Steps 1. Create a [GCP free tier](https://cloud.google.com/free/docs/free-cloud-features#free-tier) e2-micro VM instance in us-central1 (Iowa). 1. [Install Go](https://go.dev/doc/install) ```sh instance:~$ wget https://go.dev/dl/go1.23.0.linux-amd64.tar.gz instance:~$ sudo rm -rf /usr/local/go && sudo tar -C /usr/local -xzf go1.23.0.linux-amd64.tar.gz instance:~$ export PATH=$PATH:/usr/local/go/bin instance:~$ go version go version go1.23.0 linux/amd64 ``` 1. [Install Docker using the `apt` repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) 1. Install Git ```sh instance:~$ sudo apt-get install git -y -q ... instance:~$ git version git version 2.39.2 ``` 1. Clone the Tessera repository ```sh instance:~$ git clone https://github.com/transparency-dev/tessera.git Cloning into 'tessera'... ``` 1. Run `cmd/conformance/posix` via Docker compose ```sh instance:~/tessera$ sudo docker compose -f ./cmd/conformance/posix/docker/compose.yaml up ``` 1. Run `hammer` and get performance metrics ```sh hammer:~/tessera$ go run ./internal/hammer --log_public_key=example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx --log_url=http://localhost:2025 --max_read_ops=0 --num_writers=512 --max_write_ops=512 ``` transparency-dev-tessera-3cb22ee/storage/posix/README.md000066400000000000000000000062031511600621500232150ustar00rootroot00000000000000# Tessera on POSIX Filesystems This document describes the storage implementation for running Tessera on a POSIX-compliant filesystem. ## Overview POSIX provides for a small number of atomic operations on compliant filesystems. This design leverages those to safely maintain a Merkle tree log on disk, in a format which can be exposed directly via a read-only endpoint to clients of the log (for example, using `nginx` or similar). In contrast with some of other other storage backends, sequencing and integration of entries into the tree is synchronous. The implementation uses a `.state/` directory to coordinate operation. This directory does _not_ need to be visible to log clients, but it does not contain sensitive data and so it isn't a problem if it is made visible. ## Life of a Leaf In the description below, when we talk about writing to files - either appending or creating new ones, the _actual_ process used always follows the following pattern: 1. Create a temporary file on the same filesystem as the target location 1. If we're appending data, copy the contents of the prefix location into the temporary file 1. Write any new/additional data into the temporary file 1. Close the temporary file 1. Rename the temporary file into the target location. The final step in the dance above is atomic according to the POSIX spec, so in performing this sequence of actions we can avoid corrupt or partially written files being part of the tree. 1. Leaves are submitted by the binary built using Tessera via a call the storage's `Add` func. 1. The storage library batches these entries up in memory, and, after a configurable period of time has elapsed or the batch reaches a configurable size threshold, the batch is sequenced and appended to the tree: 1. An advisory lock is taken on `.state/treeState.lock` file. This helps prevent multiple frontends from stepping on each other, but isn't necesary for safety. 1. Flushed entries are assigned contiguous sequence numbers, and written out into entry bundle files. 1. Integrate newly added leaves into Merkle tree, and write tiles out as files. 1. Update `./state/treeState` file with the new size & root hash. 1. Asynchronously, at an interval determined by the `WithCheckpointInterval` option, the `checkpoint` file will be updated: 1. An advisory lock is taken on `.state/publish.lock` 1. If the last-modified date of the `checkpoint` file is older than the checkpoint update interval, a new checkpoint which commits to the latest tree state is produced and written to the `checkpoint` file. ## Filesystems This implementation has been somewhat tested on local `ext4` and `ZFS` filesystems, and on a distributed [CephFS](https://docs.ceph.com/en/reef/cephfs/) instance on GCP, in all cases with multiple personality binaries attempting to add new entries concurrently. Other POSIX compliant filesystems such as `XFS` _should_ work, but filesystems which do not offer strong POSIX compliance (e.g. `s3fs` or `NFS`) are unlikely to result in long term happiness. If in doubt, tools like https://github.com/saidsay-so/pjdfstest may help in determining whether a given filesystem is suitable. transparency-dev-tessera-3cb22ee/storage/posix/antispam/000077500000000000000000000000001511600621500235515ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/storage/posix/antispam/badger.go000066400000000000000000000276211511600621500253340ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // Package badger provides a Tessera persistent antispam driver based on // BadgerDB (https://github.com/hypermodeinc/badger), a high-performance // pure-go DB with KV support. package badger import ( "context" "encoding/binary" "errors" "fmt" "iter" "os" "sync/atomic" "time" "github.com/dgraph-io/badger/v4" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/client" "github.com/transparency-dev/tessera/internal/otel" "k8s.io/klog/v2" ) const ( DefaultMaxBatchSize = 1500 DefaultPushbackThreshold = 2048 ) var ( nextKey = []byte("@nextIdx") ) // AntispamOpts allows configuration of some tunable options. type AntispamOpts struct { // MaxBatchSize is the largest number of mutations permitted in a single BatchWrite operation when // updating the antispam index. // // Larger batches can enable (up to a point) higher throughput, but care should be taken not to // overload the Spanner instance. // // During testing, we've found that 1500 appears to offer maximum throughput when using Spanner instances // with 300 or more PU. Smaller deployments (e.g. 100 PU) will likely perform better with smaller batch // sizes of around 64. MaxBatchSize uint // PushbackThreshold allows configuration of when to start responding to Add requests with pushback due to // the antispam follower falling too far behind. // // When the antispam follower is at least this many entries behind the size of the locally integrated tree, // the antispam decorator will return tessera.ErrPushback for every Add request. PushbackThreshold uint } // NewAntispam returns an antispam driver which uses Badger to maintain a mapping between // previously seen entries and their assigned indices. // // Note that the storage for this mapping is entirely separate and unconnected to the storage used for // maintaining the Merkle tree. // // This functionality is experimental! func NewAntispam(ctx context.Context, badgerPath string, opts AntispamOpts) (*AntispamStorage, error) { if opts.MaxBatchSize == 0 { opts.MaxBatchSize = DefaultMaxBatchSize } if opts.PushbackThreshold == 0 { opts.PushbackThreshold = DefaultPushbackThreshold } // Open the Badger database located at badgerPath, it will be created if it doesn't exist. db, err := badger.Open(badger.DefaultOptions(badgerPath)) if err != nil { return nil, fmt.Errorf("failed to open badger: %v", err) } r := &AntispamStorage{ opts: opts, db: db, } go func() { ticker := time.NewTicker(1 * time.Minute) defer ticker.Stop() for { select { case <-ctx.Done(): return case <-ticker.C: } again: err := db.RunValueLogGC(0.7) if err == nil { goto again } } }() return r, nil } type AntispamStorage struct { opts AntispamOpts db *badger.DB // pushBack is used to prevent the follower from getting too far underwater. // Populate dynamically will set this to true/false based on how far behind the follower is from the // currently integrated tree size. // When pushBack is true, the decorator will start returning ErrPushback to all calls. pushBack atomic.Bool numLookups atomic.Uint64 numWrites atomic.Uint64 numHits atomic.Uint64 } // index returns the index (if any) previously associated with the provided hash func (d *AntispamStorage) index(ctx context.Context, h []byte) (*uint64, error) { _, span := tracer.Start(ctx, "tessera.antispam.badger.index") defer span.End() d.numLookups.Add(1) var idx *uint64 err := d.db.View(func(txn *badger.Txn) error { item, err := txn.Get(h) if err == badger.ErrKeyNotFound { span.AddEvent("tessera.miss") return nil } span.AddEvent("tessera.hit") d.numHits.Add(1) return item.Value(func(v []byte) error { i := binary.BigEndian.Uint64(v) idx = &i return nil }) }) return idx, err } // Decorator returns a function which will wrap an underlying Add delegate with // code to dedup against the stored data. func (d *AntispamStorage) Decorator() func(f tessera.AddFn) tessera.AddFn { return func(delegate tessera.AddFn) tessera.AddFn { return func(ctx context.Context, e *tessera.Entry) tessera.IndexFuture { ctx, span := tracer.Start(ctx, "tessera.antispam.badger.Add") defer span.End() if d.pushBack.Load() { span.AddEvent("tessera.pushback") // The follower is too far behind the currently integrated tree, so we're going to push back against // the incoming requests. // This should have two effects: // 1. The tree will cease growing, giving the follower a chance to catch up, and // 2. We'll stop doing lookups for each submission, freeing up Spanner CPU to focus on catching up. // // We may decide in the future that serving duplicate reads is more important than catching up as quickly // as possible, in which case we'd move this check down below the call to index. return func() (tessera.Index, error) { return tessera.Index{}, tessera.ErrPushbackAntispam } } idx, err := d.index(ctx, e.Identity()) if err != nil { return func() (tessera.Index, error) { return tessera.Index{}, err } } if idx != nil { return func() (tessera.Index, error) { return tessera.Index{Index: *idx, IsDup: true}, nil } } return delegate(ctx, e) } } } // Follower returns a follower which knows how to populate the antispam index. // // This implements tessera.Antispam. func (d *AntispamStorage) Follower(b func([]byte) ([][]byte, error)) tessera.Follower { f := &follower{ as: d, bundleHasher: b, } return f } // follower is a struct which knows how to populate the antispam storage with identity hashes // for entries in a log. type follower struct { as *AntispamStorage bundleHasher func([]byte) ([][]byte, error) } func (f *follower) Name() string { return "Badger antispam" } // Follow uses entry data from the log to populate the antispam storage. func (f *follower) Follow(ctx context.Context, lr tessera.LogReader) { errOutOfSync := errors.New("out-of-sync") t := time.NewTicker(time.Second) var ( next func() (client.Entry[[]byte], error, bool) stop func() curEntries [][]byte curIndex uint64 ) for { select { case <-ctx.Done(): return case <-t.C: } // logSize is the latest known size of the log we're following. // This will get initialised below, inside the loop. var logSize uint64 // Busy loop while there's work to be done for moreWork := true; moreWork; { err := f.as.db.Update(func(txn *badger.Txn) error { ctx, span := tracer.Start(ctx, "tessera.antispam.badger.FollowTxn") defer span.End() // Figure out the last entry we used to populate our antispam storage. var followFrom uint64 switch row, err := txn.Get(nextKey); { case errors.Is(err, badger.ErrKeyNotFound): // Ignore this as we're probably just running for the first time on a new DB. case err != nil: return fmt.Errorf("failed to get nextIdx: %v", err) default: if err := row.Value(func(val []byte) error { followFrom = binary.BigEndian.Uint64(val) return nil }); err != nil { return fmt.Errorf("failed to get nextIdx value: %v", err) } } span.SetAttributes(followFromKey.Int64(otel.Clamp64(followFrom))) var err error if followFrom >= logSize { // Our view of the log is out of date, update it logSize, err = lr.IntegratedSize(ctx) if err != nil { if errors.Is(err, os.ErrNotExist) { // The log probably just hasn't completed its first integration yet, so break out of here // and go back to sleep for a bit to avoid spamming errors into the log and scaring operators. moreWork = false return nil } return fmt.Errorf("populate: IntegratedSize(): %v", err) } switch { case followFrom > logSize: // Since we've got a stale view, there could be more work to do - loop and check without sleeping. moreWork = true return fmt.Errorf("followFrom %d > size %d", followFrom, logSize) case followFrom == logSize: // We're caught up, so unblock pushback and go back to sleep moreWork = false f.as.pushBack.Store(false) return nil default: // size > followFrom, so there's more work to be done! } } pushback := logSize-followFrom > uint64(f.as.opts.PushbackThreshold) span.SetAttributes(pushbackKey.Bool(pushback)) f.as.pushBack.Store(pushback) // If this is the first time around the loop we need to start the stream of entries now that we know where we want to // start reading from: if next == nil { span.AddEvent("Start streaming entries") sizeFn := func(_ context.Context) (uint64, error) { return logSize, nil } numFetchers := uint(10) next, stop = iter.Pull2(client.Entries(client.EntryBundles(ctx, numFetchers, sizeFn, lr.ReadEntryBundle, followFrom, logSize-followFrom), f.bundleHasher)) } if curIndex == followFrom && curEntries != nil { // Note that it's possible for Spanner to automatically retry transactions in some circumstances, when it does // it'll call this function again. // If the above condition holds, then we're in a retry situation and we must use the same data again rather // than continue reading entries which will take us out of sync. } else { bs := uint64(f.as.opts.MaxBatchSize) if r := logSize - followFrom; r < bs { bs = r } batch := make([][]byte, 0, bs) for i := range int(bs) { e, err, ok := next() if !ok { // The entry stream has ended so we'll need to start a new stream next time around the loop: next = nil break } if err != nil { return fmt.Errorf("entryReader.next: %v", err) } if wantIdx := followFrom + uint64(i); e.Index != wantIdx { klog.Infof("at %d, expected %d - out of sync", e.Index, wantIdx) // We're out of sync return errOutOfSync } batch = append(batch, e.Entry) } curEntries = batch curIndex = followFrom } // Now update the index. { for i, e := range curEntries { if _, err := txn.Get(e); err == badger.ErrKeyNotFound { b := make([]byte, 8) binary.BigEndian.PutUint64(b, curIndex+uint64(i)) if err := txn.Set(e, b); err != nil { return err } } } } numAdded := uint64(len(curEntries)) f.as.numWrites.Add(numAdded) // and update the follower state b := make([]byte, 8) binary.BigEndian.PutUint64(b, curIndex+numAdded) if err := txn.Set(nextKey, b); err != nil { return fmt.Errorf("failed to update follower state: %v", err) } return nil }) if err != nil { if err != errOutOfSync { klog.Errorf("Failed to commit antispam population tx: %v", err) } stop() next = nil continue } curEntries = nil } } } // EntriesProcessed returns the total number of log entries processed. func (f *follower) EntriesProcessed(ctx context.Context) (uint64, error) { var nextIdx uint64 err := f.as.db.View(func(txn *badger.Txn) error { switch item, err := txn.Get(nextKey); { case errors.Is(err, badger.ErrKeyNotFound): // Ignore this, we've just not done any following yet. return nil case err != nil: return fmt.Errorf("failed to read nextKey: %v", err) default: return item.Value(func(val []byte) error { nextIdx = binary.BigEndian.Uint64(val) return nil }) } }) return nextIdx, err } transparency-dev-tessera-3cb22ee/storage/posix/antispam/badger_test.go000066400000000000000000000136251511600621500263720ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package badger import ( "crypto/sha256" "testing" "time" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/testonly" "k8s.io/klog/v2" ) type testLookup struct { entryHash []byte wantNotFound bool } func TestAntispamStorage(t *testing.T) { for _, test := range []struct { name string opts AntispamOpts logEntries [][]byte lookupEntries []testLookup }{ { name: "roundtrip", logEntries: [][]byte{ []byte("one"), []byte("two"), []byte("three"), }, lookupEntries: []testLookup{ { entryHash: testIDHash([]byte("one")), }, { entryHash: testIDHash([]byte("two")), }, { entryHash: testIDHash([]byte("three")), }, { entryHash: testIDHash([]byte("nowhere to be found")), wantNotFound: true, }, }, }, } { t.Run(test.name, func(t *testing.T) { as, err := NewAntispam(t.Context(), t.TempDir(), test.opts) if err != nil { t.Fatalf("NewAntispam: %v", err) } fl, shutdown := testonly.NewTestLog(t, tessera.NewAppendOptions().WithCheckpointInterval(time.Second)) defer func() { if err := shutdown(t.Context()); err != nil { t.Logf("shutdown: %v", err) } }() f := as.Follower(testBundleHasher) go f.Follow(t.Context(), fl.LogReader) entryIndex := make(map[string]uint64) a := tessera.NewPublicationAwaiter(t.Context(), fl.LogReader.ReadCheckpoint, 100*time.Millisecond) for i, e := range test.logEntries { entry := tessera.NewEntry(e) f := fl.Appender.Add(t.Context(), entry) idx, _, err := a.Await(t.Context(), f) if err != nil { t.Fatalf("Await(%d): %v", i, err) } klog.Infof("%d == %x", i, entry.Identity()) entryIndex[string(testIDHash(e))] = idx.Index } for { time.Sleep(time.Second) pos, err := f.EntriesProcessed(t.Context()) if err != nil { t.Logf("EntriesProcessed: %v", err) continue } sz, err := fl.LogReader.IntegratedSize(t.Context()) if err != nil { t.Logf("IntegratedSize: %v", err) continue } klog.Infof("Wait for follower (%d) to catch up with tree (%d)", pos, sz) if pos >= sz { break } } for _, e := range test.lookupEntries { gotIndex, err := as.index(t.Context(), e.entryHash) if err != nil { t.Errorf("error looking up hash %x: %v", e.entryHash, err) } wantIndex := entryIndex[string(e.entryHash)] if gotIndex == nil { if !e.wantNotFound { t.Errorf("no index for hash %x, but expected index %d", e.entryHash, wantIndex) } continue } if *gotIndex != wantIndex { t.Errorf("got index %d, want %d from looking up hash %x", gotIndex, wantIndex, e.entryHash) } } }) } } func TestAntispamPushbackRecovers(t *testing.T) { for _, test := range []struct { name string opts AntispamOpts logEntries [][]byte }{ { name: "pushback", opts: AntispamOpts{ PushbackThreshold: 1, }, logEntries: [][]byte{ []byte("one"), []byte("two"), []byte("three"), }, }, } { t.Run(test.name, func(t *testing.T) { as, err := NewAntispam(t.Context(), t.TempDir(), test.opts) if err != nil { t.Fatalf("NewAntispam: %v", err) } fl, shutdown := testonly.NewTestLog(t, tessera.NewAppendOptions().WithCheckpointInterval(time.Second)) defer func() { if err := shutdown(t.Context()); err != nil { t.Logf("shutdown: %v", err) } }() f := as.Follower(testBundleHasher) entryIndex := make(map[string]uint64) a := tessera.NewPublicationAwaiter(t.Context(), fl.LogReader.ReadCheckpoint, 100*time.Millisecond) for i, e := range test.logEntries { entry := tessera.NewEntry(e) f := fl.Appender.Add(t.Context(), entry) idx, _, err := a.Await(t.Context(), f) if err != nil { t.Fatalf("Await(%d): %v", i, err) } klog.Infof("%d == %x", i, entry.Identity()) entryIndex[string(testIDHash(e))] = idx.Index } // Wait for entries te be integrated before we start the follower, so we know we'll hit the pushback condition go f.Follow(t.Context(), fl.LogReader) for { time.Sleep(time.Second) pos, err := f.EntriesProcessed(t.Context()) if err != nil { t.Logf("EntriesProcessed: %v", err) continue } sz, err := fl.LogReader.IntegratedSize(t.Context()) if err != nil { t.Logf("IntegratedSize: %v", err) continue } klog.Infof("Wait for follower (%d) to catch up with tree (%d)", pos, sz) if pos >= sz { break } } // Ensure that the follower gets itself _out_ of pushback mode once it's caught up. // We'll give the follower some time to do its thing and notice. // It runs onces a second, so this should be plenty of time. for i := range 5 { time.Sleep(time.Second) if !as.pushBack.Load() { t.Logf("Antispam caught up and out of pushback in %ds", i) return } } t.Fatalf("pushBack remains true after 5 seconds despite being caught up!") }) } } func testIDHash(d []byte) []byte { r := sha256.Sum256(d) return r[:] } func testBundleHasher(b []byte) ([][]byte, error) { bun := &api.EntryBundle{} err := bun.UnmarshalText(b) if err != nil { return nil, err } r := make([][]byte, len(bun.Entries)) for i, e := range bun.Entries { r[i] = testIDHash(e) } return r, err } transparency-dev-tessera-3cb22ee/storage/posix/antispam/otel.go000066400000000000000000000016511511600621500250460ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package badger import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" ) const name = "github.com/transparency-dev/tessera/storage/posix/antispam" var ( tracer = otel.Tracer(name) ) var ( followFromKey = attribute.Key("tessera.followFrom") pushbackKey = attribute.Key("tessera.pushback") ) transparency-dev-tessera-3cb22ee/storage/posix/file_ops.go000066400000000000000000000141771511600621500240760ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package posix import ( "errors" "fmt" "math/rand/v2" "os" "path/filepath" "strconv" "strings" "syscall" "k8s.io/klog/v2" ) const ( dirPerm = 0o755 filePerm = 0o644 ) // syncDir opens the specified directory and calls op before syncing and closing the handle on the directory. // // This dance ensures that the inode of the specified directory cannot be evicted from the kernel inode cache while // the operation is underway, and so any error which occurs while updating metadata about a file operation which happens // _within_ that directory is detected. // // This function is intended to be used by the other functions in this file. func syncDir(dir string, op func() error) (err error) { fd, err := os.OpenFile(dir, os.O_RDONLY|syscall.O_DIRECTORY, 0) if err != nil { return fmt.Errorf("failed to open %q: %w", dir, err) } defer func() { e := fd.Close() if err == nil { err = e } }() if err := op(); err != nil { return err } if err := fd.Sync(); err != nil { return fmt.Errorf("failed to sync %q: %w", dir, err) } return nil } // mkdirAll is a reimplementation of os.mkdirAll but where we fsync the parent directory/ies // we modify. func mkdirAll(name string, perm os.FileMode) error { name = strings.TrimSuffix(name, string(filepath.Separator)) if name == "" { return nil } // Finally, check and create the dir if necessary. dir := filepath.Dir(name) di, err := os.Lstat(name) switch { case errors.Is(err, syscall.ENOENT): // We'll see an ENOENT if there's a problem with a non-existant path element, so // we'll recurse and create the parent directory if necessary. // Don't return an error if someone else managed to get in and create the directory before us, though. if dir != "" { if err := mkdirAll(dir, perm); err != nil && !errors.Is(err, os.ErrExist) { return err } } // Once we've successfully created the parent element(s), we can drop through and // create the final entry in the requested path. fallthrough case errors.Is(err, os.ErrNotExist): return syncDir(dir, func() error { // We'll see ErrNotExist if the final entry in the requested path doesn't exist, // so we simply attempt to create it in here. if err := os.Mkdir(name, perm); err != nil { return fmt.Errorf("%q: %v", name, err) } return nil }) case err != nil: return fmt.Errorf("lstat %q: %v", name, err) case !di.IsDir(): return fmt.Errorf("%s is not a directory", name) default: return nil } } // createEx atomically creates a file at the given path containing the provided data, and syncs the // directory containing the newly created file. // // Returns an error if a file already exists at the specified location, or it's unable to fully write the // data & close the file. func createEx(name string, d []byte) error { dir := filepath.Dir(name) if err := mkdirAll(dir, dirPerm); err != nil { return fmt.Errorf("failed to make directory structure: %w", err) } return syncDir(dir, func() error { tmpName, err := createTemp(name, d) if err != nil { return fmt.Errorf("failed to create temp file: %v", err) } defer func() { if err := os.Remove(tmpName); err != nil { klog.Warningf("Failed to remove temporary file %q: %v", tmpName, err) } }() if err := os.Link(tmpName, name); err != nil { // Wrap the error here because we need to know if it's os.ErrExists at higher levels. return fmt.Errorf("failed to link temporary file to target %q: %w", name, err) } return nil }) } // overwrite atomically creates/overwrites a file at the given path containing the provided data, and syncs // the directory containing the overwritten/created file. func overwrite(name string, d []byte) error { dir := filepath.Dir(name) if err := mkdirAll(dir, dirPerm); err != nil { return fmt.Errorf("failed to make directory structure: %w", err) } return syncDir(dir, func() error { dir, _ := filepath.Split(name) if err := mkdirAll(dir, dirPerm); err != nil { return fmt.Errorf("failed to make entries directory structure: %w", err) } tmpName, err := createTemp(name, d) if err != nil { return fmt.Errorf("failed to create temp file: %v", err) } if err := os.Rename(tmpName, name); err != nil { return fmt.Errorf("failed to rename temporary file to target %q: %w", name, err) } return nil }) } // createTemp creates a new temporary file in the directory dir, with a name based on the provided prefix, // and writes the provided data to it. // // Multiple programs or goroutines calling CreateTemp simultaneously will not choose the same file. // It is the caller's responsibility to remove the file when it is no longer needed. // // Ths file data is written with O_SYNC, however the containing directory is NOT sync'd on the assumption // that this temporary file will be linked/renamed by the caller who will also sync the directory. func createTemp(prefix string, d []byte) (name string, err error) { try := 0 var f *os.File for { name = prefix + strconv.Itoa(int(rand.Int32())) f, err = os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL|os.O_SYNC, filePerm) if err == nil { break } else if os.IsExist(err) { if try++; try < 10000 { continue } return "", &os.PathError{Op: "createtemp", Path: prefix + "*", Err: os.ErrExist} } } defer func() { if errC := f.Close(); errC != nil && err == nil { err = errC } }() if n, err := f.Write(d); err != nil { return "", fmt.Errorf("failed to write to temporary file %q: %v", name, err) } else if l := len(d); n < l { return "", fmt.Errorf("short write on %q, %d < %d", name, n, l) } return name, nil } transparency-dev-tessera-3cb22ee/storage/posix/files.go000066400000000000000000001044141511600621500233720ustar00rootroot00000000000000// Copyright 2024 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package posix import ( "bytes" "context" "encoding/json" "errors" "fmt" "io" "io/fs" "net/http" "os" "path/filepath" "strconv" "sync" "syscall" "time" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/internal/fetcher" "github.com/transparency-dev/tessera/internal/migrate" "github.com/transparency-dev/tessera/internal/parse" storage "github.com/transparency-dev/tessera/storage/internal" "go.opentelemetry.io/otel/metric" "k8s.io/klog/v2" ) const ( // compatibilityVersion is the required version of the log state directory. // This should be bumped whenever a change is made that would break compatibility with old versions. // When this is bumped, ensure that the version file is only written when a new log is being // created. Currently, this version is written whenever it is missing in order to upgrade logs // that were created before we introduced this. compatibilityVersion = 1 // stateDir holds any private (but not secret) internal state needed to maintain/operate the log. stateDir = ".state" // gcStateFile contains the state of the garbage collection operations. gcStateFile = "gcState" // gcStateLock must be held when performing GC operations and updating the gcState file. gcStateLock = gcStateFile + ".lock" // publishLock must be held when checking/updating the published checkpoint. publishLock = "publish.lock" // treeStateFile contains the integrated (but not necessarily published) state of the tree. treeStateFile = "treeState" // treeStateLock must be held when integrating entries into the tree or writing to the treeState file. treeStateLock = treeStateFile + ".lock" minCheckpointInterval = 100 * time.Millisecond ) // Storage implements storage functions for a POSIX filesystem. // It leverages the POSIX atomic operations where needed. type Storage struct { mu sync.Mutex cfg Config } // appender implements the Tessera append lifecycle. type appender struct { s *Storage logStorage *logResourceStorage queue *storage.Queue curSize uint64 newCP func(context.Context, uint64, []byte) ([]byte, error) // May be nil for mirrored logs. cpUpdated chan struct{} } // logResourceStorage knows how to read and write tiled log resources via a // POSIX storage instance type logResourceStorage struct { s *Storage entriesPath func(uint64, uint8) string } // NewTreeFunc is the signature of a function which receives information about newly integrated trees. type NewTreeFunc func(size uint64, root []byte) error type Config struct { // HTTPClient will be used for outgoing HTTP requests. If unset, Tessera will use the net/http DefaultClient. HTTPClient *http.Client // Path is the path to a directory in which the log should be stored. Path string } // New creates a new POSIX storage. func New(ctx context.Context, cfg Config) (tessera.Driver, error) { if cfg.HTTPClient == nil { cfg.HTTPClient = http.DefaultClient } return &Storage{ cfg: cfg, }, nil } func (s *Storage) Appender(ctx context.Context, opts *tessera.AppendOptions) (*tessera.Appender, tessera.LogReader, error) { logStorage := &logResourceStorage{ s: s, entriesPath: opts.EntriesPath(), } a, lr, err := s.newAppender(ctx, logStorage, opts) if err != nil { return nil, nil, err } return &tessera.Appender{ Add: a.Add, }, lr, nil } func (s *Storage) newAppender(ctx context.Context, o *logResourceStorage, opts *tessera.AppendOptions) (*appender, tessera.LogReader, error) { if opts.CheckpointInterval() < minCheckpointInterval { return nil, nil, fmt.Errorf("requested CheckpointInterval (%v) is less than minimum permitted %v", opts.CheckpointInterval(), minCheckpointInterval) } a := &appender{ s: s, logStorage: o, cpUpdated: make(chan struct{}), newCP: opts.CheckpointPublisher(o, s.cfg.HTTPClient), } if err := a.initialise(ctx); err != nil { return nil, nil, err } a.queue = storage.NewQueue(ctx, opts.BatchMaxAge(), opts.BatchMaxSize(), a.sequenceBatch) go func(ctx context.Context, i time.Duration) { for { select { case <-ctx.Done(): return case <-a.cpUpdated: case <-time.After(i): } if err := a.publishCheckpoint(ctx, i, opts.CheckpointRepublishInterval()); err != nil { klog.Warningf("publishCheckpoint: %v", err) } } }(ctx, opts.CheckpointInterval()) if i := opts.GarbageCollectionInterval(); i > 0 { go a.garbageCollectorJob(ctx, i) } return a, a.logStorage, nil } // lockFile creates/opens a lock file at the specified path, and flocks it. // Once locked, the caller perform whatever operations are necessary, before // calling the returned function to unlock it. // // Note that a) this is advisory, and b) should use an non-API specified file // (e.g. .lock>) to avoid inherent brittleness of the `fcntrl` API // (*any* `Close` operation on this file (even if it's a different FD) from // this PID, or overwriting of the file by *any* process breaks the lock.) func (s *Storage) lockFile(ctx context.Context, p string) (func() error, error) { now := time.Now() p = filepath.Join(s.cfg.Path, stateDir, p) f, err := os.OpenFile(p, syscall.O_CREAT|syscall.O_RDWR|syscall.O_CLOEXEC, filePerm) if err != nil { return nil, err } flockT := syscall.Flock_t{ Type: syscall.F_WRLCK, Whence: io.SeekStart, Start: 0, Len: 0, } // Keep trying until we manage to get an answer without being interrupted. for { if err := syscall.FcntlFlock(f.Fd(), syscall.F_SETLKW, &flockT); err != syscall.EINTR { if err == nil { posixOpsHistogram.Record(ctx, time.Since(now).Milliseconds(), metric.WithAttributes(opNameKey.String(fmt.Sprintf("lock-%s", p)))) } return f.Close, err } } } // Add takes an entry and queues it for inclusion in the log. // Upon placing the entry in an in-memory queue to be sequenced, it returns a future that will // evaluate to either the sequence number assigned to this entry, or an error. // This future is made available when the entry is queued. Any further calls to Add after // this returns will guarantee that the later entry appears later in the log than any // earlier entries. Concurrent calls to Add are supported, but the order they are queued and // thus included in the log is non-deterministic. // // If the future resolves to a non-error state then it means that the entry is both // sequenced and integrated into the log. This means that a checkpoint will be available // that commits to this entry. // // It is recommended that the caller keeps the process running until all futures returned // by this method have successfully evaluated. Terminating earlier than this will likely // mean that some of the entries added are not committed to by a checkpoint, and thus are // not considered to be in the log. func (a *appender) Add(ctx context.Context, e *tessera.Entry) tessera.IndexFuture { return a.queue.Add(ctx, e) } func (l *logResourceStorage) ReadCheckpoint(_ context.Context) ([]byte, error) { r, err := os.ReadFile(filepath.Join(l.s.cfg.Path, layout.CheckpointPath)) if errors.Is(err, fs.ErrNotExist) { return r, os.ErrNotExist } return r, err } // ReadEntryBundle retrieves the Nth entries bundle for a log of the given size. func (l *logResourceStorage) ReadEntryBundle(ctx context.Context, index uint64, p uint8) ([]byte, error) { return fetcher.PartialOrFullResource(ctx, p, func(ctx context.Context, p uint8) ([]byte, error) { return os.ReadFile(filepath.Join(l.s.cfg.Path, l.entriesPath(index, p))) }) } func (l *logResourceStorage) ReadTile(ctx context.Context, level, index uint64, p uint8) ([]byte, error) { return fetcher.PartialOrFullResource(ctx, p, func(ctx context.Context, p uint8) ([]byte, error) { return os.ReadFile(filepath.Join(l.s.cfg.Path, layout.TilePath(level, index, p))) }) } func (l *logResourceStorage) IntegratedSize(ctx context.Context) (uint64, error) { size, _, err := l.s.readTreeState(ctx) return size, err } func (l *logResourceStorage) NextIndex(ctx context.Context) (uint64, error) { return l.IntegratedSize(ctx) } // sequenceBatch writes the entries from the provided batch into the entry bundle files of the log. // // This func starts filling entries bundles at the next available slot in the log, ensuring that the // sequenced entries are contiguous from the zeroth entry (i.e left-hand dense). // We try to minimise the number of partially complete entry bundles by writing entries in chunks rather // than one-by-one. func (a *appender) sequenceBatch(ctx context.Context, entries []*tessera.Entry) error { // Double locking: // - The mutex `Lock()` ensures that multiple concurrent calls to this function within a task are serialised. // - The POSIX `lockFile()` ensures that distinct tasks are serialised. a.s.mu.Lock() unlock, err := a.s.lockFile(ctx, treeStateLock) if err != nil { panic(err) } defer func() { if err := unlock(); err != nil { panic(err) } a.s.mu.Unlock() }() size, _, err := a.s.readTreeState(ctx) if err != nil { if !errors.Is(err, os.ErrNotExist) { return err } size = 0 } a.curSize = size klog.V(1).Infof("Sequencing from %d", a.curSize) if len(entries) == 0 { return nil } currTile := &bytes.Buffer{} seq := a.curSize bundleIndex, entriesInBundle := seq/layout.EntryBundleWidth, seq%layout.EntryBundleWidth if entriesInBundle > 0 { // If the latest bundle is partial, we need to read the data it contains in for our newer, larger, bundle. part, err := a.logStorage.ReadEntryBundle(ctx, bundleIndex, uint8(a.curSize%layout.EntryBundleWidth)) if err != nil { return err } if _, err := currTile.Write(part); err != nil { return fmt.Errorf("failed to write partial bundle into buffer: %v", err) } } writeBundle := func(bundleIndex uint64, partialSize uint8) error { return a.logStorage.writeBundle(ctx, bundleIndex, partialSize, currTile.Bytes()) } leafHashes := make([][]byte, 0, len(entries)) // Add new entries to the bundle for i, e := range entries { bundleData := e.MarshalBundleData(seq + uint64(i)) if _, err := currTile.Write(bundleData); err != nil { return fmt.Errorf("failed to write entry %d to currTile: %v", i, err) } leafHashes = append(leafHashes, e.LeafHash()) entriesInBundle++ if entriesInBundle == layout.EntryBundleWidth { // This bundle is full, so we need to write it out... // ... and prepare the next entry bundle for any remaining entries in the batch if err := writeBundle(bundleIndex, 0); err != nil { return err } bundleIndex++ entriesInBundle = 0 currTile = &bytes.Buffer{} } } // If we have a partial bundle remaining once we've added all the entries from the batch, // this needs writing out too. if entriesInBundle > 0 { // This check should be redundant since this is [currently] checked above, but an overflow around the uint8 below could // potentially be bad news if that check was broken/defeated as we'd be writing invalid bundle data, so do a belt-and-braces // check and bail if need be. if entriesInBundle > layout.EntryBundleWidth { return fmt.Errorf("logic error: entriesInBundle(%d) > max bundle size %d", entriesInBundle, layout.EntryBundleWidth) } if err := writeBundle(bundleIndex, uint8(entriesInBundle)); err != nil { return err } } // For simplicity, in-line the integration of these new entries into the Merkle structure too. // If this is broken out into an async process, we'll need to update the implementation of NextIndex, too. newSize, newRoot, err := doIntegrate(ctx, seq, leafHashes, a.logStorage) if err != nil { klog.Errorf("Integrate failed: %v", err) return err } if err := a.s.writeTreeState(ctx, newSize, newRoot); err != nil { return fmt.Errorf("failed to write new tree state: %v", err) } // Notify that we know for sure there's a new checkpoint, but don't block if there's already // an outstanding notification in the channel. select { case a.cpUpdated <- struct{}{}: default: } return nil } // doIntegrate handles integrating new leaf hashes into the log, and returns the new state. func doIntegrate(ctx context.Context, fromSeq uint64, leafHashes [][]byte, ls *logResourceStorage) (uint64, []byte, error) { getTiles := func(ctx context.Context, tileIDs []storage.TileID, treeSize uint64) ([]*api.HashTile, error) { n, err := ls.readTiles(ctx, tileIDs, treeSize) if err != nil { return nil, fmt.Errorf("getTiles: %w", err) } return n, nil } newSize, newRoot, tiles, err := storage.Integrate(ctx, getTiles, fromSeq, leafHashes) if err != nil { klog.Errorf("Integrate: %v", err) return 0, nil, fmt.Errorf("error in Integrate: %v", err) } for k, v := range tiles { if err := ls.storeTile(ctx, uint64(k.Level), k.Index, newSize, v); err != nil { return 0, nil, fmt.Errorf("failed to set tile(%v): %v", k, err) } } klog.V(1).Infof("New tree: %d, %x", newSize, newRoot) return newSize, newRoot, nil } func (lrs *logResourceStorage) readTiles(ctx context.Context, tileIDs []storage.TileID, treeSize uint64) ([]*api.HashTile, error) { r := make([]*api.HashTile, 0, len(tileIDs)) for _, id := range tileIDs { t, err := lrs.readTile(ctx, id.Level, id.Index, layout.PartialTileSize(id.Level, id.Index, treeSize)) if err != nil { return nil, err } r = append(r, t) } return r, nil } // readTile returns the parsed tile at the given tile-level and tile-index. // If no complete tile exists at that location, it will attempt to find a // partial tile for the given tree size at that location. func (lrs *logResourceStorage) readTile(ctx context.Context, level, index uint64, p uint8) (*api.HashTile, error) { now := time.Now() t, err := lrs.ReadTile(ctx, level, index, p) if err != nil { if errors.Is(err, os.ErrNotExist) { // We'll signal to higher levels that it wasn't found by retuning a nil for this tile. return nil, nil } return nil, err } var tile api.HashTile if err := tile.UnmarshalText(t); err != nil { return nil, fmt.Errorf("failed to parse tile: %w", err) } posixOpsHistogram.Record(ctx, time.Since(now).Milliseconds(), metric.WithAttributes(opNameKey.String("readTile"))) return &tile, nil } // storeTile writes a tile out to disk. // Fully populated tiles are stored at the path corresponding to the level & // index parameters, partially populated (i.e. right-hand edge) tiles are // stored with a .xx suffix where xx is the number of "tile leaves" in hex. func (lrs *logResourceStorage) storeTile(ctx context.Context, level, index, logSize uint64, tile *api.HashTile) error { tileSize := uint64(len(tile.Nodes)) klog.V(2).Infof("StoreTile: level %d index %x ts: %x", level, index, tileSize) if tileSize == 0 || tileSize > layout.TileWidth { return fmt.Errorf("tileSize %d must be > 0 and <= %d", tileSize, layout.TileWidth) } t, err := tile.MarshalText() if err != nil { return fmt.Errorf("failed to marshal tile: %w", err) } return lrs.writeTile(ctx, level, index, layout.PartialTileSize(level, index, logSize), t) } func (lrs *logResourceStorage) writeTile(ctx context.Context, level, index uint64, partial uint8, t []byte) error { now := time.Now() tPath := layout.TilePath(level, index, partial) if err := lrs.s.createOverwrite(tPath, t); err != nil { return err } if partial == 0 { partials, err := filepath.Glob(fmt.Sprintf("%s.p/*", tPath)) if err != nil { return fmt.Errorf("failed to list partial tiles for clean up; %w", err) } // Clean up old partial tiles by symlinking them to the new full tile. for _, p := range partials { klog.V(2).Infof("relink partial %s to %s", p, tPath) // We have to do a little dance here to get POSIX atomicity: // 1. Create a new temporary symlink to the full tile // 2. Rename the temporary symlink over the top of the old partial tile tmp := fmt.Sprintf("%s.link", tPath) _ = os.Remove(tmp) if err := os.Symlink(tPath, tmp); err != nil { return fmt.Errorf("failed to create temp link to full tile: %w", err) } if err := os.Rename(tmp, p); err != nil { return fmt.Errorf("failed to rename temp link over partial tile: %w", err) } } } posixOpsHistogram.Record(ctx, time.Since(now).Milliseconds(), metric.WithAttributes(opNameKey.String("writeTile"))) return nil } // writeBundle takes care of writing out the serialised entry bundle file. func (lrs *logResourceStorage) writeBundle(_ context.Context, index uint64, partial uint8, bundle []byte) error { bf := lrs.entriesPath(index, partial) if err := lrs.s.createOverwrite(bf, bundle); err != nil { if !errors.Is(err, os.ErrExist) { return err } } return nil } // initialise ensures that the storage location is valid by loading the checkpoint from this location, or // creating a zero-sized one if it doesn't already exist. func (a *appender) initialise(ctx context.Context) error { // Idempotent: If folder exists, nothing happens. if err := mkdirAll(filepath.Join(a.s.cfg.Path, stateDir), dirPerm); err != nil { return fmt.Errorf("failed to create log directory: %q", err) } // Double locking: // - The mutex `Lock()` ensures that multiple concurrent calls to this function within a task are serialised. // - The POSIX `lockFile()` ensures that distinct tasks are serialised. a.s.mu.Lock() unlock, err := a.s.lockFile(ctx, treeStateLock) if err != nil { panic(err) } defer func() { if err := unlock(); err != nil { panic(err) } a.s.mu.Unlock() }() if err := a.s.ensureVersion(compatibilityVersion); err != nil { return err } curSize, _, err := a.s.readTreeState(ctx) if err != nil { if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to load checkpoint for log: %v", err) } // Create the directory structure and write out an empty checkpoint klog.Infof("Initializing directory for POSIX log at %q (this should only happen ONCE per log!)", a.s.cfg.Path) if err := a.s.writeTreeState(ctx, 0, rfc6962.DefaultHasher.EmptyRoot()); err != nil { return fmt.Errorf("failed to write tree-state checkpoint: %v", err) } if a.newCP != nil { if err := a.publishCheckpoint(ctx, 0, 0); err != nil { return fmt.Errorf("failed to publish checkpoint: %v", err) } } return nil } a.curSize = curSize return nil } type treeState struct { Size uint64 `json:"size"` Root []byte `json:"root"` } // ensureVersion will fail if the compatibility version stored in the state directory // is not the expected version. If no file exists, then it is created with the expected version. func (s *Storage) ensureVersion(version uint16) error { versionFile := filepath.Join(stateDir, "version") if _, err := s.stat(versionFile); errors.Is(err, os.ErrNotExist) { klog.V(1).Infof("No version file exists, creating") data := fmt.Appendf(nil, "%d", version) if err := s.createExclusive(versionFile, data); err != nil { return fmt.Errorf("failed to create version file: %v", err) } return nil } else if err != nil { return fmt.Errorf("stat(%s): %v", versionFile, err) } data, err := s.readAll(versionFile) if err != nil { return fmt.Errorf("failed to read version file: %v", err) } parsed, err := strconv.ParseUint(string(data), 10, 16) if err != nil { return fmt.Errorf("failed to parse version: %v", err) } if got, want := uint16(parsed), version; got != want { return fmt.Errorf("wanted version %d but found %d", want, got) } return nil } // writeTreeState stores the current tree size and root hash on disk. func (s *Storage) writeTreeState(ctx context.Context, size uint64, root []byte) error { now := time.Now() raw, err := json.Marshal(treeState{Size: size, Root: root}) if err != nil { return fmt.Errorf("error in Marshal: %v", err) } if err := s.createOverwrite(filepath.Join(stateDir, treeStateFile), raw); err != nil { return fmt.Errorf("failed to create/overwrite private tree state file: %w", err) } posixOpsHistogram.Record(ctx, time.Since(now).Milliseconds(), metric.WithAttributes(opNameKey.String("writeTreeState"))) return nil } // readTreeState reads and returns the currently stored tree state. func (s *Storage) readTreeState(ctx context.Context) (uint64, []byte, error) { now := time.Now() p := filepath.Join(s.cfg.Path, stateDir, treeStateFile) raw, err := os.ReadFile(p) if err != nil { return 0, nil, fmt.Errorf("error in ReadFile(%q): %w", p, err) } ts := &treeState{} if err := json.Unmarshal(raw, ts); err != nil { return 0, nil, fmt.Errorf("error in Unmarshal: %v", err) } posixOpsHistogram.Record(ctx, time.Since(now).Milliseconds(), metric.WithAttributes(opNameKey.String("readTreeState"))) return ts.Size, ts.Root, nil } // publishCheckpoint checks whether the currently published checkpoint (if any) is more than // minStaleness old, and, if so, creates and published a fresh checkpoint from the current // stored tree state. func (a *appender) publishCheckpoint(ctx context.Context, minStalenessActive, minStalenessRepub time.Duration) error { now := time.Now() // Lock the destination "published" checkpoint location: unlock, err := a.s.lockFile(ctx, publishLock) if err != nil { return fmt.Errorf("lockFile(%s): %v", publishLock, err) } defer func() { if err := unlock(); err != nil { klog.Warningf("unlock(%s): %v", publishLock, err) } }() var publishedAge time.Duration var publishedSize uint64 cpExists := true info, err := a.s.stat(layout.CheckpointPath) if errors.Is(err, os.ErrNotExist) { klog.V(1).Infof("No checkpoint exists, publishing") cpExists = false } else if err != nil { return fmt.Errorf("stat(%s): %v", layout.CheckpointPath, err) } else { publishedAge = time.Since(info.ModTime()) if publishedAge < minStalenessActive { klog.V(1).Infof("publishCheckpoint: skipping publish because previous checkpoint published %v ago, less than %v", publishedAge, minStalenessActive) return nil } publishedSize, err = a.publishedSize(ctx) if err != nil { klog.V(1).Infof("publishCheckpoint: skipping publish because unable to determine previously published size: %v", err) return err } } size, root, err := a.s.readTreeState(ctx) if err != nil { return fmt.Errorf("readTreeState: %v", err) } if cpExists && size == publishedSize { if minStalenessRepub == 0 || publishedAge < minStalenessRepub { klog.V(1).Infof("publishCheckpoint: skipping publish because tree hasn't grown and previous checkpoint is too recent") return nil } } cpRaw, err := a.newCP(ctx, size, root) if err != nil { return fmt.Errorf("newCP: %v", err) } if err := a.s.createOverwrite(layout.CheckpointPath, cpRaw); err != nil { return fmt.Errorf("createOverwrite(%s): %v", layout.CheckpointPath, err) } klog.V(2).Infof("Published latest checkpoint: %d, %x", size, root) posixOpsHistogram.Record(ctx, time.Since(now).Milliseconds(), metric.WithAttributes(opNameKey.String("publishCheckpoint"))) return nil } // publishedSize returns the size of tree that the currently published checkpoint, if any, commits to. // // If there is no currently published checkpoint zero will be returned without error. func (a *appender) publishedSize(ctx context.Context) (uint64, error) { cp, err := a.logStorage.ReadCheckpoint(ctx) if err != nil { if errors.Is(err, os.ErrNotExist) { return 0, nil } return 0, fmt.Errorf("failed to read published checkpoint: %v", err) } _, pubSize, _, err := parse.CheckpointUnsafe(cp) if err != nil { return 0, fmt.Errorf("failed to parse published checkpoint: %v", err) } return pubSize, nil } // garbageCollectorJob is a long-running function which handles the removal of obsolete partial tiles // and entry bundles. // Blocks until ctx is done. func (a *appender) garbageCollectorJob(ctx context.Context, i time.Duration) { t := time.NewTicker(i) defer t.Stop() // Entirely arbitrary number. maxBundlesPerRun := uint(100) for { select { case <-ctx.Done(): return case <-t.C: } // Figure out the size of the latest published checkpoint - we can't be removing partial tiles implied by // that checkpoint just because we've done an integration and know about a larger (but as yet unpublished) // checkpoint! pubSize, err := a.publishedSize(ctx) if err != nil { klog.Warningf("GarbageCollect: %v", err) continue } if err := a.s.garbageCollect(ctx, pubSize, maxBundlesPerRun, a.logStorage.entriesPath); err != nil { klog.Warningf("GarbageCollect failed: %v", err) continue } } } // gcState represents a snapshot of how much of the log tree has been garbage collected. // This state structure is serialized into a private (but not sensitive) file in the log's .state directory. type gcState struct { FromSize uint64 `json:"fromSize"` } // writeGCState stores the high water mark below which garbage collection has successfully completed. func (s *Storage) writeGCState(size uint64) error { raw, err := json.Marshal(gcState{FromSize: size}) if err != nil { return fmt.Errorf("error in Marshal: %v", err) } if err := s.createOverwrite(filepath.Join(stateDir, gcStateFile), raw); err != nil { return fmt.Errorf("failed to create/overwrite private GC state file: %w", err) } return nil } // readGCState reads and returns the currently stored GC state, if any. // // If no GC state is stored, no GC run has completed successfully, so zero is returned to indicate // that GC should start from the beginning of the log. func (s *Storage) readGCState() (uint64, error) { p := filepath.Join(s.cfg.Path, stateDir, gcStateFile) raw, err := os.ReadFile(p) if err != nil { if errors.Is(err, os.ErrNotExist) { // gcState file doesn't exist yet - we've probably just not completed a GC run before so start from index 0. return 0, nil } return 0, fmt.Errorf("error in ReadFile(%q): %w", p, err) } gs := &gcState{} if err := json.Unmarshal(raw, gs); err != nil { return 0, fmt.Errorf("error in Unmarshal: %v", err) } return gs.FromSize, nil } func (s *Storage) garbageCollect(ctx context.Context, treeSize uint64, maxBundles uint, entriesPath func(uint64, uint8) string) error { // Lock the gc location: unlock, err := s.lockFile(ctx, gcStateLock) if err != nil { return fmt.Errorf("lockFile(%s): %v", gcStateLock, err) } defer func() { if err := unlock(); err != nil { klog.Warningf("unlock(%s): %v", gcStateLock, err) } }() fromSize, err := s.readGCState() if err != nil { return fmt.Errorf("readGCState: %v", err) } if fromSize == treeSize { // Nothing to do, nothing done. return nil } d := uint(0) // GC the tree in "vertical" chunks defined by entry bundles. for ri := range layout.Range(fromSize, treeSize-fromSize, treeSize) { // Only known-full bundles are in-scope for for GC, so exit if the current bundle is partial or // we've reached our limit of chunks. if ri.Partial > 0 || d > maxBundles { break } // GC any partial versions of the entry bundle itself and the tile which sits immediately above it. if err := s.removeDirAll(entriesPath(ri.Index, 0) + ".p/"); err != nil { return err } if err := s.removeDirAll(layout.TilePath(0, ri.Index, 0) + ".p/"); err != nil { return err } fromSize += uint64(ri.N) d++ // Now consider (only) the part of the tree which sits above the bundle. // We'll walk up the parent tiles for as a long as we're tracing the right-hand // edge of a perfect subtree. // This gives the property we'll only visit each parent tile once, rather than up to 256 times. pL, pIdx := uint64(0), ri.Index for isLastLeafInParent(pIdx) { // Move our coordinates up to the parent pL, pIdx = pL+1, pIdx>>layout.TileHeight // GC any partial versions of the parent tile. if err := s.removeDirAll(layout.TilePath(pL, pIdx, 0) + ".p/"); err != nil { return err } } } if err := s.writeGCState(fromSize); err != nil { return fmt.Errorf("writeGCState: %v", err) } return nil } // isLastLeafInParent returns true if a tile with the provided index is the final child node of a // (hypothetical) full parent tile. func isLastLeafInParent(i uint64) bool { return i%layout.TileWidth == layout.TileWidth-1 } // createExclusive atomically creates a file at the given path, relative to the root of the log, containing the provided data. // // It will error if a file already exists at the specified location, or it's unable to fully write the // data & close the file. func (s *Storage) createExclusive(p string, d []byte) error { return createEx(filepath.Join(s.cfg.Path, p), d) } // createOverwrite atomically creates or overwrites a file at the given path with the provided data. func (s *Storage) createOverwrite(p string, d []byte) error { return overwrite(filepath.Join(s.cfg.Path, p), d) } func (s *Storage) readAll(p string) ([]byte, error) { p = filepath.Join(s.cfg.Path, p) return os.ReadFile(p) } // stat returns os.Stat info for the speficied file relative to the log root. func (s *Storage) stat(p string) (os.FileInfo, error) { p = filepath.Join(s.cfg.Path, p) return os.Stat(p) } // removeDirAll removes the named directory and anything it contains. // The provided path is interpreted relative to the log root. func (s *Storage) removeDirAll(p string) error { p = filepath.Join(s.cfg.Path, p) klog.V(3).Infof("rm %s", p) if err := os.RemoveAll(p); err != nil && !errors.Is(err, os.ErrNotExist) { return err } return nil } // MigrationWriter creates a new POSIX storage for the MigrationTarget lifecycle mode. func (s *Storage) MigrationWriter(ctx context.Context, opts *tessera.MigrationOptions) (migrate.MigrationWriter, tessera.LogReader, error) { r := &MigrationStorage{ s: s, logStorage: &logResourceStorage{ entriesPath: opts.EntriesPath(), s: s, }, bundleHasher: opts.LeafHasher(), } if err := r.initialise(ctx); err != nil { return nil, nil, err } return r, r.logStorage, nil } // MigrationStorgage implements the tessera.MigrationTarget lifecycle contract. type MigrationStorage struct { s *Storage logStorage *logResourceStorage bundleHasher func(entryBundle []byte) ([][]byte, error) curSize uint64 } var _ migrate.MigrationWriter = &MigrationStorage{} func (m *MigrationStorage) AwaitIntegration(ctx context.Context, sourceSize uint64) ([]byte, error) { t := time.NewTicker(time.Second) defer t.Stop() for { select { case <-ctx.Done(): return nil, ctx.Err() case <-t.C: } if err := m.buildTree(ctx, sourceSize); err != nil { klog.Warningf("buildTree: %v", err) } s, r, err := m.s.readTreeState(ctx) if err != nil { klog.Warningf("readTreeState: %v", err) } if s == sourceSize { return r, nil } } } func (m *MigrationStorage) initialise(ctx context.Context) error { // Idempotent: If folder exists, nothing happens. if err := mkdirAll(filepath.Join(m.s.cfg.Path, stateDir), dirPerm); err != nil { return fmt.Errorf("failed to create log directory: %q", err) } // Double locking: // - The mutex `Lock()` ensures that multiple concurrent calls to this function within a task are serialised. // - The POSIX `lockFile()` ensures that distinct tasks are serialised. m.s.mu.Lock() unlock, err := m.s.lockFile(ctx, treeStateLock) if err != nil { panic(err) } defer func() { if err := unlock(); err != nil { panic(err) } m.s.mu.Unlock() }() if err := m.s.ensureVersion(compatibilityVersion); err != nil { return err } curSize, _, err := m.s.readTreeState(ctx) if err != nil { if !errors.Is(err, os.ErrNotExist) { return fmt.Errorf("failed to load checkpoint for log: %v", err) } // Create the directory structure and write out an empty checkpoint klog.Infof("Initializing directory for POSIX log at %q (this should only happen ONCE per log!)", m.s.cfg.Path) if err := m.s.writeTreeState(ctx, 0, rfc6962.DefaultHasher.EmptyRoot()); err != nil { return fmt.Errorf("failed to write tree-state checkpoint: %v", err) } return nil } m.curSize = curSize return nil } func (m *MigrationStorage) SetEntryBundle(ctx context.Context, index uint64, partial uint8, bundle []byte) error { return m.logStorage.writeBundle(ctx, index, partial, bundle) } func (m *MigrationStorage) IntegratedSize(ctx context.Context) (uint64, error) { sz, _, err := m.s.readTreeState(ctx) return sz, err } func (m *MigrationStorage) buildTree(ctx context.Context, targetSize uint64) error { // Double locking: // - The mutex `Lock()` ensures that multiple concurrent calls to this function within a task are serialised. // - The POSIX `lockFile()` ensures that distinct tasks are serialised. m.s.mu.Lock() unlock, err := m.s.lockFile(ctx, treeStateLock) if err != nil { panic(err) } defer func() { if err := unlock(); err != nil { panic(err) } m.s.mu.Unlock() }() size, _, err := m.s.readTreeState(ctx) if err != nil { if !errors.Is(err, os.ErrNotExist) { return err } size = 0 } m.curSize = size klog.V(1).Infof("Building from %d", m.curSize) lh, err := m.fetchLeafHashes(ctx, size, targetSize, targetSize) if err != nil { if errors.Is(err, os.ErrNotExist) { // We just don't have the bundle yet. // Bail quietly and the caller can retry. klog.V(1).Infof("fetchLeafHashes(%d, %d): %v", size, targetSize, err) return nil } return fmt.Errorf("fetchLeafHashes(%d, %d): %v", size, targetSize, err) } newSize, newRoot, err := doIntegrate(ctx, size, lh, m.logStorage) if err != nil { return fmt.Errorf("doIntegrate(%d, ...): %v", size, err) } if err := m.s.writeTreeState(ctx, newSize, newRoot); err != nil { return fmt.Errorf("failed to write new tree state: %v", err) } return nil } func (m *MigrationStorage) fetchLeafHashes(ctx context.Context, from, to, sourceSize uint64) ([][]byte, error) { const maxBundles = 300 lh := make([][]byte, 0, maxBundles) n := 0 for ri := range layout.Range(from, to, sourceSize) { b, err := m.logStorage.ReadEntryBundle(ctx, ri.Index, ri.Partial) if err != nil { return nil, fmt.Errorf("ReadEntryBundle(%d.%d): %w", ri.Index, ri.Partial, err) } bh, err := m.bundleHasher(b) if err != nil { return nil, fmt.Errorf("bundleHasherFunc for bundle index %d: %v", ri.Index, err) } lh = append(lh, bh[ri.First:ri.First+ri.N]...) n++ if n >= maxBundles { break } } return lh, nil } transparency-dev-tessera-3cb22ee/storage/posix/files_test.go000066400000000000000000000336431511600621500244360ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package posix import ( "bytes" "context" "errors" "fmt" "net/http" "os" "path/filepath" "strings" "testing" "time" "github.com/transparency-dev/merkle/rfc6962" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/api" "github.com/transparency-dev/tessera/api/layout" "github.com/transparency-dev/tessera/fsck" "golang.org/x/mod/sumdb/note" ) func TestGarbageCollect(t *testing.T) { ctx := t.Context() batchSize := uint64(60000) integrateEvery := uint64(31343) s := &Storage{ cfg: Config{ HTTPClient: http.DefaultClient, Path: t.TempDir(), }, } sk, vk := mustGenerateKeys(t) opts := tessera.NewAppendOptions(). WithCheckpointInterval(1200*time.Millisecond). WithBatching(uint(batchSize), 100*time.Millisecond). // Disable GC so we can manually invoke below. WithGarbageCollectionInterval(time.Duration(0)). WithCheckpointSigner(sk) logStorage := &logResourceStorage{ s: s, entriesPath: opts.EntriesPath(), } appender, lr, err := s.newAppender(ctx, logStorage, opts) if err != nil { t.Fatalf("Appender: %v", err) } if err := appender.publishCheckpoint(ctx, 0, 0); err != nil { t.Fatalf("publishCheckpoint: %v", err) } // Build a reasonably-sized tree with a bunch of partial resouces present, and wait for // it to be published. treeSize := uint64(256 * 384) a := tessera.NewPublicationAwaiter(ctx, lr.ReadCheckpoint, 100*time.Millisecond) // grow and garbage collect the tree several times to check continued correct operation over lifetime of the log for size := uint64(0); size < treeSize; { t.Logf("Adding entries from %d", size) for range batchSize { f := appender.Add(ctx, tessera.NewEntry(fmt.Appendf(nil, "entry %d", size))) if size%integrateEvery == 0 { t.Logf("Awaiting entry %d", size) if _, _, err := a.Await(ctx, f); err != nil { t.Fatalf("Await: %v", err) } } size++ } t.Logf("Awaiting tree at size %d", size) if _, _, err := a.Await(ctx, func() (tessera.Index, error) { return tessera.Index{Index: size - 1}, nil }); err != nil { t.Fatalf("Await final tree: %v", err) } t.Logf("Running GC at size %d", size) if err := s.garbageCollect(ctx, size, 1000, appender.logStorage.entriesPath); err != nil { t.Fatalf("garbageCollect: %v", err) } // Compare any remaining partial resources to the list of places // we'd expect them to be, given the tree size. wantPartialPrefixes := make(map[string]struct{}) for _, p := range expectedPartialPrefixes(size, appender.logStorage.entriesPath) { wantPartialPrefixes[p] = struct{}{} } allPartialDirs, err := findAllPartialDirs(t, s.cfg.Path) if err != nil { t.Fatalf("findAllPartials: %v", err) } for k := range allPartialDirs { if _, ok := wantPartialPrefixes[k]; !ok { t.Errorf("Found unwanted partial: %s", k) } } } // And finally, for good measure, assert that all the resources implied by the log's checkpoint // are present. f := fsck.New(vk.Name(), vk, lr, defaultMerkleLeafHasher, fsck.Opts{N: 1}) if err := f.Check(ctx); err != nil { t.Fatalf("FSCK failed: %v", err) } } func TestGarbageCollectOption(t *testing.T) { batchSize := uint64(60000) integrateEvery := uint64(31343) garbageCollectionInterval := 100 * time.Millisecond for _, test := range []struct { name string withCTLayout bool withGarbageCollectionInterval time.Duration }{ { name: "on", withGarbageCollectionInterval: garbageCollectionInterval, withCTLayout: false, }, { name: "on-ct", withGarbageCollectionInterval: garbageCollectionInterval, withCTLayout: true, }, { name: "off", withGarbageCollectionInterval: time.Duration(0), withCTLayout: false, }, } { t.Run(test.name, func(t *testing.T) { ctx := t.Context() s := &Storage{ cfg: Config{ HTTPClient: http.DefaultClient, Path: t.TempDir(), }, } sk, vk := mustGenerateKeys(t) opts := tessera.NewAppendOptions(). WithCheckpointInterval(1200*time.Millisecond). WithBatching(uint(batchSize), 100*time.Millisecond). WithGarbageCollectionInterval(test.withGarbageCollectionInterval). WithCheckpointSigner(sk) if test.withCTLayout { opts.WithCTLayout() } logStorage := &logResourceStorage{ s: s, entriesPath: opts.EntriesPath(), } appender, lr, err := s.newAppender(ctx, logStorage, opts) if err != nil { t.Fatalf("Appender: %v", err) } if err := appender.publishCheckpoint(ctx, 0, 0); err != nil { t.Fatalf("publishCheckpoint: %v", err) } // Build a reasonably-sized tree with a bunch of partial resouces present, and wait for // it to be published. treeSize := uint64(256 * 384) a := tessera.NewPublicationAwaiter(ctx, lr.ReadCheckpoint, 100*time.Millisecond) wantPartialPrefixes := make(map[string]struct{}) // Grow the tree several times to check continued correct operation over lifetime of the log. // Let garbage collection happen in the background. for size := uint64(0); size < treeSize; { t.Logf("Adding entries from %d", size) for range batchSize { f := appender.Add(ctx, tessera.NewEntry(fmt.Appendf(nil, "entry %d", size))) if size%integrateEvery == 0 { t.Logf("Awaiting entry %d", size) if _, _, err := a.Await(ctx, f); err != nil { t.Fatalf("Await: %v", err) } // If garbage collection is off, we want partial tiles and bundles to stick around. if test.withGarbageCollectionInterval == time.Duration(0) { for _, p := range expectedPartialPrefixes(size, appender.logStorage.entriesPath) { wantPartialPrefixes[p] = struct{}{} } } } size++ } t.Logf("Awaiting tree at size %d", size) if _, _, err := a.Await(ctx, func() (tessera.Index, error) { return tessera.Index{Index: size - 1}, nil }); err != nil { t.Fatalf("Await final tree: %v", err) } // Leave a bit of time for Garbage Collection to run. time.Sleep(3 * garbageCollectionInterval) // Compare any remaining partial resources to the list of places // we'd expect them to be, given the tree size. // Regardless of whether garbage collection is on, partial tiles corresponding to the last // checkpoint should alway be here. for _, p := range expectedPartialPrefixes(size, appender.logStorage.entriesPath) { wantPartialPrefixes[p] = struct{}{} } allPartialDirs, err := findAllPartialDirs(t, s.cfg.Path) if err != nil { t.Fatalf("findAllPartials: %v", err) } // If gargabe collection is on, no partial tiles other than the ones we expect should be // present. for k := range allPartialDirs { if _, ok := wantPartialPrefixes[k]; !ok && test.withGarbageCollectionInterval > 0 { t.Errorf("Found unwanted partial: %s", k) } delete(wantPartialPrefixes, k) } for k := range wantPartialPrefixes { t.Errorf("Did not find expected partial: %s", k) } } // And finally, for good measure, assert that all the resources implied by the log's checkpoint // are present. f := fsck.New(vk.Name(), vk, lr, defaultMerkleLeafHasher, fsck.Opts{N: 1}) if err := f.Check(ctx); err != nil { t.Fatalf("FSCK failed: %v", err) } }) } } func TestPublishTree(t *testing.T) { for _, test := range []struct { name string publishInterval time.Duration republishInterval time.Duration attempts []time.Duration growTree bool wantUpdates int }{ { name: "publish: works ok", publishInterval: 100 * time.Millisecond, attempts: []time.Duration{1 * time.Second}, growTree: true, wantUpdates: 1, }, { name: "publish: too soon, skip update", publishInterval: 10 * time.Second, growTree: true, attempts: []time.Duration{100 * time.Millisecond}, wantUpdates: 0, }, { name: "publish: too soon, skip update, but recovers", publishInterval: 2 * time.Second, growTree: true, attempts: []time.Duration{100 * time.Millisecond, 2 * time.Second}, wantUpdates: 1, }, { name: "publish: many attempts, eventually one succeeds", publishInterval: 1 * time.Second, growTree: true, attempts: []time.Duration{300 * time.Millisecond, 300 * time.Millisecond, 300 * time.Millisecond, 300 * time.Millisecond}, wantUpdates: 1, }, { name: "republish: works ok", publishInterval: minCheckpointInterval, republishInterval: 100 * time.Millisecond, attempts: []time.Duration{1 * time.Second}, wantUpdates: 1, }, { name: "republish: too soon, skip update", publishInterval: minCheckpointInterval, republishInterval: 10 * time.Second, attempts: []time.Duration{100 * time.Millisecond}, wantUpdates: 0, }, { name: "republish: too soon, skip update, but recovers", publishInterval: minCheckpointInterval, republishInterval: 2 * time.Second, attempts: []time.Duration{100 * time.Millisecond, 2 * time.Second}, wantUpdates: 1, }, { name: "republish: many attempts, eventually one succeeds", publishInterval: minCheckpointInterval, republishInterval: 1 * time.Second, attempts: []time.Duration{300 * time.Millisecond, 300 * time.Millisecond, 300 * time.Millisecond, 300 * time.Millisecond}, wantUpdates: 1, }, } { t.Run(test.name, func(t *testing.T) { ctx, cancel := context.WithCancel(t.Context()) defer cancel() s := &Storage{ cfg: Config{ HTTPClient: http.DefaultClient, Path: t.TempDir(), }, } sk, _ := mustGenerateKeys(t) opts := tessera.NewAppendOptions(). WithCheckpointInterval(10*time.Minute). // Prevent tessera from publishing checkpoints on our behalf WithBatching(1, minCheckpointInterval). WithCheckpointSigner(sk) logStorage := &logResourceStorage{ s: s, entriesPath: opts.EntriesPath(), } appender, lr, err := s.newAppender(ctx, logStorage, opts) if err != nil { t.Fatalf("Appender: %v", err) } // Add time as an extension line on the checkpoint so we can easily tell when it's been updated. appender.newCP = func(_ context.Context, size uint64, hash []byte) ([]byte, error) { return fmt.Appendf(nil, "origin\n%d\n%x\n%d\n,", size, hash, time.Now().Unix()), nil } if err := appender.publishCheckpoint(ctx, test.publishInterval, test.republishInterval); err != nil { t.Fatalf("publishTree: %v", err) } updatesSeen := 0 cpOld, err := lr.ReadCheckpoint(ctx) if err != nil && !errors.Is(err, os.ErrNotExist) { t.Fatalf("ReadCheckpoint: %v", err) } if test.growTree { // Fake the tree growing here - we don't want Tessera creating a new checkpoint for us, as we'll do that // manually below. if err := appender.s.writeTreeState(ctx, 1, []byte("root)")); err != nil { t.Fatalf("writeTreeState: %v", err) } } for _, d := range test.attempts { time.Sleep(d) if err := appender.publishCheckpoint(ctx, test.publishInterval, test.republishInterval); err != nil { t.Fatalf("publishTree: %v", err) } cpNew, err := lr.ReadCheckpoint(ctx) if err != nil { t.Fatalf("ReadCheckpoint: %v", err) } if !bytes.Equal(cpOld, cpNew) { updatesSeen++ cpOld = cpNew } } if updatesSeen != test.wantUpdates { t.Fatalf("Saw %d updates, want %d", updatesSeen, test.wantUpdates) } }) } } func findAllPartialDirs(t *testing.T, root string) (map[string]struct{}, error) { t.Helper() if !strings.HasSuffix(root, "/") { root += "/" } dirs := make(map[string]struct{}) f := func(path string, d os.DirEntry, err error) error { if d.IsDir() && strings.Contains(d.Name(), ".p") { dirs[strings.TrimPrefix(path, root)] = struct{}{} } return nil } return dirs, filepath.WalkDir(root, f) } // expectedPartialPrefixes returns a slice containing resource prefixes where it's acceptable for a // tree of the provided size to have partial resources. // // These are really just the right-hand tiles/entry bundle in the tree. func expectedPartialPrefixes(size uint64, entriesPath func(uint64, uint8) string) []string { r := []string{} for l, c := uint64(0), size; c > 0; l, c = l+1, c>>8 { idx, p := c/256, c%256 if p != 0 { if l == 0 { r = append(r, entriesPath(idx, 0)+".p") } r = append(r, layout.TilePath(l, idx, 0)+".p") } } return r } func mustGenerateKeys(t *testing.T) (note.Signer, note.Verifier) { sk, vk, err := note.GenerateKey(nil, "testlog") if err != nil { t.Fatalf("GenerateKey: %v", err) } s, err := note.NewSigner(sk) if err != nil { t.Fatalf("NewSigner: %v", err) } v, err := note.NewVerifier(vk) if err != nil { t.Fatalf("NewVerifier: %v", err) } return s, v } // defaultMerkleLeafHasher parses a C2SP tlog-tile bundle and returns the Merkle leaf hashes of each entry it contains. func defaultMerkleLeafHasher(bundle []byte) ([][]byte, error) { eb := &api.EntryBundle{} if err := eb.UnmarshalText(bundle); err != nil { return nil, fmt.Errorf("unmarshal: %v", err) } r := make([][]byte, 0, len(eb.Entries)) for _, e := range eb.Entries { h := rfc6962.DefaultHasher.HashLeaf(e) r = append(r, h[:]) } return r, nil } transparency-dev-tessera-3cb22ee/storage/posix/otel.go000066400000000000000000000030471511600621500232330ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package posix import ( "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/metric" "k8s.io/klog/v2" ) const name = "github.com/transparency-dev/tessera/storage/posix" var ( meter = otel.Meter(name) opNameKey = attribute.Key("op_name") ) var ( posixOpsHistogram metric.Int64Histogram // Custom histogram buckets as we're interested in low-millis upto low-seconds. histogramBuckets = []float64{0, 1, 2, 5, 10, 20, 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000, 1200, 1400, 1600, 1800, 2000, 2500, 3000, 4000, 5000, 6000, 8000, 10000} ) func init() { var err error posixOpsHistogram, err = meter.Int64Histogram( "tessera.appender.ops.duration", metric.WithDescription("Duration of calls to POSIX file operations"), metric.WithUnit("ms"), metric.WithExplicitBucketBoundaries(histogramBuckets...)) if err != nil { klog.Exitf("Failed to create posixOptsHistogram metric: %v", err) } } transparency-dev-tessera-3cb22ee/storage/storage_test.go000066400000000000000000000032731511600621500236320ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package storage_test import ( "os" "path/filepath" "strings" "testing" ) // The layout.EntriesPath function should never been called by storage implementations. // They should use `tessera.AppendOptions.EntriesPath` instead. func TestForbiddenFunction(t *testing.T) { rootDir := "." forbiddenName := "layout.EntriesPath" err := filepath.WalkDir(rootDir, func(path string, d os.DirEntry, err error) error { if err != nil { return err } // Skip directories if d.IsDir() { return nil } // Only check Go files if !strings.HasSuffix(d.Name(), ".go") { return nil } // Skip test files to avoid false positives (or specifically this file) if strings.HasSuffix(d.Name(), "_test.go") { return nil } // Read the file content content, err := os.ReadFile(path) if err != nil { return err } // Check for the forbidden string if strings.Contains(string(content), forbiddenName) { t.Errorf("Found forbidden call %q in file %q", forbiddenName, path) } return nil }) if err != nil { t.Fatalf("Error walking the path %q: %v", rootDir, err) } } transparency-dev-tessera-3cb22ee/testdata/000077500000000000000000000000001511600621500207405ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testdata/build_log.sh000077500000000000000000000015561511600621500232460ustar00rootroot00000000000000#!/usr/bin/env bash # # build_log.sh is a script for building a small test log. set -e # exit on any error codes from sub-commands DIR=$(cd $(dirname "${BASH_SOURCE[0]}") && pwd) export LOG=${DIR}/log/ export LOG_PRIVATE_KEY="PRIVATE+KEY+example.com/log/testdata+33d7b496+AeymY/SZAX0jZcJ8enZ5FY1Dz+wTML2yWSkK+9DSF3eg" export LOG_PUBLIC_KEY="example.com/log/testdata+33d7b496+AeHTu4Q3hEIMHNqc6fASMsq3rKNx280NI+oO5xCFkkSx" cd ${DIR} rm -fr log go run ../cmd/examples/posix-oneshot --storage_dir=${LOG} cp ${LOG}/checkpoint ${LOG}/checkpoint.0 export LEAF=`mktemp` for i in one two three four five six seven eit nain ten ileven twelf threeten fourten fivten; do echo -n "$i" > ${LEAF} go run ../cmd/examples/posix-oneshot --storage_dir=${LOG} --entries="${LEAF}" size=$(sed -n '2 p' ${LOG}/checkpoint) cp ${LOG}/checkpoint ${LOG}/checkpoint.${size} done rm ${LEAF} transparency-dev-tessera-3cb22ee/testdata/log/000077500000000000000000000000001511600621500215215ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testdata/log/.state/000077500000000000000000000000001511600621500227175ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testdata/log/.state/publish.lock000066400000000000000000000000001511600621500252250ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testdata/log/.state/treeState000066400000000000000000000001011511600621500245720ustar00rootroot00000000000000{"size":15,"root":"rKbDipCvhuX1GZ7g5BBe8sA6BbJ7ja/1nk427v383cs="}transparency-dev-tessera-3cb22ee/testdata/log/.state/treeState.lock000066400000000000000000000000001511600621500255170ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testdata/log/.state/version000066400000000000000000000000011511600621500243160ustar00rootroot000000000000001transparency-dev-tessera-3cb22ee/testdata/log/checkpoint000066400000000000000000000003041511600621500235700ustar00rootroot00000000000000example.com/log/testdata 15 rKbDipCvhuX1GZ7g5BBe8sA6BbJ7ja/1nk427v383cs= — example.com/log/testdata M9e0luh4jTG+UMBqOUo9/q+yAEvx1ilBEniMvjf1zfcBOhg+CKubMt53MvIt/uEWP6zdkdIQzd/fmwlM/dlPWF74ewI= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.0000066400000000000000000000003031511600621500237250ustar00rootroot00000000000000example.com/log/testdata 0 47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU= — example.com/log/testdata M9e0lhxJACzE1KoUdqKuPa+9p6R7dSW8BlEvrg/noo8FkGg6PbJoRNOcdjqjOq89wVvTA60eubCc4oufo5BvIVEgtQc= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.1000066400000000000000000000003031511600621500237260ustar00rootroot00000000000000example.com/log/testdata 1 0Nc2CrefWKseHj/mStd+LqC8B+NrX0btIiPt2SmN+ek= — example.com/log/testdata M9e0lhr9kuLxGdAtaUrhm84TQffVsLdcy808sN0a3WyniNlfcr9+bywoiIke1YYciCNjJGkCgIlQ8b16jhsmg/SChAM= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.10000066400000000000000000000003041511600621500240070ustar00rootroot00000000000000example.com/log/testdata 10 y8/gZsZCtitsMan09tY+k8I7Qpr7Y5fbO2tLi9GQnfU= — example.com/log/testdata M9e0lrmwaVEOWFZ+HS5ITnRd2YMOSWE5QeoYu+6DHwJLnJ+SVeJ2ZiYeD7lVC5oGJx76gbfLSyCUEq6D8JkarrsuiQQ= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.11000066400000000000000000000003041511600621500240100ustar00rootroot00000000000000example.com/log/testdata 11 Yok1cS5VdokGLBy73BC2I16b3atss6KBN9jnIi6Fz/A= — example.com/log/testdata M9e0lqbRtWirJwbUy4w3VrK2RyzfW8P+OIkc8t6b6AVG/tQd1Q2SN+uCLYzNNFnf/PhzeCdURSGQe1MRnohCxPu21wg= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.12000066400000000000000000000003041511600621500240110ustar00rootroot00000000000000example.com/log/testdata 12 uZo6M+IhVna+CLgWCrJs0xBPUHbozHkYTQASduHSLME= — example.com/log/testdata M9e0lrFzoTgi+ovhrqX0DpsEAp3wLu1coMLIkeCwSkGfS4Ox4pLXo2LHudqm4merh0ZkCNzHr8kQwq5gS5mmimIvdA4= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.13000066400000000000000000000003041511600621500240120ustar00rootroot00000000000000example.com/log/testdata 13 av0kLH0VkkVDrTuZzBGIx7d9z5G1l/GlKz/nAV/8woQ= — example.com/log/testdata M9e0liO8x8xfEOGmZVYODUjQajkEpjccPQ9EkG3yEEDet4K3BWE/DFRPH6H+b6gFFFaG5KS1bBmYS/xeZJs8lj+z+wM= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.14000066400000000000000000000003041511600621500240130ustar00rootroot00000000000000example.com/log/testdata 14 SvCd38yNade7xEPY1a/aAc1M3A2AHYVF8lIiUnsH1ao= — example.com/log/testdata M9e0lu8roIpjC+NXEG7JDkajIahoYDYWgxDtxfx/6rzLoztU9t/PvivZ48xt8ygJpjzaboSpkbVv3Jb+D3NdXWQf5wo= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.15000066400000000000000000000003041511600621500240140ustar00rootroot00000000000000example.com/log/testdata 15 rKbDipCvhuX1GZ7g5BBe8sA6BbJ7ja/1nk427v383cs= — example.com/log/testdata M9e0luh4jTG+UMBqOUo9/q+yAEvx1ilBEniMvjf1zfcBOhg+CKubMt53MvIt/uEWP6zdkdIQzd/fmwlM/dlPWF74ewI= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.2000066400000000000000000000003031511600621500237270ustar00rootroot00000000000000example.com/log/testdata 2 T1X2GdkhUjV3iyufF9b0kVsWFxIU0VI4EpNml2Teci4= — example.com/log/testdata M9e0llH4NKmlf0u7C8tkJioQOmi7l5ELqljXgKRg6R3G5JJ7vx7emDWo5fpqKuA/Dk/jRrI6il+7QCVirNhyUAWGoAk= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.3000066400000000000000000000003031511600621500237300ustar00rootroot00000000000000example.com/log/testdata 3 Wqx3HImawpLnS/Gv4ubjAvi1WIOy0b8Ze0amvqbavKk= — example.com/log/testdata M9e0lvAQRtH832pk2RTbeSeZMkp4FZSdJYFBUjO5SKxuRbywPUF3cx+HUrb50kjd3rUWW/fj53Wj1dwcwh9bGQnqVAg= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.4000066400000000000000000000003031511600621500237310ustar00rootroot00000000000000example.com/log/testdata 4 zY1lN35vrXYAPixXSd59LsU29xUJtuW4o2dNNg5Y2Co= — example.com/log/testdata M9e0lnFuWt0cp60moO4jAkeXvMNLIIOGn4WdyeHQurBt9mBi0tWdfkldiAt9k0FpwuZymvttsVn9uwyouXU3vPpeNAc= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.5000066400000000000000000000003031511600621500237320ustar00rootroot00000000000000example.com/log/testdata 5 gy5gl3aksFyiCO95a/1vLXz88A3dRq+0l9Sxte8ZqZQ= — example.com/log/testdata M9e0lmn5eDHyHP10zJObZdFAX2y1V8fdDKBi4IiHbDYf1TCUBwxZOhqwv2QZCqrdda2zTRTrmnwjwscN5RBTEmZUYAs= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.6000066400000000000000000000003031511600621500237330ustar00rootroot00000000000000example.com/log/testdata 6 a6sWvsc2eEzmj72vah7mZ5dwFltivehh2b11qwlp5Jg= — example.com/log/testdata M9e0ln+nHSdGSHEZBcjvqyYDXDbG8WxDwCDjbbGAMv/NTynAtJWhckSXIpuwpN3yJ4PTtzUbD34YGqIZr6RgJRn16gU= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.7000066400000000000000000000003031511600621500237340ustar00rootroot00000000000000example.com/log/testdata 7 IrSXADBqJ7EQoUODSDKROySgNveeL6CFhik2w/+fS7U= — example.com/log/testdata M9e0luA5JhTRUki/BBQD0QNiH6AEJ2pdtsA5CQjqJErViuuKrN6513RCTL2640wXJv0dCed3neRGI6dMzBq9uANf2Qc= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.8000066400000000000000000000003031511600621500237350ustar00rootroot00000000000000example.com/log/testdata 8 bC+VwvrJbalNVjfkdI8kUgs7mJoXrqiXCE7bgnD28pY= — example.com/log/testdata M9e0lq1EK37dHtgv9O3UzPxisw5vwIPcJsX9ab0xt4mvVfbdUR1fbMjN6sBFYpBoBEmD4+cWhj/AVgI5MitCO5+mews= transparency-dev-tessera-3cb22ee/testdata/log/checkpoint.9000066400000000000000000000003031511600621500237360ustar00rootroot00000000000000example.com/log/testdata 9 CvhHViE3S9E3htHLR+25mapWoGCJ3WckMq1UtC3VmEY= — example.com/log/testdata M9e0ls3mdnWX0qk4P3uGG0eYiFRWSwPR+RdeD1ARmrHPHjXP0x9UJWASPFTOTqJo0mp7Epck2O9RFB0J9IjDdAewagw= transparency-dev-tessera-3cb22ee/testdata/log/tile/000077500000000000000000000000001511600621500224565ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000077500000000000000000000000001511600621500226155ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/000077500000000000000000000000001511600621500233525ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/1000066400000000000000000000000401511600621500234270ustar00rootroot000000000000006 X?J~.k_F"#)transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/10000066400000000000000000000005001511600621500235100ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Z#>z&\,$ǹV@ŔiLVn% 47dlЦ[5(LN ]a¾T,AP5f/-ڻt:X)kQЩZV@ܛҤ8Fvs|N9}=& v<ρ?{} }transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/11000066400000000000000000000005401511600621500235150ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Z#>z&\,$ǹV@ŔiLVn% 47dlЦ[5(LN ]a¾T,AP5f/-ڻt:X)kQЩZV@ܛҤ8Fvs|N9}=& v<ρ?{} }-Fs1攴,B"إТ3transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/12000066400000000000000000000006001511600621500235130ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Z#>z&\,$ǹV@ŔiLVn% 47dlЦ[5(LN ]a¾T,AP5f/-ڻt:X)kQЩZV@ܛҤ8Fvs|N9}=& v<ρ?{} }-Fs1攴,B"إТ3;\|&|hMX` "xtransparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/13000066400000000000000000000006401511600621500235200ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Z#>z&\,$ǹV@ŔiLVn% 47dlЦ[5(LN ]a¾T,AP5f/-ڻt:X)kQЩZV@ܛҤ8Fvs|N9}=& v<ρ?{} }-Fs1攴,B"إТ3;\|&|hMX` "x̟ca9VKa7#c~wMfh transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/14000066400000000000000000000007001511600621500235160ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Z#>z&\,$ǹV@ŔiLVn% 47dlЦ[5(LN ]a¾T,AP5f/-ڻt:X)kQЩZV@ܛҤ8Fvs|N9}=& v<ρ?{} }-Fs1攴,B"إТ3;\|&|hMX` "x̟ca9VKa7#c~wMfh նNaIJI4N9ewI_transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/15000066400000000000000000000007401511600621500235230ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Z#>z&\,$ǹV@ŔiLVn% 47dlЦ[5(LN ]a¾T,AP5f/-ڻt:X)kQЩZV@ܛҤ8Fvs|N9}=& v<ρ?{} }-Fs1攴,B"إТ3;\|&|hMX` "x̟ca9VKa7#c~wMfh նNaIJI4N9ewI_7_E I )?!qJ+transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/2000066400000000000000000000001001511600621500234250ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/3000066400000000000000000000001401511600621500234320ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌utransparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/4000066400000000000000000000002001511600621500234300ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jtransparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/5000066400000000000000000000002401511600621500234350ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Ztransparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/6000066400000000000000000000003001511600621500234330ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Z#>z&\,$ǹV@ŔiLVn%transparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/7000066400000000000000000000003401511600621500234400ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Z#>z&\,$ǹV@ŔiLVn% 47dlЦ[5(Ltransparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/8000066400000000000000000000004001511600621500234360ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Z#>z&\,$ǹV@ŔiLVn% 47dlЦ[5(LN ]a¾T,AP5f/-ڻt:X)ktransparency-dev-tessera-3cb22ee/testdata/log/tile/0/000.p/9000066400000000000000000000004401511600621500234430ustar00rootroot000000000000006 X?J~.k_F"#)||oSA.>%gl^GK󥈍̌u?]n*ĻM[5=7 ^yϹ3jš*wWtל&!Z#>z&\,$ǹV@ŔiLVn% 47dlЦ[5(LN ]a¾T,AP5f/-ڻt:X)kQЩZV@ܛҤ8Fvs|Ntransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000077500000000000000000000000001511600621500241275ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/000077500000000000000000000000001511600621500246645ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/1000066400000000000000000000000051511600621500247420ustar00rootroot00000000000000onetransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/10000066400000000000000000000000711511600621500250250ustar00rootroot00000000000000onetwothreefourfivesixseveneitnaintentransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/11000066400000000000000000000001011511600621500250200ustar00rootroot00000000000000onetwothreefourfivesixseveneitnaintenileventransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/12000066400000000000000000000001101511600621500250210ustar00rootroot00000000000000onetwothreefourfivesixseveneitnaintenileventwelftransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/13000066400000000000000000000001221511600621500250250ustar00rootroot00000000000000onetwothreefourfivesixseveneitnaintenileventwelfthreetentransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/14000066400000000000000000000001331511600621500250300ustar00rootroot00000000000000onetwothreefourfivesixseveneitnaintenileventwelfthreetenfourtentransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/15000066400000000000000000000001431511600621500250320ustar00rootroot00000000000000onetwothreefourfivesixseveneitnaintenileventwelfthreetenfourtenfivtentransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/2000066400000000000000000000000121511600621500247410ustar00rootroot00000000000000onetwotransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/3000066400000000000000000000000211511600621500247420ustar00rootroot00000000000000onetwothreetransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/4000066400000000000000000000000271511600621500247510ustar00rootroot00000000000000onetwothreefourtransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/5000066400000000000000000000000351511600621500247510ustar00rootroot00000000000000onetwothreefourfivetransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/6000066400000000000000000000000421511600621500247500ustar00rootroot00000000000000onetwothreefourfivesixtransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/7000066400000000000000000000000511511600621500247510ustar00rootroot00000000000000onetwothreefourfivesixseventransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/8000066400000000000000000000000561511600621500247570ustar00rootroot00000000000000onetwothreefourfivesixseveneittransparency-dev-tessera-3cb22ee/testdata/log/tile/entries/000.p/9000066400000000000000000000000641511600621500247570ustar00rootroot00000000000000onetwothreefourfivesixseveneitnaintransparency-dev-tessera-3cb22ee/testonly/000077500000000000000000000000001511600621500210105ustar00rootroot00000000000000transparency-dev-tessera-3cb22ee/testonly/testlog.go000066400000000000000000000046171511600621500230300ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package testonly import ( "context" "testing" "github.com/transparency-dev/tessera" "github.com/transparency-dev/tessera/storage/posix" "golang.org/x/mod/sumdb/note" ) // NewTestLog creates a temporary POSIX log instance in Appender mode with the provided options. // // This log will be rooted in a temporary directory which will be automatically removed by the // testing package after use. // // Returns an instance of TestLog containing the various structures created, and a shutdown function // which MUST be called when the test has finished with the log. func NewTestLog(t *testing.T, opts *tessera.AppendOptions) (*TestLog, func(context.Context) error) { t.Helper() sk, vk, err := note.GenerateKey(nil, "test") if err != nil { t.Fatalf("GenerateKey: %v", err) } s, err := note.NewSigner(sk) if err != nil { t.Fatalf("NewSigner: %v", err) } v, err := note.NewVerifier(vk) if err != nil { t.Fatalf("NewVerifier: %v", err) } root := t.TempDir() driver, err := posix.New(t.Context(), posix.Config{Path: root}) if err != nil { t.Fatalf("posix.New: %v", err) } opts.WithCheckpointSigner(s) a, shutdown, lr, err := tessera.NewAppender(t.Context(), driver, opts) if err != nil { t.Fatalf("NewAppender: %v", err) } r := &TestLog{ Root: root, SigVerifier: v, LogReader: lr, Appender: a, } return r, shutdown } // TestLog represents an ephemeral POSIX log instance intended for use in tests. type TestLog struct { // Root is the path to the directory which contains the log data. Root string // SigVerifier can verify log signatures on its checkpoints. SigVerifier note.Verifier // LogReader reads from the log storage directly. LogReader tessera.LogReader // Appender provides access to the Appender lifecycle mode for this log. Appender *tessera.Appender } transparency-dev-tessera-3cb22ee/witness.go000066400000000000000000000230251511600621500211540ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "bufio" "bytes" "fmt" "net/url" "strconv" "strings" "maps" f_note "github.com/transparency-dev/formats/note" "golang.org/x/mod/sumdb/note" ) // policyComponent describes a component that makes up a policy. This is either a // single Witness, or a WitnessGroup. type policyComponent interface { // Satisfied returns true if the checkpoint is signed by the quorum of // witnesses involved in this policy component. Satisfied(cp []byte) bool // Endpoints returns the details required for updating a witness and checking the // response. The returned result is a map from the URL that should be used to update // the witness with a new checkpoint, to the value which is the verifier to check // the response is well formed. Endpoints() map[string]note.Verifier } // NewWitnessGroupFromPolicy creates a graph of witness objects that represents the // policy provided, and which can be passed directly to the WithWitnesses // appender lifecycle option. // // The policy must be structured as per the description in // https://git.glasklar.is/sigsum/core/sigsum-go/-/blob/main/doc/policy.md func NewWitnessGroupFromPolicy(p []byte) (WitnessGroup, error) { scanner := bufio.NewScanner(bytes.NewBuffer(p)) components := make(map[string]policyComponent) var quorumName string for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if i := strings.Index(line, "#"); i >= 0 { line = line[:i] } if line == "" { continue } switch fields := strings.Fields(line); fields[0] { case "log": // This keyword is important to clients who might use the policy file, but we don't need to know about it since // we _are_ the log, so just ignore it. case "witness": // Strictly, the URL is optional so policy files can be used client-side, where they don't care about the URL. // Given this function is parsing to create the graph structure which will be used by a Tessera log to witness // new checkpoints we'll ignore that special case here. if len(fields) != 4 { return WitnessGroup{}, fmt.Errorf("invalid witness definition: %q", line) } name, vkey, witnessURLStr := fields[1], fields[2], fields[3] if isBadName(name) { return WitnessGroup{}, fmt.Errorf("invalid witness name %q", name) } if _, ok := components[name]; ok { return WitnessGroup{}, fmt.Errorf("duplicate component name: %q", name) } witnessURL, err := url.Parse(witnessURLStr) if err != nil { return WitnessGroup{}, fmt.Errorf("invalid witness URL %q: %w", witnessURLStr, err) } w, err := NewWitness(vkey, witnessURL) if err != nil { return WitnessGroup{}, fmt.Errorf("invalid witness config %q: %w", line, err) } components[name] = w case "group": if len(fields) < 3 { return WitnessGroup{}, fmt.Errorf("invalid group definition: %q", line) } name, N, childrenNames := fields[1], fields[2], fields[3:] if isBadName(name) { return WitnessGroup{}, fmt.Errorf("invalid group name %q", name) } if _, ok := components[name]; ok { return WitnessGroup{}, fmt.Errorf("duplicate component name: %q", name) } var n int switch N { case "any": n = 1 case "all": n = len(childrenNames) default: i, err := strconv.ParseUint(N, 10, 8) if err != nil { return WitnessGroup{}, fmt.Errorf("invalid threshold %q for group %q: %w", N, name, err) } n = int(i) } if c := len(childrenNames); n > c { return WitnessGroup{}, fmt.Errorf("group with %d children cannot have threshold %d", c, n) } children := make([]policyComponent, len(childrenNames)) for i, cName := range childrenNames { if isBadName(cName) { return WitnessGroup{}, fmt.Errorf("invalid component name %q", cName) } child, ok := components[cName] if !ok { return WitnessGroup{}, fmt.Errorf("unknown component %q in group definition", cName) } children[i] = child } wg := NewWitnessGroup(n, children...) components[name] = wg case "quorum": if len(fields) != 2 { return WitnessGroup{}, fmt.Errorf("invalid quorum definition: %q", line) } quorumName = fields[1] default: return WitnessGroup{}, fmt.Errorf("unknown keyword: %q", fields[0]) } } if err := scanner.Err(); err != nil { return WitnessGroup{}, err } switch quorumName { case "": return WitnessGroup{}, fmt.Errorf("policy file must define a quorum") case "none": return NewWitnessGroup(0), nil default: if isBadName(quorumName) { return WitnessGroup{}, fmt.Errorf("invalid quorum name %q", quorumName) } policy, ok := components[quorumName] if !ok { return WitnessGroup{}, fmt.Errorf("quorum component %q not found", quorumName) } wg, ok := policy.(WitnessGroup) if !ok { // A single witness can be a policy. Wrap it in a group. return NewWitnessGroup(1, policy), nil } return wg, nil } } var keywords = map[string]struct{}{ "witness": {}, "group": {}, "any": {}, "all": {}, "none": {}, "quorum": {}, "log": {}, } func isBadName(n string) bool { _, isKeyword := keywords[n] return isKeyword } // NewWitness returns a Witness given a verifier key and the root URL for where this // witness can be reached. func NewWitness(vkey string, witnessRoot *url.URL) (Witness, error) { v, err := f_note.NewVerifierForCosignatureV1(vkey) if err != nil { return Witness{}, err } u := witnessRoot.JoinPath("/add-checkpoint") return Witness{ Key: v, URL: u.String(), }, err } // Witness represents a single witness that can be reached in order to perform a witnessing operation. // The URLs() method returns the URL where it can be reached for witnessing, and the Satisfied method // provides a predicate to check whether this witness has signed a checkpoint. type Witness struct { Key note.Verifier URL string } // Satisfied returns true if the checkpoint provided is signed by this witness. // This will return false if there is no signature, and also if the // checkpoint cannot be read as a valid note. It is up to the caller to ensure // that the input value represents a valid note. func (w Witness) Satisfied(cp []byte) bool { n, err := note.Open(cp, note.VerifierList(w.Key)) if err != nil { return false } return len(n.Sigs) == 1 } // Endpoints returns the details required for updating a witness and checking the // response. The returned result is a map from the URL that should be used to update // the witness with a new checkpoint, to the value which is the verifier to check // the response is well formed. func (w Witness) Endpoints() map[string]note.Verifier { return map[string]note.Verifier{w.URL: w.Key} } // NewWitnessGroup creates a grouping of Witness or WitnessGroup with a configurable threshold // of these sub-components that need to be satisfied in order for this group to be satisfied. // // The threshold should only be set to less than the number of sub-components if these are // considered fungible. func NewWitnessGroup(n int, children ...policyComponent) WitnessGroup { if n < 0 || n > len(children) { panic(fmt.Errorf("threshold of %d outside bounds for children %s", n, children)) } return WitnessGroup{ Components: children, N: n, } } // WitnessGroup defines a group of witnesses, and a threshold of // signatures that must be met for this group to be satisfied. // Witnesses within a group should be fungible, e.g. all of the Armored // Witness devices form a logical group, and N should be picked to // represent a threshold of the quorum. For some users this will be a // simple majority, but other strategies are available. // N must be <= len(WitnessKeys). type WitnessGroup struct { Components []policyComponent N int } // Satisfied returns true if the checkpoint provided has sufficient signatures // from the witnesses in this group to satisfy the threshold. // This will return false if there are insufficient signatures, and also if the // checkpoint cannot be read as a valid note. It is up to the caller to ensure // that the input value represents a valid note. // // The implementation of this requires every witness in the group to verify the // checkpoint, which is O(N). If this is called every time a witness returns a // checkpoint then this algorithm is O(N^2). To support large N, this may require // some rewriting in order to maintain performance. func (wg WitnessGroup) Satisfied(cp []byte) bool { if wg.N <= 0 { return true } satisfaction := 0 for _, c := range wg.Components { if c.Satisfied(cp) { satisfaction++ } if satisfaction >= wg.N { return true } } return false } // Endpoints returns the details required for updating a witness and checking the // response. The returned result is a map from the URL that should be used to update // the witness with a new checkpoint, to the value which is the verifier to check // the response is well formed. func (wg WitnessGroup) Endpoints() map[string]note.Verifier { endpoints := make(map[string]note.Verifier) for _, c := range wg.Components { maps.Copy(endpoints, c.Endpoints()) } return endpoints } transparency-dev-tessera-3cb22ee/witness_policy_test.go000066400000000000000000000126541511600621500236000ustar00rootroot00000000000000// Copyright 2025 The Tessera authors. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package tessera import ( "strings" "testing" ) func TestNewWitnessGroupFromPolicy(t *testing.T) { for _, test := range []struct { name string policy string }{ { name: "tidy", policy: ` witness w1 sigsum.org+e4ade967+AZuUY6B08pW3QVHu8uvsrxWPcAv9nykap2Nb4oxCee+r https://sigsum.org/witness/ witness w2 example.com+3753d3de+AebBhMcghIUoavZpjuDofa4sW6fYHyVn7gvwDBfvkvuM https://example.com/witness/ group g1 all w1 w2 quorum g1 `, }, { name: "whitespace and comments", policy: ` # comment witness w1 sigsum.org+e4ade967+AZuUY6B08pW3QVHu8uvsrxWPcAv9nykap2Nb4oxCee+r https://sigsum.org/witness/ #comment witness w2 example.com+3753d3de+AebBhMcghIUoavZpjuDofa4sW6fYHyVn7gvwDBfvkvuM https://example.com/witness/ #comment group g1 all w1 w2 quorum g1 `, }, } { t.Run(test.name, func(t *testing.T) { wg, err := NewWitnessGroupFromPolicy([]byte(test.policy)) if err != nil { t.Fatalf("NewWitnessGroupFromPolicy() failed: %v", err) } if wg.N != 2 { t.Errorf("Expected top-level group to have N=2, got %d", wg.N) } if len(wg.Components) != 2 { t.Fatalf("Expected top-level group to have 2 components, got %d", len(wg.Components)) } }) } } func TestNewWitnessGroupFromPolicy_GroupN(t *testing.T) { testCases := []struct { desc string policy string wantN int }{ { desc: "group numerical", policy: ` witness w1 sigsum.org+e4ade967+AZuUY6B08pW3QVHu8uvsrxWPcAv9nykap2Nb4oxCee+r https://sigsum.org/witness/ witness w2 example.com+3753d3de+AebBhMcghIUoavZpjuDofa4sW6fYHyVn7gvwDBfvkvuM https://example.com/witness/ witness w3 example.com+3753d3de+AebBhMcghIUoavZpjuDofa4sW6fYHyVn7gvwDBfvkvuM https://example.com/witness/ witness w4 remora.n621.de+da77ade7+BOvN63jn/bLvkieywe8R6UYAtVtNbZpXh34x7onlmtw2 https://example.com/remora group g1 2 w1 w2 w3 w4 quorum g1 `, wantN: 2, }, { desc: "group all", policy: ` witness w1 sigsum.org+e4ade967+AZuUY6B08pW3QVHu8uvsrxWPcAv9nykap2Nb4oxCee+r https://sigsum.org/witness/ witness w2 example.com+3753d3de+AebBhMcghIUoavZpjuDofa4sW6fYHyVn7gvwDBfvkvuM https://example.com/witness/ witness w3 example.com+3753d3de+AebBhMcghIUoavZpjuDofa4sW6fYHyVn7gvwDBfvkvuM https://example.com/witness/ group g1 all w1 w2 w3 quorum g1 `, wantN: 3, }, { desc: "group any", policy: ` witness w1 sigsum.org+e4ade967+AZuUY6B08pW3QVHu8uvsrxWPcAv9nykap2Nb4oxCee+r https://sigsum.org/witness/ witness w2 example.com+3753d3de+AebBhMcghIUoavZpjuDofa4sW6fYHyVn7gvwDBfvkvuM https://example.com/witness/ witness w3 example.com+3753d3de+AebBhMcghIUoavZpjuDofa4sW6fYHyVn7gvwDBfvkvuM https://example.com/witness/ group g1 any w1 quorum g1 `, wantN: 1, }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { wg, err := NewWitnessGroupFromPolicy([]byte(tc.policy)) if err != nil { t.Fatalf("NewWitnessGroupFromPolicy() failed: %v", err) } if wg.N != tc.wantN { t.Errorf("wg.N = %d, want %d", wg.N, tc.wantN) } }) } } func TestNewWitnessGroupFromPolicy_Errors(t *testing.T) { testCases := []struct { desc string policy string errStr string }{ { desc: "no quorum", policy: "witness w1 sigsum.org+e4ade967+AZuUY6B08pW3QVHu8uvsrxWPcAv9nykap2Nb4oxCee+r https://sigsum.org/witness/", errStr: "policy file must define a quorum", }, { desc: "unknown quorum component", policy: "quorum unknown", errStr: "quorum component \"unknown\" not found", }, { desc: "duplicate component name", policy: "witness w1 sigsum.org+e4ade967+AZuUY6B08pW3QVHu8uvsrxWPcAv9nykap2Nb4oxCee+r https://sigsum.org/witness/\nwitness w1 sigsum.org+e4ade967+AZuUY6B08pW3QVHu8uvsrxWPcAv9nykap2Nb4oxCee+r https://sigsum.org/witness/\nquorum w1", errStr: "duplicate component name", }, { desc: "negative threshold", policy: `witness w1 sigsum.org+e4ade967+AZuUY6B08pW3QVHu8uvsrxWPcAv9nykap2Nb4oxCee+r https://sigsum.org/witness/ witness w2 example.com+3753d3de+AebBhMcghIUoavZpjuDofa4sW6fYHyVn7gvwDBfvkvuM https://example.com/witness/ witness w3 example.com+3753d3de+AebBhMcghIUoavZpjuDofa4sW6fYHyVn7gvwDBfvkvuM https://example.com/witness/ group g1 -1 w1 quorum g1`, errStr: "invalid threshold", }, { desc: "witness name is keyword", policy: `witness all sigsum.org+e4ade967+AZuUY6B08pW3QVHu8uvsrxWPcAv9nykap2Nb4oxCee+r https://sigsum.org/witness/`, errStr: "invalid witness name", }, { desc: "witness name is keyword", policy: `group none 1 witness`, errStr: "invalid group name", }, } for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { _, err := NewWitnessGroupFromPolicy([]byte(tc.policy)) if err == nil { t.Fatal("Expected error, got nil") } if !strings.Contains(err.Error(), tc.errStr) { t.Errorf("Expected error string to contain %q, got %q", tc.errStr, err.Error()) } }) } } transparency-dev-tessera-3cb22ee/witness_test.go000066400000000000000000000160311511600621500222120ustar00rootroot00000000000000package tessera_test import ( "net/url" "slices" "testing" f_note "github.com/transparency-dev/formats/note" "github.com/transparency-dev/tessera" "golang.org/x/mod/sumdb/note" ) const ( wit1_vkey = "Wit1+55ee4561+AVhZSmQj9+SoL+p/nN0Hh76xXmF7QcHfytUrI1XfSClk" wit1_skey = "PRIVATE+KEY+Wit1+55ee4561+AeadRiG7XM4XiieCHzD8lxysXMwcViy5nYsoXURWGrlE" wit2_vkey = "Wit2+85ecc407+AWVbwFJte9wMQIPSnEnj4KibeO6vSIOEDUTDp3o63c2x" wit2_skey = "PRIVATE+KEY+Wit2+85ecc407+AfPTvxw5eUcqSgivo2vaiC7JPOMUZ/9baHPSDrWqgdGm" wit3_vkey = "Wit3+d3ed3be7+ASb6Uz1+fxAcXkMvDd7nGa3FjDce7LxIKmbbTCT0MpVn" wit3_skey = "PRIVATE+KEY+Wit3+d3ed3be7+AR2Kg8k6ccBr5QXz5SHtnkOS4UGQGEQaWi6Gfr6Mm3X5" ) var ( bastion, _ = url.Parse("https://b1.example.com/") directURL, _ = url.Parse("https://witness.example.com/") wit1, _ = tessera.NewWitness(wit1_vkey, bastion.JoinPath("wit1prefix")) wit2, _ = tessera.NewWitness(wit2_vkey, bastion.JoinPath("wit2prefix")) wit3, _ = tessera.NewWitness(wit3_vkey, directURL) wit1Sign, _ = f_note.NewSignerForCosignatureV1(wit1_skey) wit2Sign, _ = f_note.NewSignerForCosignatureV1(wit2_skey) wit3Sign, _ = f_note.NewSignerForCosignatureV1(wit3_skey) ) func TestWitnessGroup_Empty(t *testing.T) { group := tessera.WitnessGroup{} if !group.Satisfied([]byte("definitely a checkpoint\n")) { t.Error("empty group should be satisfied") } if len(group.Endpoints()) != 0 { t.Error("empty group should have no URLs") } } func TestWitnessGroup_Satisfied(t *testing.T) { testCases := []struct { desc string group tessera.WitnessGroup signers []note.Signer expectSatisfied bool }{ { desc: "One witness, required and provided", group: tessera.NewWitnessGroup(1, wit1), signers: []note.Signer{wit1Sign}, expectSatisfied: true, }, { desc: "One witness, required and not provided", group: tessera.NewWitnessGroup(1, wit1), signers: []note.Signer{}, expectSatisfied: false, }, { desc: "One witness, optional and provided", group: tessera.NewWitnessGroup(0, wit1), signers: []note.Signer{wit1Sign}, expectSatisfied: true, }, { desc: "One witness, optional and not provided", group: tessera.NewWitnessGroup(0, wit1), signers: []note.Signer{}, expectSatisfied: true, }, { desc: "One witness, required and provided, in required subgroup", group: tessera.NewWitnessGroup(1, tessera.NewWitnessGroup(1, wit1)), signers: []note.Signer{wit1Sign}, expectSatisfied: true, }, { desc: "One witness, required and provided, in optional subgroup", group: tessera.NewWitnessGroup(0, tessera.NewWitnessGroup(1, wit1)), signers: []note.Signer{wit1Sign}, expectSatisfied: true, }, { desc: "One witness, required and not provided, in required subgroup", group: tessera.NewWitnessGroup(1, tessera.NewWitnessGroup(1, wit1)), signers: []note.Signer{}, expectSatisfied: false, }, { desc: "One witness, required and not provided, in optional subgroup", group: tessera.NewWitnessGroup(0, tessera.NewWitnessGroup(1, wit1)), signers: []note.Signer{}, expectSatisfied: true, }, { desc: "One required, one of two required, all provided", group: tessera.NewWitnessGroup(2, wit1, tessera.NewWitnessGroup(1, wit2, wit3)), signers: []note.Signer{wit1Sign, wit2Sign, wit3Sign}, expectSatisfied: true, }, { desc: "One required, one of two required, min provided", group: tessera.NewWitnessGroup(2, wit1, tessera.NewWitnessGroup(1, wit2, wit3)), signers: []note.Signer{wit1Sign, wit2Sign}, expectSatisfied: true, }, { desc: "One required, one of two required, only first group satisfied", group: tessera.NewWitnessGroup(2, wit1, tessera.NewWitnessGroup(1, wit2, wit3)), signers: []note.Signer{wit1Sign}, expectSatisfied: false, }, { desc: "One required, one of two required, only second group satisfied", group: tessera.NewWitnessGroup(2, wit1, tessera.NewWitnessGroup(1, wit2, wit3)), signers: []note.Signer{wit2Sign, wit3Sign}, expectSatisfied: false, }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { n := ¬e.Note{ // The body needs to be 3 lines to meet the cosigner expectations. Text: "sign me\nI'm a\nnote\n", } cp, err := note.Sign(n, tC.signers...) if err != nil { t.Fatal(err) } if got, want := tC.group.Satisfied(cp), tC.expectSatisfied; got != want { t.Errorf("Expected satisfied = %t but got %t", want, got) } }) } } func TestWitnessGroup_URLs(t *testing.T) { testCases := []struct { desc string group tessera.WitnessGroup expectedURLs []string }{ { desc: "witness 1", group: tessera.NewWitnessGroup(1, wit1), expectedURLs: []string{"https://b1.example.com/wit1prefix/add-checkpoint"}, }, { desc: "witness 2", group: tessera.NewWitnessGroup(1, wit2), expectedURLs: []string{"https://b1.example.com/wit2prefix/add-checkpoint"}, }, { desc: "witness 3", group: tessera.NewWitnessGroup(1, wit3), expectedURLs: []string{"https://witness.example.com/add-checkpoint"}, }, { desc: "all witnesses in one group", group: tessera.NewWitnessGroup(1, wit1, wit2, wit3), expectedURLs: []string{ "https://b1.example.com/wit1prefix/add-checkpoint", "https://b1.example.com/wit2prefix/add-checkpoint", "https://witness.example.com/add-checkpoint", }, }, { desc: "all witnesses with duplicates in nests", group: tessera.NewWitnessGroup(2, tessera.NewWitnessGroup(1, wit1, wit2), tessera.NewWitnessGroup(1, wit1, wit3)), expectedURLs: []string{ "https://b1.example.com/wit1prefix/add-checkpoint", "https://b1.example.com/wit2prefix/add-checkpoint", "https://witness.example.com/add-checkpoint", }, }, } for _, tC := range testCases { t.Run(tC.desc, func(t *testing.T) { gotURLs := make([]string, 0) for u := range tC.group.Endpoints() { gotURLs = append(gotURLs, u) } slices.Sort(gotURLs) slices.Sort(tC.expectedURLs) if !slices.Equal(gotURLs, tC.expectedURLs) { t.Errorf("Expected %s but got %s", tC.expectedURLs, gotURLs) } }) } } // This is benchmarked because this may well get called a number of times, and there are potentially // other ways to implement this that don't involve so many note.Open calls. func BenchmarkWitnessGroupSatisfaction(b *testing.B) { group := tessera.NewWitnessGroup(2, wit1, tessera.NewWitnessGroup(1, wit2, wit3)) n := ¬e.Note{ // Text must contain 3 lines to meet cosig expectations. Text: "sign me\nI'm a\nnote\n", } cp, err := note.Sign(n, wit1Sign, wit2Sign, wit3Sign) if err != nil { b.Fatal(err) } for b.Loop() { if !group.Satisfied(cp) { b.Fatal("Group should have been satisfied!") } } }

Ͻ?.g+H\8aaP\C6PKBhg{ I-~qH99{;$ӿB12#K/nD !o. BS@?HV_:^bQ@ B :4 Cj3Ldy8@\SU^S4 V!EK"@t d" A,8D <IyRhETJKCbkS hsELĶkbG*rfi{%TlT*~%`fpEP^DdLNOHEUDAE`[ mKI=J^,ԣ0BNL]YmNDO\ 4MԒDVlƪ,,rEP< c(VadM@d E L\-vT\l>kj،4.FlRhk.ֆɅ, E]xjKEZ|SAHT-y.hR.aDa-}0l6w cˢ4:dɖtavy"`I( VS0HDppjX hHԑL eJD81pFЊLK\K4DIDE(䁴1 kD1,NKH5,Tq H* ̶tr!۱ CqݰH梱)1!̱S2+?V믿PCd0́T ŔH,/ M8DLΠd3(LsIMLɘT@d;28s<{ 8 9D0͂a:cD;/cA5WЀy\/O4EL޼MMEw\HgGI+H4KNJt4 4M״M4NN4OO4PP5QQ5R'R/5S7S?5TKEM {(VwjtCL5N\9#;[+ 0D5 4XSuT6aK5})O,`I PL%,QS@a:@Bc?vdߞ ta.%H'8NtvkK*-`%*1R#mr/(hT'L8&"8fC`zh;ivu_wA$j/aS*dp|ۨSUks{CELG~Dh|7sAX#\ P,pi~MWq{T( Z+vjRY |vw{7h8zvY=W{dDWDF d)9}+ٚF }q5c1TWbW~U19{\A,@C|[u ^]\TD@-)yϪZ;DD۹sIwCwC:8F:BAz:ṞB04/zcD@]ٝ]a]rIکCCzo;_MF KS() zj~;EA Dl6Ĥww/`Th:pzDx4Ç)8{OD;"%{Snau!!5c :`C;"{S<1E_@w;}R3 ,~6DACVU^;" ԁ!|i%@ {>CWʌ}؏jvHVҧWb1TF=3J:4=B9砓E8 +22A{.A !6 *E!?@‹]Hk&Ԉ0ж/ S1,3LS5 ^ °~bVY4$  >BWp$0)-S(rz@/H 1X  r 0|A{h@L$a7ac8#F Rp "%/} Lo)Sʌ('wz+a)K vs-&ln|e,LpA9)NAڂ%R"PledtKaD|@s T @ 8 5Q[`@x 0H4Ak@& V! "4R]u#?A9`TzU ֪XP  YH# pAcP ULdp 8arR$lA9^LPvi,HLc0@B6fdV:Ɣu.Mf /&8Ny:\R!"ș ؀G+8` Vlm#5^w+@>T.]$@X#xK^Dgzc#P d>oz7aTSP !C\2"seBZ- UL`4$HU"<Z.1@PPğDA ~nFo0 kn0G@%\Y Pm~@(g"@Z 6F EJSY 0Xt0 i3ar7CEmQ%@i(K @(#8@lPTux3#_ .vQTс0f6: Pַ@&mLwB뒠 cAM^Z ^y2b8@fLv.08I#p $n^8 d 2\]҄(,iqZ}, [{Ę2A!Ȁ{ahĻI2EEܟ"l] f`EAPo;Z; Ǹvh/IVS F1c"C\&"_:P7p~qIhD>2ۭsi| mH%p@L}!?=3HA7rCJKXABzDETE^$FfF.G"HH/keITJdD`$)C6$P0=MDNNDOO%ZMQXbTJeO@d<#UVUUbeVJV vWvwe ^`( 5"\\%]bZZe[).]  ֥]EWPD_<%`_^2"P+Gb(3J&c2Bg&dFa* bh(Ce&0fRǭg0p54Deq9Gj\;k(B;iqrifma1q}o"q1q1qɱ+yf&"JzPEv@x G Av&@^QnGv2 L&BvR+n Ns ` 4h:2LlZ1$O2h! r x da +g&' 5&p қ< 0M dv&)c$oG$3'};#3!7qU%1$)e+3B*(zBR#`)1. "3'c"Q#0{w~/i}F<# bf `n" ZAH62 .Q B3)2ȑ$!H@hR`( Y!fB3'5O39C 9  id`3s&TAy3AS44O357C933=oz b JDSTS4M5H%nC;41`4qH|o3ftAiG5_36 Kr#K3bn6n3$ad4xT gT@j6)61@ O%X BxfBjU L!RDBb%2 3 Tn&ީU/l @ʠ BX@ \iTԔP hBo *npoiB3P r7IrupEˢIL7g#pW(JZN7hhvם^wvQHjGq  @5VjM#"w Byz5q |Bjtv)a UFW i>Q"tf`~*QJ Nf}  x:mNJ}}R(PTDR ~`b3iE)jy@` ajgl<8 <{WRLH. 7a@X@ Ģ `dl(Br3‹w { "T Xmje69äC'`fn*Χ#V+Z+" +O57@N?@R[ d"k!5/Vazl@l)" 3H~jL`Na $ʶ ˑ*>+F˴P˯$d<E2㑉٦p )Y0,6"t&^a@`;ybb! ± 1蝷^+fkYx0 @k! Z ~JxIk ,Yq<Z Ԙ"(YxPƥaڷKFZ ~܊ʬNj>  B  J @B@z{: .j @RBDrk5/97Y @<: ⪳zcyFYϹ_ Z*-uI&j5Ǻ"Vjj v=d{kۣ "䙞TdYTEy[W ` &촋 t ^1`xa3x; 0 WhZ&x @HQ T/K8G˘ ` !F JUUϻU_, z,j.,ü":<3лY X,#8Ax62#(c`; -G);.Zb3)"ȝ8f) º"z"@B-J\B:#\Jz&R :E--ɖΑX A n뾨 6@ a gʠvl7#b~S 6@HZmB 9*k஫W#./ c滾UEN"n\ r\x@ʨ,[g,g [œ)U=ڀ%6 ƕ)f  $% "y%\4~\ |d -=ߟ8`Lƭxuv T WۺԛCK@oBfg`xh࿝`>R RBl -(34<d.mB-8M ~2+Lʾ.04b.^ibcc俬id"Mͣ؄,?a>#^- KY X` b=l~~s A1paNg?ٖٞmi t ɜv& 6`>^ڨAi`f <&f@@f`ǭ?88 b Ab8x@2`('HТGc,PĢ U(p<cO̘s(g (<8 h 44HƋr Al2L!xQ';  '#E4pϜK9Z7`9LXD&2L=4xsgϟ "PzX Eh,p W.ݩ{ iJ/` $h1ɢ2}XhE 8xw#$6N$'XlAwA`AS,^yTD&:)4oZ$^A߅7wS/L PjPEIG Hj4\q% o Fn J=EXhKԃrxES葋0bcA;J%ZYr9wЍr1qЌ}xPYbGeXaPBqgtZ[fpA@\y[HJ!RDlԑE8d 9u5U&EZF@ES m0+h*.(@V&UUapE+DVJw|Ikт4 H*ٛ [j([AЮ{Qe-rZȠE?H"E1\͌E]^jE\# !}| y42r @'ub[#4ћ`pfw]wSqRj@0A+1pjtů,_SMQ$mp38oЊA|ԡ G&i+j2}߱.S}6ouuV!]ehJ ] n&8_|k>:hd"x4h}+|DID<7P$| "Nf BFh@R8M0=C 9 L »XY$p+ܵ &d! |ĬynZ{ʐa}D&^eX[ 5>a[0 ~@A cHJ! 7P#^ 567lAjЂ>'pA yDm(P (Ȉ( , B4כ+̡)IBN3 +8F:8@>!`G)x) x3NX!, j@ *=.8f4p?l;蓚Xf3eґTG;/ SR%XT {8hD#0ƛ ")0>/X/۴Ts:̔T-AjS`Ni;g!N:=H1L% (0TEP(՗jxtNtU 1lf]! BuҊӺ6*Q 1=(`ⰼ"`;J\mkPB8©XE-@b ٞv D e%,qEZ%vP΃Q"@ cOtTr$QYrv*b |k9vv]L%48Ոqc`%dRի jDAy{&S }0 br zJ/  pOg c~'K06/Qс+-dGA88/ÂaW@=?+W RK؄EPyb4- jQWy  7KL 2XS 0#71L~yb(6B_(T@(ȉ ȅ@x8芯K(m~YYp 8H؊ňX c#8~˘_CߨH7^!XXtpǍhLH_ȏG؏Hi ɐ  )Iiɑ !)#I%ijv+ɒ-/ 1)679;ɓ=95)>)CIEiG)zl!GK xcpMds@3f@[ɕ]_ywp/!/(6PVYY`Iuiw X3{mKp~ZyqLO`:h`kWH k \  )Z~P 0@{P/$a v0 N0MI/c( Bu`r $ɘɜ<5u2!0P _ !{P"PcT7=2p p;%k: ;ׄ[t1iړ)O2W` 10Ms9!홖zPmcR0`7eGIʂsO Q*  ],|v!Da0lA/2U&:=7Tkd+`z0CډxCN:ʨ,WX0[;f#*($% P5a.u``@a*B&gڨbثqCu=;RiVvEC*W=3FuCysAD:X=@8Ѹd˰kloY\EʬftRUCjnM{ˮ/12y|SȫjƤptPĢ񇎔^UXCxȨлλ795txR鶽pbq[CF;]MP@ζ{U呖_fiKƼkrwQo,-1碢~Z\hmh9:B}oß&'.u_bHmrOÙc̯#$-yz}囜Ҽϵy~V|;=8}~eekPQX_̲ѹGJ=~ͱ?@I9;6klpijn !+톔aiX[DvشɪǦzTsty57A㣣243UV\,.9FGO]BE;otPǼ=?GYZ`12=p%iZyڑ;[ꍨZТړ 5NlGO[ l:lIn:]+:iQ](eƭGѦ.KZk[tRˤhS,l' 7])(EcT1D;.VyPBTA"7r\ǙB9HSXic,ٺN۶tү"z[`+eAYav^%2|w}MzwYx)x\kG/w8>.Wngw砇.褗^g BCLMɮɧVdD;n1˦O!oRKFt3/kwZ^ڻ}Du Kj[H`d>}[8NI:r:d8ł!M1d M"'(L W0 gHET]Nfg~ACg!wҝDb%2"HŠ(o5W*RBɶ1o%Fاō 摠=wL룾q?#F!F4 xݱuc$#IrVD#PXN$$OD8n"HS)RreNd ZŖY%E R,LYȥL2f:Ќ4IjZ3*\K6tͤ 8IrvL Nuh;N:/4*9<"O$=ޜ"HJ֓O:"x$}BEf_ģ<9EGJҒ(MJWҖ0LgJlsp)ϲYT@ jD*ԢbFOMT~al(J vrՑ,$5hU?P܅vmM&ձxKt:UBNXwį9`KMb:E T0PŲ!Zˉلt5wC_ٖؕ6Z S;+N&Nl/U* k[)hB kܖAU iQNۗ*ytW\X [wy.\Eb-{_`6mDJKIXRVw::A?X$Yݞ'QM.|rrHwu KÌ J#4%O"Ɉ1ƥe$rrPHŕɎ|GXβ.{*,f0+d6sb (ɚwAyAp @ 8.YxTsW Px7]p P 3`yOS UzY*3]P 0Kڤǔ  pȐy$zp Š 6w _= gn 6@:*z 0zkj7/_0pzz z0:*J:` gPJ J aP gP皮{10m  l I;v} vnɘـW`dPe@vPx'r@)w` ss'hPĠ ` Pw2K6 `7P0P`oprk۶s'dk=rP+? r te0 Xe@w[+#+w(  i9P((PZ@D0w zwۋ;(XkP9 `rGe{:˳> yc8pp_ pZ: ǥ7 ba0$ k2P Py諾 +0`Ќ+k+ Q@jzJ 2`+0nyWi02 0z0P 0,0wG1L`F,+Pr x   g\7 s@wu wZrwu7u[w&ιss lU `0w)w:4`pB{B5x5匰]:k=^GK.w8>wu'Ѧ-Nwwwu& @P1[^+w#P957MwHE")Vnͽt׋T>wh^nwX]yݐ؏`; Ĩ` bxxCc0;x07 '.^Ǝp 0ny'T` h *  ->Ďc̪' :ݖM4kPې zOőJ0 0P ٭*=0F` 1/}K@zLP™{v0 qp{a c_`w~ vY~ #0B r%0sZ 4LwoܒtwGr@NwH&JOr޼жf o&ߒOwost|~ѣ^u7vgsg0Mr'R|Myp` P!YN r `Xs׋7PFDP >QD2&<4BQCG!a#"@9F\=CR^Х H$E|$R\H ,tȰV4NVXe͞-K@ZJÁj0PkC@wV۫v l 3d/^zƁ&8sS2bq|6 OfdؤeI0 p)L 'Y_> $ytv@ؿ>>$UG|+"l~њ/"@4zz%2*#:J! $ Р+0hؐ@9TD L@8N @ChƀAB!60LH!$ÄZ|1g Ä.Ʉ,F1 -`hH' Ȃ`i@X@HX""jO   jg &D\ *݂ 4ʃx$(+UWeU1{MZD)H(% Xh(7&uגpj*dV%*#ܳs .+qA1,s01@28Ԋc *:! ԂF/*$4PB98!!ԺW+QL_1^9N.f; <|VbtUV>hOzsǪZ@TzSYr P:C )ȯ g*!)$o~P@*!<; Ё\vFqԑǶߎCLh@ ڢ8B%߼ :[ԶvOjeC})y9 4BPW zw۽o~͵s vB'P%>qW5gmmw*$'Gyen!cСXusF1?xҕt']WyԥjASzցrwf>vgG؛vyp>wwWtǼi|?xoG|/ |%kWG 0yw}E?zҗG}Uzַ}e}u{~?|wG~|7χ~?u~}wdm`Gտ~:Q,p}?4DTT=Wr? ?Cj $4y@<tH<!$"4#=zp~@!D'()Ը|pz-.'llȆv3D4T<v&59:\@<=>@A?$CDDT3EtGA !,(*6QS[񯰰78C깺HJSjkqPQZ锕stz쉊ܽ=?I35@46Atu{aciz{yz~MOX57B8:DegnZ[bCEN㴵DFO!",ABL:qsxJKT̝rsy+-8bckLNWҌ{}ľYZbEGP[\c-/;NPXopv~xy~FHR۞FGQ`ah_`g轾.0<]^eVX`KLU校efmnouprw>@J02=,.9kms@BK˼gho、hipKMVuw|SU]凈䃄ijp}~IKTlntuv{bdjVW_XZacdl()/ l+,3d[t/12{￘ͲǧʭFH=Mw烼ļ{UOBQPEIbfJ795,A9@C:sxQY\E=?8+85RUCptP:<77yIBE;;A-GQZeJ:bL2f:Ќ4IjZ̦6p 8Ir6ϛ:ctL[8_E*.DA&ː9Hd AFr(!FDhA'Q =W1U2d 򔯚Fl_Ty!I1iG/E/a3J#E {o$jDd *tE.;Td!AFȯ[ֶ=-4 ɳ߃ YcaB,Qi15]<N7ܜ.{,/&HbV RoxM'ۅ7HT7 Yx~h?b$Fƛ:#&wJNq7E*w̜!I7RAl r(B2x| 9j5T#_Hrwd'Y`겜 NH'v-0 BǦ i/(菑Fc{O%ֻg{uoq/ R0`@ |'X /H B$%AB"cod؃ 5D`?{.!x {W8P@{ P P,p@bH؁؁V0HH`! у!^H 0DPp@VaN!BW}Cb {a(QJ-X0>zȇ~{7;qxp(aMh!OX@,b@uOȊhpk'.,1~P `\PVpVT`+p#P0aP q@s!hш20xtp鸎HȎHֈqR|$ h؍ 0i{S0,A)ިIvPtَ @hP)h@X#Y@@N%` P@B0fv@~P?O?y`K`P3Dp}8QQH șǐ9yT0f z٘y阐I zɗq Q̉ᗀ)!EP Z_@) yY@ et@ٛǙ C)pcY6Y2 !DW]M,rY9 o0t`'ij0%Pa PipY<ʣS3Z =LZx&jZ8`| 0aJ ph qpٓP RJ " j?ʥ^0 fP8u`zgAb;Q| uPk;B` PK0S'1@W 28@)I11IBJ! Jݚ qF᪰J d`)PQeP4:e@hv80ω p ?1haMk :ى@pJ&bY [@. Hq!ЊG{iQ70TYj4˭4( AAfGAxW j fl,6@`\{ 5Jp@k 6`pz*p{ps)T?02{80Kk L@RЏ99J/ڻP p[Fp(F˽DwzD x[[ٻq `[{dff= qJ|>0>@5 Xp~gXn`IhF 1/gbx`p 4Z +V&G|Dh xб?LpY @ YpêAB\I `p04/3|p \kwku|ZKy 2{Nб\a r`X'Gao n0Wm@8`U Y0 4 {9_a a f|У$W٩Y-+J3;Zƫ)p PT^A I&an ,Nkۍ` -w?*k; ϫ`@ m0X0 -`֛>}@" oPpg- ɨ|K'В0]g #,˴l˸˼,=#˜-Ў; Uyp@E !#D qL}kIM$в,/!, (*6QS[HJSZ[begn칺yz~jkq=?IPQZ78CCENtu{ABL!",㔕IKTz{aci~MOXꤤ{}stzqsx8:D`ah̟NPX46A35@EGPYZbrsyFHRprw҉xy~KLULNWbckuw|DFO-/;KMV57B>@JFGQ:蠡VX`_`g㊋Ղ\^eopv˅[]defm[\c+-8SU]hipijp}~UV_XZa@BK02<ЪbdklntǏdemnou]_f^_f<=GڮlcŤt{̱􈌑],-1ɫFH=%&.ԿMwзWi6:4AD:=?8bfJ\=P[_GotPIo,A9CPsxQ7|QY\EHK=y|T-I?RUCUXCDh1`GLO@koNм4nLKM?gkK@w_bHX[D`dIjnMOR@3N*>?GϹ/QBT J1H,ggl&*\ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲˉ{IfX6sɳϟ@ Jhw%&0EFJsիXjʵkTzijF^UEY&۷pʝKWc?`˷߿ I֢ҠmE&>xpƎ#KL剅/kvϠC/c^28<װc˞ 4۸sͻ2Mȓ+_μУKyسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(,0(4h8<@)DiH&L6PF)TVie\ _v)&`aVAg&խe#ti睎ؖ矀# !j1*裐F*餔Vj饘f馜v駠*ꨤjꩨꪬ꫰*~·e:[ ~ lkJrdFQ+f)y'覫+k,l'~I>Ă5 Z۠qvw,2no([rʡ %*ќ,<@BmH'L7PG-TWmXGTr\Ft_S8Y }l6WolYmHwxGB{[vU}-8.<!^F|B.9M'Tgw砇.褗n騧ꬷj.۳~yxx_ـ.'7G/Wogwo;Uo/?|į HLѓAW-@R|y$ؐ}L!ܼVDcOBpiaG`Ć DL p!vD,h&vȉ9F.(MY.\$:!Ulũm;bwp2OƿԱ^oHGU y6Ƚ6wL"F: y6HZ̤&7Nz (GIRL*WV򕰌,gIZ̥.w^ 0OY*tZ*Ylęց&FjĚ9EndN"D6mV&rrvg1'Yys/8<{Ğ]9RN!AeȝyFA!фTHF=rQdGGOc! ?!$-eKΗ4 5HA" $4$?}/2TU IRщ#60P3JqĪ(I)?STt-OIN{ԊPTHZ'Og3*/bÌ @]RW,4+Q6!gkYꅥ )FdiF qlfubd%O_ӑ|5+Kb̕'kwۆ`!V9ӄ56`hS -nT/$U VrRlUti2܄lW"@MΉW'q!ܬ5!XOtSR]t"[sڇt Y/D݄F pF,/'ovOHX3Q]! f GL(N1BV1xEL2# F#=bqC J0- |@L]c2ʣ*w.$X&?C0s41r٢Re㴹!ețre3h:<9π Ї&thEшv }d @ i"UX! {dխ6ȨWRtA1`ApSK86$P@    3 65[ 5 fd6ȶ5"(:Qa"p| ' c#;n mn;x5‡ Cry{#:C$TH69q^L! ?N$Y@lh[ql 4!`,!^8r^} %` M:BnЀ^~/H &``Y0H1 ҇D0+|z.>)<$jyA` I` P$}gzI%y%~FHԧS @^O2? O(? w(a19  ^ /$PLAH dgPof}}8X RPG~~awm70(C {PqlT7qpm?VA40T's#g^hAhpa4` my@ 036Ђ!H\hxd(r lx&d?`O fV=ǃp|]1{ cE(qЖP"`l(Fp >ʘgm ~ H(o(A uP`(say VPHrXͨ]6u `V`>5P)q]K *yyA Ijl @+<1o0)We A=`A-e8U %@0C0Q`P '<07uP(zq&)fx9È5o!` Q ,`@@  .17NA.׋) :Иl g4 A(y0B0B0=Ж:ij|f Ps/0;А7iO~yki {dp:` ^ PpirS@qvw{pq> ɑ** ?`)Pj\pw'(%zzy˙Qc FjFɑ00MQ>W_ Pzj``?#J_ApjKhl*{? P*0Gj?j 0::'7&2 0~T0fq#ЕsoNЪaZ#0n a1*6 V!Ɋ pzѩs z% \uZ5J,:,z!@0 Q06v iVRzmiKx&PTiĊ0FZ Ro!pPF9 ,P l|·.A0A 9MJLI& *;s`UVqi劳* -c@;q8Gd*+&07 vWkm,;q{L`tI>pwTwq&rGwvzx&!vw h A )\0A +[p {p2@ ƻw 0ȹugv ` ZPGHX!@ꗂw G4pВ[$ kk6q` pni[ fv;hhLj:|7 Q y0pCa[8PJF{q@JbckFGQnouOPYbdjegnFHR+-9BDNz{ҟ:BE;koM;=7ܻLO@UXCKM?eiKdtE5;4_bHGJ=DG<0F,TWC`dIϺhlLOR@;j!gkKWZD3Q)H*\ȰÇ#JHŋ+³*Ǐ C%ɓ(S\ɲ%F+0 P̚aHh 1] JѣH*]ʴӦV*u!գ"b׋YnJỌg˪Řbۆo]K.ҫv/v˻p/_~&0 NZa"K LVPvBm_4B~bM)pJWby=䥘h:`?#dm E!izT&CUf|Qf5Kwt&hс^doIכYڒs>ԩAXќh9Vf  ` hDD^tk@iD `TkFE*BĆ Qh;*RE (D#RhQjѤ&;d!/A=/E/Ϻ+8@]?'> +]b$/_/zR% \,t_ːz,03S@e?nH'+J7= NG-TWmXg\w`-dmhlp-tmxmwԃBG8BC)$Tތ7_w,TA 4JGt9|㨧z T9c iuΫF%P^V B~B9_!ؗwA_Z{٧pRO9Ex5A' `ڂr r{HdD@$0I Jz7UdKɠI`WER1!ATT,$2C"E8@PhݱdaE|Hl=Ĉ/Lƽ OăpHDI "D@!D$H2E o`ύqp>׷ ԰(4 ]dJR= JE9:򑐌$'IJZ̤&7Nz`}(GIRL*WV򕰌,gIZ̥.HQF ƁrDX́,!R41b:ё̦6!B$@7%2M#yH1]7l4‰IyIgA9*!k=OӛBA)2<1!DQɈMqJwЁ@O' &J1QW`F !ؠ+$@pmqk Ђ4ЄhH",5`.dH!A0x8 .Ϝ 8<__ypKS H=ڍvA_K``( R/D>m`yk&<-6%ls|pO 7 GDAD]/*hppF Af `0}~`O`7~:0 ]>09  d0 fc+Vop:qk:ח}W (7G$h~G|-4X7}Vт4 #n`ux&8GVRf%#p-apx-FPmPM(m@pG P0 h6`r'gl{K8{Ioc%`!V!"Bkg" (`DR p047?:tQ X@u! ]@t :fk8 aшЌxQ8jP0~Qy( [@L0_q$]Pi )rp`ㆆ T D' 0XsP ÉHgVjY&eAgF[P9@i3 ( 3 0&A 0@ ti hXJeFI  pf)iau*yzE@L9~ 1ɚ8u)wC `Rv &Ei9ɑqi+`@%B֩ T0N@i{ xF0y`YHPp鹞О-@ ƙ? r&†{y[ɕNao.eAGS ![1J04ŶrpC`){وP  ~& i<@* q`ХN(@`kquPaPn| }g\&z8PKpXm[_:HLzI ZWZSA:{ڧt:PU d-N:Q*:]= C=*JK YJ4`Өzy JC2 SPP9,@:pflP*GrV0zV*1C**,&PW8H:V80CcZ|ל0ЯVhuA`a*F|U0w~b$g] ?pfF ;dɰ; QA'+$ .;( HU0j0밠* ;<@1i=7ɵB amwAOUkc9m حӆxކXX2KfZa<-{(:e52dY1YQ:[Rae@[/وgl>P0E`fEf0{[=xZ (&p%Nm0aļQ1QKϢ ˹KֻCmp8N,AZM;WP@ U;,pWu`<m@lk0ۼ%%/ D HL@6 +6$a%`v+[ 4uAR[*TSa ATXKPC;o@+ פ a)\ 40su @cLd[N4@&q<<r*SRLʼnrLbLƗc\Ɣ9ZkK,Np\Ƀ3clyX˴f kTul! YLF P{`֫ 0 8 V_и@o, @Pcи:`@0HaRO_&)PR@OF } ,7aC g؊ aVq-N MO  Asx!ә ] A1MRTWG-5 @y/]&><|\+`о(L0,ah_6jZյ2Č.WPttr;Lv ؈Vڢ؎]Uٴٔ!n,MY(*6QR[jkqtu{ACMHJSqsxOPY=?IBDNkms8:D68C֚>@Jmntabiz{46AFHR:uv{^_fUV_̟nouĘijpwx~ڧ24@/1<-/;Z[bxy~_`gı}~Փ,.9egn[]dhippqwTU^cdl+-8)*0+,5H*\ȰÇ#JHŋ3jȱǏ CIFc&S\ɲ˗0cʜI͛8sꄉrϟ@ JѣH*]ʴӧPJJcϓ8Vʵׯ`ÊKٳ5AT۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘3k̹ϠCMӨS^ͺEd@Q. ߀3t8ȓӵУ.aҳk}3z%_|{/?(}(@5ƁEF݃7}理v[ĀdLVLxa(4h8<@)DiH&L6PF)TViXf8`Fn 0afNfFfFlzUxԜt矀24W|4h`^c6*ervȢVމԩKAwVj*c}CJPzMB& Rb_AEXBTn-APІM)0v xPkoTP2Cж&pC G,Wlgw $"b2˧" F8l&%)@ pC 4 PAPOP@p@UQA 4|P/*`C, @I*P2@]sA]mPKT݉mU tA  PCl=HJD9A>/TT<uE"Ԙ%ed|PɿJ3ĀAO_Q1 }a$B=Լ.`p M  &1A)11 >"P1 !Xp ]405@`# A  < 9C(!!H'Ha7! rBAB& bA j0$* aXE- $0&`")V0Β #r, ED Bj@RE}ۀA|!c&YG@ sP4 D XXЁ ؅ߢ@%w@@pBP(Dc8 rH]ĕ@:I"3MҔJt˒@DN$xB 'V@?#PDWNxViR,A*W@Xb1UD:v$1)JS#Ԑdb *K)RT, M%H тXo!<@@)+x_@ F-R*5 qY E8AzLs p(FQI=r0HB x@Sx@-`Hd`) Mvm}kA ")YP`b4l -r @J WY dE@\ K'l@p#9xi0 DC0-dZ`88Hn1 X%@"P*EdSUITٔ#YM@h<8y`, jCX!Zr": | 8BZڂ>8` u̐CB BAVaL@ã@nQ$Bk W)U.ߎ>02l!d@TP2l `i2(i^dMZ,L(hBAYjX3|BIC&hES!E,fĒ0DMzsdCyu;4na$!ChU ҧUv lRy<& dB)J$ B!0`iEn 0 g] B&< "<Wk|7` 녁`0: Bt7yr@4'WW9M~%D) MH N?y40@Pq\@`QώGz8Ir ~(%jHL3k_&b(#IO`3r 5{&*/ub3ᨘT2> 0g 03! UI, Q#s_mYe v`PudU3cPVUvTf yK9)F@7vBОB^ЋʉYu8YR81L0&`I nGP\v^Bprm':WPl4( )EPA(dXE!joQ`.!73 pq `S+|b}@  &6 76 bXZ}A7,)4T$>Mr sK7J)sQ^d[Q[n1Ed[\E5AnP9V0ȧ~eNY0`sy@_4FTX:hwy4*@+ E,>u I6/cSh)ɮ ^B`6ДZ@1*@Ԗqc2P!'**B< 1)1eQga#J$[! w/W3"'+9++5s%=);1бPn"6W3C?>Pq0` b_qbkV:*`rP P'] rd<͙r0r7pv0CpZQ H*peb+ۖh`L{`j@pV {};aqd;۹%7mq+eZ@*8D' ~lHKG' ^d[˓ g*@+rC+2*i%1D“Q2 t5lZ5Q.b ,5DT]1 2LL,AB/ZDl0s:ZQ b@%́1e0O p H|TlLV< 1_7t:0 o<ȀDŽ|ȈȌ5Ȏlg+\ɖqɜ>ʢl|ʨʬrɮ˩ʲ\ˡL˶˖˺ˌ˾̃ \,CƜ$\ C֜ͼΔ\Bn!,3s(*6QR[68C񯰰qsxHJSݢ46Atu{jkq8:DACM+-8=?I24@BDNΚ57B:@JFHR{}ƌ13>~uw|KLU,.8̟')5Ѵnou/1_bM}038رEHA䥦iw|WotT`nf`dN35:r\eiOmyefm>A>hlQjhAD@ILC_|[|Y{YZ^K.A-]N9<=X[J[_uzWcY]KU C3N*H1H,=oA{:e#H*\p& hq#$t8pH+;P\ʜ9p6SGM;{(z8 *=aҨ7jʵׯ`ÊKٳhӪ]˶ Yj1 DC!IF@C>tq@8H L a&XPP5k7\[0鯮8СD21`Ë7B@ jνËOoɰM3/B x F@ (t%PLmgUP~%GmH ov @Nх@؀Ad*"AXuyސDiH&]yL52͂!"P)D#P.Bi!XC!\!_P ,` 07"&@3A H A"@y6(LB/Yti -xA|šTa C 0A#8ADA#馝+ +r)@";$-U @p@d+L,OM20d'p2`&@#I/RH)I+I(+1+`20 $%0( _ X`'$fҥX{BxBV 0]υ7b!"pzZ1gҼG,+! u_-s Pbm6u}y5FA%[o/ @<#+BqƇ+@@_4cLAG* kX0B%Z xPZ'?A4@>Q Y6 D A IT`EPl+H%.ЮHDo҂ $HA/)俁PH ^((av7x` H2B@b⍣ьY/1Rb p@ !;(.q4 a-J MQ6IiO$)1N ^Ђ A*.48. -L f'\^H P T}LaIU"* [ (P`6@)Nt D\.Hz8T($ {w@ыR C%I`gSA;`?Qr @I'}3@`Pڔ [e !@| +QL t"0 f<}jT X0@\)h@F\A2 vRIN8L4H1x  7+}{5CͷAt# QD3)(A/AY pSiT LZ ül,ÂP %h@)$@%|ܲr %q Iny[1n2bf ľ@- @ ?Qu;.b(C ] c| +bD0"ZlKSHA|PU. #Xkrҁ BlR % y !+$y:WwJ! @Bpr! dH 17n6H b aB&>?sSy3+a2`0H~X`t<.pgMk0J .DQO!X(!J<YQ<$p@Qkuin !0eRq 2@o-; AD|"  tF"qx[Y!%0US v`F 6 -x#*7#AP| H U xAyHԧNiԠzdB B`x!}qq^'` S@ 2 Ĥ uL` Xwꐏ'oRLcc ;v!/͛OZv }dĉ*Ͻw?!OO;ЏO!Ͼ{OOO_ڹ~?Xx ؀~8X7؁ 8~$X&HW-8}QL ~}&+HhG0H}B+0}MJ}UE'؅^H#b8d~)uq p}Q0Lzp} x~}loA~}}~}WٷQاǡ}7e؊(s8((wW>k h ݷx(ohgHȋ }'~}ۘ} R߈}~~*r}-#5p#?}H(%cX %p}`"&U}f&֐}l&p"'tb'x} r}}`P`x0L77-.b**+7+3}B}},G,Ƃ,,,HiR׷"}-:*}B}r}r}wP/F)Yvzi0 0 p1ا11"C2g}(2,20#30RH4W}>`"PnI0+Ɍ-錚4Ҩ}V5W 5^6W63țl3!p#7}}|.}I-9g}8839U83w#:j6ɹ}C}}œ{ٟ&j~<<=Cp}@p===y}c ڧ>3?pn3p}i7} @ DCWA"}G}^$Vy}+.t}1WCQOԣQ0 D֘ۗwE$DQ4EAKt}E)طx|}?FnGt4 GطG} HGHǡITIpI4s0+z` 0ښ8JZJmZ?AjCWKKN} 7LWLۇLL0JZkp}ؤM/p#:~}NTNN׭N׷ {PŨyPz}}P Q )RpXܗxJp P ߷RϨ0u}2}4eS5Yhښ}A~K}5T'Ub}WU[zm*:{}<{}fVm}nW7WWW7zW~Xw:d{姶%7YŠY YYY}Z&}SOpw[ا[\\*ʥ*S'_\0KG]ܗ]]Ykp'^ڇ!\}k++@]eCIX`ڧ__{}V`ȣh۽ge8& קaaaC"%'*jw/6}/<Hd0hЕח4O-p~MWe|p`eIFާ˃ֿ"eKf{fdwm/tFiS}6hkh}F ̲6g9LiwiU8jv}&hv}Rjڇg`<%8kl–Ưlv}m}זm鿼x}?親 @G.p8 }.0t}on:}E,s7}< pVz}Gpr4qr#Wrpr)rDDžTs:GL?<Gtɼ?}\d|ͰؼIu ~a7v}g&vow}ygvg~}0xWهxG|\'v ~o7~}}}}]l~ׅ{`(f*=v |.]6M}:>6@=i[}F}HJLNPR=T]V0,q](0/j+qڇ_h 42_Ͷ xl$.8"CDw^G=pؐݏMdM9H] (Pb81Y~ q'7jp5vAV"4۷ J@ae0 2740_>q-܆^a= ڂ Qڧ:!ڝ$A]5@ Q}׀=a 5516-&d݁l.!A%`:Ep)xpgdI xPp,B;[Q/r} p{]r@?qXQh>h_$]]:ö}tف\ty>f1~k V?5m@QT aP6pn H0{!@![n}P _q镡af-0t8n P*.3^ q 膎n>Pa4+>S{pP n^ھn9=>:dȑ{؇am(>7rs *49’Np`?#7aPXWp@[)U>M n>bR_pEsؘݏ^_M|~s!$KiQi`x'j7Ž4b# P_m3!Z;"p/a2v~{  ݽq  D /$ qs n]&/(%P m?n>e9/o;Q'/|OACn"nNi1o3-9xȨQ-Y%W%[%_&c¶&em&q2'0`@ Pa g?HfIBr`G1J 1,<@C0.b:Z>X"SB&bÅ dǀ.0,ЩpupBys,rz@>˞M+ /ڡqT_l$L6*  T.4/kX4DDXr5uk&b5Z% ?x%)Jnv9@ pA-qXC6bOzBJOZ Cleq% q\&rH!$ X${C "ZEI\ >LQpҩ98bZ&ȀN& yPb<"YO@@,P ~a=& b(5` n8ņ?.hAv"5nhbeiУ Θ H{e p!(mf탤ϧj9_h~rPI2I|8uA1- !@7WvwOmH3{` 7րsW6.A -a&,`b`jz` 8&炚D1,:0A3Yɶk# $y*ԠIfL &(6 > ! xy"&1̡ ) F sp@2 hP2åT',7!X@jЄ.4()UPPU(>=T40Ij `U Nr#DBL14x"UUN))YJ|@5!  #`}5-8!T5!0ѱ1 #%փ$B.3mk"<`hFg?$ If7ك@  pqY5fǸ{3SFz&6[ Ӟ5YLoPlXÚ8c4k>B!qKeP+xxPHnwa8@HGjȚ E5{n~S%3\ j ?8¦DrAbɣApf&}`Bx)B )3~ɋӘCA"O &8X@6gt  C9؂tSl )tu L%%+ P5а#!Atpƞ:ի" 8H!ADdI#@D&i.@bSD$ A1Ptլk YӚJ _R0A7L o)v7}D k욻*0 8o] \ٴ$ '!+@ # L1yӂD9#`pIG[mK`ǐ`ZӐ@ V`U`}-9Z88V9 g DglK h ^Y/A$MiK39krcS/ɸ=og<_gb'wK Pv @ nZOCq$+>к= `FP۟<'* HV!zyX((0H%M(b"4t'< @0P3@4P/r 1Huvډുi@Th:d*"Q' ?X#h3̣H*T "d >LP/TGX6 g I`k;2jB/C11?= HusC0 C&|BDTD):T!$B0B#C:B;@BJ,B8 BI.4DG 0%[ԣQBK|N A8FHC1E[E] Ȍ|<ѫ)z$`8%G0$ GLʥlʉk*Z\4]C=\CȐ$J(т"4NGI)/Cbm\0Mx[/@ <&yj54bD@  !@ AV vd'4&Hþd$d:A`*X3B @ [B@g X<L̘)HhKP<yxpNt` 5NO| ONDINii 4f@`QhHfIL+؄W+`*׼+Xy] (&@}1we%p+xN0dx(HY yhyMpK = m$ZIHz]Oȼ!v}(- ?=BPBX>H69IYYKΡ4H(!MSpMDB-)؃ÑZ E @FH7xϛv==Z ڃڃܪڿ=؝5»!K\9Ipqܦ́ MUE,0[\e];P ȝڽ͝ppʅ)Xx$` uC gkz8 LjW}U^<崐8YzeAЂ罗荠X3GB(DC]E|&P@3 p +e_a%Y[;] 6[>_啑 X=]] @*R xU.Ѐh$=$^& ݤ%Y}Ef.U(#zx(h a ec]BB TC8RmV\(ehHJxO؄MLW(Rn1C']=( ʼnH,0N 0<"+c,J8`ꊎHiYKU  TT(ԭZ8@Y"؟8 P+ฃf 6.$S [49P'8{g}H1pכjR}rtis6gmf1x h0"EWRi8ikHjp$V@.ME RZfbXc`e֧(Z]^+e1AHH;Zvj(PpseXj &QtZ(<`R>e;l Kb`(ӾlΧZFeRRiǎ 8 ɂ$)",oo6j뚸h~mecN> 8k>_dBb_ccxb`CK(xb(8)0@' _jե xaq XPa >`]xS?NmfK]z(QMUm-ܸrҭk.^fz T!TQ%-_{jx0b\# l9ߢjV]oc!sNZ0缪Wn5زg4Msg/v8ݸn4iɺ7n1b={]h{/6Ǔ/o|k}oe/آd"lhyj-u7w*G؁i^vI8!Zx!z !uvFw7U~q-BvzXpUYM_A 9$E|eItE qamq/1y"Vإczu^~aߔ(^{9'uy'8~L |c|!S>>~Iq']'):O*y΅ha^ݗF@ e++慫kj,*, RHl6;-Z{-B-z-;.{.骻..;/{//rF.v5V "J"j `I#Ljajjv2 Y%# \r>X ʨ(|n.:\%EԲrhS1~i[Ywڬ&p5}f4gǝJ7^Zj(j*6X-#"&-qmit ;[W$zʁy%cWiLqkyߨ1+[RDZ6遛 ̓9vZb r4zfĭHo]h]qeʸ֦O~̈́|b">o^2"lX<+z ԾlOi = @zƽ 2A/J$*W? pS/>Dp@\Yi[`5cs^Y{B`tFvA*A%c #d/cҘ[ĒtD Ht=/4@O@0T'4Hbqk!檂-l,ՠd0͎c$XPI$&T)Aw Y:w쟫$/S@#!'a8^DX9ʈjD 'Cd8'^.VC(-~3Ii ;я̙ K>BqgB 0Ũ#bOhVuG \pq%TG|e>uQ@v|çDW8r <`" xV tI;mp1ET/7rvk;)=yfq\"RUrMK6v;IYEMnP(4h*ܕurҜbP$U*˫6|˃Z/OTg&ĵ 4 Aj+9?GZ..uByQT,geV\D~,4P*5fjW?ci$( B׻mM"'9m,Q rx2aTyeb}\|9QC*雯&54}mWX- j2g1 nƅjߙ1D\)9]+RxSIc.ys 2rfJkTZ:ږ-Lu>2ns׹.i_CWarIHe <,`0\  716uYmZdF[?yس0Cr{4TU-j^N5eLS5k#^Vx"9cDW#CX@ *` "@C D @/ ,;.D #T!LA(@H%LcA @H 4c9IV\A"B(V후n(J@!n,4(*6!",kFH=훡cġĢkloƧ[_GӽbfJAD:XY\EKLS=@8˰RUCxƥptPjz̯ʬsγy|TCF;IL>^ĢkoN023]ȨͲ{}uлѹu_cHqtxRз{UaKM?h弟ζf795sk\rwQꆋZhdeLO@oǼʨ_oeiK[ȫßҙc缧ڵpռ78@ԪϵǪUXCmIJQ;=8%&-YZ`mrOz_fԾϼ̲嗞b~V~,-2?@H㲳9;6ijnhlL|ɭQRX}~eek:iII)bUz%P +34 3.!7߂ 6 GE IBL"F:򑐌$'IX̤&7Nz (GIR-6<*WV򕰌,gIZ̥.w^ & @bSt1f*it4rL0!DH43M@ gRk*Aod5Nz@FiOx&=YejӠ@ ̈́s}9 c IA9zVn9І:Ԡ%IKҌ&LH71ԣ AMoМ>T'=I_JԖU=i7y*҃ (=)zQp^XEIT;ҨƔV]RzzajV}\)> Vy5<}kOS}`QvriW:U*E,dW|=lYZֳ.ֳjD~T}(fW됹֮6UYX{*t!5eYۑbDqmr#m?R:9}tZ7ս4znKMz|Kͯ~[_LN;X4)'L [ΰ7{ GL (NWyqK8αw@L"9.&;PVL*[1rw}2h6NrL:PֲfYi),b p2aBy.сtivk*hB3`|06ΰpBϺֱX` a0\gG"Qn8wŠAX67xkp ;]x˛|]"zUzmj&$ 0%(oq*4683^y+P\`6o zdq͋pqz` 5ype)``f{ /C=A%lzAU>_8>ʁ'+y4/ ~WǗ<[޽qNԏw-cN:~s|D_ vգa_KQhh 7؟_aXl~Q Ads70QHs70S]hzxVfC0P^Pp`hph赀 %@^!X4+yy嵂! @,0^,P^Ł"8^` 1] PP(TX^K؄1W':ȃ楁u` 8P vP^fxiH^wB lzЀVB@ _p U B0jkjzu]@,G^KT`ɐ^NH"e{ vP?"[ltp'``Q``R eu[ p`ƈ(t(Ű[" H|q6 R` a@/' vf s р(` v@$ GP 9af@acP&vq`1#Y-\0aP y )+ɒ.~f0  )fp<'YEɑ@ { Z IЕ_"n] ܕ 5 =x~9 ^=P]9{Ufzuxj@ 1^9^% `: 2Ѓe < W q 1㵛Y^T4:a`&鵚`&dr 0hgY䵝dFuɹ jP  =j` P^ٟ9^`!#G xm@!P!СU DP @ 60䅉赠/2h<lpK&jwx0^9y^` Z@ q }` ~ H|GptysI/Gl0F Z@|` cpg```*)tezifP`?[ڥ)PH|xa F ) y@WtuF PR@"@J @az> c a[u3Zk` {` ЪʫzJ ~ @uj*ac #9ګꪰI*Fmki!f7bP0 ]PK` 0{b b])|cW@#8^l0P^ap u I^Up畳;kyw^``x5h/yuiuM P뜝 bz \N`30^/wㅈhlkoK۴ㅵZqFK^{H^ p` p @^K^%!_ #@hV@5 +ڢg^ r y0^lj/h`jwm}_ {_l:`k9_ P`ثؽ /Pt|P   K֋`8` 0|x auPs8P )P }g '@&ܕ#\v`aU@2L`[PsPF kWvEf7`jJ0\a8@9\P bLf bppe p ~z~ p G]0 Gp%ɔ8ֶKk; P^v0^S5%e浴kk^V;iRPK^N  a`pK H+z*AE)Ep&pͦ,l^K+k^ иʣ\^^,{ +#0k9_,:^p {[^ܶrWe/jrKK͊Ӝi^k`'п K@6  {?__7@U{c_RX_:7BMF| ~ 4mH$\l. )` q|| `>]J~*lv }]mšٛ PcP\0a.ЬR G@f0|lU|Ŏ p> `F|@] ھM$PaȭtubwP0 '0b]ߎ{cP ]EP MF^< I ^<^T˖˥T,ιƌ̊$N˿ ;8^YV 0> 2y<-.^^>p^|  NE~UB0^z MkB`6 .Z^TN0 A0^`7K^/ij8GF `>P_Ps+Q`sr@@P {%QWʍ5 G- __~s>d-f"_`q@ vja؊ۍ-٭}šk\a( 캽š^Pm 'ar@ؾk'a PQܞV}ۈ$V@0}mOa]O` gb_0V1` Pp)& .]&}6Ieg& 1%.>?x~׶*BUCZ^2^^g.^@JJἝE ,<^fio^ooW!{{^0 hk/xor- %-A2>{Zq^<^>`v+)t_  ` @`v;P_{vp_L'dx>-(@m|MatPs^v^a[yg `>ѐP O_DtxDXT, X@} (0 :%MS@IRHB* d pXS(f=@UnfMO-3WhғM>% @TUB4V]~kF. d玠vVᕬY h1W N ]Q &\+f`†Kб $0 @+nWgA\(2a+.,V3m{Y-;@_3o`W;1(m2%.Y pI_?;]8F.@!\RV+!$Ch`~$"! HhVNk!4 ?9WյӂȠ HLEUUMi[J-2(UW >T ruSeK=5U6iv xW$Q X`HzėL'JLJx6ɔMLxⒺDHFNeE&9+KzԪW61fjc0&0(i+Ab9.P Z@ڀ Z iDAH4H3ģ 桓~VB6 n+*d9 騳;(pPr2=ʊ"ઍp!!\81rF"  lm`3kýV܆7O駧B#(S%_8R0IH E7%/ṡ>6}Ӝt}/|DRXoZAN%Rԥ2/lq 8U `XIZZ$pxu+[XH4]pW٢F $0Ï g-p¡S+z \ ,h^ 1C \dĆ ^qmH|c$a)SpLbE cQqLɗW$Vfɘu ! ,2,%'Uʯ a\94B-oKRfeP)!-'?4z3E7PjKVQy(HLbetU9GEd yH3J(a/)A>҄Dq_1ŨMqIXcSOS$5jSbT|›]B`qX4M}RUvN*Vg&֙ED ̮韰իo=C:Wծw,תԯ_ Q @' lb%ձ,VYnUZ,PYvֳ-W,l mjWֵֶ:I2ֶrBַ/lpBVEnr\6׹[mt;]Rmv]v$ox_V>׼Eozջ^V׽o^W׾ůHŻ_d<`Fp`7p%%7JρFthF7Zh~hJWҗteVDГtE=jR?SկuW̏V#Ϡuuk^HYkbu;!v)Ȇv=mEmnws>=nr[6wսnvw=ow!,3(*668C!",MOXQR[ACMjkqbckYZb46AMNWabiPQZ񽾽24@xy~IKTtu{һ@BKrsy餤=?Iz{FGQ8:DBDN괵DFO57BHJS㏐KLUyz~}~_`g>@J:VX``ahghoSU]ıegnOPYmntbdjؐopv+-8/1A>5:4XORF:<8FLavq$h(e %`0(4h8<@)DiH&L6PF)TVi\v%P8})diHl6UdGQX.56)lpuzbAt``p`u2b-GS}w?Ǧ 驨DOm*kJ)B)fX;*R*Y,6;Uw"TkJtHնId@&J A:G{8hkol'ڊmӋ G,Wlgw ,$lE 4mR3T3G !ܲ?;ij l<$)5Q}-G@,SMQkyM!M9n"\I5P/jE%Zз zwH3mHpN!z='5^PeU d Q6T.9A@9EWtQS"U c%Dݤ J:&P60jCCуVfD#ưl#ޟj(MJWҖ0LgJӚ8ͩNwӞ@ PJԢHMRԦ:PTJժZXͪVծz` XJֲhMZֶp\J׺xͫ׾KXBMb:6>yd'KZͬf7z hGKҚMjWֺlgKͭnw(pKMr\ =s]2Zͮvz xKw%P7_޴a=`|߁췿m/23J/[(@q0O)L a hvH rBԃ sȪ<6 @A>7$φ` d G NG9B,/DA5bj H<\G!: &H m>De )6EH13rD!3?$6HR 1'd@Є47~@_cB F2 / 7su8#BT/;CD^0M+q97XB yw An~*H2u#W߈RuP!'B=rYh@.odI\\(^U>sܝЂCߒ,@B xŏ %o`|Cby#y|A:?u3CP/@鷃r=%~KR{^*#E?8?9W@ W+B/W)|\5N_ΛIPwW$Ѷy`&'X-@!|P1}w Gjg n2g!(Qwafoa/nvn2q$!q5pQo*I8!FkfzA]g{%  u! V^xPhnpAhGu(4xf)um}xn|x2s&60inQo1XE2eaSvmeX1 hXo q9de6+{Tz ]w،8\p؈И؍֘ٸэx(](An.q30 P3-` *2h2 ِS0XXqژk Z @S$ Qq4 :@-9Qp "`LT( (R&q)yѕ1x.` $Y]/3Y:)<> i)З2y@!&|`vɖmj!p{`@0a\4 w 1QUo h` @PUx2#@ii;ٓ?np!i31 t)ɜq鹞i: fG`1 I>P qHPJ ZaQ!jɞJ'@99D,*ZtCʢЖ] v PE@w 0?z &Io; E7N+hipl70X0Ds3 {)oP `3uz|꧎& Q} :ZڧZ)"qpGipjPPcѫp'*gZ @SZWZ|Ѭ j]JjxڪZ@jʦ9 ڗZKNQ]Pݸ]*_*?УKn [` ya!jP '8 Uv N`۱+ @& 9(;";;%;A˟ HЊC P`a}'PaUJ`X[U{jʚbK@n q {=,qa9u+KKˤ{0#@ ^ K+Hp up)а sYD 86 P 1Ku +:+k+J![ @~ {AM"닾űa!k{In{ + jkii{˽]:;T0˥  ?Na 2  0;s> VBp9,ˀ;A<Q:›Ly* ,S;0_ʫ)AfNAZ y< mzȃ\KƐL]LF1p|;þIɄl!Ʀl(sH K!v@e p [GƙpHW `aaܰS1 3} ūl`,|ϗly[ Ȭ̞ <[,A !Ϩ|¯ P-fp:,úL10 W ( iv `x <OQ\C]1fIdK|r t2`J-L}ɺuq#oMLrs-5}ә<&* | ~PJ@F͎+ K;A}ח\h ٧;S !]l Rp91={@`A8^?^ f}\pΈ9 ]ݝzԏ `|a-L{J>#p'bٽݍ8q^nAmA, }۹.ᔭ #ޥM}q i>*N" nA-%L` 1 HUNTnZnD /fu ~Y^ hgk.K^o~rQNp{NGUJ!"#I Qr~^3`Nj7'~X!n[^Tq뽕!x,4(*6QR[ACM!",񉊍鯰stzyz~68CHJSegnBDNZ[b@BKDFO=?IOPYFHRYZb~8:DIKTxy~MOXPQZMNWĤ{}EFQkmsuw|\^emntghoKMVշ/1<Δabicdl13?_`gSU]opv}~prwvx~KLUҒ+-857B̄`ah˪bdjUW_זjkq:@JќFGQ訨-/;efm46AXZaijpdem<=G괵VX`[\cԭ[]dlĢ{d򈏕`/22ﴻqvsnsQ̰FH=%&.\϶ɪ,-1haeKx|U~Y_Ec=?:@C;낇YԽ139SVCƧ|W]LN@sxQ_bK57;㻸HK>X[ECF;685eiL:=:0E,DGAkoMOREhiphlOC6:49;6мQ ̳ILCZ^KѺPSFżfjPYZ_ϹMNUH*4mÇ fHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ mѣB ]ʴӧPJeYtիXjʵׯ`-r[9fٞQ>L۷pE$x˷߿QL È+^,!l Fdk̹ȻS^( K1vF(6q,YgnM5I#_μУKNسkνËOӫ_Ͼ˟OϿ67\h{y4с 6F()G!LJ]ZY (X"7: "WݔA3h8&ADX@)dm6p,TMIPFI桔va Q BUBTHHIIP\)BWix|矀*蠄j衈&袌6裐F*餔VjLט5iZL bZFJq2Г}*-Z kP:!Џ+AvlDn-YMѴΕW [GNE;:uKk۔X/KSmqjY0R2ARXu2I+> Ul QǩQ)-<˘m 5/2ACCaY~1}P7LIB]՝o3m+6_N WoK^So/o觯/o HL:'H Z̠7z ݐ>uxeVITfD;8"CP#7|HKB P  ٳ ؂"GH DqK[N%dLM3B bAd# O M72GMJ kK 5ĬAC' ERCФd̹EQURW$ԫfj1,;Rx!&Ad*S3&]J1.{_ZAxATK¤bYV+AVđ C " ճE+ QnV"*TFź'A-D -e32@{mM-+¶|K7vȯ~{dG$|!" (D (@$ <~$Nqb4L8L xO D)fBf<@25x"c,()#n= 8G@РHҢLB/@`@$ Cp$` L@ @ ,`/>k:`  iISZ @aKTD@s}eyr*RxA0JD6|@"(,l XX@Pp@84<*hA]fGY5Mo{s8 nkZ׵Qtƾ_sB(:,j$~;b P@AT'I 8l n$7A@ DB'zrd N8tA BÿL Ǘi q;B^>C0" #2`az&P a Th7L  Dep,cz#r ||aC^KA@XaM Qnu|:kSMk@'C"Pd@%z/ %hBB0` K0 1FHwy{ h A{P8gY`f, \ +pցhvw{Af1}gjyj`kx04 @0]50 d{ .>0O`e0<# 1PH@wZp.QvFpာ`(0!A]` &S Ek6PW !A:a ÖW x!9/l! Pio``GvP>'X-k h`y5gF`7H@aJ  M0p tɑHgw F'xVȈ"!=T@  k$lA w iq(pqsaЏB @P$`Peo.PuilsHzYkzV7 bI0zs0kyٓ#wq (wQ/9 !T`bGʶt"9Q q/p9 &R> ^!t }N H `d0j l4٘a 0| aP+*h-\-lf AGi#QZ(, :qp`0 F`V?ӢR1SЃ.DZDm@:8>^_f{TV^fBE@uXZaobdkm`sxy~8;=㖜eבcc~Zmnt=@>ɧ|YoqvKr;=Fuw|󈇌^6vO\`bh.PB/1<,.:snklr河Bd-0857B߀RUHEIB,F>Dh{|JqRUG13:Df?^kAb/D,ADAMwbfO].L@u[\c;X|joRLursy1[E7:<7{P@`2`GGmFk+?;-]8RrxUO 2fJ:V&(4T C=o:e#A{3Q)V3L+5qMZH&L[‡|3E DD^L(B0Aʜyr͛+i Q!@Т'(hQd9CQx@@DP}׷^>pQV -]ᶅeU/AHRv+L{bʹϠCM"\&XM۴C0P1HHͳ-%`xe*e:1yT:jL`0DECِAEDk >cg6l`OG<e\t2%P t3@R0!@"p|p`Z ʅ!rhA=@*PĊ-(/ e$mMޕXf\ny2x% Ae%DnGq2-:0 H hFkv &hi\b|%}tF*5;Cf`@gP lU &8 A (8)A\ '*dZ I,EL&R+8$p9p5RL7N5l@dA$Ci ]T@EfBBa p:pWfpC,1A G(Gf:SXA:ѐn0)J0t -iIO:M7Ʉ<]+LЂIB aVzǬ dA.`Y 9kLHT H@rSNѫ@:~/D {@6,Ṙ@1إAJtY9= P ^0x&I[ʘb:{` 2Q yypNz˘(#P@1u:wl@0E4x0yHJ~`A -C>\:HH"*ce w'RipqJ:5 |Ě wY!*A x3y@Zz蚮n` A %z?Z%V%{ ۰;{۱ ";$[&{(k,۲.02;4[6{8:<۳>@B;D[F{HJ[NPR;T۳3QXZ\۵^`b[M;f{hR{j۶npr;t{e[xz;l~;f{˴+۸k[}{۹ еp~[+8ۺ[;ƫ{ʻۼkDmjK9۽{+[˽曾껾{[k;{[ZB[<\QL ̴l\۽[<l l $\t& .Ë0\-<6|ỿ8|5>Î+@ңɕHJLb4RMLB͸Ҽ+]BMԒ-|MΜLPA a֚@ѕ,Ǔղl| ͷ[" _P, R5L 5@ x8PLvik]˹LFMϚT\}cRVڡתڔ<ײʷ-, =ۥ<;Mۧk\ȝ\;= xzٜ = M0Ȉ-%Yv]й  P-=y}>}u}.k-<3 eٮCԸ *>|+؋ }<٩<}=K =dֺ䡜=>D'~טȬ ]B~r}`~ =M潝hڠ])>bm]q+.[L/1JPMA>%^=!NM$J.IY`mM<̶}ن~͔.ܢD҄ ɝNLmlsؼw x} ޗ-n >,lG&֨޵ n.Vү>m~θύ ./o*+ E2L M<^vnTN^qj^]~]Nլm>en:onץNnѽD^3 <%p}=JP#!o.%?X}HBv=At /{q^̲D]Js //L\꠭.ΰQ~F?Y>?]XKͧ/PO_YrW_\&o(_s@@ DPB >QD-^ĘQF=~RH%MDRJ-]\SL5męSN=}3PDETRM>UԊERŚUV]~բb͞EVZm$k\uśWoиCX`… `Xbƍ?qdʕ-_Ɯyd͝=ZtJΣMFZ5ҫ][JֳmƝ[7ܾ}\xÍGvq͝?Nytխ_;vݽ?]xW7^zѷ_!L,s!",(*6kFH=훡cġĢbfJkloƧ[_GӽKLS@C:sxQJL>Xp=?8˰ƥзZptPjY\E~ʬ̯sγRUCz/12xĢ^8:5UXCɬȨͲ튏\{koMѹ껫ӼܸqucBE;鿗fɍ]텑_{Uhζ弟LO@y|Tokͼ㿗,-189ARSZmw{ehKdheu꼧>?H&'.;=8Ϛß_bHa+-8mrOм芋wx|}ghn~VǪ354XY_}~⁡f_GJ=Ծ漴Ƽ̲ !+#$-hlLX[Ex{SGIQsasijn뼛rswzTɪ24>OR@bcj]01;FGOMNVLS DϹ缮/C-_`g2J+[Ϻ-5h R УK."n{s,Ķ 'u{_ᆁ οBJ 4A B)TU6!Q[(OA|$(∝C{ Vg(PRqh+6"C+ԣ@;s&VmVEՅAP{Q&2Q)P79%D?tHiMh r)WlgqRBj dRODΗ 硈QvT1 ,phERZUB E?8YC&DA.tgT*B"qpMCk:P;skg{k,VɆ@ lVi܆e+IzK+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG-TWmXg\w`ddh+]ivktǬCI)0u.hhDQB,G.6xcI[cN}.zDB #،)*nh SB'MF(34: q>$AB6[xc4 =4 AKD})I5tS8?Gd 4^ HH#:@R2(b4xalq ! ~zGx09 aN $B( !H Q$p xHK&'1b`HElU̢sE!n`L2q.c<׈4pH:x̣> IBL"F:򑐌$'IJZ̤&7Nz (GIR*WV,gIZ̥.w^ 0IbL0f:Ќ4IjZ̦6Ml 8IrL:vOUFT@JЂMB*n:D'Jg~sͨF7юz HGJRWdJH-}JgJӚtͩNwӞJ PJԢzHMRWyR4Dh.\Fu(@bx Wkխ>W-fZyVBC bU:K CPP@Y ` Ą!US3Sх6` j; rQrv5hWO7N1 }ʖ4O3|Im*Jz=5).K'$  &ew^Zj׬.vϪKӽA/p_`Υ `2cK `BkM С>cg֠0A#@MͲ hgu.(j H%c3d:c֓~)XB?gdVV^g9q%!7ܒ>U B8&e ܠ4o-\ޅ嘋9g^9p- @W !lS/3?ЇTL&1; dztF528meH `K @drv;ޱT;! H|8Lo{;OinYaEǝ,"x`EfYs\e+!!,aX&mr!&̒K+4<߲@x,KAd@%B2<t2sIhl1{Ԣef$@8 &00E-.AjNp7yP.Q |5-?ʼn9Buh yB|389h/տ~AlN4c#@ ?PN,6N@7~w~g}wj mP ~~}6p1HH~~@ +P\`iu4NekʠJ[@ְJ. 6rI5 +|闀 ɘvj٘vPID >JS~Y}Ii` UZ5XǙ6Q@0Z J@I` ahJ ԶJbhmIPWKn@0K2p m(K) JffVKP/wPVftN` Z_Շ 2*f4 cPP!KuVdu8У?ڡ*^*K`I'ҵ ׀4TjP_ppo j t$ U  K`8 Kx_Ϡ|` _||_ tu;~ȡOJxIM7L X Lgh_M;6@L0My_0f ZִMuN` |tKڭ O:aNJbP`t0ԇwYij Yc+9NpI@pJm`#P# ijZ+[:f į:N TGK ;Ni9 N3[НdBJ@ ~ 뙴K۴O;hk@Q{p T0#uP=@ 'KB: KjKqKK6JKdD#ڈDI  , `Pp]J gAC0Ct_k ˤ 븐K}+KYॴ] K[K[it v$ oЀuHbK" zZv8Kۣ_e6dW_:qۺ*L @L``@L9L `Pb]p,L5̈́T, |` L3ppz5,\M P seYPD\D< F\E,L  `jcy jW *N&a ;0lN pc5 : k5fZ'K}L,S1K^LNjR0ɣZuᴳ0N@#L&P`̤ lPb]U-*50 )LJ` ʹ.P]w}``=d/7e-WmMP 4T6@ԇcc N-dكLȑIٟij4s N%!;٘ ڡ, -NMN& \'N}ɭ@XGKT`OH  @ JUmqm4 @KмdߵJЃĺKKNVk\\$uXe0KK?_P8K ]Ah*^$ᲴLKN-M+Pϔ*a5 PLj`ַk0MrLtwL~ ÿWQ}] و\ŮE`pٚNRի&d\̽ %0 UPb\p P@hi۩Pƻ}눼cGNN ٪N䝬JF d` -@JH ` `g N5SN@ 0MKY ypl_KU`VkN xe_$1nWCr0R=AK:r,,Vw3>P_Ky0U_r  g 4 d Kа 3b Q_rи 7o =Vf @ `N G;f~ piu J `z0zz.0%@ pv@@Mo/a oM-@`z< Nٞ+} :P+ I& .KPb>>50};Fʩ!Bj~ч+#9 HC)|p `"ՊP(4HD'VC1L5=:@ҥQ=" L%2 CRajVLyai~H +M0`ܘś7/}W FXo6)ǚE:eŚ+Vv8L.ఈ]k0t euk8 D@ ƆG>F* nNzԉ t2Ȑ%@ _ NJۿ.$& % S {_&\Űgh uH; G&h& 2 1E ֪+ H#D2I%dR裕%!@d>rH`h@ŃJ(4XfGZi&0$L4՜@@3j,S",2Vh0QG%T"@ 'Z!r @0E)x@µv`YS86q`a/hT(Xȑf`rIW^}6'eu[iG(a%6v`l'`DU  ?H+0֘x-RE$Y/S9gDn +^guF:i uf @1ĢΙi 31- 螛谓B+G* [$17Y亖@4q*rd ,W8ar5OW^ϙgoG5r=(w3j 0Ž>z}>r'fx{χp k`"[`}`?0gV %nZ 8Aư΂`5AvI!7BlЅ/7C֐} R(hV`v8D"шG Љ9DbA*VъWbCſЋ_$a++0ьaըFQoc8G:юbG>jOڽh@:PԠE(23hD%:QVԢ}719iHE:RԤI,QSԥ/iLe:SR3>Қԧ?jPz"(GT6թO7񍕒JQjVUv)ꨮUլgEkD3Ѥ4ok\:Wt; ]Wկļ7ҼհElbڎo|Aŋb%;YVhG!{vֳmOZҖִhQZֵֶTke;[֝nmnu!, s(*669;QS[ABLr]oMQE􉉊CENPSFh޽`dNmyz~uzWWZJ脌`57;jkqKOEz+-7hmQFGQ~Z_bMstzotSORF35:{vi]`L79CEHAx}~?B?r!",u;>>bdjy}XegnOPY}fHLCMNWsGKC@B@8:DCFAHJSY\JRUGfnfjPXZaZ[b`@BKm46AqvUKLU셑cclqS8;=e}mntoxy~-/96vO\=@>w|WJMD|Y^bgO]=?IaTXH~_`g̥k[Kr138OzpVW_prwSU]:W57B13:BE@Z^KjkطrDh]{|߀ADA?^GlDf2bHLu*::|-J?htu{opv.PBBd7}Q1[E,D=Abz:==_ѲJq()0*69Hn᯵o5qLNxIpŤ쿚̯Fj>\=[8Rзӽ]ptPoT G?@HϺJKRMH*\hCOJ,H aĉ3GY>ЖH"L˗ 7!D̛8GaΟ@ JhQoFKʴ.xIeզXyk]>#٬ KݻxZWX[ ˿L! gcgdU( %̹Ϡa DXɌ5 (0CJ p "aק?^vRE idfp3AU5nk˟pDFwD]0mVHW4Wxv C>AJOJ1bPq8qi5ue(xЂY@1dGzI(R* @gH H!_A`VbU |PAcfFZjEXRADPHgЍz #Q|jhQ@4B$Fڥ)EP#MJRav^,JaiB1 ѝsA+FjB*F%+GTu%"pj[:2 tV+ E[+u.QSѻP/Qn9*g7pzDG [Qˇ+MJ,)Ng׾'P &A+rA3PB5?W"Ӓ5|SH'P0NFrת BT{rQ<uVItuPY{5Sq|VdAz"CtZ1MGz5}5Ar$7|_?qgw\t蟗n騧:Hz.n/o'7G/Wo w/k?Fߒ3  :( Zv3A z LCHq#< W0]cH8̡w@ H"HLD,%C2R`6XЀ,"B0Op3r5 $"`lc rȈ!A(d/R$eMQiH)6 l!Le6O\C @0MB% PŜ΢ ȔDR.L 8jZs ` NCIrMhA@P挧ac @B;x$j2$=T2[m"L$YA*.et=]ph-c "Ց3{ ( dY/^ PJչz%(qw!fMC`\"Q 2 $@ H[ᄜa-I=2cYDj&\+@ Y!s#!%X"Jv%0ƈaܑĘb'䚗,eqRW6' eH?g |'l{oǀbl1C`t"Z! *tKO-D; [#kVȣS!@YNʞnsi12#:c]'g% I@1+D4l:B|"GyE"zץNXWYwCկd wpZCDԮkmUz b8-C B, O'/߱Jox ۽ aRt I 颷mcb %&'HGOϽw}Ѡ|#?K1_>|"Bҟ~  `,$X5B 0HERW $ cVHl~rP&2!0Q.P@ W'2P PF&P 0Mg K#H*,h>Xw0~JpW8HpOC ~5(p}Q#2 }34 °*` uF10<0|. @9q L0#Ai7@1`lp( w -W p6(x@O78 N4hPA 8{bgBF  Ar``⸌{A :#!HAfeXV} $!g0mH qy7P1-0,0%y!IRx) RAg;5X1G@8@K @ `8` aa :BFUIC98Xd fI@iV!5[(P Q p "`wP /0Q  "]@avi ?pIy p򚱹)ȩ A0gtx0)m2 .I扞Q癞1͉s@p0 "p Ipٜ Iٚp R ( :%ʠv2 m)_Np#apA _ _``JKc0 DʖD)o)qi6p^1 Wrڙ~y!z   1 0 { pwP q/ 6(pk0R \V ow0 q ǰ` 6*ZzP*0|P )ڪ j" 'kppW@+pꪰ qg2ʮWڮA%LbMaz ڬj۩ɪZoV 0 :"݊{-K8@e_ d2B#dY]_0e0 UEe q \1 Hd@D F _0jju{# #0|z*y ko7 ]S0n L |h0Wp۝;˹۹k! %z*"j1&ǻ"ʻțAQv@ Agbẖ;ƺa/A a c 'XJ:@ Q @fЌ N@;; 'vp ` 0;) }d;$l(NF[+ wz}!( 1 W0 "% )P+#p0 y(C0@q.Qjx _bL gkƃ<< *PJ :jnLPʝ<ܽ/*qʩ;]ƍ\!iʬ|Ⱦ;1 +i80(+b0j)P$w`@&@DP ``8 4 ~)l+dxlLf M6L "`mXlOeC }"e ΰĐć p 1l8-d-&q b0@ \"8_9{,BMԣzԎlLeH LR֖̆"[-P/@ N?3w=yx״|;iw؃= j=1$p{m3-؄}ڕMA9=@J/̳qG&PCAPgrd8Pd DzP'*L,ܳ p mp/d`e`bim;-=mM{,% ' ĈK Ka~<WM#@W :P&a L,$n+)׀`Xf<>Ⱊ%}6.5̂ PɾL #^Ia *n,7")0NPlFmA 7J@2KzGJ 4A `X S۪B ! P~佔 =pnD AP H .^ Avz` 0PHW.@ n"'P>!~w&Na W0&.-.>Jz =p&`$P 1~> /NbM#n Ys?R0N6P-J N]0fhڝF _uMzYvPM [`@fB\|4] \d0XBaGKF^5M -n{-L > ܀€ > Q3 hN ` A?@`2z@ PPW.0ϞaW` /1+pjoA7p5V;"Oz PnO_ /_x|` V`OY PlI@BBB$LT'F<꘲c@qh*V I*YręSFlqBml INe!F6C!ʑj)R5`DDWt&sC]:X\jٺ+oBVõO$Dȉ$0 ȼuFZj֭][lڵm[@b#rmv-Xhy2B.L@(/7Jq6yO+<=?8oJKT=?IHK=mmnt !+_`gTp)\$10+7+BP "ՌϠEZhA: dadžH;l;*p-.Hn16vF8 -Al{Ë(zcW9@]P{FahN@=1y H<] qTW"t>m3,NeB0$@4u@9Xc>rv mL"5D4) U*chD)]JbjT&h : 3QStMҹtِ(ׁ& A_4 w6d$JbizF*CDd E `KONj@”*LiA*U`*!I뮼ZTS ^4CTl&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DmH'L7PG- HXGAeo$mz+& t7lF]|SF E`7v7x;T@>n埡%Qw礗.+1ꬷ.n/o'7G/Wogw/o觯/o?lBC7\!V! ̈$62E0#HQtxV#%%$5$ HužP%)Wp.<+  eHrĈ#,H#ĊȢ +*F(c$ 9 B` h"R5IELJ!o|~ CP1?R"H$HL!H Hюd%)ʴ#7JT0cG^IVD@.{A/Ėe-%&D%AHh|b | 0M 7b9"xH-rȉ"0H9guJnbDxI!z(A' "(0>`OhI*|Ki'DF)s#6Gp5ARͤu KUbRHT??cъ$1K1`#4fNz 2)!FAfdkD"_=grM1MHX:d$%*AĮiBZ*\H`ӂ'@*&%uQ!8 v%R%B2!T:'چ|#UW7V"t+*V|3ks(VaUف#-dEYD",Ḿ6%OZڙmָW$.RbįZkO<4_׿@( $/3kYnH.]4)G[ScL ?^7!f,nBГ*&F$1iHͪ{rA "D b g%4F_ S!hFp vrXrU]%})BׇYq RgĬx i.n |3AhgX! ItAJ50 RoD ,BPꁰZ$6ȫ5k[Gr2Xá1 ʦ ;)4>f;fMmjGgȶj{n A = AsN;}cN9| # t0){un#g8H٧cTN#%TIGmq}8D8 $= 9HTYԧ4A*H'%WDdq;N? DG6`FY$Ru>!2A@a'B%'rA"!pQBqBb1 ; $q0'#r38#C8B+@2!V'qA{~JzC[a@P$nqn.coj!J% ߀G~lG 7s{X; {`  `| )1:.az>b%1Wz%j! '~ psyo"{AlNP(9;P)'0)h  w Qʁ@aט'`H7hHH X_2рXrx2~Hx< w(tr X#8w,i< 333@Í.ᒩcģ349<6y7³;!*, L(*6P{!",Hm*8:Mv.L@-H?Lu4oM7}Q@`>]LuHoKr*69Df1[EJq2`GGl(,6BdAb>](08Nx'+6Ks3iK2dIGmFj=[,F>/QB8}Q'*56yO(/75tMHnNy,C>Jr,B=NyKt5sMLvkEH>+-7d69:\Ģ022PSE񇠦h_bLnǨADGILCSWFWZJ !+|~*+/鼜IL>lpN`ah굵mot;XUXC()0UV^uGKCm~gkL_6vO\]dDh]WY_-H \ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜI͛8sɳϟ@ JѣH*]ʴ)Oe9JիXjʵ'KٳhӪ]˶۷pʝKݻx7W LÈ J̸ǐ#KL˘B̹ϠCMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv (U T9L*)8hH"<XKLC$ YRI.)T0q2&%"Y ?~XdL_ʔX4U&QgD%BQ3a҉Y+2Av$Di@LE@hCqAe u"A4EmʩG[ TjG@a EC tkkHVTɰU$1BP*!Ѡ?)ABl I-C+6TCc"An{apmA4nD.I@ХgDIRFn4pPW̐*C:pCE EDoTT/D)گoMv VE!Ktf"$U yLҡfԳ@Fo$E?BQtM3aԵX_;gqJewODS;ɒQētJ"LC17ZacTDcCԸJuLDI'9Hoy+)WN]+Pn.^DLAKC;4'-Pj/U̬N+#ib~A8֯=  LvCv |Al.GK6 KR0y*忇hxT4UB0 $ 9H}a!8̡w@ "W@"QHL"&ĈPTH*Fb.GԢH /:`$xOpA'$0vH=V#C* Đأ=r" "TOL1IIJGL%%9Y"" A< p ظzIb(]Ab8R.NAAa0SCEj69;46:]QS[LPEORFABL+-7o,.7r-/9zyz~X[J^aLDEO?B?m79Ch~Z139/1=Z[brtz`dN46ANOXegnabiEHAr\ŔHJS`jkq}botS}~hmQHꛢfnouu{}=@>FHRslGKCkp;>>`ejPfdo@BKKLVvw}8;=xqvURUGKrx}XcTWHbgO()06vOmqSVW_ٸ[^Kxz~XHLDGmDfjoRMv1[E7|P_HoJq]_fKs'+6>\=[3iKFj2dIOz (\Ç#JHŋ3jȱǏ CIɓ(S\ɲ˗0cʜY!B8iɳϟ@ JѣH"y'CPJJիXjjiB *A(˵[h9} qP˷P7HÄ#Phcq0(j`*b"ئV,],C>5qM8R5tNHlHnNyHmIp:V.PBP{*8:.L@?^Bd(,6*69-H?Nx@`LuMvAb'+6'*56yO,B=2dI8}Q,F>3iK+=;)39*:;(082`G=[Jq@_KrBdGlKsAbJqDgJrHoFj>\Gm8R@pH,Hdq(h:˨tJZجvzxL.zn|N?'wIyzPJQGjzjBG£G K ٵ r #FÇ#JBnBv&jȱf/iɓ(Sj c?0cʜI KQ.ɳO>Iѣ.ӧP)4իX:2֯`Êm$KhӪ][+lʝ5!,;(*6abiǡ.PB]LuMvAb'+6'*56yO,B=,C>2dI8}Q,F>3iK+=;)395qM*:;(088R5tN2`GHl=[JqHnNy@_KrBdGlKs:VAbJqHmDgJrHoFj>\IpGm8R0)ͻ`(dihlp,tmx| BȤrl:ШtJZHĈzxL.tS[Z|Np)|~,~NnFRɽЈS]+ o(ÇG0HE!,&(*6z{@BKDFOVW_.01[E+=;)39*695qM*:;8R5tNHlNxJqHnNy@`@_>]:VHmLuJrIp8R di@+*0tmx|pH,Ȥrl:ШtJEXzxL.z {N~n-'q2^*pq>ZA[AFƊE&FD* *'*  *r dD*\$~ #JHaA+jȱ㳋#2zIIO E%<ɲ˗yRX ͛8Ȥϟ@sѣ C!,Tu(*6X[J')5/18FJBKOE-/868;029x}XY]KDGA57;MQE>A>v{V7:1[E@BK5qMDFOVW_8R5tN.0]:VHmIpp8FrD:ШtJZجvzxL.znIG~RuDwxMVEPSBľ #Jhˡ "OJ!iND{*\Ȱ9HH 3jȱ#)G,Ƞd =\ɲ6 L8@  RɳOM1!p ~*]ʴi KJh0ʩիX*J֯`ÊuسhӪS6ڷp6ܻxD߿!,ed(*6c,.768;KOE/18}ZrotTaeNx}XILC9<=>A>ufjPvkoR-/8@C@+-6rwU=@?FJB[zte{ngq|_bMw|W029^aLuzW|Y~{}j[{hႆ]_^z{@BKDFOVW_.0>chOEHAPSFhmQY]KJMD;==DGA57;lpRLPE.08FJB[~Zlenr}imk|߀僕d^az{fjPotTw|W@BK029x}XILC9<=DFOVW_^aLuzW>A>.0ϠC&נּѨS^gְc˞53ڸs- [ȓO !,ed(*6QS[69;rABLPSF]57C57;`dNstzmDFOMQEWZJKOE_bMo35:hmQegnc46Ayz~~?B?@B@uzWORF')5}otSHJSh}riZ[by}XzGKCDGAHLCFHR`jkq~ZY\Jw|Wv8:D{CEN+-7謳nOPYs7:>KLUƄ]f?AKc`xqvUŒ`mxy~cdkejP򈯶oYZb,.8-/;/1TXH=@?emntmqS^|Y8;=٥kRUGEIBprwaciklrghoxMNWv.08:<=pVW_{|z{opvPQZ_`gkۤjgSU]`ahXZatRUH;=F`dMhtuzj][mjoR<=G_}ZijprxU|ǂ8aC#JHŋ3jȱǏ CIɓ(S\ɲ˗0c$H3Û2sɳϟ@ JѣH$häPJJիXjݪT&LRiCb-9P BPTSIS:ԨH`B1H $(KE;WsF`0 * (Z ۰d gmHL/h&>$k⭃c/ qepCfU;p‡ݗI*/ z>@!\ >&Ppmzlф*qB ,,DfBY:WB  !@ _Vx!mCN$r4-1)P(P$ Sǐ IPDPuFS~r040DE pEEP'tډg0CAM P DQRP ج,+MAۨX<!TA@v|/% C$aIp$@MDxC"I6PbPg~}/P>4!A| ^}+@w uTA`E7K~:߁n:M>di* A|`r0a 0Wp*-D$ fhQf(-c/Ёjrgz P4cJS0b"bHX:HDي~\ⵁ0d ]4K<њ kx&PNxP$p8H~@ ` 6)n#aGL40GDd)YC2$ c5p@@^&Y( %_ 8  PS s#h\)lLฐ9Tr)t_1Qd*g*72j1O d ":;(H2T4(,+0D})sʠ ")|Q 8) DT< )I 4}"JճeN`l@t !BH:2h#Z "U؝`W!XZJ` |Q_6!CH +`׿ĖP.tx_ ac Vh6!\D'Vur $lLZ,F }mUU"\ a@4N U \d8@`C*`ϬQcAp0K8E8^$f6hC@թ)I _>, 0AHJ!.o 1P*kCh\* bfFDs&n2 -hA:`f `hA 20"0$$A˙-x" 1 05NU,c0 ` IH)s =39m_Yt3il nhH:f 30A. "|3[F9΢V4<3`Hf X$q~b&|irٺ}){q BIo  @gjA^A![E6) a͹'e@.{$$~b;®p'_CD?8_X1b r|u:7L.' ps B/ev pid WP%q-8xu5&Id Xfgdp~! vQ#wFLwzwif5P ?Ӱ$ #x9hYQxu~}amy L Bn# cP |8 {  y&AQ-jYc k9P2~yG ! Iԉ  Zp.췊 pqYaR5P[ps!z 0;/.Dcp!OeͨH@5P j`pP 7Ϩ҈YOFP%(I(xp`.h x04x.pZø;8i a0s`KҎF"xɸ E ' P *OQ +g` _ m5/ PP!Q2 {  g2 _pgS _ .p p2  n)blS0 F#Qq\0 $i pI+3I~i,Q =~q gc o`TX /p KwǸ1 ;xو)xRpUx-М霴> {KT[r ?@ɝ`qɞ)[̈́ 3x01Oq0>y x٠ OObF a` O-D(Ր(P ޅ51 Py q u \_]_E #N ʗ |RC!lPT 0q P+uz]P~g~:~-3"! Bn@ j!/ mca0BAn#ĩ ԩ؉5ʪA$ 1R`! 5 1aK"^x7 ' Q( 蒬'g`O! 12szK@zT*P ٪:R t  خ=!yW T;q iPEz 17p (ۀԱ;b ['^s@ ~j@@;1,'D W.[L"ۯ$۴R;ZV{VPT۵^O[Qd[f{hTa+$Enpr+!,ed(*6QS[69;㪱mPSFn35:ORFhmQMQE]rWZJKOE`dN139stzhDFOotS')5ABL}_bMNOX?B?z`@B@GKCHJSc57;~egnႽsuzWᕖjkq~ZXZayz~57CoŶZ[b]`LrY\Jꂇ\uf;>>x`mCFA=?IHLCeiPv횚`qvU惠i]}FHR.0813>v|VlmtTXHKLUJMDCEN㫲nEHA7:<*,7.0.\De91JbsJ}3PT@ <`p W%1Q%t/R =ЇmU .(P.|vTk %q d<4B!z8^$"IGCB (NXh` ,4A,u8AI-{psb@-֬GPTPYA[f9hz @Wࠃ@Y4GsRН@p2pp10P'.A QGYDt }+J-  *`6I_GЀBOp @f* SCmH`Cy {>y~iPZ ~N pF[d_0$6G8p kq-qsg(0$`Bs,L 52 f2 tnC*h S\`Bs=}w8.;pI u.8F2? K5ͳϐ #P R#[EB @z15a:5` Q@1-DgQ%hs1i9΂ ," BP ʚ54XB@Khrۘe@b d!B <`E)63$Є2 u'Fq["?fg-d@!d!;`l0 څ2A3е4 D / *GpW6 /ٙ7ƑtXT,(8`$39-ә 1#x-l DF$Bh-[FgJ-t(DVBD!(d }4L!0 iїEE`  ƅ$cnpC ĢWUvU/@:ն=DDk P Ic.: 0$"c pVomˤh*N(Cˋ6`(d |Q(0`a ߽ ` dJ dHtakY*T\e -y v(0b$H%A "  Pd0jl! C`= q@znyahchHH?@S]DN Q Do~*ʠ,fRԡTQS%jeP:rp*e5[fPlA m5N 0,MiYX*6XuzLiA p@xզJJfrD|b "@ /d^gYڅ"k±c?9R4B ,g:Yg4f"0T3Xx!Je.v&8a7 /)P pc H,xC-@%‹_( mT¶ 4"8H!"ҲabNa@@ cS,[l/ Љ&e) f.,Ȁ  Oz LH, dF$+K z!UBt;}Bt~2(0\Ȍחt3sz=pD A,K`,̈\00 cF 0`G> h<~˓~h7 Od WQ F~ @p rq7(#Z`JP;~HPwO7PJ6)ba `:H3ZH&f-Z v[  u!t>'xPxj0,uQ `N1 E@uAqQ>DQ@ ,+,1R? @І:4`,p+<3SeQ&Ƨ 4`j)30"VT6 ƒ}Wj0> tV!G0 QnBG~PH=U7ZpIHe)`ZF_ >K0e{њ= g$)5Y~qaqgX%f=p y6TQB u1nFJ``opJ1i6gD2a `P1@$06By`  AfRN@ %\9?_;T{ IZYkR[sw./`R!M07  ] 1e6nҥ٦ort_? e !Wv 騂z+b|}pd'|ڠ?r"Q0_ a Д73ZܒVEjbwqh{+Bl*W'E@B {V#`ں#Vd}ܪ 1[` *"DP+FpzxBWFW}5!{2 Ձ*{k5Z ";8! ,j_(*669;QS[PSFnMQEr]m35:WZJhmQKOE`dN57;hORFstzABLILCNOXz`?B?egn}r139GKC@B@c]`LotSHJSDFO~Z~68C_bMsZ[byz~;>>x}WuzWCFAʷujkqYZb46AoꩰmLMVRUG`=?Ix13>i\nv7:<惖dAD@qvU`Y\JUV^]}f8;=CEN8:DFHR,.7p')5|Y;=Fƙf.08{IKTcdk^+-7UYI\^eg.0<׏󈚚[PQZprw{?AJv]EFQ[rmqSxy~bfOacicEHA[^K57BfjPopvj߀kvx}uw|z{~_`g{|klraz~Xt`dM녢jkmnteiPxܽ`ahchOv{VEIBtuzghoSVHkoR-/;_X[JrxU|ijpbԄWY`*L8Ç#JHŋ3jȱǏ CIɓ(S\ɲ˗0c؀m*#Jv߰ 縠̰*XFVx!; {tsNyBcHlp8kϿ̾ EXBEE D nWG qYX0(#Z{@ I("D --|a dж>++P+H+9$J29F)!R4Zk @ ;,e(eD:FmsoP;4@\Ø@$F#O A҄~2P% 5Ճɩ5 .ȭF+VmM dA/ؠ@׳Z)&%`A DD,!@8A@ 2{cp(u@iCE yBRFˁX2Q`pې Z! ;~w_~86t=PޑG66 ͒<P=7n_II$n Z0sLmY1 *dЙApTF@dVH @c:6TC#T%c rP 0` '!2 ~+9UAf|@`4m~>B3Fh7 HR,@` ,L`q6|rLA(G=я\8gb44p:.$! ȁpA@hP` , ` ީK,Z #/`6JW2{)Lȴqx&9 1d,49M!h?! 0L (x3⒍F`8@?'a9b +(0j/Af (V3 pfh4| K!P ,|!/j&BHJJH#7XAH_4v5[|H& .^*D<peUb8$ȃ6T$jM8@)*t¨I B }AU,X1lRA X,xA䘁wV>DN1#h"X0*pĪ-l 6hJf2`gp C@m6ia2 5 T @, oq3i :D =]j "d"t[ 0,"d&1Z|+rjG=4CJkk@(P=t"nAq P##,=؃b ;Hh21 W0ê1_A-!U8b ک&r:_k@3083ѐ-@3g8a .Ghp9 "sm֜xKv-#isqL 8,}0~e p( eV@$ CD3!c (#ΌH]Ұ/pi,T%3_1[Z6X#Hd !N!؄'gh)Z-!8KҼ .gA!eq1fgA x Pad HbV-(yqfi afZuR M"AЀ,8WwoiT0* s,5x(JxN(4" P.4;Pm!?%lÑYp ˷ W!]3;zBC&q2gC}}0 p}Z V8~׉X| 4t~N0Wgr(c `/ jHHw1u V70¸daO oFq`0qx!HqqJP1-@yWLX89Ux' hXw`l7Lfdp9fzǢLј8. !qqP?%q txy YZ%ਊ*t*B_ wj|ɵ?A$. (Op1f^O@1̆ȚuVku>`o@ )q.3"Xq01"a wRa  R r`  گz5`aNB h`,{^T$3`K[DkU";&{&k#,;\-2;4[R6a:<۳>!N,gb(*669;QS[rMQEoPSF]WZJmABLh`dNKOEORFhmQstzGKC?B?35:NOXegnotS酑cz}@B@_bMDFO~HJSjkqILCr~Z;>>`yz~v{VfjPY\J傇\uzWwy~]`LZ[b57;sRUGumCFA]if}x`68C,.7=?I_`gvnJKTLMV^|Y󈌑`YZbAD@qvU13>x}XCENEHA-/;{cacjFHR[UYI13:7:hq D( -8B&)R:Da[4X`qZAq100!4EQ eEA)' Qiߵ6X(edD߭z~ _FY `y tÎmȁ@fC PՏ,؊$Ul @G.XPK2PvFZ@C}|fBgn㘓cXa@Ca \i%Fg AC} X\&<$ߍ2- ^CJD6tӟtME; xkoq$p=3Lsu@Њd$:e";l z1|kk0m X[GP, @x +6Zlh0#q:0D h0[N(\ CB> 8wPxFqc8@QHoJCd&BQ> 'l!1C ֮!6I,dߧ-H73Y5]aR,xmE` %@w&-A&Ʈy"@Y 1N+bYDRIJ- D`dhy - f S8fF Z,11]j#Ce -|4uft-/wifp0 -@@GvdC[J @la8XVE&dfE~/lg+ ϤMmkPXr@AXq." !i'eLMN!xhCQV9q P ȋb6HF` W%c,R@4?2u{mb 0X0Hrׅ%7yI'0%ph3;(E?zҗ38]47`r.^YGcLw&[U*V WhHDʎ0|(,PA;2Ŋ񬉋cE x/ 9A!˔ mӏd{]:!3@c@@#0$HR6byt1PK2go / xxLA~T}'Hvڠe8vrGҁ6w~р>` ` '(D4 l A!UkGQP(1@@ Gn2z7ztp&tSRc4t:c@SR2y |Wp W HipPE"6b-9 tڣш4pv h~X-."- MF @s6x+Jq50sfX7Ќu l` HK(g"(xg F]X0r N)Gs[g+F@F`  1 |k1A`h`D{ /Q4`_Y:6 e/46Rfa!hyxfy|UE"0 5N[pwpa ܐMPĖn ah!X_ Ϸ ,1q+'!4_=!D njdEY0BP@"99@t!ve|њ,@wpt9[)bіoya('8pTP[ɝ5}8F#kR`(=N yPzC `@ 5cC=mz0cebN 2` 8dG`sڇ `eFz  ] wA6&AQې'] 8Zʥty@DDf 43V*/e ^mz-xQ )d@ ":'=#P3F] gpIׇ}:yrc:3k@0zW$ `%+O* H Hѭ4p#y0 ~ڮ]4-:zxگ{!AL ۰D {[{)!,8(*6@BL69;QS[r㪱m]oPSF?B?MQENOXyz~r79C+-8ORFhmQzILCWZJDFOotSh57;35:`dN`stzKOE}i{_bM`~GKCFGQZ[bsuzW}tv{;>>徿EHAw|Vegny}Xnou\]`LJKT46A8:DLMVuCEN⁔Y\JjkqoHJSf.09ɪXZa~Zvc8;=RUGqvU_`gmeKr=?I]=@>ccdk6vOfCFAUYI;=F139ka_dhO󈏐[^Kه^|Ygho[vAD@{p*78mqSklrfjPGlacjBdz{\^e03957BprwxVW_bfOvx}DhSU]{|pPQZ[Jqg߀?^DfAbDGAk`dMMwj.L@joR]1[E,G>Lu[/C-;X7{Pe,C=@`2`G-]8R;j!2J+Hn2fJM:V')5VJA{T 3Q)Q 5qMGH#F[‡@3"E 8cC4@pH(Aʜ qP8Wb *|CF 6@QI Tr&U2Њ(EY%@pfMX!+KWu.Kp<;Pbr0S7^fJ+psϠCMӨf6gܢlE?>tdGR E@(@lˁ}S0H4'2!c{oAs gd(@ 3f<l`IehP@ `8@T@VGESZÄi0 TJXzb<ŠC0H@j`*\1Smey\v` 9H㍗wՖ WP t[pSq5r0x Fpbz)z`t IZ~:A!16m@IlS@t!.R+l ( ftkAtdK&lE%c\i[`6j覫nyDFc)|@9J1 t;b!idD x1 $  F! 'Au)]PA,@ UpW+\`Gl3:,1CD 1,vE=4DFp< 4Hm5Z}u[hA;TLS}|0}vJt ]!}sI0'7RC\@,D D "@h$K 4mE4Q $)Ȃ4-P@J+Agt@J2,edkQʈ'p+/N$T$: 8@*!O0"9/LQ?`j/ЄQ})"`N|PapN qЄ pOa, 80 YB0@},P z0*GLPPN@p! 8P "X$`)HXp'"Q#4oR@ B,H@@ {E(Zđ$[hdQL,H0HІ2G@v`&C&_ |:S d E0V& b  RAn, E6c$zӜ@JeCBzIdH ( :`AJ`/xZKḄLzH5с L8U :MA3 08iNU1 yiLgDd4%`q@Qԁ0!O& J8*(D`>A@G HXH@U u" P cBͯ* S5U'ABXV  .0l gX(@ `.B J5zhA MX^hy96 40>74-0HAPHE:`l|KDA \D@ax"P*k,Y񕉦*`@ ,2R  8(Sd`jUM?b@ ΩRԒ< *ŀ:1R$8 Y.Lc R3)@ c<5R+ARg,X{X 1L@^'1S2# ix1`%"C@ka+mtc,~Eݪ"P@B @x[ @0K(b$ҕƠb '*(0 El8,$nt])F*K@|##,9tWDS d\ `V0@lP \Q 'e4L "q qiU툄QxZavPWql,] pX bV(CS^0ؤ_p@_zԢR)ƠQkJɔ WOQu$n@8.px|xx pp  W` a.V0x0P2PIQz1 # TAuən,ȰY0.1dq 0TitzYIYwď hH։Xi0Cm[q@2)yٜ9c#V0 4'9"nU W ?l0*# 11` @ cP,"% DPa#~PZfp]g&VPFPJCF0 XjD&Yycjj5 @ _\!l u}J|2/)>@)٨z IobI1agt33zp:?%PIQ"1@?W\TЕŪlj_0 gк|C#%jي|(0 є 9LjM0 ڮz  %z*J` %=p{% ;[{k ";$[&{(,۲.02;4[6{8:<۳>@B;D[F{HJ`NPR;T۳3QXZ\۵^`b[M;f{hR{j۶npr;t{e[xz;l~;f{{+}۸F۹:;;˴2Q%[{D+;{K{ȻۼkB;x{؛mڻp{;[\˽曾k+۾?;Ck{ \;{ ̾<\ <$\\,(,|.Fz](){8:<3S5=A=X]Өf\ЖYx=b e tp8y ^mZ۠-+Ҳ `[-׿̎g]ǘ ڡML-v4֬-ڛəթ=\A]]tڶZ_Hq֛}Ý֤-Ӕӿ]ե-PQ`ǂ-P_-0ͶIL=|>ZM oM]= |MО]Ӝ}-aڃLuۉ\ܕ]pڽ] .^$` Z݄xkΎǐM>ߚj-c}  hM]Bnܯ]-T>=N-P}dS f#^rNIM=Z0Nޏm-Ie=ԓ8~S}=-ܻmm fmͬ]߳7Օnڣ5n9M}6nsA qI0[|.̖`ôqѭ䈎,?m㰍VѶ߇N鵮֬ M,{c[n^9s % ]9Zzx `QY ֞4NQ!N] nJ<ϟ}>XC"=\Zol^` 1:ޤ #1ԧ^S6=>83O>]qoՃZN鉿D]>H4 =oF%β<^"|†>Iw>AbIsO1?o?|wP%N_OQ/_rȗ:^%L;!^ @@ DPB >QD-^ĘQF<~Q%MRJ-]SL5męSNAРEETRM>a@!,,r!",(*6kFH=훡cԽĢƧĠklo@C:KLSbfJ[_GJL>X=?8녨js˰hptP~ʬ̯/12fwγRUCoy|TCF;񇎔^ĢƤ]{ͲȨkoMmY\E{795лѹá:<7rsxqputxRcyз_cH_{UTWCLO@rwQ\ͼἕƻ𨨨ZdОeu绪ԇ[-.3&'.ζZ]Eȫ89AasmrO缧ڻ?@HST[}GIRy~V򈢢ijnϵǪ_ehK̲漭廜#$-146 !+l}~\akɭhlK漛YZ`VYDMNUzTɪǦ:;C+,/OR@`biܣfiK\`GGJ=/D-yz}FFNPQYS LDHK=͵gglۼ-z )ɓ(S\ɲˇ^xI͛8syO dU'ϣH*]#PJJ*/ 4*QZХ鬊K,vd=90c]KXG \ESEVT;È7.Ϥ? RrG1҉5s`靚+T∀*m# M=qv@<ۡ<0vu'Za`ߏO&(7!2B{t~CCn1lה#g%7W%ǒQH$%`B\A$F6C",2.(D<ĚR4bIu$NA= D`C#3&$JA%U$JWeNS*A9)(an4lѐ,a"Гd  P: /heBN%vP)}Rf_i0VE%fť`w,HШъꭸ+AkDz!tktyV+1,F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<@-DkH#]LMG-uO'=Xs5Xt`M#}q"Ȃp]|6%߀sGV-6i7N6Z}PGS dw*z8*| +nՆ)†1Ԏ@lc d) )ݭBՁs C/eyD.( M#XJDU]+  GL"F:򑐌$'IJZ̤#Nz (GňғqL*WV򕰌,HZ̥.wiGCj 0IbL2YES2S& ֔6nʒ 8I<ҘL:b>Nob F|qhnA5(*P{ӟl%8 g>8DhAXYΒQS؁ (!QR @c*PH礤2Ρ:20hg&HJePT?IXTl ʬn>Y Y"ĠbU*Oz>Ӟ0F?|1PF4O -vݢ_Jաa+%;Fʺ *-j@ MUԦ  0@}̀Tc 0l%J^ $qYH@:C\.#+IH`ם*'eaC6a@%l|2m/-@U|'!(׼[iO3 `6΀\|pbeC fcǸaXa^0?3^B:ІhB&r"I3b!W- `ÓasE6NEy]r<Lortb<X$.?BT#N 0`G!E kӕu3 +# `)mi,Di9  l-:إ;V[ttЉ.ЉND" =tZmDgQЄ&_!tDH-02 "`4# h,Fhh3d!@3phlac4UAMaPrs|D8PX9G‘:؎2ʁ1c0E9`>>-ЅNtXa cCP 69keI#{BBTyBt@#!u 0|AdP2}{yǗ oH`b aE1Q B㐡=t0 DV _|wA/pq4)y[APg!Of `H/}_'?!(~*b!07>QF9GDUQ~zO|LqEuP~u *EEIpP?p7kXT >p[pX`jFjX1EY 60?ք P&o]!8%P&Yr#VWYQ8ل*ȂX=(ցX ~u`h{*PzOYĆmqs؇E0$.@ E F@I@E` 0HE E~8Z>pE `ub PgXRH'+؂_r} =pTӀ @`aP c0pʰwx0: !@GGWGap8# {dبsčnW T(ԸGlWGLN=&\0w;N gd/ 0` ЏVpk PJ Sf A ؐ opHcxPɑ?}@0% p)7 /<ٓPV -19 ApPpy8i(,IJKSUsY~El@% R `RԘ9Ep SD9E' @g{JԀ[QIE\EXE\t` EPVYP6aYDZYĜv%eZTaYD X4ZXo扞ɹXdRYnY v~wٛEęE$ f EEb[4 !Z 0#7R%qrQyٟ+G{dut0G /sTJ@G=Pb'EK s zdXzv?ǥ^Z#0%  P*TGm[vjG_* BΕ awpH ^GP6d4ntJ:7 zH j|LPJ%00 90a׀}:ezZzN0Hd:**e<ZzV 0  ` f0fQd: f JEpHVRJꙜ&E Z* XtEEEEɩ[DX$iYYBhMP3`iC8Cp\xFx QgTQ,дOY0;۳?E+E\ Ƞ] EiEgkE .`X8(p*Ew<8Z vXDQZ4ZɉkXPFIy{ZG T N@20GKZ@00[s4tr%ftTo@G`;G{;X `5;G: wY`{{++q1@ᐻx0gJ#H0Щl:e <o eH1pz5 p <ЬX 6| A0 0%F Q7|zHJ DFHD{l` I], d@ T P˚ J@ PhpG@ XFQY$WP')E~ J E2;6;8q<j0?+9E+ ?ʠ- >jXc Rpvʩla(Epə}Z jEl q뷾Ɍz;$@X Z@s <ǍE\ CEP[E#QʪG!$nG,gWsb:H*.L@ IEh `և Q]0 c!  :M\Xn]GVHbLE~؈tpE]& Rt sQNPǐǐْE.Tt  qMR0 E}]™El-0u9  Ǝ5^NI ,DFn0[G@mY+EhMEBHNmEמ[$P<E/ FpZ Yq΀`to %&iC>`& HZR ^NLw8V EJ5 ZcaPOWi@ [סGN?7U90JL,&#ЃP[lڵmm;k^X`vr"<H5FXdȮŁaT_Ԇ&4x^&˛?.X(K FË_O!-, 0B 'P.% Pd R A/"&B fqCz `_$@ x`HSz$Q$`$qGHE21% !/X$`^fgQH#\C .& F1cb"|TH3eq;%FO?QI%RK-ut*%`$KY!K;4K+eDQ 0Rn>AKF4S 7]W_}_5cEv \xFg_[d6 Bl  "ܜ7YUYcbZj坷3ƭp)3ߜ h Zv](@7 RpGBҀo=76ySVeG.vH#G h+4i/F/Ģ;&;&줥z04ގXb) -mN;aPp1F!^x+tɅ=8vy6y矇^n~7؅?|gXIR'}n?~?py=_hЀ>6Ё`%8Aς`?:haE8BJ!Є'|HAЅ/a ɧAІ`ICЇdCa(=PGDb ЉOsxVъWJMhb8F2ьgDcոF6эoc8G:юwcF=яd 9HBҐDd"H3򑑏d$%9IJVҒd&5y4Gnғe(E9JRҔ"^N e,e9KZҖ21uf09LbӘj-KWәτf49MjҰq>jvӛg8)L{/ǹNvӝg 3g>O~`f?:PԠ4.yYπԡhD%zHlc hF5QvT:QԤ'EDˑ*/iLe`cuLuSiP:TEEjRTBOjT!-, r(*669;QS[ABLrVZJ]PSF?B?m񉊍MQE`dNoyz~ORFhmQuzWh57;35:fgnDFO13:voKOErzstz{~Z_bMi}]`L`79CotSGKCs}~}!",y}XOPY蹹,.8uMNW;>>FHRCFAbcjžHLC35Af惙f8:Djkq]HJSCENwy~RUG\ejPXZaZ[b@BKqvUKLUY\J8;=c+-7򈬳neɞmntmEHAbgOw|W=@>`c=?Ix^*7957BkjoRgjΊnsS{|GlDfLuzʦopvefmAb,C=1[E.L@*<;Jq6{P_:==Ny5rM;X:V@_8R2aHHn()/Nx,G>2_GIp;Wʭ=[à3iJFjзӽ]мDT ϺMJHKȰaC)#ċNjr>0U*(ScKF b( ̕$9 N> JѣH*]*0$SxJXjJ_d&ׁqAgʝK]~½kBX| /U OmdYw`e˜3kޜa<(/YVIAN m]G*4MJ4Qk##U@4Eಂ5Fv}X 2WuQ# 6$|5Pb>3/]卢S!GVT@ @0.xLަ:R2OIIHIJ$#A|B:r;JvHHZH㒊"p, L%R*H a/2KUm" A$^ 0IbL2f:Ќ4IjZ̦6nz 8IrL:v~ @JЂMBІ:HIDڋX!e@ GCj;!"؀A6KcN%[J^pb=,^JD~S(] @"U a@ Uɣ HIX2֯u$@K V!_5Yc)m[ RׄD4{MH\ T|  T -0b%XD3#:ˆDYRFAHH<ՒdGAh*IJ\eHy聸),;Ӧjg B\bdAY =R+~$#A/AK^: Iz"JIB3rx e"ŶA@Ʒɇ:aO0Y1,2x#ʍ3W(/I"H ȌPcܸ!9GqD1u*AC!ZvȗfڽR\v.-F r8#$JLVC3 b!G~ C)mn?Ix)V["2 D &I/i(REE\t īƈjՕÿr.$%"[$nHlF"3Bj^|Y)Q2"(vHKp(AP (da H71܃cH.p'(pLl ;0^ב1: V $&(f䖚 Ȩrt<"߆,]q|#Srr'yBn.ޝQc޵0](H#xo앦Wמh~T VľݔT;bՎf` Y@rG$oGR7 B@\ ЕL%@x}VEl'$'yX^AVJ?٦OWֻOfO{ǾȽs,{{_=J4XHJ Q2X } :1C0?L'0g )3I  g~}c9]P`g#$P%0 3A؁8DOǀF؁LԀ,؁Y:4FP*H `1ʰ SUwE{4|`Ő *@$S9 #1j`  0a. Q\x؅  pl@/v{hl0H m1APK bpNnw !( `:%VC`}0!@ Bf(e%B3$<rH1ipi4VXIM N|qTbX" `XdX 4p Lp8 I )_#=C1FT` a0, ApeEvH@: ~PqF!v e[R hK,`b `Nٓ8鸎_9 aC0oQIo1{0  a  0 p)p W !( ;@ 2A `.p@%Z(yi4)pja蚰9[J Ia*t1y9ɑ5[u( Щ`+2iyI'Y9@1 ưPYɜ0PК@ƉQma +p j˹) v` G`6 p.q %3! eiPJfaAf:*kꨥ(إ0!w@ Qnq{Mr1Ia `  0@߰ *9y p-;0'E 1ِcy J Q|@  .0PZ:T`!jɝ*? Q00u -,ךZ@c 4~ t0 j-J m@4pl  J[Ɗ ` p+Z5۱*:cnQ`0`H_P_p^ oj W n1 ``zb QЖhJh¦!@D OP _@::˩|t`ꨐ y2Y*03y ak$A—A P1pf9 "c YԠp˻!9 x +qC8p !&b; 1аaVP"m L`Ëkm%zA4ֻ+B \i` \ [UTf@ucO>pDP ^ e@h0APqT%7/p+4lt&0"YPQJ ;}M P|+`+U縕mk!SP'2-0 =KydX$A!;rLv<2 KL 3Вab_q @<ɕ|q3 ʼ,KK`Zl˸{2QNJȎ, йࡍp|˫zLpAPq jѕ,NeO }5hc! p ^Ji@Px 78ܔb }g ~Cl6 p @Dgr.mƅ| 1` X\Ѱ`p>@H-$h2Ym?P`*lp$qI-)@\-^} LN Wz\}sE]  %y-)&Ppyo&l )̎=M (پҦ(pٝ͞ Ü-[աt-p+w{pl"A&,FDs* :@𦦘2b 8qHA$5# b W+ -B kACe6!%WH!Pl1/' 0 Cm@ 44p50s20 ?i}4.K.0 眜\ ِ1T SM/?*!Ю%M`N@&;p ?۾"S "OH`bidӽʭ~Y惕rȪxѸb#$-}8:CMNWnᶷs{Wijp13>fjNsxR|}y}XOPYbfJj<@7*78ٷrp6vO8;=cgNyvw|795ptPmqQP{:<7mntiTWDRUC~.P?.J; !+Kr\Dg輨켜僑^RUGƄ^Gm')3SU]nsR禧Ip-C7,<4:WcdlAbBd1[E5f=8Q@y2`G@`EK=[BdVT Q NFj*:;VW\H*\x Ç#JH3j⊎ CI\C/[ʜI͛8>ϟ@ J%@~XҢPJJkVjʵ+N^9$9vѰhӪ]mJ-x 3kF'ޘb)* 22B=TlϠC`csCIDd a+IBظ핽 :@n)F'TsDϠ[=l'iA'x`1巎Yf復9DQϿ'A*Q-QFR6pDԩDЄGq Vg'V5@OQ3%" /#ZU֎B|])kyȟa8@F)%WPeMYxQq@A3զ_)g[IAneI+i\3'КE(F*)TMj#5e*ꨤ*d꫰h7jZP+k&6F+Vkfv+k覫+k,l' 7G,Wlgw ,$l(,0,4l8<8BD]}27Tz"A]\C5Ab}]m6_)P!Cۑ }d xHr߀.Nn'7G.Wngw砇.褗n騧ꬷ.n/o'7G/Wo/BۿN=DUT> ~IqQ~4яn*IAhB3P*yH<޸. Y D4X!<#" aTF!2#Ұ"0tAT(a)! *9GF!Pr2D& C@6*$A^Ū@!2 )aAHĎcFGr*v)dBtFE!@\$2$P4IjiL"'5"$L@>lDjE`k@V%ElY/a"ip!'$$@"L@EdK0Y_HYn&I3O]nagB舙yċg<YDS"$H K-SHBkL&cB`^Ǖ A)@#}F@1t@ĉeB)ӈȱ|, e) R ةL1<N DQ3H2UDu*LE3EF*T rƒ44!WHYVC^EI[Abs.9Rτ5#w}*S!LHO`>Ăǐ ĥ% o -2&W=TKˌ+ERےa-vX4 Ŷ-F1T!+A,N6]Hp+SVlRF; RĺB\K*Ļ@$ &D%rEhFS$HEC2$ĕch"G1'}DtAⷹ%~YRRTx"U4b]ҤqI+?C6bށP#+pAD*ȬdoNg':V D -c,X!w$Qrc8 SɅe}!=ȗ5T!eNTӌ?w l>V_k2\]"9rgQ6ٛ%ȻJlЄF;ѐ' XҖiLo7!1GtjI+h^=R%δvL %AE|bRAvK}TCHBE<kH]a+ik"[F>T"ɽٝkѽE"FxRT!3ysdppd !qL!)A4&iyB& "9'%&H֐d@i7c ߑ BV !bHg~c,فt [* bhHG9d$s'!y-IN0 UiӴ }#`Pթig K#J>_O2A@T?#sBDWgIx<"HAG Q_8DOa$ 9qT*gH! % ~#/?D~! #w݈ @B b0"qG qj%vH hWlWEWVC~"q~'AxQ A':=bGW;GVC5GD08MUցm*C8Q%Qp%2u]Av8| 8ZHZXх(dqd*Ako?xm{ n&(j Ұ ?~2Q`1Lj+4vW8S8?X_HXHW~?7"{| )3!#nQ) 5!u)mT H9i9h*jJ?-a>xXB#h(.8H-ُ ِղ)-)yRʢ 9"$yb(,09"4yr&b8R<9@9"DyaHL!B,_(*6P{!",)49HoNyIp@^*9:/QBDh+<6yO5rM+?=2dH8RLuOz8RLu/SC4kKNwEj9TLv0VECdFH>kdOREX[G23:022_bL69:QS[hw{Vy\Ģ-/:иbfKzӽqZABL>@?ɫĠγ^otQ-/1SVD+-7q,.7kmp[_Hs&'.rsxމ_|rwR~uHLCk57:cn9;7#$-CF=˱@C:yz~ƧMNWKOBm{V9;DDEOJL>dZ[bˮ68B24?}~lpPhmP=?8~Y㑖aKLSdfmƥxn悙㥫irfjN+,0 !+읞`ah輜FGQrȨhio[>?H)*/jkqHJSh;Xuuw|8;<~VW^ѱo6vOIKT"#,Krҳ_BdJKR]2L*[/C-+51KLUAbDO ?u]U 1[E?AJWX]K2_GNx:e#H:JȰÇ#JHŋ3jȱǏ CIɓ(S\ɲK^ʜI͛8sɳϟ@6IѣH*]ʴӧPJJիXjʵS2^ÊKٳhӾ @ڷpʝK]l˷߿LÈ+hǐ#KLʘ3k̹J\CMӨS^ͺװc˞M۸sͻ Nȓ+_μУKNسkνËOӫ_Ͼ˟OϿ(h& 6F(Vhfv ($h(T$%cR" #ш*Δ ~8HCْd#IAiٱKDVDe&i$I^$elWLAcCi)` @g*f I ĈF{dAFFwQ%zҠJp) )БA*Xj7E!P+1LBYI)I4J1P4?a X%C|DlVzq HK*%a $R8;1A>jB8 XZ@D@t$/\^] KHJ^ AD eIDG6eFA:Tf+Tҙ1I:\P\vٿP-LQ\>;Q)@=?%7qBOO-%y:XP@MX  h[9MPi͐l!AZķClTD#zS |M )e٘-t&n8j_z:BJgxAgA8p@.AJI;E9AfE&%q~>< ;AGD=DO~BWDI8]aλAW|m q $,hF4r8iG9T SB"18s88+vR(#DHQBBtoI 0,D\֐S#vK;COr.e `p NBbΌ| d#Rĺ(I vgeR:1e"e58۲fUo]@H@)6NxA KzRQd q3UV `@D,$\:o"Z$+j ȄiNv@o  Ne-$Uu.7́1R:+ěH&H<qƙDL>~49-BC2Nt U*эt(hI[^7=L#Ӡ>CMjEz:NOVծ5`-Zֶεp^W׾6-b/NfΎ6-j߇ζ|n' !+,$(*6P{Hm*8:Mv-H?.L@Lu4oM7}Q>]@`LuHoKrDf2`GGl(,6AbJq1[EBd>],F>*69NxKsFj2dI/QB'+63iKGm8}Q(08'*5=[5sM5tMHnNy,C>6yO,B=JrLvKtNy(/76yP69;QS[@BL.0:NQE]+-746=rPSFyz~,.7HLC79C35:zmu{W^aL139?B?MNWVYIo`dNklrotT脋`CEN~Zsuz녶regn\hhmQŔCFAKOEFGQov}~acj垟i};>><>H13>}y}XEHA`ᛢfsܵəfY\Jͥk{HJSlcejPpqwduXZaZ[b]_flpRJKT8;=x[^K”c6vOٸUV_Dhx:W)*0tchO8R1\F7{PIp*7:Oz+=;H*\ȰÇ#JHŋ3jȱǏ CIdBZ&S\ɲ˗0cʜI͛8srϟ@ JѣH*]ʴӧPJF5Rʵׯ`ÊKٳhӪ]˶۷pʝKݻx˷߿ LÈ+^̸ǐ#KL˘5̙Ϡ?gM8;װc˖3kгssmϷ5u+{spsE|k2ٮjூa_iG_ϼDy뇺 "`@@ 0Y(A %ER7[1geT$8A,`A ( )aЃLXP'阑!_(#M}#NGtW$cBkl$GOP0BV)T1UfbEt&d1twXP @Pۗ@ NtH ;O.iF @(t)w@ũRmj'Pp&A] PbU 0@g.J% UREREBɹĖ;Ql|`- .9akO:W0~>4pG0Dy@4p@0 ԛP"&N$2 ѬYЛ:ùqVL\ )4LE8pʽXgZfup]dSf^7w5tFx]7_߀~Y@!,/(*6P{.PB*9:?^Bd@`+@=.L@(,6AbLu*69Nx@`6yO,B=+=;)39*:;Jq@_Jr8R2dI'*5Jq5sM4lK2bH,D>/SCEiLvDg?_OzDhKt-H?Mv'+61[E8}Q,F>3iK(082`G=[KrBdGl>]KsAbDgHoFj>\Gm@pFRl2ШtJZجvzxL.zn|N#SwGyzOIR&{S!#hMkF RLʻ;BFQ˒R@yo^=(\ȰÇ/ɘ3k &!,"/(*66vOKrDfP{*69Ab+=;JqBdGl2_G')5*7:7{P,E>1\F2aHLuDgOz.PB*8:.L@?^Bd(,6-H?Nx@`LuMv'+6'*56yO,B=2dI1[E8}Q,F>3iK)39*:;(082`G=[@_>]KsAbJqJrHoFj>\Gm8R d)h l˚p,tmx|pH,Ȥrl:ШtT\.vzxL.ؚ¶|N~G Wn|pnK(mN+uX#sʳ79( Yx(   H`X05#Jx 6րQȱljHɓBCɲ˗TI͛ad>ɳϟ1B!,;/(*6')5abi57B첳HJSz{stz24@]_f:3iK)392_G*7:7{P,E>*:;1\F(082aH2`G=[Lu@_Dg>]KsAbJqJrHoOzFj>\Gm8Rp(GrD:ШtJZجvzxL.znL$J̹~luCwxMpEQL oJ RUQ$䞟R#PJ! :(ЏÇ#J$h BJ04ȱǏ }U<"B%6\ɲ˗F*aɳOG+2XX?*]TbP>իX{֯`ÊŵسhӪTڷp]t.ٹx+%!,5/(*6ABL8:DCENmnt,.:@BKPSGibdjRT\LNWghoGIRxy~79CkmsKMVDFOFGQ}~थ̹櫬|x酒cP{.PB6vOKr*9:?^BdAb.L@+=;*69(,6JqDf6yO,B=)392_G')5*7:7{P,E>*:;1\F.0<2aHNxLu@`@_DgBdGlLuJrOz8Rˢު &$协  H -Z #JHŋ @A|)ɓ(S\2։ * D -sɳO 2$@s_4?*]ʴZ >eiPPSFILC*,7rwU57;.08[~Z|Yzst~wnxq}gdoABL6vOKr8:DCENmnt,.:@BKDfbdjRT\LNW+=;gho2_GGIRxy~79C*69*7:7{PkmsKMV,E>1\FDFOFGQ.0<2aH}~LuJqDgBdGlP{OzAbथ̍櫬@pH, $88NtJZجvzxL.zn|nV*P(}\vIxyQ`H wR]HLϽZĶEy rH DP(KL$! G@, a‹3jxP S\ɲ%,R&x0BC(]ɳX>0dPbA )r\ʴӧR9@LAʵ׌RJٳ۷pWm,ݻx+޿ &w]+~!$,m/\(*6/18FJB酶rX[J')557;rg}tCFA69;029KOE-/8x}XY]KDGAMQE>A>v{V7:w|Wx}XILC9<=;==^aL>A>~Z|Y~{}i[mduhႆ]^ABL`rz8:DCENmnt,.:UYI@BKCFA.08`dMbdjRT\koR[_LLNWghoGIRxy~79CPSFHLDkmsKMVbfOSVHDFO57;FGQ}~}Yzsfepgg}t\kथ̍櫬ǐّʈ ܊ HZ`@ŋ3jܨ) QĀS\ɲkF(E*>68CotSOPYƮCENvuKLU=?I]]`Lfs`{FHRxn`qvUY\J8:Dxy~mcdkscw|W7:<,.8䯶o\^eՂ\AD@f񈎔aIKT\EFQ-/;CFA{pe@BKmntdhOTXH=@?mqSkٷEIB^RUGprw8;=|YSU^klrgho֫x/1pfjP{|z{PQZ_`gkopv^aL녿vg天j.08VW_`ah57BtRUHhjtuz]nsSbfO[mrxUX[J…ijp?AJv{V_koRNQE|*,68aC#JHŋ3jȱǏ CIɓ(S\ɲ˗0c$H3Û2sɳϟ@ JѣH$häPJJիXjݪT`1L,`QiCb&9Ȱ ',PTS@Y:EcB1H V(KE;WtpE`b8 ( (0-*2Z-\2B6T^&>&k⭃coMpkhKax<ܒw^-Tw5;,q%$r 9HA1r@Lae* B}'a3,p(Y: h(H:c~ ! xYU s4 -1G$J +IFb7D<u>S~r @,0 Dr HP'*tډg[0pC!Mtz(zD}|@$0*+ xӃ5af0K``P(X@p,ABNF$O3tGA DJ29F-2V$C1pAD<pshq.9C 0 ء B؁ 8 Am@p  lҧA@ԾJ}.Xg ׂJHDRJ È']6\ +Lp_L015dN\4PG#bܡ8A qb 9Lbk~뙃@} 8/Ab6ؐA}>}At`7z;]A!d _$0<~.8 Fۦ-CN E:D( CAp\dedx/ҊIjHT!`0L/@B% 0}BA AB PMG C,㷳1NkPVFH aQ$8F4<SfAD|0 E< PO0`-PCЈ!j 6ѐRa 3{.bz$PKBRl|h` x1^`Yt(KyJ1{-q4m\HGL  DЁr|o(G>dDeh(FШDC%$0Q4)@2+(pD}!sȠ "$:E: l!@{HhB>$险`4 m @dاP.Ƞ)t!(Ţ*Cش!TzCAI 6p% (AD>0]I&BPp$iZ"BT ]`cPZ^l*XS ejAWi@.S i,Y Vh6![V" Lb,F.oH=HbŮ*D,A<\bln ̀H-d BE  @j,<( "!4G€J5&tHCηJ֊flk(PR6 pAD PM0UkH2la>h21 A 0`́A0dz pD@̵@,a!R&?9-P@ f/ |NcfknL#,cP˺l DC0h7н}B:O<[q)b`VKC Q!xApDa:LX^D4!@HKhaVD&1g%ٯ^082 mwUB+2ZS'@ ' G7CT>-q CU4x d`A5QpE`.RZ$^!@r _8L*Ac IXI071|ԠMk9de  XLTAZgD w 19Bd"I&mb&zEV i/Nm A .- @  gO=@CLC?@  A(`*ܴ7t"g0EiO,/cPB,ܠ!`chd\C_pdo tk!qf| J@<<) ",>H= ] Q?/@ ujjh?Pe1vݢ vRsZt1gsie@$p=9Kxo8W~a (C]BNz0Bn" Uc |0 { AGB]B!E$&zq o,U z0@ЄG  L~JH~#1؀p gg0]7 #PHu)1!EhQpFeA w.4YU# puzI8wM)v w`؍߈x)pZ ;Hw+h Q@t߳$r8sJ76n6 y" Ӏ=V/ʠ @R [u0GA 1 P-v7S9Q+0 |/ -+f oPX0axD Ocp8Y,$]v}v)~=b 3x WW ( ?r*7i%pI %`MbW&XE`3eO0%p6p|V`~L3p EdAD8It9d yw11OA1p Cy pIy^) a sOT$pp$@J e5 CP @ wCyP  i@_^_U G 8<* 1nlQP ov`1`~)7"lKQ~O46 vJ/S\mӀ^@,ӧdJjZz9<u+xn AE  @$z ,~ 0ZcP'$aO P`^8 1aVmzњDE~MЙtA=b.>*~MzP|a. i 2:Kj~zNx5 [粯[ZW q*=P&[ޕ|"y:2{~Ks(:8Z<۳<*{:;DkjZJL۴N b! ,e/d(*669;QS[35:mPSFORF?B?hmQnMQEr]`dNWZJ57;')5KOEhotSstzABL}egnGKCz`jkqcHJS]`L13:DFO~68CruzWs~Zyz~7:>46AKMVx=?I`OPYCFA139iHLC13>.08n8:D+-6veiPlnt+-7Ƶ`YZbAD@qvUY\Jo{]}f,.9.0e9d xe+q4 UȬ (DL~^mP2f =!hr>T4L;ei5eWzt Q:E.lgϿ?OT0RX@D&,ԁ !g 1( 8E*di[SC b Bl% ㏆]PЊ݈ V^JByRabcIA# Y2*p;qeCqVY\ HRLY0XKCuܙ Y,s-1{JJB'Q D}FBd$,@*H~=B~i (d46D@зP/LePYv 1 n q zJD]P9@v6umJyYa`HwxZ؊)1 `P t荲WxX0py M )A2@I @;!PAQDX WQ +p++q1({eIIʡJp D4+PQS1g| Q i}ttY!1EgE6}8bo 0p OGE/ 5Y S1Ly` WhuIpXJ(-eq o@ [d^0 $nS Ssats{G oyg0` S0kV 𘑙vaX s/ iG.101f c$F npYK1Vf(pĀz?@13 `_ce`*MP  2\'`6Z d p@j` ` 0v`I~`zQe$B!q`7 @V szta`GdK{ڧE3/WmEs -+kk "(aKa$ JI`^ Lpq:bJdJGwF h rQ+ "ti. A g7d0 Ⱥj}E׮m~p2! ,Gd$t@ [A'?(.20 ;od* [!;$+z%*,۲P! ,l/](*6o69;hMQEQS[􉴻rPSF]KOE`dNWZJmORFhmQstzABL35:z?B?egnGKC')5}_bM@B@HJScotS~DFOuuzWx139~Z`vyz~y}X57;AD@]`LjkqZ[biႶrY\J;>>MNWOPYƌ`c=?IDGAtHLC|Y68C킇\YZb`qvU惸s܄]UV^}fs13>v|VdhOTXHJMDCENFHRnRUH7:<^IKTcdk.0<]{p;=FRUGKMVprwPQZ[8;=m.08EFQ[46AmqSxy~?AJ8:Daf\^eo{acirxUbfOvx}fjP{|opv^aLuw|z{k~jCFA=@?klremnt_`g녰pxXZa߀`ah,.:gZ^Kgho,.7EIBtuz57Bjހ|ڲijpѩm[\c_kjoR*,7@ A"ȰÇ#JHŋ3jȱǏ CIɓ(S\ɲ˗0c4hPB8sɳϟ@ JѣiӧPJJիXZ2Ji1 ,"AgXc 24L.(T)AZ  fԠCHGy@Vȳo=pā8@vP@խ:+Rj4@J2ɚBR4Zk 2|'8U[\8 (F *2J\,kb[ H:@qC '@rP3 =#J뮿VmH/q.$" 9V)WW@İup1' -o߀1(BXn\LLPKA oCB ѱڔ%bE NP 0+04S@wA}>r][PRB@:K" '~C^v8E K@ cX7otb ]X*H֊Le fi2 (c ߸VJUG3ƴo$m4\d.@' 0腮&*%Xbp(ic֗RD40B 9])@  `.#1` Q=Z{">Yd!< d 0r9ѐc >1؄Z BHx#8`S.nBEf&ƀt||&!Hj HϘF+[@G8Aˢ!nej^]PNHS 9ąa 0"3g>[!6-!i QdP /+@C p4dЋ[`NLRBdDA@By& V*F-A X @S |]g9F#,pөڮ|T$M%9Krی"ׂ  AAx/=Bt6k (V e `tA.YpuׄKYϚVmK(B Zu^L2ڦ0'1RK mφ1dPf9[0yB22Ƞ<)(P-!dfYZQuBR QC2!8 ƶ] rm(C0DZEtOK!W, ٖqM Bp2AB+H84L}i@5\,h!Y^/L ܞ @Y =( 0t;1P qO8,,`{Ť7]Pɩ="Q#ސn xNOPaAk(dxp m#F]A 3ј- N请Q30.HH@`r ? /iAso[0!EoyQYaC&0,б:Z< Kj4 Hh!n p dH2V-uvViph XQ"AAZ~! vpi3p/hW"HvW($2 ~D{" Z@4*r'7 U !  x7_MR^gm2%{1Ć1SR |R Շ}&}`b 7?߇66nVn  z׀)pHsp1u Uks$ l$ q @ h!0%J)`wxggȌ dxA qPqrٸ h(H肂Fe0 0# ppHژw(x>CRap@y`K YB s+5Qp1@Dp% ODuϰD0Q`b&C! DI1p P` miH?4Д&+7CQű| Gn ϰ tY!8,asrBh6෉6bC `V/$ QbMZJ  F Fx*`IY)}xGp<ؒRZ`9 M"<@ <q0P1:wIu<r霊ɘ)1Tfp1h0 PY: ꩝#ywf P 's+ `^nhu H17Fñ79 [@0Þ@M 6Gy6`a)K gѓ+7ml r [P @oy siXnVD2wX`Q a} 6&qSK X@ j-"sBಧ!d*D" cQ3wc\zcJ^ʦT~I'Dpr5^a M@q?#,6Lh )q+iS#@@ *" a e w ^Ǘ (ʮ+ w~AD㕨Z141  RѰ{}ɫ7۱ !,e/d(*669;QS[c􉭴oMQEPSFr]m35:WZJKOE`dNhORF57;hmQstzDFO57BABLzILC?B?otSegn}@B@ꅽ]`L')5HJSX\J~rGKCuzW`XZa~ZZ[byz~;>>_bMRUGjkqႂ\46AfoMNWtmsxOPY`=?I13>CFAinveiP7:<󈖜dFHRuAD@qvUŒ`+-7{}w|V13:CEN8:DmqS.0EHAfmntvbfO~j۠vx}uw|kz{_`g{|TU^[^KaklrxֶVW_`dMchO`ah;=F<=G߀opvEIBjkԀ\ghotuz_joR-/;rxUހ؈uijp[bЄǂH"aC#JHŋ3jȱǏ CIɓ(S\ɲ˗0c,PÛ3sɳϟ@ JѣHLxaҧPJJիXj5t!9o "g0IEEyQ6hZS̘ bT |Q2że-<ܝm DVniu bb VHO6bi?)Y`*jM -dv^ +,V]+ڡU{smh@ TYK @@eA"uD1'!%pvhRQ @(ChI!6 *D=zzXP QP0bA9@ >(i] - /H1e &?pA `@P`qRVq4\\LK8`HM\2pRp9@vuRʀsCBN.* B7'B(z1j%Pax@ fVƥ3v*`pV4BU"1G RkmHik> %ngq@Ġ[r @ q%$5aƝزB\$v ,:p9\<\D<1A B:!tqA0ip L. BīF$+F-ԿQ P4B=+, 5`q$I d@Ld <RQqγk$ L"Gp@7Bi L[2yo41:BDC䯔eF'(:oĞ}Gio ܋Qs qn05E*z;7_JLvxf|0X!,pg `uȂ f6@O`YXB.1f?gJ ըV iز;r V Dr`XDDJ"tkbC Nk ҇S@/>B R_"60a J! 0a5q<ꑏ~W@y  y _6@M@ Bɻq  J# *` W}cavʍ@hG<Q8.` k#}h&,HNSHϘ{I f0& -@4梊H)3i9jd+4l0z/ `(H:B (XPhhHq$ -q'dJt H.1]#LLRr) )r|qXjٸ?B" T@ +-AJ54@[u.~[; $rC-ׄ1B}cAŎ` @>B e"/ӌ5b%+VV.iC΂D20W# DVZQ"!먄@C CD)%TR$`c0 8 XY *QM=U`Պ(3sMH% H]7,& 3=#T\ 8[V E! 18`0Wi +B`Y \@eS@@ / X ##YX4AA|z1$%@Ro3Zk8VJ99!ABD " gV4tY A:\5"9 P8]!Ю@K`"1,L8.c$8C0 X(aT7~iq@\yi 0F) )+K` $`>- H, $)H0d 19s+\ ! d [<!E VV^+.?~r,DLa {ٕTB*\+g=K4 RME/bD ~nӣ>uB'gǕr#r!`p;ԥi 0{qwjA/!.j| n@04a (@LH~ySi9\0 1 ^B \!AxF}Kˍv N'XTrxqlĊ 7 4vC)>!tf2`H ,߯X5 ʐ 09 zdBBr$fC-Gv$iqa@;gJa/~vcsMEf8rF@((u'>XrB()#& 3-T:l`PAoͷo\ J8}7bp32x P*=#2`Jp+31q$nۤp7HQ(hP l qxPo!0J n 80|'(SxvEdP ӍX3+Z؊(HP.e8}{Y  x,D0y0109ys z U TՄ0+p|,+`{XN“K0pj& VPc37n@ᒌ~ Xz}+^s-XqW?q PP [F@/08<hY@  E``q1FЙR҂# H0YPu5@Z0 9r"iEPEpJvs}QH XWÅ9ހY"'8S"[(0ɚ@_Hpi  +ppr0~NR S{0s > `z |`0ƣԠfQ"P8&?_;t`4p` yɗb: s 걦C*5jJ+nJ40J\'NЪ2# 6\EK]yg3dZƊZbP }x p\! @ wK"f ڮ.]q>_p z 4+p  {'-P [K8a$ $[}igj*,۲.+!,n/[(*669;QS[o]PSFMQE@BKrWZJmstzKOE`dNhORFhmQ?B?35:}egnjkq`@B@xc]`LHJSotSz~DFO_bMGKCuzWጒ`139f57;Ⴖryz~;>>Z[b~ZsRUGmCFAu讯vw}JKTOPY=?I68CY\JeiPlntv킇\YZbqvU`46Ai܄]}JMDnEHA^13>cv|V+-7-/<')5TXHոCENacjFHR7:zp)@/)7Q28%shf#)s2S e CQ  ( +(o17y1! 024Г(+O 86,}  20tt0H%v eYH D2h9hHR* Y #@6n3GS pgȘ\25Z`I J2 [  2 ]"eG "pf^V}rrcr1p d[)ɜq1HP"‚fQ, =!. %nB_x D@1 *) \p0I `b0` =>z⁋`rv68Cx@B@cotS`DFOjkq7:uzWfRUG=?IsOPYvw}eiP8:DCENAD@}š`qvU_`gFHR13:{EHAmqSe{|Y[sIKT;=Fx}X139KMVap.0<.08n^xy~߀EFQbdk~TXHPQZĄ]էprwkaci\^e57Bjf{Ymntklr`dMnعchOopvbfOۇk{|̅]=@?gt|tuz\efm9<=VW_EIBjoR-/;}Z^rxUeSU]Z^K]TU^?AJhSVH[\czH0(\Ç#JHŋ3jȱǏ CIɓ(S\ɲ˗0cZDM8sɳϟ@ JѠ4dxӧPJJիX#&M ܘ`AD Gٳv^}'@0 $\7>6aZ8-;%4 E[x`x(QكT>( a;f2X h]gs:8֤:/Ktpn!4Lhm/ YO5U =^0P3ADx+( !xp("iI 9 A CmY dC:jD"``8܌ND $.t2~ȷ-!n1c+iIY`"؉=!24!4tuCy zbHp98 @C|LCe p+k[`C Xcg!@ . WQRbp ˤ\0b{zgP}%ѐD{$JvPA)!ہ91۳P5Dc(AdkD‗I']m80t"l<@㘓o=øx0 @J ꚮF-~r,b'T]W( : 6N) V L[z4 I\8!@[Xk=WwwC x( rP5&Mw@T"utb(x'@Dx8:k:㵌P/p: x^~sC;qo=7}E lmsM£^ PL)tb[hD ?;U2胳|F xf7 QAThA(1f)@gl.2U!I9r .ۨ)HģB"v`/OX`Yf\D|QZ0rpKV ,H^ %pI5qc|I.!hs G&:q J&aHp z d`C)%I;u (F HBIPR rXΒ8eWUVUa81J!*^@E"vDX'cj@#0p0tOr0C Z$<K=;!gK :%=?tFsS 8qQ. : Yt.PiDQjÛ @+ =Bs\ 6 pC{0,9'@<Y!=S $ R`R@Z:?IY ZEc^Ŷ : Tf @~[ւ !pAp8􉃑8j/n}cDheg Mt1 K}uLQ}q}E~u*%g7hi}w'|z)   y7 pn q z@P PpAxG3wt&sR&AR(.e%0n@C |ׇ~ pfoas 4yEPs?p;+9pSB1V~6GcT a q#QIT?; 'vQ \ xVqt`u@ƈ(Xϑ؉^QxtgQ%rA0@ x}0p3(p+X X_ "Eh@1  @ P+V0P60+6`lpZ03I1ޠQ -C :iC3d=60< W |`P%>ЌP|<|&o P` NFI@:` xtf% XqTg'>`d2 b=AT AfU0Y` a`N(P EPK0Y+0Y )FW{ٗI~` p8y~ĩRT 0q0g#ކ) .O?sx%P B <]BġB 0 5sC6^eep .S` PAQR0: OY0Sq+P S b9aiPoK$pG6%amsJ7qTTPP+CaAvVh11$=ՠ0=5'Fg*L2{X*bj ! p w3cڬJɗϊx@Q z :DѠ }꺮 ]2 |`}z:5! ۰! , 7(*6@BLQS[]ro?B?㞥hMQEPSF69;޽mY[byz~DHAORFzILCrDFOhmQ`dNWZJ䋐`KOEstz_bM֐b{}|Y}~FGQlmsY\J`ibdjGKCuzWMOXvJKTegndiOsCENLMVw|V]k]`L79C46AoHJSpmn{jkqBE@Śftu{qwURUG;>>57;ntSvx}ׂ\-/9_`gxOPYotTmqSec~Z35:03:u6vO^Krt_x}Xa[fgho\^eTXH‘;=F河z{\VW_GlgBd[^K8;=xDhPQZ۬`dMprwopv{|JqSU],F>bfODf.L@AbADA?^eMwj]*78joR1[ELu[;X7{P_@`/C-,<42_GB}ހNyKE4mL>]3N*=[Hnu8RO 2bH:V')5=oT V1H,:e#,B=H Z‡i8A5 ^x@E:=̠8ա͛8Ο@zAȂ&D5!TД K0mb)@(0=P('K0A;6c?XssK\u(a߁LѓcS_MӨS~gYs槪* `pό ,Pm@3PB]z@"0X5P7p+zQ*Z>ÊY|mÌPqHB 83$Q_* ]@ HE2\@G X+a%3)=Q DeAx4ޑ@,0HT@dIM|!3i9.z7B 0 K%gha* ҄4 #1_paX >/eŵ 1Є, $+lan0B. H+¦g02!02~P +~Bra `q@ d4@QTȢ4T4a0H ^G o;OSMEOSb)P0 5qNU #x+_ W7u"RYM]L![dd**V d w8@tUh@,.` > s%r[$8@'u>A7 2 O$ Avn @ ǀB3T&=}nx{% hpT 7)7 La)8AiEL! Q7Z`p"j (F =D$jP583.A QpQN;@@dT#W@0$ȉSb2Ql\*B6eeWƱm 9hA p/ W"NRD ߁뛐=L FKB2b[$NNp` ~0Ra NإnEOL@rpOY1\pd`0 @| &c\ ?lR e" bC{yW[ddla"c*<%#A(gzmb> Mrmtdmix )ì{c(`6i~PPT*r5Ɉxt #x x upc 茚I*@H{`HTsLwa  P3i-yyHhuVpD H.`zv%Q  HQ^`p}qpsmDW_">@\8\|dIfnm}$\N?aw؋8"up ` ` 8 $A $@ tC0XrKDR>i3$( ؀$ /@R `&rHp)95690@ Znd) @y*H扞0pFF@ID0P :B ɚ w:j͙CI=V P> yy'Q ' *3|g1 }q+Pk&@l=ƍr .P]# ,ᵝ޹͜ M߷,-+nӐ}M7B~-ܻ]ґgۭbm`Ml҇mX$=nh;ъ]NZNI_Li>r~^b}m->~No~-m>g.!=''1ނ L4=苞U>铞 npZ`\p ͘ݑmꑮkӍMT> jޝXp>׉N o ~> OﺎN]. s  '[n Yt~nҨnvB^Bͮ㪎 #} ;K _nZ.hoN*N @~?s_x2 8 @`|= '!=`cN^LN M-ߦ<ۥؐ]뙯 x~{~y?AEG`^ȯMbM̫׿}զu_eO $ُr@c?eO_ϏCO5^&~\m@@ DP ,QD-^ĘQF=~9QH$CD)ʆ)]`Ki|XSN=! '˕eDTRM*,iRG^ŚUV])VUXe͞ZlLkݾW\um!, r!",(*6kFH=cĢĢklpKLSƧ[_GzӽbfJʬY\EX795=?8AD:LN?022녻s˰xh伝{ptPjγRUCoĠUXCȫˮIL>^Ƥ켲]ȨͲ9:Bq}koMлѹ_cHuCF;տзf_{Uζ尶my|T\brwQ꼯ͼżZehKѲodetxRST[;=8[$%-漧9;6ßGHQ??HasmrOu̯¼劋~Vijn !+ϵ02<,.9@C:⁡f}~_̲弭,-1ꭳlstxhlLx{SvӜ뼟伛YZ`_`gʹǦzTORAyz}PQX45>efkٸqa]Si/C-sxR-gb %}I&/@8d=)& JjHepSdIZ eU.ɑ]rr)R^w؃*K#liX3/̦6vMmL8qdf8vH~ @JЂMBІ:D'JъZͨF7юz HGJҒ(MJWҖLgJӚڴȩNwӞ@ PJԢHMRԦ:PR@ժZXͪVծz` XJV.hMZֶp\J׺xͫ^zӾ `KXFb:d'ԲZͬf7٫u hGKҚMjWZ3lTDlwv pKjMrЍtgZ6`P@fw 0rW ;-y{^mj|:_b3wS Im Xz `@_݆ ʚXU]En]ݠ szⶦx8Êg Xd51 6E&X^"ЁL&;ѭ.l{70P( ` F + iϜf2+T.7Dc/p[`V) ]/< `B &U~􅏐`(0A#(}\ .bp!PAH[W5sAkS@+&hJ JLh`h״ `AJ4"PEA@ Jםn~=h yy7,ӀtЅFE8]-jiL;zќjAU[1]}=]N᪈@ejrZ{6On g )bX/Z'}2-F" 2 gUxo' L app)S+h@:glh0h څSpSCIp< Yh P`xSj@`fi_  *U iU T @KxqUp`aT [2+c05{p ۀ+۲^U%UV`U)0d;Pd[ hKSn 9V' ( @s hջ`W8t  ` 0@tpt2K;`t`Y5E hDFS@ 5Z%B0y:SwP ʐS\`9><ū<SzySR  @ :ufjo@%I>(_0_( *l9e:ES0  LSD\GSQ~  s:0hOC~SQ,[h9ū`;uhy9u9D5\%TkUp m @-@U;@zpiT%U%2PU8`U Tɓ,,ɛ+08ȍɏU X0 @Q9^' MXknYkS mFIp`$y:uhBP؄]؃lօlZ pUE@9PU&p=KUUhmGGݟ8pPUPU MUapޏ޻,8-U [5lU"V@͓`mVnV"#M j 3Vq).(pϐV*˷0/3 ?lPiH,V]/HO[VW{54e SA 3E 2ehOPJ=l5E P 3 [ֵ[P` i\upvֆ=@ h `B $.oIRpftpBe?ZH{[ 'y[_ǟS${SOG;uY I׿ٿ?1$P  Ab1H%>@ĉ(Ղ@Is5męSNqq$ErN]dI>  nz͉# HB CC=?Ə>:\c&<4Q>Lq_2>qYd!b>dI'쌳'R&+2ˁJ;(&H#&(,pg.l %(0 O=5 H 9 &?O=3N% 81O 9P< IeEHԅ0tNbGJ>"<#PMnube蒅 B B#[h v5& h tX7eBPq$<ȀH"<1 8ԀVcaiʡ]RH!Tv&Ne9+)Zi>CJ0w)C-0PFHGdUn7#^!6x2k;pNK= WK R|R/hH .\[T@ PDwI@c^[ƛ--?zـjF\ Ł"x \%Wy 4n:d]G? ~x *rk̸GXR,|ɇ?~,I~W#&(xLw }Xt@*[ 7I&<ƅ„~"P "`0&B UNT0 MH 9!&?@5UQkd14ռ8FlbM hT!wdcAF4~rGI{HACh1pdHFh7Io B2(%:dd(E9JR򓗄)ǧI/4H+̒uK^җM9јD1PluLf49EҚf#MRʨnaoӜDg:G96`A~P-9OӞg>b2h@0MEhP2t gD%:QVԢLQvԣg?:RԤ'PCURF>iLe:SiNu̐?jP:Fըݳ"CmT6թON:U&jVUի^jVլgEkZպVխok\:Wծwk^ײl`;XְElbX66~uld%;YVֲlf5YzmhE;ZҖִEmj; x`kUmle;[ֶŭi vCkmp;\׸Enf! tɅnt;]V׺u7d1ox;^<׽o|+cPo~_׿sۤFpK3Up%~ n8c c@0 1! u(B3 %X !MHGi=-@T"-uTΐ$ )J57!@GWc ȠII ,51O}?bT Cv80m1:RB)g50$RV(i[j Į GXҍnc^Ac)o\W@zTD9*N,hHTwZESجK"iIQZ rs|V8n{[V M @!݃THv2RJ-Zz d.Cvm/G K*e=z ],U "Nڄ ȁB!=H⋗&$FDax!'&HG|ӄv/gș60p:حp, Bl#m94Y"/Hn$@^\2A .Q b"ɼ9*0#DfSk`@WRdh=H5+d"kkϛP'e 2ꍘA@8gd Bb^' @&kNQHg< 4B} Hb?`k_EBP!>͒{Wvmymu{dɪBeU/-ق| DY [PDG}$n˿_otDZ~!,_{!5Ⱦca%$"qbyA> &"xQtCx#b歓DAANG eBʋRn&J }#77 sCBdT &H=x8~ J)a ْ g+kD& p#l$U:U{.SۈQ򕿑 |H±s>Ea7cԫ4O~L~O;O|A5H"S'`Pl h)UF1,yq*P8ba xkс!usY# $a4DqaF'g1\U85T$W:G B8DXFxH6LȄIӄRT(@Jstzegn57BPQZNPXnouuv{[\c,.:\^eSU]یKLUbdj:Z[b}~DFOefmmnt{}rsyPSF')5gho*,8`ah_`g]_f-0;68;02<ˍP{⁆]~TU^rz?B?ꅬëm䷾r}`DGAbu`dNhip.Q@*89VYIoxhprw,B=<=G[^K~ZHKCLPEx}XJND_bMKro\HnuzWhlQ;>=d^1[EIq.@.Ab>9T7{P@`陼qvUހFkCfjXTIpG?_va]nrS{9UNz;X[B>\n5tM8S8R;j!R M2J+HVHA  #LР7*(Ǝ CI$D")R)Y!M4IfNv2e:tBSfѣH*]ʴӧPJJ5u^Qu`fQ#PBӋ9 1Cn\%?t W"EdNCA&I !㑞=H+Imˎ-1β=MӨS^]z[ ܻq$RǛܦ59@C)4oxbPo܃vG:߿60<bGG,2MZ FZt˟Oo]cdG.v!6uOzU@ CPB$\(!-Y `@ G`Ȑx_L@PXdG(` L@Q'P@"@Kc?i"*J 8 +Lp + $T`qUAۉ bb &xY&2gt".dez3$79h#)l 2**무jU'9to"@ؼc,pJqN  |8(9 '( YA0AGv@Dm*`L/e'%.F9D6h^J\X,1 \ˮU! < @W4!Pk, @!X'w)b42(Q܀MA0hS1928 AD339s/2.Հ.ۧ+NHܲ@ꌃ:,#F! U mp@{fT] U XfDEܜ} B $9{E?oP5/C$;U1tӍ} @,}AB @E$Dӆ+ Ct-౷|$2@ 1)| FNz PW!a;ByG<PA}pN$BcQH"x#Zb?t%q2'%N%%P L0!(`]P#=%!2ȌcR&g&ᨋa3bb0(&db&h#k&&֨r'y:%A)2*a))qѠ ɀ L")*(2) t<遇B+b$NKplP,BH,Ҕb-آ--"R.*.2/q/Z /# a2 X  l/$=1;1>#ec0,#y @vaP'r-mY-ap.GVy}t(20#342"/[) )m33F63:33A34Ƒ%A `vo7sCf6j3  )7t#_6cAp ܰ=yxXq8N!AB9S9̦9[C^U: :<A#Eg h;CeuF;<,;`=RF<"h1:ߖ;:}==#>Mp#:'=>#`>Z=?oTEP` D)Pvs%vT@q `9M`B@ @ `9-tB14Cx7ԤC&@$DtD%PJDNE z;V4IFEkGBG cFiԩ`ZuJGm$F 2I:Hn ѨBdZG$56ĨAGԣI*HZE*ZF|DJHଐtgdI:IǚF[j%A @6%vTL_tKKduzWkHˀvMޔXA $tNs'YNNxyOnUP%rJPQc*QS 2/eQUR'RT.R:+SRfaS'+gA$wJdYfprhPdoVo!˺op2k#$|prqpqr|qќ'0 nrmm[l7gaLTrkX27pӌܾ}rr s2Gs[js8g0]up*uT' \utLtd ǐ ,lvow[w3V#zwu{hy!x77W/ Mԟyz#Qԥ'0,(E]a[-^]XMWTMw}Ii!{8hp-H|"X1{vU'}g}אSAw~g}'ٜٞ٠ڢ=ڤ]ڦ}ڨڪڬڮڰ۲=۴}O]pW/b+[B)ܵܡ y A۬x<-8VZ(a $ʊd*PJ 8۝ PCɠ- )9a ")*ɒ.9q|):r"%VЈxXxY7#x؊HH=&[Џ @.!1 %>`Tj&A:uz2U, 9pS<u ńqϰ *7 )6@)$UixDiyu02}N2y2Ú+c>ј)Z9V@ -ihi/ٛ3 @#4Dz >=Zn*q!nuu M kՙPve98Pn2(3Dj=Wz= Ӥq:FJU+":3> AZ(Zc _Kj=o@z#SH>n~FOt7!*4#zШJ0(!hGڪIIxaGHJH:׊ J:tJ OntHu>4dz\z^~pP p0A@`)pPVjEMUW P 80(XԳ*A)gSr{R0R[%h"We[W k7tXߣ&@ĖcĈIA 4N b 7^Ugv< @fxXcvIzåȰm6!Eh*%%gm/}sҖL@N\Fl=CYve]ў7P28yfˌ,Dmx2sHFS:(PqR4fB:SѓEg=)5ԔĨGSԅfFctG3,&=ei )L:Ϛvի3 N:1BQ̜耖N]k(J/e&KIG|r52ՠU :ؗuW[SBԱXkL 4m/;YbVAjWZ6IӖ\ll*Ϣ-JxNg%t XsմdVwRi`Owz=lJ6"k,Q R%O=Ѵ`=vo~WpFFp`7p%1A`$Dj fB$ a}) 9 V1y!qN| < Q8N^cLY21 <%'I\)>&R$ure4 j2A &&NܓA)HhRhA)rQW5A qJb2@!8}xHu@?q>ЅLH@=p  djWZs~eBN6A ?8HɬklqZڹ0c#[)>P ̄ivl/%3`jG6tzn RN3N!,&@J;9yS<=dx;`u'Ln.pRV3Bn 0*~04P`t2@D`8&ȡ'膳 ntd hou+$DyMY\s}Nb>sw7A  @rj.aBD a #Y lB' 2Z\po-Zf EЂofz 2 ?PӿJaڏ `p-+ݻ@؃CP'HЁ =1#+ 8@h+ˉσAAtAX;+|A@A` =C0pC75A=1,C-B;?4Á(1D&CHiCXI > C*KA4xD3"8*P PXO[xX< ;# 8Bhx :8 x ȅS; XL&X0,R p  nƬǯ8< |'$?*(2r s3N6H1& H Q *H6hN[>:P ,ۺ(E^ŋ |DiȍT$$tH (ʢɞ$<6FI 4,˸ PD(:]RhS()RܳΧ1RHR:SHLC S $N=N6BCP R"TJeǃ*ɂ,(- *v p"Mȁ(ӁH4[OA V)P:PNyЁp+@ CzG}NPmo%nN T#8!pʪpr=Wk[XsEWumȊp8~ m,5 Aɥ{ p]%_=a bHW"1X5J':P S9ԙHٕ}=UR.ק%Lف,P_ UYڷZt489[mYHhj ܤc\iV%Y0ٽZ׊*`E(@x P  85@(@`lZȁյ4HSw *1Ѿ\W3xu(}3  {()›-e75^S^%1M I\3K BX=hZx) +M];158_8ZXE&uY8^`p]O]V`5aa#ah!I%V P ). h*݄؂ (f`Ε3Hkv頾Ng`yKNxFtf5-gP1N.lXnn)ΦOFI< &Gs~=Jтdox馠lzZ<(IOa89 9J A @68 n) ي XȁqڠX$c(6mq"qfqH 믠`r׊)O@qXq G-Y 9h xSfrqHf阏#h&o`g mxxv ?#?O%gr' Gr!;<;s=s6sQsT8ܶSt@ Xpؘ9O/u H.h(v: Tag/Lvg&0+ZnK?,?3( wb/8ˉǹ0z'8-ȸ:gq!wa pI)Xw 1Smyqwyωؙyro΢SI!a iʂ o70 z2Qxf'7GWgwY4 3R;8 Cd|b|; :3{|?13f88cğ1g FZ1f2;{3<#=볜81 gIK ʼnCKE3~ѴM[8UShs6(7g+8u `6bÉ[aļahj6w{É~O5QnVvص58(2l%BhLXh1 "EN Љ]͟Uc1ApbC*BTDʃȨR=*`0CQ6!׎SP6$V.H-ܸrҭk.އ%MFq/ߺX:04 \c  e0CcLD ƅ+ qt @jgvmu遱CpBESx93jER{vOa@`{Z! ('NPJ[~60}m[ 8 x ~eR-h` 9@\8)T@suPFor`EJd!iaGC.@~87cb:_ ڈc_,xx!DJ$"LXVa28&+0[pV?T9 x` {r&|B !`$\aДF4ة=QE (|gK bYz ?&,* z.D @B1pY.4@)QUj(C~-n)'jE}ِ2.$+"X B1/Cf(p: j [|1R'{ }hWE8%СO Dd T<&*:YL$XGBx#EWv5ct_j:S^l3 &(r`f9j^W'>@E!| " Yi=P'5LP+pfWg(ޥ7Q hHGmHUab;J 1P91@ 'W@yDvpKNBo}w^|G7ސ&wq '|XYPK6هI$Cّ