diff --git a/.circleci/config.yml b/.circleci/config.yml
new file mode 100644
index 00000000..cd3a50ed
--- /dev/null
+++ b/.circleci/config.yml
@@ -0,0 +1,67 @@
+version: 2.1
+jobs:
+ build_and_test:
+ docker:
+ - image: ${CIPHEROWL_ECR_URL}/cipherowl/circleci:latest
+ working_directory: ~/chainstorage
+ steps:
+ - checkout
+ - setup_remote_docker:
+ version: default
+ - run:
+ name: Install dependencies
+ command: |
+ set +x
+ make bootstrap
+ - run:
+ name: Run unit tests
+ command: "make test"
+ - run:
+ name: Run integration tests
+ command: |
+ set +x
+
+ docker compose -f docker-compose-testing.yml up -d --force-recreate
+ sleep 10
+
+ # Due to how the remote docker engine works with docker-compose
+ # in circleci, we have to run our integration tests from
+ # a remote container so that the tests can access network services
+ # spun up by docker-compose.
+ docker create -v /home/circleci --name chainstorage alpine:3.21 /bin/true
+ # docker cp /home/circleci/go chainstorage:/home/circleci/go
+ docker cp /home/circleci/chainstorage chainstorage:/home/circleci/chainstorage
+
+ docker run --network chainstorage_default \
+ --volumes-from chainstorage \
+ -w /home/circleci/chainstorage \
+ -e CHAINSTORAGE_AWS_POSTGRES_HOST=postgres \
+ -e CHAINSTORAGE_AWS_POSTGRES_PORT=5432 \
+ -e CHAINSTORAGE_AWS_POSTGRES_USER=postgres \
+ -e CHAINSTORAGE_AWS_POSTGRES_PASSWORD=postgres \
+ -e CHAINSTORAGE_AWS_POSTGRES_DATABASE=postgres \
+ ${CIPHEROWL_ECR_URL}/cipherowl/circleci:latest \
+ /bin/bash -c "sudo chown -R circleci:circleci ~/ && make bootstrap && \
+ echo '🔧 Test databases already set up by init script' && \
+ TEST_TYPE=integration go test ./... -v -p=1 -parallel=1 -timeout=15m -failfast -run=TestIntegration"
+ - run:
+ name: Run functional tests
+ command: |
+ set +x
+ echo "functional tests skipped"
+
+ # docker run --network chainstorage_default \
+ # --volumes-from chainstorage \
+ # -w /home/circleci/chainstorage \
+ # ${CIPHEROWL_ECR_URL}/cipherowl/circleci:latest \
+ # /bin/bash -c "sudo chown -R circleci:circleci ~/ && make bootstrap && TEST_TYPE=functional go test ./... -v -p=1 -parallel=1 -timeout=45m -failfast -run=TestIntegration"
+
+ docker compose -f docker-compose-testing.yml down
+
+workflows:
+ version: 2
+ default:
+ jobs:
+ - build_and_test:
+ name: build_and_test
+ context: cipherowl_build_context
\ No newline at end of file
diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
index 2c7d1708..342d204b 100644
--- a/.github/dependabot.yaml
+++ b/.github/dependabot.yaml
@@ -1,7 +1,73 @@
version: 2
+
+# Policy: ignore semver-major for version updates to reduce noise.
+# Security *alerts* still fire — triage manually when only a major bump fixes it.
+# Ref: aligned with monorepo dependabot policy.
+
updates:
- # Maintain dependencies for GitHub Actions
+ # ─── Go modules ───────────────────────────────────────────
+ - package-ecosystem: "gomod"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ cooldown: &cooldown-policy
+ default-days: 7
+ semver-major-days: 14
+ semver-minor-days: 7
+ semver-patch-days: 3
+ groups:
+ go_modules:
+ patterns:
+ - "*"
+ labels:
+ - "dependencies"
+ - "go"
+ open-pull-requests-limit: 10
+ ignore:
+ - dependency-name: "*"
+ update-types:
+ - "version-update:semver-major"
+
+ # ─── GitHub Actions ───────────────────────────────────────
- package-ecosystem: "github-actions"
directory: "/"
schedule:
- interval: "daily"
+ interval: "weekly"
+ cooldown: *cooldown-policy
+ groups:
+ github-actions:
+ patterns:
+ - "*"
+ labels:
+ - "dependencies"
+ - "github-actions"
+ open-pull-requests-limit: 5
+ ignore:
+ - dependency-name: "*"
+ update-types:
+ - "version-update:semver-major"
+
+ # ─── Docker ───────────────────────────────────────────────
+ # Only tracks Dockerfiles; docker-compose-*.yml is not monitored by design.
+ - package-ecosystem: "docker"
+ directory: "/"
+ schedule:
+ interval: "weekly"
+ cooldown: *cooldown-policy
+ groups:
+ docker:
+ patterns:
+ - "*"
+ labels:
+ - "dependencies"
+ - "docker"
+ open-pull-requests-limit: 5
+ ignore:
+ - dependency-name: "*"
+ update-types:
+ - "version-update:semver-major"
+ # Pin golang base image to current major.minor.
+ # Upgrading Go affects other services' dependencies and must be
+ # coordinated manually across the org.
+ - dependency-name: "golang"
+ versions: [">= 1.25"]
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 05b2edd4..3d7e7dcd 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -17,7 +17,7 @@ jobs:
- "1.21"
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: "Setup environment"
uses: ./.github/workflows/setup
with:
@@ -34,7 +34,7 @@ jobs:
- "1.21"
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: "Setup environment"
uses: ./.github/workflows/setup
with:
@@ -51,7 +51,7 @@ jobs:
- "1.21"
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v7
- name: "Setup environment"
uses: ./.github/workflows/setup
with:
diff --git a/.gitignore b/.gitignore
index f944dfba..b9e540d3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -28,5 +28,8 @@
# pg
postgres_*
+# ignore all files in dev-data/
+dev-data/
+
# ignore binaries
/scripts/bin
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 00000000..49aa57b3
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,173 @@
+# ChainStorage Development Guide
+
+This guide helps Claude understand the ChainStorage project structure and common development tasks.
+
+## Project Overview
+
+ChainStorage is a blockchain data storage and processing system that:
+- Continuously replicates blockchain changes (new blocks)
+- Acts as a distributed file system for blockchain data
+- Stores raw data in horizontally-scalable storage (S3 + DynamoDB)
+- Supports multiple blockchains: Ethereum, Bitcoin, Solana, Polygon, etc.
+- Can serve up to 1,500 blocks per second in production
+
+## Key Commands
+
+### Testing
+```bash
+# Run all unit tests
+make test
+
+# Run specific package tests
+make test TARGET=internal/blockchain/...
+
+# Run integration tests
+make integration TARGET=internal/storage/...
+
+# Run functional tests (requires secrets.yml)
+make functional TARGET=internal/workflow/...
+```
+
+### Linting and Type Checking
+```bash
+# Run linter (includes go vet, errcheck, ineffassign)
+make lint
+# Note: May encounter errors with Go versions > 1.22
+
+# No separate typecheck command - type checking happens during build
+```
+
+### Building
+```bash
+# Initial setup (once)
+make bootstrap
+
+# Build everything
+make build
+
+# Generate protobuf files
+make proto
+
+# Generate configs from templates
+make config
+```
+
+### Local Development
+```bash
+# Start local infrastructure (LocalStack)
+make localstack
+
+# Start server (Ethereum mainnet)
+make server
+
+# Start server (other networks)
+make server CHAINSTORAGE_CONFIG=ethereum_goerli
+```
+
+## Project Structure
+
+```
+/cmd/ - Command line tools
+ /admin/ - Admin CLI tool
+ /api/ - API server
+ /server/ - Main server
+ /worker/ - Worker processes
+
+/internal/ - Core implementation
+ /blockchain/ - Blockchain clients and parsers
+ /client/ - Chain-specific clients
+ /parser/ - Chain-specific parsers
+ /storage/ - Storage implementations
+ /blobstorage/ - S3/GCS storage
+ /metastorage/ - DynamoDB/Firestore storage
+ /workflow/ - Temporal workflows
+ /activity/ - Workflow activities
+
+/config/ - Generated configurations
+/config_templates/ - Configuration templates
+/protos/ - Protocol buffer definitions
+/sdk/ - Go SDK for consumers
+```
+
+## Key Workflows
+
+1. **Backfiller**: Backfills historical blocks
+2. **Poller**: Polls for new blocks continuously
+3. **Streamer**: Streams blocks in real-time
+4. **Monitor**: Monitors system health
+5. **Benchmarker**: Benchmarks performance
+
+## Environment Variables
+
+- `CHAINSTORAGE_NAMESPACE`: Service namespace (default: chainstorage)
+- `CHAINSTORAGE_CONFIG`: Format: `{blockchain}-{network}` (e.g., ethereum-mainnet)
+- `CHAINSTORAGE_ENVIRONMENT`: Environment (local/development/production)
+
+## Common Tasks
+
+### Adding Support for New Blockchain
+1. Create config templates in `/config_templates/chainstorage/{blockchain}/{network}/`
+2. Implement client in `/internal/blockchain/client/{blockchain}/`
+3. Implement parser in `/internal/blockchain/parser/{blockchain}/`
+4. Run `make config` to generate configs
+5. Add tests
+
+### Debugging LocalStack Services
+```bash
+# Check S3 files
+aws s3 --no-sign-request --region local --endpoint-url http://localhost:4566 ls --recursive example-chainstorage-ethereum-mainnet-dev/
+
+# Check DynamoDB
+aws dynamodb --no-sign-request --region local --endpoint-url http://localhost:4566 scan --table-name example_chainstorage_blocks_ethereum_mainnet
+
+# Check SQS DLQ
+aws sqs --no-sign-request --region local --endpoint-url http://localhost:4566 receive-message --queue-url "http://localhost:4566/000000000000/example_chainstorage_blocks_ethereum_mainnet_dlq"
+```
+
+### Working with Temporal Workflows
+```bash
+# Start backfiller
+go run ./cmd/admin workflow start --workflow backfiller --input '{"StartHeight": 11000000, "EndHeight": 11000100}' --blockchain ethereum --network mainnet --env local
+
+# Start poller
+go run ./cmd/admin workflow start --workflow poller --input '{"Tag": 0, "MaxBlocksToSync": 100}' --blockchain ethereum --network mainnet --env local
+
+# Check workflow status
+tctl --address localhost:7233 --namespace chainstorage-ethereum-mainnet workflow show --workflow_id workflow.backfiller
+```
+
+## Important Notes
+
+1. **Always run tests before committing**: Use `make test` and `make lint`
+2. **Config generation**: After modifying templates, run `make config`
+3. **Secrets**: Never commit `secrets.yml` files (used for endpoint configurations)
+4. **Endpoint groups**: Master (sticky) for canonical chain, Slave (round-robin) for data ingestion
+5. **Parser types**: Native (default), Mesh, or Raw
+
+## Dependencies
+
+- Go 1.22 (required - newer versions may cause lint errors)
+- Protobuf 25.2
+- Temporal (workflow engine)
+- LocalStack (local AWS services)
+- Docker & Docker Compose
+
+## Architecture Insights
+
+### Client Architecture
+- **Multi-endpoint system**: Master (primary), Slave (load distribution), Validator, Consensus
+- **Protocol support**: JSON-RPC (most chains) and REST API (Rosetta)
+- **Shared implementations**: EVM chains (Polygon, BSC, Arbitrum) share Ethereum client code
+- **Factory pattern**: Each blockchain has a client factory registered with dependency injection
+
+### Key Design Patterns
+1. **Interceptor Pattern**: Wraps clients for instrumentation and parsing
+2. **Option Pattern**: Modifies client behavior (e.g., WithBestEffort())
+3. **Batch Processing**: Configurable batch sizes for performance
+4. **Error Handling**: Standardized errors with network-specific handling
+
+### Supported Blockchains
+- **EVM-based**: Ethereum, Polygon, BSC, Arbitrum, Optimism, Base, Fantom, Avalanche
+- **Bitcoin-based**: Bitcoin, Bitcoin Cash, Dogecoin, Litecoin
+- **Other**: Solana, Aptos, Tron
+- **Special**: Ethereum Beacon Chain support
\ No newline at end of file
diff --git a/COMPONENT_DIAGRAM.md b/COMPONENT_DIAGRAM.md
new file mode 100644
index 00000000..1c979cf6
--- /dev/null
+++ b/COMPONENT_DIAGRAM.md
@@ -0,0 +1,365 @@
+# ChainStorage Component Diagram
+
+## Overview
+
+ChainStorage is a distributed blockchain data storage and processing system that continuously replicates blockchain data and serves it through APIs.
+
+## High-Level Architecture (Simplified)
+
+```mermaid
+graph LR
+ %% External
+ BC[Blockchain
Nodes]
+ ExtClients[External
Clients]
+
+ %% Core Components
+ API[API Server]
+ WF[Workflow
Engine]
+ BCC[Blockchain
Clients]
+ P[Parsers]
+
+ %% Storage
+ BS[Blob
Storage]
+ MS[Meta
Storage]
+
+ %% Flow
+ ExtClients -->|REST/gRPC| API
+ API --> BS
+ API --> MS
+
+ WF -->|Fetch| BCC
+ BCC -->|Raw Data| BC
+ BCC --> P
+ P --> BS
+ P --> MS
+
+ %% Styling with Nord palette
+ style BC fill:#d8dee9,stroke:#2e3440,stroke-width:2px,color:#2e3440
+ style ExtClients fill:#d8dee9,stroke:#2e3440,stroke-width:2px,color:#2e3440
+ style API fill:#3b4252,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style WF fill:#434c5e,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style BCC fill:#4c566a,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style P fill:#4c566a,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style BS fill:#3b4252,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style MS fill:#3b4252,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+```
+
+## Detailed Architecture
+
+```mermaid
+graph TB
+ %% External Systems
+ subgraph EXT["🌐 External Systems"]
+ BC[Blockchain Nodes
Bitcoin/Ethereum/Solana/etc]
+ Client[External Clients
SDK/REST/gRPC]
+ Temporal[Temporal Server]
+ AWS[AWS Services
S3/DynamoDB/SQS]
+ GCP[GCP Services
GCS/Firestore]
+ end
+
+ %% Entry Points
+ subgraph ENTRY["🚀 Entry Points (cmd/)"]
+ API[API Server
REST/gRPC]
+ Server[Main Server
Workflow Manager]
+ Worker[Worker
Activity Executor]
+ Admin[Admin CLI]
+ Cron[Cron Jobs]
+ end
+
+ %% Storage Layer
+ subgraph STORAGE["💾 Storage Layer"]
+ BlobStorage[BlobStorage
Raw Block Data]
+ MetaStorage[MetaStorage
Metadata & Indexes]
+ end
+
+ %% Blockchain Layer
+ subgraph BLOCKCHAIN["⛓️ Blockchain Layer"]
+ ClientFactory[Client Factory]
+ subgraph CLIENTS["Multi-Endpoint Clients"]
+ Master[Master Client
Sticky Sessions]
+ Slave[Slave Client
Load Balanced]
+ Validator[Validator Client]
+ Consensus[Consensus Client]
+ end
+
+ subgraph PARSERS["Parsers"]
+ NativeParser[Native Parser]
+ RosettaParser[Rosetta Parser]
+ TrustlessValidator[Trustless Validator]
+ end
+ end
+
+ %% Workflow Engine
+ subgraph WORKFLOW["⚙️ Workflow Engine"]
+ subgraph WORKFLOWS["Workflows"]
+ Backfiller[Backfiller
Historical Blocks]
+ Poller[Poller
New Blocks]
+ Streamer[Streamer
Real-time]
+ Monitor[Monitor
Health Check]
+ CrossValidator[Cross Validator]
+ Replicator[Replicator]
+ end
+
+ subgraph ACTIVITIES["Activities"]
+ Extractor[Extractor]
+ Loader[Loader]
+ Syncer[Syncer]
+ Reader[Reader]
+ EventLoader[Event Loader]
+ end
+ end
+
+ %% Connections
+ Client -->|REST/gRPC| API
+ API -->|Read| BlobStorage
+ API -->|Query| MetaStorage
+
+ Server -->|Schedule| Temporal
+ Worker -->|Execute| Temporal
+ Temporal -->|Orchestrate| WORKFLOWS
+
+ WORKFLOWS -->|Execute| ACTIVITIES
+ ACTIVITIES -->|Use| ClientFactory
+ ClientFactory -->|Create| Master
+ ClientFactory -->|Create| Slave
+ ClientFactory -->|Create| Validator
+ ClientFactory -->|Create| Consensus
+
+ Master -->|Fetch| BC
+ Slave -->|Fetch| BC
+ Validator -->|Validate| BC
+ Consensus -->|Check| BC
+
+ ACTIVITIES -->|Parse| NativeParser
+ NativeParser -->|Convert| RosettaParser
+ ACTIVITIES -->|Validate| TrustlessValidator
+
+ ACTIVITIES -->|Store Raw| BlobStorage
+ ACTIVITIES -->|Store Meta| MetaStorage
+ BlobStorage -->|S3/GCS| AWS
+ BlobStorage -->|S3/GCS| GCP
+ MetaStorage -->|DynamoDB| AWS
+ MetaStorage -->|Firestore| GCP
+
+ Admin -->|Manual Ops| WORKFLOWS
+ Admin -->|Direct Access| BlobStorage
+ Admin -->|Direct Access| MetaStorage
+
+ Cron -->|Scheduled| WORKFLOWS
+
+ %% Styling with Nord palette
+ classDef external fill:#d8dee9,stroke:#2e3440,stroke-width:2px,color:#2e3440
+ classDef entry fill:#3b4252,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ classDef storage fill:#434c5e,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ classDef workflow fill:#4c566a,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ classDef blockchain fill:#3b4252,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ classDef subgraphStyle fill:#d8dee9,stroke:#2e3440,stroke-width:1px,color:#2e3440
+
+ class BC,Client,Temporal,AWS,GCP external
+ class API,Server,Worker,Admin,Cron entry
+ class BlobStorage,MetaStorage storage
+ class Backfiller,Poller,Streamer,Monitor,CrossValidator,Replicator,Extractor,Loader,Syncer,Reader,EventLoader workflow
+ class ClientFactory,Master,Slave,Validator,Consensus,NativeParser,RosettaParser,TrustlessValidator blockchain
+ class EXT,ENTRY,STORAGE,BLOCKCHAIN,WORKFLOW,WORKFLOWS,ACTIVITIES,CLIENTS,PARSERS subgraphStyle
+```
+
+## Component Breakdown
+
+### Workflow Engine Detail
+
+```mermaid
+graph TB
+ T[Temporal Server]
+
+ subgraph Workflows
+ BF[Backfiller]
+ PL[Poller]
+ ST[Streamer]
+ MN[Monitor]
+ CV[Cross Validator]
+ RP[Replicator]
+ end
+
+ subgraph Activities
+ EX[Extractor]
+ LD[Loader]
+ SY[Syncer]
+ RD[Reader]
+ EL[Event Loader]
+ end
+
+ T --> Workflows
+ Workflows --> Activities
+
+ style T fill:#d8dee9,stroke:#2e3440,stroke-width:2px,color:#2e3440
+ style Workflows fill:#434c5e,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style Activities fill:#4c566a,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+```
+
+### Blockchain Client Architecture
+
+```mermaid
+graph LR
+ CF[Client Factory]
+
+ subgraph Endpoints
+ M[Master
Sticky Sessions]
+ S[Slave
Load Balanced]
+ V[Validator]
+ C[Consensus]
+ end
+
+ subgraph Parsers
+ NP[Native Parser]
+ RP[Rosetta Parser]
+ TV[Trustless Validator]
+ end
+
+ CF --> Endpoints
+ Endpoints --> Parsers
+
+ style CF fill:#3b4252,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style Endpoints fill:#434c5e,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style Parsers fill:#4c566a,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+```
+
+### Storage Architecture
+
+```mermaid
+graph TB
+ subgraph BlobStorage
+ S3[AWS S3]
+ GCS[Google Cloud Storage]
+ end
+
+ subgraph MetaStorage
+ DDB[DynamoDB]
+ FS[Firestore]
+ end
+
+ BS[Blob Storage
Interface] --> S3
+ BS --> GCS
+
+ MS[Meta Storage
Interface] --> DDB
+ MS --> FS
+
+ style BlobStorage fill:#434c5e,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style MetaStorage fill:#434c5e,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style BS fill:#3b4252,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+ style MS fill:#3b4252,stroke:#2e3440,stroke-width:2px,color:#d8dee9
+```
+
+## Component Details
+
+### Entry Points
+
+1. **API Server** (`cmd/api`)
+ - Serves REST and gRPC endpoints
+ - Provides block data, events, and metadata
+ - Handles authentication and rate limiting
+
+2. **Main Server** (`cmd/server`)
+ - Manages Temporal workflows
+ - Coordinates blockchain data ingestion
+ - Handles workflow scheduling
+
+3. **Worker** (`cmd/worker`)
+ - Executes Temporal activities
+ - Performs actual blockchain data fetching
+ - Handles parsing and validation
+
+4. **Admin CLI** (`cmd/admin`)
+ - Manual workflow management
+ - Direct storage operations
+ - Debugging and maintenance
+
+5. **Cron** (`cmd/cron`)
+ - Scheduled maintenance tasks
+ - Periodic health checks
+ - Cleanup operations
+
+### Storage Layer
+
+1. **BlobStorage**
+ - Stores compressed raw block data
+ - Supports S3 (AWS) and GCS (GCP)
+ - Provides presigned URLs for direct access
+ - Handles block versioning
+
+2. **MetaStorage**
+ - Stores block metadata (height, hash, timestamp)
+ - Maintains transaction indexes
+ - Tracks events (block added/removed)
+ - Supports DynamoDB (AWS) and Firestore (GCP)
+
+### Blockchain Layer
+
+1. **Client System**
+ - **Master**: Primary endpoint with sticky sessions
+ - **Slave**: Load-balanced endpoints for ingestion
+ - **Validator**: Data validation endpoints
+ - **Consensus**: Consensus verification endpoints
+
+2. **Parser System**
+ - **Native Parser**: Blockchain-specific parsing
+ - **Rosetta Parser**: Standardized Rosetta format
+ - **Trustless Validator**: Cryptographic validation
+
+### Workflow Engine
+
+1. **Core Workflows**
+ - **Backfiller**: Historical block ingestion
+ - **Poller**: Continuous new block polling
+ - **Streamer**: Real-time block streaming
+ - **Monitor**: System health monitoring
+ - **Cross Validator**: Cross-chain validation
+ - **Replicator**: Data replication
+
+2. **Activities**
+ - **Extractor**: Fetches raw blocks
+ - **Loader**: Stores processed blocks
+ - **Syncer**: Synchronizes blockchain state
+ - **Reader**: Reads stored data
+ - **Event Loader**: Processes blockchain events
+
+## Data Flow
+
+1. **Ingestion Flow**
+ ```
+ Blockchain Node → Client → Parser → Validator → Storage
+ ```
+
+2. **Query Flow**
+ ```
+ External Client → API Server → Storage → Response
+ ```
+
+3. **Workflow Flow**
+ ```
+ Temporal → Workflow → Activity → Client → Blockchain
+ ↓
+ Storage
+ ```
+
+## Key Design Patterns
+
+- **Factory Pattern**: Dynamic blockchain client/parser creation
+- **Interceptor Pattern**: Request/response instrumentation
+- **Module Pattern**: Clean separation of concerns
+- **Dependency Injection**: Using Uber FX framework
+- **Option Pattern**: Flexible configuration
+
+## Supported Blockchains
+
+- **EVM**: Ethereum, Polygon, BSC, Arbitrum, Optimism, Base, Fantom, Avalanche
+- **Bitcoin-based**: Bitcoin, Bitcoin Cash, Dogecoin, Litecoin
+- **Others**: Solana, Aptos, Tron
+- **Special**: Ethereum Beacon Chain
+
+## Scalability Features
+
+- Horizontal scaling via distributed storage
+- Multi-endpoint load balancing
+- Temporal workflow orchestration
+- Configurable batch processing
+- Support for 1,500+ blocks/second
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 8d875713..82210788 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.22 as build
+FROM golang:1.24 as build
ENV GO111MODULE on
@@ -11,7 +11,7 @@ RUN go mod download
COPY . ./
RUN make bin
-FROM golang:1.22 as deploy
+FROM golang:1.24 as deploy
WORKDIR /app/
diff --git a/README.md b/README.md
index 2b334835..94b0ee93 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,10 @@
- [Overriding the Configuration](#overriding-the-configuration)
- [Development](#development)
- [Running Server](#running-server)
+ - [Running with PostgreSQL](#running-with-postgresql)
+ - [PostgreSQL Role-Based Setup (Recommended)](#postgresql-role-based-setup-recommended)
+ - [Local Development Setup](#local-development-setup)
+ - [Admin CLI](#admin-cli)
- [AWS localstack](#aws-localstack)
- [Temporal Workflow](#temporal-workflow)
- [Failover](#failover)
@@ -55,33 +59,52 @@ It aims to provide an efficient and flexible way to access the on-chain data:
- Flexibility is improved by decoupling data interpretation from data ingestion. ChainStorage stores the raw data, and the parsing is deferred until the data is consumed. The parsers are shipped as part of the SDK and run on the consumer side. Thanks to the ELT (Extract, Load, Transform) architecture, we can quickly iterate on the parser without ingesting the data from the blockchain again.
## Quick Start
-
-Make sure your local go version is 1.22 by running the following commands:
-```shell
-brew install go@1.22
-brew unlink go
-brew link go@1.22
-
-brew install protobuf@25.2
-brew unlink protobuf
-brew link protobuf
-
-```
-
-To set up for the first time (only done once):
+This section will guide you through setting up ChainStorage on your local machine for development.
+
+### Prerequisites
+1. **Go (version 1.24):**
+ ```shell
+ brew install go@1.24
+ brew unlink go
+ brew link go@1.24
+ ```
+ Verify your Go installation:
+ ```shell
+ go version
+ ```
+2. **Protocol Buffer Compiler (protobuf):** Used for code generation based on `.proto` files.
+ ```shell
+ brew install protobuf@29
+ brew unlink protobuf
+ brew link protobuf@29
+ ```
+ Verify your installation:
+ ```shell
+ protoc --version
+ ```
+
+### Initial Setup
+
+This command (run only once) installs necessary Go tools for development, like linters and code generators.
```shell
make bootstrap
```
-Rebuild everything:
+### Build the Project
+
+This command compiles the ChainStorage Go programs.
```shell
make build
```
+You'll run this command whenever you make changes to the Go source code.
-Generate Protos:
+### Generate Protocol Buffers
+
+ChainStorage uses Protocol Buffers to define data structures. This command generates Go code from those definitions.
```shell
make proto
```
+You'll need to run this if you change any `.proto` files (usually located in the `protos/` directory).
## Command Line
@@ -119,14 +142,24 @@ All sub-commands require the `blockchain`, `env`, `network` flags.
### Block Command
-Fetch a block from ethereum mainnet:
+This command allows you to fetch and inspect individual blocks from a specified blockchain and network.
+
+**Usage Example:**
+
+Fetch block #46147 from Ethereum mainnet, using your local configuration:
```shell
-go run ./cmd/admin block --blockchain ethereum --network mainnet --env local --height 46147
+go run ./cmd/admin/main.go block --blockchain ethereum --network mainnet --env local --height 46147
```
+* `block`: The command to fetch block data.
+* `--blockchain ethereum --network mainnet --env local`: These flags specify the target (Ethereum mainnet) and the configuration environment (local).
+* `--height 46147`: The specific block number you want to retrieve.
+
+You can also fetch blocks from other supported blockchains and networks by changing the flag values:
-Fetch a block from ethereum goerli:
+Fetch a block from Ethereum Goerli testnet:
```shell
-go run ./cmd/admin block --blockchain ethereum --network goerli --env local --height 46147
+# Assuming Goerli is configured and data is available
+go run ./cmd/admin/main.go block --blockchain ethereum --network goerli --env local --height 12345
```
### Backfill Command (development)
@@ -187,19 +220,44 @@ make functional TARGET=internal/blockchain/... TEST_FILTER=TestIntegrationPolygo
```
## Configuration
-
+Configuration in ChainStorage tells the system:
+1. Which blockchain to connect to (like Ethereum, Bitcoin, etc.)
+2. How to connect to that blockchain (which nodes/servers to use)
+3. Where to store the data it collects
+4. How to process and manage the data
### Dependency overview
To understand the structure and elements of ChainStorage's config, it's important to comprehend its dependencies.
-- **Temporal**: Temporal is a workflow engine that orchestrates the data ingestion workflow. It calls ChainStorage service endpoint to complete various of tasks.
-- **Blob storage** - current implementation is on AWS S3, and the local service is provied by localstack
-- **Key value storage** - current implemnentation is based on dynamodb and the local service is provied by localstack
-- **Dead Letter queue** - current implementation is on SQS and the local service is provied by localstack
+ChainStorage needs several services to work properly:
+
+1. **Blockchain Nodes**: These are servers that maintain a copy of the blockchain. ChainStorage connects to these to get blockchain data.
+ - Example: An Ethereum node that provides information about Ethereum transactions and blocks
+
+2. **Storage Systems**:
+ - **Blob storage** - current implementation is on AWS S3, and the local service is provied by localstack
+ - **Key value storage** - current implemnentation is based on dynamodb and the local service is provied by localstack
+ - **Dead Letter queue** - current implementation is on SQS and the local service is provied by localstack
+
+3. **Workflow Engine** (Temporal):Temporal is a workflow engine that orchestrates the data ingestion workflow. It calls ChainStorage service endpoint to complete various of tasks.
+
### Template location and generated config
The config template directory is in `config_templates/config`, which `make config` reads this directory and generates the config into the `config/chainstorage` directory.
+### Creating New Configurations
+
+Every new asset in ChainStorage consists of ChainStorage configuration files.
+These configuration files are generated from `.template.yml` template files using:
+
+```shell
+make config
+```
+
+these templates will be under a directory dedicated to storing the config templates
+in a structure that mirrors the final config structure of the `config`
+directories. All configurations from this directory will be generated within the final
+respective config directories
### Environment Variables
@@ -218,19 +276,6 @@ The directory structure is as follows: `config/{namespace}/{blockchain}/{network
This env var controls the `{environment}` in which the service is deployed. Possible values include `production`
, `development`, and `local` (which is also the default value).
-### Creating New Configurations
-
-Every new asset in ChainStorage consists of ChainStorage configuration files.
-These configuration files are generated from `.template.yml` template files using:
-
-```shell
-make config
-```
-
-these templates will be under a directory dedicated to storing the config templates
-in a structure that mirrors the final config structure of the `config`
-directories. All configurations from this directory will be generated within the final
-respective config directories
### Template Format and Inheritance
@@ -325,7 +370,9 @@ chain:
```
### Overriding the Configuration
+You can override configurations in two ways:
+1. **Environment Variables**:
You may override any configuration using an environment variable. The environment variable should be prefixed with
"CHAINSTORAGE_". For nested dictionary, use underscore to separate the keys.
@@ -333,10 +380,31 @@ For example, you may override the endpoint group config at runtime by injecting
* master: CHAINSTORAGE_CHAIN_CLIENT_MASTER_ENDPOINT_GROUP
* slave: CHAINSTORAGE_CHAIN_CLIENT_SLAVE_ENDPOINT_GROUP
-Alternatively, you may override the configuration by creating `secrets.yml` within the same directory. Its attributes
+**Security Best Practice - PostgreSQL Credentials:**
+For sensitive data like database passwords, always use environment variables instead of hardcoding them in config files:
+```shell
+# PostgreSQL credentials (never put these in config files!)
+export CHAINSTORAGE_AWS_POSTGRES_USER="your_username"
+export CHAINSTORAGE_AWS_POSTGRES_PASSWORD="your_secure_password"
+
+# Storage type configuration
+export CHAINSTORAGE_STORAGE_TYPE_META="POSTGRES"
+```
+
+2. **secrets.yml**: Alternatively, you may override the configuration by creating `secrets.yml` within the same directory. Its attributes
will be merged into the runtime configuration and take the highest precedence. Note that this file may contain
credentials and is excluded from check-in by `.gitignore`.
+Example `config/chainstorage/ethereum/mainnet/.secrets.yml`:
+```yaml
+storage_type:
+ meta: POSTGRES
+aws:
+ postgres:
+ user: your_username
+ password: your_secure_password
+```
+
## Development
### Running Server
@@ -362,6 +430,211 @@ make server
make server CHAINSTORAGE_CONFIG=ethereum_goerli
```
+### Running with PostgreSQL
+
+ChainStorage supports PostgreSQL as an alternative to DynamoDB for metadata storage. Here's how to set it up:
+
+#### 1. Start PostgreSQL Database
+
+You can use Docker to run PostgreSQL locally:
+
+```shell
+# Start PostgreSQL container
+docker run --name chainstorage-postgres \
+ -e POSTGRES_USER=temporal \
+ -e POSTGRES_PASSWORD=temporal \
+ -e POSTGRES_DB=postgres \
+ -p 5432:5432 \
+ -d postgres:13
+```
+
+Or add it to your existing docker-compose setup.
+
+#### 2. Configure Meta Storage Type
+
+Create or modify your local config to use PostgreSQL instead of DynamoDB. You have two options:
+
+**Option A: Create a local secrets file (recommended for development)**
+
+Create `config/chainstorage/{blockchain}/{network}/.secrets.yml` (e.g., `config/chainstorage/ethereum/mainnet/.secrets.yml`):
+
+```yaml
+storage_type:
+ meta: POSTGRES
+```
+
+**Option B: Set via environment variable**
+
+```shell
+export CHAINSTORAGE_STORAGE_TYPE_META=POSTGRES
+```
+
+#### 3. Set PostgreSQL Credentials
+
+Since PostgreSQL credentials should not be hardcoded in config files, set them via environment variables:
+
+```shell
+# PostgreSQL connection details
+export CHAINSTORAGE_AWS_POSTGRES_USER="temporal"
+export CHAINSTORAGE_AWS_POSTGRES_PASSWORD="temporal"
+export CHAINSTORAGE_AWS_POSTGRES_HOST="localhost"
+export CHAINSTORAGE_AWS_POSTGRES_PORT="5432"
+export CHAINSTORAGE_AWS_POSTGRES_SSL_MODE="require"
+```
+
+#### 4. Run the Server
+
+Now start the server with PostgreSQL configuration:
+
+```shell
+# Method 1: Using exported environment variables
+make server
+
+# Method 2: Setting environment variables inline
+CHAINSTORAGE_STORAGE_TYPE_META=POSTGRES \
+CHAINSTORAGE_AWS_POSTGRES_USER="temporal" \
+CHAINSTORAGE_AWS_POSTGRES_PASSWORD="temporal" \
+make server
+```
+
+#### PostgreSQL Configuration Reference
+
+The following environment variables can be used to configure PostgreSQL:
+
+| Environment Variable | Config Path | Description | Default |
+|---------------------|-------------|-------------|---------|
+| `CHAINSTORAGE_AWS_POSTGRES_HOST` | `aws.postgres.host` | PostgreSQL hostname | `localhost` |
+| `CHAINSTORAGE_AWS_POSTGRES_PORT` | `aws.postgres.port` | PostgreSQL port | `5432` |
+| `CHAINSTORAGE_AWS_POSTGRES_USER` | `aws.postgres.user` | PostgreSQL username | (required) |
+| `CHAINSTORAGE_AWS_POSTGRES_PASSWORD` | `aws.postgres.password` | PostgreSQL password | (required) |
+| `CHAINSTORAGE_AWS_POSTGRES_DATABASE` | `aws.postgres.database` | Database name | `chainstorage_{blockchain}_{network}` |
+| `CHAINSTORAGE_AWS_POSTGRES_SSL_MODE` | `aws.postgres.ssl_mode` | SSL mode | `require` |
+| `CHAINSTORAGE_AWS_POSTGRES_MAX_CONNECTIONS` | `aws.postgres.max_connections` | Maximum connection pool size | `25` |
+| `CHAINSTORAGE_AWS_POSTGRES_MIN_CONNECTIONS` | `aws.postgres.min_connections` | Minimum connection pool size | `5` |
+| `CHAINSTORAGE_AWS_POSTGRES_CONNECT_TIMEOUT` | `aws.postgres.connect_timeout` | Connection establishment timeout | `30s` |
+| `CHAINSTORAGE_AWS_POSTGRES_STATEMENT_TIMEOUT` | `aws.postgres.statement_timeout` | Statement/transaction timeout | `60s` |
+| `CHAINSTORAGE_STORAGE_TYPE_META` | `storage_type.meta` | Meta storage type | `DYNAMODB` |
+
+#### Database Schema
+
+ChainStorage will automatically create the necessary database schema and run migrations when it starts up. The database will contain tables for:
+- `block_metadata` - Block metadata and headers
+- `canonical_blocks` - Canonical chain state
+- `block_events` - Blockchain event log
+
+### PostgreSQL Setup
+
+ChainStorage supports PostgreSQL as an alternative to DynamoDB for metadata storage with role-based access for enhanced security.
+
+#### Local Development
+
+**Quick Start:**
+```bash
+# Start PostgreSQL with automatic database initialization
+docker-compose -f docker-compose-local-dev.yml up -d chainstorage-postgres
+```
+
+This automatically creates:
+- Shared `chainstorage_worker` and `chainstorage_server` roles
+- Databases for all supported networks (ethereum_mainnet, bitcoin_mainnet, etc.)
+- Proper permissions (worker: read-write, server: read-only)
+
+**Default Credentials:**
+- Worker: `chainstorage_worker` / `worker_password`
+- Server: `chainstorage_server` / `server_password`
+
+**Manual Setup:**
+```bash
+chainstorage admin setup-postgres \
+ --blockchain ethereum \
+ --network mainnet \
+ --env local \
+ --master-user postgres \
+ --master-password postgres \
+ --worker-password worker_password \
+ --server-password server_password
+```
+
+#### Production/Development Setup
+
+In production, databases are initialized using the `db-init` command:
+
+```bash
+# Connect to admin pod
+kubectl exec -it deploy/chainstorage-admin-dev-console -c chainstorage-admin -- /bin/bash
+
+# Initialize database for ethereum-mainnet
+./admin db-init --blockchain ethereum --network mainnet --env dev
+```
+
+The `db-init` command:
+1. Reads master credentials from environment variables (injected by Kubernetes)
+2. Fetches network-specific credentials from AWS Secrets Manager (`chainstorage/db-creds/{env}`)
+3. Creates the database (e.g., `chainstorage_ethereum_mainnet`)
+4. Creates network-specific users with passwords from the secret
+5. Grants appropriate permissions
+
+#### Database Naming Convention
+
+Databases follow the pattern: `chainstorage_{blockchain}_{network}`
+
+Examples:
+- `chainstorage_ethereum_mainnet`
+- `chainstorage_bitcoin_mainnet`
+- `chainstorage_polygon_testnet`
+
+Note: Hyphens in blockchain/network names are replaced with underscores.
+
+### Local Development Setup
+
+#### Complete Local Environment
+
+Start the full local development stack:
+
+```shell
+# Start all services (PostgreSQL, Temporal, LocalStack)
+make localstack
+
+# Load environment variables
+source scripts/postgres-roles-local.env
+```
+
+#### Available Commands
+
+**Database Operations:**
+```shell
+# Set up PostgreSQL database and roles for a new network
+go run ./cmd/admin setup-postgres \
+ --blockchain ethereum \
+ --network mainnet \
+ --env local \
+ --master-user postgres \
+ --master-password postgres \
+ --host localhost \
+ --port 5433
+
+# Initialize databases from AWS Secrets Manager (production)
+go run ./cmd/admin db-init \
+ --secret-name chainstorage/db-init/prod \
+ --aws-region us-east-1
+
+# Migrate data from DynamoDB to PostgreSQL
+chainstorage admin migrate-dynamodb-to-postgres \
+ --blockchain ethereum \
+ --network mainnet \
+ --env local \
+ --start-height 1000000 \
+ --end-height 1001000
+```
+
+#### Command Reference
+
+| Command | Description | Example |
+|---------|-------------|---------|
+| `setup-postgres` | Create database and roles | `setup-postgres --master-user postgres --master-password postgres` |
+| `db-init` | Initialize from AWS Secrets Manager | `db-init --blockchain ethereum --network mainnet --env dev` |
+| `migrate-dynamodb-to-postgres` | Migrate data from DynamoDB to PostgreSQL | `migrate-dynamodb-to-postgres --start-height 1000000 --end-height 1001000` |
+
### AWS localstack
Check S3 files:
@@ -412,6 +685,73 @@ Start the streamer workflow:
go run ./cmd/admin workflow start --workflow streamer --input '{}' --blockchain ethereum --network goerli --env local
```
+Start the migrator workflow (event-driven migration from DynamoDB to PostgreSQL):
+```shell
+# Migrate events by sequence range
+go run ./cmd/admin workflow start --workflow migrator --input '{"StartEventSequence": 1, "EndEventSequence": 1000, "Tag": 2, "EventTag": 3, "BatchSize": 500, "Parallelism": 2, "CheckpointSize": 10000}' --blockchain ethereum --network mainnet --env local
+
+# Auto-resume from last migrated position
+go run ./cmd/admin workflow start --workflow migrator --input '{"StartEventSequence": 0, "EndEventSequence": 100000, "Tag": 2, "EventTag": 3, "AutoResume": true}' --blockchain ethereum --network mainnet --env local
+
+# Continuous sync mode with auto-resume
+go run ./cmd/admin workflow start --workflow migrator --input '{"StartEventSequence": 0, "EndEventSequence": 0, "Tag": 2, "EventTag": 3, "AutoResume": true, "ContinuousSync": true, "BatchSize": 500,"Parallelism": 2, "CheckpointSize": 10000}' --blockchain ethereum --network mainnet --env local
+
+# Custom batch size and parallelism for large migrations
+go run ./cmd/admin workflow start --workflow migrator --input '{"StartEventSequence": 1000000, "EndEventSequence": 2000000, "Tag": 1, "EventTag": 0, "BatchSize": 10000, "Parallelism": 16, "CheckpointSize": 100000}' --blockchain ethereum --network mainnet --env local
+```
+Note: The migrator uses an event-driven architecture where events are fetched by sequence number and blocks are extracted from BLOCK_ADDED events. This ensures data consistency and proper handling of blockchain reorganizations.
+
+Start the cross validator workflow:
+```shell
+go run ./cmd/admin workflow start --workflow cross_validator --input '{"StartHeight": 15500000, "Tag": 0}' --blockchain ethereum --network mainnet --env local
+```
+
+Start the event backfiller workflow:
+```shell
+go run ./cmd/admin workflow start --workflow event_backfiller --input '{"Tag": 0, "EventTag": 0, "StartSequence": 1000, "EndSequence": 2000}' --blockchain ethereum --network mainnet --env local
+```
+
+Start the replicator workflow:
+```shell
+go run ./cmd/admin workflow start --workflow replicator --input '{"Tag": 0, "StartHeight": 1000000, "EndHeight": 1001000}' --blockchain ethereum --network mainnet --env local
+```
+
+Inspect currently due single-block retention cohorts in an exact repaired range
+without deleting:
+```shell
+go run ./cmd/admin workflow start --workflow single_block_retention --input '{"Tag": 0, "StartHeight": 100000, "EndHeight": 110000, "MaxObjectRanges": 20, "Parallelism": 20}' --blockchain solana --network mainnet --env local
+```
+
+The workflow is manual-only while production retention is being verified; no
+recurring cron or Temporal Schedule starts it. `MaxObjectRanges` bounds each
+Temporal run, while optional `Parallelism` bounds concurrent CSCB cohort
+lifecycles from 1 to 20 and defaults to 1. Each concurrency slot schedules one
+CSCB cohort activity, which processes all covered single-block rows; Temporal
+may dispatch a bounded retry to a different worker. Selected cohorts are
+validated as unique and non-overlapping before execution on new workflow
+histories. If any post-processing failure occurs after cohort activities start,
+the terminal Temporal application error includes structured outcomes for every
+cohort launched in that run; a failed wave starts no later wave. A read-only run
+remains bounded, reports the current backlog through `MoreEligibleRanges`, and
+returns its frozen `EligibilityCutoff`. A new
+execute sweep must supply that exact dry-run cutoff. It continues as new with
+the same cutoff, selection, and approval envelope until a final empty selection
+confirms that no cohort from the frozen set remains; it never admits cohorts
+that become eligible later or bypasses each cohort's retention clock. Execution
+requires Postgres metadata plus versioned S3 storage, an explicit
+`FallbackReadsValidated` assertion with zero `FallbackErrorCount`, and a
+separate operator approval (`ApprovedChain`, `ApprovedStartHeight`,
+`ApprovedEndHeight`) that must exactly match the selection range. Every selected
+cohort must be fully contained by that immutable envelope across continuations;
+the approval is passed through unchanged and is never derived from selector
+output, so unbounded execution requests are rejected. API and SDK clients
+continue to use the same Chainstorage interface; the
+`DirectStorageClientsGuarded` execution gate applies only to consumers that
+bypass Chainstorage and access object storage directly. The process activity
+heartbeats from the row and per-version delete loops under a bounded heartbeat
+timeout, so stopping the workflow cancels destructive work between steps
+instead of letting it run to completion.
+
Stop the monitor workflow:
```shell
go run ./cmd/admin workflow stop --workflow monitor --blockchain ethereum --network mainnet --env local
@@ -427,7 +767,7 @@ Using Temporal CLI to check the status of the workflow:
brew install tctl
tctl --address localhost:7233 --namespace chainstorage-ethereum-mainnet workflow show --workflow_id workflow.backfiller
-````
+```
## Failover
@@ -556,6 +896,36 @@ and out of order, the logical ordering guarantee is preserved.
6. Update watermark once all the batches have been processed.
7. Repeat above steps.
+## Data Migration Tool
+
+Tool to migrate blockchain data from DynamoDB to PostgreSQL with complete reorg support and data integrity preservation.
+
+### Overview
+
+The migration tool performs a comprehensive transfer of blockchain data:
+- **Block metadata** from DynamoDB to PostgreSQL (`block_metadata` + `canonical_blocks` tables)
+- **Events** from DynamoDB to PostgreSQL (`block_events` table)
+- **Complete reorg data** including both canonical and non-canonical blocks
+- **Event ID-based migration** for efficient sequential processing
+
+**Critical Requirements**:
+1. Block metadata **must** be migrated before events (foreign key dependencies)
+2. Migration preserves complete blockchain history including all reorg blocks
+3. Canonical block identification is maintained through migration ordering
+
+### Basic Usage
+
+```bash
+# Migrate both blocks and events for a height range
+go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=1000000 \
+ --end-height=1001000 \
+ --tag=1 \
+ --event-tag=0
+```
## Contact Us
We have set up a Discord server soon. Here is the link to join (limited 10) https://discord.com/channels/1079683467018764328/1079683467786334220.
diff --git a/SEARCHABILITY_PROPOSAL.md b/SEARCHABILITY_PROPOSAL.md
new file mode 100644
index 00000000..237bbbe2
--- /dev/null
+++ b/SEARCHABILITY_PROPOSAL.md
@@ -0,0 +1,440 @@
+# ChainStorage Searchability Enhancement Proposal
+## Leveraging Delta Lake and Uniform Format for Advanced Blockchain Analytics
+
+### Executive Summary
+
+This proposal outlines a comprehensive enhancement to ChainStorage's architecture by introducing Delta Lake with Uniform format support to dramatically improve blockchain data searchability and analytics capabilities. By adopting a lakehouse architecture, ChainStorage can maintain its high-throughput ingestion while enabling complex analytical queries through multiple query engines.
+
+### Current Limitations
+
+ChainStorage's current architecture faces several searchability constraints:
+
+1. **No Transaction Content Search**: Only exact hash lookups supported
+2. **No Address-Based Queries**: Cannot find transactions by sender/receiver
+3. **No Token Transfer Analytics**: ERC20/721 transfers not indexed
+4. **No Event Log Search**: Smart contract events not queryable
+5. **Limited Time-Based Queries**: Only indirect via block height
+6. **No Cross-Block Analytics**: Each block queried independently
+7. **No Columnar Storage**: Inefficient for analytical workloads
+
+### Proposed Architecture
+
+```mermaid
+graph TB
+ subgraph "Data Ingestion"
+ BC[Blockchain Nodes]
+ CS[ChainStorage Workers]
+ end
+
+ subgraph "Delta Lake Layer"
+ subgraph "Bronze Tables"
+ RawBlocks[Raw Blocks
Delta Table]
+ RawTxs[Raw Transactions
Delta Table]
+ RawLogs[Raw Event Logs
Delta Table]
+ end
+
+ subgraph "Silver Tables"
+ Blocks[Enriched Blocks
Delta Table]
+ Txs[Parsed Transactions
Delta Table]
+ TokenTransfers[Token Transfers
Delta Table]
+ Events[Decoded Events
Delta Table]
+ Addresses[Address Activity
Delta Table]
+ end
+
+ subgraph "Gold Tables"
+ DailyStats[Daily Statistics
Delta Table]
+ AddressBalances[Address Balances
Delta Table]
+ TokenMetrics[Token Metrics
Delta Table]
+ end
+ end
+
+ subgraph "Query Layer"
+ subgraph "Uniform Format"
+ IcebergMeta[Iceberg Metadata]
+ HudiMeta[Hudi Metadata]
+ end
+
+ subgraph "Query Engines"
+ Spark[Apache Spark]
+ Presto[Presto/Trino]
+ Athena[AWS Athena]
+ Dremio[Dremio]
+ API[ChainStorage API]
+ end
+ end
+
+ BC --> CS
+ CS --> RawBlocks
+ CS --> RawTxs
+ CS --> RawLogs
+
+ RawBlocks --> Blocks
+ RawTxs --> Txs
+ RawLogs --> Events
+
+ Txs --> TokenTransfers
+ Txs --> Addresses
+ Events --> TokenTransfers
+
+ Blocks --> DailyStats
+ TokenTransfers --> TokenMetrics
+ Addresses --> AddressBalances
+
+ Bronze Tables --> IcebergMeta
+ Silver Tables --> IcebergMeta
+ Gold Tables --> IcebergMeta
+
+ IcebergMeta --> Spark
+ IcebergMeta --> Presto
+ IcebergMeta --> Athena
+ IcebergMeta --> Dremio
+
+ Query Engines --> API
+
+ style Bronze Tables fill:#cd7f32,stroke:#2e3440,color:#fff
+ style Silver Tables fill:#c0c0c0,stroke:#2e3440,color:#000
+ style Gold Tables fill:#ffd700,stroke:#2e3440,color:#000
+```
+
+### Implementation Details
+
+#### 1. Bronze Layer (Raw Data Ingestion)
+
+```sql
+-- Raw Blocks Table
+CREATE TABLE IF NOT EXISTS raw_blocks (
+ blockchain STRING,
+ network STRING,
+ block_height BIGINT,
+ block_hash STRING,
+ parent_hash STRING,
+ block_timestamp TIMESTAMP,
+ raw_data BINARY, -- Compressed protobuf
+ ingestion_timestamp TIMESTAMP,
+ -- Partitioning for efficient queries
+ year INT GENERATED ALWAYS AS (YEAR(block_timestamp)),
+ month INT GENERATED ALWAYS AS (MONTH(block_timestamp)),
+ day INT GENERATED ALWAYS AS (DAY(block_timestamp))
+) USING DELTA
+PARTITIONED BY (blockchain, network, year, month, day)
+TBLPROPERTIES (
+ 'delta.enableIcebergCompatV2' = 'true',
+ 'delta.universalFormat.enabledFormats' = 'iceberg',
+ 'delta.autoOptimize.optimizeWrite' = 'true',
+ 'delta.autoOptimize.autoCompact' = 'true'
+);
+
+-- Raw Transactions Table
+CREATE TABLE IF NOT EXISTS raw_transactions (
+ blockchain STRING,
+ network STRING,
+ block_height BIGINT,
+ block_hash STRING,
+ tx_hash STRING,
+ tx_index INT,
+ raw_tx BINARY, -- Compressed transaction data
+ -- Partitioning
+ year INT,
+ month INT,
+ day INT
+) USING DELTA
+PARTITIONED BY (blockchain, network, year, month, day)
+TBLPROPERTIES (
+ 'delta.enableIcebergCompatV2' = 'true',
+ 'delta.universalFormat.enabledFormats' = 'iceberg'
+);
+```
+
+#### 2. Silver Layer (Parsed and Enriched Data)
+
+```sql
+-- Parsed Transactions Table
+CREATE TABLE IF NOT EXISTS transactions (
+ blockchain STRING,
+ network STRING,
+ block_height BIGINT,
+ block_timestamp TIMESTAMP,
+ tx_hash STRING,
+ tx_index INT,
+ from_address STRING,
+ to_address STRING,
+ value DECIMAL(38, 0),
+ gas_price BIGINT,
+ gas_used BIGINT,
+ status INT,
+ input_data STRING,
+ contract_address STRING,
+ method_id STRING, -- First 4 bytes of input
+ -- Time partitioning
+ year INT,
+ month INT,
+ day INT
+) USING DELTA
+PARTITIONED BY (blockchain, network, year, month, day)
+CLUSTERED BY (from_address, to_address) INTO 100 BUCKETS
+TBLPROPERTIES (
+ 'delta.enableIcebergCompatV2' = 'true',
+ 'delta.universalFormat.enabledFormats' = 'iceberg',
+ 'delta.dataSkippingNumIndexedCols' = '8'
+);
+
+-- Token Transfers Table
+CREATE TABLE IF NOT EXISTS token_transfers (
+ blockchain STRING,
+ network STRING,
+ block_height BIGINT,
+ block_timestamp TIMESTAMP,
+ tx_hash STRING,
+ log_index INT,
+ token_address STRING,
+ token_type STRING, -- ERC20, ERC721, ERC1155
+ from_address STRING,
+ to_address STRING,
+ value DECIMAL(38, 0),
+ token_id BIGINT, -- For NFTs
+ -- Partitioning
+ year INT,
+ month INT
+) USING DELTA
+PARTITIONED BY (blockchain, network, year, month)
+CLUSTERED BY (token_address) INTO 50 BUCKETS;
+
+-- Smart Contract Events Table
+CREATE TABLE IF NOT EXISTS contract_events (
+ blockchain STRING,
+ network STRING,
+ block_height BIGINT,
+ block_timestamp TIMESTAMP,
+ tx_hash STRING,
+ log_index INT,
+ contract_address STRING,
+ event_signature STRING,
+ topic0 STRING,
+ topic1 STRING,
+ topic2 STRING,
+ topic3 STRING,
+ data STRING,
+ decoded_event MAP, -- JSON decoded parameters
+ -- Partitioning
+ year INT,
+ month INT,
+ day INT
+) USING DELTA
+PARTITIONED BY (blockchain, network, year, month, day)
+CLUSTERED BY (contract_address, event_signature) INTO 100 BUCKETS;
+```
+
+#### 3. Gold Layer (Aggregated Analytics)
+
+```sql
+-- Daily Statistics
+CREATE TABLE IF NOT EXISTS daily_statistics (
+ blockchain STRING,
+ network STRING,
+ date DATE,
+ total_blocks BIGINT,
+ total_transactions BIGINT,
+ total_value DECIMAL(38, 0),
+ unique_addresses BIGINT,
+ total_gas_used BIGINT,
+ avg_gas_price DECIMAL(18, 9),
+ active_contracts BIGINT,
+ token_transfer_count BIGINT
+) USING DELTA
+PARTITIONED BY (blockchain, network, year(date));
+
+-- Address Activity Summary
+CREATE TABLE IF NOT EXISTS address_activity (
+ blockchain STRING,
+ network STRING,
+ address STRING,
+ first_seen_block BIGINT,
+ last_seen_block BIGINT,
+ total_sent_txs BIGINT,
+ total_received_txs BIGINT,
+ total_value_sent DECIMAL(38, 0),
+ total_value_received DECIMAL(38, 0),
+ unique_interacted_addresses BIGINT,
+ last_updated TIMESTAMP
+) USING DELTA
+PARTITIONED BY (blockchain, network)
+CLUSTERED BY (address) INTO 1000 BUCKETS;
+```
+
+### Query Examples
+
+#### 1. Find All Transactions for an Address
+```sql
+SELECT * FROM transactions
+WHERE blockchain = 'ethereum'
+ AND network = 'mainnet'
+ AND (from_address = '0x123...' OR to_address = '0x123...')
+ AND block_timestamp >= '2024-01-01'
+ORDER BY block_timestamp DESC;
+```
+
+#### 2. Token Transfer Analytics
+```sql
+SELECT
+ token_address,
+ COUNT(*) as transfer_count,
+ COUNT(DISTINCT from_address) as unique_senders,
+ COUNT(DISTINCT to_address) as unique_receivers,
+ SUM(value) as total_volume
+FROM token_transfers
+WHERE blockchain = 'ethereum'
+ AND network = 'mainnet'
+ AND block_timestamp >= CURRENT_DATE - INTERVAL 7 DAYS
+GROUP BY token_address
+ORDER BY transfer_count DESC
+LIMIT 100;
+```
+
+#### 3. Smart Contract Event Search
+```sql
+SELECT * FROM contract_events
+WHERE blockchain = 'ethereum'
+ AND contract_address = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' -- USDC
+ AND event_signature = 'Transfer(address,address,uint256)'
+ AND topic1 = '0x000...' -- From address
+ AND block_timestamp >= '2024-01-01';
+```
+
+#### 4. Cross-Block Analytics
+```sql
+WITH daily_gas AS (
+ SELECT
+ DATE(block_timestamp) as date,
+ AVG(gas_price) as avg_gas_price,
+ MAX(gas_price) as max_gas_price,
+ PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY gas_price) as median_gas_price
+ FROM transactions
+ WHERE blockchain = 'ethereum'
+ AND block_timestamp >= CURRENT_DATE - INTERVAL 30 DAYS
+ GROUP BY DATE(block_timestamp)
+)
+SELECT * FROM daily_gas ORDER BY date DESC;
+```
+
+### Integration with ChainStorage
+
+#### 1. Minimal Changes to Existing Code
+
+```go
+// New writer interface for Delta tables
+type DeltaWriter interface {
+ WriteBlock(ctx context.Context, block *api.Block) error
+ WriteTransactions(ctx context.Context, txs []*api.Transaction) error
+ WriteEvents(ctx context.Context, events []*api.Event) error
+}
+
+// Implementation using Delta-RS or Spark
+type deltaWriterImpl struct {
+ sparkSession *spark.Session
+ batchSize int
+}
+
+// Add to existing workflow activities
+func (a *Activity) ExtractAndLoad(ctx context.Context, block *api.Block) error {
+ // Existing blob storage write
+ if err := a.blobStorage.Write(ctx, block); err != nil {
+ return err
+ }
+
+ // New: Write to Delta tables
+ if a.deltaWriter != nil {
+ if err := a.deltaWriter.WriteBlock(ctx, block); err != nil {
+ // Log but don't fail - Delta write is async
+ a.logger.Warn("failed to write to delta", zap.Error(err))
+ }
+ }
+
+ return nil
+}
+```
+
+#### 2. Streaming Updates
+
+```go
+// Use Delta's streaming capabilities
+func (d *deltaWriterImpl) StartStreaming(ctx context.Context) {
+ stream := d.sparkSession.
+ ReadStream().
+ Format("delta").
+ Load("raw_blocks").
+ WriteStream().
+ Trigger(spark.ProcessingTime("10 seconds")).
+ ForEachBatch(d.processBatch).
+ Start()
+}
+```
+
+### Benefits of This Approach
+
+#### 1. **Multi-Engine Query Support**
+- Spark for complex analytics
+- Presto/Trino for interactive queries
+- AWS Athena for serverless analytics
+- Existing ChainStorage API continues to work
+
+#### 2. **Improved Performance**
+- Columnar storage format (Parquet)
+- Predicate pushdown and column pruning
+- Z-order clustering for common query patterns
+- Automatic file compaction
+
+#### 3. **Advanced Analytics Capabilities**
+- Time-travel queries to any point in history
+- ACID transactions for consistent reads
+- Schema evolution support
+- Incremental processing with CDC
+
+#### 4. **Cost Optimization**
+- Data compression (10-100x reduction)
+- Efficient storage with deduplication
+- Query only required columns
+- Automatic data lifecycle management
+
+#### 5. **Operational Benefits**
+- No changes to existing ChainStorage workflows
+- Gradual migration path
+- Backward compatibility maintained
+- Single storage layer for all formats
+
+### Implementation Roadmap
+
+#### Phase 1: Foundation (Month 1-2)
+- Set up Delta Lake infrastructure
+- Implement basic Bronze layer tables
+- Create DeltaWriter interface
+- Add to Backfiller workflow
+
+#### Phase 2: Core Tables (Month 2-3)
+- Implement Silver layer parsing
+- Create transaction and event tables
+- Add clustering and optimization
+- Enable Uniform format
+
+#### Phase 3: Analytics (Month 3-4)
+- Build Gold layer aggregations
+- Implement streaming updates
+- Add query API endpoints
+- Create example dashboards
+
+#### Phase 4: Production (Month 4-5)
+- Performance optimization
+- Migration of historical data
+- Monitoring and alerting
+- Documentation and training
+
+### Conclusion
+
+By adopting Delta Lake with Uniform format, ChainStorage can evolve from a pure storage system to a comprehensive blockchain analytics platform. This approach:
+
+1. **Preserves existing functionality** while adding new capabilities
+2. **Enables multiple query engines** through Uniform format
+3. **Dramatically improves query performance** with columnar storage
+4. **Supports advanced analytics** use cases
+5. **Reduces storage costs** through compression and deduplication
+
+The lakehouse architecture provides the best of both worlds: the reliability and performance of a data warehouse with the flexibility and cost-effectiveness of a data lake.
\ No newline at end of file
diff --git a/cmd/admin/README_migrate.md b/cmd/admin/README_migrate.md
new file mode 100644
index 00000000..a2e5b454
--- /dev/null
+++ b/cmd/admin/README_migrate.md
@@ -0,0 +1,318 @@
+# Data Migration Tool
+
+Tool to migrate blockchain data from DynamoDB to PostgreSQL with complete reorg support and data integrity preservation.
+
+## Overview
+
+The migration tool performs a comprehensive transfer of blockchain data:
+- **Block metadata** from DynamoDB to PostgreSQL (`block_metadata` + `canonical_blocks` tables)
+- **Events** from DynamoDB to PostgreSQL (`block_events` table)
+- **Complete reorg data** including both canonical and non-canonical blocks
+- **Event ID-based migration** for efficient sequential processing
+
+**Critical Requirements**:
+1. Block metadata **must** be migrated before events (foreign key dependencies)
+2. Migration preserves complete blockchain history including all reorg blocks
+3. Canonical block identification is maintained through migration ordering
+
+## Architecture
+
+### Advanced Migration Strategy
+The tool uses a sophisticated **height-by-height approach** that:
+
+1. **Queries ALL blocks** at each height from DynamoDB (canonical + non-canonical)
+2. **Migrates non-canonical blocks first** to preserve reorg history
+3. **Migrates canonical block last** to ensure proper canonicality in PostgreSQL
+4. **Uses event ID ranges** for efficient event migration
+
+### Reorg Handling Design
+- **DynamoDB**: Stores both actual block hash entries and "canonical" markers
+- **PostgreSQL**: Uses separate `block_metadata` (all blocks) and `canonical_blocks` (canonical only) tables
+- **Migration**: Preserves complete reorg history while maintaining canonical block identification
+
+## Basic Usage
+
+```bash
+# Migrate both blocks and events for a height range
+go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=1000000 \
+ --end-height=1001000 \
+ --tag=1 \
+ --event-tag=0
+```
+
+## Command Line Flags
+
+### Basic Parameters
+| Flag | Required | Description | Default |
+|------|----------|-------------|---------|
+| `--start-height` | ✅ | Start block height (inclusive) | - |
+| `--end-height` | ✅ | End block height (exclusive) | - |
+| `--env` | ✅ | Environment (local/development/production) | - |
+| `--blockchain` | ✅ | Blockchain name (e.g., ethereum, base) | - |
+| `--network` | ✅ | Network name (e.g., mainnet, testnet) | - |
+| `--tag` | | Block tag for migration | 1 |
+| `--event-tag` | | Event tag for migration | 0 |
+
+### Performance & Batch Parameters
+| Flag | Required | Description | Default |
+|------|----------|-------------|---------|
+| `--batch-size` | | Number of blocks to process in each workflow batch | 100 |
+| `--mini-batch-size` | | Number of blocks to process in each activity mini-batch | batch-size/10 |
+| `--checkpoint-size` | | Number of blocks to process before creating a workflow checkpoint | 10000 |
+| `--parallelism` | | Number of parallel workers for processing mini-batches | 1 |
+| `--backoff-interval` | | Time duration to wait between batches (e.g., '1s', '500ms') | - |
+
+### Migration Mode Parameters
+| Flag | Required | Description | Default |
+|------|----------|-------------|---------|
+| `--skip-blocks` | | Skip block migration (events only) | false |
+| `--skip-events` | | Skip event migration (blocks only) | false |
+| `--continuous-sync` | | Enable continuous sync mode (workflow only, not supported in direct mode) | false |
+| `--sync-interval` | | Time duration to wait between continuous sync cycles (e.g., '1m', '30s') | 1m |
+
+## Continuous Sync Mode
+
+The migrator supports **continuous sync mode** for real-time data synchronization. This mode is designed for workflow-based migrations and enables:
+
+- **Infinite loop operation**: Automatically restarts migration when current batch completes
+- **Dynamic end height**: Sets new StartHeight to current EndHeight and resets EndHeight to 0 (sync to latest)
+- **Configurable sync intervals**: Wait duration between continuous sync cycles
+- **Automatic workflow continuation**: Uses Temporal's ContinueAsNewError for seamless restarts
+
+### Continuous Sync Process
+1. Complete current migration batch (StartHeight → EndHeight)
+2. Set new StartHeight = previous EndHeight
+3. Reset EndHeight = 0 (query latest block from source)
+4. Wait for SyncInterval duration
+5. Restart workflow with new parameters using ContinueAsNewError
+6. Repeat indefinitely
+
+### Validation Rules for Continuous Sync
+- `EndHeight` must be 0 OR greater than `StartHeight` when `ContinuousSync` is enabled
+- When `EndHeight = 0`, the tool automatically queries the latest block from DynamoDB
+- `SyncInterval` defaults to 1 minute if not specified or invalid
+
+### Continuous Sync Examples
+
+```bash
+# Basic continuous sync - syncs every minute from height 1000000 to latest
+go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=1000000 \
+ --tag=2 \
+ --event-tag=3 \
+ --continuous-sync
+
+# High-performance continuous sync with custom parameters
+go run cmd/admin/*.go migrate \
+ --env=production \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=18000000 \
+ --tag=1 \
+ --event-tag=0 \
+ --continuous-sync \
+ --sync-interval=30s \
+ --batch-size=500 \
+ --mini-batch-size=50 \
+ --parallelism=4 \
+ --checkpoint-size=5000
+```
+
+**Note**: Continuous sync is only available when using the Temporal workflow system. The direct migration command will show a warning and perform a one-time migration if `--continuous-sync` is specified.
+
+## Migration Phases
+
+### Phase 1: Height-by-Height Block Migration
+For each height in the range:
+
+1. **Query ALL blocks** at height from DynamoDB using direct table queries
+2. **Separate canonical vs non-canonical** blocks client-side
+3. **Migrate non-canonical blocks first** (preserves reorg history)
+4. **Migrate canonical block last** (ensures canonicality in PostgreSQL)
+
+```sql
+-- DynamoDB Query Pattern:
+-- All blocks: BlockPid = "{tag}-{height}"
+-- Canonical: BlockPid = "{tag}-{height}" AND BlockRid = "canonical"
+-- Non-canonical: BlockPid = "{tag}-{height}" AND BlockRid != "canonical"
+```
+
+### Phase 2: Event ID-Based Migration
+1. **Determine event ID range** from start/end heights
+2. **Migrate events sequentially** by event ID in batches
+3. **Establish foreign key relationships** to migrated block metadata
+4. **Handle missing events gracefully** (logged as debug)
+
+**CRITICAL REQUIREMENT for Events-Only Migration:**
+When using `--skip-blocks` (events-only migration), the corresponding block metadata **must already exist** in PostgreSQL. Events depend on block metadata through foreign key constraints (`block_events.block_metadata_id` → `block_metadata.id`).
+
+If block metadata is missing, the migration will fail with an error. To resolve this:
+1. First run migration with `--skip-events` to migrate block metadata
+2. Then run migration with `--skip-blocks` to migrate events
+
+```bash
+# Step 1: Migrate blocks first
+go run ./cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=1000000 \
+ --end-height=1001000 \
+ --skip-events
+
+# Step 2: Migrate events (now that block metadata exists)
+go run ./cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=1000000 \
+ --end-height=1001000 \
+ --skip-blocks
+```
+
+## PostgreSQL Schema Design
+
+### Block Storage Tables
+```sql
+-- All blocks ever observed (append-only)
+CREATE TABLE block_metadata (
+ id BIGSERIAL PRIMARY KEY,
+ height BIGINT NOT NULL,
+ tag INT NOT NULL,
+ hash VARCHAR(66),
+ parent_hash VARCHAR(66),
+ object_key_main VARCHAR(255),
+ timestamp TIMESTAMPTZ NOT NULL,
+ skipped BOOLEAN NOT NULL DEFAULT FALSE
+);
+
+-- Canonical block tracking (current "winner" at each height)
+CREATE TABLE canonical_blocks (
+ height BIGINT NOT NULL,
+ block_metadata_id BIGINT NOT NULL,
+ tag INT NOT NULL,
+ PRIMARY KEY (height, tag),
+ FOREIGN KEY (block_metadata_id) REFERENCES block_metadata (id)
+);
+```
+
+### Event Storage Table
+```sql
+-- Blockchain state change events (append-only)
+CREATE TABLE block_events (
+ event_tag INT NOT NULL DEFAULT 0,
+ event_sequence BIGINT NOT NULL,
+ event_type event_type_enum NOT NULL,
+ block_metadata_id BIGINT NOT NULL,
+ height BIGINT NOT NULL,
+ hash VARCHAR(66),
+ PRIMARY KEY (event_tag, event_sequence),
+ FOREIGN KEY (block_metadata_id) REFERENCES block_metadata (id)
+);
+```
+
+## Complete Reorg Support
+
+### DynamoDB Storage Pattern
+For height with reorgs, DynamoDB contains:
+```
+BlockPid: "1-12345", BlockRid: "0xabc123..." (non-canonical block)
+BlockPid: "1-12345", BlockRid: "0xdef456..." (another non-canonical block)
+BlockPid: "1-12345", BlockRid: "canonical" (canonical marker pointing to winner)
+```
+
+### Migration Process
+1. **Query all blocks** at height: `BlockPid = "1-12345"`
+2. **Filter canonical vs non-canonical** client-side based on `BlockRid`
+3. **Migrate non-canonical first**: All reorg blocks → `block_metadata` only
+4. **Migrate canonical last**: Winner block → `block_metadata` + `canonical_blocks`
+
+### PostgreSQL Result
+```sql
+-- block_metadata table (ALL blocks)
+id | height | hash | ...
+1 | 12345 | 0xabc123... | ... (non-canonical)
+2 | 12345 | 0xdef456... | ... (non-canonical)
+3 | 12345 | 0x789abc... | ... (canonical)
+
+-- canonical_blocks table (canonical only)
+height | block_metadata_id | tag
+12345 | 3 | 1 (points to canonical block)
+```
+
+## Schema Mapping Details
+
+### DynamoDB → PostgreSQL Block Metadata
+```
+DynamoDB BlockMetaDataDDBEntry → PostgreSQL Tables
+├── Hash → block_metadata.hash
+├── ParentHash → block_metadata.parent_hash
+├── Height → block_metadata.height
+├── Tag → block_metadata.tag
+├── ObjectKeyMain → block_metadata.object_key_main
+├── Timestamp → block_metadata.timestamp
+├── Skipped → block_metadata.skipped
+└── (canonical status) → canonical_blocks.block_metadata_id (if canonical)
+```
+
+### DynamoDB → PostgreSQL Events
+```
+DynamoDB VersionedEventDDBEntry → PostgreSQL block_events
+├── EventId → event_sequence
+├── BlockHeight → height
+├── BlockHash → hash
+├── EventTag → event_tag
+├── EventType → event_type
+├── Sequence → event_sequence
+└── (block reference) → block_metadata_id (via foreign key)
+```
+
+## Usage Examples
+
+### Complete Migration
+```bash
+# Migrate both blocks and events with full reorg support
+go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=18000000 \
+ --end-height=18001000 \
+ --tag=1 \
+ --event-tag=3
+```
+
+### Block-Only Migration
+```bash
+# Migrate only block metadata (useful for preparing for event migration)
+go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=base \
+ --network=mainnet \
+ --start-height=1000000 \
+ --end-height=1001000 \
+ --skip-events
+```
+
+### Event-Only Migration
+```bash
+# IMPORTANT: Block metadata must already exist in PostgreSQL!
+# Run this ONLY after blocks have been migrated for this height range
+
+# Migrate only events (requires blocks already migrated)
+go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=polygon \
+ --network=mainnet \
+ --start-height=50000000 \
+ --end-height=50001000 \
+ --skip-blocks \
+ --event-tag=2
+```
\ No newline at end of file
diff --git a/cmd/admin/backfill.go b/cmd/admin/backfill.go
index e202d325..7ca8e6de 100644
--- a/cmd/admin/backfill.go
+++ b/cmd/admin/backfill.go
@@ -39,10 +39,10 @@ var (
RunE: func(cmd *cobra.Command, args []string) error {
var deps struct {
fx.In
- Config *config.Config
- Client client.Client `name:"slave"`
- BlobStorage blobstorage.BlobStorage
- MetaStorage metastorage.MetaStorage
+ Config *config.Config
+ Client client.Client `name:"slave"`
+ SingleBlockUploader blobstorage.SingleBlockUploader
+ MetaStorage metastorage.MetaStorage
}
app := startApp(
@@ -92,7 +92,7 @@ var (
return xerrors.Errorf("failed to get block: %w", err)
}
- objectKey, err := deps.BlobStorage.Upload(ctx, block, compression)
+ objectKey, err := deps.SingleBlockUploader.Upload(ctx, block, compression)
if err != nil {
return xerrors.Errorf("failed to upload block in blob storage: %w", err)
}
diff --git a/cmd/admin/common.go b/cmd/admin/common.go
index 62347527..20c8009a 100644
--- a/cmd/admin/common.go
+++ b/cmd/admin/common.go
@@ -281,7 +281,7 @@ func confirm(prompt string) bool {
return true
}
- fmt.Printf(prompt)
+ fmt.Print(prompt)
response, err := bufio.NewReader(os.Stdin).ReadString('\n')
if err != nil {
logger.Error("failed to read from console", zap.Error(err))
diff --git a/cmd/admin/cscb_validate.go b/cmd/admin/cscb_validate.go
new file mode 100644
index 00000000..66a99e95
--- /dev/null
+++ b/cmd/admin/cscb_validate.go
@@ -0,0 +1,1152 @@
+package main
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "math"
+ "math/rand"
+ "net/http"
+ "os"
+ "path/filepath"
+ "regexp"
+ "sort"
+ "strings"
+ "sync/atomic"
+ "time"
+
+ "github.com/spf13/cobra"
+ "go.uber.org/fx"
+ "golang.org/x/xerrors"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/gateway"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+const (
+ defaultCSCBValidationTimeout = 2 * time.Minute
+ defaultCSCBRandomSeed = int64(914)
+)
+
+var (
+ cscbURLRegexp = regexp.MustCompile(`https?://[^\s,})]+`)
+
+ cscbFlags struct {
+ startHeight uint64
+ endHeight uint64
+ tag uint32
+ chunkBlocks uint64
+ randomSamples int
+ randomSeed int64
+ iterations int
+ fullObjectIterations int
+ timeout time.Duration
+ fallbackHeight uint64
+ disableFallbackCase bool
+ serverAddress string
+ grpc bool
+ clientID string
+ reportJSON string
+ reportMarkdown string
+ }
+
+ cscbCmd = &cobra.Command{
+ Use: "cscb",
+ Short: "CSCB validation utilities",
+ }
+
+ cscbValidateCmd = &cobra.Command{
+ Use: "validate",
+ Short: "Compare single-block and consolidated CSCB reads and write a validation report.",
+ RunE: runCSCBValidate,
+ }
+)
+
+type (
+ cscbValidationCase struct {
+ Name string `json:"name"`
+ Kind string `json:"kind"`
+ StartHeight uint64 `json:"start_height"`
+ EndHeight uint64 `json:"end_height"`
+ Heights []uint64 `json:"heights,omitempty"`
+ ExpectFallback bool `json:"expect_fallback"`
+ Iterations int `json:"iterations"`
+ }
+
+ cscbValidationReport struct {
+ GeneratedAt string `json:"generated_at"`
+ Command string `json:"command"`
+ Environment string `json:"environment"`
+ Blockchain string `json:"blockchain"`
+ Network string `json:"network"`
+ Tag uint32 `json:"tag"`
+ StartHeight uint64 `json:"start_height"`
+ EndHeight uint64 `json:"end_height"`
+ ChunkBlocks uint64 `json:"chunk_blocks"`
+ RandomSeed int64 `json:"random_seed"`
+ RandomSamples int `json:"random_samples"`
+ Iterations int `json:"iterations"`
+ FullObjectIterations int `json:"full_object_iterations"`
+ FallbackHeight uint64 `json:"fallback_height,omitempty"`
+ ServerAddressOverride string `json:"server_address_override,omitempty"`
+ GRPCTransportOverride bool `json:"grpc_transport_override"`
+ ClientID string `json:"client_id,omitempty"`
+ Passed bool `json:"passed"`
+ TotalCases int `json:"total_cases"`
+ PassedCases int `json:"passed_cases"`
+ FailedCases int `json:"failed_cases"`
+ CorrectnessMismatches int `json:"correctness_mismatches"`
+ ObservedFallbackBlocks int `json:"observed_fallback_blocks"`
+ Cases []cscbValidationCaseResult `json:"cases"`
+ }
+
+ cscbValidationCaseResult struct {
+ Name string `json:"name"`
+ Kind string `json:"kind"`
+ StartHeight uint64 `json:"start_height"`
+ EndHeight uint64 `json:"end_height"`
+ Heights []uint64 `json:"heights,omitempty"`
+ Iterations int `json:"iterations"`
+ ExpectFallback bool `json:"expect_fallback"`
+ ObservedFallback bool `json:"observed_fallback"`
+ ObservedFallbackBlocks int `json:"observed_fallback_blocks"`
+ Correct bool `json:"correct"`
+ Passed bool `json:"passed"`
+ Error string `json:"error,omitempty"`
+ MismatchHeights []uint64 `json:"mismatch_heights,omitempty"`
+ SingleBlock cscbSourceSummary `json:"single_block"`
+ Consolidated cscbSourceSummary `json:"consolidated"`
+ LatencyRatioConsolidated float64 `json:"latency_ratio_consolidated_vs_single_block,omitempty"`
+ BytesRatioConsolidated float64 `json:"bytes_ratio_consolidated_vs_single_block,omitempty"`
+ CanonicalHashesCompared int `json:"canonical_hashes_compared"`
+ FirstCanonicalHashSingleBlock string `json:"first_canonical_hash_single_block,omitempty"`
+ FirstCanonicalHashCSCB string `json:"first_canonical_hash_consolidated,omitempty"`
+ }
+
+ cscbSourceSummary struct {
+ Latency cscbLatencySummary `json:"latency"`
+ HTTP cscbHTTPSummary `json:"http"`
+ Files cscbFileSummary `json:"files"`
+ ErrorCount int `json:"error_count"`
+ }
+
+ cscbLatencySummary struct {
+ Samples int `json:"samples"`
+ P50Ms float64 `json:"p50_ms"`
+ P95Ms float64 `json:"p95_ms"`
+ MinMs float64 `json:"min_ms"`
+ MaxMs float64 `json:"max_ms"`
+ }
+
+ cscbHTTPSummary struct {
+ Requests int64 `json:"requests"`
+ Bytes int64 `json:"bytes"`
+ }
+
+ cscbFileSummary struct {
+ Count int `json:"count"`
+ SingleBlockObjectCount int `json:"single_block_object_count"`
+ ConsolidatedObjectCount int `json:"consolidated_object_count"`
+ SkippedCount int `json:"skipped_count"`
+ MetadataByteLength uint64 `json:"metadata_byte_length"`
+ MetadataUncompressedBytes uint64 `json:"metadata_uncompressed_bytes"`
+ Formats []string `json:"formats"`
+ FirstHeight uint64 `json:"first_height,omitempty"`
+ LastHeight uint64 `json:"last_height,omitempty"`
+ }
+
+ cscbFetchResult struct {
+ files []*api.BlockFile
+ digests []cscbBlockDigest
+ duration time.Duration
+ http cscbHTTPSnapshot
+ err error
+ }
+
+ cscbBlockDigest struct {
+ height uint64
+ hash string
+ }
+
+ cscbHTTPSnapshot struct {
+ requests int64
+ bytes int64
+ }
+
+ cscbCountingHTTPClient struct {
+ base downloader.HTTPClient
+ requests atomic.Int64
+ bytes atomic.Int64
+ }
+
+ cscbCountingReadCloser struct {
+ body io.ReadCloser
+ client *cscbCountingHTTPClient
+ }
+
+ cscbValidator struct {
+ client gateway.Client
+ downloader downloader.BlockDownloader
+ counter *cscbCountingHTTPClient
+ tag uint32
+ timeout time.Duration
+ }
+)
+
+func init() {
+ cscbValidateCmd.Flags().Uint64Var(&cscbFlags.startHeight, "start-height", 0, "inclusive start height for the CSCB canary object")
+ cscbValidateCmd.Flags().Uint64Var(&cscbFlags.endHeight, "end-height", 0, "exclusive end height for the CSCB canary object")
+ cscbValidateCmd.Flags().Uint32Var(&cscbFlags.tag, "tag", 0, "block tag; 0 uses chainstorage effective stable tag")
+ cscbValidateCmd.Flags().Uint64Var(&cscbFlags.chunkBlocks, "chunk-blocks", 25, "expected CSCB compression chunk size in blocks")
+ cscbValidateCmd.Flags().IntVar(&cscbFlags.randomSamples, "random-samples", 5, "number of deterministic random single-block samples")
+ cscbValidateCmd.Flags().Int64Var(&cscbFlags.randomSeed, "random-seed", defaultCSCBRandomSeed, "random seed for single-block sample selection")
+ cscbValidateCmd.Flags().IntVar(&cscbFlags.iterations, "iterations", 3, "iterations for non-full-object cases")
+ cscbValidateCmd.Flags().IntVar(&cscbFlags.fullObjectIterations, "full-object-iterations", 1, "iterations for the full-object range case")
+ cscbValidateCmd.Flags().DurationVar(&cscbFlags.timeout, "timeout", defaultCSCBValidationTimeout, "timeout for each source read")
+ cscbValidateCmd.Flags().Uint64Var(&cscbFlags.fallbackHeight, "fallback-height", 0, "height expected to miss consolidated metadata; defaults to start-height - 1 when possible")
+ cscbValidateCmd.Flags().BoolVar(&cscbFlags.disableFallbackCase, "disable-fallback-case", false, "skip the consolidated miss/fallback case")
+ cscbValidateCmd.Flags().StringVar(&cscbFlags.serverAddress, "server-address", "", "override Chainstorage SDK server address for this run")
+ cscbValidateCmd.Flags().BoolVar(&cscbFlags.grpc, "grpc", false, "force gRPC SDK transport; useful with a verified localhost port-forward")
+ cscbValidateCmd.Flags().StringVar(&cscbFlags.clientID, "client-id", "INF-914-cscb-dual-read-validation", "client ID header for server-side metrics/logs")
+ cscbValidateCmd.Flags().StringVar(&cscbFlags.reportJSON, "report-json", "", "write JSON report to this path")
+ cscbValidateCmd.Flags().StringVar(&cscbFlags.reportMarkdown, "report-md", "", "write Markdown report to this path")
+
+ if err := cscbValidateCmd.MarkFlagRequired("start-height"); err != nil {
+ panic(err)
+ }
+ if err := cscbValidateCmd.MarkFlagRequired("end-height"); err != nil {
+ panic(err)
+ }
+
+ cscbCmd.AddCommand(cscbValidateCmd)
+ rootCmd.AddCommand(cscbCmd)
+}
+
+func runCSCBValidate(cmd *cobra.Command, args []string) error {
+ if cscbFlags.startHeight >= cscbFlags.endHeight {
+ return xerrors.New("start-height must be less than end-height")
+ }
+ if cscbFlags.chunkBlocks == 0 {
+ return xerrors.New("chunk-blocks must be positive")
+ }
+ if cscbFlags.randomSamples < 0 {
+ return xerrors.New("random-samples cannot be negative")
+ }
+ if cscbFlags.iterations <= 0 {
+ return xerrors.New("iterations must be positive")
+ }
+ if cscbFlags.fullObjectIterations <= 0 {
+ return xerrors.New("full-object-iterations must be positive")
+ }
+ if cscbFlags.timeout <= 0 {
+ return xerrors.New("timeout must be positive")
+ }
+ if cscbFlags.serverAddress != "" {
+ if err := os.Setenv("CHAINSTORAGE_SDK_CHAINSTORAGE_ADDRESS", cscbFlags.serverAddress); err != nil {
+ return xerrors.Errorf("failed to set server address override: %w", err)
+ }
+ }
+ if cscbFlags.grpc {
+ if err := os.Setenv("CHAINSTORAGE_SDK_RESTFUL", "false"); err != nil {
+ return xerrors.Errorf("failed to set gRPC transport override: %w", err)
+ }
+ }
+
+ counter := newCSCBCountingHTTPClient(downloader.NewHTTPClient())
+ var deps struct {
+ fx.In
+ Client gateway.Client
+ Downloader downloader.BlockDownloader
+ }
+ app := startApp(
+ fx.Provide(func() downloader.HTTPClient { return counter }),
+ fx.Provide(downloader.NewBlockDownloader),
+ gateway.WithClientID(cscbFlags.clientID),
+ fx.Populate(&deps),
+ )
+ defer app.Close()
+
+ tag := app.Config().GetEffectiveBlockTag(cscbFlags.tag)
+ validator := &cscbValidator{
+ client: deps.Client,
+ downloader: deps.Downloader,
+ counter: counter,
+ tag: tag,
+ timeout: cscbFlags.timeout,
+ }
+ report, validationErr := validator.run(cmd.Context(), cscbValidationRunConfig{
+ startHeight: cscbFlags.startHeight,
+ endHeight: cscbFlags.endHeight,
+ chunkBlocks: cscbFlags.chunkBlocks,
+ randomSamples: cscbFlags.randomSamples,
+ randomSeed: cscbFlags.randomSeed,
+ iterations: cscbFlags.iterations,
+ fullObjectIterations: cscbFlags.fullObjectIterations,
+ fallbackHeight: cscbFlags.fallbackHeight,
+ disableFallbackCase: cscbFlags.disableFallbackCase,
+ serverAddress: cscbFlags.serverAddress,
+ grpc: cscbFlags.grpc,
+ clientID: cscbFlags.clientID,
+ })
+ report.Command = strings.Join(os.Args, " ")
+ report.Environment = string(env)
+ report.Blockchain = blockchain.String()
+ report.Network = network.String()
+
+ if cscbFlags.reportJSON != "" {
+ if err := writeCSCBJSONReport(cscbFlags.reportJSON, report); err != nil {
+ return err
+ }
+ }
+ if cscbFlags.reportMarkdown != "" {
+ if err := writeCSCBMarkdownReport(cscbFlags.reportMarkdown, report); err != nil {
+ return err
+ }
+ }
+ if validationErr != nil {
+ return validationErr
+ }
+ if !report.Passed {
+ return xerrors.Errorf("CSCB validation failed: %d failed cases, %d mismatch heights", report.FailedCases, report.CorrectnessMismatches)
+ }
+ return nil
+}
+
+type cscbValidationRunConfig struct {
+ startHeight uint64
+ endHeight uint64
+ chunkBlocks uint64
+ randomSamples int
+ randomSeed int64
+ iterations int
+ fullObjectIterations int
+ fallbackHeight uint64
+ disableFallbackCase bool
+ serverAddress string
+ grpc bool
+ clientID string
+}
+
+func (v *cscbValidator) run(ctx context.Context, cfg cscbValidationRunConfig) (*cscbValidationReport, error) {
+ fallbackHeight := cfg.fallbackHeight
+ if fallbackHeight == 0 && cfg.startHeight > 0 {
+ fallbackHeight = cfg.startHeight - 1
+ }
+ cases := buildCSCBValidationCases(cfg, fallbackHeight)
+
+ report := &cscbValidationReport{
+ GeneratedAt: time.Now().UTC().Format(time.RFC3339),
+ Tag: v.tag,
+ StartHeight: cfg.startHeight,
+ EndHeight: cfg.endHeight,
+ ChunkBlocks: cfg.chunkBlocks,
+ RandomSeed: cfg.randomSeed,
+ RandomSamples: cfg.randomSamples,
+ Iterations: cfg.iterations,
+ FullObjectIterations: cfg.fullObjectIterations,
+ FallbackHeight: fallbackHeight,
+ ServerAddressOverride: cfg.serverAddress,
+ GRPCTransportOverride: cfg.grpc,
+ ClientID: cfg.clientID,
+ TotalCases: len(cases),
+ }
+
+ for _, validationCase := range cases {
+ result := v.runCase(ctx, validationCase)
+ report.Cases = append(report.Cases, result)
+ if result.Passed {
+ report.PassedCases++
+ } else {
+ report.FailedCases++
+ }
+ report.CorrectnessMismatches += len(result.MismatchHeights)
+ report.ObservedFallbackBlocks += result.ObservedFallbackBlocks
+ }
+ report.Passed = report.FailedCases == 0
+ return report, nil
+}
+
+func buildCSCBValidationCases(cfg cscbValidationRunConfig, fallbackHeight uint64) []cscbValidationCase {
+ cases := []cscbValidationCase{
+ {
+ Name: "single_first_block",
+ Kind: "single",
+ StartHeight: cfg.startHeight,
+ EndHeight: cfg.startHeight + 1,
+ Heights: []uint64{cfg.startHeight},
+ Iterations: cfg.iterations,
+ },
+ {
+ Name: "single_last_block",
+ Kind: "single",
+ StartHeight: cfg.endHeight - 1,
+ EndHeight: cfg.endHeight,
+ Heights: []uint64{cfg.endHeight - 1},
+ Iterations: cfg.iterations,
+ },
+ }
+
+ for _, height := range chunkBoundaryHeights(cfg.startHeight, cfg.endHeight, cfg.chunkBlocks) {
+ cases = append(cases, cscbValidationCase{
+ Name: fmt.Sprintf("single_chunk_boundary_%d", height),
+ Kind: "single",
+ StartHeight: height,
+ EndHeight: height + 1,
+ Heights: []uint64{height},
+ Iterations: cfg.iterations,
+ })
+ }
+
+ for _, height := range randomSampleHeights(cfg.startHeight, cfg.endHeight, cfg.randomSamples, cfg.randomSeed) {
+ cases = append(cases, cscbValidationCase{
+ Name: fmt.Sprintf("single_random_%d", height),
+ Kind: "single",
+ StartHeight: height,
+ EndHeight: height + 1,
+ Heights: []uint64{height},
+ Iterations: cfg.iterations,
+ })
+ }
+
+ withinStart := boundedHeight(cfg.startHeight+5, cfg.startHeight, cfg.endHeight-1)
+ withinEnd := boundedHeight(withinStart+10, withinStart+1, cfg.endHeight)
+ cases = append(cases, cscbValidationCase{
+ Name: "range_within_one_chunk",
+ Kind: "range",
+ StartHeight: withinStart,
+ EndHeight: withinEnd,
+ Iterations: cfg.iterations,
+ })
+
+ boundary := cfg.startHeight + cfg.chunkBlocks
+ if boundary <= cfg.startHeight || boundary >= cfg.endHeight {
+ boundary = cfg.startHeight + (cfg.endHeight-cfg.startHeight)/2
+ }
+ acrossStart := boundedHeight(boundary-5, cfg.startHeight, cfg.endHeight-1)
+ acrossEnd := boundedHeight(boundary+5, acrossStart+1, cfg.endHeight)
+ cases = append(cases, cscbValidationCase{
+ Name: "range_across_chunk_boundary",
+ Kind: "range",
+ StartHeight: acrossStart,
+ EndHeight: acrossEnd,
+ Iterations: cfg.iterations,
+ })
+
+ twoChunkEnd := boundedHeight(cfg.startHeight+2*cfg.chunkBlocks, cfg.startHeight+1, cfg.endHeight)
+ cases = append(cases, cscbValidationCase{
+ Name: "range_two_chunks",
+ Kind: "range",
+ StartHeight: cfg.startHeight,
+ EndHeight: twoChunkEnd,
+ Iterations: cfg.iterations,
+ })
+
+ cases = append(cases, cscbValidationCase{
+ Name: "range_full_object",
+ Kind: "range",
+ StartHeight: cfg.startHeight,
+ EndHeight: cfg.endHeight,
+ Iterations: cfg.fullObjectIterations,
+ })
+
+ if !cfg.disableFallbackCase && fallbackHeight > 0 {
+ cases = append(cases, cscbValidationCase{
+ Name: "single_consolidated_miss_fallback",
+ Kind: "single",
+ StartHeight: fallbackHeight,
+ EndHeight: fallbackHeight + 1,
+ Heights: []uint64{fallbackHeight},
+ ExpectFallback: true,
+ Iterations: cfg.iterations,
+ })
+ }
+
+ return cases
+}
+
+func chunkBoundaryHeights(startHeight uint64, endHeight uint64, chunkBlocks uint64) []uint64 {
+ if chunkBlocks == 0 || endHeight <= startHeight+1 {
+ return nil
+ }
+ candidates := make([]uint64, 0, 6)
+ for boundary := startHeight + chunkBlocks; boundary < endHeight && len(candidates) < 6; boundary += chunkBlocks {
+ if boundary > startHeight {
+ candidates = append(candidates, boundary-1)
+ }
+ candidates = append(candidates, boundary)
+ }
+ return uniqueSortedHeights(candidates, startHeight, endHeight)
+}
+
+func randomSampleHeights(startHeight uint64, endHeight uint64, samples int, seed int64) []uint64 {
+ if samples <= 0 || endHeight <= startHeight {
+ return nil
+ }
+ span := endHeight - startHeight
+ if uint64(samples) >= span {
+ heights := make([]uint64, 0, span)
+ for height := startHeight; height < endHeight; height++ {
+ heights = append(heights, height)
+ }
+ return heights
+ }
+
+ rng := rand.New(rand.NewSource(seed))
+ seen := make(map[uint64]struct{}, samples)
+ for len(seen) < samples {
+ height := startHeight + uint64(rng.Int63n(int64(span)))
+ seen[height] = struct{}{}
+ }
+
+ heights := make([]uint64, 0, len(seen))
+ for height := range seen {
+ heights = append(heights, height)
+ }
+ sort.Slice(heights, func(i int, j int) bool { return heights[i] < heights[j] })
+ return heights
+}
+
+func uniqueSortedHeights(heights []uint64, startHeight uint64, endHeight uint64) []uint64 {
+ seen := make(map[uint64]struct{}, len(heights))
+ for _, height := range heights {
+ if height < startHeight || height >= endHeight {
+ continue
+ }
+ seen[height] = struct{}{}
+ }
+ out := make([]uint64, 0, len(seen))
+ for height := range seen {
+ out = append(out, height)
+ }
+ sort.Slice(out, func(i int, j int) bool { return out[i] < out[j] })
+ return out
+}
+
+func boundedHeight(value uint64, min uint64, max uint64) uint64 {
+ if value < min {
+ return min
+ }
+ if value > max {
+ return max
+ }
+ return value
+}
+
+func (v *cscbValidator) runCase(ctx context.Context, validationCase cscbValidationCase) cscbValidationCaseResult {
+ result := cscbValidationCaseResult{
+ Name: validationCase.Name,
+ Kind: validationCase.Kind,
+ StartHeight: validationCase.StartHeight,
+ EndHeight: validationCase.EndHeight,
+ Heights: validationCase.Heights,
+ Iterations: validationCase.Iterations,
+ ExpectFallback: validationCase.ExpectFallback,
+ Correct: true,
+ Passed: true,
+ }
+
+ var singleBlockDurations []time.Duration
+ var consolidatedDurations []time.Duration
+ var singleBlockRequests int64
+ var singleBlockBytes int64
+ var consolidatedRequests int64
+ var consolidatedBytes int64
+
+ for i := 0; i < validationCase.Iterations; i++ {
+ singleBlock := v.fetch(ctx, validationCase, api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK)
+ consolidated := v.fetch(ctx, validationCase, api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED)
+
+ if singleBlock.err != nil {
+ result.SingleBlock.ErrorCount++
+ result.Error = appendResultError(result.Error, "single-block", singleBlock.err)
+ } else {
+ singleBlockDurations = append(singleBlockDurations, singleBlock.duration)
+ singleBlockRequests += singleBlock.http.requests
+ singleBlockBytes += singleBlock.http.bytes
+ if result.SingleBlock.Files.Count == 0 {
+ result.SingleBlock.Files = summarizeCSCBFiles(singleBlock.files)
+ }
+ }
+
+ if consolidated.err != nil {
+ result.Consolidated.ErrorCount++
+ result.Error = appendResultError(result.Error, "consolidated", consolidated.err)
+ } else {
+ consolidatedDurations = append(consolidatedDurations, consolidated.duration)
+ consolidatedRequests += consolidated.http.requests
+ consolidatedBytes += consolidated.http.bytes
+ if result.Consolidated.Files.Count == 0 {
+ result.Consolidated.Files = summarizeCSCBFiles(consolidated.files)
+ }
+ }
+
+ if singleBlock.err != nil || consolidated.err != nil {
+ result.Correct = false
+ result.Passed = false
+ continue
+ }
+
+ comparison := compareCSCBDigests(singleBlock.digests, consolidated.digests)
+ if !comparison.correct {
+ result.Correct = false
+ result.Passed = false
+ result.MismatchHeights = appendUnique(result.MismatchHeights, comparison.mismatchHeights...)
+ }
+ if result.CanonicalHashesCompared == 0 && comparison.hashesCompared > 0 {
+ result.CanonicalHashesCompared = comparison.hashesCompared
+ result.FirstCanonicalHashSingleBlock = comparison.firstSingleBlockHash
+ result.FirstCanonicalHashCSCB = comparison.firstConsolidatedHash
+ }
+ }
+
+ result.SingleBlock.Latency = summarizeDurations(singleBlockDurations)
+ result.Consolidated.Latency = summarizeDurations(consolidatedDurations)
+ result.SingleBlock.HTTP = cscbHTTPSummary{Requests: singleBlockRequests, Bytes: singleBlockBytes}
+ result.Consolidated.HTTP = cscbHTTPSummary{Requests: consolidatedRequests, Bytes: consolidatedBytes}
+ result.ObservedFallbackBlocks = result.Consolidated.Files.SingleBlockObjectCount
+ result.ObservedFallback = result.ObservedFallbackBlocks > 0
+ if validationCase.ExpectFallback && !result.ObservedFallback {
+ result.Passed = false
+ result.Error = appendResultErrorString(result.Error, "expected consolidated read to fall back to single-block metadata")
+ }
+ if !validationCase.ExpectFallback && result.ObservedFallback {
+ result.Passed = false
+ result.Error = appendResultErrorString(result.Error, "unexpected consolidated fallback to single-block metadata")
+ }
+ result.LatencyRatioConsolidated = ratio(result.Consolidated.Latency.P50Ms, result.SingleBlock.Latency.P50Ms)
+ result.BytesRatioConsolidated = ratio(float64(result.Consolidated.HTTP.Bytes), float64(result.SingleBlock.HTTP.Bytes))
+ return result
+}
+
+func (v *cscbValidator) fetch(ctx context.Context, validationCase cscbValidationCase, readSource api.BlockReadSource) cscbFetchResult {
+ ctx, cancel := context.WithTimeout(ctx, v.timeout)
+ defer cancel()
+
+ v.counter.Reset()
+ start := time.Now()
+ files, err := v.getFiles(ctx, validationCase, readSource)
+ if err != nil {
+ return cscbFetchResult{duration: time.Since(start), http: v.counter.Snapshot(), err: err}
+ }
+ digests, err := v.hashFiles(ctx, files)
+ return cscbFetchResult{
+ files: files,
+ digests: digests,
+ duration: time.Since(start),
+ http: v.counter.Snapshot(),
+ err: err,
+ }
+}
+
+func (v *cscbValidator) getFiles(ctx context.Context, validationCase cscbValidationCase, readSource api.BlockReadSource) ([]*api.BlockFile, error) {
+ if validationCase.Kind == "single" {
+ resp, err := v.client.GetBlockFile(ctx, &api.GetBlockFileRequest{
+ Tag: v.tag,
+ Height: validationCase.StartHeight,
+ ReadSource: readSource,
+ })
+ if err != nil {
+ return nil, xerrors.Errorf("GetBlockFile failed: %w", err)
+ }
+ if resp.GetFile() == nil {
+ return nil, xerrors.New("GetBlockFile returned nil file")
+ }
+ return []*api.BlockFile{resp.GetFile()}, nil
+ }
+
+ resp, err := v.client.GetBlockFilesByRange(ctx, &api.GetBlockFilesByRangeRequest{
+ Tag: v.tag,
+ StartHeight: validationCase.StartHeight,
+ EndHeight: validationCase.EndHeight,
+ ReadSource: readSource,
+ })
+ if err != nil {
+ return nil, xerrors.Errorf("GetBlockFilesByRange failed: %w", err)
+ }
+ if len(resp.GetFiles()) == 0 {
+ return nil, xerrors.New("GetBlockFilesByRange returned no files")
+ }
+ return resp.GetFiles(), nil
+}
+
+func (v *cscbValidator) hashFiles(ctx context.Context, files []*api.BlockFile) (_ []cscbBlockDigest, retErr error) {
+ iter, err := v.downloader.OpenRawBlockPayloads(ctx, files)
+ if err != nil {
+ return nil, xerrors.Errorf("OpenRawBlockPayloads failed: %w", err)
+ }
+ defer func() {
+ if closeErr := iter.Close(); retErr == nil && closeErr != nil {
+ retErr = xerrors.Errorf("close raw block payload iterator failed: %w", closeErr)
+ }
+ }()
+
+ digests := make([]cscbBlockDigest, 0, len(files))
+ for {
+ payload, err := iter.Next(ctx)
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, xerrors.Errorf("read raw block payload failed: %w", err)
+ }
+
+ digest, digestErr := canonicalRawPayloadHash(payload)
+ var closeErr error
+ if payload != nil {
+ closeErr = payload.Close()
+ }
+ if digestErr != nil {
+ return nil, digestErr
+ }
+ if closeErr != nil {
+ return nil, xerrors.Errorf("close raw block payload failed: %w", closeErr)
+ }
+ digests = append(digests, digest)
+ }
+ return digests, nil
+}
+
+type cscbBlockComparison struct {
+ correct bool
+ mismatchHeights []uint64
+ hashesCompared int
+ firstSingleBlockHash string
+ firstConsolidatedHash string
+}
+
+func compareCSCBBlocks(singleBlock []*api.Block, consolidated []*api.Block) cscbBlockComparison {
+ comparison := cscbBlockComparison{correct: true}
+ if len(singleBlock) != len(consolidated) {
+ comparison.correct = false
+ comparison.mismatchHeights = append(comparison.mismatchHeights, collectBlockHeights(singleBlock)...)
+ comparison.mismatchHeights = append(comparison.mismatchHeights, collectBlockHeights(consolidated)...)
+ comparison.mismatchHeights = uniqueSortedHeights(comparison.mismatchHeights, 0, ^uint64(0))
+ return comparison
+ }
+ for i := range singleBlock {
+ singleBlockHash, singleBlockErr := canonicalBlockHash(singleBlock[i])
+ consolidatedHash, consolidatedErr := canonicalBlockHash(consolidated[i])
+ height := firstNonZeroHeight(singleBlock[i], consolidated[i])
+ if comparison.hashesCompared == 0 {
+ comparison.firstSingleBlockHash = singleBlockHash
+ comparison.firstConsolidatedHash = consolidatedHash
+ }
+ comparison.hashesCompared++
+ if singleBlockErr != nil || consolidatedErr != nil || singleBlockHash != consolidatedHash {
+ comparison.correct = false
+ comparison.mismatchHeights = append(comparison.mismatchHeights, height)
+ }
+ }
+ comparison.mismatchHeights = uniqueSortedHeights(comparison.mismatchHeights, 0, ^uint64(0))
+ return comparison
+}
+
+func compareCSCBDigests(singleBlock []cscbBlockDigest, consolidated []cscbBlockDigest) cscbBlockComparison {
+ comparison := cscbBlockComparison{correct: true}
+ if len(singleBlock) != len(consolidated) {
+ comparison.correct = false
+ comparison.mismatchHeights = append(comparison.mismatchHeights, collectDigestHeights(singleBlock)...)
+ comparison.mismatchHeights = append(comparison.mismatchHeights, collectDigestHeights(consolidated)...)
+ comparison.mismatchHeights = uniqueSortedHeights(comparison.mismatchHeights, 0, ^uint64(0))
+ return comparison
+ }
+
+ for i := range singleBlock {
+ if comparison.hashesCompared == 0 {
+ comparison.firstSingleBlockHash = singleBlock[i].hash
+ comparison.firstConsolidatedHash = consolidated[i].hash
+ }
+ comparison.hashesCompared++
+
+ if singleBlock[i].height != consolidated[i].height || singleBlock[i].hash != consolidated[i].hash {
+ comparison.correct = false
+ comparison.mismatchHeights = append(comparison.mismatchHeights, singleBlock[i].height, consolidated[i].height)
+ }
+ }
+ comparison.mismatchHeights = uniqueSortedHeights(comparison.mismatchHeights, 0, ^uint64(0))
+ return comparison
+}
+
+func collectDigestHeights(digests []cscbBlockDigest) []uint64 {
+ heights := make([]uint64, 0, len(digests))
+ for _, digest := range digests {
+ heights = append(heights, digest.height)
+ }
+ return heights
+}
+
+func collectBlockHeights(blocks []*api.Block) []uint64 {
+ heights := make([]uint64, 0, len(blocks))
+ for _, block := range blocks {
+ if height := firstNonZeroHeight(block); height > 0 {
+ heights = append(heights, height)
+ }
+ }
+ return heights
+}
+
+func firstNonZeroHeight(blocks ...*api.Block) uint64 {
+ for _, block := range blocks {
+ if block == nil || block.GetMetadata() == nil {
+ continue
+ }
+ if height := block.GetMetadata().GetHeight(); height > 0 {
+ return height
+ }
+ }
+ return 0
+}
+
+func canonicalBlockHash(block *api.Block) (string, error) {
+ if block == nil {
+ return "", xerrors.New("nil block")
+ }
+ if block.Metadata != nil {
+ originalObjectKeyMain := block.Metadata.ObjectKeyMain
+ originalObjectFormat := block.Metadata.ObjectFormat
+ originalByteOffset := block.Metadata.ByteOffset
+ originalByteLength := block.Metadata.ByteLength
+ originalUncompressedLength := block.Metadata.UncompressedLength
+
+ block.Metadata.ObjectKeyMain = ""
+ block.Metadata.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK
+ block.Metadata.ByteOffset = 0
+ block.Metadata.ByteLength = 0
+ block.Metadata.UncompressedLength = 0
+
+ defer func() {
+ block.Metadata.ObjectKeyMain = originalObjectKeyMain
+ block.Metadata.ObjectFormat = originalObjectFormat
+ block.Metadata.ByteOffset = originalByteOffset
+ block.Metadata.ByteLength = originalByteLength
+ block.Metadata.UncompressedLength = originalUncompressedLength
+ }()
+ }
+ payload, err := proto.MarshalOptions{Deterministic: true}.Marshal(block)
+ if err != nil {
+ return "", err
+ }
+ sum := sha256.Sum256(payload)
+ return hex.EncodeToString(sum[:]), nil
+}
+
+func canonicalRawPayloadHash(payload *downloader.RawBlockPayload) (cscbBlockDigest, error) {
+ if payload == nil || payload.BlockFile == nil {
+ return cscbBlockDigest{}, xerrors.New("nil raw block payload")
+ }
+ data, err := io.ReadAll(payload)
+ if err != nil {
+ return cscbBlockDigest{}, xerrors.Errorf("read raw block payload for height %d failed: %w", payload.BlockFile.GetHeight(), err)
+ }
+ hash, err := canonicalRawBlockDataHash(payload.BlockFile, data)
+ if err != nil {
+ return cscbBlockDigest{}, err
+ }
+ return cscbBlockDigest{height: payload.BlockFile.GetHeight(), hash: hash}, nil
+}
+
+func canonicalRawBlockDataHash(blockFile *api.BlockFile, data []byte) (string, error) {
+ if blockFile == nil {
+ return "", xerrors.New("nil block file")
+ }
+ if blockFile.GetSkipped() {
+ return canonicalBlockHash(&api.Block{
+ Metadata: &api.BlockMetadata{
+ Tag: blockFile.GetTag(),
+ Height: blockFile.GetHeight(),
+ Skipped: true,
+ },
+ })
+ }
+
+ var block api.Block
+ if err := proto.Unmarshal(data, &block); err != nil {
+ return "", xerrors.Errorf("unmarshal raw block payload for height %d failed: %w", blockFile.GetHeight(), err)
+ }
+ if isCSCBFile(blockFile) {
+ block.Metadata = cscbMetadataFromBlockFile(blockFile)
+ }
+ return canonicalBlockHash(&block)
+}
+
+func cscbMetadataFromBlockFile(blockFile *api.BlockFile) *api.BlockMetadata {
+ return &api.BlockMetadata{
+ Tag: blockFile.GetTag(),
+ Hash: blockFile.GetHash(),
+ ParentHash: blockFile.GetParentHash(),
+ Height: blockFile.GetHeight(),
+ ParentHeight: blockFile.GetParentHeight(),
+ Skipped: blockFile.GetSkipped(),
+ Timestamp: blockFile.GetBlockTimestamp(),
+ ObjectFormat: blockFile.GetObjectFormat(),
+ ByteOffset: blockFile.GetByteOffset(),
+ ByteLength: blockFile.GetByteLength(),
+ UncompressedLength: blockFile.GetUncompressedLength(),
+ }
+}
+
+func summarizeDurations(durations []time.Duration) cscbLatencySummary {
+ if len(durations) == 0 {
+ return cscbLatencySummary{}
+ }
+ sorted := append([]time.Duration(nil), durations...)
+ sort.Slice(sorted, func(i int, j int) bool { return sorted[i] < sorted[j] })
+ return cscbLatencySummary{
+ Samples: len(sorted),
+ P50Ms: durationMillis(percentileDuration(sorted, 0.50)),
+ P95Ms: durationMillis(percentileDuration(sorted, 0.95)),
+ MinMs: durationMillis(sorted[0]),
+ MaxMs: durationMillis(sorted[len(sorted)-1]),
+ }
+}
+
+func percentileDuration(sorted []time.Duration, percentile float64) time.Duration {
+ if len(sorted) == 0 {
+ return 0
+ }
+ if percentile <= 0 {
+ return sorted[0]
+ }
+ if percentile >= 1 {
+ return sorted[len(sorted)-1]
+ }
+ index := int(math.Ceil(percentile*float64(len(sorted)))) - 1
+ if index < 0 {
+ index = 0
+ }
+ if index >= len(sorted) {
+ index = len(sorted) - 1
+ }
+ return sorted[index]
+}
+
+func durationMillis(duration time.Duration) float64 {
+ return float64(duration.Microseconds()) / 1000
+}
+
+func summarizeCSCBFiles(files []*api.BlockFile) cscbFileSummary {
+ summary := cscbFileSummary{Count: len(files)}
+ formats := make(map[string]struct{})
+ for i, file := range files {
+ if i == 0 {
+ summary.FirstHeight = file.GetHeight()
+ }
+ summary.LastHeight = file.GetHeight()
+ if file.GetSkipped() {
+ summary.SkippedCount++
+ } else if isCSCBFile(file) {
+ summary.ConsolidatedObjectCount++
+ formats[cscbBlockObjectFormatName(file.GetObjectFormat())] = struct{}{}
+ } else {
+ summary.SingleBlockObjectCount++
+ formats[cscbBlockObjectFormatName(file.GetObjectFormat())] = struct{}{}
+ }
+ summary.MetadataByteLength += file.GetByteLength()
+ summary.MetadataUncompressedBytes += file.GetUncompressedLength()
+ }
+ for format := range formats {
+ summary.Formats = append(summary.Formats, format)
+ }
+ sort.Strings(summary.Formats)
+ return summary
+}
+
+func cscbBlockObjectFormatName(format api.BlockObjectFormat) string {
+ if format == api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK {
+ return "BLOCK_OBJECT_FORMAT_SINGLE_BLOCK"
+ }
+ return format.String()
+}
+
+func isCSCBFile(file *api.BlockFile) bool {
+ return file.GetObjectFormat() == api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH && file.GetByteLength() > 0
+}
+
+func appendUnique(existing []uint64, additions ...uint64) []uint64 {
+ seen := make(map[uint64]struct{}, len(existing)+len(additions))
+ for _, value := range existing {
+ seen[value] = struct{}{}
+ }
+ for _, value := range additions {
+ seen[value] = struct{}{}
+ }
+ out := make([]uint64, 0, len(seen))
+ for value := range seen {
+ out = append(out, value)
+ }
+ sort.Slice(out, func(i int, j int) bool { return out[i] < out[j] })
+ return out
+}
+
+func appendResultError(existing string, source string, err error) string {
+ return appendResultErrorString(existing, fmt.Sprintf("%s: %s", source, sanitizeCSCBError(err)))
+}
+
+func appendResultErrorString(existing string, message string) string {
+ if existing == "" {
+ return message
+ }
+ return existing + "; " + message
+}
+
+func sanitizeCSCBError(err error) string {
+ if err == nil {
+ return ""
+ }
+ return sanitizeCSCBString(err.Error())
+}
+
+func sanitizeCSCBString(value string) string {
+ return cscbURLRegexp.ReplaceAllString(value, "")
+}
+
+func ratio(numerator float64, denominator float64) float64 {
+ if denominator == 0 {
+ return 0
+ }
+ return numerator / denominator
+}
+
+func newCSCBCountingHTTPClient(base downloader.HTTPClient) *cscbCountingHTTPClient {
+ return &cscbCountingHTTPClient{base: base}
+}
+
+func (c *cscbCountingHTTPClient) Do(req *http.Request) (*http.Response, error) {
+ c.requests.Add(1)
+ resp, err := c.base.Do(req)
+ if err != nil || resp == nil || resp.Body == nil {
+ return resp, err
+ }
+ resp.Body = &cscbCountingReadCloser{body: resp.Body, client: c}
+ return resp, nil
+}
+
+func (c *cscbCountingHTTPClient) Reset() {
+ c.requests.Store(0)
+ c.bytes.Store(0)
+}
+
+func (c *cscbCountingHTTPClient) Snapshot() cscbHTTPSnapshot {
+ return cscbHTTPSnapshot{
+ requests: c.requests.Load(),
+ bytes: c.bytes.Load(),
+ }
+}
+
+func (r *cscbCountingReadCloser) Read(p []byte) (int, error) {
+ n, err := r.body.Read(p)
+ if n > 0 {
+ r.client.bytes.Add(int64(n))
+ }
+ return n, err
+}
+
+func (r *cscbCountingReadCloser) Close() error {
+ return r.body.Close()
+}
+
+func writeCSCBJSONReport(path string, report *cscbValidationReport) error {
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return xerrors.Errorf("failed to create report directory: %w", err)
+ }
+ data, err := json.MarshalIndent(report, "", " ")
+ if err != nil {
+ return xerrors.Errorf("failed to marshal JSON report: %w", err)
+ }
+ data = append(data, '\n')
+ if err := os.WriteFile(path, data, 0o644); err != nil {
+ return xerrors.Errorf("failed to write JSON report: %w", err)
+ }
+ return nil
+}
+
+func writeCSCBMarkdownReport(path string, report *cscbValidationReport) error {
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
+ return xerrors.Errorf("failed to create report directory: %w", err)
+ }
+ var b strings.Builder
+ fmt.Fprintf(&b, "# CSCB Dual-Read Validation Report\n\n")
+ fmt.Fprintf(&b, "- generated_at: `%s`\n", report.GeneratedAt)
+ fmt.Fprintf(&b, "- target: `%s/%s/%s`\n", report.Environment, report.Blockchain, report.Network)
+ fmt.Fprintf(&b, "- tag: `%d`\n", report.Tag)
+ fmt.Fprintf(&b, "- canary_range: `[%d, %d)`\n", report.StartHeight, report.EndHeight)
+ fmt.Fprintf(&b, "- chunk_blocks: `%d`\n", report.ChunkBlocks)
+ if report.ServerAddressOverride != "" {
+ fmt.Fprintf(&b, "- server_address_override: `%s`\n", report.ServerAddressOverride)
+ }
+ fmt.Fprintf(&b, "- grpc_transport_override: `%t`\n", report.GRPCTransportOverride)
+ if report.ClientID != "" {
+ fmt.Fprintf(&b, "- client_id: `%s`\n", report.ClientID)
+ }
+ fmt.Fprintf(&b, "- passed: `%t`\n", report.Passed)
+ fmt.Fprintf(&b, "- cases: `%d passed / %d total`\n", report.PassedCases, report.TotalCases)
+ fmt.Fprintf(&b, "- mismatch_heights: `%d`\n", report.CorrectnessMismatches)
+ fmt.Fprintf(&b, "- observed_fallback_blocks: `%d`\n\n", report.ObservedFallbackBlocks)
+
+ fmt.Fprintf(&b, "## Case Results\n\n")
+ fmt.Fprintf(&b, "| case | range | pass | fallback | single-block p50/p95 ms | consolidated p50/p95 ms | single-block bytes | consolidated bytes | mismatches |\n")
+ fmt.Fprintf(&b, "|---|---:|---:|---:|---:|---:|---:|---:|---:|\n")
+ for _, c := range report.Cases {
+ fmt.Fprintf(
+ &b,
+ "| %s | `[%d,%d)` | %t | %t | %.2f / %.2f | %.2f / %.2f | %d | %d | %s |\n",
+ c.Name,
+ c.StartHeight,
+ c.EndHeight,
+ c.Passed,
+ c.ObservedFallback,
+ c.SingleBlock.Latency.P50Ms,
+ c.SingleBlock.Latency.P95Ms,
+ c.Consolidated.Latency.P50Ms,
+ c.Consolidated.Latency.P95Ms,
+ c.SingleBlock.HTTP.Bytes,
+ c.Consolidated.HTTP.Bytes,
+ formatHeights(c.MismatchHeights),
+ )
+ }
+
+ fmt.Fprintf(&b, "\n## Source Summary\n\n")
+ for _, c := range report.Cases {
+ if c.Error != "" {
+ fmt.Fprintf(&b, "### %s\n\nerror: `%s`\n\n", c.Name, c.Error)
+ continue
+ }
+ fmt.Fprintf(&b, "### %s\n\n", c.Name)
+ fmt.Fprintf(&b, "- single-block files: `%d`, skipped: `%d`, single_block_objects: `%d`, consolidated_objects: `%d`, formats: `%s`, HTTP requests: `%d`, bytes: `%d`\n",
+ c.SingleBlock.Files.Count,
+ c.SingleBlock.Files.SkippedCount,
+ c.SingleBlock.Files.SingleBlockObjectCount,
+ c.SingleBlock.Files.ConsolidatedObjectCount,
+ strings.Join(c.SingleBlock.Files.Formats, ","),
+ c.SingleBlock.HTTP.Requests,
+ c.SingleBlock.HTTP.Bytes,
+ )
+ fmt.Fprintf(&b, "- consolidated files: `%d`, skipped: `%d`, single_block_objects: `%d`, consolidated_objects: `%d`, formats: `%s`, HTTP requests: `%d`, bytes: `%d`\n",
+ c.Consolidated.Files.Count,
+ c.Consolidated.Files.SkippedCount,
+ c.Consolidated.Files.SingleBlockObjectCount,
+ c.Consolidated.Files.ConsolidatedObjectCount,
+ strings.Join(c.Consolidated.Files.Formats, ","),
+ c.Consolidated.HTTP.Requests,
+ c.Consolidated.HTTP.Bytes,
+ )
+ fmt.Fprintf(&b, "- canonical hashes compared: `%d`\n\n", c.CanonicalHashesCompared)
+ }
+ return os.WriteFile(path, []byte(b.String()), 0o644)
+}
+
+func formatHeights(heights []uint64) string {
+ if len(heights) == 0 {
+ return ""
+ }
+ values := make([]string, len(heights))
+ for i, height := range heights {
+ values[i] = fmt.Sprintf("%d", height)
+ }
+ return strings.Join(values, ",")
+}
diff --git a/cmd/admin/cscb_validate_test.go b/cmd/admin/cscb_validate_test.go
new file mode 100644
index 00000000..323b3246
--- /dev/null
+++ b/cmd/admin/cscb_validate_test.go
@@ -0,0 +1,279 @@
+package main
+
+import (
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/proto"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestBuildCSCBValidationCasesIncludesRequiredCoverage(t *testing.T) {
+ require := require.New(t)
+ cfg := cscbValidationRunConfig{
+ startHeight: 1000,
+ endHeight: 1100,
+ chunkBlocks: 25,
+ randomSamples: 2,
+ randomSeed: 914,
+ iterations: 3,
+ fullObjectIterations: 1,
+ }
+
+ cases := buildCSCBValidationCases(cfg, 999)
+ names := make(map[string]cscbValidationCase, len(cases))
+ for _, validationCase := range cases {
+ names[validationCase.Name] = validationCase
+ }
+
+ require.Contains(names, "single_first_block")
+ require.Contains(names, "single_last_block")
+ require.Contains(names, "range_within_one_chunk")
+ require.Contains(names, "range_across_chunk_boundary")
+ require.Contains(names, "range_full_object")
+ require.Contains(names, "single_consolidated_miss_fallback")
+ require.True(names["single_consolidated_miss_fallback"].ExpectFallback)
+ require.Equal(1, names["range_full_object"].Iterations)
+ require.Equal(3, names["range_across_chunk_boundary"].Iterations)
+ require.NotEmpty(chunkBoundaryHeights(1000, 1100, 25))
+ require.Len(randomSampleHeights(1000, 1100, 2, 914), 2)
+}
+
+func TestCompareCSCBBlocksIgnoresSourceSpecificMetadata(t *testing.T) {
+ require := require.New(t)
+ singleBlock := &api.Block{
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Height: 42,
+ Hash: "hash",
+ ParentHash: "parent",
+ ObjectKeyMain: "single-block/object",
+ },
+ }
+ consolidated := &api.Block{
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Height: 42,
+ Hash: "hash",
+ ParentHash: "parent",
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 1024,
+ ByteLength: 2048,
+ UncompressedLength: 4096,
+ },
+ }
+
+ comparison := compareCSCBBlocks([]*api.Block{singleBlock}, []*api.Block{consolidated})
+ require.True(comparison.correct)
+ require.Empty(comparison.mismatchHeights)
+ require.Equal(1, comparison.hashesCompared)
+ require.Equal(comparison.firstSingleBlockHash, comparison.firstConsolidatedHash)
+}
+
+func TestCompareCSCBBlocksDetectsMismatch(t *testing.T) {
+ require := require.New(t)
+ singleBlock := &api.Block{
+ Metadata: &api.BlockMetadata{Height: 42, Hash: "hash"},
+ }
+ consolidated := &api.Block{
+ Metadata: &api.BlockMetadata{Height: 42, Hash: "different"},
+ }
+
+ comparison := compareCSCBBlocks([]*api.Block{singleBlock}, []*api.Block{consolidated})
+ require.False(comparison.correct)
+ require.Equal([]uint64{42}, comparison.mismatchHeights)
+}
+
+func TestCompareCSCBBlocksHandlesNilBlocks(t *testing.T) {
+ require := require.New(t)
+ consolidated := &api.Block{
+ Metadata: &api.BlockMetadata{Height: 42, Hash: "hash"},
+ }
+
+ comparison := compareCSCBBlocks([]*api.Block{nil}, []*api.Block{consolidated})
+ require.False(comparison.correct)
+ require.Equal([]uint64{42}, comparison.mismatchHeights)
+}
+
+func TestCanonicalBlockHashRestoresMetadata(t *testing.T) {
+ require := require.New(t)
+ block := &api.Block{
+ Metadata: &api.BlockMetadata{
+ Height: 42,
+ Hash: "hash",
+ ObjectKeyMain: "consolidated/object",
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 10,
+ ByteLength: 20,
+ UncompressedLength: 30,
+ },
+ }
+
+ _, err := canonicalBlockHash(block)
+ require.NoError(err)
+ require.Equal("consolidated/object", block.Metadata.ObjectKeyMain)
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, block.Metadata.ObjectFormat)
+ require.Equal(uint64(10), block.Metadata.ByteOffset)
+ require.Equal(uint64(20), block.Metadata.ByteLength)
+ require.Equal(uint64(30), block.Metadata.UncompressedLength)
+}
+
+func TestCanonicalRawBlockDataHashIgnoresSourceSpecificMetadata(t *testing.T) {
+ require := require.New(t)
+ block := &api.Block{
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Height: 42,
+ Hash: "hash",
+ ParentHash: "parent",
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 1024,
+ ByteLength: 2048,
+ UncompressedLength: 4096,
+ },
+ }
+ payload, err := proto.Marshal(block)
+ require.NoError(err)
+
+ rawHash, err := canonicalRawBlockDataHash(&api.BlockFile{Height: 42}, payload)
+ require.NoError(err)
+ expectedHash, err := canonicalBlockHash(block)
+ require.NoError(err)
+ require.Equal(expectedHash, rawHash)
+}
+
+func TestCanonicalRawBlockDataHashUsesCSCBBlockFileMetadata(t *testing.T) {
+ require := require.New(t)
+ block := &api.Block{
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Height: 42,
+ Hash: "stale-embedded-hash",
+ ParentHash: "stale-embedded-parent",
+ },
+ }
+ payload, err := proto.Marshal(block)
+ require.NoError(err)
+
+ blockFile := &api.BlockFile{
+ Tag: 1,
+ Height: 42,
+ Hash: "metadata-hash",
+ ParentHash: "metadata-parent",
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteLength: uint64(len(payload)),
+ }
+ rawHash, err := canonicalRawBlockDataHash(blockFile, payload)
+ require.NoError(err)
+
+ expectedHash, err := canonicalBlockHash(&api.Block{
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Height: 42,
+ Hash: "metadata-hash",
+ ParentHash: "metadata-parent",
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteLength: uint64(len(payload)),
+ },
+ })
+ require.NoError(err)
+ require.Equal(expectedHash, rawHash)
+}
+
+func TestCanonicalRawBlockDataHashHandlesSkippedBlock(t *testing.T) {
+ require := require.New(t)
+ rawHash, err := canonicalRawBlockDataHash(&api.BlockFile{Tag: 1, Height: 42, Skipped: true}, nil)
+ require.NoError(err)
+ expectedHash, err := canonicalBlockHash(&api.Block{
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Height: 42,
+ Skipped: true,
+ },
+ })
+ require.NoError(err)
+ require.Equal(expectedHash, rawHash)
+}
+
+func TestCompareCSCBDigestsDetectsMismatch(t *testing.T) {
+ require := require.New(t)
+ comparison := compareCSCBDigests(
+ []cscbBlockDigest{{height: 42, hash: "single-block"}},
+ []cscbBlockDigest{{height: 42, hash: "consolidated"}},
+ )
+
+ require.False(comparison.correct)
+ require.Equal([]uint64{42}, comparison.mismatchHeights)
+ require.Equal(1, comparison.hashesCompared)
+ require.Equal("single-block", comparison.firstSingleBlockHash)
+ require.Equal("consolidated", comparison.firstConsolidatedHash)
+}
+
+func TestCompareCSCBDigestsHandlesLengthMismatch(t *testing.T) {
+ require := require.New(t)
+ comparison := compareCSCBDigests(
+ []cscbBlockDigest{{height: 0, hash: "hash"}},
+ []cscbBlockDigest{{height: 0, hash: "hash"}, {height: 1, hash: "hash"}},
+ )
+
+ require.False(comparison.correct)
+ require.Equal([]uint64{0, 1}, comparison.mismatchHeights)
+ require.Zero(comparison.hashesCompared)
+}
+
+func TestSummarizeDurationsUsesNearestRankPercentile(t *testing.T) {
+ require := require.New(t)
+ summary := summarizeDurations([]time.Duration{
+ 10 * time.Millisecond,
+ 20 * time.Millisecond,
+ 30 * time.Millisecond,
+ 40 * time.Millisecond,
+ 50 * time.Millisecond,
+ })
+
+ require.Equal(5, summary.Samples)
+ require.Equal(float64(30), summary.P50Ms)
+ require.Equal(float64(50), summary.P95Ms)
+ require.Equal(float64(10), summary.MinMs)
+ require.Equal(float64(50), summary.MaxMs)
+}
+
+func TestSummarizeCSCBFilesDoesNotCountSkippedBlocksAsSingleBlockFallback(t *testing.T) {
+ require := require.New(t)
+
+ summary := summarizeCSCBFiles([]*api.BlockFile{
+ {
+ Height: 100,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteLength: 1024,
+ },
+ {
+ Height: 101,
+ Skipped: true,
+ },
+ {
+ Height: 102,
+ ByteLength: 512,
+ },
+ })
+
+ require.Equal(3, summary.Count)
+ require.Equal(1, summary.ConsolidatedObjectCount)
+ require.Equal(1, summary.SkippedCount)
+ require.Equal(1, summary.SingleBlockObjectCount)
+ require.Equal(uint64(100), summary.FirstHeight)
+ require.Equal(uint64(102), summary.LastHeight)
+ require.Equal([]string{"BLOCK_OBJECT_FORMAT_CSCB_BATCH", "BLOCK_OBJECT_FORMAT_SINGLE_BLOCK"}, summary.Formats)
+}
+
+func TestSanitizeCSCBStringRedactsEmbeddedURLs(t *testing.T) {
+ require := require.New(t)
+ errText := `Download failed: blockFile={FileUrl:"https://bucket.s3.amazonaws.com/key?X-Amz-Signature=secret", Height:42}`
+
+ sanitized := sanitizeCSCBString(errText)
+ require.NotContains(sanitized, "X-Amz-Signature")
+ require.NotContains(sanitized, "secret")
+ require.Contains(sanitized, "")
+}
diff --git a/cmd/admin/db_init.go b/cmd/admin/db_init.go
new file mode 100644
index 00000000..d0ef9166
--- /dev/null
+++ b/cmd/admin/db_init.go
@@ -0,0 +1,473 @@
+package main
+
+import (
+ "context"
+ "database/sql"
+ "encoding/json"
+ "fmt"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/secretsmanager"
+ "github.com/lib/pq"
+ _ "github.com/lib/pq"
+ "github.com/pressly/goose/v3"
+ "github.com/spf13/cobra"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/utils/log"
+)
+
+// DBInitSecret is no longer needed - we parse flat JSON directly
+
+func newDBInitCommand() *cobra.Command {
+ var (
+ awsRegion string
+ dryRun bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "db-init",
+ Short: "Initialize database and users for a specific network from AWS Secrets Manager",
+ Long: `Initialize PostgreSQL database and users for a specific blockchain network based on configuration stored in AWS Secrets Manager.
+
+This command MUST be run from the ChainStorage admin pod, which has the master PostgreSQL credentials
+injected as environment variables through Kubernetes secrets.
+
+The command:
+1. Uses master credentials from environment variables (CHAINSTORAGE_AWS_POSTGRES_*)
+2. Fetches network-specific credentials from AWS Secrets Manager
+3. Creates a database for the specified network
+4. Creates network-specific server (read-only) and worker (read-write) users
+5. Sets up proper permissions for each role
+6. Is idempotent - can be run multiple times safely
+
+Required environment variables (automatically available in admin pod):
+- CHAINSTORAGE_AWS_POSTGRES_HOST: PostgreSQL cluster endpoint
+- CHAINSTORAGE_AWS_POSTGRES_PORT: PostgreSQL port
+- CHAINSTORAGE_AWS_POSTGRES_USER: Master username
+- CHAINSTORAGE_AWS_POSTGRES_PASSWORD: Master password
+
+Example usage (run from admin pod):
+ # Initialize database for ethereum-mainnet
+ ./admin db-init --blockchain ethereum --network mainnet --env dev
+
+ # Dry run to preview changes
+ ./admin db-init --blockchain ethereum --network mainnet --env dev --dry-run
+
+ # Use specific AWS region
+ ./admin db-init --blockchain ethereum --network mainnet --env prod --aws-region us-west-2`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // Use commonFlags from common.go for blockchain, network, and env
+ return runDBInit(commonFlags.blockchain, commonFlags.network, commonFlags.env, awsRegion, dryRun)
+ },
+ }
+
+ cmd.Flags().StringVar(&awsRegion, "aws-region", "us-east-1", "AWS region")
+ cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Preview changes without applying them")
+
+ return cmd
+}
+
+func runDBInit(blockchain, network, env, awsRegion string, dryRun bool) error {
+ ctx := context.Background()
+ logger := log.WithPackage(logger)
+
+ logger.Info("Starting database initialization",
+ zap.String("blockchain", blockchain),
+ zap.String("network", network),
+ zap.String("environment", env),
+ zap.String("region", awsRegion),
+ zap.Bool("dry_run", dryRun))
+
+ // Get master credentials from environment variables (as set in admin pod deployment)
+ masterHost := os.Getenv("CHAINSTORAGE_AWS_POSTGRES_HOST")
+ masterPortStr := os.Getenv("CHAINSTORAGE_AWS_POSTGRES_PORT")
+ masterUser := os.Getenv("CHAINSTORAGE_AWS_POSTGRES_USER")
+ masterPassword := os.Getenv("CHAINSTORAGE_AWS_POSTGRES_PASSWORD")
+
+ if masterHost == "" || masterPortStr == "" || masterUser == "" || masterPassword == "" {
+ return xerrors.New("missing required environment variables: CHAINSTORAGE_AWS_POSTGRES_HOST, CHAINSTORAGE_AWS_POSTGRES_PORT, CHAINSTORAGE_AWS_POSTGRES_USER, CHAINSTORAGE_AWS_POSTGRES_PASSWORD - ensure this command is run from the admin pod")
+ }
+
+ var masterPort int
+ if _, err := fmt.Sscanf(masterPortStr, "%d", &masterPort); err != nil {
+ return xerrors.Errorf("invalid port number: %s", masterPortStr)
+ }
+
+ // Construct secret name
+ secretName := fmt.Sprintf("chainstorage/db-creds/%s", env)
+
+ // Fetch secret from AWS Secrets Manager
+ secretData, err := fetchSecret(ctx, secretName, awsRegion)
+ if err != nil {
+ return xerrors.Errorf("failed to fetch secret: %w", err)
+ }
+
+ // Construct lookup keys for this network
+ // Replace hyphens with underscores for the key lookup
+ networkKey := strings.ReplaceAll(fmt.Sprintf("%s_%s", blockchain, network), "-", "_")
+
+ // Extract network-specific values from flat secret
+ dbName, err := getStringFromSecret(secretData, fmt.Sprintf("%s_database_name", networkKey))
+ if err != nil {
+ return xerrors.Errorf("failed to get database name: %w", err)
+ }
+
+ serverUsername, err := getStringFromSecret(secretData, fmt.Sprintf("%s_server_username", networkKey))
+ if err != nil {
+ return xerrors.Errorf("failed to get server username: %w", err)
+ }
+
+ serverPassword, err := getStringFromSecret(secretData, fmt.Sprintf("%s_server_password", networkKey))
+ if err != nil {
+ return xerrors.Errorf("failed to get server password: %w", err)
+ }
+
+ workerUsername, err := getStringFromSecret(secretData, fmt.Sprintf("%s_worker_username", networkKey))
+ if err != nil {
+ return xerrors.Errorf("failed to get worker username: %w", err)
+ }
+
+ workerPassword, err := getStringFromSecret(secretData, fmt.Sprintf("%s_worker_password", networkKey))
+ if err != nil {
+ return xerrors.Errorf("failed to get worker password: %w", err)
+ }
+
+ logger.Info("Successfully fetched credentials from AWS Secrets Manager",
+ zap.String("database", dbName),
+ zap.String("server_user", serverUsername),
+ zap.String("worker_user", workerUsername))
+
+ if dryRun {
+ logger.Info("DRY RUN MODE - No changes will be made",
+ zap.String("database", dbName),
+ zap.String("server_user", serverUsername),
+ zap.String("worker_user", workerUsername))
+ return nil
+ }
+
+ // Connect to PostgreSQL as master user
+ masterDSN := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=postgres sslmode=require",
+ masterHost, masterPort, masterUser, masterPassword)
+
+ db, err := sql.Open("postgres", masterDSN)
+ if err != nil {
+ return xerrors.Errorf("failed to connect to database: %w", err)
+ }
+ defer func() {
+ if closeErr := db.Close(); closeErr != nil {
+ logger.Warn("Failed to close database connection", zap.Error(closeErr))
+ }
+ }()
+
+ // Set connection pool settings
+ db.SetMaxOpenConns(5)
+ db.SetMaxIdleConns(2)
+ db.SetConnMaxLifetime(30 * time.Second)
+
+ // Test connection
+ if err := db.PingContext(ctx); err != nil {
+ return xerrors.Errorf("failed to ping database: %w", err)
+ }
+
+ logger.Info("Successfully connected to PostgreSQL cluster")
+
+ // Create users
+ logger.Info("Creating users")
+
+ if err := createUser(db, workerUsername, workerPassword, true, logger); err != nil {
+ return xerrors.Errorf("failed to create worker user %s: %w", workerUsername, err)
+ }
+ logger.Info("Created/verified worker user", zap.String("username", workerUsername))
+
+ if err := createUser(db, serverUsername, serverPassword, false, logger); err != nil {
+ return xerrors.Errorf("failed to create server user %s: %w", serverUsername, err)
+ }
+ logger.Info("Created/verified server user", zap.String("username", serverUsername))
+
+ // Create database
+ logger.Info("Creating database")
+ if err := createDatabase(db, dbName, workerUsername, logger); err != nil {
+ return xerrors.Errorf("failed to create database %s: %w", dbName, err)
+ }
+ logger.Info("Created/verified database", zap.String("database", dbName))
+
+ // Run migrations on the network database
+ logger.Info("Running migrations")
+ if err := runMigrations(ctx, masterHost, masterPort, masterUser, masterPassword, dbName, logger); err != nil {
+ return xerrors.Errorf("failed to run migrations: %w", err)
+ }
+ logger.Info("Migrations completed successfully")
+
+ // Grant permissions
+ logger.Info("Setting up permissions")
+
+ // Grant CONNECT permission to server user
+ if err := grantConnectPermission(db, dbName, serverUsername, logger); err != nil {
+ return xerrors.Errorf("failed to grant CONNECT permission on %s to %s: %w",
+ dbName, serverUsername, err)
+ }
+
+ // Connect to the network database to grant permissions
+ netDSN := fmt.Sprintf("host=%s port=%d user=%s password=%s dbname=%s sslmode=require",
+ masterHost, masterPort, masterUser, masterPassword, dbName)
+
+ netDB, err := sql.Open("postgres", netDSN)
+ if err != nil {
+ return xerrors.Errorf("failed to connect to database %s: %w", dbName, err)
+ }
+ defer func() {
+ if closeErr := netDB.Close(); closeErr != nil {
+ logger.Warn("Failed to close network database connection", zap.Error(closeErr))
+ }
+ }()
+
+ // Grant full access to worker user (owner)
+ if err := grantFullAccess(netDB, workerUsername, logger); err != nil {
+ return xerrors.Errorf("failed to grant full access on %s to %s: %w", dbName, workerUsername, err)
+ }
+ logger.Info("Granted full access to worker user", zap.String("username", workerUsername))
+
+ // Grant permissions to server user
+ if err := grantReadOnlyAccess(netDB, serverUsername, workerUsername, logger); err != nil {
+ return xerrors.Errorf("failed to grant permissions on %s: %w", dbName, err)
+ }
+ logger.Info("Granted read-only access to server user", zap.String("username", serverUsername))
+
+ logger.Info("Database initialization completed successfully",
+ zap.String("network", fmt.Sprintf("%s-%s", blockchain, network)),
+ zap.String("database", dbName),
+ zap.String("worker_user", workerUsername),
+ zap.String("server_user", serverUsername))
+
+ logger.Info("Next steps",
+ zap.String("step1", "Deploy chainstorage worker pods to start data ingestion"),
+ zap.String("step2", "Deploy chainstorage server pods for API access"),
+ zap.String("step3", "Monitor logs for successful connections"))
+
+ return nil
+}
+
+func fetchSecret(ctx context.Context, secretName, region string) (map[string]interface{}, error) {
+ cfg, err := config.LoadDefaultConfig(ctx, config.WithRegion(region))
+ if err != nil {
+ return nil, xerrors.Errorf("failed to load AWS config: %w", err)
+ }
+
+ client := secretsmanager.NewFromConfig(cfg)
+
+ input := &secretsmanager.GetSecretValueInput{
+ SecretId: &secretName,
+ }
+
+ result, err := client.GetSecretValue(ctx, input)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get secret value: %w", err)
+ }
+
+ // Parse the flat JSON secret
+ var secretData map[string]interface{}
+ if err := json.Unmarshal([]byte(*result.SecretString), &secretData); err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal secret: %w", err)
+ }
+
+ return secretData, nil
+}
+
+func getStringFromSecret(secret map[string]interface{}, key string) (string, error) {
+ value, ok := secret[key]
+ if !ok {
+ return "", xerrors.Errorf("key %s not found in secret", key)
+ }
+
+ strValue, ok := value.(string)
+ if !ok {
+ return "", xerrors.Errorf("value for key %s is not a string", key)
+ }
+
+ return strValue, nil
+}
+
+func createUser(db *sql.DB, username, password string, canCreateDB bool, logger *zap.Logger) error {
+ // Check if user exists
+ var exists bool
+ query := `SELECT EXISTS(SELECT 1 FROM pg_user WHERE usename = $1)`
+ if err := db.QueryRow(query, username).Scan(&exists); err != nil {
+ return xerrors.Errorf("failed to check if user exists: %w", err)
+ }
+
+ if exists {
+ logger.Info("User already exists, updating password", zap.String("username", username))
+ // Update password for existing user
+ alterQuery := fmt.Sprintf("ALTER USER %s WITH PASSWORD %s",
+ pq.QuoteIdentifier(username), pq.QuoteLiteral(password))
+ if _, err := db.Exec(alterQuery); err != nil {
+ return xerrors.Errorf("failed to update user password: %w", err)
+ }
+ return nil
+ }
+
+ // Create user with proper quoting
+ createQuery := fmt.Sprintf("CREATE USER %s WITH LOGIN PASSWORD %s",
+ pq.QuoteIdentifier(username), pq.QuoteLiteral(password))
+
+ if canCreateDB {
+ createQuery += " CREATEDB"
+ }
+
+ if _, err := db.Exec(createQuery); err != nil {
+ return xerrors.Errorf("failed to create user: %w", err)
+ }
+
+ return nil
+}
+
+func createDatabase(db *sql.DB, dbName, owner string, logger *zap.Logger) error {
+ // Check if database exists
+ var exists bool
+ query := `SELECT EXISTS(SELECT 1 FROM pg_database WHERE datname = $1)`
+ if err := db.QueryRow(query, dbName).Scan(&exists); err != nil {
+ return xerrors.Errorf("failed to check if database exists: %w", err)
+ }
+
+ if exists {
+ logger.Info("Database already exists, checking ownership", zap.String("database", dbName))
+ // Check current owner
+ var currentOwner string
+ ownerQuery := `SELECT pg_get_userbyid(datdba) FROM pg_database WHERE datname = $1`
+ if err := db.QueryRow(ownerQuery, dbName).Scan(¤tOwner); err != nil {
+ return xerrors.Errorf("failed to get database owner: %w", err)
+ }
+
+ if currentOwner == owner {
+ logger.Info("Database already owned by expected owner", zap.String("database", dbName), zap.String("owner", owner))
+ return nil
+ } else {
+ logger.Info("Transferring database ownership", zap.String("database", dbName), zap.String("current_owner", currentOwner), zap.String("new_owner", owner))
+ // Transfer ownership
+ alterQuery := fmt.Sprintf("ALTER DATABASE %s OWNER TO %s",
+ pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(owner))
+ if _, err := db.Exec(alterQuery); err != nil {
+ return xerrors.Errorf("failed to transfer database ownership: %w", err)
+ }
+ return nil
+ }
+ }
+
+ // Create database with master user as owner first, then transfer ownership
+ createQuery := fmt.Sprintf("CREATE DATABASE %s ENCODING 'UTF8'",
+ pq.QuoteIdentifier(dbName))
+
+ if _, err := db.Exec(createQuery); err != nil {
+ return xerrors.Errorf("failed to create database: %w", err)
+ }
+
+ // Transfer ownership to the specified owner
+ alterQuery := fmt.Sprintf("ALTER DATABASE %s OWNER TO %s",
+ pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(owner))
+
+ if _, err := db.Exec(alterQuery); err != nil {
+ return xerrors.Errorf("failed to transfer database ownership: %w", err)
+ }
+
+ return nil
+}
+
+func grantConnectPermission(db *sql.DB, dbName, username string, logger *zap.Logger) error {
+ // Grant CONNECT permission on database
+ grantQuery := fmt.Sprintf("GRANT CONNECT ON DATABASE %s TO %s",
+ pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(username))
+
+ if _, err := db.Exec(grantQuery); err != nil {
+ // This might fail if permission already exists, which is fine
+ return xerrors.Errorf("failed to grant connect permission: %w", err)
+ }
+
+ return nil
+}
+
+func grantReadOnlyAccess(db *sql.DB, readUser, ownerUser string, logger *zap.Logger) error {
+ queries := []string{
+ fmt.Sprintf("GRANT USAGE ON SCHEMA public TO %s", pq.QuoteIdentifier(readUser)),
+ fmt.Sprintf("GRANT SELECT ON ALL TABLES IN SCHEMA public TO %s", pq.QuoteIdentifier(readUser)),
+ fmt.Sprintf("GRANT SELECT ON ALL SEQUENCES IN SCHEMA public TO %s", pq.QuoteIdentifier(readUser)),
+ // Set default privileges for future objects created by the owner
+ fmt.Sprintf("ALTER DEFAULT PRIVILEGES FOR USER %s IN SCHEMA public GRANT SELECT ON TABLES TO %s",
+ pq.QuoteIdentifier(ownerUser), pq.QuoteIdentifier(readUser)),
+ fmt.Sprintf("ALTER DEFAULT PRIVILEGES FOR USER %s IN SCHEMA public GRANT SELECT ON SEQUENCES TO %s",
+ pq.QuoteIdentifier(ownerUser), pq.QuoteIdentifier(readUser)),
+ }
+
+ for _, q := range queries {
+ if _, err := db.Exec(q); err != nil {
+ // Some permissions might already exist, log but don't fail
+ logger.Warn("Failed to grant read-only permission (continuing)", zap.Error(err))
+ }
+ }
+
+ return nil
+}
+
+func grantFullAccess(db *sql.DB, username string, logger *zap.Logger) error {
+ queries := []string{
+ fmt.Sprintf("GRANT ALL PRIVILEGES ON SCHEMA public TO %s", pq.QuoteIdentifier(username)),
+ fmt.Sprintf("GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO %s", pq.QuoteIdentifier(username)),
+ fmt.Sprintf("GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO %s", pq.QuoteIdentifier(username)),
+ fmt.Sprintf("GRANT ALL PRIVILEGES ON ALL FUNCTIONS IN SCHEMA public TO %s", pq.QuoteIdentifier(username)),
+ // Set default privileges for future objects
+ fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO %s", pq.QuoteIdentifier(username)),
+ fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO %s", pq.QuoteIdentifier(username)),
+ fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON FUNCTIONS TO %s", pq.QuoteIdentifier(username)),
+ }
+
+ for _, q := range queries {
+ if _, err := db.Exec(q); err != nil {
+ // Some permissions might already exist, log but don't fail
+ logger.Warn("Failed to grant full access permission (continuing)", zap.Error(err))
+ }
+ }
+
+ return nil
+}
+
+func runMigrations(ctx context.Context, host string, port int, user, password, dbName string, logger *zap.Logger) error {
+ // Connect to the network database to run migrations
+ migrationDSN := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=require",
+ host, port, dbName, user, password)
+
+ migrationDB, err := sql.Open("postgres", migrationDSN)
+ if err != nil {
+ return xerrors.Errorf("failed to connect to database for migrations: %w", err)
+ }
+ defer func() {
+ if closeErr := migrationDB.Close(); closeErr != nil {
+ logger.Warn("Failed to close migration database connection", zap.Error(closeErr))
+ }
+ }()
+
+ // Test connection
+ if err := migrationDB.PingContext(ctx); err != nil {
+ return xerrors.Errorf("failed to ping migration database: %w", err)
+ }
+
+ // Set dialect for goose
+ if err := goose.SetDialect("postgres"); err != nil {
+ return xerrors.Errorf("failed to set goose dialect: %w", err)
+ }
+
+ // Run migrations using the file system path
+ migrationsDir := "/app/migrations"
+ if err := goose.UpContext(ctx, migrationDB, migrationsDir); err != nil {
+ return xerrors.Errorf("failed to run migrations: %w", err)
+ }
+
+ return nil
+}
+
+func init() {
+ rootCmd.AddCommand(newDBInitCommand())
+}
diff --git a/cmd/admin/db_migrate.go b/cmd/admin/db_migrate.go
new file mode 100644
index 00000000..0fae977b
--- /dev/null
+++ b/cmd/admin/db_migrate.go
@@ -0,0 +1,215 @@
+package main
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "os"
+ "time"
+
+ _ "github.com/lib/pq"
+ "github.com/pressly/goose/v3"
+ "github.com/spf13/cobra"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+ "github.com/coinbase/chainstorage/internal/utils/log"
+)
+
+const maxMigrationVersion int64 = 1<<63 - 1
+
+func newDBMigrateCommand() *cobra.Command {
+ var (
+ masterUser string
+ masterPassword string
+ host string
+ port int
+ sslMode string
+ connectTimeout time.Duration
+ dryRun bool
+ dbName string
+ )
+
+ cmd := &cobra.Command{
+ Use: "db-migrate",
+ Short: "Run PostgreSQL schema migrations",
+ Long: `Run PostgreSQL schema migrations using admin/master credentials.
+
+This command MUST be run from the ChainStorage admin pod or with master credentials.
+It applies any pending schema migrations to the database.
+
+The migrations are tracked via the goose_db_version table, ensuring idempotency.
+Running this command multiple times is safe - it will only apply new migrations.
+
+Required flags:
+- --master-user: PostgreSQL master/admin username
+- --master-password: PostgreSQL master/admin password
+
+Optional flags:
+- --host: PostgreSQL host (default: from environment)
+- --port: PostgreSQL port (default: 5432)
+- --db-name: Database name (default: chainstorage_{blockchain}_{network})
+- --ssl-mode: SSL mode (default: require)
+- --dry-run: Show pending migrations without applying them
+
+Example usage:
+ # Run migrations for ethereum-mainnet (from admin pod)
+ ./admin db-migrate --blockchain ethereum --network mainnet --env dev --master-user postgres --master-password
+
+ # Dry run to see pending migrations
+ ./admin db-migrate --blockchain ethereum --network mainnet --env dev --master-user postgres --master-password --dry-run
+
+ # Use custom database name
+ ./admin db-migrate --blockchain ethereum --network mainnet --env dev --master-user postgres --master-password --db-name my_custom_db`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runDBMigrate(masterUser, masterPassword, host, port, dbName, sslMode, connectTimeout, dryRun)
+ },
+ }
+
+ cmd.Flags().StringVar(&masterUser, "master-user", "", "PostgreSQL master/admin username (required)")
+ cmd.Flags().StringVar(&masterPassword, "master-password", "", "PostgreSQL master/admin password (required)")
+ cmd.Flags().StringVar(&host, "host", "", "PostgreSQL host (uses environment if not specified)")
+ cmd.Flags().IntVar(&port, "port", 5432, "PostgreSQL port")
+ cmd.Flags().StringVar(&dbName, "db-name", "", "Database name (default: chainstorage_{blockchain}_{network})")
+ cmd.Flags().StringVar(&sslMode, "ssl-mode", "require", "SSL mode (disable, require, verify-ca, verify-full)")
+ cmd.Flags().DurationVar(&connectTimeout, "connect-timeout", 30*time.Second, "Connection timeout")
+ cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Show pending migrations without applying them")
+
+ _ = cmd.MarkFlagRequired("master-user")
+ _ = cmd.MarkFlagRequired("master-password")
+
+ return cmd
+}
+
+func runDBMigrate(masterUser, masterPassword, host string, port int, dbName, sslMode string, connectTimeout time.Duration, dryRun bool) error {
+ ctx := context.Background()
+ logger := log.WithPackage(logger)
+
+ // Determine host from environment if not provided
+ if host == "" {
+ host = getEnvOrDefault("CHAINSTORAGE_AWS_POSTGRES_HOST", "localhost")
+ }
+
+ // Determine database name if not provided
+ if dbName == "" {
+ dbName = fmt.Sprintf("chainstorage_%s_%s", commonFlags.blockchain, commonFlags.network)
+ dbName = replaceHyphensWithUnderscores(dbName)
+ }
+
+ logger.Info("Starting database migration",
+ zap.String("blockchain", commonFlags.blockchain),
+ zap.String("network", commonFlags.network),
+ zap.String("env", commonFlags.env),
+ zap.String("host", host),
+ zap.Int("port", port),
+ zap.String("database", dbName),
+ zap.Bool("dry_run", dryRun),
+ )
+
+ // Build connection config
+ cfg := &config.PostgresConfig{
+ Host: host,
+ Port: port,
+ Database: dbName,
+ User: masterUser,
+ Password: masterPassword,
+ SSLMode: sslMode,
+ ConnectTimeout: connectTimeout,
+ }
+
+ // Connect to database
+ dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s connect_timeout=%d",
+ cfg.Host, cfg.Port, cfg.Database, cfg.User, cfg.Password, cfg.SSLMode, int(cfg.ConnectTimeout.Seconds()))
+
+ db, err := sql.Open("postgres", dsn)
+ if err != nil {
+ return xerrors.Errorf("failed to open database connection: %w", err)
+ }
+ defer func() {
+ _ = db.Close()
+ }()
+
+ if err := db.PingContext(ctx); err != nil {
+ return xerrors.Errorf("failed to ping database: %w", err)
+ }
+
+ logger.Info("Successfully connected to database")
+
+ // Set up goose
+ goose.SetBaseFS(postgres.GetEmbeddedMigrations())
+ if err := goose.SetDialect("postgres"); err != nil {
+ return xerrors.Errorf("failed to set goose dialect: %w", err)
+ }
+
+ if dryRun {
+ // Show pending migrations
+ logger.Info("Checking for pending migrations (dry run)")
+
+ currentVersion, err := goose.GetDBVersion(db)
+ if err != nil {
+ return xerrors.Errorf("failed to get current database version: %w", err)
+ }
+
+ logger.Info("Current database version", zap.Int64("version", currentVersion))
+
+ migrations, err := collectDBMigrations()
+ if err != nil {
+ return xerrors.Errorf("failed to collect migrations: %w", err)
+ }
+
+ fmt.Printf("\n📋 Migration Status:\n")
+ fmt.Printf("Current version: %d\n\n", currentVersion)
+
+ hasPending := false
+ for _, migration := range migrations {
+ if migration.Version > currentVersion {
+ fmt.Printf(" [PENDING] %d: %s\n", migration.Version, migration.Source)
+ hasPending = true
+ } else {
+ fmt.Printf(" [APPLIED] %d: %s\n", migration.Version, migration.Source)
+ }
+ }
+
+ if !hasPending {
+ fmt.Printf("\n✅ No pending migrations. Database is up to date!\n")
+ } else {
+ fmt.Printf("\n⚠️ Pending migrations found. Run without --dry-run to apply them.\n")
+ }
+
+ return nil
+ }
+
+ // Apply migrations
+ logger.Info("Applying database migrations")
+ if err := goose.UpContext(ctx, db, "db/migrations"); err != nil {
+ return xerrors.Errorf("failed to run migrations: %w", err)
+ }
+
+ currentVersion, err := goose.GetDBVersion(db)
+ if err != nil {
+ return xerrors.Errorf("failed to get current database version: %w", err)
+ }
+
+ logger.Info("Migrations completed successfully", zap.Int64("current_version", currentVersion))
+ fmt.Printf("\n✅ Database migrations completed successfully!\n")
+ fmt.Printf("Current version: %d\n", currentVersion)
+
+ return nil
+}
+
+func collectDBMigrations() (goose.Migrations, error) {
+ return goose.CollectMigrations("db/migrations", 0, maxMigrationVersion)
+}
+
+func getEnvOrDefault(key, defaultValue string) string {
+ if value := os.Getenv(key); value != "" {
+ return value
+ }
+ return defaultValue
+}
+
+func init() {
+ rootCmd.AddCommand(newDBMigrateCommand())
+}
diff --git a/cmd/admin/db_migrate_test.go b/cmd/admin/db_migrate_test.go
new file mode 100644
index 00000000..f1e2893f
--- /dev/null
+++ b/cmd/admin/db_migrate_test.go
@@ -0,0 +1,30 @@
+package main
+
+import (
+ "testing"
+
+ "github.com/pressly/goose/v3"
+ "github.com/stretchr/testify/require"
+
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+)
+
+func TestCollectDBMigrationsIncludesTimestampMigrations(t *testing.T) {
+ goose.SetBaseFS(postgres.GetEmbeddedMigrations())
+ t.Cleanup(func() {
+ goose.SetBaseFS(nil)
+ })
+
+ migrations, err := collectDBMigrations()
+ require.NoError(t, err)
+
+ var hasRetirementMigration bool
+ for _, migration := range migrations {
+ if migration.Version == 20260703000001 {
+ hasRetirementMigration = true
+ break
+ }
+ }
+
+ require.True(t, hasRetirementMigration)
+}
diff --git a/cmd/admin/env_set.go b/cmd/admin/env_set.go
new file mode 100644
index 00000000..db5b1a9f
--- /dev/null
+++ b/cmd/admin/env_set.go
@@ -0,0 +1,186 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/spf13/cobra"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/utils/log"
+)
+
+func newEnvSetCommand() *cobra.Command {
+ var (
+ awsRegion string
+ quiet bool
+ )
+
+ cmd := &cobra.Command{
+ Use: "env-set",
+ Short: "Set environment variables for PostgreSQL credentials from AWS Secrets Manager",
+ Long: `Set environment variables for PostgreSQL credentials from AWS Secrets Manager.
+
+This command fetches database credentials from AWS Secrets Manager and outputs export commands
+that can be evaluated in your shell to set environment variables.
+
+To use this command and set the environment variables in your current shell session:
+ eval $(./admin env-set --blockchain ethereum --network mainnet --env dev --quiet)
+
+Example usage:
+ # Set environment variables for ethereum mainnet (worker role)
+ eval $(./admin env-set --blockchain ethereum --network mainnet --env dev --quiet)
+
+The command will output the following environment variables:
+ - CHAINSTORAGE_AWS_POSTGRES_USER
+ - CHAINSTORAGE_AWS_POSTGRES_PASSWORD
+ - CHAINSTORAGE_AWS_POSTGRES_DATABASE
+ - CHAINSTORAGE_AWS_POSTGRES_HOST (from master credentials)
+ - CHAINSTORAGE_AWS_POSTGRES_PORT (from master credentials)
+ - CHAINSTORAGE_AWS_POSTGRES_SSL_MODE (default: require)`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // Use worker role by default
+ role := "worker"
+ return runEnvSet(commonFlags.blockchain, commonFlags.network, commonFlags.env, awsRegion, role, quiet)
+ },
+ }
+
+ cmd.Flags().StringVar(&awsRegion, "aws-region", "us-east-1", "AWS region for Secrets Manager")
+ cmd.Flags().BoolVar(&quiet, "quiet", false, "Suppress log output (use with eval)")
+
+ return cmd
+}
+
+type EnvVars struct {
+ User string `json:"CHAINSTORAGE_AWS_POSTGRES_USER"`
+ Password string `json:"CHAINSTORAGE_AWS_POSTGRES_PASSWORD"`
+ Database string `json:"CHAINSTORAGE_AWS_POSTGRES_DATABASE"`
+ Host string `json:"CHAINSTORAGE_AWS_POSTGRES_HOST"`
+ Port string `json:"CHAINSTORAGE_AWS_POSTGRES_PORT"`
+ SSLMode string `json:"CHAINSTORAGE_AWS_POSTGRES_SSL_MODE"`
+}
+
+func runEnvSet(blockchain, network, env, awsRegion, role string, quiet bool) error {
+ ctx := context.Background()
+ logger := log.WithPackage(logger)
+
+ if !quiet {
+ logger.Info("Setting environment variables",
+ zap.String("blockchain", blockchain),
+ zap.String("network", network),
+ zap.String("environment", env),
+ zap.String("aws_region", awsRegion),
+ zap.String("role", role))
+ }
+
+ // Get master credentials from environment variables
+ masterHost := os.Getenv("CHAINSTORAGE_CLUSTER_ENDPOINT")
+ masterPortStr := os.Getenv("CHAINSTORAGE_CLUSTER_PORT")
+
+ if masterHost == "" || masterPortStr == "" {
+ return xerrors.New("missing required environment variables: CHAINSTORAGE_CLUSTER_ENDPOINT, CHAINSTORAGE_CLUSTER_PORT")
+ }
+
+ // Construct secret name
+ secretName := fmt.Sprintf("chainstorage/db-creds/%s", env)
+
+ // Fetch secret from AWS Secrets Manager
+ secretData, err := fetchSecret(ctx, secretName, awsRegion)
+ if err != nil {
+ return xerrors.Errorf("failed to fetch secret: %w", err)
+ }
+
+ // Construct lookup keys for this network
+ // Replace hyphens with underscores for the key lookup
+ networkKey := strings.ReplaceAll(fmt.Sprintf("%s_%s", blockchain, network), "-", "_")
+
+ // Extract network-specific values from flat secret
+ dbName, err := getStringFromSecret(secretData, fmt.Sprintf("%s_database_name", networkKey))
+ if err != nil {
+ return xerrors.Errorf("failed to get database name: %w", err)
+ }
+
+ var username, password string
+ if role == "worker" {
+ username, err = getStringFromSecret(secretData, fmt.Sprintf("%s_worker_username", networkKey))
+ if err != nil {
+ return xerrors.Errorf("failed to get worker username: %w", err)
+ }
+ password, err = getStringFromSecret(secretData, fmt.Sprintf("%s_worker_password", networkKey))
+ if err != nil {
+ return xerrors.Errorf("failed to get worker password: %w", err)
+ }
+ } else {
+ username, err = getStringFromSecret(secretData, fmt.Sprintf("%s_server_username", networkKey))
+ if err != nil {
+ return xerrors.Errorf("failed to get server username: %w", err)
+ }
+ password, err = getStringFromSecret(secretData, fmt.Sprintf("%s_server_password", networkKey))
+ if err != nil {
+ return xerrors.Errorf("failed to get server password: %w", err)
+ }
+ }
+
+ if !quiet {
+ logger.Info("Successfully fetched credentials from AWS Secrets Manager",
+ zap.String("database", dbName),
+ zap.String("user", username))
+ }
+
+ // Create environment variables
+ envVars := EnvVars{
+ User: username,
+ Password: password,
+ Database: dbName,
+ Host: masterHost,
+ Port: masterPortStr,
+ SSLMode: "require", // Default SSL mode
+ }
+
+ // Set environment variables in the current process
+ if err := os.Setenv("CHAINSTORAGE_AWS_POSTGRES_USER", envVars.User); err != nil {
+ return xerrors.Errorf("failed to set CHAINSTORAGE_AWS_POSTGRES_USER: %w", err)
+ }
+ if err := os.Setenv("CHAINSTORAGE_AWS_POSTGRES_PASSWORD", envVars.Password); err != nil {
+ return xerrors.Errorf("failed to set CHAINSTORAGE_AWS_POSTGRES_PASSWORD: %w", err)
+ }
+ if err := os.Setenv("CHAINSTORAGE_AWS_POSTGRES_DATABASE", envVars.Database); err != nil {
+ return xerrors.Errorf("failed to set CHAINSTORAGE_AWS_POSTGRES_DATABASE: %w", err)
+ }
+ if err := os.Setenv("CHAINSTORAGE_AWS_POSTGRES_HOST", envVars.Host); err != nil {
+ return xerrors.Errorf("failed to set CHAINSTORAGE_AWS_POSTGRES_HOST: %w", err)
+ }
+ if err := os.Setenv("CHAINSTORAGE_AWS_POSTGRES_PORT", envVars.Port); err != nil {
+ return xerrors.Errorf("failed to set CHAINSTORAGE_AWS_POSTGRES_PORT: %w", err)
+ }
+ if err := os.Setenv("CHAINSTORAGE_AWS_POSTGRES_SSL_MODE", envVars.SSLMode); err != nil {
+ return xerrors.Errorf("failed to set CHAINSTORAGE_AWS_POSTGRES_SSL_MODE: %w", err)
+ }
+
+ // Output environment variables for reference
+ // Shell export format (for sourcing in bash)
+ fmt.Printf("export CHAINSTORAGE_AWS_POSTGRES_USER=\"%s\"\n", envVars.User)
+ fmt.Printf("export CHAINSTORAGE_AWS_POSTGRES_PASSWORD=\"%s\"\n", envVars.Password)
+ fmt.Printf("export CHAINSTORAGE_AWS_POSTGRES_DATABASE=\"%s\"\n", envVars.Database)
+ fmt.Printf("export CHAINSTORAGE_AWS_POSTGRES_HOST=\"%s\"\n", envVars.Host)
+ fmt.Printf("export CHAINSTORAGE_AWS_POSTGRES_PORT=\"%s\"\n", envVars.Port)
+ fmt.Printf("export CHAINSTORAGE_AWS_POSTGRES_SSL_MODE=\"%s\"\n", envVars.SSLMode)
+
+ if !quiet {
+ logger.Info("Environment variables set successfully",
+ zap.String("blockchain", blockchain),
+ zap.String("network", network),
+ zap.String("role", role),
+ zap.String("database", dbName),
+ zap.String("user", username))
+ }
+
+ return nil
+}
+
+func init() {
+ rootCmd.AddCommand(newEnvSetCommand())
+}
diff --git a/cmd/admin/migrate.go b/cmd/admin/migrate.go
new file mode 100644
index 00000000..2f69f30c
--- /dev/null
+++ b/cmd/admin/migrate.go
@@ -0,0 +1,727 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ awssdk "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb"
+ dynamodbtypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
+ "github.com/spf13/cobra"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/aws"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ dynamodb_storage "github.com/coinbase/chainstorage/internal/storage/metastorage/dynamodb"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/dynamodb/model"
+ postgres_storage "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+var (
+ migrateFlags struct {
+ startHeight uint64
+ endHeight uint64
+ eventTag uint32
+ tag uint32
+ batchSize int
+ miniBatchSize int
+ checkpointSize int
+ parallelism int
+ skipEvents bool
+ skipBlocks bool
+ continuousSync bool
+ syncInterval string
+ backoffInterval string
+ autoResume bool
+ }
+)
+
+var (
+ migrateCmd = &cobra.Command{
+ Use: "migrate",
+ Short: "Migrate data from DynamoDB to PostgreSQL with optional continuous sync",
+ Long: `Migrate block metadata and events from DynamoDB to PostgreSQL.
+
+Block Migration:
+- Handles reorgs by migrating non-canonical blocks first, then canonical blocks last
+- Captures complete reorg data by querying DynamoDB directly for all blocks at each height
+- Maintains canonical block identification in PostgreSQL through migration order
+
+Event Migration:
+- Uses event ID-based iteration for efficient migration
+- Gets first event ID from start height, last event ID from end height
+- Migrates events sequentially by event ID range in batches
+- Event IDs in DynamoDB correspond directly to event sequences in PostgreSQL
+
+Continuous Sync Mode:
+- Enables infinite loop mode for real-time data synchronization
+- When enabled and current batch completes:
+ - Sets new StartHeight to current EndHeight
+ - Resets EndHeight to 0 (meaning "sync to latest")
+ - Waits for SyncInterval duration
+ - Restarts migration with new parameters
+- Validation: EndHeight must be 0 OR greater than StartHeight when ContinuousSync is enabled
+
+Performance Parameters:
+- BatchSize: Number of blocks to process in each workflow batch
+- MiniBatchSize: Number of blocks to process in each activity mini-batch (for parallelism)
+- CheckpointSize: Number of blocks to process before creating a workflow checkpoint
+- Parallelism: Number of parallel workers for processing mini-batches
+
+End Height:
+- If --end-height is not provided, the tool will automatically query the latest block
+ from DynamoDB and use that as the end height (exclusive)
+- Note: Auto-detection is only available in the migrate command, not in workflow mode
+
+Note: Block metadata must be migrated before events since events reference blocks via foreign keys.
+
+Examples:
+ # Migrate blocks and events from height 1000000 to latest block (auto-detected)
+ go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=1000000 \
+ --tag=2 \
+ --event-tag=3
+
+ # Migrate specific height range with custom batch sizes
+ go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=100 \
+ --end-height=152 \
+ --tag=2 \
+ --event-tag=3 \
+ --batch-size=50 \
+ --mini-batch-size=10 \
+ --parallelism=4
+
+ # Continuous sync mode - syncs continuously with 30 second intervals
+ go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=1000000 \
+ --tag=2 \
+ --event-tag=3 \
+ --continuous-sync \
+ --sync-interval=30s \
+ --batch-size=100 \
+ --mini-batch-size=20 \
+ --parallelism=2
+
+ # Migrate blocks only (skip events)
+ go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=1000000 \
+ --end-height=1001000 \
+ --tag=2 \
+ --event-tag=3 \
+ --skip-events
+
+ # Migrate events only (requires blocks to exist first)
+ go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=1000000 \
+ --end-height=1001000 \
+ --tag=2 \
+ --event-tag=3 \
+ --skip-blocks \
+ --backoff-interval=1s
+
+ # High throughput migration with checkpoints
+ go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --start-height=1000000 \
+ --end-height=2000000 \
+ --tag=2 \
+ --event-tag=3 \
+ --batch-size=1000 \
+ --mini-batch-size=100 \
+ --checkpoint-size=10000 \
+ --parallelism=8
+
+ # Auto-resume from where previous migration left off
+ go run cmd/admin/*.go migrate \
+ --env=local \
+ --blockchain=ethereum \
+ --network=mainnet \
+ --auto-resume \
+ --tag=2 \
+ --event-tag=3`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ // Validate flag combinations
+ if !migrateFlags.autoResume && migrateFlags.startHeight == 0 {
+ return xerrors.New("start-height is required unless --auto-resume is enabled")
+ }
+ if migrateFlags.autoResume && migrateFlags.startHeight != 0 {
+ return xerrors.New("cannot specify both --auto-resume and --start-height (auto-resume will determine start height)")
+ }
+
+ var deps struct {
+ fx.In
+ Config *config.Config
+ AWSConfig awssdk.Config
+ Params fxparams.Params
+ }
+
+ app := startApp(
+ aws.Module,
+ storage.Module,
+ fx.Populate(&deps),
+ )
+ defer func() {
+ app.Close()
+ }()
+
+ // Create DynamoDB storage directly
+ dynamoDBParams := dynamodb_storage.Params{
+ Params: deps.Params,
+ AWSConfig: deps.AWSConfig,
+ }
+ sourceResult, err := dynamodb_storage.NewMetaStorage(dynamoDBParams)
+ if err != nil {
+ return xerrors.Errorf("failed to create DynamoDB storage: %w", err)
+ }
+
+ // Create PostgreSQL storage directly
+ postgresParams := postgres_storage.Params{
+ Params: deps.Params,
+ }
+ destResult, err := postgres_storage.NewMetaStorage(postgresParams)
+ if err != nil {
+ return xerrors.Errorf("failed to create PostgreSQL storage: %w", err)
+ }
+
+ // Note: Validation will happen after end height auto-detection
+
+ if migrateFlags.batchSize <= 0 {
+ migrateFlags.batchSize = 100
+ }
+
+ if migrateFlags.miniBatchSize <= 0 {
+ migrateFlags.miniBatchSize = migrateFlags.batchSize / 10
+ if migrateFlags.miniBatchSize <= 0 {
+ migrateFlags.miniBatchSize = 10
+ }
+ }
+
+ if migrateFlags.checkpointSize <= 0 {
+ migrateFlags.checkpointSize = 10000
+ }
+
+ if migrateFlags.parallelism <= 0 {
+ migrateFlags.parallelism = 1
+ }
+
+ // Both skip flags cannot be true
+ if migrateFlags.skipEvents && migrateFlags.skipBlocks {
+ return xerrors.New("cannot skip both events and blocks - nothing to migrate")
+ }
+
+ // Validate continuous sync parameters
+ if migrateFlags.continuousSync {
+ logger.Warn("WARNING: Continuous sync is not supported in direct migration mode")
+ logger.Warn("Continuous sync is only available when using the migrator workflow")
+ logger.Warn("This tool will perform a one-time migration and exit")
+
+ if migrateFlags.endHeight != 0 && migrateFlags.endHeight <= migrateFlags.startHeight {
+ return xerrors.Errorf("with continuous sync enabled, end height (%d) must be 0 OR greater than start height (%d)",
+ migrateFlags.endHeight, migrateFlags.startHeight)
+ }
+ }
+
+ // Warn about skip-blocks requirements
+ if migrateFlags.skipBlocks && !migrateFlags.skipEvents {
+ logger.Warn("IMPORTANT: Using --skip-blocks (events-only migration)")
+ logger.Warn("Block metadata MUST already exist in PostgreSQL for the specified height range")
+ logger.Warn("If block metadata is missing, the migration will fail with foreign key errors")
+ logger.Warn("To fix: First migrate blocks with --skip-events, then migrate events with --skip-blocks")
+
+ prompt := "Are you sure block metadata already exists in PostgreSQL for this range? (y/N): "
+ if !confirm(prompt) {
+ logger.Info("Migration cancelled - migrate blocks first with --skip-events")
+ return nil
+ }
+ }
+
+ ctx := context.Background()
+
+ // Handle auto-resume functionality
+ if migrateFlags.autoResume && migrateFlags.startHeight == 0 {
+ logger.Info("AutoResume enabled, querying PostgreSQL destination for latest migrated block")
+ latestBlock, err := destResult.MetaStorage.GetLatestBlock(ctx, migrateFlags.tag)
+ if err != nil {
+ // Check if it's a "not found" error, which means no blocks migrated yet
+ errStr := strings.ToLower(err.Error())
+ if strings.Contains(errStr, "not found") || strings.Contains(errStr, "no rows") {
+ logger.Info("Auto-resume: no blocks found in PostgreSQL destination, starting from beginning")
+ migrateFlags.startHeight = 0
+ } else {
+ return xerrors.Errorf("failed to get latest block height from PostgreSQL: %w", err)
+ }
+ } else {
+ // Resume from the next block after the latest migrated block
+ migrateFlags.startHeight = latestBlock.Height + 1
+ logger.Info("Auto-resume: found latest block in PostgreSQL destination",
+ zap.Uint64("latestHeight", latestBlock.Height),
+ zap.Uint64("resumeFromHeight", migrateFlags.startHeight))
+ }
+ }
+
+ // Handle end height - if not provided, query latest block from DynamoDB
+ if migrateFlags.endHeight == 0 {
+ logger.Info("No end height provided, querying latest block from DynamoDB...")
+
+ // Query latest block from DynamoDB
+ latestBlock, err := sourceResult.MetaStorage.GetLatestBlock(ctx, migrateFlags.tag)
+ if err != nil {
+ return xerrors.Errorf("failed to get latest block from DynamoDB: %w", err)
+ }
+
+ migrateFlags.endHeight = latestBlock.Height + 1 // Make it exclusive
+ logger.Info("Found latest block in DynamoDB",
+ zap.Uint64("latestHeight", latestBlock.Height),
+ zap.Uint64("endHeight", migrateFlags.endHeight),
+ zap.String("latestHash", latestBlock.Hash))
+ }
+
+ // Validate flags after end height auto-detection and auto-resume
+ if !migrateFlags.continuousSync && migrateFlags.startHeight >= migrateFlags.endHeight {
+ // Special case: if auto-resume found we're already caught up
+ if migrateFlags.autoResume {
+ logger.Info("Auto-resume detected: already caught up, no migration needed",
+ zap.Uint64("startHeight", migrateFlags.startHeight),
+ zap.Uint64("endHeight", migrateFlags.endHeight))
+ return nil // Successfully completed with no work to do
+ }
+ return xerrors.Errorf("startHeight (%d) must be less than endHeight (%d)",
+ migrateFlags.startHeight, migrateFlags.endHeight)
+ }
+ // Create DynamoDB client for direct queries
+ dynamoClient := dynamodb.NewFromConfig(deps.AWSConfig)
+ blockTable := deps.Config.AWS.DynamoDB.BlockTable
+
+ migrator := &DataMigrator{
+ sourceStorage: sourceResult.MetaStorage,
+ destStorage: destResult.MetaStorage,
+ config: deps.Config,
+ logger: logger,
+ dynamoClient: dynamoClient,
+ blockTable: blockTable,
+ }
+
+ // Confirmation prompt
+ prompt := fmt.Sprintf("This will migrate data from height %d to %d. Continue? (y/N): ",
+ migrateFlags.startHeight, migrateFlags.endHeight)
+ if !confirm(prompt) {
+ logger.Info("Migration cancelled")
+ return nil
+ }
+
+ migrateParams := MigrationParams{
+ StartHeight: migrateFlags.startHeight,
+ EndHeight: migrateFlags.endHeight,
+ EventTag: migrateFlags.eventTag,
+ Tag: migrateFlags.tag,
+ BatchSize: migrateFlags.batchSize,
+ MiniBatchSize: migrateFlags.miniBatchSize,
+ CheckpointSize: migrateFlags.checkpointSize,
+ Parallelism: migrateFlags.parallelism,
+ SkipEvents: migrateFlags.skipEvents,
+ SkipBlocks: migrateFlags.skipBlocks,
+ ContinuousSync: migrateFlags.continuousSync,
+ SyncInterval: migrateFlags.syncInterval,
+ BackoffInterval: migrateFlags.backoffInterval,
+ AutoResume: migrateFlags.autoResume,
+ }
+
+ return migrator.Migrate(ctx, migrateParams)
+ },
+ }
+)
+
+type MigrationParams struct {
+ StartHeight uint64
+ EndHeight uint64
+ EventTag uint32
+ Tag uint32
+ BatchSize int
+ MiniBatchSize int
+ CheckpointSize int
+ Parallelism int
+ SkipEvents bool
+ SkipBlocks bool
+ ContinuousSync bool
+ SyncInterval string
+ BackoffInterval string
+ AutoResume bool
+}
+
+type DataMigrator struct {
+ sourceStorage metastorage.MetaStorage
+ destStorage metastorage.MetaStorage
+ config *config.Config
+ logger *zap.Logger
+ // Direct DynamoDB access for querying all blocks
+ dynamoClient *dynamodb.Client
+ blockTable string
+}
+
+func (m *DataMigrator) Migrate(ctx context.Context, params MigrationParams) error {
+ m.logger.Info("Starting migration",
+ zap.Uint64("startHeight", params.StartHeight),
+ zap.Uint64("endHeight", params.EndHeight),
+ zap.Bool("skipBlocks", params.SkipBlocks),
+ zap.Bool("skipEvents", params.SkipEvents))
+
+ startTime := time.Now()
+
+ // Phase 1: Migrate block metadata FIRST (required for foreign key references)
+ if !params.SkipBlocks {
+ if err := m.migrateBlocksPerHeight(ctx, params); err != nil {
+ return xerrors.Errorf("failed to migrate blocks: %w", err)
+ }
+ }
+
+ // Phase 2: Migrate events AFTER blocks (depends on block metadata foreign keys)
+ if !params.SkipEvents {
+ if err := m.migrateEvents(ctx, params); err != nil {
+ return xerrors.Errorf("failed to migrate events: %w", err)
+ }
+ }
+
+ duration := time.Since(startTime)
+ m.logger.Info("Migration completed successfully",
+ zap.Duration("duration", duration),
+ zap.Uint64("heightRange", params.EndHeight-params.StartHeight))
+
+ return nil
+}
+
+func (m *DataMigrator) migrateBlocksPerHeight(ctx context.Context, params MigrationParams) error {
+ m.logger.Info("Starting height-by-height block metadata migration with complete reorg support")
+
+ totalHeights := params.EndHeight - params.StartHeight
+ processedHeights := uint64(0)
+ totalNonCanonicalBlocks := 0
+
+ for height := params.StartHeight; height < params.EndHeight; height++ {
+ nonCanonicalCount, err := m.migrateBlocksAtHeight(ctx, params, height)
+ if err != nil {
+ return xerrors.Errorf("failed to migrate blocks at height %d: %w", height, err)
+ }
+
+ totalNonCanonicalBlocks += nonCanonicalCount
+ processedHeights++
+
+ // Progress logging every 100 heights
+ if processedHeights%100 == 0 {
+ percentage := float64(processedHeights) / float64(totalHeights) * 100
+ m.logger.Info("Block migration progress",
+ zap.Uint64("processed", processedHeights),
+ zap.Uint64("total", totalHeights),
+ zap.Float64("percentage", percentage),
+ zap.Int("totalNonCanonicalBlocks", totalNonCanonicalBlocks))
+ }
+ }
+
+ m.logger.Info("Height-by-height block metadata migration completed",
+ zap.Int("totalNonCanonicalBlocks", totalNonCanonicalBlocks))
+ return nil
+}
+
+func (m *DataMigrator) migrateBlocksAtHeight(ctx context.Context, params MigrationParams, height uint64) (int, error) {
+ blockPid := fmt.Sprintf("%d-%d", params.Tag, height)
+
+ // Phase 1: Get and persist non-canonical blocks first
+ // Query: BlockPid = "{tag}-{height}" AND BlockRid != "canonical"
+ nonCanonicalBlocks, err := m.getNonCanonicalBlocksAtHeight(ctx, blockPid)
+ if err != nil && !xerrors.Is(err, storage.ErrItemNotFound) {
+ return 0, xerrors.Errorf("failed to get non-canonical blocks at height %d: %w", height, err)
+ }
+
+ nonCanonicalCount := len(nonCanonicalBlocks)
+ if nonCanonicalCount > 0 {
+ m.logger.Debug("Found non-canonical (reorg) blocks at height",
+ zap.Uint64("height", height),
+ zap.Int("count", nonCanonicalCount))
+
+ // Persist non-canonical blocks FIRST
+ err = m.destStorage.PersistBlockMetas(ctx, false, nonCanonicalBlocks, nil)
+ if err != nil {
+ return 0, xerrors.Errorf("failed to persist non-canonical blocks at height %d: %w", height, err)
+ }
+ }
+
+ // Phase 2: Get and persist canonical block LAST
+ // Query: BlockPid = "{tag}-{height}" AND BlockRid = "canonical"
+ canonicalBlock, err := m.getCanonicalBlockAtHeight(ctx, blockPid)
+ if err != nil {
+ if xerrors.Is(err, storage.ErrItemNotFound) {
+ m.logger.Debug("No canonical block found at height", zap.Uint64("height", height))
+ return nonCanonicalCount, nil
+ }
+ return 0, xerrors.Errorf("failed to get canonical block at height %d: %w", height, err)
+ }
+
+ m.logger.Debug("Found canonical block at height",
+ zap.Uint64("height", height),
+ zap.String("hash", canonicalBlock.Hash),
+ zap.Int("reorgBlockCount", nonCanonicalCount))
+
+ // Persist canonical block LAST - this ensures it becomes canonical in PostgreSQL
+ err = m.destStorage.PersistBlockMetas(ctx, true, []*api.BlockMetadata{canonicalBlock}, nil)
+ if err != nil {
+ return 0, xerrors.Errorf("failed to persist canonical block at height %d: %w", height, err)
+ }
+
+ return nonCanonicalCount, nil
+}
+
+func (m *DataMigrator) getNonCanonicalBlocksAtHeight(ctx context.Context, blockPid string) ([]*api.BlockMetadata, error) {
+ // Query DynamoDB for ALL blocks at this height: BlockPid = blockPid
+ // Then filter out the canonical one client-side
+ input := &dynamodb.QueryInput{
+ TableName: awssdk.String(m.blockTable),
+ KeyConditionExpression: awssdk.String("block_pid = :blockPid"),
+ ExpressionAttributeValues: map[string]dynamodbtypes.AttributeValue{
+ ":blockPid": &dynamodbtypes.AttributeValueMemberS{Value: blockPid},
+ },
+ ConsistentRead: awssdk.Bool(true),
+ }
+
+ result, err := m.dynamoClient.Query(ctx, input)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to query blocks at height: %w", err)
+ }
+
+ // Filter out canonical blocks client-side
+ var nonCanonicalBlocks []*api.BlockMetadata
+ for _, item := range result.Items {
+ var blockEntry model.BlockMetaDataDDBEntry
+ err := attributevalue.UnmarshalMap(item, &blockEntry)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal DynamoDB item: %w", err)
+ }
+
+ // Skip canonical blocks (BlockRid = "canonical")
+ if blockEntry.BlockRid == "canonical" {
+ continue
+ }
+
+ nonCanonicalBlocks = append(nonCanonicalBlocks, model.BlockMetadataToProto(&blockEntry))
+ }
+
+ return nonCanonicalBlocks, nil
+}
+
+func (m *DataMigrator) getCanonicalBlockAtHeight(ctx context.Context, blockPid string) (*api.BlockMetadata, error) {
+ // Query DynamoDB directly: BlockPid = blockPid AND BlockRid = "canonical"
+ input := &dynamodb.QueryInput{
+ TableName: awssdk.String(m.blockTable),
+ KeyConditionExpression: awssdk.String("block_pid = :blockPid AND block_rid = :canonical"),
+ ExpressionAttributeValues: map[string]dynamodbtypes.AttributeValue{
+ ":blockPid": &dynamodbtypes.AttributeValueMemberS{Value: blockPid},
+ ":canonical": &dynamodbtypes.AttributeValueMemberS{Value: "canonical"},
+ },
+ ConsistentRead: awssdk.Bool(true),
+ }
+
+ result, err := m.dynamoClient.Query(ctx, input)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to query canonical block: %w", err)
+ }
+
+ if len(result.Items) == 0 {
+ return nil, storage.ErrItemNotFound
+ }
+
+ if len(result.Items) > 1 {
+ return nil, xerrors.Errorf("multiple canonical blocks found for %s", blockPid)
+ }
+
+ var blockEntry model.BlockMetaDataDDBEntry
+ err = attributevalue.UnmarshalMap(result.Items[0], &blockEntry)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal canonical block: %w", err)
+ }
+
+ return model.BlockMetadataToProto(&blockEntry), nil
+}
+
+func (m *DataMigrator) migrateEvents(ctx context.Context, params MigrationParams) error {
+ m.logger.Info("Starting event ID-based migration")
+
+ // Step 1: Get the first event ID at start height
+ startEventId, err := m.sourceStorage.GetFirstEventIdByBlockHeight(ctx, params.EventTag, params.StartHeight)
+ if err != nil {
+ if xerrors.Is(err, storage.ErrItemNotFound) {
+ m.logger.Info("No events found at start height", zap.Uint64("startHeight", params.StartHeight))
+ return nil
+ }
+ return xerrors.Errorf("failed to get first event ID at start height %d: %w", params.StartHeight, err)
+ }
+
+ // Step 2: Find the last event ID within the height range [startHeight, endHeight)
+ var endEventId int64
+ if params.EndHeight > params.StartHeight {
+ endEventId, err = m.findLastEventIdInRange(ctx, params.EventTag, params.StartHeight, params.EndHeight)
+ if err != nil {
+ return xerrors.Errorf("failed to find last event ID in range [%d, %d): %w", params.StartHeight, params.EndHeight, err)
+ }
+
+ // If no events found in the range beyond startEventId, just process starting event
+ if endEventId < startEventId {
+ endEventId = startEventId
+ }
+ } else {
+ endEventId = startEventId
+ }
+
+ m.logger.Info("Event ID range determined",
+ zap.Int64("startEventId", startEventId),
+ zap.Int64("endEventId", endEventId),
+ zap.Int64("totalEvents", endEventId-startEventId+1))
+
+ if endEventId < startEventId {
+ m.logger.Info("No events to migrate (end event ID < start event ID)")
+ return nil
+ }
+
+ // Step 3: Migrate events by event ID range in batches
+ totalEvents := endEventId - startEventId + 1
+ processedEvents := int64(0)
+ batchSize := int64(params.BatchSize)
+
+ for currentEventId := startEventId; currentEventId <= endEventId; currentEventId += batchSize {
+ // Calculate the end of this batch
+ batchEndEventId := currentEventId + batchSize - 1
+ if batchEndEventId > endEventId {
+ batchEndEventId = endEventId
+ }
+
+ // Get events in this range from DynamoDB
+ sourceEvents, err := m.sourceStorage.GetEventsByEventIdRange(ctx, params.EventTag, currentEventId, batchEndEventId+1)
+ if err != nil {
+ if xerrors.Is(err, storage.ErrItemNotFound) {
+ m.logger.Debug("No events found in event ID range",
+ zap.Int64("startEventId", currentEventId),
+ zap.Int64("endEventId", batchEndEventId))
+ processedEvents += batchEndEventId - currentEventId + 1
+ continue
+ }
+ return xerrors.Errorf("failed to get events in range [%d, %d]: %w", currentEventId, batchEndEventId, err)
+ }
+
+ if len(sourceEvents) == 0 {
+ processedEvents += batchEndEventId - currentEventId + 1
+ continue
+ }
+
+ m.logger.Debug("Migrating event batch",
+ zap.Int("count", len(sourceEvents)),
+ zap.Int64("startEventId", currentEventId),
+ zap.Int64("endEventId", batchEndEventId))
+
+ // Migrate this batch to PostgreSQL
+ err = m.destStorage.AddEventEntries(ctx, params.EventTag, sourceEvents)
+ if err != nil {
+ return xerrors.Errorf("failed to add events batch [%d, %d] to PostgreSQL: %w", currentEventId, batchEndEventId, err)
+ }
+
+ processedEvents += int64(len(sourceEvents))
+
+ // Progress logging every 1000 events
+ if processedEvents%1000 == 0 || processedEvents == totalEvents {
+ percentage := float64(processedEvents) / float64(totalEvents) * 100
+ m.logger.Info("Event migration progress",
+ zap.Int64("processed", processedEvents),
+ zap.Int64("total", totalEvents),
+ zap.Float64("percentage", percentage))
+ }
+ }
+
+ m.logger.Info("Event ID-based migration completed",
+ zap.Int64("totalEventsMigrated", processedEvents))
+ return nil
+}
+
+// findLastEventIdInRange finds the maximum event ID within the specified height range
+// by searching backwards from endHeight-1 until an event is found or reaching startHeight
+func (m *DataMigrator) findLastEventIdInRange(ctx context.Context, eventTag uint32, startHeight, endHeight uint64) (int64, error) {
+ m.logger.Debug("Finding last event ID in height range",
+ zap.Uint64("startHeight", startHeight),
+ zap.Uint64("endHeight", endHeight))
+
+ // Search backwards from endHeight-1 to startHeight to find the last event
+ for height := endHeight - 1; height >= startHeight; height-- {
+ events, err := m.sourceStorage.GetEventsByBlockHeight(ctx, eventTag, height)
+ if err != nil {
+ if xerrors.Is(err, storage.ErrItemNotFound) {
+ // No events at this height, continue searching backwards
+ m.logger.Debug("No events found at height", zap.Uint64("height", height))
+ continue
+ }
+ return 0, xerrors.Errorf("failed to get events at height %d: %w", height, err)
+ }
+
+ // Find the maximum event ID at this height
+ var maxEventId int64 = -1
+ for _, event := range events {
+ if event.EventId > maxEventId {
+ maxEventId = event.EventId
+ }
+ }
+
+ if maxEventId >= 0 {
+ m.logger.Debug("Found last event in range",
+ zap.Uint64("height", height),
+ zap.Int64("eventId", maxEventId))
+ return maxEventId, nil
+ }
+ }
+
+ // No events found in the entire range
+ m.logger.Debug("No events found in the specified height range")
+ return -1, storage.ErrItemNotFound
+}
+
+func init() {
+ migrateCmd.Flags().Uint64Var(&migrateFlags.startHeight, "start-height", 0, "start block height (inclusive)")
+ migrateCmd.Flags().Uint64Var(&migrateFlags.endHeight, "end-height", 0, "end block height (exclusive, optional - if not provided, will query latest block from DynamoDB)")
+ migrateCmd.Flags().Uint32Var(&migrateFlags.eventTag, "event-tag", 0, "event tag for migration")
+ migrateCmd.Flags().Uint32Var(&migrateFlags.tag, "tag", 1, "block tag for migration")
+ migrateCmd.Flags().IntVar(&migrateFlags.batchSize, "batch-size", 100, "number of blocks to process in each workflow batch")
+ migrateCmd.Flags().IntVar(&migrateFlags.miniBatchSize, "mini-batch-size", 0, "number of blocks to process in each activity mini-batch (default: batch-size/10)")
+ migrateCmd.Flags().IntVar(&migrateFlags.checkpointSize, "checkpoint-size", 10000, "number of blocks to process before creating a workflow checkpoint")
+ migrateCmd.Flags().IntVar(&migrateFlags.parallelism, "parallelism", 1, "number of parallel workers for processing mini-batches")
+ migrateCmd.Flags().BoolVar(&migrateFlags.skipEvents, "skip-events", false, "skip event migration (blocks only)")
+ migrateCmd.Flags().BoolVar(&migrateFlags.skipBlocks, "skip-blocks", false, "skip block migration (events only)")
+ migrateCmd.Flags().BoolVar(&migrateFlags.continuousSync, "continuous-sync", false, "enable continuous sync mode (infinite loop, workflow only)")
+ migrateCmd.Flags().StringVar(&migrateFlags.syncInterval, "sync-interval", "1m", "time duration to wait between continuous sync cycles (e.g., '1m', '30s')")
+ migrateCmd.Flags().StringVar(&migrateFlags.backoffInterval, "backoff-interval", "", "time duration to wait between batches (e.g., '1s', '500ms')")
+ migrateCmd.Flags().BoolVar(&migrateFlags.autoResume, "auto-resume", false, "automatically determine start height from latest block in PostgreSQL destination")
+
+ // start-height is required unless auto-resume is enabled
+ // end-height is optional - if not provided, will query latest block from DynamoDB
+
+ rootCmd.AddCommand(migrateCmd)
+}
diff --git a/cmd/admin/postgres.go b/cmd/admin/postgres.go
new file mode 100644
index 00000000..a76d924a
--- /dev/null
+++ b/cmd/admin/postgres.go
@@ -0,0 +1,199 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/spf13/cobra"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+)
+
+const (
+ masterUserFlag = "master-user"
+ masterPassFlag = "master-password"
+ hostFlag = "host"
+ portFlag = "port"
+ workerUserFlag = "worker-user"
+ workerPassFlag = "worker-password"
+ serverUserFlag = "server-user"
+ serverPassFlag = "server-password"
+ sslModeFlag = "ssl-mode"
+ dbNameFlag = "db-name"
+ connectTimeoutFlag = "connect-timeout"
+)
+
+func newPostgresCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "setup-postgres",
+ Short: "Create database and roles for a new network in PostgreSQL.",
+ Long: `Create database and roles for a new network in PostgreSQL.
+
+This command connects to PostgreSQL using master/admin credentials and creates:
+1. A worker role (read/write permissions)
+2. A server role (read-only permissions)
+3. A database owned by the worker role
+4. Proper permissions for both roles
+
+Example usage:
+ # Set up database for ethereum-mainnet
+ chainstorage admin setup-postgres --blockchain ethereum --network mainnet --env local --master-user postgres --master-password mypassword
+
+ # Set up database with custom name
+ chainstorage admin setup-postgres --blockchain ethereum --network mainnet --env local --db-name my_custom_db --master-user admin --master-password secret
+
+ # Use with custom PostgreSQL instance
+ chainstorage admin setup-postgres --blockchain bitcoin --network mainnet --env development --host mydb.example.com --port 5433 --ssl-mode require`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ app := startApp()
+ defer app.Close()
+
+ // Parse all flags
+ masterUser, err := cmd.Flags().GetString(masterUserFlag)
+ if err != nil {
+ return err
+ }
+ masterPassword, err := cmd.Flags().GetString(masterPassFlag)
+ if err != nil {
+ return err
+ }
+ host, err := cmd.Flags().GetString(hostFlag)
+ if err != nil {
+ return err
+ }
+ port, err := cmd.Flags().GetInt(portFlag)
+ if err != nil {
+ return err
+ }
+ workerUser, err := cmd.Flags().GetString(workerUserFlag)
+ if err != nil {
+ return err
+ }
+ workerPassword, err := cmd.Flags().GetString(workerPassFlag)
+ if err != nil {
+ return err
+ }
+ serverUser, err := cmd.Flags().GetString(serverUserFlag)
+ if err != nil {
+ return err
+ }
+ serverPassword, err := cmd.Flags().GetString(serverPassFlag)
+ if err != nil {
+ return err
+ }
+ sslMode, err := cmd.Flags().GetString(sslModeFlag)
+ if err != nil {
+ return err
+ }
+ dbName, err := cmd.Flags().GetString(dbNameFlag)
+ if err != nil {
+ return err
+ }
+ connectTimeout, err := cmd.Flags().GetDuration(connectTimeoutFlag)
+ if err != nil {
+ return err
+ }
+
+ // Validation
+ if masterUser == "" {
+ return xerrors.New("master-user is required")
+ }
+ if masterPassword == "" {
+ return xerrors.New("master-password is required")
+ }
+ if host == "" {
+ return xerrors.New("host is required")
+ }
+ if port <= 0 || port > 65535 {
+ return xerrors.New("port must be between 1 and 65535")
+ }
+ if workerUser == "" {
+ return xerrors.New("worker-user is required")
+ }
+ if workerPassword == "" {
+ return xerrors.New("worker-password is required")
+ }
+ if serverUser == "" {
+ return xerrors.New("server-user is required")
+ }
+ if serverPassword == "" {
+ return xerrors.New("server-password is required")
+ }
+ if workerUser == serverUser {
+ return xerrors.New("worker-user and server-user must be different")
+ }
+
+ // Determine database name using global blockchain and network flags
+ if dbName == "" {
+ // Use global flags from common.go (commonFlags.blockchain and commonFlags.network)
+ // e.g., blockchain="ethereum", network="mainnet" -> "chainstorage_ethereum_mainnet"
+ dbName = fmt.Sprintf("chainstorage_%s_%s", commonFlags.blockchain, commonFlags.network)
+ // Replace hyphens with underscores for valid database name
+ dbName = replaceHyphensWithUnderscores(dbName)
+ }
+
+ // Build master config
+ masterCfg := &config.PostgresConfig{
+ Host: host,
+ Port: port,
+ Database: "postgres", // Always connect to postgres database first
+ User: masterUser,
+ Password: masterPassword,
+ SSLMode: sslMode,
+ ConnectTimeout: connectTimeout,
+ }
+
+ fmt.Printf("🚀 Setting up PostgreSQL for chainstorage...\n")
+ fmt.Printf(" Database: %s\n", dbName)
+ fmt.Printf(" Worker role: %s\n", workerUser)
+ fmt.Printf(" Server role: %s\n", serverUser)
+ fmt.Printf(" Host: %s:%d\n", host, port)
+ fmt.Printf(" Blockchain: %s\n", commonFlags.blockchain)
+ fmt.Printf(" Network: %s\n", commonFlags.network)
+ fmt.Printf(" Environment: %s\n", commonFlags.env)
+ fmt.Printf("\n")
+
+ return postgres.SetupDatabase(context.Background(), masterCfg, workerUser, workerPassword, serverUser, serverPassword, dbName)
+ },
+ }
+
+ // Define flags with reasonable defaults
+ cmd.Flags().String(masterUserFlag, "postgres", "Master/admin user for PostgreSQL")
+ cmd.Flags().String(masterPassFlag, "", "Master/admin password for PostgreSQL")
+ cmd.Flags().String(hostFlag, "localhost", "PostgreSQL host")
+ cmd.Flags().Int(portFlag, 5432, "PostgreSQL port")
+ cmd.Flags().String(workerUserFlag, "chainstorage_worker", "Name for the read/write worker role")
+ cmd.Flags().String(workerPassFlag, "", "Password for the worker role")
+ cmd.Flags().String(serverUserFlag, "chainstorage_server", "Name for the read-only server role")
+ cmd.Flags().String(serverPassFlag, "", "Password for the server role")
+ cmd.Flags().String(dbNameFlag, "", "Directly specify the database name to create (overrides default naming)")
+ cmd.Flags().String(sslModeFlag, "disable", "PostgreSQL SSL mode (disable, require, verify-ca, verify-full)")
+ cmd.Flags().Duration(connectTimeoutFlag, 30*time.Second, "PostgreSQL connection timeout")
+
+ // Mark required flags
+ if err := cmd.MarkFlagRequired(masterPassFlag); err != nil {
+ return nil
+ }
+ if err := cmd.MarkFlagRequired(workerPassFlag); err != nil {
+ return nil
+ }
+ if err := cmd.MarkFlagRequired(serverPassFlag); err != nil {
+ return nil
+ }
+
+ return cmd
+}
+
+// replaceHyphensWithUnderscores converts network names like "ethereum-mainnet" to "ethereum_mainnet"
+// for valid PostgreSQL database naming
+func replaceHyphensWithUnderscores(s string) string {
+ return strings.ReplaceAll(s, "-", "_")
+}
+
+func init() {
+ rootCmd.AddCommand(newPostgresCommand())
+}
diff --git a/cmd/admin/retirement.go b/cmd/admin/retirement.go
new file mode 100644
index 00000000..9f0d34f5
--- /dev/null
+++ b/cmd/admin/retirement.go
@@ -0,0 +1,352 @@
+package main
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "os"
+ "strings"
+ "time"
+
+ _ "github.com/lib/pq"
+ "github.com/spf13/cobra"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/aws"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/s3"
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+)
+
+type retirementFlags struct {
+ tag uint32
+ startHeight uint64
+ endHeight uint64
+ limit uint64
+ approveChain string
+ approveStartHeight uint64
+ approveEndHeight uint64
+ directStorageClientsGuarded bool
+ singleBlockWritersGuarded bool
+ fallbackErrorCount uint64
+ execute bool
+ confirmProductionDelete bool
+ reportFile string
+}
+
+var (
+ singleBlockRetirementFlags retirementFlags
+ singleBlockRetirementReconcileFlags retirementFlags
+)
+
+func newRetirementCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "retirement",
+ Short: "plan and execute guarded storage retirement operations",
+ }
+ cmd.AddCommand(newSingleBlockRetirementPlanCommand())
+ cmd.AddCommand(newSingleBlockRetirementReconcileCommand())
+ return cmd
+}
+
+func newSingleBlockRetirementPlanCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "plan-single-blocks",
+ Short: "dry-run single-block object retirement after CSCB validation",
+ Long: `Plan retirement for single-block S3 objects whose active metadata has already been promoted to validated CSCB metadata.
+
+The command is dry-run by default. It emits one auditable JSON report containing the bucket,
+single-block key, version id when available, height, hash, single-block bytes, consolidated key,
+validated_at, retired_at, eligible_at, action, and skip reason for every scanned canonical row.
+
+Execution persists a write-ahead manifest, independently parses and compares the pinned single-block
+version with the pinned CSCB payload, revalidates immediately before deleting exactly one S3
+version, verifies the key has no remaining versions, transactionally clears the single-block path,
+then performs a fresh CSCB range read before marking the retirement verified.
+The command also verifies the live bucket policy denies every unconditional write and every API
+delete to each CSCB key, so no newer version or delete marker can replace the pinned payload.
+Chainstorage API and SDK clients remain compatible with CSCB storage. Execution requires
+--direct-storage-clients-guarded only for consumers that bypass Chainstorage and read object
+storage directly, plus --single-block-writers-guarded to confirm every live single-block writer
+honors the retirement fence. Production execution additionally requires --confirm-production-delete.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runSingleBlockRetirementPlan(cmd.Context(), singleBlockRetirementFlags)
+ },
+ }
+
+ addSingleBlockRetirementFlags(cmd, &singleBlockRetirementFlags)
+ return cmd
+}
+
+func newSingleBlockRetirementReconcileCommand() *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "reconcile-single-blocks",
+ Short: "inspect or resume durable single-block object retirements",
+ Long: `Inspect write-ahead retirement manifests left in eligible, deleting, or deleted-pending-verification state.
+
+The command is report-only by default. With explicit execution gates, it can safely resume a
+pre-delete manifest, record an already-completed S3 deletion while clearing the single-block path, or
+finish fresh CSCB verification against the persisted digest after the path has been cleared.
+Execution remains blocked unless every live single-block writer honors the retirement fence and
+the live CSCB write-once bucket policy is verifiable.`,
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return runSingleBlockRetirementReconcile(cmd.Context(), singleBlockRetirementReconcileFlags)
+ },
+ }
+ addSingleBlockRetirementFlags(cmd, &singleBlockRetirementReconcileFlags)
+ return cmd
+}
+
+func addSingleBlockRetirementFlags(cmd *cobra.Command, flags *retirementFlags) {
+ cmd.Flags().Uint32Var(&flags.tag, "tag", 0, "block tag; default zero resolves to the configured stable tag")
+ cmd.Flags().Uint64Var(&flags.startHeight, "start-height", 0, "inclusive block start height")
+ cmd.Flags().Uint64Var(&flags.endHeight, "end-height", 0, "exclusive block end height")
+ cmd.Flags().Uint64Var(&flags.limit, "limit", 0, "maximum rows to scan; default scans the full range")
+ cmd.Flags().StringVar(&flags.approveChain, "approve-chain", "", "explicit chain approval, e.g. solana-mainnet")
+ cmd.Flags().Uint64Var(&flags.approveStartHeight, "approve-start-height", 0, "explicit approved start height")
+ cmd.Flags().Uint64Var(&flags.approveEndHeight, "approve-end-height", 0, "explicit approved end height")
+ cmd.Flags().BoolVar(&flags.directStorageClientsGuarded, "direct-storage-clients-guarded", false, "confirm direct object-storage consumers are migrated or out of scope; Chainstorage API and SDK clients are compatible")
+ cmd.Flags().BoolVar(&flags.directStorageClientsGuarded, "client-migration-approved", false, "deprecated alias for --direct-storage-clients-guarded")
+ _ = cmd.Flags().MarkDeprecated("client-migration-approved", "use --direct-storage-clients-guarded; API and SDK clients do not require migration")
+ cmd.Flags().BoolVar(&flags.singleBlockWritersGuarded, "single-block-writers-guarded", false, "confirm every live single-block writer honors the retirement fence")
+ cmd.Flags().Uint64Var(&flags.fallbackErrorCount, "fallback-read-errors", 0, "active fallback/read error count from the operator's observation window")
+ cmd.Flags().BoolVar(&flags.execute, "execute", false, "execute guarded retirement state transitions and exact-version deletion")
+ cmd.Flags().BoolVar(&flags.confirmProductionDelete, "confirm-production-delete", false, "second explicit gate required with --execute in production")
+ cmd.Flags().StringVar(&flags.reportFile, "report-file", "", "write JSON report to this file instead of stdout")
+
+ _ = cmd.MarkFlagRequired("start-height")
+ _ = cmd.MarkFlagRequired("end-height")
+}
+
+func runSingleBlockRetirementPlan(ctx context.Context, flags retirementFlags) error {
+ if flags.endHeight <= flags.startHeight {
+ return xerrors.Errorf("end height must be greater than start height: start=%d end=%d", flags.startHeight, flags.endHeight)
+ }
+ if flags.execute && isProductionEnvironment(commonFlags.env) && !flags.confirmProductionDelete {
+ return xerrors.New("production execution requires --execute and --confirm-production-delete")
+ }
+ if flags.execute && !flags.singleBlockWritersGuarded {
+ return xerrors.New("retirement execution requires --single-block-writers-guarded")
+ }
+
+ var deps struct {
+ fx.In
+ S3Client s3.Client
+ }
+ app := startApp(
+ aws.Module,
+ s3.Module,
+ fx.Populate(&deps),
+ )
+ defer app.Close()
+ cfg := app.Config()
+ if cfg.StorageType.MetaStorageType != config.MetaStorageType_POSTGRES {
+ return xerrors.Errorf("single-block retirement planner requires Postgres meta storage, got %v", cfg.StorageType.MetaStorageType)
+ }
+ if cfg.StorageType.BlobStorageType != config.BlobStorageType_UNSPECIFIED && cfg.StorageType.BlobStorageType != config.BlobStorageType_S3 {
+ return xerrors.Errorf("single-block retirement planner requires S3 blob storage, got %v", cfg.StorageType.BlobStorageType)
+ }
+ if cfg.AWS.Postgres == nil {
+ return xerrors.New("postgres config is required")
+ }
+
+ tag := cfg.GetEffectiveBlockTag(flags.tag)
+ targetChain := approvalChainFromFlags()
+ logger.Info("planning single-block object retirement",
+ zap.String("environment", string(cfg.Env())),
+ zap.String("chain", targetChain),
+ zap.String("bucket", cfg.AWS.Bucket),
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", flags.startHeight),
+ zap.Uint64("end_height", flags.endHeight),
+ zap.Bool("execute", flags.execute),
+ )
+
+ db, err := openRetirementPostgres(ctx, cfg.AWS.Postgres, !flags.execute)
+ if err != nil {
+ return xerrors.Errorf("failed to open retirement postgres connection: %w", err)
+ }
+ defer func() {
+ _ = db.Close()
+ }()
+
+ planner := retirement.NewPlanner(
+ retirement.NewPostgresRepository(db),
+ retirement.NewS3ObjectStore(deps.S3Client),
+ )
+ req := retirement.PlanRequest{
+ Environment: string(cfg.Env()),
+ Blockchain: commonFlags.blockchain,
+ Network: commonFlags.network,
+ Sidechain: commonFlags.sidechain,
+ Bucket: cfg.AWS.Bucket,
+ Tag: tag,
+ StartHeight: flags.startHeight,
+ EndHeight: flags.endHeight,
+ Limit: flags.limit,
+ Now: time.Now().UTC(),
+ Execute: flags.execute,
+ ProductionDeleteEnabled: flags.confirmProductionDelete,
+ DirectStorageClientsGuarded: flags.directStorageClientsGuarded,
+ SingleBlockWritersGuarded: flags.singleBlockWritersGuarded,
+ FallbackErrorCount: flags.fallbackErrorCount,
+ Approval: retirement.Approval{
+ Chain: flags.approveChain,
+ StartHeight: flags.approveStartHeight,
+ EndHeight: flags.approveEndHeight,
+ },
+ }
+
+ report, err := planner.Plan(ctx, req)
+ if err != nil {
+ return xerrors.Errorf("failed to plan single-block retirement: %w", err)
+ }
+ if flags.execute {
+ err = planner.Apply(ctx, req, report)
+ }
+ if reportErr := writeRetirementReport(flags.reportFile, report); reportErr != nil {
+ if err != nil {
+ return xerrors.Errorf("single-block retirement failed (%v) and report write failed: %w", err, reportErr)
+ }
+ return reportErr
+ }
+ if err != nil {
+ return xerrors.Errorf("failed to execute one or more single-block retirements: %w", err)
+ }
+ return nil
+}
+
+func runSingleBlockRetirementReconcile(ctx context.Context, flags retirementFlags) error {
+ if flags.endHeight <= flags.startHeight {
+ return xerrors.Errorf("end height must be greater than start height: start=%d end=%d", flags.startHeight, flags.endHeight)
+ }
+ if flags.execute && isProductionEnvironment(commonFlags.env) && !flags.confirmProductionDelete {
+ return xerrors.New("production reconciliation requires --execute and --confirm-production-delete")
+ }
+ if flags.execute && !flags.singleBlockWritersGuarded {
+ return xerrors.New("retirement reconciliation requires --single-block-writers-guarded")
+ }
+
+ var deps struct {
+ fx.In
+ S3Client s3.Client
+ }
+ app := startApp(aws.Module, s3.Module, fx.Populate(&deps))
+ defer app.Close()
+ cfg := app.Config()
+ if cfg.StorageType.MetaStorageType != config.MetaStorageType_POSTGRES || cfg.AWS.Postgres == nil {
+ return xerrors.New("single-block retirement reconciler requires Postgres meta storage")
+ }
+ if cfg.StorageType.BlobStorageType != config.BlobStorageType_UNSPECIFIED && cfg.StorageType.BlobStorageType != config.BlobStorageType_S3 {
+ return xerrors.Errorf("single-block retirement reconciler requires S3 blob storage, got %v", cfg.StorageType.BlobStorageType)
+ }
+
+ tag := cfg.GetEffectiveBlockTag(flags.tag)
+ db, err := openRetirementPostgres(ctx, cfg.AWS.Postgres, !flags.execute)
+ if err != nil {
+ return xerrors.Errorf("failed to open retirement postgres connection: %w", err)
+ }
+ defer func() { _ = db.Close() }()
+
+ planner := retirement.NewPlanner(retirement.NewPostgresRepository(db), retirement.NewS3ObjectStore(deps.S3Client))
+ req := retirement.PlanRequest{
+ Environment: string(cfg.Env()),
+ Blockchain: commonFlags.blockchain,
+ Network: commonFlags.network,
+ Sidechain: commonFlags.sidechain,
+ Bucket: cfg.AWS.Bucket,
+ Tag: tag,
+ StartHeight: flags.startHeight,
+ EndHeight: flags.endHeight,
+ Limit: flags.limit,
+ Now: time.Now().UTC(),
+ Execute: flags.execute,
+ ProductionDeleteEnabled: flags.confirmProductionDelete,
+ DirectStorageClientsGuarded: flags.directStorageClientsGuarded,
+ SingleBlockWritersGuarded: flags.singleBlockWritersGuarded,
+ FallbackErrorCount: flags.fallbackErrorCount,
+ Approval: retirement.Approval{
+ Chain: flags.approveChain,
+ StartHeight: flags.approveStartHeight,
+ EndHeight: flags.approveEndHeight,
+ },
+ }
+ report, reconcileErr := planner.Reconcile(ctx, req)
+ if report == nil {
+ return xerrors.Errorf("failed to reconcile single-block retirements: %w", reconcileErr)
+ }
+ if reportErr := writeRetirementReport(flags.reportFile, report); reportErr != nil {
+ if reconcileErr != nil {
+ return xerrors.Errorf("single-block retirement reconciliation failed (%v) and report write failed: %w", reconcileErr, reportErr)
+ }
+ return reportErr
+ }
+ if reconcileErr != nil {
+ return xerrors.Errorf("failed to reconcile one or more single-block retirements: %w", reconcileErr)
+ }
+ return nil
+}
+
+func openRetirementPostgres(ctx context.Context, cfg *config.PostgresConfig, readOnly bool) (*sql.DB, error) {
+ dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
+ cfg.Host, cfg.Port, cfg.Database, cfg.User, cfg.Password, cfg.SSLMode)
+ if cfg.ConnectTimeout > 0 {
+ dsn += fmt.Sprintf(" connect_timeout=%d", int(cfg.ConnectTimeout.Seconds()))
+ }
+ if readOnly {
+ dsn += " options='-c default_transaction_read_only=on'"
+ }
+
+ db, err := sql.Open("postgres", dsn)
+ if err != nil {
+ return nil, err
+ }
+ if cfg.MaxConnections > 0 {
+ db.SetMaxOpenConns(cfg.MaxConnections)
+ }
+ if cfg.MinConnections > 0 {
+ db.SetMaxIdleConns(cfg.MinConnections)
+ }
+ db.SetConnMaxLifetime(cfg.MaxLifetime)
+ db.SetConnMaxIdleTime(cfg.MaxIdleTime)
+ if err := db.PingContext(ctx); err != nil {
+ _ = db.Close()
+ return nil, err
+ }
+ return db, nil
+}
+
+func writeRetirementReport(path string, report *retirement.Report) error {
+ if path == "" {
+ return retirement.WriteReportJSON(os.Stdout, report)
+ }
+ file, err := os.Create(path)
+ if err != nil {
+ return xerrors.Errorf("failed to create report file %s: %w", path, err)
+ }
+ defer func() {
+ _ = file.Close()
+ }()
+ if err := retirement.WriteReportJSON(file, report); err != nil {
+ return xerrors.Errorf("failed to write report file %s: %w", path, err)
+ }
+ return nil
+}
+
+func approvalChainFromFlags() string {
+ parts := []string{commonFlags.blockchain, commonFlags.network}
+ if commonFlags.sidechain != "" {
+ parts = append(parts, commonFlags.sidechain)
+ }
+ return strings.Join(parts, "-")
+}
+
+func isProductionEnvironment(value string) bool {
+ return strings.EqualFold(value, string(config.EnvProduction)) || strings.EqualFold(value, "prod")
+}
+
+func init() {
+ rootCmd.AddCommand(newRetirementCommand())
+}
diff --git a/cmd/admin/sdk.go b/cmd/admin/sdk.go
index ee9fef00..3c0cdcd1 100644
--- a/cmd/admin/sdk.go
+++ b/cmd/admin/sdk.go
@@ -2,6 +2,7 @@ package main
import (
"context"
+ "strings"
"github.com/coinbase/chainstorage/sdk/services"
@@ -23,6 +24,7 @@ var (
eventTag uint32
tag uint32
blockValidation bool
+ readSource string
}
sdkCmd = &cobra.Command{
@@ -33,7 +35,15 @@ var (
Use: "range",
Short: "Fetch and parse blocks in the given range.",
RunE: newSDKCommand(func(session sdk.Session) error {
- resp, err := session.Client().GetBlocksByRangeWithTag(context.Background(), sdkFlags.tag, sdkFlags.startHeight, sdkFlags.endHeight)
+ readSource, err := parseBlockReadSource(sdkFlags.readSource)
+ if err != nil {
+ return err
+ }
+ readSourceClient, err := getReadSourceClient(session.Client())
+ if err != nil {
+ return err
+ }
+ resp, err := readSourceClient.GetBlocksByRangeWithTagAndReadSource(context.Background(), sdkFlags.tag, sdkFlags.startHeight, sdkFlags.endHeight, readSource)
if err != nil {
return xerrors.Errorf("failed to get block file metadata: %w", err)
}
@@ -56,7 +66,15 @@ var (
client.SetBlockValidation(true)
}
- rawBlock, err := client.GetBlockWithTag(context.Background(), sdkFlags.tag, sdkFlags.startHeight, sdkFlags.hash)
+ readSource, err := parseBlockReadSource(sdkFlags.readSource)
+ if err != nil {
+ return err
+ }
+ readSourceClient, err := getReadSourceClient(client)
+ if err != nil {
+ return err
+ }
+ rawBlock, err := readSourceClient.GetBlockWithTagAndReadSource(context.Background(), sdkFlags.tag, sdkFlags.startHeight, sdkFlags.hash, readSource)
if err != nil {
return xerrors.Errorf("failed to get block file metadata: %w", err)
}
@@ -143,6 +161,29 @@ var (
}
)
+func getReadSourceClient(client sdk.Client) (sdk.ReadSourceClient, error) {
+ readSourceClient, ok := client.(sdk.ReadSourceClient)
+ if !ok {
+ return nil, xerrors.Errorf("sdk client %T does not support read source override", client)
+ }
+ return readSourceClient, nil
+}
+
+func parseBlockReadSource(value string) (api.BlockReadSource, error) {
+ switch strings.ToLower(value) {
+ case "", "default":
+ return api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT, nil
+ case "single-block", "single_block":
+ return api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK, nil
+ case "legacy": // Backward-compatible input; canonical help text advertises single-block only.
+ return api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK, nil
+ case "consolidated":
+ return api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED, nil
+ default:
+ return api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT, xerrors.Errorf("invalid read source %q: expected default, single-block, or consolidated", value)
+ }
+}
+
func newSDKCommand(fn func(session sdk.Session) error) func(cmd *cobra.Command, args []string) error {
return func(cmd *cobra.Command, args []string) error {
if err := initializeChainInfoFromFlags(); err != nil {
@@ -172,8 +213,10 @@ func init() {
sdkCmd.PersistentFlags().Uint64Var(&sdkFlags.endHeight, "end-height", 0, "block end height")
sdkCmd.PersistentFlags().Uint32Var(&sdkFlags.tag, "tag", 0, "block tag")
+ blocksByRangeCmd.Flags().StringVar(&sdkFlags.readSource, "read-source", "default", "block read source: default, single-block, or consolidated")
getBlockCmd.Flags().StringVar(&sdkFlags.hash, "hash", "", "block hash (optional)")
getBlockCmd.Flags().BoolVar(&sdkFlags.blockValidation, "block-validation", false, "validate block (optional)")
+ getBlockCmd.Flags().StringVar(&sdkFlags.readSource, "read-source", "default", "block read source: default, single-block, or consolidated")
streamBlockCmd.Flags().Int64Var(&sdkFlags.sequence, "sequence", 0, "last processed event sequence")
streamBlockCmd.Flags().Uint32Var(&sdkFlags.eventTag, "event-tag", 0, "event tag")
diff --git a/cmd/admin/workflow.go b/cmd/admin/workflow.go
index 274cf55f..d6b07d90 100644
--- a/cmd/admin/workflow.go
+++ b/cmd/admin/workflow.go
@@ -40,14 +40,18 @@ var (
type executors struct {
fx.In
- Backfiller *workflow.Backfiller
- Monitor *workflow.Monitor
- Poller *workflow.Poller
- Streamer *workflow.Streamer
- Benchmarker *workflow.Benchmarker
- CrossValidator *workflow.CrossValidator
- EventBackfiller *workflow.EventBackfiller
- Replicator *workflow.Replicator
+ Backfiller *workflow.Backfiller
+ Monitor *workflow.Monitor
+ Poller *workflow.Poller
+ Streamer *workflow.Streamer
+ Benchmarker *workflow.Benchmarker
+ CrossValidator *workflow.CrossValidator
+ EventBackfiller *workflow.EventBackfiller
+ Replicator *workflow.Replicator
+ Migrator *workflow.Migrator
+ BatchConsolidator *workflow.BatchConsolidator
+ SingleBlockRetention *workflow.SingleBlockRetention
+ Runtime cadence.Runtime
}
var (
@@ -72,6 +76,14 @@ var (
},
}
+ listWorkflowCmd = &cobra.Command{
+ Use: "list",
+ Short: "list running workflows",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ return listWorkflows()
+ },
+ }
+
workflowFlags struct {
workflow string
workflowID string
@@ -86,11 +98,37 @@ func init() {
workflowCmd.AddCommand(startWorkflowCmd)
workflowCmd.AddCommand(stopWorkflowCmd)
+ workflowCmd.AddCommand(listWorkflowCmd)
rootCmd.AddCommand(workflowCmd)
}
+func listWorkflows() error {
+ app, executors, err := initApp()
+ if err != nil {
+ return xerrors.Errorf("failed to init app: %w", err)
+ }
+ defer app.Close()
+ ctx := context.Background()
+ workflows, err := executors.Runtime.ListOpenWorkflows(ctx, app.Config().Cadence.Domain, 0, "") //list all workflows
+ if err != nil {
+ return xerrors.Errorf("failed to list workflows: %w", err)
+ } else {
+ logger.Info("\nlisting all workflows: ")
+ }
+ for _, workflow := range workflows.Executions { //print all workflows
+ logger.Info("\nworkflow",
+ zap.String("workflowID", workflow.Execution.GetWorkflowId()),
+ zap.String("runID", workflow.Execution.GetRunId()),
+ zap.String("type", workflow.Type.GetName()),
+ zap.Time("startTime", workflow.StartTime.AsTime()),
+ zap.String("status", workflow.Status.String()),
+ )
+ }
+ return nil
+}
func startWorkflow() error {
workflowIdentity := workflow.GetWorkflowIdentify(workflowFlags.workflow)
+ workflowId := workflowFlags.workflowID
if workflowIdentity == workflow.UnknownIdentity {
return xerrors.Errorf("invalid workflow: %v", workflowFlags.workflow)
}
@@ -101,7 +139,7 @@ func startWorkflow() error {
}
defer app.Close()
- ctx := context.Background()
+ ctx := workflow.WithWorkflowID(context.Background(), workflowId)
workflowIdentityString, err := workflowIdentity.String()
if err != nil {
return xerrors.Errorf("error parsing workflowIdentity: %w", err)
@@ -166,6 +204,24 @@ func startWorkflow() error {
return xerrors.Errorf("error converting to request type")
}
run, err = executors.Replicator.Execute(ctx, &request)
+ case workflow.MigratorIdentity:
+ request, ok := req.(workflow.MigratorRequest)
+ if !ok {
+ return xerrors.Errorf("error converting to request type")
+ }
+ run, err = executors.Migrator.Execute(ctx, &request)
+ case workflow.BatchConsolidatorIdentity:
+ request, ok := req.(workflow.BatchConsolidatorRequest)
+ if !ok {
+ return xerrors.Errorf("error converting to request type")
+ }
+ run, err = executors.BatchConsolidator.Execute(ctx, &request)
+ case workflow.SingleBlockRetentionIdentity:
+ request, ok := req.(workflow.SingleBlockRetentionRequest)
+ if !ok {
+ return xerrors.Errorf("error converting to request type")
+ }
+ run, err = executors.SingleBlockRetention.Execute(ctx, &request)
default:
return xerrors.Errorf("unsupported workflow identity: %v", workflowIdentity)
}
@@ -236,6 +292,12 @@ func stopWorkflow() error {
err = executors.EventBackfiller.StopWorkflow(ctx, workflowIdentityString, reason)
case workflow.ReplicatorIdentity:
err = executors.Replicator.StopWorkflow(ctx, workflowIdentityString, reason)
+ case workflow.MigratorIdentity:
+ err = executors.Migrator.StopWorkflow(ctx, workflowIdentityString, reason)
+ case workflow.BatchConsolidatorIdentity:
+ err = executors.BatchConsolidator.StopWorkflow(ctx, workflowIdentityString, reason)
+ case workflow.SingleBlockRetentionIdentity:
+ err = executors.SingleBlockRetention.StopWorkflow(ctx, workflowIdentityString, reason)
default:
return xerrors.Errorf("unsupported workflow identity: %v", workflowIdentity)
}
diff --git a/cmd/cron/main.go b/cmd/cron/main.go
index b44b87dc..1e376080 100644
--- a/cmd/cron/main.go
+++ b/cmd/cron/main.go
@@ -17,10 +17,12 @@ import (
"github.com/coinbase/chainstorage/internal/gateway"
"github.com/coinbase/chainstorage/internal/s3"
"github.com/coinbase/chainstorage/internal/storage"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
"github.com/coinbase/chainstorage/internal/tally"
"github.com/coinbase/chainstorage/internal/tracer"
"github.com/coinbase/chainstorage/internal/utils/consts"
"github.com/coinbase/chainstorage/internal/utils/fxparams"
+ "github.com/coinbase/chainstorage/internal/workflow"
"github.com/coinbase/chainstorage/sdk"
"github.com/coinbase/chainstorage/sdk/services"
)
@@ -32,9 +34,29 @@ func main() {
func startManager(opts ...fx.Option) services.SystemManager {
manager := services.NewManager()
- logger := manager.Logger()
ctx := manager.Context()
+ opts = cronAppOptions(manager, opts...)
+ app := fx.New(opts...)
+
+ logger := manager.Logger()
+ if err := app.Start(ctx); err != nil {
+ logger.Fatal("failed to start app", zap.Error(err))
+ }
+ manager.AddPreShutdownHook(func() {
+ logger.Info("shutting down cron")
+ if err := app.Stop(ctx); err != nil {
+ logger.Error("failed to stop app", zap.Error(err))
+ }
+ })
+
+ logger.Info("started cron")
+ return manager
+}
+
+func cronAppOptions(manager services.SystemManager, opts ...fx.Option) []fx.Option {
+ logger := manager.Logger()
+
opts = append(
opts,
aws.Module,
@@ -54,24 +76,14 @@ func startManager(opts ...fx.Option) services.SystemManager {
parser.Module,
s3.Module,
storage.Module,
+ downloader.Module,
tally.Module,
fx.NopLogger,
+ workflow.Module,
fx.Provide(func() services.SystemManager { return manager }),
fx.Provide(func() *zap.Logger { return logger }),
fx.Invoke(cron.RegisterRunner),
)
- app := fx.New(opts...)
-
- if err := app.Start(ctx); err != nil {
- logger.Fatal("failed to start app", zap.Error(err))
- }
- manager.AddPreShutdownHook(func() {
- logger.Info("shutting down cron")
- if err := app.Stop(ctx); err != nil {
- logger.Error("failed to stop app", zap.Error(err))
- }
- })
- logger.Info("started cron")
- return manager
+ return opts
}
diff --git a/cmd/cron/main_test.go b/cmd/cron/main_test.go
index 571e8499..463fdafb 100644
--- a/cmd/cron/main_test.go
+++ b/cmd/cron/main_test.go
@@ -3,11 +3,14 @@ package main
import (
"testing"
+ "github.com/stretchr/testify/require"
"go.temporal.io/sdk/testsuite"
+ "go.uber.org/fx"
"github.com/coinbase/chainstorage/internal/cadence"
"github.com/coinbase/chainstorage/internal/config"
"github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/sdk/services"
)
type (
@@ -21,6 +24,13 @@ func (s CronTestSuite) T() *testing.T {
return s.t
}
+func TestCronAppDependencyGraph(t *testing.T) {
+ manager := services.NewManager()
+ defer manager.Shutdown()
+
+ require.NoError(t, fx.ValidateApp(cronAppOptions(manager)...))
+}
+
func TestIntegrationCron(t *testing.T) {
testapp.TestAllConfigs(t, func(t *testing.T, cfg *config.Config) {
if !cfg.IsFunctionalTest() {
diff --git a/config/chainstorage/abstract/mainnet/base.yml b/config/chainstorage/abstract/mainnet/base.yml
new file mode 100644
index 00000000..6c3d00cd
--- /dev/null
+++ b/config/chainstorage/abstract/mainnet/base.yml
@@ -0,0 +1,296 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_abstract_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_abstract_mainnet
+ transaction_table: example_chainstorage_transactions_table_abstract_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_abstract_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_abstract_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_abstract_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_abstract_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-abstract-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 2s
+ blockchain: BLOCKCHAIN_ABSTRACT
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 1
+ stable: 1
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 10
+ network: NETWORK_ABSTRACT_MAINNET
+config_name: abstract_mainnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-abstract-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ event_height_delta: 60
+ event_time_delta: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ time_since_last_event: 2m30s
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 20m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 20
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 10
+ task_list: default
+ validation_percentage: 100
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 0s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 100
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 0s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/abstract/mainnet/development.yml b/config/chainstorage/abstract/mainnet/development.yml
new file mode 100644
index 00000000..3f69d997
--- /dev/null
+++ b/config/chainstorage/abstract/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-abstract-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/abstract/mainnet/local.yml b/config/chainstorage/abstract/mainnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/abstract/mainnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/abstract/mainnet/production.yml b/config/chainstorage/abstract/mainnet/production.yml
new file mode 100644
index 00000000..28e15815
--- /dev/null
+++ b/config/chainstorage/abstract/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-abstract-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/aptos/mainnet/base.yml b/config/chainstorage/aptos/mainnet/base.yml
index a7e0514f..51c52fd3 100644
--- a/config/chainstorage/aptos/mainnet/base.yml
+++ b/config/chainstorage/aptos/mainnet/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_aptos_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_aptos_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_aptos_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_aptos_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_aptos_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -103,6 +114,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -149,6 +175,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -213,6 +253,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/arbitrum/mainnet/base.yml b/config/chainstorage/arbitrum/mainnet/base.yml
index 8185923c..36c1fd25 100644
--- a/config/chainstorage/arbitrum/mainnet/base.yml
+++ b/config/chainstorage/arbitrum/mainnet/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_arbitrum_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_arbitrum_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_arbitrum_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_arbitrum_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_arbitrum_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -105,6 +116,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -153,6 +179,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -217,6 +257,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/avacchain/mainnet/base.yml b/config/chainstorage/avacchain/mainnet/base.yml
index 20f3f43d..08ad716f 100644
--- a/config/chainstorage/avacchain/mainnet/base.yml
+++ b/config/chainstorage/avacchain/mainnet/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_avacchain_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_avacchain_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_avacchain_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_avacchain_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_avacchain_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -104,6 +115,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -150,6 +176,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -214,6 +254,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/avacchain/mainnet/development.yml b/config/chainstorage/avacchain/mainnet/development.yml
index e0058a3b..4dbab422 100644
--- a/config/chainstorage/avacchain/mainnet/development.yml
+++ b/config/chainstorage/avacchain/mainnet/development.yml
@@ -11,3 +11,17 @@ server:
workflows:
cross_validator:
validation_start_height: 16000000
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config/chainstorage/base/goerli/base.yml b/config/chainstorage/base/goerli/base.yml
index 20f8e798..0602e6f0 100644
--- a/config/chainstorage/base/goerli/base.yml
+++ b/config/chainstorage/base/goerli/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_base_goerli
versioned_event_table: example_chainstorage_versioned_block_events_base_goerli
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_base_goerli
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_base_goerli
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_base_goerli_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -105,6 +116,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -151,6 +177,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -215,6 +255,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/base/mainnet/base.yml b/config/chainstorage/base/mainnet/base.yml
index ce2169a9..b743e8b7 100644
--- a/config/chainstorage/base/mainnet/base.yml
+++ b/config/chainstorage/base/mainnet/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_base_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_base_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_base_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_base_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_base_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -106,6 +117,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -152,6 +178,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -220,6 +260,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/base/mainnet/development.yml b/config/chainstorage/base/mainnet/development.yml
index af8e4a8a..df419246 100644
--- a/config/chainstorage/base/mainnet/development.yml
+++ b/config/chainstorage/base/mainnet/development.yml
@@ -9,3 +9,17 @@ server:
workflows:
cross_validator:
validation_percentage: 20
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config/chainstorage/bitcoin/mainnet/base.yml b/config/chainstorage/bitcoin/mainnet/base.yml
index aeaee065..0c3217a4 100644
--- a/config/chainstorage/bitcoin/mainnet/base.yml
+++ b/config/chainstorage/bitcoin/mainnet/base.yml
@@ -24,6 +24,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_bitcoin_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_bitcoin_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_bitcoin_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_bitcoin_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_bitcoin_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -106,6 +117,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -152,6 +178,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -216,6 +256,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/bitcoincash/mainnet/base.yml b/config/chainstorage/bitcoincash/mainnet/base.yml
new file mode 100644
index 00000000..6d93e56a
--- /dev/null
+++ b/config/chainstorage/bitcoincash/mainnet/base.yml
@@ -0,0 +1,295 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 5
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 30m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_bitcoincash_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_bitcoincash_mainnet
+ event_table: example_chainstorage_block_events_bitcoincash_mainnet
+ event_table_height_index: example_chainstorage_block_events_by_height_bitcoincash_mainnet
+ transaction_table: example_chainstorage_transactions_table_bitcoincash_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_bitcoincash_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_bitcoincash_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_bitcoincash_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_bitcoincash_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-bitcoincash-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 2
+ stable: 2
+ block_time: 10m
+ blockchain: BLOCKCHAIN_BITCOINCASH
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ rpc_timeout_get_block: 30s
+ rpc_timeout_get_raw_tx: 60s
+ slave:
+ endpoint_group: ""
+ tx_batch_size: 100
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 0
+ stable: 0
+ feature:
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 2
+ network: NETWORK_BITCOINCASH_MAINNET
+config_name: bitcoincash_mainnet
+cron:
+ block_range_size: 2
+ disable_dlq_processor: true
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-bitcoincash-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 5
+ block_time_delta: 1h
+ event_height_delta: 5
+ event_time_delta: 1h
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ out_of_sync_node_distance: 10
+ tier: 2
+ time_since_last_block: 1h15m
+ time_since_last_event: 1h15m
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 21
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 4
+ task_list: default
+ validation_percentage: 10
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 250
+ event_gap_limit: 300
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 15m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 10s
+ checkpoint_size: 1000
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 5
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: false
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 10s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/bitcoincash/mainnet/development.yml b/config/chainstorage/bitcoincash/mainnet/development.yml
new file mode 100644
index 00000000..45916cdb
--- /dev/null
+++ b/config/chainstorage/bitcoincash/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-bitcoincash-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/bitcoincash/mainnet/local.yml b/config/chainstorage/bitcoincash/mainnet/local.yml
new file mode 100644
index 00000000..e1199fb7
--- /dev/null
+++ b/config/chainstorage/bitcoincash/mainnet/local.yml
@@ -0,0 +1,38 @@
+# This file is generated by "make config". DO NOT EDIT.
+chain:
+ client:
+ master:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: https://go.getblock.io/9fab7739b91042e7903fca6001e81b23
+ weight: 1
+ slave:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: https://go.getblock.io/9fab7739b91042e7903fca6001e81b23
+ weight: 1
+ validator:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: https://go.getblock.io/9fab7739b91042e7903fca6001e81b23
+ weight: 1
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
+workflows:
+ monitor:
+ failover_enabled: false
+ poller:
+ failover_enabled: false
diff --git a/config/chainstorage/bitcoincash/mainnet/production.yml b/config/chainstorage/bitcoincash/mainnet/production.yml
new file mode 100644
index 00000000..f4f58486
--- /dev/null
+++ b/config/chainstorage/bitcoincash/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-bitcoincash-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/bsc/mainnet/base.yml b/config/chainstorage/bsc/mainnet/base.yml
index f0aabd87..efc96127 100644
--- a/config/chainstorage/bsc/mainnet/base.yml
+++ b/config/chainstorage/bsc/mainnet/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_bsc_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_bsc_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_bsc_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_bsc_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_bsc_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -106,6 +117,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -152,6 +178,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -216,6 +256,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/dash/mainnet/base.yml b/config/chainstorage/dash/mainnet/base.yml
new file mode 100644
index 00000000..c568d8df
--- /dev/null
+++ b/config/chainstorage/dash/mainnet/base.yml
@@ -0,0 +1,293 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 5
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 30m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_dash_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_dash_mainnet
+ event_table: example_chainstorage_block_events_dash_mainnet
+ event_table_height_index: example_chainstorage_block_events_by_height_dash_mainnet
+ transaction_table: example_chainstorage_transactions_table_dash_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_dash_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_dash_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_dash_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_dash_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-dash-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 10m
+ blockchain: BLOCKCHAIN_DASH
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ tx_batch_size: 100
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 0
+ stable: 0
+ feature:
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 2
+ network: NETWORK_DASH_MAINNET
+config_name: dash_mainnet
+cron:
+ block_range_size: 2
+ disable_dlq_processor: true
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-dash-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 5
+ block_time_delta: 1h
+ event_height_delta: 5
+ event_time_delta: 1h
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ out_of_sync_node_distance: 10
+ tier: 2
+ time_since_last_block: 1h15m
+ time_since_last_event: 1h15m
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 21
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 4
+ task_list: default
+ validation_percentage: 10
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 250
+ event_gap_limit: 300
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 15m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 10s
+ checkpoint_size: 1000
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 5
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: false
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 10s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/dash/mainnet/development.yml b/config/chainstorage/dash/mainnet/development.yml
new file mode 100644
index 00000000..a6682add
--- /dev/null
+++ b/config/chainstorage/dash/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-dash-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/dash/mainnet/local.yml b/config/chainstorage/dash/mainnet/local.yml
new file mode 100644
index 00000000..75bd162f
--- /dev/null
+++ b/config/chainstorage/dash/mainnet/local.yml
@@ -0,0 +1,38 @@
+# This file is generated by "make config". DO NOT EDIT.
+chain:
+ client:
+ master:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: ""
+ weight: 1
+ slave:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: ""
+ weight: 1
+ validator:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: ""
+ weight: 1
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
+workflows:
+ monitor:
+ failover_enabled: false
+ poller:
+ failover_enabled: false
diff --git a/config/chainstorage/dash/mainnet/production.yml b/config/chainstorage/dash/mainnet/production.yml
new file mode 100644
index 00000000..dc3a871f
--- /dev/null
+++ b/config/chainstorage/dash/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-dash-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/dogecoin/mainnet/base.yml b/config/chainstorage/dogecoin/mainnet/base.yml
index 164a3e5f..cf5f33f3 100644
--- a/config/chainstorage/dogecoin/mainnet/base.yml
+++ b/config/chainstorage/dogecoin/mainnet/base.yml
@@ -24,6 +24,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_dogecoin_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_dogecoin_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_dogecoin_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_dogecoin_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_dogecoin_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -57,13 +68,9 @@ chain:
stable: 0
feature:
default_stable_event: true
- rosetta_parser: true
+ rosetta_parser: false
irreversible_distance: 60
network: NETWORK_DOGECOIN_MAINNET
- rosetta:
- block_not_found_error_codes: 4
- blockchain: dogecoin
- network: mainnet
config_name: dogecoin_mainnet
cron:
block_range_size: 4
@@ -110,6 +117,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -156,6 +178,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -220,6 +256,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/ethereum/goerli/base.yml b/config/chainstorage/ethereum/goerli/base.yml
index e80904c0..911c7692 100644
--- a/config/chainstorage/ethereum/goerli/base.yml
+++ b/config/chainstorage/ethereum/goerli/base.yml
@@ -24,6 +24,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_ethereum_goerli
versioned_event_table: example_chainstorage_versioned_block_events_ethereum_goerli
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_ethereum_goerli
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_ethereum_goerli
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_ethereum_goerli_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -107,6 +118,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -153,6 +179,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -219,6 +259,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/ethereum/holesky/base.yml b/config/chainstorage/ethereum/holesky/base.yml
index d77a380f..dda766e4 100644
--- a/config/chainstorage/ethereum/holesky/base.yml
+++ b/config/chainstorage/ethereum/holesky/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_ethereum_holesky
versioned_event_table: example_chainstorage_versioned_block_events_ethereum_holesky
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_ethereum_holesky
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_ethereum_holesky
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_ethereum_holesky_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -103,6 +114,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -149,6 +175,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -213,6 +253,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/ethereum/holesky/beacon/base.yml b/config/chainstorage/ethereum/holesky/beacon/base.yml
index 5c16c714..16dc47eb 100644
--- a/config/chainstorage/ethereum/holesky/beacon/base.yml
+++ b/config/chainstorage/ethereum/holesky/beacon/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_ethereum_holesky_beacon
versioned_event_table: example_chainstorage_versioned_block_events_ethereum_holesky_beacon
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_ethereum_holesky_beacon
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_ethereum_holesky
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_ethereum_holesky_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -104,6 +115,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -150,6 +176,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -215,6 +255,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/ethereum/mainnet/base.yml b/config/chainstorage/ethereum/mainnet/base.yml
index 1d0ae70a..f58e85e4 100644
--- a/config/chainstorage/ethereum/mainnet/base.yml
+++ b/config/chainstorage/ethereum/mainnet/base.yml
@@ -24,6 +24,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_ethereum_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_ethereum_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_ethereum_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_ethereum_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_ethereum_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -108,6 +119,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -155,6 +181,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -223,6 +263,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/ethereum/mainnet/beacon/base.yml b/config/chainstorage/ethereum/mainnet/beacon/base.yml
index 9a0f30ae..d3a55e38 100644
--- a/config/chainstorage/ethereum/mainnet/beacon/base.yml
+++ b/config/chainstorage/ethereum/mainnet/beacon/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_ethereum_mainnet_beacon
versioned_event_table: example_chainstorage_versioned_block_events_ethereum_mainnet_beacon
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_ethereum_mainnet_beacon
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_ethereum_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_ethereum_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -104,6 +115,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -150,6 +176,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -216,6 +256,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/ethereum/mainnet/development.yml b/config/chainstorage/ethereum/mainnet/development.yml
index 422fe064..4a10a4dc 100644
--- a/config/chainstorage/ethereum/mainnet/development.yml
+++ b/config/chainstorage/ethereum/mainnet/development.yml
@@ -28,6 +28,20 @@ sla:
time_since_last_block: 3m
time_since_last_event: 3m
workflows:
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
failover_enabled: false
poller:
diff --git a/config/chainstorage/ethereumclassic/mainnet/base.yml b/config/chainstorage/ethereumclassic/mainnet/base.yml
new file mode 100644
index 00000000..5c6ae434
--- /dev/null
+++ b/config/chainstorage/ethereumclassic/mainnet/base.yml
@@ -0,0 +1,299 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: example-chainstorage-ethereumclassic-mainnet-dev
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_ethereumclassic_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_ethereumclassic_mainnet
+ event_table: example_chainstorage_block_events_ethereumclassic_mainnet
+ event_table_height_index: example_chainstorage_block_events_by_height_ethclassic_main
+ transaction_table: example_chainstorage_transactions_table_ethereumclassic_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_ethereumclassic_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_ethereumclassic_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_ethereumclassic_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_ethereumclassic_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-ethereumclassic-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 2
+ stable: 2
+ block_time: 12s
+ blockchain: BLOCKCHAIN_ETHEREUMCLASSIC
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 3
+ stable: 3
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 12
+ network: NETWORK_ETHEREUMCLASSIC_MAINNET
+config_name: ethereumclassic_mainnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-ethereumclassic-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 10
+ block_time_delta: 2m
+ event_height_delta: 10
+ event_time_delta: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 10
+ tier: 1
+ time_since_last_block: 2m
+ time_since_last_event: 2m
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 24
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 1000
+ checkpoint_size: 1000
+ parallelism: 4
+ task_list: default
+ validation_percentage: 1
+ validation_start_height: 15500000
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 100
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 1s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/ethereumclassic/mainnet/development.yml b/config/chainstorage/ethereumclassic/mainnet/development.yml
new file mode 100644
index 00000000..1d691b81
--- /dev/null
+++ b/config/chainstorage/ethereumclassic/mainnet/development.yml
@@ -0,0 +1,48 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-ethereumclassic-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+chain:
+ event_tag:
+ latest: 2
+ stable: 0
+ feature:
+ default_stable_event: false
+server:
+ bind_address: 0.0.0.0:9090
+sla:
+ block_height_delta: 12
+ block_time_delta: 3m
+ event_height_delta: 12
+ event_time_delta: 3m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - streamer/event_tag=1
+ - streamer/event_tag=2
+ - cross_validator
+ out_of_sync_node_distance: 12
+ time_since_last_block: 3m
+ time_since_last_event: 3m
+workflows:
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ failover_enabled: false
+ poller:
+ failover_enabled: false
diff --git a/config/chainstorage/ethereumclassic/mainnet/local.yml b/config/chainstorage/ethereumclassic/mainnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/ethereumclassic/mainnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/ethereumclassic/mainnet/production.yml b/config/chainstorage/ethereumclassic/mainnet/production.yml
new file mode 100644
index 00000000..3fef5f35
--- /dev/null
+++ b/config/chainstorage/ethereumclassic/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-ethereumclassic-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/fantom/mainnet/base.yml b/config/chainstorage/fantom/mainnet/base.yml
index 4002878e..1a532500 100644
--- a/config/chainstorage/fantom/mainnet/base.yml
+++ b/config/chainstorage/fantom/mainnet/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_fantom_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_fantom_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_fantom_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_fantom_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_fantom_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -103,6 +114,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -149,6 +175,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -213,6 +253,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/litecoin/mainnet/base.yml b/config/chainstorage/litecoin/mainnet/base.yml
new file mode 100644
index 00000000..b43c8e22
--- /dev/null
+++ b/config/chainstorage/litecoin/mainnet/base.yml
@@ -0,0 +1,292 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 5
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 30m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_litecoin_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_litecoin_mainnet
+ event_table: example_chainstorage_block_events_litecoin_mainnet
+ event_table_height_index: example_chainstorage_block_events_by_height_litecoin_mainnet
+ transaction_table: example_chainstorage_transactions_table_litecoin_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_litecoin_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_litecoin_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_litecoin_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_litecoin_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-litecoin-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 2
+ stable: 2
+ block_time: 10m
+ blockchain: BLOCKCHAIN_LITECOIN
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 0
+ stable: 0
+ feature:
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 2
+ network: NETWORK_LITECOIN_MAINNET
+config_name: litecoin_mainnet
+cron:
+ block_range_size: 2
+ disable_dlq_processor: true
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-litecoin-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 5
+ block_time_delta: 1h
+ event_height_delta: 5
+ event_time_delta: 1h
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ out_of_sync_node_distance: 10
+ tier: 2
+ time_since_last_block: 1h15m
+ time_since_last_event: 1h15m
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 21
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 4
+ task_list: default
+ validation_percentage: 10
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 250
+ event_gap_limit: 300
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 15m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 10s
+ checkpoint_size: 1000
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 5
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: false
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 10s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/litecoin/mainnet/development.yml b/config/chainstorage/litecoin/mainnet/development.yml
new file mode 100644
index 00000000..4d9b76c3
--- /dev/null
+++ b/config/chainstorage/litecoin/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-litecoin-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/litecoin/mainnet/local.yml b/config/chainstorage/litecoin/mainnet/local.yml
new file mode 100644
index 00000000..9f5cd26c
--- /dev/null
+++ b/config/chainstorage/litecoin/mainnet/local.yml
@@ -0,0 +1,38 @@
+# This file is generated by "make config". DO NOT EDIT.
+chain:
+ client:
+ master:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: https://go.getblock.io/50d006b05722430b940d0c63e47ff893
+ weight: 1
+ slave:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: https://go.getblock.io/50d006b05722430b940d0c63e47ff893
+ weight: 1
+ validator:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: https://go.getblock.io/50d006b05722430b940d0c63e47ff893
+ weight: 1
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
+workflows:
+ monitor:
+ failover_enabled: false
+ poller:
+ failover_enabled: false
diff --git a/config/chainstorage/litecoin/mainnet/production.yml b/config/chainstorage/litecoin/mainnet/production.yml
new file mode 100644
index 00000000..3c3c7f07
--- /dev/null
+++ b/config/chainstorage/litecoin/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-litecoin-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/megaeth/mainnet/base.yml b/config/chainstorage/megaeth/mainnet/base.yml
new file mode 100644
index 00000000..6c806e4d
--- /dev/null
+++ b/config/chainstorage/megaeth/mainnet/base.yml
@@ -0,0 +1,296 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_megaeth_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_megaeth_mainnet
+ transaction_table: example_chainstorage_transactions_table_megaeth_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_megaeth_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_megaeth_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_megaeth_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_megaeth_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-megaeth-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 2s
+ blockchain: BLOCKCHAIN_MEGAETH
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 1
+ stable: 1
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 10
+ network: NETWORK_MEGAETH_MAINNET
+config_name: megaeth_mainnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-megaeth-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ event_height_delta: 60
+ event_time_delta: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ time_since_last_event: 2m30s
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 20m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 20
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 10
+ task_list: default
+ validation_percentage: 100
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 0s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 100
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 0s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/megaeth/mainnet/development.yml b/config/chainstorage/megaeth/mainnet/development.yml
new file mode 100644
index 00000000..e00e7b08
--- /dev/null
+++ b/config/chainstorage/megaeth/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-megaeth-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/megaeth/mainnet/local.yml b/config/chainstorage/megaeth/mainnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/megaeth/mainnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/megaeth/mainnet/production.yml b/config/chainstorage/megaeth/mainnet/production.yml
new file mode 100644
index 00000000..d3c81083
--- /dev/null
+++ b/config/chainstorage/megaeth/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-megaeth-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/monad/mainnet/base.yml b/config/chainstorage/monad/mainnet/base.yml
new file mode 100644
index 00000000..aa4e37e9
--- /dev/null
+++ b/config/chainstorage/monad/mainnet/base.yml
@@ -0,0 +1,296 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_monad_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_monad_mainnet
+ transaction_table: example_chainstorage_transactions_table_monad_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_monad_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_monad_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_monad_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_monad_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-monad-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 2s
+ blockchain: BLOCKCHAIN_MONAD
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 1
+ stable: 1
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 10
+ network: NETWORK_MONAD_MAINNET
+config_name: monad_mainnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-monad-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ event_height_delta: 60
+ event_time_delta: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ time_since_last_event: 2m30s
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 20m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 20
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 10
+ task_list: default
+ validation_percentage: 100
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 0s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 100
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 0s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/monad/mainnet/development.yml b/config/chainstorage/monad/mainnet/development.yml
new file mode 100644
index 00000000..61c66900
--- /dev/null
+++ b/config/chainstorage/monad/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-monad-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/monad/mainnet/local.yml b/config/chainstorage/monad/mainnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/monad/mainnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/monad/mainnet/production.yml b/config/chainstorage/monad/mainnet/production.yml
new file mode 100644
index 00000000..7e476a57
--- /dev/null
+++ b/config/chainstorage/monad/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-monad-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/optimism/mainnet/base.yml b/config/chainstorage/optimism/mainnet/base.yml
index 7f5c93f7..2cc55f08 100644
--- a/config/chainstorage/optimism/mainnet/base.yml
+++ b/config/chainstorage/optimism/mainnet/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_optimism_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_optimism_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_optimism_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_optimism_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_optimism_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -103,6 +114,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -149,6 +175,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -213,6 +253,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/plasma/mainnet/base.yml b/config/chainstorage/plasma/mainnet/base.yml
new file mode 100644
index 00000000..cd2e0417
--- /dev/null
+++ b/config/chainstorage/plasma/mainnet/base.yml
@@ -0,0 +1,296 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_plasma_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_plasma_mainnet
+ transaction_table: example_chainstorage_transactions_table_plasma_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_plasma_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_plasma_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_plasma_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_plasma_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-plasma-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 2s
+ blockchain: BLOCKCHAIN_PLASMA
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 1
+ stable: 1
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 10
+ network: NETWORK_PLASMA_MAINNET
+config_name: plasma_mainnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-plasma-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ event_height_delta: 60
+ event_time_delta: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ time_since_last_event: 2m30s
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 20m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 20
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 10
+ task_list: default
+ validation_percentage: 100
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 0s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 100
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 0s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/plasma/mainnet/development.yml b/config/chainstorage/plasma/mainnet/development.yml
new file mode 100644
index 00000000..0e0031f8
--- /dev/null
+++ b/config/chainstorage/plasma/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-plasma-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/plasma/mainnet/local.yml b/config/chainstorage/plasma/mainnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/plasma/mainnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/plasma/mainnet/production.yml b/config/chainstorage/plasma/mainnet/production.yml
new file mode 100644
index 00000000..e9e40b64
--- /dev/null
+++ b/config/chainstorage/plasma/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-plasma-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/polygon/mainnet/base.yml b/config/chainstorage/polygon/mainnet/base.yml
index 626538d4..8484d68d 100644
--- a/config/chainstorage/polygon/mainnet/base.yml
+++ b/config/chainstorage/polygon/mainnet/base.yml
@@ -24,6 +24,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_polygon_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_polygon_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_polygon_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_polygon_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_polygon_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -111,6 +122,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -158,6 +184,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -227,6 +267,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/polygon/mainnet/development.yml b/config/chainstorage/polygon/mainnet/development.yml
index 78784b62..dc87202d 100644
--- a/config/chainstorage/polygon/mainnet/development.yml
+++ b/config/chainstorage/polygon/mainnet/development.yml
@@ -13,3 +13,17 @@ server:
workflows:
cross_validator:
validation_percentage: 20
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config/chainstorage/polygon/testnet/base.yml b/config/chainstorage/polygon/testnet/base.yml
index 7c28545f..2e1bfc03 100644
--- a/config/chainstorage/polygon/testnet/base.yml
+++ b/config/chainstorage/polygon/testnet/base.yml
@@ -22,6 +22,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_polygon_testnet
versioned_event_table: example_chainstorage_versioned_block_events_polygon_testnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_polygon_testnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_polygon_testnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_polygon_testnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -105,6 +116,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -152,6 +178,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -218,6 +258,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/polygon/testnet/development.yml b/config/chainstorage/polygon/testnet/development.yml
index 51a6eca3..20d2f3d4 100644
--- a/config/chainstorage/polygon/testnet/development.yml
+++ b/config/chainstorage/polygon/testnet/development.yml
@@ -11,5 +11,19 @@ server:
workflows:
cross_validator:
validation_percentage: 10
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
poller:
session_enabled: true
diff --git a/config/chainstorage/robinhood/mainnet/base.yml b/config/chainstorage/robinhood/mainnet/base.yml
new file mode 100644
index 00000000..81a013c5
--- /dev/null
+++ b/config/chainstorage/robinhood/mainnet/base.yml
@@ -0,0 +1,297 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_robinhood_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_robinhood_mainnet
+ transaction_table: example_chainstorage_transactions_table_robinhood_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_robinhood_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_robinhood_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_robinhood_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_robinhood_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-robinhood-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 250ms
+ blockchain: BLOCKCHAIN_ROBINHOOD
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 1
+ stable: 1
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: false
+ irreversible_distance: 1
+ network: NETWORK_ROBINHOOD_MAINNET
+config_name: robinhood_mainnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-robinhood-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 200
+ block_time_delta: 1m
+ event_height_delta: 200
+ event_time_delta: 1m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 500
+ out_of_sync_validator_node_distance: 1200
+ tier: 2
+ time_since_last_block: 1m30s
+ time_since_last_event: 1m30s
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 20m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 20
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 10
+ task_list: default
+ validation_percentage: 100
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 0s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: true
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 200
+ parallelism: 50
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 0s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/robinhood/mainnet/development.yml b/config/chainstorage/robinhood/mainnet/development.yml
new file mode 100644
index 00000000..78a83e3d
--- /dev/null
+++ b/config/chainstorage/robinhood/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-robinhood-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/robinhood/mainnet/local.yml b/config/chainstorage/robinhood/mainnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/robinhood/mainnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/robinhood/mainnet/production.yml b/config/chainstorage/robinhood/mainnet/production.yml
new file mode 100644
index 00000000..13f65318
--- /dev/null
+++ b/config/chainstorage/robinhood/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-robinhood-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/robinhood/testnet/base.yml b/config/chainstorage/robinhood/testnet/base.yml
new file mode 100644
index 00000000..00ed518f
--- /dev/null
+++ b/config/chainstorage/robinhood/testnet/base.yml
@@ -0,0 +1,297 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_robinhood_testnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_robinhood_testnet
+ transaction_table: example_chainstorage_transactions_table_robinhood_testnet
+ versioned_event_table: example_chainstorage_versioned_block_events_robinhood_testnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_robinhood_testnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_robinhood_testnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_robinhood_testnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-robinhood-testnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 250ms
+ blockchain: BLOCKCHAIN_ROBINHOOD
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 1
+ stable: 1
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: false
+ irreversible_distance: 1
+ network: NETWORK_ROBINHOOD_TESTNET
+config_name: robinhood_testnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-robinhood-testnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 200
+ block_time_delta: 1m
+ event_height_delta: 200
+ event_time_delta: 1m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 500
+ out_of_sync_validator_node_distance: 1200
+ tier: 2
+ time_since_last_block: 1m30s
+ time_since_last_event: 1m30s
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 20m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 20
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 10
+ task_list: default
+ validation_percentage: 100
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 0s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: true
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 200
+ parallelism: 50
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 0s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/robinhood/testnet/development.yml b/config/chainstorage/robinhood/testnet/development.yml
new file mode 100644
index 00000000..f48b964e
--- /dev/null
+++ b/config/chainstorage/robinhood/testnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-robinhood-testnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/robinhood/testnet/local.yml b/config/chainstorage/robinhood/testnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/robinhood/testnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/robinhood/testnet/production.yml b/config/chainstorage/robinhood/testnet/production.yml
new file mode 100644
index 00000000..3b298eea
--- /dev/null
+++ b/config/chainstorage/robinhood/testnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-robinhood-testnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/seismic/mainnet/base.yml b/config/chainstorage/seismic/mainnet/base.yml
new file mode 100644
index 00000000..1c2b642c
--- /dev/null
+++ b/config/chainstorage/seismic/mainnet/base.yml
@@ -0,0 +1,296 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_seismic_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_seismic_mainnet
+ transaction_table: example_chainstorage_transactions_table_seismic_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_seismic_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_seismic_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_seismic_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_seismic_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-seismic-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 2s
+ blockchain: BLOCKCHAIN_SEISMIC
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 1
+ stable: 1
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 10
+ network: NETWORK_SEISMIC_MAINNET
+config_name: seismic_mainnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-seismic-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ event_height_delta: 60
+ event_time_delta: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ time_since_last_event: 2m30s
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 20m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 20
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 10
+ task_list: default
+ validation_percentage: 100
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 0s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 100
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 0s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/seismic/mainnet/development.yml b/config/chainstorage/seismic/mainnet/development.yml
new file mode 100644
index 00000000..8af1399c
--- /dev/null
+++ b/config/chainstorage/seismic/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-seismic-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/seismic/mainnet/local.yml b/config/chainstorage/seismic/mainnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/seismic/mainnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/seismic/mainnet/production.yml b/config/chainstorage/seismic/mainnet/production.yml
new file mode 100644
index 00000000..01dc3353
--- /dev/null
+++ b/config/chainstorage/seismic/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-seismic-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/seismic/testnet/base.yml b/config/chainstorage/seismic/testnet/base.yml
new file mode 100644
index 00000000..cffd37c5
--- /dev/null
+++ b/config/chainstorage/seismic/testnet/base.yml
@@ -0,0 +1,296 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_seismic_testnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_seismic_testnet
+ transaction_table: example_chainstorage_transactions_table_seismic_testnet
+ versioned_event_table: example_chainstorage_versioned_block_events_seismic_testnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_seismic_testnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_seismic_testnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_seismic_testnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-seismic-testnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 2s
+ blockchain: BLOCKCHAIN_SEISMIC
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 1
+ stable: 1
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 10
+ network: NETWORK_SEISMIC_TESTNET
+config_name: seismic_testnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-seismic-testnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ event_height_delta: 60
+ event_time_delta: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ time_since_last_event: 2m30s
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 20m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 20
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 10
+ task_list: default
+ validation_percentage: 100
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 0s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 100
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 0s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/seismic/testnet/development.yml b/config/chainstorage/seismic/testnet/development.yml
new file mode 100644
index 00000000..3bab82bf
--- /dev/null
+++ b/config/chainstorage/seismic/testnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-seismic-testnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/seismic/testnet/local.yml b/config/chainstorage/seismic/testnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/seismic/testnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/seismic/testnet/production.yml b/config/chainstorage/seismic/testnet/production.yml
new file mode 100644
index 00000000..88cd2295
--- /dev/null
+++ b/config/chainstorage/seismic/testnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-seismic-testnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/solana/mainnet/base.yml b/config/chainstorage/solana/mainnet/base.yml
index 2a8e8f77..fc87f070 100644
--- a/config/chainstorage/solana/mainnet/base.yml
+++ b/config/chainstorage/solana/mainnet/base.yml
@@ -24,6 +24,17 @@ aws:
transaction_table: example_chainstorage_transactions_table_solana_mainnet
versioned_event_table: example_chainstorage_versioned_block_events_solana_mainnet
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_solana_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_solana_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_solana_mainnet_worker
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -106,6 +117,21 @@ workflows:
task_list: default
workflow_identity: workflow.backfiller
workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
benchmarker:
activity_retry:
backoff_coefficient: 2
@@ -152,6 +178,20 @@ workflows:
task_list: default
workflow_identity: workflow.event_backfiller
workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
monitor:
activity_retry:
backoff_coefficient: 2
@@ -217,6 +257,19 @@ workflows:
task_list: default
workflow_identity: workflow.replicator
workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
streamer:
activity_retry:
backoff_coefficient: 2
diff --git a/config/chainstorage/solana/mainnet/development.yml b/config/chainstorage/solana/mainnet/development.yml
index 99839d63..f2581d8a 100644
--- a/config/chainstorage/solana/mainnet/development.yml
+++ b/config/chainstorage/solana/mainnet/development.yml
@@ -11,5 +11,19 @@ chain:
server:
bind_address: 0.0.0.0:9090
workflows:
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
poller:
num_blocks_to_skip: 10
diff --git a/config/chainstorage/solana/mainnet/production.yml b/config/chainstorage/solana/mainnet/production.yml
index 266f4729..7e5277fc 100644
--- a/config/chainstorage/solana/mainnet/production.yml
+++ b/config/chainstorage/solana/mainnet/production.yml
@@ -2,10 +2,22 @@
aws:
aws_account: production
bucket: example-chainstorage-solana-mainnet-prod
+ storage:
+ consolidation:
+ read_shadow_first: true
cadence:
address: temporal.example.com:7233
+ keep_alive_time: 10s
+ keep_alive_timeout: 30s
+cron:
+ batch_consolidator:
+ spec: '@every 15m'
+ workflow_parallelism: 4
server:
bind_address: 0.0.0.0:9090
+storage_type:
+ blob: S3
+ meta: POSTGRES
workflows:
backfiller:
num_concurrent_extractors: 150
diff --git a/config/chainstorage/story/mainnet/base.yml b/config/chainstorage/story/mainnet/base.yml
new file mode 100644
index 00000000..198422a0
--- /dev/null
+++ b/config/chainstorage/story/mainnet/base.yml
@@ -0,0 +1,296 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_story_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_story_mainnet
+ transaction_table: example_chainstorage_transactions_table_story_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_story_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_story_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_story_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_story_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-story-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 2s
+ blockchain: BLOCKCHAIN_STORY
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 1
+ stable: 1
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 10
+ network: NETWORK_STORY_MAINNET
+config_name: story_mainnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-story-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ event_height_delta: 60
+ event_time_delta: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ time_since_last_event: 2m30s
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 20m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 20
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 10
+ task_list: default
+ validation_percentage: 100
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 0s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 100
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 0s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/story/mainnet/development.yml b/config/chainstorage/story/mainnet/development.yml
new file mode 100644
index 00000000..9e7331ca
--- /dev/null
+++ b/config/chainstorage/story/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-story-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/story/mainnet/local.yml b/config/chainstorage/story/mainnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/story/mainnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/story/mainnet/production.yml b/config/chainstorage/story/mainnet/production.yml
new file mode 100644
index 00000000..50fe8f1d
--- /dev/null
+++ b/config/chainstorage/story/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-story-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/tempo/mainnet/base.yml b/config/chainstorage/tempo/mainnet/base.yml
new file mode 100644
index 00000000..6c4ba66f
--- /dev/null
+++ b/config/chainstorage/tempo/mainnet/base.yml
@@ -0,0 +1,296 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_tempo_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_tempo_mainnet
+ transaction_table: example_chainstorage_transactions_table_tempo_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_tempo_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_tempo_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_tempo_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_tempo_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-tempo-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 2s
+ blockchain: BLOCKCHAIN_TEMPO
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 1
+ stable: 1
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 10
+ network: NETWORK_TEMPO_MAINNET
+config_name: tempo_mainnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-tempo-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ event_height_delta: 60
+ event_time_delta: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ time_since_last_event: 2m30s
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 20m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 20
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 10
+ task_list: default
+ validation_percentage: 100
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 0s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 100
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 0s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/tempo/mainnet/development.yml b/config/chainstorage/tempo/mainnet/development.yml
new file mode 100644
index 00000000..5cdf73ab
--- /dev/null
+++ b/config/chainstorage/tempo/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-tempo-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/tempo/mainnet/local.yml b/config/chainstorage/tempo/mainnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/tempo/mainnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/tempo/mainnet/production.yml b/config/chainstorage/tempo/mainnet/production.yml
new file mode 100644
index 00000000..ef6afa29
--- /dev/null
+++ b/config/chainstorage/tempo/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-tempo-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/tron/mainnet/base.yml b/config/chainstorage/tron/mainnet/base.yml
new file mode 100644
index 00000000..44c1969b
--- /dev/null
+++ b/config/chainstorage/tron/mainnet/base.yml
@@ -0,0 +1,301 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 50
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 10m
+aws:
+ aws_account: development
+ bucket: example-chainstorage-tron-mainnet-dev
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_tron_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_tron_mainnet
+ event_table: example_chainstorage_block_events_tron_mainnet
+ event_table_height_index: example_chainstorage_block_events_by_height_tron_mainnet
+ transaction_table: example_chainstorage_transactions_table_tron_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_tron_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_tron_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_tron_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_tron_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: ZSTD
+cadence:
+ address: ""
+ domain: chainstorage-tron-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 2
+ stable: 2
+ block_time: 12s
+ blockchain: BLOCKCHAIN_TRON
+ client:
+ additional:
+ endpoint_group: ""
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 3
+ stable: 3
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 12
+ network: NETWORK_TRON_MAINNET
+config_name: tron_mainnet
+cron:
+ block_range_size: 4
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-tron-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 10
+ block_time_delta: 2m
+ event_height_delta: 10
+ event_time_delta: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+ out_of_sync_node_distance: 10
+ tier: 1
+ time_since_last_block: 2m
+ time_since_last_event: 2m
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 24
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 1000
+ checkpoint_size: 1000
+ parallelism: 4
+ task_list: default
+ validation_percentage: 1
+ validation_start_height: 15500000
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 500
+ event_gap_limit: 300
+ failover_enabled: true
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 2m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 1s
+ checkpoint_size: 1000
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 100
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: true
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 1s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/tron/mainnet/development.yml b/config/chainstorage/tron/mainnet/development.yml
new file mode 100644
index 00000000..7840da7d
--- /dev/null
+++ b/config/chainstorage/tron/mainnet/development.yml
@@ -0,0 +1,48 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-tron-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+chain:
+ event_tag:
+ latest: 2
+ stable: 0
+ feature:
+ default_stable_event: false
+server:
+ bind_address: 0.0.0.0:9090
+sla:
+ block_height_delta: 12
+ block_time_delta: 3m
+ event_height_delta: 12
+ event_time_delta: 3m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - streamer/event_tag=1
+ - streamer/event_tag=2
+ - cross_validator
+ out_of_sync_node_distance: 12
+ time_since_last_block: 3m
+ time_since_last_event: 3m
+workflows:
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ failover_enabled: false
+ poller:
+ failover_enabled: false
diff --git a/config/chainstorage/tron/mainnet/local.yml b/config/chainstorage/tron/mainnet/local.yml
new file mode 100644
index 00000000..cc1d22d9
--- /dev/null
+++ b/config/chainstorage/tron/mainnet/local.yml
@@ -0,0 +1,10 @@
+# This file is generated by "make config". DO NOT EDIT.
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
diff --git a/config/chainstorage/tron/mainnet/production.yml b/config/chainstorage/tron/mainnet/production.yml
new file mode 100644
index 00000000..632b861f
--- /dev/null
+++ b/config/chainstorage/tron/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-tron-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/zcash/mainnet/base.yml b/config/chainstorage/zcash/mainnet/base.yml
new file mode 100644
index 00000000..b6318f42
--- /dev/null
+++ b/config/chainstorage/zcash/mainnet/base.yml
@@ -0,0 +1,293 @@
+# This file is generated by "make config". DO NOT EDIT.
+api:
+ auth: ""
+ max_num_block_files: 1000
+ max_num_blocks: 5
+ num_workers: 10
+ rate_limit:
+ global_rps: 3000
+ per_client_rps: 2000
+ streaming_batch_size: 50
+ streaming_interval: 1s
+ streaming_max_no_event_time: 30m
+aws:
+ aws_account: development
+ bucket: ""
+ dlq:
+ delay_secs: 900
+ name: example_chainstorage_blocks_zcash_mainnet_dlq
+ visibility_timeout_secs: 600
+ dynamodb:
+ block_table: example_chainstorage_blocks_zcash_mainnet
+ event_table: example_chainstorage_block_events_zcash_mainnet
+ event_table_height_index: example_chainstorage_block_events_by_height_zcash_mainnet
+ transaction_table: example_chainstorage_transactions_table_zcash_mainnet
+ versioned_event_table: example_chainstorage_versioned_block_events_zcash_mainnet
+ versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_zcash_mainnet
+ postgres:
+ connect_timeout: 30s
+ database: chainstorage_zcash_mainnet
+ host: localhost
+ max_connections: 25
+ min_connections: 5
+ password: ""
+ port: 5433
+ ssl_mode: require
+ statement_timeout: 60s
+ user: cs_zcash_mainnet_worker
+ presigned_url_expiration: 30m
+ region: us-east-1
+ storage:
+ data_compression: GZIP
+cadence:
+ address: ""
+ domain: chainstorage-zcash-mainnet
+ retention_period: 7
+ tls:
+ enabled: true
+ validate_hostname: true
+chain:
+ block_start_height: 0
+ block_tag:
+ latest: 1
+ stable: 1
+ block_time: 10m
+ blockchain: BLOCKCHAIN_ZCASH
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ tx_batch_size: 100
+ validator:
+ endpoint_group: ""
+ event_tag:
+ latest: 0
+ stable: 0
+ feature:
+ default_stable_event: true
+ rosetta_parser: true
+ irreversible_distance: 2
+ network: NETWORK_ZCASH_MAINNET
+config_name: zcash_mainnet
+cron:
+ block_range_size: 2
+ disable_dlq_processor: true
+functional_test: ""
+gcp:
+ presigned_url_expiration: 30m
+ project: development
+sdk:
+ auth_header: ""
+ auth_token: ""
+ chainstorage_address: https://example-chainstorage-zcash-mainnet
+ num_workers: 10
+ restful: true
+server:
+ bind_address: localhost:9090
+sla:
+ block_height_delta: 5
+ block_time_delta: 1h
+ event_height_delta: 5
+ event_time_delta: 1h
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ out_of_sync_node_distance: 10
+ tier: 2
+ time_since_last_block: 1h15m
+ time_since_last_event: 1h15m
+workflows:
+ backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 2500
+ checkpoint_size: 5000
+ max_reprocessed_per_batch: 30
+ mini_batch_size: 1
+ num_concurrent_extractors: 21
+ task_list: default
+ workflow_identity: workflow.backfiller
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ benchmarker:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ child_workflow_execution_start_to_close_timeout: 60m
+ task_list: default
+ workflow_identity: workflow.benchmarker
+ workflow_run_timeout: 24h
+ cross_validator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 8
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 100
+ checkpoint_size: 1000
+ parallelism: 4
+ task_list: default
+ validation_percentage: 10
+ workflow_identity: workflow.cross_validator
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ event_backfiller:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 250
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.event_backfiller
+ workflow_run_timeout: 24h
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ monitor:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ backoff_interval: 10s
+ batch_size: 50
+ block_gap_limit: 3000
+ checkpoint_size: 250
+ event_gap_limit: 300
+ parallelism: 4
+ task_list: default
+ workflow_identity: workflow.monitor
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ poller:
+ activity_heartbeat_timeout: 15m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 6
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 10s
+ checkpoint_size: 1000
+ fast_sync: false
+ liveness_check_enabled: true
+ liveness_check_interval: 1m
+ liveness_check_violation_limit: 10
+ max_blocks_to_sync_per_cycle: 5
+ parallelism: 10
+ session_creation_timeout: 2m
+ session_enabled: false
+ task_list: default
+ workflow_identity: workflow.poller
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 6
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ replicator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 10m
+ batch_size: 1000
+ checkpoint_size: 10000
+ mini_batch_size: 100
+ parallelism: 10
+ task_list: default
+ workflow_identity: workflow.replicator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_heartbeat_timeout: 5m
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
+ streamer:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 5
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 2m
+ backoff_interval: 10s
+ batch_size: 500
+ checkpoint_size: 500
+ task_list: default
+ workflow_identity: workflow.streamer
+ workflow_retry:
+ backoff_coefficient: 1
+ initial_interval: 30s
+ maximum_attempts: 3
+ maximum_interval: 30s
+ workflow_run_timeout: 24h
+ workers:
+ - task_list: default
diff --git a/config/chainstorage/zcash/mainnet/development.yml b/config/chainstorage/zcash/mainnet/development.yml
new file mode 100644
index 00000000..bf977802
--- /dev/null
+++ b/config/chainstorage/zcash/mainnet/development.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: development
+ bucket: example-chainstorage-zcash-mainnet-dev
+cadence:
+ address: temporal-dev.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config/chainstorage/zcash/mainnet/local.yml b/config/chainstorage/zcash/mainnet/local.yml
new file mode 100644
index 00000000..75bd162f
--- /dev/null
+++ b/config/chainstorage/zcash/mainnet/local.yml
@@ -0,0 +1,38 @@
+# This file is generated by "make config". DO NOT EDIT.
+chain:
+ client:
+ master:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: ""
+ weight: 1
+ slave:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: ""
+ weight: 1
+ validator:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ rps: 1
+ url: ""
+ weight: 1
+gcp:
+ project: chainstorage-local
+sdk:
+ chainstorage_address: localhost:9090
+ restful: false
+storage_type:
+ blob: S3
+ dlq: SQS
+ meta: DYNAMODB
+workflows:
+ monitor:
+ failover_enabled: false
+ poller:
+ failover_enabled: false
diff --git a/config/chainstorage/zcash/mainnet/production.yml b/config/chainstorage/zcash/mainnet/production.yml
new file mode 100644
index 00000000..c0aa44f4
--- /dev/null
+++ b/config/chainstorage/zcash/mainnet/production.yml
@@ -0,0 +1,8 @@
+# This file is generated by "make config". DO NOT EDIT.
+aws:
+ aws_account: production
+ bucket: example-chainstorage-zcash-mainnet-prod
+cadence:
+ address: temporal.example.com:7233
+server:
+ bind_address: 0.0.0.0:9090
diff --git a/config_templates/config/base.template.yml b/config_templates/config/base.template.yml
index ccb956fe..9685066e 100644
--- a/config_templates/config/base.template.yml
+++ b/config_templates/config/base.template.yml
@@ -21,6 +21,17 @@ aws:
versioned_event_table: example_chainstorage_versioned_block_events_{{blockchain}}_{{network}}
versioned_event_table_block_index: example_chainstorage_versioned_block_events_by_block_id_{{blockchain}}_{{network}}
transaction_table: example_chainstorage_transactions_table_{{blockchain}}_{{network}}
+ postgres:
+ host: localhost # Override with CHAINSTORAGE_AWS_POSTGRES_HOST for non-dev environments
+ port: 5433
+ user: "cs_{{blockchain}}_{{network}}_worker" # Dynamically generated username based on blockchain and network
+ password: "" # Set via CHAINSTORAGE_AWS_POSTGRES_PASSWORD env var for security
+ database: chainstorage_{{blockchain}}_{{network}}
+ ssl_mode: require # Use 'require' for production, 'disable' for local development
+ max_connections: 25 # Connection pool maximum size
+ min_connections: 5 # Connection pool minimum size
+ connect_timeout: 30s # Connection establishment timeout
+ statement_timeout: 60s # Individual statement/transaction timeout
presigned_url_expiration: 30m
region: us-east-1
storage:
@@ -222,5 +233,47 @@ workflows:
task_list: default
workflow_run_timeout: 24h
workflow_identity: workflow.replicator
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ batch_consolidator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_heartbeat_timeout: 5m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ batch_size: 10000
+ checkpoint_size: 100000
+ max_blocks: 0
+ task_list: default
+ workflow_identity: workflow.batch_consolidator
+ workflow_run_timeout: 24h
+ single_block_retention:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_heartbeat_timeout: 5m
+ activity_schedule_to_close_timeout: 12h
+ activity_start_to_close_timeout: 6h
+ max_object_ranges: 1
+ task_list: default
+ workflow_identity: workflow.single_block_retention
+ workflow_run_timeout: 24h
workers:
- task_list: default
diff --git a/config_templates/config/chainstorage/abstract/mainnet/base.template.yml b/config_templates/config/chainstorage/abstract/mainnet/base.template.yml
new file mode 100644
index 00000000..6297d5d7
--- /dev/null
+++ b/config_templates/config/chainstorage/abstract/mainnet/base.template.yml
@@ -0,0 +1,54 @@
+chain:
+ block_time: 2s
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ rosetta_parser: true
+ irreversible_distance: 10
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ event_height_delta: 60
+ event_time_delta: 2m
+ time_since_last_event: 2m30s
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ num_concurrent_extractors: 20
+ activity_start_to_close_timeout: 20m
+ cross_validator:
+ backoff_interval: 1s
+ parallelism: 10
+ validation_percentage: 100
+ poller:
+ backoff_interval: 0s
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ parallelism: 10
+ session_enabled: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/abstract/mainnet/development.template.yml b/config_templates/config/chainstorage/abstract/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/abstract/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/abstract/mainnet/local.template.yml b/config_templates/config/chainstorage/abstract/mainnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/abstract/mainnet/production.template.yml b/config_templates/config/chainstorage/abstract/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/aptos/mainnet/base.template.yml b/config_templates/config/chainstorage/aptos/mainnet/base.template.yml
index 72a52639..836357c0 100644
--- a/config_templates/config/chainstorage/aptos/mainnet/base.template.yml
+++ b/config_templates/config/chainstorage/aptos/mainnet/base.template.yml
@@ -27,3 +27,17 @@ workflows:
max_blocks_to_sync_per_cycle: 300
streamer:
backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/arbitrum/mainnet/base.template.yml b/config_templates/config/chainstorage/arbitrum/mainnet/base.template.yml
index 87735333..06375a69 100644
--- a/config_templates/config/chainstorage/arbitrum/mainnet/base.template.yml
+++ b/config_templates/config/chainstorage/arbitrum/mainnet/base.template.yml
@@ -36,3 +36,17 @@ workflows:
validation_start_height: 22207816 # CREL nodes do not support arb_trace API which was used before the NITRO upgrade
validation_percentage: 1
irreversible_distance: 500
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/avacchain/mainnet/base.template.yml b/config_templates/config/chainstorage/avacchain/mainnet/base.template.yml
index 14ee6d11..77654496 100644
--- a/config_templates/config/chainstorage/avacchain/mainnet/base.template.yml
+++ b/config_templates/config/chainstorage/avacchain/mainnet/base.template.yml
@@ -31,3 +31,17 @@ workflows:
cross_validator:
batch_size: 1000
validation_percentage: 1
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/avacchain/mainnet/development.template.yml b/config_templates/config/chainstorage/avacchain/mainnet/development.template.yml
index a554dd5a..cfb7d463 100644
--- a/config_templates/config/chainstorage/avacchain/mainnet/development.template.yml
+++ b/config_templates/config/chainstorage/avacchain/mainnet/development.template.yml
@@ -3,3 +3,17 @@ chain:
workflows:
cross_validator:
validation_start_height: 16000000
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/base/goerli/base.template.yml b/config_templates/config/chainstorage/base/goerli/base.template.yml
index 4d1b3c20..eacd006b 100644
--- a/config_templates/config/chainstorage/base/goerli/base.template.yml
+++ b/config_templates/config/chainstorage/base/goerli/base.template.yml
@@ -23,3 +23,17 @@ workflows:
session_enabled: true
streamer:
backoff_interval: 2s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/base/mainnet/base.template.yml b/config_templates/config/chainstorage/base/mainnet/base.template.yml
index 01e2129a..6297d5d7 100644
--- a/config_templates/config/chainstorage/base/mainnet/base.template.yml
+++ b/config_templates/config/chainstorage/base/mainnet/base.template.yml
@@ -38,3 +38,17 @@ workflows:
failover_enabled: true
streamer:
backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/base/mainnet/development.template.yml b/config_templates/config/chainstorage/base/mainnet/development.template.yml
index 35e9ee65..3ba48479 100644
--- a/config_templates/config/chainstorage/base/mainnet/development.template.yml
+++ b/config_templates/config/chainstorage/base/mainnet/development.template.yml
@@ -1,3 +1,17 @@
workflows:
cross_validator:
validation_percentage: 20
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/bitcoin/mainnet/base.template.yml b/config_templates/config/chainstorage/bitcoin/mainnet/base.template.yml
index 5b7a4926..abdc1160 100644
--- a/config_templates/config/chainstorage/bitcoin/mainnet/base.template.yml
+++ b/config_templates/config/chainstorage/bitcoin/mainnet/base.template.yml
@@ -41,3 +41,17 @@ workflows:
parallelism: 10
streamer:
backoff_interval: 10s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/bitcoincash/mainnet/base.template.yml b/config_templates/config/chainstorage/bitcoincash/mainnet/base.template.yml
new file mode 100644
index 00000000..3e63ced6
--- /dev/null
+++ b/config_templates/config/chainstorage/bitcoincash/mainnet/base.template.yml
@@ -0,0 +1,61 @@
+api:
+ max_num_blocks: 5
+ streaming_max_no_event_time: 30m
+aws:
+ dynamodb:
+ event_table: example_chainstorage_block_events_{{blockchain}}_{{network}}
+ event_table_height_index: example_chainstorage_block_events_by_height_{{blockchain}}_{{network}}
+chain:
+ client:
+ tx_batch_size: 100
+ rpc_timeout_get_block: 30s
+ rpc_timeout_get_raw_tx: 60s
+ block_tag:
+ latest: 2
+ stable: 2
+ event_tag:
+ latest: 0
+ stable: 0
+ block_time: 10m
+ irreversible_distance: 2
+ feature:
+ rosetta_parser: true
+cron:
+ block_range_size: 2
+ disable_dlq_processor: true
+sla:
+ block_height_delta: 5
+ block_time_delta: 1h
+ out_of_sync_node_distance: 10
+ tier: 2
+ time_since_last_block: 1h15m
+ event_height_delta: 5
+ event_time_delta: 1h
+ time_since_last_event: 1h15m
+workflows:
+ backfiller:
+ num_concurrent_extractors: 21
+ monitor:
+ checkpoint_size: 250
+ poller:
+ activity_heartbeat_timeout: 15m
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 10s
+ max_blocks_to_sync_per_cycle: 5
+ parallelism: 10
+ streamer:
+ backoff_interval: 10s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/bitcoincash/mainnet/development.template.yml b/config_templates/config/chainstorage/bitcoincash/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/bitcoincash/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/bitcoincash/mainnet/local.template.yml b/config_templates/config/chainstorage/bitcoincash/mainnet/local.template.yml
new file mode 100644
index 00000000..cb485766
--- /dev/null
+++ b/config_templates/config/chainstorage/bitcoincash/mainnet/local.template.yml
@@ -0,0 +1,30 @@
+
+chain:
+ client:
+ master:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: https://go.getblock.io/9fab7739b91042e7903fca6001e81b23
+ weight: 1
+ rps: 1
+ slave:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: https://go.getblock.io/9fab7739b91042e7903fca6001e81b23
+ weight: 1
+ rps: 1
+ validator:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: https://go.getblock.io/9fab7739b91042e7903fca6001e81b23
+ weight: 1
+ rps: 1
+workflows:
+ poller:
+ failover_enabled: false
+ monitor:
+ failover_enabled: false
+
diff --git a/config_templates/config/chainstorage/bitcoincash/mainnet/production.template.yml b/config_templates/config/chainstorage/bitcoincash/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/bsc/mainnet/base.template.yml b/config_templates/config/chainstorage/bsc/mainnet/base.template.yml
index 362040a7..46774447 100644
--- a/config_templates/config/chainstorage/bsc/mainnet/base.template.yml
+++ b/config_templates/config/chainstorage/bsc/mainnet/base.template.yml
@@ -23,6 +23,20 @@ workflows:
backoff_interval: 3s
streamer:
backoff_interval: 1s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
chain:
block_tag:
latest: 2
diff --git a/config_templates/config/chainstorage/dash/mainnet/base.template.yml b/config_templates/config/chainstorage/dash/mainnet/base.template.yml
new file mode 100644
index 00000000..49ddf282
--- /dev/null
+++ b/config_templates/config/chainstorage/dash/mainnet/base.template.yml
@@ -0,0 +1,59 @@
+api:
+ max_num_blocks: 5
+ streaming_max_no_event_time: 30m
+aws:
+ dynamodb:
+ event_table: example_chainstorage_block_events_{{blockchain}}_{{network}}
+ event_table_height_index: example_chainstorage_block_events_by_height_{{blockchain}}_{{network}}
+chain:
+ client:
+ tx_batch_size: 100
+ block_tag:
+ latest: 1
+ stable: 1
+ event_tag:
+ latest: 0
+ stable: 0
+ block_time: 10m
+ irreversible_distance: 2
+ feature:
+ rosetta_parser: true
+cron:
+ block_range_size: 2
+ disable_dlq_processor: true
+sla:
+ block_height_delta: 5
+ block_time_delta: 1h
+ out_of_sync_node_distance: 10
+ tier: 2
+ time_since_last_block: 1h15m
+ event_height_delta: 5
+ event_time_delta: 1h
+ time_since_last_event: 1h15m
+workflows:
+ backfiller:
+ num_concurrent_extractors: 21
+ monitor:
+ checkpoint_size: 250
+ poller:
+ activity_heartbeat_timeout: 15m
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 10s
+ max_blocks_to_sync_per_cycle: 5
+ parallelism: 10
+ streamer:
+ backoff_interval: 10s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/dash/mainnet/development.template.yml b/config_templates/config/chainstorage/dash/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/dash/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/dash/mainnet/local.template.yml b/config_templates/config/chainstorage/dash/mainnet/local.template.yml
new file mode 100644
index 00000000..2f36c7b8
--- /dev/null
+++ b/config_templates/config/chainstorage/dash/mainnet/local.template.yml
@@ -0,0 +1,30 @@
+
+chain:
+ client:
+ master:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: ""
+ weight: 1
+ rps: 1
+ slave:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: ""
+ weight: 1
+ rps: 1
+ validator:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: ""
+ weight: 1
+ rps: 1
+workflows:
+ poller:
+ failover_enabled: false
+ monitor:
+ failover_enabled: false
+
diff --git a/config_templates/config/chainstorage/dash/mainnet/production.template.yml b/config_templates/config/chainstorage/dash/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/dogecoin/mainnet/base.template.yml b/config_templates/config/chainstorage/dogecoin/mainnet/base.template.yml
index 855eeda9..e51b7021 100644
--- a/config_templates/config/chainstorage/dogecoin/mainnet/base.template.yml
+++ b/config_templates/config/chainstorage/dogecoin/mainnet/base.template.yml
@@ -3,12 +3,8 @@ chain:
event_tag:
latest: 0
stable: 0
- rosetta:
- blockchain: dogecoin
- network: mainnet
- block_not_found_error_codes: 4
feature:
- rosetta_parser: true
+ rosetta_parser: false
irreversible_distance: 60
aws:
dynamodb:
@@ -32,3 +28,17 @@ workflows:
parallelism: 10
max_blocks_to_sync_per_cycle: 50
session_enabled: true
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/ethereum/goerli/base.template.yml b/config_templates/config/chainstorage/ethereum/goerli/base.template.yml
index 50ec4b0b..5f4c1cc4 100644
--- a/config_templates/config/chainstorage/ethereum/goerli/base.template.yml
+++ b/config_templates/config/chainstorage/ethereum/goerli/base.template.yml
@@ -29,3 +29,17 @@ workflows:
session_enabled: true
consensus_validation: true
consensus_validation_muted: true
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/ethereum/holesky/base.template.yml b/config_templates/config/chainstorage/ethereum/holesky/base.template.yml
index 553a2d08..d4a31a3c 100644
--- a/config_templates/config/chainstorage/ethereum/holesky/base.template.yml
+++ b/config_templates/config/chainstorage/ethereum/holesky/base.template.yml
@@ -19,3 +19,17 @@ workflows:
num_concurrent_extractors: 24
poller:
session_enabled: true
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/ethereum/holesky/beacon/base.template.yml b/config_templates/config/chainstorage/ethereum/holesky/beacon/base.template.yml
index f7f9909e..96d60ae1 100644
--- a/config_templates/config/chainstorage/ethereum/holesky/beacon/base.template.yml
+++ b/config_templates/config/chainstorage/ethereum/holesky/beacon/base.template.yml
@@ -29,3 +29,17 @@ workflows:
session_enabled: true
monitor:
irreversible_distance: 10
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/ethereum/mainnet/beacon/base.template.yml b/config_templates/config/chainstorage/ethereum/mainnet/beacon/base.template.yml
index 60d14f59..f11bd221 100644
--- a/config_templates/config/chainstorage/ethereum/mainnet/beacon/base.template.yml
+++ b/config_templates/config/chainstorage/ethereum/mainnet/beacon/base.template.yml
@@ -30,3 +30,17 @@ workflows:
failover_enabled: true
monitor:
failover_enabled: true
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/ethereum/mainnet/development.template.yml b/config_templates/config/chainstorage/ethereum/mainnet/development.template.yml
index 401fee12..86f80acf 100644
--- a/config_templates/config/chainstorage/ethereum/mainnet/development.template.yml
+++ b/config_templates/config/chainstorage/ethereum/mainnet/development.template.yml
@@ -22,6 +22,20 @@ sla:
- streamer/event_tag=2
- cross_validator
workflows:
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
poller:
failover_enabled: false
monitor:
diff --git a/config_templates/config/chainstorage/ethereumclassic/mainnet/base.template.yml b/config_templates/config/chainstorage/ethereumclassic/mainnet/base.template.yml
new file mode 100644
index 00000000..23d71158
--- /dev/null
+++ b/config_templates/config/chainstorage/ethereumclassic/mainnet/base.template.yml
@@ -0,0 +1,56 @@
+aws:
+ aws_account: development
+ bucket: example-chainstorage-ethereumclassic-mainnet-dev
+ dlq:
+ name: example_chainstorage_blocks_ethereumclassic_mainnet_dlq
+ dynamodb:
+ block_table: example_chainstorage_blocks_ethereumclassic_mainnet
+ event_table: example_chainstorage_block_events_ethereumclassic_mainnet
+ event_table_height_index: example_chainstorage_block_events_by_height_ethclassic_main
+chain:
+ block_tag:
+ latest: 2
+ stable: 2
+ block_time: 12s
+ event_tag:
+ latest: 3
+ stable: 3
+ irreversible_distance: 12
+ feature:
+ rosetta_parser: true
+ default_stable_event: true
+ block_validation_enabled: true
+ block_validation_muted: true
+sla:
+ block_height_delta: 10
+ block_time_delta: 2m
+ out_of_sync_node_distance: 10
+ tier: 1
+ time_since_last_block: 2m
+ event_height_delta: 10
+ event_time_delta: 2m
+ time_since_last_event: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ checkpoint_size: 5000
+ num_concurrent_extractors: 24
+ cross_validator:
+ batch_size: 1000
+ validation_start_height: 15500000
+ validation_percentage: 1
+ poller:
+ parallelism: 10
+ failover_enabled: true
+ session_enabled: true
+ backoff_interval: 1s
+ consensus_validation: true
+ consensus_validation_muted: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 1s
diff --git a/config_templates/config/chainstorage/ethereumclassic/mainnet/development.template.yml b/config_templates/config/chainstorage/ethereumclassic/mainnet/development.template.yml
new file mode 100644
index 00000000..86f80acf
--- /dev/null
+++ b/config_templates/config/chainstorage/ethereumclassic/mainnet/development.template.yml
@@ -0,0 +1,42 @@
+chain:
+ event_tag:
+ latest: 2
+ stable: 0
+ feature:
+ default_stable_event: false
+aws:
+ aws_account: development
+sla:
+ block_height_delta: 12
+ block_time_delta: 3m
+ out_of_sync_node_distance: 12
+ time_since_last_block: 3m
+ event_height_delta: 12
+ event_time_delta: 3m
+ time_since_last_event: 3m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - streamer/event_tag=1
+ - streamer/event_tag=2
+ - cross_validator
+workflows:
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ poller:
+ failover_enabled: false
+ monitor:
+ failover_enabled: false
diff --git a/config_templates/config/chainstorage/ethereumclassic/mainnet/local.template.yml b/config_templates/config/chainstorage/ethereumclassic/mainnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/ethereumclassic/mainnet/production.template.yml b/config_templates/config/chainstorage/ethereumclassic/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/fantom/mainnet/base.template.yml b/config_templates/config/chainstorage/fantom/mainnet/base.template.yml
index 7356fb1e..5364281a 100644
--- a/config_templates/config/chainstorage/fantom/mainnet/base.template.yml
+++ b/config_templates/config/chainstorage/fantom/mainnet/base.template.yml
@@ -23,3 +23,17 @@ workflows:
fast_sync: true
streamer:
backoff_interval: 1s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/litecoin/mainnet/base.template.yml b/config_templates/config/chainstorage/litecoin/mainnet/base.template.yml
new file mode 100644
index 00000000..abdc1160
--- /dev/null
+++ b/config_templates/config/chainstorage/litecoin/mainnet/base.template.yml
@@ -0,0 +1,57 @@
+api:
+ max_num_blocks: 5
+ streaming_max_no_event_time: 30m
+aws:
+ dynamodb:
+ event_table: example_chainstorage_block_events_{{blockchain}}_{{network}}
+ event_table_height_index: example_chainstorage_block_events_by_height_{{blockchain}}_{{network}}
+chain:
+ block_tag:
+ latest: 2
+ stable: 2
+ event_tag:
+ latest: 0
+ stable: 0
+ block_time: 10m
+ irreversible_distance: 2
+ feature:
+ rosetta_parser: true
+cron:
+ block_range_size: 2
+ disable_dlq_processor: true
+sla:
+ block_height_delta: 5
+ block_time_delta: 1h
+ out_of_sync_node_distance: 10
+ tier: 2
+ time_since_last_block: 1h15m
+ event_height_delta: 5
+ event_time_delta: 1h
+ time_since_last_event: 1h15m
+workflows:
+ backfiller:
+ num_concurrent_extractors: 21
+ monitor:
+ checkpoint_size: 250
+ poller:
+ activity_heartbeat_timeout: 15m
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 10s
+ max_blocks_to_sync_per_cycle: 5
+ parallelism: 10
+ streamer:
+ backoff_interval: 10s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/litecoin/mainnet/development.template.yml b/config_templates/config/chainstorage/litecoin/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/litecoin/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/litecoin/mainnet/local.template.yml b/config_templates/config/chainstorage/litecoin/mainnet/local.template.yml
new file mode 100644
index 00000000..214634d9
--- /dev/null
+++ b/config_templates/config/chainstorage/litecoin/mainnet/local.template.yml
@@ -0,0 +1,30 @@
+
+chain:
+ client:
+ master:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: https://go.getblock.io/50d006b05722430b940d0c63e47ff893
+ weight: 1
+ rps: 1
+ slave:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: https://go.getblock.io/50d006b05722430b940d0c63e47ff893
+ weight: 1
+ rps: 1
+ validator:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: https://go.getblock.io/50d006b05722430b940d0c63e47ff893
+ weight: 1
+ rps: 1
+workflows:
+ poller:
+ failover_enabled: false
+ monitor:
+ failover_enabled: false
+
diff --git a/config_templates/config/chainstorage/litecoin/mainnet/production.template.yml b/config_templates/config/chainstorage/litecoin/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/megaeth/mainnet/base.template.yml b/config_templates/config/chainstorage/megaeth/mainnet/base.template.yml
new file mode 100644
index 00000000..6297d5d7
--- /dev/null
+++ b/config_templates/config/chainstorage/megaeth/mainnet/base.template.yml
@@ -0,0 +1,54 @@
+chain:
+ block_time: 2s
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ rosetta_parser: true
+ irreversible_distance: 10
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ event_height_delta: 60
+ event_time_delta: 2m
+ time_since_last_event: 2m30s
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ num_concurrent_extractors: 20
+ activity_start_to_close_timeout: 20m
+ cross_validator:
+ backoff_interval: 1s
+ parallelism: 10
+ validation_percentage: 100
+ poller:
+ backoff_interval: 0s
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ parallelism: 10
+ session_enabled: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/megaeth/mainnet/development.template.yml b/config_templates/config/chainstorage/megaeth/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/megaeth/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/megaeth/mainnet/local.template.yml b/config_templates/config/chainstorage/megaeth/mainnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/megaeth/mainnet/production.template.yml b/config_templates/config/chainstorage/megaeth/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/monad/mainnet/base.template.yml b/config_templates/config/chainstorage/monad/mainnet/base.template.yml
new file mode 100644
index 00000000..6297d5d7
--- /dev/null
+++ b/config_templates/config/chainstorage/monad/mainnet/base.template.yml
@@ -0,0 +1,54 @@
+chain:
+ block_time: 2s
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ rosetta_parser: true
+ irreversible_distance: 10
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ event_height_delta: 60
+ event_time_delta: 2m
+ time_since_last_event: 2m30s
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ num_concurrent_extractors: 20
+ activity_start_to_close_timeout: 20m
+ cross_validator:
+ backoff_interval: 1s
+ parallelism: 10
+ validation_percentage: 100
+ poller:
+ backoff_interval: 0s
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ parallelism: 10
+ session_enabled: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/monad/mainnet/development.template.yml b/config_templates/config/chainstorage/monad/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/monad/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/monad/mainnet/local.template.yml b/config_templates/config/chainstorage/monad/mainnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/monad/mainnet/production.template.yml b/config_templates/config/chainstorage/monad/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/optimism/mainnet/base.template.yml b/config_templates/config/chainstorage/optimism/mainnet/base.template.yml
index be52a5d7..f2ad0724 100644
--- a/config_templates/config/chainstorage/optimism/mainnet/base.template.yml
+++ b/config_templates/config/chainstorage/optimism/mainnet/base.template.yml
@@ -29,3 +29,17 @@ workflows:
monitor:
backoff_interval: 0s
parallelism: 10
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/plasma/mainnet/base.template.yml b/config_templates/config/chainstorage/plasma/mainnet/base.template.yml
new file mode 100644
index 00000000..6297d5d7
--- /dev/null
+++ b/config_templates/config/chainstorage/plasma/mainnet/base.template.yml
@@ -0,0 +1,54 @@
+chain:
+ block_time: 2s
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ rosetta_parser: true
+ irreversible_distance: 10
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ event_height_delta: 60
+ event_time_delta: 2m
+ time_since_last_event: 2m30s
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ num_concurrent_extractors: 20
+ activity_start_to_close_timeout: 20m
+ cross_validator:
+ backoff_interval: 1s
+ parallelism: 10
+ validation_percentage: 100
+ poller:
+ backoff_interval: 0s
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ parallelism: 10
+ session_enabled: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/plasma/mainnet/development.template.yml b/config_templates/config/chainstorage/plasma/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/plasma/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/plasma/mainnet/local.template.yml b/config_templates/config/chainstorage/plasma/mainnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/plasma/mainnet/production.template.yml b/config_templates/config/chainstorage/plasma/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/polygon/mainnet/development.template.yml b/config_templates/config/chainstorage/polygon/mainnet/development.template.yml
index 1f9dc3b7..6eeab526 100644
--- a/config_templates/config/chainstorage/polygon/mainnet/development.template.yml
+++ b/config_templates/config/chainstorage/polygon/mainnet/development.template.yml
@@ -7,3 +7,17 @@ chain:
workflows:
cross_validator:
validation_percentage: 20
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/polygon/testnet/base.template.yml b/config_templates/config/chainstorage/polygon/testnet/base.template.yml
index 7586aecb..ad4896fb 100644
--- a/config_templates/config/chainstorage/polygon/testnet/base.template.yml
+++ b/config_templates/config/chainstorage/polygon/testnet/base.template.yml
@@ -31,3 +31,17 @@ workflows:
checkpoint_size: 500
validation_start_height: 37000000
validation_percentage: 20
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/polygon/testnet/development.template.yml b/config_templates/config/chainstorage/polygon/testnet/development.template.yml
index fbc164fc..97e0561d 100644
--- a/config_templates/config/chainstorage/polygon/testnet/development.template.yml
+++ b/config_templates/config/chainstorage/polygon/testnet/development.template.yml
@@ -8,3 +8,17 @@ workflows:
session_enabled: true
cross_validator:
validation_percentage: 10
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/robinhood/mainnet/base.template.yml b/config_templates/config/chainstorage/robinhood/mainnet/base.template.yml
new file mode 100644
index 00000000..2e124749
--- /dev/null
+++ b/config_templates/config/chainstorage/robinhood/mainnet/base.template.yml
@@ -0,0 +1,56 @@
+chain:
+ block_time: 250ms
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ irreversible_distance: 1
+sla:
+ block_height_delta: 200
+ block_time_delta: 1m
+ out_of_sync_node_distance: 500
+ out_of_sync_validator_node_distance: 1200
+ tier: 2
+ time_since_last_block: 1m30s
+ event_height_delta: 200
+ event_time_delta: 1m
+ time_since_last_event: 1m30s
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ num_concurrent_extractors: 20
+ activity_start_to_close_timeout: 20m
+ cross_validator:
+ backoff_interval: 1s
+ parallelism: 10
+ validation_percentage: 100
+ poller:
+ backoff_interval: 0s
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: true
+ max_blocks_to_sync_per_cycle: 200
+ parallelism: 50
+ session_enabled: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/robinhood/mainnet/development.template.yml b/config_templates/config/chainstorage/robinhood/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/robinhood/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/robinhood/mainnet/local.template.yml b/config_templates/config/chainstorage/robinhood/mainnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/robinhood/mainnet/production.template.yml b/config_templates/config/chainstorage/robinhood/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/robinhood/testnet/base.template.yml b/config_templates/config/chainstorage/robinhood/testnet/base.template.yml
new file mode 100644
index 00000000..2e124749
--- /dev/null
+++ b/config_templates/config/chainstorage/robinhood/testnet/base.template.yml
@@ -0,0 +1,56 @@
+chain:
+ block_time: 250ms
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ irreversible_distance: 1
+sla:
+ block_height_delta: 200
+ block_time_delta: 1m
+ out_of_sync_node_distance: 500
+ out_of_sync_validator_node_distance: 1200
+ tier: 2
+ time_since_last_block: 1m30s
+ event_height_delta: 200
+ event_time_delta: 1m
+ time_since_last_event: 1m30s
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ num_concurrent_extractors: 20
+ activity_start_to_close_timeout: 20m
+ cross_validator:
+ backoff_interval: 1s
+ parallelism: 10
+ validation_percentage: 100
+ poller:
+ backoff_interval: 0s
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ fast_sync: true
+ max_blocks_to_sync_per_cycle: 200
+ parallelism: 50
+ session_enabled: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/robinhood/testnet/development.template.yml b/config_templates/config/chainstorage/robinhood/testnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/robinhood/testnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/robinhood/testnet/local.template.yml b/config_templates/config/chainstorage/robinhood/testnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/robinhood/testnet/production.template.yml b/config_templates/config/chainstorage/robinhood/testnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/seismic/mainnet/base.template.yml b/config_templates/config/chainstorage/seismic/mainnet/base.template.yml
new file mode 100644
index 00000000..6297d5d7
--- /dev/null
+++ b/config_templates/config/chainstorage/seismic/mainnet/base.template.yml
@@ -0,0 +1,54 @@
+chain:
+ block_time: 2s
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ rosetta_parser: true
+ irreversible_distance: 10
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ event_height_delta: 60
+ event_time_delta: 2m
+ time_since_last_event: 2m30s
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ num_concurrent_extractors: 20
+ activity_start_to_close_timeout: 20m
+ cross_validator:
+ backoff_interval: 1s
+ parallelism: 10
+ validation_percentage: 100
+ poller:
+ backoff_interval: 0s
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ parallelism: 10
+ session_enabled: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/seismic/mainnet/development.template.yml b/config_templates/config/chainstorage/seismic/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/seismic/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/seismic/mainnet/local.template.yml b/config_templates/config/chainstorage/seismic/mainnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/seismic/mainnet/production.template.yml b/config_templates/config/chainstorage/seismic/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/seismic/testnet/base.template.yml b/config_templates/config/chainstorage/seismic/testnet/base.template.yml
new file mode 100644
index 00000000..6297d5d7
--- /dev/null
+++ b/config_templates/config/chainstorage/seismic/testnet/base.template.yml
@@ -0,0 +1,54 @@
+chain:
+ block_time: 2s
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ rosetta_parser: true
+ irreversible_distance: 10
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ event_height_delta: 60
+ event_time_delta: 2m
+ time_since_last_event: 2m30s
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ num_concurrent_extractors: 20
+ activity_start_to_close_timeout: 20m
+ cross_validator:
+ backoff_interval: 1s
+ parallelism: 10
+ validation_percentage: 100
+ poller:
+ backoff_interval: 0s
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ parallelism: 10
+ session_enabled: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/seismic/testnet/development.template.yml b/config_templates/config/chainstorage/seismic/testnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/seismic/testnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/seismic/testnet/local.template.yml b/config_templates/config/chainstorage/seismic/testnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/seismic/testnet/production.template.yml b/config_templates/config/chainstorage/seismic/testnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/solana/mainnet/base.template.yml b/config_templates/config/chainstorage/solana/mainnet/base.template.yml
index 600ea96b..51ac0810 100644
--- a/config_templates/config/chainstorage/solana/mainnet/base.template.yml
+++ b/config_templates/config/chainstorage/solana/mainnet/base.template.yml
@@ -53,3 +53,17 @@ workflows:
session_enabled: true
streamer:
backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/solana/mainnet/development.template.yml b/config_templates/config/chainstorage/solana/mainnet/development.template.yml
index 8eba3a7e..15ce33a7 100644
--- a/config_templates/config/chainstorage/solana/mainnet/development.template.yml
+++ b/config_templates/config/chainstorage/solana/mainnet/development.template.yml
@@ -7,3 +7,17 @@ chain:
workflows:
poller:
num_blocks_to_skip: 10
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/solana/mainnet/production.template.yml b/config_templates/config/chainstorage/solana/mainnet/production.template.yml
index f19da5a3..cdba8d1e 100644
--- a/config_templates/config/chainstorage/solana/mainnet/production.template.yml
+++ b/config_templates/config/chainstorage/solana/mainnet/production.template.yml
@@ -1,5 +1,18 @@
aws:
aws_account: production
+ storage:
+ consolidation:
+ read_shadow_first: true
+storage_type:
+ blob: S3
+ meta: POSTGRES
+cadence:
+ keep_alive_time: 10s
+ keep_alive_timeout: 30s
+cron:
+ batch_consolidator:
+ spec: "@every 15m"
+ workflow_parallelism: 4
workflows:
backfiller:
num_concurrent_extractors: 150
diff --git a/config_templates/config/chainstorage/story/mainnet/base.template.yml b/config_templates/config/chainstorage/story/mainnet/base.template.yml
new file mode 100644
index 00000000..6297d5d7
--- /dev/null
+++ b/config_templates/config/chainstorage/story/mainnet/base.template.yml
@@ -0,0 +1,54 @@
+chain:
+ block_time: 2s
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ rosetta_parser: true
+ irreversible_distance: 10
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ event_height_delta: 60
+ event_time_delta: 2m
+ time_since_last_event: 2m30s
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ num_concurrent_extractors: 20
+ activity_start_to_close_timeout: 20m
+ cross_validator:
+ backoff_interval: 1s
+ parallelism: 10
+ validation_percentage: 100
+ poller:
+ backoff_interval: 0s
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ parallelism: 10
+ session_enabled: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/story/mainnet/development.template.yml b/config_templates/config/chainstorage/story/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/story/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/story/mainnet/local.template.yml b/config_templates/config/chainstorage/story/mainnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/story/mainnet/production.template.yml b/config_templates/config/chainstorage/story/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/tempo/mainnet/base.template.yml b/config_templates/config/chainstorage/tempo/mainnet/base.template.yml
new file mode 100644
index 00000000..6297d5d7
--- /dev/null
+++ b/config_templates/config/chainstorage/tempo/mainnet/base.template.yml
@@ -0,0 +1,54 @@
+chain:
+ block_time: 2s
+ feature:
+ block_validation_enabled: true
+ block_validation_muted: true
+ rosetta_parser: true
+ irreversible_distance: 10
+sla:
+ block_height_delta: 60
+ block_time_delta: 2m
+ out_of_sync_node_distance: 60
+ tier: 1
+ time_since_last_block: 2m30s
+ event_height_delta: 60
+ event_time_delta: 2m
+ time_since_last_event: 2m30s
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ num_concurrent_extractors: 20
+ activity_start_to_close_timeout: 20m
+ cross_validator:
+ backoff_interval: 1s
+ parallelism: 10
+ validation_percentage: 100
+ poller:
+ backoff_interval: 0s
+ consensus_validation: true
+ consensus_validation_muted: true
+ failover_enabled: true
+ parallelism: 10
+ session_enabled: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 0s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/tempo/mainnet/development.template.yml b/config_templates/config/chainstorage/tempo/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/tempo/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/tempo/mainnet/local.template.yml b/config_templates/config/chainstorage/tempo/mainnet/local.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/tempo/mainnet/local.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/tempo/mainnet/production.template.yml b/config_templates/config/chainstorage/tempo/mainnet/production.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/tempo/mainnet/production.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/tron/mainnet/base.template.yml b/config_templates/config/chainstorage/tron/mainnet/base.template.yml
new file mode 100644
index 00000000..d3245729
--- /dev/null
+++ b/config_templates/config/chainstorage/tron/mainnet/base.template.yml
@@ -0,0 +1,83 @@
+aws:
+ aws_account: development
+ bucket: example-chainstorage-{{blockchain}}-{{network}}-dev
+ dlq:
+ name: example_chainstorage_blocks_{{blockchain}}_{{network}}_dlq
+ dynamodb:
+ event_table: example_chainstorage_block_events_{{blockchain}}_{{network}}
+ event_table_height_index: example_chainstorage_block_events_by_height_{{blockchain}}_{{network}}
+ storage:
+ data_compression: ZSTD
+chain:
+ client:
+ consensus:
+ endpoint_group: ""
+ http_timeout: 0s
+ master:
+ endpoint_group: ""
+ slave:
+ endpoint_group: ""
+ validator:
+ endpoint_group: ""
+ additional:
+ endpoint_group: ""
+ block_tag:
+ latest: 2
+ stable: 2
+ block_time: 12s
+ event_tag:
+ latest: 3
+ stable: 3
+ irreversible_distance: 12
+ feature:
+ rosetta_parser: true
+ default_stable_event: true
+ block_validation_enabled: true
+ block_validation_muted: true
+sla:
+ block_height_delta: 10
+ block_time_delta: 2m
+ out_of_sync_node_distance: 10
+ tier: 1
+ time_since_last_block: 2m
+ event_height_delta: 10
+ event_time_delta: 2m
+ time_since_last_event: 2m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - cross_validator
+workflows:
+ backfiller:
+ checkpoint_size: 5000
+ num_concurrent_extractors: 24
+ cross_validator:
+ batch_size: 1000
+ validation_start_height: 15500000
+ validation_percentage: 1
+ poller:
+ parallelism: 10
+ failover_enabled: true
+ session_enabled: true
+ backoff_interval: 1s
+ consensus_validation: true
+ consensus_validation_muted: true
+ monitor:
+ failover_enabled: true
+ streamer:
+ backoff_interval: 1s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/tron/mainnet/development.template.yml b/config_templates/config/chainstorage/tron/mainnet/development.template.yml
new file mode 100644
index 00000000..86f80acf
--- /dev/null
+++ b/config_templates/config/chainstorage/tron/mainnet/development.template.yml
@@ -0,0 +1,42 @@
+chain:
+ event_tag:
+ latest: 2
+ stable: 0
+ feature:
+ default_stable_event: false
+aws:
+ aws_account: development
+sla:
+ block_height_delta: 12
+ block_time_delta: 3m
+ out_of_sync_node_distance: 12
+ time_since_last_block: 3m
+ event_height_delta: 12
+ event_time_delta: 3m
+ time_since_last_event: 3m
+ expected_workflows:
+ - monitor
+ - poller
+ - streamer
+ - streamer/event_tag=1
+ - streamer/event_tag=2
+ - cross_validator
+workflows:
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
+ poller:
+ failover_enabled: false
+ monitor:
+ failover_enabled: false
diff --git a/config_templates/config/chainstorage/tron/mainnet/local.template.yml b/config_templates/config/chainstorage/tron/mainnet/local.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/tron/mainnet/production.template.yml b/config_templates/config/chainstorage/tron/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/config_templates/config/chainstorage/zcash/mainnet/base.template.yml b/config_templates/config/chainstorage/zcash/mainnet/base.template.yml
new file mode 100644
index 00000000..49ddf282
--- /dev/null
+++ b/config_templates/config/chainstorage/zcash/mainnet/base.template.yml
@@ -0,0 +1,59 @@
+api:
+ max_num_blocks: 5
+ streaming_max_no_event_time: 30m
+aws:
+ dynamodb:
+ event_table: example_chainstorage_block_events_{{blockchain}}_{{network}}
+ event_table_height_index: example_chainstorage_block_events_by_height_{{blockchain}}_{{network}}
+chain:
+ client:
+ tx_batch_size: 100
+ block_tag:
+ latest: 1
+ stable: 1
+ event_tag:
+ latest: 0
+ stable: 0
+ block_time: 10m
+ irreversible_distance: 2
+ feature:
+ rosetta_parser: true
+cron:
+ block_range_size: 2
+ disable_dlq_processor: true
+sla:
+ block_height_delta: 5
+ block_time_delta: 1h
+ out_of_sync_node_distance: 10
+ tier: 2
+ time_since_last_block: 1h15m
+ event_height_delta: 5
+ event_time_delta: 1h
+ time_since_last_event: 1h15m
+workflows:
+ backfiller:
+ num_concurrent_extractors: 21
+ monitor:
+ checkpoint_size: 250
+ poller:
+ activity_heartbeat_timeout: 15m
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 10s
+ max_blocks_to_sync_per_cycle: 5
+ parallelism: 10
+ streamer:
+ backoff_interval: 10s
+ migrator:
+ activity_retry:
+ backoff_coefficient: 2
+ initial_interval: 10s
+ maximum_attempts: 3
+ maximum_interval: 3m
+ activity_schedule_to_close_timeout: 1h
+ activity_start_to_close_timeout: 30m
+ backoff_interval: 5s
+ batch_size: 1000
+ checkpoint_size: 5000
+ task_list: default
+ workflow_identity: workflow.migrator
+ workflow_run_timeout: 24h
diff --git a/config_templates/config/chainstorage/zcash/mainnet/development.template.yml b/config_templates/config/chainstorage/zcash/mainnet/development.template.yml
new file mode 100644
index 00000000..8b137891
--- /dev/null
+++ b/config_templates/config/chainstorage/zcash/mainnet/development.template.yml
@@ -0,0 +1 @@
+
diff --git a/config_templates/config/chainstorage/zcash/mainnet/local.template.yml b/config_templates/config/chainstorage/zcash/mainnet/local.template.yml
new file mode 100644
index 00000000..2f36c7b8
--- /dev/null
+++ b/config_templates/config/chainstorage/zcash/mainnet/local.template.yml
@@ -0,0 +1,30 @@
+
+chain:
+ client:
+ master:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: ""
+ weight: 1
+ rps: 1
+ slave:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: ""
+ weight: 1
+ rps: 1
+ validator:
+ endpoint_group:
+ endpoints:
+ - name: getblock
+ url: ""
+ weight: 1
+ rps: 1
+workflows:
+ poller:
+ failover_enabled: false
+ monitor:
+ failover_enabled: false
+
diff --git a/config_templates/config/chainstorage/zcash/mainnet/production.template.yml b/config_templates/config/chainstorage/zcash/mainnet/production.template.yml
new file mode 100644
index 00000000..e69de29b
diff --git a/docker-compose-local-dev.yml b/docker-compose-local-dev.yml
new file mode 100644
index 00000000..e7d6c9e9
--- /dev/null
+++ b/docker-compose-local-dev.yml
@@ -0,0 +1,126 @@
+version: "3"
+volumes:
+ localstack_data: {}
+ temporal_postgres_data: {}
+ chainstorage_postgres_data: {}
+services:
+ localstack:
+ image: localstack/localstack:3.1.0
+ ports:
+ - 4566:4566
+ - 4510-4559:4510-4559 # external services port range
+ environment:
+ - DEBUG=1
+ - DOCKER_HOST=unix:///var/run/docker.sock
+ - AWS_DEFAULT_REGION=us-east-1
+ - AWS_ACCESS_KEY_ID=requirednotused
+ - AWS_SECRET_ACCESS_KEY=requirednotused
+ volumes:
+ - /var/run/docker.sock:/var/run/docker.sock
+ - ./bin/localstack:/docker-entrypoint-initaws.d
+ - ./bin/localstack/policies:/policies
+ - ./dev-data/localstack:/var/lib/localstack
+ temporal-postgres:
+ image: postgres:15.6-alpine
+ ports:
+ - ${TEMPORAL_POSTGRES_PORT:-5432}:5432
+ environment:
+ - POSTGRES_USER=temporal
+ - POSTGRES_PASSWORD=temporal
+ - POSTGRES_DB=temporal
+ volumes:
+ - ./dev-data/temporal-pg:/var/lib/postgresql/data
+ - ./scripts/init-temporal-postgres.sh:/docker-entrypoint-initdb.d/init-temporal-postgres.sh
+
+ chainstorage-postgres:
+ image: postgres:15.6-alpine
+ ports:
+ - ${CHAINSTORAGE_POSTGRES_PORT:-5433}:5432
+ environment:
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASSWORD=postgres
+ - POSTGRES_DB=postgres
+ # Shared passwords for the per-network roles created by the init script
+ - CHAINSTORAGE_WORKER_PASSWORD=worker_password
+ - CHAINSTORAGE_SERVER_PASSWORD=server_password
+ volumes:
+ - ./dev-data/chainstorage-pg:/var/lib/postgresql/data
+ - ./scripts/init-local-postgres.sh:/docker-entrypoint-initdb.d/init-local-postgres.sh
+
+ temporal:
+ image: temporalio/auto-setup:1.22.4
+ ports:
+ - 7233:7233
+ labels:
+ service_group: temporal
+ environment:
+ - DB=postgresql
+ - DB_PORT=5432
+ - POSTGRES_USER=temporal
+ - POSTGRES_PWD=temporal
+ - POSTGRES_SEEDS=temporal-postgres
+ - USE_HOSTNAME_IP=true
+ restart: always
+ depends_on:
+ - temporal-postgres
+ temporal-ui:
+ image: temporalio/ui:2.23.0
+ ports:
+ - 8088:8080
+ labels:
+ service_group: temporal-web
+ environment:
+ - TEMPORAL_CSRF_COOKIE_INSECURE=true
+ - TEMPORAL_ADDRESS=temporal:7233
+ depends_on:
+ - temporal
+ temporal-admin-tools:
+ depends_on:
+ - temporal
+ environment:
+ - TEMPORAL_ADDRESS=temporal:7233
+ - TEMPORAL_CLI_ADDRESS=temporal:7233
+ image: temporalio/admin-tools:1.22.0
+ stdin_open: true
+ tty: true
+ s3manager:
+ image: cloudlena/s3manager:latest
+ ports:
+ - 8081:8080
+ environment:
+ - ENDPOINT=localstack:4566
+ - REGION=us-east-1
+ - ACCESS_KEY_ID=requirednotused
+ - SECRET_ACCESS_KEY=requirednotused
+ - USE_SSL=false
+ depends_on:
+ - localstack
+ dynamodb-admin:
+ image: aaronshaf/dynamodb-admin:4.6.1
+ ports:
+ - 8082:8001
+ environment:
+ - DYNAMO_ENDPOINT=localstack:4566
+ - REGION=us-east-1
+ - ACCESS_KEY_ID=requirednotused
+ - SECRET_ACCESS_KEY=requirednotused
+ - USE_SSL=false
+ depends_on:
+ - localstack
+ # chainstorage-server:
+ # image: pika-chainstorage
+ # command: ["/app/server"]
+ # ports:
+ # - 9090:9090
+ # environment:
+ # - CHAINSTORAGE_CONFIG=tron_mainnet
+ # - CHAINSTORAGE_SDK_AUTH_HEADER=cb-nft-api-token
+ # - CHAINSTORAGE_SDK_AUTH_TOKEN=123321
+ # depends_on:
+ # - localstack
+ # - temporal
+ # restart: always
+ # network_mode: host
+
+# SETUP: Just run 'docker-compose -f docker-compose-local-dev.yml up -d'
+# Everything is automatically set up - databases, roles, and permissions
\ No newline at end of file
diff --git a/docker-compose-testing.yml b/docker-compose-testing.yml
index db728013..80346d04 100644
--- a/docker-compose-testing.yml
+++ b/docker-compose-testing.yml
@@ -2,7 +2,7 @@ version: "3"
services:
localstack:
- image: localstack/localstack:2.3.2
+ image: localstack/localstack:3.1.0
ports:
- 4566:4566
- 4510-4559:4510-4559 # external services port range
@@ -14,3 +14,23 @@ services:
- AWS_SECRET_ACCESS_KEY=requirednotused
volumes:
- /var/run/docker.sock:/var/run/docker.sock
+ postgres:
+ image: postgres:13
+ ports:
+ - "5433:5432"
+ environment:
+ - POSTGRES_USER=postgres
+ - POSTGRES_PASSWORD=postgres
+ - POSTGRES_DB=postgres
+ # Shared passwords for the per-network roles created by the init script
+ - CHAINSTORAGE_WORKER_PASSWORD=worker_password
+ - CHAINSTORAGE_SERVER_PASSWORD=server_password
+ command: >
+ postgres
+ -c ssl=on
+ -c ssl_cert_file=/etc/ssl/certs/ssl-cert-snakeoil.pem
+ -c ssl_key_file=/etc/ssl/private/ssl-cert-snakeoil.key
+ volumes:
+ # Creates roles and databases automatically
+ - ./scripts/init-local-postgres.sh:/docker-entrypoint-initdb.d/init-local-postgres.sh
+ restart: always
\ No newline at end of file
diff --git a/docs/reports/INF-914-cscb-dual-read-validation.json b/docs/reports/INF-914-cscb-dual-read-validation.json
new file mode 100644
index 00000000..932d030f
--- /dev/null
+++ b/docs/reports/INF-914-cscb-dual-read-validation.json
@@ -0,0 +1,1347 @@
+{
+ "generated_at": "2026-06-22T05:28:49Z",
+ "command": "/Users/boyangxu/.cache/go/build/9d/9d56949bbf7c987975c8069d8d0715461b3dfd2181f50913602759aaaff0ad2f-d/admin --blockchain solana --network mainnet --env production cscb validate --start-height 428058000 --end-height 428059000 --chunk-blocks 25 --random-samples 5 --random-seed 914 --iterations 3 --full-object-iterations 1 --fallback-height 428057999 --timeout 5m --server-address localhost:19090 --grpc --report-json docs/reports/INF-914-cscb-dual-read-validation.json --report-md docs/reports/INF-914-cscb-dual-read-validation.md",
+ "environment": "production",
+ "blockchain": "BLOCKCHAIN_SOLANA",
+ "network": "NETWORK_SOLANA_MAINNET",
+ "tag": 2,
+ "start_height": 428058000,
+ "end_height": 428059000,
+ "chunk_blocks": 25,
+ "random_seed": 914,
+ "random_samples": 5,
+ "iterations": 3,
+ "full_object_iterations": 1,
+ "fallback_height": 428057999,
+ "server_address_override": "localhost:19090",
+ "grpc_transport_override": true,
+ "client_id": "INF-914-cscb-dual-read-validation",
+ "passed": true,
+ "total_cases": 18,
+ "passed_cases": 18,
+ "failed_cases": 0,
+ "correctness_mismatches": 0,
+ "observed_fallback_blocks": 1,
+ "cases": [
+ {
+ "name": "single_first_block",
+ "kind": "single",
+ "start_height": 428058000,
+ "end_height": 428058001,
+ "heights": [
+ 428058000
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 1389.387,
+ "p95_ms": 1728.344,
+ "min_ms": 1251.48,
+ "max_ms": 1728.344
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 2847939
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058000,
+ "last_height": 428058000
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 553.336,
+ "p95_ms": 571.616,
+ "min_ms": 460.878,
+ "max_ms": 571.616
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 2882226
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 8915187,
+ "metadata_uncompressed_bytes": 8915187,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058000,
+ "last_height": 428058000
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 0.3982590883605504,
+ "bytes_ratio_consolidated_vs_legacy": 1.0120392325818777,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "0e0e470e962f11735a8de95a692a8d5c0542cddace6f2c5f4e7481a889a11df7",
+ "first_canonical_hash_consolidated": "0e0e470e962f11735a8de95a692a8d5c0542cddace6f2c5f4e7481a889a11df7"
+ },
+ {
+ "name": "single_last_block",
+ "kind": "single",
+ "start_height": 428058999,
+ "end_height": 428059000,
+ "heights": [
+ 428058999
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 218.822,
+ "p95_ms": 1013.836,
+ "min_ms": 198.491,
+ "max_ms": 1013.836
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 2247348
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058999,
+ "last_height": 428058999
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 399.035,
+ "p95_ms": 521.437,
+ "min_ms": 362.858,
+ "max_ms": 521.437
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 3777807
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 6842578,
+ "metadata_uncompressed_bytes": 6842578,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058999,
+ "last_height": 428058999
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 1.8235597883211012,
+ "bytes_ratio_consolidated_vs_legacy": 1.681006679873344,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "c3b881848de06cadbedb95271bfdc6992ff752c5a010f4b708abf22a8d7dbe07",
+ "first_canonical_hash_consolidated": "c3b881848de06cadbedb95271bfdc6992ff752c5a010f4b708abf22a8d7dbe07"
+ },
+ {
+ "name": "single_chunk_boundary_428058024",
+ "kind": "single",
+ "start_height": 428058024,
+ "end_height": 428058025,
+ "heights": [
+ 428058024
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 207.551,
+ "p95_ms": 288.986,
+ "min_ms": 175.27,
+ "max_ms": 288.986
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 3084087
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058024,
+ "last_height": 428058024
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 1039.391,
+ "p95_ms": 1052.372,
+ "min_ms": 936.045,
+ "max_ms": 1052.372
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 39109401
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 10758103,
+ "metadata_uncompressed_bytes": 10758103,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058024,
+ "last_height": 428058024
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 5.007882399988437,
+ "bytes_ratio_consolidated_vs_legacy": 12.68103039894789,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "62b48b812677a5b3c5042d9fcd0113561e6cabc3f679586645aa1f2024cb7d00",
+ "first_canonical_hash_consolidated": "62b48b812677a5b3c5042d9fcd0113561e6cabc3f679586645aa1f2024cb7d00"
+ },
+ {
+ "name": "single_chunk_boundary_428058025",
+ "kind": "single",
+ "start_height": 428058025,
+ "end_height": 428058026,
+ "heights": [
+ 428058025
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 681.192,
+ "p95_ms": 688.676,
+ "min_ms": 309.035,
+ "max_ms": 688.676
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 2469318
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058025,
+ "last_height": 428058025
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 417.139,
+ "p95_ms": 441.671,
+ "min_ms": 344.894,
+ "max_ms": 441.671
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 2451210
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 7925570,
+ "metadata_uncompressed_bytes": 7925570,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058025,
+ "last_height": 428058025
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 0.6123662638433804,
+ "bytes_ratio_consolidated_vs_legacy": 0.9926668011167455,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "07f3cf2f7356f80d7f9b3928ff11888187c7c3af7d2f0d896a5e670643d66630",
+ "first_canonical_hash_consolidated": "07f3cf2f7356f80d7f9b3928ff11888187c7c3af7d2f0d896a5e670643d66630"
+ },
+ {
+ "name": "single_chunk_boundary_428058049",
+ "kind": "single",
+ "start_height": 428058049,
+ "end_height": 428058050,
+ "heights": [
+ 428058049
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 190.734,
+ "p95_ms": 710.165,
+ "min_ms": 176.139,
+ "max_ms": 710.165
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 2358117
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058049,
+ "last_height": 428058049
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 872.054,
+ "p95_ms": 897.705,
+ "min_ms": 782.807,
+ "max_ms": 897.705
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 33572286
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 7593128,
+ "metadata_uncompressed_bytes": 7593128,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058049,
+ "last_height": 428058049
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 4.572095169188503,
+ "bytes_ratio_consolidated_vs_legacy": 14.236904275742043,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "636c30d02e3390276e0091ca9928603d8f1e507ef5be9ccd818a99f644732028",
+ "first_canonical_hash_consolidated": "636c30d02e3390276e0091ca9928603d8f1e507ef5be9ccd818a99f644732028"
+ },
+ {
+ "name": "single_chunk_boundary_428058050",
+ "kind": "single",
+ "start_height": 428058050,
+ "end_height": 428058051,
+ "heights": [
+ 428058050
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 658.691,
+ "p95_ms": 698.487,
+ "min_ms": 251.748,
+ "max_ms": 698.487
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 2188680
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058050,
+ "last_height": 428058050
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 452.178,
+ "p95_ms": 471.371,
+ "min_ms": 360.972,
+ "max_ms": 471.371
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 2255577
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 7301670,
+ "metadata_uncompressed_bytes": 7301670,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058050,
+ "last_height": 428058050
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 0.6864796998896295,
+ "bytes_ratio_consolidated_vs_legacy": 1.0305649980810352,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "5c5d5d0db508d736390aaffd5a4470c3f7ffddb32a5967d280b0b556e59b4c19",
+ "first_canonical_hash_consolidated": "5c5d5d0db508d736390aaffd5a4470c3f7ffddb32a5967d280b0b556e59b4c19"
+ },
+ {
+ "name": "single_chunk_boundary_428058074",
+ "kind": "single",
+ "start_height": 428058074,
+ "end_height": 428058075,
+ "heights": [
+ 428058074
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 208.264,
+ "p95_ms": 766.694,
+ "min_ms": 176.772,
+ "max_ms": 766.694
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 3051528
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058074,
+ "last_height": 428058074
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 912.411,
+ "p95_ms": 1308.56,
+ "min_ms": 875.434,
+ "max_ms": 1308.56
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 36464772
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 10799431,
+ "metadata_uncompressed_bytes": 10799431,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058074,
+ "last_height": 428058074
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 4.3810308070525865,
+ "bytes_ratio_consolidated_vs_legacy": 11.949676358860216,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "68f1ef051c791e532127a753fb58da3677d5bce8f7bb7c5bceca7ab9ff44ab7c",
+ "first_canonical_hash_consolidated": "68f1ef051c791e532127a753fb58da3677d5bce8f7bb7c5bceca7ab9ff44ab7c"
+ },
+ {
+ "name": "single_chunk_boundary_428058075",
+ "kind": "single",
+ "start_height": 428058075,
+ "end_height": 428058076,
+ "heights": [
+ 428058075
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 684.618,
+ "p95_ms": 1262.177,
+ "min_ms": 199.987,
+ "max_ms": 1262.177
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 2627094
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058075,
+ "last_height": 428058075
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 398.145,
+ "p95_ms": 501.07,
+ "min_ms": 382.178,
+ "max_ms": 501.07
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 2465205
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 8888611,
+ "metadata_uncompressed_bytes": 8888611,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058075,
+ "last_height": 428058075
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 0.5815578906777209,
+ "bytes_ratio_consolidated_vs_legacy": 0.9383771574218509,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "f0faa51f65d2278bfde95eb2444ccd22386d40d3c6b79bd4ff2a5e9b913edf57",
+ "first_canonical_hash_consolidated": "f0faa51f65d2278bfde95eb2444ccd22386d40d3c6b79bd4ff2a5e9b913edf57"
+ },
+ {
+ "name": "single_random_428058100",
+ "kind": "single",
+ "start_height": 428058100,
+ "end_height": 428058101,
+ "heights": [
+ 428058100
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 718.612,
+ "p95_ms": 1282.682,
+ "min_ms": 700.807,
+ "max_ms": 1282.682
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 2908002
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058100,
+ "last_height": 428058100
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 421.256,
+ "p95_ms": 423.046,
+ "min_ms": 349.698,
+ "max_ms": 423.046
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 2756445
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 9653365,
+ "metadata_uncompressed_bytes": 9653365,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058100,
+ "last_height": 428058100
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 0.5862078562562273,
+ "bytes_ratio_consolidated_vs_legacy": 0.9478827731205137,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "178de0a69cedd530d65e26b5c0657114e251d15dccb29e9c61955342e52dda83",
+ "first_canonical_hash_consolidated": "178de0a69cedd530d65e26b5c0657114e251d15dccb29e9c61955342e52dda83"
+ },
+ {
+ "name": "single_random_428058118",
+ "kind": "single",
+ "start_height": 428058118,
+ "end_height": 428058119,
+ "heights": [
+ 428058118
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 1272.78,
+ "p95_ms": 1463.85,
+ "min_ms": 1212.492,
+ "max_ms": 1463.85
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 1939224
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058118,
+ "last_height": 428058118
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 3170.186,
+ "p95_ms": 6313.939,
+ "min_ms": 2924.594,
+ "max_ms": 6313.939
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 30511797
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 6627227,
+ "metadata_uncompressed_bytes": 6627227,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058118,
+ "last_height": 428058118
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 2.490757240057198,
+ "bytes_ratio_consolidated_vs_legacy": 15.734024021979925,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "382f8598c23a17710a3abb7162ecd9cdfebd5b15a7ef883ed22fbed73c086397",
+ "first_canonical_hash_consolidated": "382f8598c23a17710a3abb7162ecd9cdfebd5b15a7ef883ed22fbed73c086397"
+ },
+ {
+ "name": "single_random_428058141",
+ "kind": "single",
+ "start_height": 428058141,
+ "end_height": 428058142,
+ "heights": [
+ 428058141
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 984.129,
+ "p95_ms": 1626.596,
+ "min_ms": 895.663,
+ "max_ms": 1626.596
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 3889650
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058141,
+ "last_height": 428058141
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 3797.58,
+ "p95_ms": 4665.425,
+ "min_ms": 2487.59,
+ "max_ms": 4665.425
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 36896916
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 11708771,
+ "metadata_uncompressed_bytes": 11708771,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058141,
+ "last_height": 428058141
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 3.858823385958548,
+ "bytes_ratio_consolidated_vs_legacy": 9.485921869576954,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "7f6b184b49151bdd44b8b2b2b1fe219d2678593ebf5cb7e384925921c4e3356b",
+ "first_canonical_hash_consolidated": "7f6b184b49151bdd44b8b2b2b1fe219d2678593ebf5cb7e384925921c4e3356b"
+ },
+ {
+ "name": "single_random_428058339",
+ "kind": "single",
+ "start_height": 428058339,
+ "end_height": 428058340,
+ "heights": [
+ 428058339
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 913.407,
+ "p95_ms": 1296.782,
+ "min_ms": 888.824,
+ "max_ms": 1296.782
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 2794089
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058339,
+ "last_height": 428058339
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 1415.481,
+ "p95_ms": 2200.593,
+ "min_ms": 1096.766,
+ "max_ms": 2200.593
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 29017683
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 8447888,
+ "metadata_uncompressed_bytes": 8447888,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058339,
+ "last_height": 428058339
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 1.5496717235580633,
+ "bytes_ratio_consolidated_vs_legacy": 10.385382498553195,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "d7a2448d89026352190c721845f3f317e23311605a6112526ba7a739ae777066",
+ "first_canonical_hash_consolidated": "d7a2448d89026352190c721845f3f317e23311605a6112526ba7a739ae777066"
+ },
+ {
+ "name": "single_random_428058828",
+ "kind": "single",
+ "start_height": 428058828,
+ "end_height": 428058829,
+ "heights": [
+ 428058828
+ ],
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 1492.801,
+ "p95_ms": 1633.386,
+ "min_ms": 881.652,
+ "max_ms": 1633.386
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 3594321
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058828,
+ "last_height": 428058828
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 922.264,
+ "p95_ms": 978.437,
+ "min_ms": 605.366,
+ "max_ms": 978.437
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 11397528
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1,
+ "skipped_count": 0,
+ "metadata_byte_length": 12007263,
+ "metadata_uncompressed_bytes": 12007263,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058828,
+ "last_height": 428058828
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 0.6178077319080039,
+ "bytes_ratio_consolidated_vs_legacy": 3.170982224459084,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "25ee3596b012244de477ca5a4d79864a0472cd6dcc8a985010d0ebf87b03bd5f",
+ "first_canonical_hash_consolidated": "25ee3596b012244de477ca5a4d79864a0472cd6dcc8a985010d0ebf87b03bd5f"
+ },
+ {
+ "name": "range_within_one_chunk",
+ "kind": "range",
+ "start_height": 428058005,
+ "end_height": 428058015,
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 2996.342,
+ "p95_ms": 3001.567,
+ "min_ms": 2891.89,
+ "max_ms": 3001.567
+ },
+ "http": {
+ "requests": 30,
+ "bytes": 22951683
+ },
+ "files": {
+ "count": 10,
+ "legacy_object_count": 10,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058005,
+ "last_height": 428058014
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 2013.972,
+ "p95_ms": 2089.991,
+ "min_ms": 1583.112,
+ "max_ms": 2089.991
+ },
+ "http": {
+ "requests": 9,
+ "bytes": 24104910
+ },
+ "files": {
+ "count": 10,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 10,
+ "skipped_count": 0,
+ "metadata_byte_length": 72347946,
+ "metadata_uncompressed_bytes": 72347946,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058005,
+ "last_height": 428058014
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 0.672143567056097,
+ "bytes_ratio_consolidated_vs_legacy": 1.0502458577874223,
+ "canonical_hashes_compared": 10,
+ "first_canonical_hash_legacy": "41960aeeec354c48c1f133a447f79cd4a96954187a76447e4683d91f5239ddad",
+ "first_canonical_hash_consolidated": "41960aeeec354c48c1f133a447f79cd4a96954187a76447e4683d91f5239ddad"
+ },
+ {
+ "name": "range_across_chunk_boundary",
+ "kind": "range",
+ "start_height": 428058020,
+ "end_height": 428058030,
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 2977.525,
+ "p95_ms": 3042.113,
+ "min_ms": 1368.422,
+ "max_ms": 3042.113
+ },
+ "http": {
+ "requests": 30,
+ "bytes": 21927276
+ },
+ "files": {
+ "count": 10,
+ "legacy_object_count": 10,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058020,
+ "last_height": 428058029
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 3097.036,
+ "p95_ms": 5206.642,
+ "min_ms": 2334.32,
+ "max_ms": 5206.642
+ },
+ "http": {
+ "requests": 12,
+ "bytes": 45903672
+ },
+ "files": {
+ "count": 10,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 10,
+ "skipped_count": 0,
+ "metadata_byte_length": 72287021,
+ "metadata_uncompressed_bytes": 72287021,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058020,
+ "last_height": 428058029
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 1.040137698256102,
+ "bytes_ratio_consolidated_vs_legacy": 2.093450732320786,
+ "canonical_hashes_compared": 10,
+ "first_canonical_hash_legacy": "a90299cae6b502a6bf518c887995fd04f680fef17b45e891371a078353ac7419",
+ "first_canonical_hash_consolidated": "a90299cae6b502a6bf518c887995fd04f680fef17b45e891371a078353ac7419"
+ },
+ {
+ "name": "range_two_chunks",
+ "kind": "range",
+ "start_height": 428058000,
+ "end_height": 428058050,
+ "iterations": 3,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 4055.683,
+ "p95_ms": 4705.356,
+ "min_ms": 3396.787,
+ "max_ms": 4705.356
+ },
+ "http": {
+ "requests": 150,
+ "bytes": 118865385
+ },
+ "files": {
+ "count": 50,
+ "legacy_object_count": 50,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058000,
+ "last_height": 428058049
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 4934.945,
+ "p95_ms": 5072.744,
+ "min_ms": 2676.124,
+ "max_ms": 5072.744
+ },
+ "http": {
+ "requests": 12,
+ "bytes": 72384303
+ },
+ "files": {
+ "count": 50,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 50,
+ "skipped_count": 0,
+ "metadata_byte_length": 385274802,
+ "metadata_uncompressed_bytes": 385274802,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058000,
+ "last_height": 428058049
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 1.2167975159794293,
+ "bytes_ratio_consolidated_vs_legacy": 0.6089603209546665,
+ "canonical_hashes_compared": 50,
+ "first_canonical_hash_legacy": "0e0e470e962f11735a8de95a692a8d5c0542cddace6f2c5f4e7481a889a11df7",
+ "first_canonical_hash_consolidated": "0e0e470e962f11735a8de95a692a8d5c0542cddace6f2c5f4e7481a889a11df7"
+ },
+ {
+ "name": "range_full_object",
+ "kind": "range",
+ "start_height": 428058000,
+ "end_height": 428059000,
+ "iterations": 1,
+ "expect_fallback": false,
+ "observed_fallback": false,
+ "observed_fallback_blocks": 0,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 1,
+ "p50_ms": 103929.955,
+ "p95_ms": 103929.955,
+ "min_ms": 103929.955,
+ "max_ms": 103929.955
+ },
+ "http": {
+ "requests": 1000,
+ "bytes": 907486583
+ },
+ "files": {
+ "count": 1000,
+ "legacy_object_count": 1000,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428058000,
+ "last_height": 428058999
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 1,
+ "p50_ms": 39572.801,
+ "p95_ms": 39572.801,
+ "min_ms": 39572.801,
+ "max_ms": 39572.801
+ },
+ "http": {
+ "requests": 43,
+ "bytes": 567346484
+ },
+ "files": {
+ "count": 1000,
+ "legacy_object_count": 0,
+ "consolidated_object_count": 1000,
+ "skipped_count": 0,
+ "metadata_byte_length": 8876157284,
+ "metadata_uncompressed_bytes": 8876157284,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH"
+ ],
+ "first_height": 428058000,
+ "last_height": 428058999
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 0.3807641502394569,
+ "bytes_ratio_consolidated_vs_legacy": 0.6251844320655923,
+ "canonical_hashes_compared": 1000,
+ "first_canonical_hash_legacy": "0e0e470e962f11735a8de95a692a8d5c0542cddace6f2c5f4e7481a889a11df7",
+ "first_canonical_hash_consolidated": "0e0e470e962f11735a8de95a692a8d5c0542cddace6f2c5f4e7481a889a11df7"
+ },
+ {
+ "name": "single_consolidated_miss_fallback",
+ "kind": "single",
+ "start_height": 428057999,
+ "end_height": 428058000,
+ "heights": [
+ 428057999
+ ],
+ "iterations": 3,
+ "expect_fallback": true,
+ "observed_fallback": true,
+ "observed_fallback_blocks": 1,
+ "correct": true,
+ "passed": true,
+ "legacy": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 447.05,
+ "p95_ms": 2794.868,
+ "min_ms": 420.81,
+ "max_ms": 2794.868
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 3351267
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428057999,
+ "last_height": 428057999
+ },
+ "error_count": 0
+ },
+ "consolidated": {
+ "latency": {
+ "samples": 3,
+ "p50_ms": 421.771,
+ "p95_ms": 805.597,
+ "min_ms": 412.696,
+ "max_ms": 805.597
+ },
+ "http": {
+ "requests": 3,
+ "bytes": 3351267
+ },
+ "files": {
+ "count": 1,
+ "legacy_object_count": 1,
+ "consolidated_object_count": 0,
+ "skipped_count": 0,
+ "metadata_byte_length": 0,
+ "metadata_uncompressed_bytes": 0,
+ "formats": [
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK"
+ ],
+ "first_height": 428057999,
+ "last_height": 428057999
+ },
+ "error_count": 0
+ },
+ "latency_ratio_consolidated_vs_legacy": 0.9434537523766916,
+ "bytes_ratio_consolidated_vs_legacy": 1,
+ "canonical_hashes_compared": 1,
+ "first_canonical_hash_legacy": "f1762d11493396aba7147080fa3f96964ddbd0daf07c06af08df04ef347973d2",
+ "first_canonical_hash_consolidated": "f1762d11493396aba7147080fa3f96964ddbd0daf07c06af08df04ef347973d2"
+ }
+ ]
+}
diff --git a/docs/reports/INF-914-cscb-dual-read-validation.md b/docs/reports/INF-914-cscb-dual-read-validation.md
new file mode 100644
index 00000000..d56a3cdf
--- /dev/null
+++ b/docs/reports/INF-914-cscb-dual-read-validation.md
@@ -0,0 +1,164 @@
+# CSCB Dual-Read Validation Report
+
+- generated_at: `2026-06-22T05:28:49Z`
+- target: `production/BLOCKCHAIN_SOLANA/NETWORK_SOLANA_MAINNET`
+- tag: `2`
+- canary_range: `[428058000, 428059000)`
+- chunk_blocks: `25`
+- server_address_override: `localhost:19090`
+- grpc_transport_override: `true`
+- client_id: `INF-914-cscb-dual-read-validation`
+- passed: `true`
+- cases: `18 passed / 18 total`
+- mismatch_heights: `0`
+- observed_fallback_blocks: `1`
+
+## Canary Object and Expected Profile
+
+- consolidated_object: `BLOCKCHAIN_SOLANA/NETWORK_SOLANA_MAINNET/consolidated/v=2/shard=00000000000428050000-00000000000428060000/00000000000428058000-00000000000428059000-20a1b269fb75137d829d4ade11b3cdf67bab5c3a8c9be046fd8069396607b906.cscb.zstd`
+- legacy_baseline: `1000 objects`, `907486583 bytes`, `865.45 MiB`
+- consolidated_object_size: `567346484 bytes`, `541.06 MiB`, `37.48% saving`
+- selected_config: `max_blocks=1000`, `compression_chunk_blocks=25`, `zstd_level=12`, `long_window_log=27`, `memory_budget=256MiB`
+
+## Production Execution Evidence
+
+- connection: read-only gRPC validation through `kubectl --context prod port-forward -n chainstorage-prod svc/chainstorage-solana-mainnet-prod-server-svc 19090:9090`
+- target_pod: `chainstorage-prod/chainstorage-solana-mainnet-prod-server-6f7bd7b5bb-9k7pr`
+- safety: prod operations were read-only (`get`, `top`, `logs`, `port-forward`, and Chainstorage read APIs); no database writes or object mutations were performed.
+- pre_validation_resources: `chainstorage-server CPU=2m RSS=15Mi restarts=0 ready=true`; `linkerd-proxy CPU=1m RSS=3Mi restarts=0 ready=true`
+- post_validation_resources: `chainstorage-server CPU=2m RSS=23Mi restarts=0 ready=true`; `linkerd-proxy CPU=1m RSS=3Mi restarts=0 ready=true`
+- post_validation_error_logs: no matching `error|panic|fatal|fallback|cscb|shadow|INF-914` lines in `chainstorage-server` logs over the validation window.
+- full_object_result: legacy `1000` HTTP reads / `907486583` bytes / p50 `103929.96ms`; consolidated `43` HTTP reads / `567346484` bytes / p50 `39572.80ms`; canonical hashes matched for all `1000` blocks.
+
+## Case Results
+
+| case | range | pass | fallback | legacy p50/p95 ms | consolidated p50/p95 ms | legacy bytes | consolidated bytes | mismatches |
+|---|---:|---:|---:|---:|---:|---:|---:|---:|
+| single_first_block | `[428058000,428058001)` | true | false | 1389.39 / 1728.34 | 553.34 / 571.62 | 2847939 | 2882226 | |
+| single_last_block | `[428058999,428059000)` | true | false | 218.82 / 1013.84 | 399.04 / 521.44 | 2247348 | 3777807 | |
+| single_chunk_boundary_428058024 | `[428058024,428058025)` | true | false | 207.55 / 288.99 | 1039.39 / 1052.37 | 3084087 | 39109401 | |
+| single_chunk_boundary_428058025 | `[428058025,428058026)` | true | false | 681.19 / 688.68 | 417.14 / 441.67 | 2469318 | 2451210 | |
+| single_chunk_boundary_428058049 | `[428058049,428058050)` | true | false | 190.73 / 710.16 | 872.05 / 897.71 | 2358117 | 33572286 | |
+| single_chunk_boundary_428058050 | `[428058050,428058051)` | true | false | 658.69 / 698.49 | 452.18 / 471.37 | 2188680 | 2255577 | |
+| single_chunk_boundary_428058074 | `[428058074,428058075)` | true | false | 208.26 / 766.69 | 912.41 / 1308.56 | 3051528 | 36464772 | |
+| single_chunk_boundary_428058075 | `[428058075,428058076)` | true | false | 684.62 / 1262.18 | 398.14 / 501.07 | 2627094 | 2465205 | |
+| single_random_428058100 | `[428058100,428058101)` | true | false | 718.61 / 1282.68 | 421.26 / 423.05 | 2908002 | 2756445 | |
+| single_random_428058118 | `[428058118,428058119)` | true | false | 1272.78 / 1463.85 | 3170.19 / 6313.94 | 1939224 | 30511797 | |
+| single_random_428058141 | `[428058141,428058142)` | true | false | 984.13 / 1626.60 | 3797.58 / 4665.43 | 3889650 | 36896916 | |
+| single_random_428058339 | `[428058339,428058340)` | true | false | 913.41 / 1296.78 | 1415.48 / 2200.59 | 2794089 | 29017683 | |
+| single_random_428058828 | `[428058828,428058829)` | true | false | 1492.80 / 1633.39 | 922.26 / 978.44 | 3594321 | 11397528 | |
+| range_within_one_chunk | `[428058005,428058015)` | true | false | 2996.34 / 3001.57 | 2013.97 / 2089.99 | 22951683 | 24104910 | |
+| range_across_chunk_boundary | `[428058020,428058030)` | true | false | 2977.53 / 3042.11 | 3097.04 / 5206.64 | 21927276 | 45903672 | |
+| range_two_chunks | `[428058000,428058050)` | true | false | 4055.68 / 4705.36 | 4934.94 / 5072.74 | 118865385 | 72384303 | |
+| range_full_object | `[428058000,428059000)` | true | false | 103929.96 / 103929.96 | 39572.80 / 39572.80 | 907486583 | 567346484 | |
+| single_consolidated_miss_fallback | `[428057999,428058000)` | true | true | 447.05 / 2794.87 | 421.77 / 805.60 | 3351267 | 3351267 | |
+
+## Source Summary
+
+### single_first_block
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `2847939`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `2882226`
+- canonical hashes compared: `1`
+
+### single_last_block
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `2247348`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `3777807`
+- canonical hashes compared: `1`
+
+### single_chunk_boundary_428058024
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `3084087`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `39109401`
+- canonical hashes compared: `1`
+
+### single_chunk_boundary_428058025
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `2469318`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `2451210`
+- canonical hashes compared: `1`
+
+### single_chunk_boundary_428058049
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `2358117`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `33572286`
+- canonical hashes compared: `1`
+
+### single_chunk_boundary_428058050
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `2188680`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `2255577`
+- canonical hashes compared: `1`
+
+### single_chunk_boundary_428058074
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `3051528`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `36464772`
+- canonical hashes compared: `1`
+
+### single_chunk_boundary_428058075
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `2627094`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `2465205`
+- canonical hashes compared: `1`
+
+### single_random_428058100
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `2908002`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `2756445`
+- canonical hashes compared: `1`
+
+### single_random_428058118
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `1939224`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `30511797`
+- canonical hashes compared: `1`
+
+### single_random_428058141
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `3889650`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `36896916`
+- canonical hashes compared: `1`
+
+### single_random_428058339
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `2794089`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `29017683`
+- canonical hashes compared: `1`
+
+### single_random_428058828
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `3594321`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `11397528`
+- canonical hashes compared: `1`
+
+### range_within_one_chunk
+
+- legacy files: `10`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `30`, bytes: `22951683`
+- consolidated files: `10`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `9`, bytes: `24104910`
+- canonical hashes compared: `10`
+
+### range_across_chunk_boundary
+
+- legacy files: `10`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `30`, bytes: `21927276`
+- consolidated files: `10`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `12`, bytes: `45903672`
+- canonical hashes compared: `10`
+
+### range_two_chunks
+
+- legacy files: `50`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `150`, bytes: `118865385`
+- consolidated files: `50`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `12`, bytes: `72384303`
+- canonical hashes compared: `50`
+
+### range_full_object
+
+- legacy files: `1000`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `1000`, bytes: `907486583`
+- consolidated files: `1000`, formats: `BLOCK_OBJECT_FORMAT_CSCB_BATCH`, HTTP requests: `43`, bytes: `567346484`
+- canonical hashes compared: `1000`
+
+### single_consolidated_miss_fallback
+
+- legacy files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `3351267`
+- consolidated files: `1`, formats: `BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK`, HTTP requests: `3`, bytes: `3351267`
+- canonical hashes compared: `1`
diff --git a/gen/src/python/coinbase/c3/common/common_pb2.py b/gen/src/python/coinbase/c3/common/common_pb2.py
new file mode 100644
index 00000000..26dd40c5
--- /dev/null
+++ b/gen/src/python/coinbase/c3/common/common_pb2.py
@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/c3/common/common.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/c3/common/common.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x63oinbase/c3/common/common.proto\x12\x12\x63oinbase.c3.common*\x9f\x03\n\nBlockchain\x12\x16\n\x12\x42LOCKCHAIN_UNKNOWN\x10\x00\x12\x15\n\x11\x42LOCKCHAIN_SOLANA\x10\x0b\x12\x16\n\x12\x42LOCKCHAIN_BITCOIN\x10\x10\x12\x17\n\x13\x42LOCKCHAIN_ETHEREUM\x10\x11\x12\x1a\n\x16\x42LOCKCHAIN_BITCOINCASH\x10\x12\x12\x17\n\x13\x42LOCKCHAIN_LITECOIN\x10\x13\x12\x17\n\x13\x42LOCKCHAIN_DOGECOIN\x10\x1a\x12\x13\n\x0f\x42LOCKCHAIN_TRON\x10\x1e\x12\x12\n\x0e\x42LOCKCHAIN_BSC\x10\x1f\x12\x18\n\x14\x42LOCKCHAIN_AVACCHAIN\x10 \x12\x16\n\x12\x42LOCKCHAIN_POLYGON\x10#\x12\x17\n\x13\x42LOCKCHAIN_OPTIMISM\x10\'\x12\x17\n\x13\x42LOCKCHAIN_ARBITRUM\x10)\x12\x14\n\x10\x42LOCKCHAIN_APTOS\x10/\x12\x15\n\x11\x42LOCKCHAIN_FANTOM\x10\x33\x12\x13\n\x0f\x42LOCKCHAIN_BASE\x10\x38\x12\x14\n\x10\x42LOCKCHAIN_STORY\x10<*\xd5\x07\n\x07Network\x12\x13\n\x0fNETWORK_UNKNOWN\x10\x00\x12\x1a\n\x16NETWORK_SOLANA_MAINNET\x10\x16\x12\x1a\n\x16NETWORK_SOLANA_TESTNET\x10\x17\x12\x1b\n\x17NETWORK_BITCOIN_MAINNET\x10!\x12\x1b\n\x17NETWORK_BITCOIN_TESTNET\x10\"\x12\x1c\n\x18NETWORK_ETHEREUM_MAINNET\x10#\x12\x1c\n\x18NETWORK_ETHEREUM_TESTNET\x10$\x12\x1f\n\x1bNETWORK_BITCOINCASH_MAINNET\x10%\x12\x1f\n\x1bNETWORK_BITCOINCASH_TESTNET\x10&\x12\x1c\n\x18NETWORK_LITECOIN_MAINNET\x10\'\x12\x1c\n\x18NETWORK_LITECOIN_TESTNET\x10(\x12\x18\n\x14NETWORK_TRON_MAINNET\x10@\x12\x18\n\x14NETWORK_TRON_TESTNET\x10\x41\x12\x1b\n\x17NETWORK_ETHEREUM_GOERLI\x10\x42\x12\x1c\n\x18NETWORK_DOGECOIN_MAINNET\x10\x38\x12\x1c\n\x18NETWORK_DOGECOIN_TESTNET\x10\x39\x12\x17\n\x13NETWORK_BSC_MAINNET\x10\x46\x12\x17\n\x13NETWORK_BSC_TESTNET\x10G\x12\x1d\n\x19NETWORK_AVACCHAIN_MAINNET\x10H\x12\x1d\n\x19NETWORK_AVACCHAIN_TESTNET\x10I\x12\x1b\n\x17NETWORK_POLYGON_MAINNET\x10N\x12\x1b\n\x17NETWORK_POLYGON_TESTNET\x10O\x12\x1c\n\x18NETWORK_OPTIMISM_MAINNET\x10V\x12\x1c\n\x18NETWORK_OPTIMISM_TESTNET\x10W\x12\x1c\n\x18NETWORK_ARBITRUM_MAINNET\x10[\x12\x1c\n\x18NETWORK_ARBITRUM_TESTNET\x10\\\x12\x19\n\x15NETWORK_APTOS_MAINNET\x10g\x12\x19\n\x15NETWORK_APTOS_TESTNET\x10h\x12\x1a\n\x16NETWORK_FANTOM_MAINNET\x10o\x12\x1a\n\x16NETWORK_FANTOM_TESTNET\x10p\x12\x18\n\x14NETWORK_BASE_MAINNET\x10{\x12\x17\n\x13NETWORK_BASE_GOERLI\x10}\x12\x1d\n\x18NETWORK_ETHEREUM_HOLESKY\x10\x88\x01\x12\x1a\n\x15NETWORK_STORY_MAINNET\x10\x8c\x01\x42={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/chainstorage/api_pb2.py b/gen/src/python/coinbase/chainstorage/api_pb2.py
new file mode 100644
index 00000000..98e32a36
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/api_pb2.py
@@ -0,0 +1,129 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/chainstorage/api.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/chainstorage/api.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+from coinbase.chainstorage import blockchain_pb2 as coinbase_dot_chainstorage_dot_blockchain__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x1f\x63oinbase/chainstorage/api.proto\x12\x15\x63oinbase.chainstorage\x1a\x1fgoogle/protobuf/timestamp.proto\x1a&coinbase/chainstorage/blockchain.proto\"\xfb\x02\n\tBlockFile\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x0c\n\x04hash\x18\x02 \x01(\t\x12\x13\n\x0bparent_hash\x18\x03 \x01(\t\x12\x0e\n\x06height\x18\x04 \x01(\x04\x12\x10\n\x08\x66ile_url\x18\x05 \x01(\t\x12\x15\n\rparent_height\x18\x06 \x01(\x04\x12\x0f\n\x07skipped\x18\x07 \x01(\x08\x12\x37\n\x0b\x63ompression\x18\x08 \x01(\x0e\x32\".coinbase.chainstorage.Compression\x12\x33\n\x0f\x62lock_timestamp\x18\t \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x13\n\x0b\x62yte_offset\x18\n \x01(\x04\x12\x13\n\x0b\x62yte_length\x18\x0b \x01(\x04\x12\x1b\n\x13uncompressed_length\x18\x0c \x01(\x04\x12?\n\robject_format\x18\r \x01(\x0e\x32(.coinbase.chainstorage.BlockObjectFormat\"\xfb\x01\n\x0f\x42lockchainEvent\x12\x14\n\x08sequence\x18\x01 \x01(\tB\x02\x18\x01\x12\x39\n\x04type\x18\x02 \x01(\x0e\x32+.coinbase.chainstorage.BlockchainEvent.Type\x12\x35\n\x05\x62lock\x18\x03 \x01(\x0b\x32&.coinbase.chainstorage.BlockIdentifier\x12\x11\n\tevent_tag\x18\x04 \x01(\r\x12\x14\n\x0csequence_num\x18\x05 \x01(\x03\"7\n\x04Type\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0f\n\x0b\x42LOCK_ADDED\x10\x01\x12\x11\n\rBLOCK_REMOVED\x10\x02\"$\n\x15GetLatestBlockRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\"\x87\x01\n\x16GetLatestBlockResponse\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x0c\n\x04hash\x18\x02 \x01(\t\x12\x13\n\x0bparent_hash\x18\x03 \x01(\t\x12\x0e\n\x06height\x18\x04 \x01(\x04\x12-\n\ttimestamp\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"}\n\x13GetBlockFileRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x0e\n\x06height\x18\x02 \x01(\x04\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12;\n\x0bread_source\x18\x04 \x01(\x0e\x32&.coinbase.chainstorage.BlockReadSource\"F\n\x14GetBlockFileResponse\x12.\n\x04\x66ile\x18\x01 \x01(\x0b\x32 .coinbase.chainstorage.BlockFile\"\x91\x01\n\x1bGetBlockFilesByRangeRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x14\n\x0cstart_height\x18\x02 \x01(\x04\x12\x12\n\nend_height\x18\x03 \x01(\x04\x12;\n\x0bread_source\x18\x04 \x01(\x0e\x32&.coinbase.chainstorage.BlockReadSource\"O\n\x1cGetBlockFilesByRangeResponse\x12/\n\x05\x66iles\x18\x01 \x03(\x0b\x32 .coinbase.chainstorage.BlockFile\"|\n\x12GetRawBlockRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x0e\n\x06height\x18\x02 \x01(\x04\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12;\n\x0bread_source\x18\x04 \x01(\x0e\x32&.coinbase.chainstorage.BlockReadSource\"B\n\x13GetRawBlockResponse\x12+\n\x05\x62lock\x18\x01 \x01(\x0b\x32\x1c.coinbase.chainstorage.Block\"\x90\x01\n\x1aGetRawBlocksByRangeRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x14\n\x0cstart_height\x18\x02 \x01(\x04\x12\x12\n\nend_height\x18\x03 \x01(\x04\x12;\n\x0bread_source\x18\x04 \x01(\x0e\x32&.coinbase.chainstorage.BlockReadSource\"K\n\x1bGetRawBlocksByRangeResponse\x12,\n\x06\x62locks\x18\x01 \x03(\x0b\x32\x1c.coinbase.chainstorage.Block\"\x7f\n\x15GetNativeBlockRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x0e\n\x06height\x18\x02 \x01(\x04\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12;\n\x0bread_source\x18\x04 \x01(\x0e\x32&.coinbase.chainstorage.BlockReadSource\"K\n\x16GetNativeBlockResponse\x12\x31\n\x05\x62lock\x18\x01 \x01(\x0b\x32\".coinbase.chainstorage.NativeBlock\"\x93\x01\n\x1dGetNativeBlocksByRangeRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x14\n\x0cstart_height\x18\x02 \x01(\x04\x12\x12\n\nend_height\x18\x03 \x01(\x04\x12;\n\x0bread_source\x18\x04 \x01(\x0e\x32&.coinbase.chainstorage.BlockReadSource\"T\n\x1eGetNativeBlocksByRangeResponse\x12\x32\n\x06\x62locks\x18\x01 \x03(\x0b\x32\".coinbase.chainstorage.NativeBlock\"\x80\x01\n\x16GetRosettaBlockRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x0e\n\x06height\x18\x02 \x01(\x04\x12\x0c\n\x04hash\x18\x03 \x01(\t\x12;\n\x0bread_source\x18\x04 \x01(\x0e\x32&.coinbase.chainstorage.BlockReadSource\"M\n\x17GetRosettaBlockResponse\x12\x32\n\x05\x62lock\x18\x01 \x01(\x0b\x32#.coinbase.chainstorage.RosettaBlock\"\x94\x01\n\x1eGetRosettaBlocksByRangeRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x14\n\x0cstart_height\x18\x02 \x01(\x04\x12\x12\n\nend_height\x18\x03 \x01(\x04\x12;\n\x0bread_source\x18\x04 \x01(\x0e\x32&.coinbase.chainstorage.BlockReadSource\"V\n\x1fGetRosettaBlocksByRangeResponse\x12\x33\n\x06\x62locks\x18\x01 \x03(\x0b\x32#.coinbase.chainstorage.RosettaBlock\"w\n\x12\x43hainEventsRequest\x12\"\n\x1ainitial_position_in_stream\x18\x01 \x01(\t\x12\x14\n\x08sequence\x18\x02 \x01(\tB\x02\x18\x01\x12\x11\n\tevent_tag\x18\x03 \x01(\r\x12\x14\n\x0csequence_num\x18\x04 \x01(\x03\"L\n\x13\x43hainEventsResponse\x12\x35\n\x05\x65vent\x18\x01 \x01(\x0b\x32&.coinbase.chainstorage.BlockchainEvent\"\x92\x01\n\x15GetChainEventsRequest\x12\x14\n\x08sequence\x18\x01 \x01(\tB\x02\x18\x01\x12\"\n\x1ainitial_position_in_stream\x18\x02 \x01(\t\x12\x16\n\x0emax_num_events\x18\x03 \x01(\x04\x12\x11\n\tevent_tag\x18\x04 \x01(\r\x12\x14\n\x0csequence_num\x18\x05 \x01(\x03\"P\n\x16GetChainEventsResponse\x12\x36\n\x06\x65vents\x18\x01 \x03(\x0b\x32&.coinbase.chainstorage.BlockchainEvent\"\x19\n\x17GetChainMetadataRequest\"\xd1\x01\n\x18GetChainMetadataResponse\x12\x18\n\x10latest_block_tag\x18\x08 \x01(\r\x12\x18\n\x10stable_block_tag\x18\t \x01(\r\x12\x18\n\x10latest_event_tag\x18\n \x01(\r\x12\x18\n\x10stable_event_tag\x18\x0b \x01(\r\x12\x1a\n\x12\x62lock_start_height\x18\x0c \x01(\x04\x12\x1d\n\x15irreversible_distance\x18\r \x01(\x04\x12\x12\n\nblock_time\x18\x0e \x01(\t\"\x83\x01\n\x1dGetVersionedChainEventRequest\x12\x16\n\x0e\x66rom_event_tag\x18\x01 \x01(\r\x12\x19\n\rfrom_sequence\x18\x02 \x01(\tB\x02\x18\x01\x12\x14\n\x0cto_event_tag\x18\x03 \x01(\r\x12\x19\n\x11\x66rom_sequence_num\x18\x04 \x01(\x03\"W\n\x1eGetVersionedChainEventResponse\x12\x35\n\x05\x65vent\x18\x01 \x01(\x0b\x32&.coinbase.chainstorage.BlockchainEvent\"E\n\x1cGetBlockByTransactionRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x18\n\x10transaction_hash\x18\x02 \x01(\t\"W\n\x1dGetBlockByTransactionResponse\x12\x36\n\x06\x62locks\x18\x01 \x03(\x0b\x32&.coinbase.chainstorage.BlockIdentifier\"D\n\x1bGetNativeTransactionRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x18\n\x10transaction_hash\x18\x02 \x01(\t\"^\n\x1cGetNativeTransactionResponse\x12>\n\x0ctransactions\x18\x01 \x03(\x0b\x32(.coinbase.chainstorage.NativeTransaction\"l\n\x1eGetVerifiedAccountStateRequest\x12J\n\x03req\x18\x01 \x01(\x0b\x32=.coinbase.chainstorage.InternalGetVerifiedAccountStateRequest\"h\n\x1fGetVerifiedAccountStateResponse\x12\x45\n\x08response\x18\x01 \x01(\x0b\x32\x33.coinbase.chainstorage.ValidateAccountStateResponse\"<\n\x1aGetBlockByTimestampRequest\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x11\n\ttimestamp\x18\x02 \x01(\x04\"p\n\x1bGetBlockByTimestampResponse\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x0c\n\x04hash\x18\x02 \x01(\t\x12\x13\n\x0bparent_hash\x18\x03 \x01(\t\x12\x0e\n\x06height\x18\x04 \x01(\x04\x12\x11\n\ttimestamp\x18\x05 \x01(\x04*+\n\x0b\x43ompression\x12\x08\n\x04NONE\x10\x00\x12\x08\n\x04GZIP\x10\x01\x12\x08\n\x04ZSTD\x10\x02*+\n\x0fInitialPosition\x12\x0c\n\x08\x45\x41RLIEST\x10\x00\x12\n\n\x06LATEST\x10\x01*\x9a\x01\n\x0f\x42lockReadSource\x12\x1d\n\x19\x42LOCK_READ_SOURCE_DEFAULT\x10\x00\x12\x1c\n\x18\x42LOCK_READ_SOURCE_LEGACY\x10\x01\x12\"\n\x1e\x42LOCK_READ_SOURCE_SINGLE_BLOCK\x10\x01\x12\"\n\x1e\x42LOCK_READ_SOURCE_CONSOLIDATED\x10\x02\x1a\x02\x10\x01\x32\xa8\x10\n\x0c\x43hainStorage\x12m\n\x0eGetLatestBlock\x12,.coinbase.chainstorage.GetLatestBlockRequest\x1a-.coinbase.chainstorage.GetLatestBlockResponse\x12g\n\x0cGetBlockFile\x12*.coinbase.chainstorage.GetBlockFileRequest\x1a+.coinbase.chainstorage.GetBlockFileResponse\x12\x7f\n\x14GetBlockFilesByRange\x12\x32.coinbase.chainstorage.GetBlockFilesByRangeRequest\x1a\x33.coinbase.chainstorage.GetBlockFilesByRangeResponse\x12\x64\n\x0bGetRawBlock\x12).coinbase.chainstorage.GetRawBlockRequest\x1a*.coinbase.chainstorage.GetRawBlockResponse\x12|\n\x13GetRawBlocksByRange\x12\x31.coinbase.chainstorage.GetRawBlocksByRangeRequest\x1a\x32.coinbase.chainstorage.GetRawBlocksByRangeResponse\x12m\n\x0eGetNativeBlock\x12,.coinbase.chainstorage.GetNativeBlockRequest\x1a-.coinbase.chainstorage.GetNativeBlockResponse\x12\x85\x01\n\x16GetNativeBlocksByRange\x12\x34.coinbase.chainstorage.GetNativeBlocksByRangeRequest\x1a\x35.coinbase.chainstorage.GetNativeBlocksByRangeResponse\x12p\n\x0fGetRosettaBlock\x12-.coinbase.chainstorage.GetRosettaBlockRequest\x1a..coinbase.chainstorage.GetRosettaBlockResponse\x12\x88\x01\n\x17GetRosettaBlocksByRange\x12\x35.coinbase.chainstorage.GetRosettaBlocksByRangeRequest\x1a\x36.coinbase.chainstorage.GetRosettaBlocksByRangeResponse\x12l\n\x11StreamChainEvents\x12).coinbase.chainstorage.ChainEventsRequest\x1a*.coinbase.chainstorage.ChainEventsResponse0\x01\x12m\n\x0eGetChainEvents\x12,.coinbase.chainstorage.GetChainEventsRequest\x1a-.coinbase.chainstorage.GetChainEventsResponse\x12s\n\x10GetChainMetadata\x12..coinbase.chainstorage.GetChainMetadataRequest\x1a/.coinbase.chainstorage.GetChainMetadataResponse\x12\x85\x01\n\x16GetVersionedChainEvent\x12\x34.coinbase.chainstorage.GetVersionedChainEventRequest\x1a\x35.coinbase.chainstorage.GetVersionedChainEventResponse\x12\x82\x01\n\x15GetBlockByTransaction\x12\x33.coinbase.chainstorage.GetBlockByTransactionRequest\x1a\x34.coinbase.chainstorage.GetBlockByTransactionResponse\x12\x7f\n\x14GetNativeTransaction\x12\x32.coinbase.chainstorage.GetNativeTransactionRequest\x1a\x33.coinbase.chainstorage.GetNativeTransactionResponse\x12\x88\x01\n\x17GetVerifiedAccountState\x12\x35.coinbase.chainstorage.GetVerifiedAccountStateRequest\x1a\x36.coinbase.chainstorage.GetVerifiedAccountStateResponse\x12|\n\x13GetBlockByTimestamp\x12\x31.coinbase.chainstorage.GetBlockByTimestampRequest\x1a\x32.coinbase.chainstorage.GetBlockByTimestampResponseB?Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorageb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.chainstorage.api_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorage'
+ _globals['_BLOCKREADSOURCE']._loaded_options = None
+ _globals['_BLOCKREADSOURCE']._serialized_options = b'\020\001'
+ _globals['_BLOCKCHAINEVENT'].fields_by_name['sequence']._loaded_options = None
+ _globals['_BLOCKCHAINEVENT'].fields_by_name['sequence']._serialized_options = b'\030\001'
+ _globals['_CHAINEVENTSREQUEST'].fields_by_name['sequence']._loaded_options = None
+ _globals['_CHAINEVENTSREQUEST'].fields_by_name['sequence']._serialized_options = b'\030\001'
+ _globals['_GETCHAINEVENTSREQUEST'].fields_by_name['sequence']._loaded_options = None
+ _globals['_GETCHAINEVENTSREQUEST'].fields_by_name['sequence']._serialized_options = b'\030\001'
+ _globals['_GETVERSIONEDCHAINEVENTREQUEST'].fields_by_name['from_sequence']._loaded_options = None
+ _globals['_GETVERSIONEDCHAINEVENTREQUEST'].fields_by_name['from_sequence']._serialized_options = b'\030\001'
+ _globals['_COMPRESSION']._serialized_start=4290
+ _globals['_COMPRESSION']._serialized_end=4333
+ _globals['_INITIALPOSITION']._serialized_start=4335
+ _globals['_INITIALPOSITION']._serialized_end=4378
+ _globals['_BLOCKREADSOURCE']._serialized_start=4381
+ _globals['_BLOCKREADSOURCE']._serialized_end=4535
+ _globals['_BLOCKFILE']._serialized_start=132
+ _globals['_BLOCKFILE']._serialized_end=511
+ _globals['_BLOCKCHAINEVENT']._serialized_start=514
+ _globals['_BLOCKCHAINEVENT']._serialized_end=765
+ _globals['_BLOCKCHAINEVENT_TYPE']._serialized_start=710
+ _globals['_BLOCKCHAINEVENT_TYPE']._serialized_end=765
+ _globals['_GETLATESTBLOCKREQUEST']._serialized_start=767
+ _globals['_GETLATESTBLOCKREQUEST']._serialized_end=803
+ _globals['_GETLATESTBLOCKRESPONSE']._serialized_start=806
+ _globals['_GETLATESTBLOCKRESPONSE']._serialized_end=941
+ _globals['_GETBLOCKFILEREQUEST']._serialized_start=943
+ _globals['_GETBLOCKFILEREQUEST']._serialized_end=1068
+ _globals['_GETBLOCKFILERESPONSE']._serialized_start=1070
+ _globals['_GETBLOCKFILERESPONSE']._serialized_end=1140
+ _globals['_GETBLOCKFILESBYRANGEREQUEST']._serialized_start=1143
+ _globals['_GETBLOCKFILESBYRANGEREQUEST']._serialized_end=1288
+ _globals['_GETBLOCKFILESBYRANGERESPONSE']._serialized_start=1290
+ _globals['_GETBLOCKFILESBYRANGERESPONSE']._serialized_end=1369
+ _globals['_GETRAWBLOCKREQUEST']._serialized_start=1371
+ _globals['_GETRAWBLOCKREQUEST']._serialized_end=1495
+ _globals['_GETRAWBLOCKRESPONSE']._serialized_start=1497
+ _globals['_GETRAWBLOCKRESPONSE']._serialized_end=1563
+ _globals['_GETRAWBLOCKSBYRANGEREQUEST']._serialized_start=1566
+ _globals['_GETRAWBLOCKSBYRANGEREQUEST']._serialized_end=1710
+ _globals['_GETRAWBLOCKSBYRANGERESPONSE']._serialized_start=1712
+ _globals['_GETRAWBLOCKSBYRANGERESPONSE']._serialized_end=1787
+ _globals['_GETNATIVEBLOCKREQUEST']._serialized_start=1789
+ _globals['_GETNATIVEBLOCKREQUEST']._serialized_end=1916
+ _globals['_GETNATIVEBLOCKRESPONSE']._serialized_start=1918
+ _globals['_GETNATIVEBLOCKRESPONSE']._serialized_end=1993
+ _globals['_GETNATIVEBLOCKSBYRANGEREQUEST']._serialized_start=1996
+ _globals['_GETNATIVEBLOCKSBYRANGEREQUEST']._serialized_end=2143
+ _globals['_GETNATIVEBLOCKSBYRANGERESPONSE']._serialized_start=2145
+ _globals['_GETNATIVEBLOCKSBYRANGERESPONSE']._serialized_end=2229
+ _globals['_GETROSETTABLOCKREQUEST']._serialized_start=2232
+ _globals['_GETROSETTABLOCKREQUEST']._serialized_end=2360
+ _globals['_GETROSETTABLOCKRESPONSE']._serialized_start=2362
+ _globals['_GETROSETTABLOCKRESPONSE']._serialized_end=2439
+ _globals['_GETROSETTABLOCKSBYRANGEREQUEST']._serialized_start=2442
+ _globals['_GETROSETTABLOCKSBYRANGEREQUEST']._serialized_end=2590
+ _globals['_GETROSETTABLOCKSBYRANGERESPONSE']._serialized_start=2592
+ _globals['_GETROSETTABLOCKSBYRANGERESPONSE']._serialized_end=2678
+ _globals['_CHAINEVENTSREQUEST']._serialized_start=2680
+ _globals['_CHAINEVENTSREQUEST']._serialized_end=2799
+ _globals['_CHAINEVENTSRESPONSE']._serialized_start=2801
+ _globals['_CHAINEVENTSRESPONSE']._serialized_end=2877
+ _globals['_GETCHAINEVENTSREQUEST']._serialized_start=2880
+ _globals['_GETCHAINEVENTSREQUEST']._serialized_end=3026
+ _globals['_GETCHAINEVENTSRESPONSE']._serialized_start=3028
+ _globals['_GETCHAINEVENTSRESPONSE']._serialized_end=3108
+ _globals['_GETCHAINMETADATAREQUEST']._serialized_start=3110
+ _globals['_GETCHAINMETADATAREQUEST']._serialized_end=3135
+ _globals['_GETCHAINMETADATARESPONSE']._serialized_start=3138
+ _globals['_GETCHAINMETADATARESPONSE']._serialized_end=3347
+ _globals['_GETVERSIONEDCHAINEVENTREQUEST']._serialized_start=3350
+ _globals['_GETVERSIONEDCHAINEVENTREQUEST']._serialized_end=3481
+ _globals['_GETVERSIONEDCHAINEVENTRESPONSE']._serialized_start=3483
+ _globals['_GETVERSIONEDCHAINEVENTRESPONSE']._serialized_end=3570
+ _globals['_GETBLOCKBYTRANSACTIONREQUEST']._serialized_start=3572
+ _globals['_GETBLOCKBYTRANSACTIONREQUEST']._serialized_end=3641
+ _globals['_GETBLOCKBYTRANSACTIONRESPONSE']._serialized_start=3643
+ _globals['_GETBLOCKBYTRANSACTIONRESPONSE']._serialized_end=3730
+ _globals['_GETNATIVETRANSACTIONREQUEST']._serialized_start=3732
+ _globals['_GETNATIVETRANSACTIONREQUEST']._serialized_end=3800
+ _globals['_GETNATIVETRANSACTIONRESPONSE']._serialized_start=3802
+ _globals['_GETNATIVETRANSACTIONRESPONSE']._serialized_end=3896
+ _globals['_GETVERIFIEDACCOUNTSTATEREQUEST']._serialized_start=3898
+ _globals['_GETVERIFIEDACCOUNTSTATEREQUEST']._serialized_end=4006
+ _globals['_GETVERIFIEDACCOUNTSTATERESPONSE']._serialized_start=4008
+ _globals['_GETVERIFIEDACCOUNTSTATERESPONSE']._serialized_end=4112
+ _globals['_GETBLOCKBYTIMESTAMPREQUEST']._serialized_start=4114
+ _globals['_GETBLOCKBYTIMESTAMPREQUEST']._serialized_end=4174
+ _globals['_GETBLOCKBYTIMESTAMPRESPONSE']._serialized_start=4176
+ _globals['_GETBLOCKBYTIMESTAMPRESPONSE']._serialized_end=4288
+ _globals['_CHAINSTORAGE']._serialized_start=4538
+ _globals['_CHAINSTORAGE']._serialized_end=6626
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/chainstorage/api_pb2_grpc.py b/gen/src/python/coinbase/chainstorage/api_pb2_grpc.py
new file mode 100644
index 00000000..080639fc
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/api_pb2_grpc.py
@@ -0,0 +1,742 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+from coinbase.chainstorage import api_pb2 as coinbase_dot_chainstorage_dot_api__pb2
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/chainstorage/api_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
+
+
+class ChainStorageStub(object):
+ """Missing associated documentation comment in .proto file."""
+
+ def __init__(self, channel):
+ """Constructor.
+
+ Args:
+ channel: A grpc.Channel.
+ """
+ self.GetLatestBlock = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetLatestBlock',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetLatestBlockRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetLatestBlockResponse.FromString,
+ _registered_method=True)
+ self.GetBlockFile = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetBlockFile',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockFileRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockFileResponse.FromString,
+ _registered_method=True)
+ self.GetBlockFilesByRange = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetBlockFilesByRange',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockFilesByRangeRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockFilesByRangeResponse.FromString,
+ _registered_method=True)
+ self.GetRawBlock = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetRawBlock',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetRawBlockRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetRawBlockResponse.FromString,
+ _registered_method=True)
+ self.GetRawBlocksByRange = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetRawBlocksByRange',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetRawBlocksByRangeRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetRawBlocksByRangeResponse.FromString,
+ _registered_method=True)
+ self.GetNativeBlock = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetNativeBlock',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlockRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlockResponse.FromString,
+ _registered_method=True)
+ self.GetNativeBlocksByRange = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetNativeBlocksByRange',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlocksByRangeRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlocksByRangeResponse.FromString,
+ _registered_method=True)
+ self.GetRosettaBlock = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetRosettaBlock',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlockRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlockResponse.FromString,
+ _registered_method=True)
+ self.GetRosettaBlocksByRange = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetRosettaBlocksByRange',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlocksByRangeRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlocksByRangeResponse.FromString,
+ _registered_method=True)
+ self.StreamChainEvents = channel.unary_stream(
+ '/coinbase.chainstorage.ChainStorage/StreamChainEvents',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.ChainEventsRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.ChainEventsResponse.FromString,
+ _registered_method=True)
+ self.GetChainEvents = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetChainEvents',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetChainEventsRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetChainEventsResponse.FromString,
+ _registered_method=True)
+ self.GetChainMetadata = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetChainMetadata',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetChainMetadataRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetChainMetadataResponse.FromString,
+ _registered_method=True)
+ self.GetVersionedChainEvent = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetVersionedChainEvent',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetVersionedChainEventRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetVersionedChainEventResponse.FromString,
+ _registered_method=True)
+ self.GetBlockByTransaction = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetBlockByTransaction',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockByTransactionRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockByTransactionResponse.FromString,
+ _registered_method=True)
+ self.GetNativeTransaction = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetNativeTransaction',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeTransactionRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeTransactionResponse.FromString,
+ _registered_method=True)
+ self.GetVerifiedAccountState = channel.unary_unary(
+ '/coinbase.chainstorage.ChainStorage/GetVerifiedAccountState',
+ request_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetVerifiedAccountStateRequest.SerializeToString,
+ response_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetVerifiedAccountStateResponse.FromString,
+ _registered_method=True)
+
+
+class ChainStorageServicer(object):
+ """Missing associated documentation comment in .proto file."""
+
+ def GetLatestBlock(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetBlockFile(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetBlockFilesByRange(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetRawBlock(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetRawBlocksByRange(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetNativeBlock(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetNativeBlocksByRange(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetRosettaBlock(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetRosettaBlocksByRange(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def StreamChainEvents(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetChainEvents(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetChainMetadata(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetVersionedChainEvent(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetBlockByTransaction(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetNativeTransaction(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+ def GetVerifiedAccountState(self, request, context):
+ """Missing associated documentation comment in .proto file."""
+ context.set_code(grpc.StatusCode.UNIMPLEMENTED)
+ context.set_details('Method not implemented!')
+ raise NotImplementedError('Method not implemented!')
+
+
+def add_ChainStorageServicer_to_server(servicer, server):
+ rpc_method_handlers = {
+ 'GetLatestBlock': grpc.unary_unary_rpc_method_handler(
+ servicer.GetLatestBlock,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetLatestBlockRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetLatestBlockResponse.SerializeToString,
+ ),
+ 'GetBlockFile': grpc.unary_unary_rpc_method_handler(
+ servicer.GetBlockFile,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockFileRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockFileResponse.SerializeToString,
+ ),
+ 'GetBlockFilesByRange': grpc.unary_unary_rpc_method_handler(
+ servicer.GetBlockFilesByRange,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockFilesByRangeRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockFilesByRangeResponse.SerializeToString,
+ ),
+ 'GetRawBlock': grpc.unary_unary_rpc_method_handler(
+ servicer.GetRawBlock,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetRawBlockRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetRawBlockResponse.SerializeToString,
+ ),
+ 'GetRawBlocksByRange': grpc.unary_unary_rpc_method_handler(
+ servicer.GetRawBlocksByRange,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetRawBlocksByRangeRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetRawBlocksByRangeResponse.SerializeToString,
+ ),
+ 'GetNativeBlock': grpc.unary_unary_rpc_method_handler(
+ servicer.GetNativeBlock,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlockRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlockResponse.SerializeToString,
+ ),
+ 'GetNativeBlocksByRange': grpc.unary_unary_rpc_method_handler(
+ servicer.GetNativeBlocksByRange,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlocksByRangeRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlocksByRangeResponse.SerializeToString,
+ ),
+ 'GetRosettaBlock': grpc.unary_unary_rpc_method_handler(
+ servicer.GetRosettaBlock,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlockRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlockResponse.SerializeToString,
+ ),
+ 'GetRosettaBlocksByRange': grpc.unary_unary_rpc_method_handler(
+ servicer.GetRosettaBlocksByRange,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlocksByRangeRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlocksByRangeResponse.SerializeToString,
+ ),
+ 'StreamChainEvents': grpc.unary_stream_rpc_method_handler(
+ servicer.StreamChainEvents,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.ChainEventsRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.ChainEventsResponse.SerializeToString,
+ ),
+ 'GetChainEvents': grpc.unary_unary_rpc_method_handler(
+ servicer.GetChainEvents,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetChainEventsRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetChainEventsResponse.SerializeToString,
+ ),
+ 'GetChainMetadata': grpc.unary_unary_rpc_method_handler(
+ servicer.GetChainMetadata,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetChainMetadataRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetChainMetadataResponse.SerializeToString,
+ ),
+ 'GetVersionedChainEvent': grpc.unary_unary_rpc_method_handler(
+ servicer.GetVersionedChainEvent,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetVersionedChainEventRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetVersionedChainEventResponse.SerializeToString,
+ ),
+ 'GetBlockByTransaction': grpc.unary_unary_rpc_method_handler(
+ servicer.GetBlockByTransaction,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockByTransactionRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetBlockByTransactionResponse.SerializeToString,
+ ),
+ 'GetNativeTransaction': grpc.unary_unary_rpc_method_handler(
+ servicer.GetNativeTransaction,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeTransactionRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetNativeTransactionResponse.SerializeToString,
+ ),
+ 'GetVerifiedAccountState': grpc.unary_unary_rpc_method_handler(
+ servicer.GetVerifiedAccountState,
+ request_deserializer=coinbase_dot_chainstorage_dot_api__pb2.GetVerifiedAccountStateRequest.FromString,
+ response_serializer=coinbase_dot_chainstorage_dot_api__pb2.GetVerifiedAccountStateResponse.SerializeToString,
+ ),
+ }
+ generic_handler = grpc.method_handlers_generic_handler(
+ 'coinbase.chainstorage.ChainStorage', rpc_method_handlers)
+ server.add_generic_rpc_handlers((generic_handler,))
+ server.add_registered_method_handlers('coinbase.chainstorage.ChainStorage', rpc_method_handlers)
+
+
+ # This class is part of an EXPERIMENTAL API.
+class ChainStorage(object):
+ """Missing associated documentation comment in .proto file."""
+
+ @staticmethod
+ def GetLatestBlock(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetLatestBlock',
+ coinbase_dot_chainstorage_dot_api__pb2.GetLatestBlockRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetLatestBlockResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetBlockFile(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetBlockFile',
+ coinbase_dot_chainstorage_dot_api__pb2.GetBlockFileRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetBlockFileResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetBlockFilesByRange(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetBlockFilesByRange',
+ coinbase_dot_chainstorage_dot_api__pb2.GetBlockFilesByRangeRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetBlockFilesByRangeResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetRawBlock(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetRawBlock',
+ coinbase_dot_chainstorage_dot_api__pb2.GetRawBlockRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetRawBlockResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetRawBlocksByRange(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetRawBlocksByRange',
+ coinbase_dot_chainstorage_dot_api__pb2.GetRawBlocksByRangeRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetRawBlocksByRangeResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetNativeBlock(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetNativeBlock',
+ coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlockRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlockResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetNativeBlocksByRange(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetNativeBlocksByRange',
+ coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlocksByRangeRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetNativeBlocksByRangeResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetRosettaBlock(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetRosettaBlock',
+ coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlockRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlockResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetRosettaBlocksByRange(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetRosettaBlocksByRange',
+ coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlocksByRangeRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetRosettaBlocksByRangeResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def StreamChainEvents(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_stream(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/StreamChainEvents',
+ coinbase_dot_chainstorage_dot_api__pb2.ChainEventsRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.ChainEventsResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetChainEvents(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetChainEvents',
+ coinbase_dot_chainstorage_dot_api__pb2.GetChainEventsRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetChainEventsResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetChainMetadata(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetChainMetadata',
+ coinbase_dot_chainstorage_dot_api__pb2.GetChainMetadataRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetChainMetadataResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetVersionedChainEvent(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetVersionedChainEvent',
+ coinbase_dot_chainstorage_dot_api__pb2.GetVersionedChainEventRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetVersionedChainEventResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetBlockByTransaction(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetBlockByTransaction',
+ coinbase_dot_chainstorage_dot_api__pb2.GetBlockByTransactionRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetBlockByTransactionResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetNativeTransaction(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetNativeTransaction',
+ coinbase_dot_chainstorage_dot_api__pb2.GetNativeTransactionRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetNativeTransactionResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
+
+ @staticmethod
+ def GetVerifiedAccountState(request,
+ target,
+ options=(),
+ channel_credentials=None,
+ call_credentials=None,
+ insecure=False,
+ compression=None,
+ wait_for_ready=None,
+ timeout=None,
+ metadata=None):
+ return grpc.experimental.unary_unary(
+ request,
+ target,
+ '/coinbase.chainstorage.ChainStorage/GetVerifiedAccountState',
+ coinbase_dot_chainstorage_dot_api__pb2.GetVerifiedAccountStateRequest.SerializeToString,
+ coinbase_dot_chainstorage_dot_api__pb2.GetVerifiedAccountStateResponse.FromString,
+ options,
+ channel_credentials,
+ insecure,
+ call_credentials,
+ compression,
+ wait_for_ready,
+ timeout,
+ metadata,
+ _registered_method=True)
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_aptos_pb2.py b/gen/src/python/coinbase/chainstorage/blockchain_aptos_pb2.py
new file mode 100644
index 00000000..a983affd
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_aptos_pb2.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/chainstorage/blockchain_aptos.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/chainstorage/blockchain_aptos.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n,coinbase/chainstorage/blockchain_aptos.proto\x12\x15\x63oinbase.chainstorage\x1a\x1fgoogle/protobuf/timestamp.proto\"\x1e\n\rAptosBlobdata\x12\r\n\x05\x62lock\x18\x01 \x01(\x0c\"\x7f\n\nAptosBlock\x12\x32\n\x06header\x18\x01 \x01(\x0b\x32\".coinbase.chainstorage.AptosHeader\x12=\n\x0ctransactions\x18\x02 \x03(\x0b\x32\'.coinbase.chainstorage.AptosTransaction\"g\n\x0b\x41ptosHeader\x12\x14\n\x0c\x62lock_height\x18\x01 \x01(\x04\x12\x12\n\nblock_hash\x18\x02 \x01(\t\x12.\n\nblock_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xd5\x05\n\x10\x41ptosTransaction\x12\x0f\n\x07version\x18\x01 \x01(\x04\x12\x14\n\x0c\x62lock_height\x18\x02 \x01(\x04\x12-\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x39\n\x04info\x18\x04 \x01(\x0b\x32+.coinbase.chainstorage.AptosTransactionInfo\x12\x45\n\x04type\x18\x05 \x01(\x0e\x32\x37.coinbase.chainstorage.AptosTransaction.TransactionType\x12N\n\x0e\x62lock_metadata\x18\x64 \x01(\x0b\x32\x34.coinbase.chainstorage.AptosBlockMetadataTransactionH\x00\x12\x41\n\x07genesis\x18\x65 \x01(\x0b\x32..coinbase.chainstorage.AptosGenesisTransactionH\x00\x12R\n\x10state_checkpoint\x18\x66 \x01(\x0b\x32\x36.coinbase.chainstorage.AptosStateCheckpointTransactionH\x00\x12;\n\x04user\x18g \x01(\x0b\x32+.coinbase.chainstorage.AptosUserTransactionH\x00\x12\x45\n\tvalidator\x18h \x01(\x0b\x32\x30.coinbase.chainstorage.AptosValidatorTransactionH\x00\"r\n\x0fTransactionType\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07GENESIS\x10\x01\x12\x12\n\x0e\x42LOCK_METADATA\x10\x02\x12\x14\n\x10STATE_CHECKPOINT\x10\x03\x12\x08\n\x04USER\x10\x04\x12\r\n\tVALIDATOR\x10\x05\x42\n\n\x08txn_data\"\xad\x02\n\x14\x41ptosTransactionInfo\x12\x0c\n\x04hash\x18\x01 \x01(\t\x12\x19\n\x11state_change_hash\x18\x02 \x01(\t\x12\x17\n\x0f\x65vent_root_hash\x18\x03 \x01(\t\x12\x1f\n\x15state_checkpoint_hash\x18\x04 \x01(\tH\x00\x12\x10\n\x08gas_used\x18\x05 \x01(\x04\x12\x0f\n\x07success\x18\x06 \x01(\x08\x12\x11\n\tvm_status\x18\x07 \x01(\t\x12\x1d\n\x15\x61\x63\x63umulator_root_hash\x18\x08 \x01(\t\x12;\n\x07\x63hanges\x18\t \x03(\x0b\x32*.coinbase.chainstorage.AptosWriteSetChangeB \n\x1eoptional_state_checkpoint_hash\"\x95\x05\n\x13\x41ptosWriteSetChange\x12=\n\x04type\x18\x01 \x01(\x0e\x32/.coinbase.chainstorage.AptosWriteSetChange.Type\x12\x41\n\rdelete_module\x18\x64 \x01(\x0b\x32(.coinbase.chainstorage.AptosDeleteModuleH\x00\x12\x45\n\x0f\x64\x65lete_resource\x18\x65 \x01(\x0b\x32*.coinbase.chainstorage.AptosDeleteResourceH\x00\x12H\n\x11\x64\x65lete_table_item\x18\x66 \x01(\x0b\x32+.coinbase.chainstorage.AptosDeleteTableItemH\x00\x12?\n\x0cwrite_module\x18g \x01(\x0b\x32\'.coinbase.chainstorage.AptosWriteModuleH\x00\x12\x43\n\x0ewrite_resource\x18h \x01(\x0b\x32).coinbase.chainstorage.AptosWriteResourceH\x00\x12\x46\n\x10write_table_item\x18i \x01(\x0b\x32*.coinbase.chainstorage.AptosWriteTableItemH\x00\"\x92\x01\n\x04Type\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x11\n\rDELETE_MODULE\x10\x01\x12\x13\n\x0f\x44\x45LETE_RESOURCE\x10\x02\x12\x15\n\x11\x44\x45LETE_TABLE_ITEM\x10\x03\x12\x10\n\x0cWRITE_MODULE\x10\x04\x12\x12\n\x0eWRITE_RESOURCE\x10\x05\x12\x14\n\x10WRITE_TABLE_ITEM\x10\x06\x42\x08\n\x06\x63hange\"v\n\x11\x41ptosDeleteModule\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x16\n\x0estate_key_hash\x18\x02 \x01(\t\x12\x38\n\x06module\x18\x03 \x01(\x0b\x32(.coinbase.chainstorage.AptosMoveModuleId\"P\n\x13\x41ptosDeleteResource\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x16\n\x0estate_key_hash\x18\x02 \x01(\t\x12\x10\n\x08resource\x18\x03 \x01(\t\"\x86\x01\n\x14\x41ptosDeleteTableItem\x12\x16\n\x0estate_key_hash\x18\x01 \x01(\t\x12\x0e\n\x06handle\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\x12\x39\n\x04\x64\x61ta\x18\x04 \x01(\x0b\x32+.coinbase.chainstorage.AptosDeleteTableData\"5\n\x14\x41ptosDeleteTableData\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x10\n\x08key_type\x18\x02 \x01(\t\"y\n\x10\x41ptosWriteModule\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x16\n\x0estate_key_hash\x18\x02 \x01(\t\x12<\n\x04\x64\x61ta\x18\x03 \x01(\x0b\x32..coinbase.chainstorage.AptosMoveModuleBytecode\"]\n\x12\x41ptosWriteResource\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x16\n\x0estate_key_hash\x18\x02 \x01(\t\x12\x10\n\x08type_str\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\"\x97\x01\n\x13\x41ptosWriteTableItem\x12\x16\n\x0estate_key_hash\x18\x01 \x01(\t\x12\x0e\n\x06handle\x18\x02 \x01(\t\x12\x0b\n\x03key\x18\x03 \x01(\t\x12\r\n\x05value\x18\x04 \x01(\t\x12<\n\x04\x64\x61ta\x18\x05 \x01(\x0b\x32..coinbase.chainstorage.AptosWriteTableItemData\"[\n\x17\x41ptosWriteTableItemData\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x10\n\x08key_type\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\x12\x12\n\nvalue_type\x18\x04 \x01(\t\"\xd4\x01\n\x1d\x41ptosBlockMetadataTransaction\x12\n\n\x02id\x18\x01 \x01(\t\x12\r\n\x05\x65poch\x18\x02 \x01(\x04\x12\r\n\x05round\x18\x03 \x01(\x04\x12\x31\n\x06\x65vents\x18\x04 \x03(\x0b\x32!.coinbase.chainstorage.AptosEvent\x12#\n\x1bprevious_block_votes_bitvec\x18\x05 \x01(\x0c\x12\x10\n\x08proposer\x18\x06 \x01(\t\x12\x1f\n\x17\x66\x61iled_proposer_indices\x18\x07 \x03(\r\"t\n\nAptosEvent\x12\x31\n\x03key\x18\x01 \x01(\x0b\x32$.coinbase.chainstorage.AptosEventKey\x12\x17\n\x0fsequence_number\x18\x02 \x01(\x04\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x04 \x01(\t\"A\n\rAptosEventKey\x12\x17\n\x0f\x63reation_number\x18\x01 \x01(\x04\x12\x17\n\x0f\x61\x63\x63ount_address\x18\x02 \x01(\t\"!\n\x1f\x41ptosStateCheckpointTransaction\"\x83\x01\n\x17\x41ptosGenesisTransaction\x12\x35\n\x07payload\x18\x01 \x01(\x0b\x32$.coinbase.chainstorage.AptosWriteSet\x12\x31\n\x06\x65vents\x18\x02 \x03(\x0b\x32!.coinbase.chainstorage.AptosEvent\"\xb4\x02\n\rAptosWriteSet\x12\x41\n\x0ewrite_set_type\x18\x01 \x01(\x0e\x32).coinbase.chainstorage.AptosWriteSet.Type\x12\x46\n\x10script_write_set\x18\x64 \x01(\x0b\x32*.coinbase.chainstorage.AptosScriptWriteSetH\x00\x12\x46\n\x10\x64irect_write_set\x18\x65 \x01(\x0b\x32*.coinbase.chainstorage.AptosDirectWriteSetH\x00\"C\n\x04Type\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x14\n\x10SCRIPT_WRITE_SET\x10\x01\x12\x14\n\x10\x44IRECT_WRITE_SET\x10\x02\x42\x0b\n\twrite_set\"d\n\x13\x41ptosScriptWriteSet\x12\x12\n\nexecute_as\x18\x01 \x01(\t\x12\x39\n\x06script\x18\x02 \x01(\x0b\x32).coinbase.chainstorage.AptosScriptPayload\"\x8e\x01\n\x13\x41ptosDirectWriteSet\x12\x44\n\x10write_set_change\x18\x01 \x03(\x0b\x32*.coinbase.chainstorage.AptosWriteSetChange\x12\x31\n\x06\x65vents\x18\x02 \x03(\x0b\x32!.coinbase.chainstorage.AptosEvent\"\x8e\x01\n\x14\x41ptosUserTransaction\x12\x43\n\x07request\x18\x01 \x01(\x0b\x32\x32.coinbase.chainstorage.AptosUserTransactionRequest\x12\x31\n\x06\x65vents\x18\x02 \x03(\x0b\x32!.coinbase.chainstorage.AptosEvent\"\xb0\x02\n\x1b\x41ptosUserTransactionRequest\x12\x0e\n\x06sender\x18\x01 \x01(\t\x12\x17\n\x0fsequence_number\x18\x02 \x01(\x04\x12\x16\n\x0emax_gas_amount\x18\x03 \x01(\x04\x12\x16\n\x0egas_unit_price\x18\x04 \x01(\x04\x12=\n\x19\x65xpiration_timestamp_secs\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12?\n\x07payload\x18\x06 \x01(\x0b\x32..coinbase.chainstorage.AptosTransactionPayload\x12\x38\n\tsignature\x18\x07 \x01(\x0b\x32%.coinbase.chainstorage.AptosSignature\"\xf7\x04\n\x17\x41ptosTransactionPayload\x12\x41\n\x04type\x18\x01 \x01(\x0e\x32\x33.coinbase.chainstorage.AptosTransactionPayload.Type\x12R\n\x16\x65ntry_function_payload\x18\x64 \x01(\x0b\x32\x30.coinbase.chainstorage.AptosEntryFunctionPayloadH\x00\x12\x43\n\x0escript_payload\x18\x65 \x01(\x0b\x32).coinbase.chainstorage.AptosScriptPayloadH\x00\x12P\n\x15module_bundle_payload\x18\x66 \x01(\x0b\x32/.coinbase.chainstorage.AptosModuleBundlePayloadH\x00\x12H\n\x11write_set_payload\x18g \x01(\x0b\x32+.coinbase.chainstorage.AptosWriteSetPayloadH\x00\x12G\n\x10multisig_payload\x18h \x01(\x0b\x32+.coinbase.chainstorage.AptosMultisigPayloadH\x00\"\x8f\x01\n\x04Type\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x1a\n\x16\x45NTRY_FUNCTION_PAYLOAD\x10\x01\x12\x12\n\x0eSCRIPT_PAYLOAD\x10\x02\x12\x19\n\x15MODULE_BUNDLE_PAYLOAD\x10\x03\x12\x15\n\x11WRITE_SET_PAYLOAD\x10\x04\x12\x14\n\x10MULTISIG_PAYLOAD\x10\x05\x42\t\n\x07payload\"\x85\x01\n\x19\x41ptosEntryFunctionPayload\x12=\n\x08\x66unction\x18\x01 \x01(\x0b\x32+.coinbase.chainstorage.AptosEntryFunctionId\x12\x16\n\x0etype_arguments\x18\x02 \x03(\t\x12\x11\n\targuments\x18\x03 \x03(\x0c\"g\n\x14\x41ptosEntryFunctionId\x12\x38\n\x06module\x18\x01 \x01(\x0b\x32(.coinbase.chainstorage.AptosMoveModuleId\x12\x15\n\rfunction_name\x18\x02 \x01(\t\"2\n\x11\x41ptosMoveModuleId\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\"}\n\x12\x41ptosScriptPayload\x12<\n\x04\x63ode\x18\x01 \x01(\x0b\x32..coinbase.chainstorage.AptosMoveScriptBytecode\x12\x16\n\x0etype_arguments\x18\x02 \x03(\t\x12\x11\n\targuments\x18\x03 \x03(\x0c\"b\n\x17\x41ptosMoveScriptBytecode\x12\x10\n\x08\x62ytecode\x18\x01 \x01(\t\x12\x35\n\x03\x61\x62i\x18\x02 \x01(\x0b\x32(.coinbase.chainstorage.AptosMoveFunction\"\xab\x02\n\x11\x41ptosMoveFunction\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x41\n\nvisibility\x18\x02 \x01(\x0e\x32-.coinbase.chainstorage.AptosMoveFunction.Type\x12\x10\n\x08is_entry\x18\x03 \x01(\x08\x12U\n\x13generic_type_params\x18\x04 \x03(\x0b\x32\x38.coinbase.chainstorage.AptosMoveFunctionGenericTypeParam\x12\x0e\n\x06params\x18\x05 \x03(\t\x12\x0e\n\x06return\x18\x06 \x03(\t\"<\n\x04Type\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07PRIVATE\x10\x01\x12\n\n\x06PUBLIC\x10\x02\x12\n\n\x06\x46RIEND\x10\x03\"8\n!AptosMoveFunctionGenericTypeParam\x12\x13\n\x0b\x63onstraints\x18\x01 \x03(\t\"[\n\x18\x41ptosModuleBundlePayload\x12?\n\x07modules\x18\x01 \x03(\x0b\x32..coinbase.chainstorage.AptosMoveModuleBytecode\"`\n\x17\x41ptosMoveModuleBytecode\x12\x10\n\x08\x62ytecode\x18\x01 \x01(\t\x12\x33\n\x03\x61\x62i\x18\x02 \x01(\x0b\x32&.coinbase.chainstorage.AptosMoveModule\"\xe9\x01\n\x0f\x41ptosMoveModule\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x0c\n\x04name\x18\x02 \x01(\t\x12\x39\n\x07\x66riends\x18\x03 \x03(\x0b\x32(.coinbase.chainstorage.AptosMoveModuleId\x12\x43\n\x11\x65xposed_functions\x18\x04 \x03(\x0b\x32(.coinbase.chainstorage.AptosMoveFunction\x12\x37\n\x07structs\x18\x05 \x03(\x0b\x32&.coinbase.chainstorage.AptosMoveStruct\"\xd7\x01\n\x0f\x41ptosMoveStruct\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x11\n\tis_native\x18\x02 \x01(\x08\x12\x11\n\tabilities\x18\x03 \x03(\t\x12S\n\x13generic_type_params\x18\x04 \x03(\x0b\x32\x36.coinbase.chainstorage.AptosMoveStructGenericTypeParam\x12;\n\x06\x66ields\x18\x05 \x03(\x0b\x32+.coinbase.chainstorage.AptosMoveStructField\"6\n\x1f\x41ptosMoveStructGenericTypeParam\x12\x13\n\x0b\x63onstraints\x18\x01 \x03(\t\"2\n\x14\x41ptosMoveStructField\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\"O\n\x14\x41ptosWriteSetPayload\x12\x37\n\twrite_set\x18\x01 \x01(\x0b\x32$.coinbase.chainstorage.AptosWriteSet\"\xa7\x01\n\x14\x41ptosMultisigPayload\x12\x18\n\x10multisig_address\x18\x01 \x01(\t\x12U\n\x13transaction_payload\x18\x02 \x01(\x0b\x32\x36.coinbase.chainstorage.AptosMultisigTransactionPayloadH\x00\x42\x1e\n\x1coptional_transaction_payload\"\x80\x02\n\x1f\x41ptosMultisigTransactionPayload\x12I\n\x04type\x18\x01 \x01(\x0e\x32;.coinbase.chainstorage.AptosMultisigTransactionPayload.Type\x12R\n\x16\x65ntry_function_payload\x18\x64 \x01(\x0b\x32\x30.coinbase.chainstorage.AptosEntryFunctionPayloadH\x00\"3\n\x04Type\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x1a\n\x16\x45NTRY_FUNCTION_PAYLOAD\x10\x01\x42\t\n\x07payload\"\xa8\x04\n\x0e\x41ptosSignature\x12\x38\n\x04type\x18\x01 \x01(\x0e\x32*.coinbase.chainstorage.AptosSignature.Type\x12?\n\x07\x65\x64\x32\x35\x35\x31\x39\x18\x02 \x01(\x0b\x32,.coinbase.chainstorage.AptosEd25519SignatureH\x00\x12J\n\rmulti_ed25519\x18\x03 \x01(\x0b\x32\x31.coinbase.chainstorage.AptosMultiEd25519SignatureH\x00\x12\x46\n\x0bmulti_agent\x18\x04 \x01(\x0b\x32/.coinbase.chainstorage.AptosMultiAgentSignatureH\x00\x12\x42\n\tfee_payer\x18\x05 \x01(\x0b\x32-.coinbase.chainstorage.AptosFeePayerSignatureH\x00\x12J\n\rsingle_sender\x18\x06 \x01(\x0b\x32\x31.coinbase.chainstorage.AptosSingleSenderSignatureH\x00\"j\n\x04Type\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07\x45\x44\x32\x35\x35\x31\x39\x10\x01\x12\x11\n\rMULTI_ED25519\x10\x02\x12\x0f\n\x0bMULTI_AGENT\x10\x03\x12\r\n\tFEE_PAYER\x10\x04\x12\x11\n\rSINGLE_SENDER\x10\x05\x42\x0b\n\tsignature\">\n\x15\x41ptosEd25519Signature\x12\x12\n\npublic_key\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\t\"t\n\x1a\x41ptosMultiEd25519Signature\x12\x13\n\x0bpublic_keys\x18\x01 \x03(\t\x12\x12\n\nsignatures\x18\x02 \x03(\t\x12\x11\n\tthreshold\x18\x03 \x01(\r\x12\x1a\n\x12public_key_indices\x18\x04 \x01(\t\"\xc5\x01\n\x18\x41ptosMultiAgentSignature\x12<\n\x06sender\x18\x01 \x01(\x0b\x32,.coinbase.chainstorage.AptosAccountSignature\x12\"\n\x1asecondary_signer_addresses\x18\x02 \x03(\t\x12G\n\x11secondary_signers\x18\x03 \x03(\x0b\x32,.coinbase.chainstorage.AptosAccountSignature\"\xa6\x02\n\x16\x41ptosFeePayerSignature\x12<\n\x06sender\x18\x01 \x01(\x0b\x32,.coinbase.chainstorage.AptosAccountSignature\x12\"\n\x1asecondary_signer_addresses\x18\x02 \x03(\t\x12G\n\x11secondary_signers\x18\x03 \x03(\x0b\x32,.coinbase.chainstorage.AptosAccountSignature\x12\x46\n\x10\x66\x65\x65_payer_signer\x18\x04 \x01(\x0b\x32,.coinbase.chainstorage.AptosAccountSignature\x12\x19\n\x11\x66\x65\x65_payer_address\x18\x05 \x01(\t\"C\n\x1a\x41ptosSingleSenderSignature\x12\x12\n\npublic_key\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\t\"@\n\x17\x41ptosSingleKeySignature\x12\x12\n\npublic_key\x18\x01 \x01(\t\x12\x11\n\tsignature\x18\x02 \x01(\t\"^\n\x16\x41ptosMultiKeySignature\x12\x13\n\x0bpublic_keys\x18\x01 \x03(\t\x12\x12\n\nsignatures\x18\x02 \x03(\t\x12\x1b\n\x13signatures_required\x18\x03 \x01(\r\"\xda\x03\n\x15\x41ptosAccountSignature\x12?\n\x04type\x18\x01 \x01(\x0e\x32\x31.coinbase.chainstorage.AptosAccountSignature.Type\x12?\n\x07\x65\x64\x32\x35\x35\x31\x39\x18\x02 \x01(\x0b\x32,.coinbase.chainstorage.AptosEd25519SignatureH\x00\x12J\n\rmulti_ed25519\x18\x03 \x01(\x0b\x32\x31.coinbase.chainstorage.AptosMultiEd25519SignatureH\x00\x12\x44\n\nsingle_key\x18\x05 \x01(\x0b\x32..coinbase.chainstorage.AptosSingleKeySignatureH\x00\x12\x42\n\tmulti_key\x18\x06 \x01(\x0b\x32-.coinbase.chainstorage.AptosMultiKeySignatureH\x00\"\\\n\x04Type\x12\x0f\n\x0bUNSPECIFIED\x10\x00\x12\x0b\n\x07\x45\x44\x32\x35\x35\x31\x39\x10\x01\x12\x11\n\rMULTI_ED25519\x10\x02\x12\x0e\n\nSINGLE_KEY\x10\x04\x12\r\n\tMULTI_KEY\x10\x05\"\x04\x08\x03\x10\x03\x42\x0b\n\tsignature\"N\n\x19\x41ptosValidatorTransaction\x12\x31\n\x06\x65vents\x18\x01 \x03(\x0b\x32!.coinbase.chainstorage.AptosEventB?Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorageb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.chainstorage.blockchain_aptos_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorage'
+ _globals['_APTOSBLOBDATA']._serialized_start=104
+ _globals['_APTOSBLOBDATA']._serialized_end=134
+ _globals['_APTOSBLOCK']._serialized_start=136
+ _globals['_APTOSBLOCK']._serialized_end=263
+ _globals['_APTOSHEADER']._serialized_start=265
+ _globals['_APTOSHEADER']._serialized_end=368
+ _globals['_APTOSTRANSACTION']._serialized_start=371
+ _globals['_APTOSTRANSACTION']._serialized_end=1096
+ _globals['_APTOSTRANSACTION_TRANSACTIONTYPE']._serialized_start=970
+ _globals['_APTOSTRANSACTION_TRANSACTIONTYPE']._serialized_end=1084
+ _globals['_APTOSTRANSACTIONINFO']._serialized_start=1099
+ _globals['_APTOSTRANSACTIONINFO']._serialized_end=1400
+ _globals['_APTOSWRITESETCHANGE']._serialized_start=1403
+ _globals['_APTOSWRITESETCHANGE']._serialized_end=2064
+ _globals['_APTOSWRITESETCHANGE_TYPE']._serialized_start=1908
+ _globals['_APTOSWRITESETCHANGE_TYPE']._serialized_end=2054
+ _globals['_APTOSDELETEMODULE']._serialized_start=2066
+ _globals['_APTOSDELETEMODULE']._serialized_end=2184
+ _globals['_APTOSDELETERESOURCE']._serialized_start=2186
+ _globals['_APTOSDELETERESOURCE']._serialized_end=2266
+ _globals['_APTOSDELETETABLEITEM']._serialized_start=2269
+ _globals['_APTOSDELETETABLEITEM']._serialized_end=2403
+ _globals['_APTOSDELETETABLEDATA']._serialized_start=2405
+ _globals['_APTOSDELETETABLEDATA']._serialized_end=2458
+ _globals['_APTOSWRITEMODULE']._serialized_start=2460
+ _globals['_APTOSWRITEMODULE']._serialized_end=2581
+ _globals['_APTOSWRITERESOURCE']._serialized_start=2583
+ _globals['_APTOSWRITERESOURCE']._serialized_end=2676
+ _globals['_APTOSWRITETABLEITEM']._serialized_start=2679
+ _globals['_APTOSWRITETABLEITEM']._serialized_end=2830
+ _globals['_APTOSWRITETABLEITEMDATA']._serialized_start=2832
+ _globals['_APTOSWRITETABLEITEMDATA']._serialized_end=2923
+ _globals['_APTOSBLOCKMETADATATRANSACTION']._serialized_start=2926
+ _globals['_APTOSBLOCKMETADATATRANSACTION']._serialized_end=3138
+ _globals['_APTOSEVENT']._serialized_start=3140
+ _globals['_APTOSEVENT']._serialized_end=3256
+ _globals['_APTOSEVENTKEY']._serialized_start=3258
+ _globals['_APTOSEVENTKEY']._serialized_end=3323
+ _globals['_APTOSSTATECHECKPOINTTRANSACTION']._serialized_start=3325
+ _globals['_APTOSSTATECHECKPOINTTRANSACTION']._serialized_end=3358
+ _globals['_APTOSGENESISTRANSACTION']._serialized_start=3361
+ _globals['_APTOSGENESISTRANSACTION']._serialized_end=3492
+ _globals['_APTOSWRITESET']._serialized_start=3495
+ _globals['_APTOSWRITESET']._serialized_end=3803
+ _globals['_APTOSWRITESET_TYPE']._serialized_start=3723
+ _globals['_APTOSWRITESET_TYPE']._serialized_end=3790
+ _globals['_APTOSSCRIPTWRITESET']._serialized_start=3805
+ _globals['_APTOSSCRIPTWRITESET']._serialized_end=3905
+ _globals['_APTOSDIRECTWRITESET']._serialized_start=3908
+ _globals['_APTOSDIRECTWRITESET']._serialized_end=4050
+ _globals['_APTOSUSERTRANSACTION']._serialized_start=4053
+ _globals['_APTOSUSERTRANSACTION']._serialized_end=4195
+ _globals['_APTOSUSERTRANSACTIONREQUEST']._serialized_start=4198
+ _globals['_APTOSUSERTRANSACTIONREQUEST']._serialized_end=4502
+ _globals['_APTOSTRANSACTIONPAYLOAD']._serialized_start=4505
+ _globals['_APTOSTRANSACTIONPAYLOAD']._serialized_end=5136
+ _globals['_APTOSTRANSACTIONPAYLOAD_TYPE']._serialized_start=4982
+ _globals['_APTOSTRANSACTIONPAYLOAD_TYPE']._serialized_end=5125
+ _globals['_APTOSENTRYFUNCTIONPAYLOAD']._serialized_start=5139
+ _globals['_APTOSENTRYFUNCTIONPAYLOAD']._serialized_end=5272
+ _globals['_APTOSENTRYFUNCTIONID']._serialized_start=5274
+ _globals['_APTOSENTRYFUNCTIONID']._serialized_end=5377
+ _globals['_APTOSMOVEMODULEID']._serialized_start=5379
+ _globals['_APTOSMOVEMODULEID']._serialized_end=5429
+ _globals['_APTOSSCRIPTPAYLOAD']._serialized_start=5431
+ _globals['_APTOSSCRIPTPAYLOAD']._serialized_end=5556
+ _globals['_APTOSMOVESCRIPTBYTECODE']._serialized_start=5558
+ _globals['_APTOSMOVESCRIPTBYTECODE']._serialized_end=5656
+ _globals['_APTOSMOVEFUNCTION']._serialized_start=5659
+ _globals['_APTOSMOVEFUNCTION']._serialized_end=5958
+ _globals['_APTOSMOVEFUNCTION_TYPE']._serialized_start=5898
+ _globals['_APTOSMOVEFUNCTION_TYPE']._serialized_end=5958
+ _globals['_APTOSMOVEFUNCTIONGENERICTYPEPARAM']._serialized_start=5960
+ _globals['_APTOSMOVEFUNCTIONGENERICTYPEPARAM']._serialized_end=6016
+ _globals['_APTOSMODULEBUNDLEPAYLOAD']._serialized_start=6018
+ _globals['_APTOSMODULEBUNDLEPAYLOAD']._serialized_end=6109
+ _globals['_APTOSMOVEMODULEBYTECODE']._serialized_start=6111
+ _globals['_APTOSMOVEMODULEBYTECODE']._serialized_end=6207
+ _globals['_APTOSMOVEMODULE']._serialized_start=6210
+ _globals['_APTOSMOVEMODULE']._serialized_end=6443
+ _globals['_APTOSMOVESTRUCT']._serialized_start=6446
+ _globals['_APTOSMOVESTRUCT']._serialized_end=6661
+ _globals['_APTOSMOVESTRUCTGENERICTYPEPARAM']._serialized_start=6663
+ _globals['_APTOSMOVESTRUCTGENERICTYPEPARAM']._serialized_end=6717
+ _globals['_APTOSMOVESTRUCTFIELD']._serialized_start=6719
+ _globals['_APTOSMOVESTRUCTFIELD']._serialized_end=6769
+ _globals['_APTOSWRITESETPAYLOAD']._serialized_start=6771
+ _globals['_APTOSWRITESETPAYLOAD']._serialized_end=6850
+ _globals['_APTOSMULTISIGPAYLOAD']._serialized_start=6853
+ _globals['_APTOSMULTISIGPAYLOAD']._serialized_end=7020
+ _globals['_APTOSMULTISIGTRANSACTIONPAYLOAD']._serialized_start=7023
+ _globals['_APTOSMULTISIGTRANSACTIONPAYLOAD']._serialized_end=7279
+ _globals['_APTOSMULTISIGTRANSACTIONPAYLOAD_TYPE']._serialized_start=4982
+ _globals['_APTOSMULTISIGTRANSACTIONPAYLOAD_TYPE']._serialized_end=5033
+ _globals['_APTOSSIGNATURE']._serialized_start=7282
+ _globals['_APTOSSIGNATURE']._serialized_end=7834
+ _globals['_APTOSSIGNATURE_TYPE']._serialized_start=7715
+ _globals['_APTOSSIGNATURE_TYPE']._serialized_end=7821
+ _globals['_APTOSED25519SIGNATURE']._serialized_start=7836
+ _globals['_APTOSED25519SIGNATURE']._serialized_end=7898
+ _globals['_APTOSMULTIED25519SIGNATURE']._serialized_start=7900
+ _globals['_APTOSMULTIED25519SIGNATURE']._serialized_end=8016
+ _globals['_APTOSMULTIAGENTSIGNATURE']._serialized_start=8019
+ _globals['_APTOSMULTIAGENTSIGNATURE']._serialized_end=8216
+ _globals['_APTOSFEEPAYERSIGNATURE']._serialized_start=8219
+ _globals['_APTOSFEEPAYERSIGNATURE']._serialized_end=8513
+ _globals['_APTOSSINGLESENDERSIGNATURE']._serialized_start=8515
+ _globals['_APTOSSINGLESENDERSIGNATURE']._serialized_end=8582
+ _globals['_APTOSSINGLEKEYSIGNATURE']._serialized_start=8584
+ _globals['_APTOSSINGLEKEYSIGNATURE']._serialized_end=8648
+ _globals['_APTOSMULTIKEYSIGNATURE']._serialized_start=8650
+ _globals['_APTOSMULTIKEYSIGNATURE']._serialized_end=8744
+ _globals['_APTOSACCOUNTSIGNATURE']._serialized_start=8747
+ _globals['_APTOSACCOUNTSIGNATURE']._serialized_end=9221
+ _globals['_APTOSACCOUNTSIGNATURE_TYPE']._serialized_start=9116
+ _globals['_APTOSACCOUNTSIGNATURE_TYPE']._serialized_end=9208
+ _globals['_APTOSVALIDATORTRANSACTION']._serialized_start=9223
+ _globals['_APTOSVALIDATORTRANSACTION']._serialized_end=9301
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_aptos_pb2_grpc.py b/gen/src/python/coinbase/chainstorage/blockchain_aptos_pb2_grpc.py
new file mode 100644
index 00000000..5e3fb7e1
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_aptos_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/chainstorage/blockchain_aptos_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_bitcoin_pb2.py b/gen/src/python/coinbase/chainstorage/blockchain_bitcoin_pb2.py
new file mode 100644
index 00000000..a1dd4f24
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_bitcoin_pb2.py
@@ -0,0 +1,54 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/chainstorage/blockchain_bitcoin.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/chainstorage/blockchain_bitcoin.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.coinbase/chainstorage/blockchain_bitcoin.proto\x12\x15\x63oinbase.chainstorage\x1a\x1fgoogle/protobuf/timestamp.proto\"c\n\x0f\x42itcoinBlobdata\x12\x0e\n\x06header\x18\x01 \x01(\x0c\x12@\n\x12input_transactions\x18\x02 \x03(\x0b\x32$.coinbase.chainstorage.RepeatedBytes\"\x1d\n\rRepeatedBytes\x12\x0c\n\x04\x64\x61ta\x18\x01 \x03(\x0c\"\x8a\x03\n\rBitcoinHeader\x12\x0c\n\x04hash\x18\x01 \x01(\t\x12\x15\n\rstripped_size\x18\x03 \x01(\x04\x12\x0c\n\x04size\x18\x04 \x01(\x04\x12\x0e\n\x06weight\x18\x05 \x01(\x04\x12\x0e\n\x06height\x18\x06 \x01(\x04\x12\x0f\n\x07version\x18\x07 \x01(\x04\x12\x13\n\x0bversion_hex\x18\x08 \x01(\t\x12\x13\n\x0bmerkle_root\x18\t \x01(\t\x12\x0c\n\x04time\x18\n \x01(\x04\x12\x13\n\x0bmedian_time\x18\x0b \x01(\x04\x12\r\n\x05nonce\x18\x0c \x01(\x04\x12\x0c\n\x04\x62its\x18\r \x01(\t\x12\x12\n\ndifficulty\x18\x0e \x01(\t\x12\x12\n\nchain_work\x18\x0f \x01(\t\x12\x1e\n\x16number_of_transactions\x18\x10 \x01(\x04\x12\x1b\n\x13previous_block_hash\x18\x11 \x01(\t\x12\x17\n\x0fnext_block_hash\x18\x12 \x01(\t\x12-\n\ttimestamp\x18\x13 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xde\x03\n\x12\x42itcoinTransaction\x12\x0b\n\x03hex\x18\x02 \x01(\t\x12\x16\n\x0etransaction_id\x18\x03 \x01(\t\x12\x0c\n\x04hash\x18\x04 \x01(\t\x12\x0c\n\x04size\x18\x05 \x01(\x04\x12\x14\n\x0cvirtual_size\x18\x06 \x01(\x04\x12\x0e\n\x06weight\x18\x07 \x01(\x04\x12\x0f\n\x07version\x18\x08 \x01(\x04\x12\x11\n\tlock_time\x18\t \x01(\x04\x12>\n\x06inputs\x18\n \x03(\x0b\x32..coinbase.chainstorage.BitcoinTransactionInput\x12@\n\x07outputs\x18\x0b \x03(\x0b\x32/.coinbase.chainstorage.BitcoinTransactionOutput\x12\x12\n\nblock_hash\x18\x0c \x01(\t\x12\x12\n\nblock_time\x18\x0e \x01(\x04\x12\x0c\n\x04time\x18\x0f \x01(\x04\x12\x13\n\x0bis_coinbase\x18\x10 \x01(\x08\x12\r\n\x05index\x18\x11 \x01(\x04\x12\x13\n\x0binput_count\x18\x12 \x01(\x04\x12\x14\n\x0coutput_count\x18\x13 \x01(\x04\x12\x13\n\x0binput_value\x18\x14 \x01(\x04\x12\x14\n\x0coutput_value\x18\x15 \x01(\x04\x12\x0b\n\x03\x66\x65\x65\x18\x16 \x01(\x04\"\xb3\x02\n\x17\x42itcoinTransactionInput\x12\x10\n\x08\x63oinbase\x18\x01 \x01(\t\x12\x16\n\x0etransaction_id\x18\x02 \x01(\t\x12\x19\n\x11\x66rom_output_index\x18\x03 \x01(\x04\x12G\n\x10script_signature\x18\x04 \x01(\x0b\x32-.coinbase.chainstorage.BitcoinScriptSignature\x12\x10\n\x08sequence\x18\x05 \x01(\x04\x12#\n\x1btransaction_input_witnesses\x18\x06 \x03(\t\x12\x44\n\x0b\x66rom_output\x18\x07 \x01(\x0b\x32/.coinbase.chainstorage.BitcoinTransactionOutput\x12\r\n\x05index\x18\x08 \x01(\x04\"7\n\x16\x42itcoinScriptSignature\x12\x10\n\x08\x61ssembly\x18\x01 \x01(\t\x12\x0b\n\x03hex\x18\x02 \x01(\t\"\x82\x01\n\x18\x42itcoinTransactionOutput\x12\r\n\x05index\x18\x02 \x01(\x04\x12H\n\x11script_public_key\x18\x03 \x01(\x0b\x32-.coinbase.chainstorage.BitcoinScriptPublicKey\x12\r\n\x05value\x18\x04 \x01(\x04\"V\n\x16\x42itcoinScriptPublicKey\x12\x10\n\x08\x61ssembly\x18\x01 \x01(\t\x12\x0b\n\x03hex\x18\x02 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\t\x12\x0f\n\x07\x61\x64\x64ress\x18\x06 \x01(\t\"\x85\x01\n\x0c\x42itcoinBlock\x12\x34\n\x06header\x18\x01 \x01(\x0b\x32$.coinbase.chainstorage.BitcoinHeader\x12?\n\x0ctransactions\x18\x02 \x03(\x0b\x32).coinbase.chainstorage.BitcoinTransactionB?Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorageb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.chainstorage.blockchain_bitcoin_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorage'
+ _globals['_BITCOINBLOBDATA']._serialized_start=106
+ _globals['_BITCOINBLOBDATA']._serialized_end=205
+ _globals['_REPEATEDBYTES']._serialized_start=207
+ _globals['_REPEATEDBYTES']._serialized_end=236
+ _globals['_BITCOINHEADER']._serialized_start=239
+ _globals['_BITCOINHEADER']._serialized_end=633
+ _globals['_BITCOINTRANSACTION']._serialized_start=636
+ _globals['_BITCOINTRANSACTION']._serialized_end=1114
+ _globals['_BITCOINTRANSACTIONINPUT']._serialized_start=1117
+ _globals['_BITCOINTRANSACTIONINPUT']._serialized_end=1424
+ _globals['_BITCOINSCRIPTSIGNATURE']._serialized_start=1426
+ _globals['_BITCOINSCRIPTSIGNATURE']._serialized_end=1481
+ _globals['_BITCOINTRANSACTIONOUTPUT']._serialized_start=1484
+ _globals['_BITCOINTRANSACTIONOUTPUT']._serialized_end=1614
+ _globals['_BITCOINSCRIPTPUBLICKEY']._serialized_start=1616
+ _globals['_BITCOINSCRIPTPUBLICKEY']._serialized_end=1702
+ _globals['_BITCOINBLOCK']._serialized_start=1705
+ _globals['_BITCOINBLOCK']._serialized_end=1838
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_bitcoin_pb2_grpc.py b/gen/src/python/coinbase/chainstorage/blockchain_bitcoin_pb2_grpc.py
new file mode 100644
index 00000000..bb2d32c4
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_bitcoin_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/chainstorage/blockchain_bitcoin_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_ethereum_beacon_pb2.py b/gen/src/python/coinbase/chainstorage/blockchain_ethereum_beacon_pb2.py
new file mode 100644
index 00000000..1db64782
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_ethereum_beacon_pb2.py
@@ -0,0 +1,67 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/chainstorage/blockchain_ethereum_beacon.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/chainstorage/blockchain_ethereum_beacon.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+from coinbase.chainstorage import blockchain_ethereum_pb2 as coinbase_dot_chainstorage_dot_blockchain__ethereum__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n6coinbase/chainstorage/blockchain_ethereum_beacon.proto\x12\x15\x63oinbase.chainstorage\x1a\x1fgoogle/protobuf/timestamp.proto\x1a/coinbase/chainstorage/blockchain_ethereum.proto\"L\n\x16\x45thereumBeaconBlobdata\x12\x0e\n\x06header\x18\x01 \x01(\x0c\x12\r\n\x05\x62lock\x18\x02 \x01(\x0c\x12\r\n\x05\x62lobs\x18\x04 \x01(\x0cJ\x04\x08\x03\x10\x04\"\xd0\x01\n\x13\x45thereumBeaconBlock\x12@\n\x06header\x18\x01 \x01(\x0b\x32\x30.coinbase.chainstorage.EthereumBeaconBlockHeader\x12=\n\x05\x62lock\x18\x02 \x01(\x0b\x32..coinbase.chainstorage.EthereumBeaconBlockData\x12\x38\n\x05\x62lobs\x18\x03 \x03(\x0b\x32).coinbase.chainstorage.EthereumBeaconBlob\"\xad\x01\n\x19\x45thereumBeaconBlockHeader\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x16\n\x0eproposer_index\x18\x02 \x01(\x04\x12\x13\n\x0bparent_root\x18\x03 \x01(\t\x12\x12\n\nstate_root\x18\x04 \x01(\t\x12\x11\n\tbody_root\x18\x05 \x01(\t\x12\x11\n\tsignature\x18\x06 \x01(\t\x12\x0c\n\x04root\x18\x07 \x01(\t\x12\r\n\x05\x65poch\x18\x08 \x01(\x04\"\xc0\x04\n\x17\x45thereumBeaconBlockData\x12=\n\x07version\x18\x01 \x01(\x0e\x32,.coinbase.chainstorage.EthereumBeaconVersion\x12\x0c\n\x04slot\x18\x02 \x01(\x04\x12\x16\n\x0eproposer_index\x18\x03 \x01(\x04\x12\x13\n\x0bparent_root\x18\x04 \x01(\t\x12\x12\n\nstate_root\x18\x05 \x01(\t\x12\x11\n\tsignature\x18\x06 \x01(\t\x12H\n\x0cphase0_block\x18\x64 \x01(\x0b\x32\x30.coinbase.chainstorage.EthereumBeaconBlockPhase0H\x00\x12H\n\x0c\x61ltair_block\x18\x65 \x01(\x0b\x32\x30.coinbase.chainstorage.EthereumBeaconBlockAltairH\x00\x12N\n\x0f\x62\x65llatrix_block\x18\x66 \x01(\x0b\x32\x33.coinbase.chainstorage.EthereumBeaconBlockBellatrixH\x00\x12J\n\rcapella_block\x18g \x01(\x0b\x32\x31.coinbase.chainstorage.EthereumBeaconBlockCapellaH\x00\x12\x46\n\x0b\x64\x65neb_block\x18h \x01(\x0b\x32/.coinbase.chainstorage.EthereumBeaconBlockDenebH\x00\x42\x0c\n\nblock_data\"t\n\x19\x45thereumBeaconBlockPhase0\x12\x15\n\rrandao_reveal\x18\x01 \x01(\t\x12@\n\teth1_data\x18\x02 \x01(\x0b\x32-.coinbase.chainstorage.EthereumBeaconEth1Data\"t\n\x19\x45thereumBeaconBlockAltair\x12\x15\n\rrandao_reveal\x18\x01 \x01(\t\x12@\n\teth1_data\x18\x02 \x01(\x0b\x32-.coinbase.chainstorage.EthereumBeaconEth1Data\"\xd2\x01\n\x1c\x45thereumBeaconBlockBellatrix\x12\x15\n\rrandao_reveal\x18\x01 \x01(\t\x12@\n\teth1_data\x18\x02 \x01(\x0b\x32-.coinbase.chainstorage.EthereumBeaconEth1Data\x12Y\n\x11\x65xecution_payload\x18\x03 \x01(\x0b\x32>.coinbase.chainstorage.EthereumBeaconExecutionPayloadBellatrix\"\xce\x01\n\x1a\x45thereumBeaconBlockCapella\x12\x15\n\rrandao_reveal\x18\x01 \x01(\t\x12@\n\teth1_data\x18\x02 \x01(\x0b\x32-.coinbase.chainstorage.EthereumBeaconEth1Data\x12W\n\x11\x65xecution_payload\x18\x03 \x01(\x0b\x32<.coinbase.chainstorage.EthereumBeaconExecutionPayloadCapella\"\xe8\x01\n\x18\x45thereumBeaconBlockDeneb\x12\x15\n\rrandao_reveal\x18\x01 \x01(\t\x12@\n\teth1_data\x18\x02 \x01(\x0b\x32-.coinbase.chainstorage.EthereumBeaconEth1Data\x12U\n\x11\x65xecution_payload\x18\x03 \x01(\x0b\x32:.coinbase.chainstorage.EthereumBeaconExecutionPayloadDeneb\x12\x1c\n\x14\x62lob_kzg_commitments\x18\x04 \x03(\t\"Y\n\x16\x45thereumBeaconEth1Data\x12\x14\n\x0c\x64\x65posit_root\x18\x01 \x01(\t\x12\x15\n\rdeposit_count\x18\x02 \x01(\x04\x12\x12\n\nblock_hash\x18\x03 \x01(\t\"\xeb\x02\n\'EthereumBeaconExecutionPayloadBellatrix\x12\x13\n\x0bparent_hash\x18\x01 \x01(\t\x12\x15\n\rfee_recipient\x18\x02 \x01(\t\x12\x12\n\nstate_root\x18\x03 \x01(\t\x12\x15\n\rreceipts_root\x18\x04 \x01(\t\x12\x12\n\nlogs_bloom\x18\x05 \x01(\t\x12\x13\n\x0bprev_randao\x18\x06 \x01(\t\x12\x14\n\x0c\x62lock_number\x18\x07 \x01(\x04\x12\x11\n\tgas_limit\x18\x08 \x01(\x04\x12\x10\n\x08gas_used\x18\t \x01(\x04\x12-\n\ttimestamp\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nextra_data\x18\x0b \x01(\t\x12\x18\n\x10\x62\x61se_fee_per_gas\x18\x0c \x01(\t\x12\x12\n\nblock_hash\x18\r \x01(\t\x12\x14\n\x0ctransactions\x18\x0e \x03(\x0c\"\xa9\x03\n%EthereumBeaconExecutionPayloadCapella\x12\x13\n\x0bparent_hash\x18\x01 \x01(\t\x12\x15\n\rfee_recipient\x18\x02 \x01(\t\x12\x12\n\nstate_root\x18\x03 \x01(\t\x12\x15\n\rreceipts_root\x18\x04 \x01(\t\x12\x12\n\nlogs_bloom\x18\x05 \x01(\t\x12\x13\n\x0bprev_randao\x18\x06 \x01(\t\x12\x14\n\x0c\x62lock_number\x18\x07 \x01(\x04\x12\x11\n\tgas_limit\x18\x08 \x01(\x04\x12\x10\n\x08gas_used\x18\t \x01(\x04\x12-\n\ttimestamp\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nextra_data\x18\x0b \x01(\t\x12\x18\n\x10\x62\x61se_fee_per_gas\x18\x0c \x01(\t\x12\x12\n\nblock_hash\x18\r \x01(\t\x12\x14\n\x0ctransactions\x18\x0e \x03(\x0c\x12>\n\x0bwithdrawals\x18\x0f \x03(\x0b\x32).coinbase.chainstorage.EthereumWithdrawal\"\xd7\x03\n#EthereumBeaconExecutionPayloadDeneb\x12\x13\n\x0bparent_hash\x18\x01 \x01(\t\x12\x15\n\rfee_recipient\x18\x02 \x01(\t\x12\x12\n\nstate_root\x18\x03 \x01(\t\x12\x15\n\rreceipts_root\x18\x04 \x01(\t\x12\x12\n\nlogs_bloom\x18\x05 \x01(\t\x12\x13\n\x0bprev_randao\x18\x06 \x01(\t\x12\x14\n\x0c\x62lock_number\x18\x07 \x01(\x04\x12\x11\n\tgas_limit\x18\x08 \x01(\x04\x12\x10\n\x08gas_used\x18\t \x01(\x04\x12-\n\ttimestamp\x18\n \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x12\n\nextra_data\x18\x0b \x01(\t\x12\x18\n\x10\x62\x61se_fee_per_gas\x18\x0c \x01(\t\x12\x12\n\nblock_hash\x18\r \x01(\t\x12\x14\n\x0ctransactions\x18\x0e \x03(\x0c\x12>\n\x0bwithdrawals\x18\x0f \x03(\x0b\x32).coinbase.chainstorage.EthereumWithdrawal\x12\x15\n\rblob_gas_used\x18\x10 \x01(\x04\x12\x17\n\x0f\x65xcess_blob_gas\x18\x11 \x01(\x04\"\xa7\x01\n\x12\x45thereumBeaconBlob\x12\x0c\n\x04slot\x18\x01 \x01(\x04\x12\x13\n\x0bparent_root\x18\x02 \x01(\t\x12\r\n\x05index\x18\x03 \x01(\x04\x12\x0c\n\x04\x62lob\x18\x04 \x01(\x0c\x12\x16\n\x0ekzg_commitment\x18\x05 \x01(\t\x12\x11\n\tkzg_proof\x18\x06 \x01(\t\x12&\n\x1ekzg_commitment_inclusion_proof\x18\x07 \x03(\t*c\n\x15\x45thereumBeaconVersion\x12\x0b\n\x07UNKNOWN\x10\x00\x12\n\n\x06PHASE0\x10\x01\x12\n\n\x06\x41LTAIR\x10\x02\x12\r\n\tBELLATRIX\x10\x03\x12\x0b\n\x07\x43\x41PELLA\x10\x04\x12\t\n\x05\x44\x45NEB\x10\x05\x42?Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorageb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.chainstorage.blockchain_ethereum_beacon_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorage'
+ _globals['_ETHEREUMBEACONVERSION']._serialized_start=3629
+ _globals['_ETHEREUMBEACONVERSION']._serialized_end=3728
+ _globals['_ETHEREUMBEACONBLOBDATA']._serialized_start=163
+ _globals['_ETHEREUMBEACONBLOBDATA']._serialized_end=239
+ _globals['_ETHEREUMBEACONBLOCK']._serialized_start=242
+ _globals['_ETHEREUMBEACONBLOCK']._serialized_end=450
+ _globals['_ETHEREUMBEACONBLOCKHEADER']._serialized_start=453
+ _globals['_ETHEREUMBEACONBLOCKHEADER']._serialized_end=626
+ _globals['_ETHEREUMBEACONBLOCKDATA']._serialized_start=629
+ _globals['_ETHEREUMBEACONBLOCKDATA']._serialized_end=1205
+ _globals['_ETHEREUMBEACONBLOCKPHASE0']._serialized_start=1207
+ _globals['_ETHEREUMBEACONBLOCKPHASE0']._serialized_end=1323
+ _globals['_ETHEREUMBEACONBLOCKALTAIR']._serialized_start=1325
+ _globals['_ETHEREUMBEACONBLOCKALTAIR']._serialized_end=1441
+ _globals['_ETHEREUMBEACONBLOCKBELLATRIX']._serialized_start=1444
+ _globals['_ETHEREUMBEACONBLOCKBELLATRIX']._serialized_end=1654
+ _globals['_ETHEREUMBEACONBLOCKCAPELLA']._serialized_start=1657
+ _globals['_ETHEREUMBEACONBLOCKCAPELLA']._serialized_end=1863
+ _globals['_ETHEREUMBEACONBLOCKDENEB']._serialized_start=1866
+ _globals['_ETHEREUMBEACONBLOCKDENEB']._serialized_end=2098
+ _globals['_ETHEREUMBEACONETH1DATA']._serialized_start=2100
+ _globals['_ETHEREUMBEACONETH1DATA']._serialized_end=2189
+ _globals['_ETHEREUMBEACONEXECUTIONPAYLOADBELLATRIX']._serialized_start=2192
+ _globals['_ETHEREUMBEACONEXECUTIONPAYLOADBELLATRIX']._serialized_end=2555
+ _globals['_ETHEREUMBEACONEXECUTIONPAYLOADCAPELLA']._serialized_start=2558
+ _globals['_ETHEREUMBEACONEXECUTIONPAYLOADCAPELLA']._serialized_end=2983
+ _globals['_ETHEREUMBEACONEXECUTIONPAYLOADDENEB']._serialized_start=2986
+ _globals['_ETHEREUMBEACONEXECUTIONPAYLOADDENEB']._serialized_end=3457
+ _globals['_ETHEREUMBEACONBLOB']._serialized_start=3460
+ _globals['_ETHEREUMBEACONBLOB']._serialized_end=3627
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_ethereum_beacon_pb2_grpc.py b/gen/src/python/coinbase/chainstorage/blockchain_ethereum_beacon_pb2_grpc.py
new file mode 100644
index 00000000..3a938960
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_ethereum_beacon_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/chainstorage/blockchain_ethereum_beacon_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_ethereum_pb2.py b/gen/src/python/coinbase/chainstorage/blockchain_ethereum_pb2.py
new file mode 100644
index 00000000..10eebcc0
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_ethereum_pb2.py
@@ -0,0 +1,74 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/chainstorage/blockchain_ethereum.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/chainstorage/blockchain_ethereum.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/coinbase/chainstorage/blockchain_ethereum.proto\x12\x15\x63oinbase.chainstorage\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb6\x01\n\x10\x45thereumBlobdata\x12\x0e\n\x06header\x18\x01 \x01(\x0c\x12\x1c\n\x14transaction_receipts\x18\x02 \x03(\x0c\x12\x1a\n\x12transaction_traces\x18\x03 \x03(\x0c\x12\x0e\n\x06uncles\x18\x04 \x03(\x0c\x12:\n\x07polygon\x18\x64 \x01(\x0b\x32\'.coinbase.chainstorage.PolygonExtraDataH\x00\x42\x0c\n\nextra_data\"\"\n\x10PolygonExtraData\x12\x0e\n\x06\x61uthor\x18\x01 \x01(\x0c\"\xbf\x01\n\rEthereumBlock\x12\x35\n\x06header\x18\x01 \x01(\x0b\x32%.coinbase.chainstorage.EthereumHeader\x12@\n\x0ctransactions\x18\x02 \x03(\x0b\x32*.coinbase.chainstorage.EthereumTransaction\x12\x35\n\x06uncles\x18\x03 \x03(\x0b\x32%.coinbase.chainstorage.EthereumHeader\"]\n\x12\x45thereumWithdrawal\x12\r\n\x05index\x18\x01 \x01(\x04\x12\x17\n\x0fvalidator_index\x18\x02 \x01(\x04\x12\x0f\n\x07\x61\x64\x64ress\x18\x03 \x01(\t\x12\x0e\n\x06\x61mount\x18\x04 \x01(\x04\"\x92\x06\n\x0e\x45thereumHeader\x12\x0c\n\x04hash\x18\x01 \x01(\t\x12\x13\n\x0bparent_hash\x18\x02 \x01(\t\x12\x0e\n\x06number\x18\x03 \x01(\x04\x12-\n\ttimestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x14\n\x0ctransactions\x18\x05 \x03(\t\x12\r\n\x05nonce\x18\x06 \x01(\t\x12\x13\n\x0bsha3_uncles\x18\x07 \x01(\t\x12\x12\n\nlogs_bloom\x18\x08 \x01(\t\x12\x19\n\x11transactions_root\x18\t \x01(\t\x12\x12\n\nstate_root\x18\n \x01(\t\x12\x15\n\rreceipts_root\x18\x0b \x01(\t\x12\r\n\x05miner\x18\x0c \x01(\t\x12\x12\n\ndifficulty\x18\r \x01(\x04\x12\x18\n\x10total_difficulty\x18\x0e \x01(\t\x12\x12\n\nextra_data\x18\x0f \x01(\t\x12\x0c\n\x04size\x18\x10 \x01(\x04\x12\x11\n\tgas_limit\x18\x11 \x01(\x04\x12\x10\n\x08gas_used\x18\x12 \x01(\x04\x12\x0e\n\x06uncles\x18\x13 \x03(\t\x12\x1a\n\x10\x62\x61se_fee_per_gas\x18\x14 \x01(\x04H\x00\x12\x10\n\x08mix_hash\x18\x15 \x01(\t\x12>\n\x0bwithdrawals\x18\x16 \x03(\x0b\x32).coinbase.chainstorage.EthereumWithdrawal\x12\x18\n\x10withdrawals_root\x18\x17 \x01(\t\x12\x10\n\x06\x61uthor\x18\x18 \x01(\tH\x01\x12\x17\n\rblob_gas_used\x18\x19 \x01(\x04H\x02\x12\x19\n\x0f\x65xcess_blob_gas\x18\x1a \x01(\x04H\x03\x12 \n\x18parent_beacon_block_root\x18\x1b \x01(\t\x12\x18\n\x10\x62lock_extra_data\x18\x1c \x01(\tB\x1b\n\x19optional_base_fee_per_gasB\x19\n\x17optional_polygon_authorB\x18\n\x16optional_blob_gas_usedB\x1a\n\x18optional_excess_blob_gas\"B\n\x19\x45thereumTransactionAccess\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12\x14\n\x0cstorage_keys\x18\x02 \x03(\t\"f\n\x1d\x45thereumTransactionAccessList\x12\x45\n\x0b\x61\x63\x63\x65ss_list\x18\x01 \x03(\x0b\x32\x30.coinbase.chainstorage.EthereumTransactionAccess\"\x99\x08\n\x13\x45thereumTransaction\x12\x12\n\nblock_hash\x18\x01 \x01(\t\x12\x14\n\x0c\x62lock_number\x18\x02 \x01(\x04\x12\x0c\n\x04\x66rom\x18\x03 \x01(\t\x12\x0b\n\x03gas\x18\x04 \x01(\x04\x12\x11\n\tgas_price\x18\x05 \x01(\x04\x12\x0c\n\x04hash\x18\x06 \x01(\t\x12\r\n\x05input\x18\x07 \x01(\t\x12\r\n\x05nonce\x18\x08 \x01(\x04\x12\n\n\x02to\x18\t \x01(\t\x12\r\n\x05index\x18\n \x01(\x04\x12\r\n\x05value\x18\x0b \x01(\t\x12\x42\n\x07receipt\x18\x0c \x01(\x0b\x32\x31.coinbase.chainstorage.EthereumTransactionReceipt\x12\x45\n\x0ftoken_transfers\x18\x0e \x03(\x0b\x32,.coinbase.chainstorage.EthereumTokenTransfer\x12\x0c\n\x04type\x18\x0f \x01(\x04\x12\x19\n\x0fmax_fee_per_gas\x18\x10 \x01(\x04H\x00\x12\"\n\x18max_priority_fee_per_gas\x18\x11 \x01(\x04H\x01\x12W\n\x17transaction_access_list\x18\x12 \x01(\x0b\x32\x34.coinbase.chainstorage.EthereumTransactionAccessListH\x02\x12R\n\x10\x66lattened_traces\x18\x13 \x03(\x0b\x32\x38.coinbase.chainstorage.EthereumTransactionFlattenedTrace\x12\x33\n\x0f\x62lock_timestamp\x18\x14 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x1e\n\x14priority_fee_per_gas\x18\x15 \x01(\x04H\x03\x12\x0e\n\x04mint\x18\x16 \x01(\tH\x04\x12\t\n\x01v\x18\x17 \x01(\t\x12\t\n\x01r\x18\x18 \x01(\t\x12\t\n\x01s\x18\x19 \x01(\t\x12\x12\n\x08\x63hain_id\x18\x1a \x01(\x04H\x05\x12\x13\n\x0bsource_hash\x18\x1b \x01(\t\x12\x14\n\x0cis_system_tx\x18\x1c \x01(\x08\x12\x1e\n\x14max_fee_per_blob_gas\x18\x1d \x01(\tH\x06\x12\x1d\n\x15\x62lob_versioned_hashes\x18\x1e \x03(\tB\x1a\n\x18optional_max_fee_per_gasB#\n!optional_max_priority_fee_per_gasB\"\n optional_transaction_access_listB\x1f\n\x1doptional_priority_fee_per_gasB\x0f\n\roptional_mintB\x13\n\x11optional_chain_idB\x1f\n\x1doptional_max_fee_per_blob_gas\"\xc6\t\n\x1a\x45thereumTransactionReceipt\x12\x18\n\x10transaction_hash\x18\x01 \x01(\t\x12\x19\n\x11transaction_index\x18\x02 \x01(\x04\x12\x12\n\nblock_hash\x18\x03 \x01(\t\x12\x14\n\x0c\x62lock_number\x18\x04 \x01(\x04\x12\x0c\n\x04\x66rom\x18\x05 \x01(\t\x12\n\n\x02to\x18\x06 \x01(\t\x12\x1b\n\x13\x63umulative_gas_used\x18\x07 \x01(\x04\x12\x10\n\x08gas_used\x18\x08 \x01(\x04\x12\x18\n\x10\x63ontract_address\x18\t \x01(\t\x12\x35\n\x04logs\x18\n \x03(\x0b\x32\'.coinbase.chainstorage.EthereumEventLog\x12\x12\n\nlogs_bloom\x18\x0b \x01(\t\x12\x0c\n\x04root\x18\x0c \x01(\t\x12\x10\n\x06status\x18\x0e \x01(\x04H\x00\x12\x0c\n\x04type\x18\x0f \x01(\x04\x12\x1b\n\x13\x65\x66\x66\x65\x63tive_gas_price\x18\x10 \x01(\x04\x12R\n\x0bl1_fee_info\x18\x11 \x01(\x0b\x32;.coinbase.chainstorage.EthereumTransactionReceipt.L1FeeInfoH\x01\x12\x17\n\rdeposit_nonce\x18\x12 \x01(\x04H\x02\x12!\n\x17\x64\x65posit_receipt_version\x18\x13 \x01(\x04H\x03\x12\x18\n\x0e\x62lob_gas_price\x18\x14 \x01(\x04H\x04\x12\x17\n\rblob_gas_used\x18\x15 \x01(\x04H\x05\x12\r\n\x03\x66\x65\x65\x18\x16 \x01(\x04H\x06\x12\x11\n\x07net_fee\x18\x17 \x01(\x04H\x07\x12\x13\n\tnet_usage\x18\x18 \x01(\x04H\x08\x12\x16\n\x0c\x65nergy_usage\x18\x19 \x01(\x04H\t\x12\x14\n\nenergy_fee\x18\x1a \x01(\x04H\n\x12\x1d\n\x13origin_energy_usage\x18\x1b \x01(\x04H\x0b\x12\x1c\n\x12\x65nergy_usage_total\x18\x1c \x01(\x04H\x0c\x12\x1e\n\x14\x65nergy_penalty_total\x18\x1d \x01(\x04H\r\x1a]\n\tL1FeeInfo\x12\x13\n\x0bl1_gas_used\x18\x01 \x01(\x04\x12\x14\n\x0cl1_gas_price\x18\x02 \x01(\x04\x12\x0e\n\x06l1_fee\x18\x03 \x01(\x04\x12\x15\n\rl1_fee_scalar\x18\x04 \x01(\tB\x11\n\x0foptional_statusB\x16\n\x14optional_l1_fee_infoB\x18\n\x16optional_deposit_nonceB\"\n optional_deposit_receipt_versionB\x19\n\x17optional_blob_gas_priceB\x18\n\x16optional_blob_gas_usedB\x0e\n\x0coptional_feeB\x12\n\x10optional_net_feeB\x14\n\x12optional_net_usageB\x17\n\x15optional_energy_usageB\x15\n\x13optional_energy_feeB\x1e\n\x1coptional_origin_energy_usageB\x1d\n\x1boptional_energy_usage_totalB\x1f\n\x1doptional_energy_penalty_totalJ\x04\x08\r\x10\x0e\"\xc4\x01\n\x10\x45thereumEventLog\x12\x0f\n\x07removed\x18\x01 \x01(\x08\x12\x11\n\tlog_index\x18\x02 \x01(\x04\x12\x18\n\x10transaction_hash\x18\x03 \x01(\t\x12\x19\n\x11transaction_index\x18\x04 \x01(\x04\x12\x12\n\nblock_hash\x18\x05 \x01(\t\x12\x14\n\x0c\x62lock_number\x18\x06 \x01(\x04\x12\x0f\n\x07\x61\x64\x64ress\x18\x07 \x01(\t\x12\x0c\n\x04\x64\x61ta\x18\x08 \x01(\t\x12\x0e\n\x06topics\x18\t \x03(\t\"\xde\x01\n\x18\x45thereumTransactionTrace\x12\r\n\x05\x65rror\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0c\n\x04\x66rom\x18\x03 \x01(\t\x12\n\n\x02to\x18\x04 \x01(\t\x12\r\n\x05value\x18\x05 \x01(\t\x12\x0b\n\x03gas\x18\x06 \x01(\x04\x12\x10\n\x08gas_used\x18\x07 \x01(\x04\x12\r\n\x05input\x18\x08 \x01(\t\x12\x0e\n\x06output\x18\t \x01(\t\x12>\n\x05\x63\x61lls\x18\n \x03(\x0b\x32/.coinbase.chainstorage.EthereumTransactionTrace\"\xf9\x02\n!EthereumTransactionFlattenedTrace\x12\r\n\x05\x65rror\x18\x01 \x01(\t\x12\x0c\n\x04type\x18\x02 \x01(\t\x12\x0c\n\x04\x66rom\x18\x03 \x01(\t\x12\n\n\x02to\x18\x04 \x01(\t\x12\r\n\x05value\x18\x05 \x01(\t\x12\x0b\n\x03gas\x18\x06 \x01(\x04\x12\x10\n\x08gas_used\x18\x07 \x01(\x04\x12\r\n\x05input\x18\x08 \x01(\t\x12\x0e\n\x06output\x18\t \x01(\t\x12\x11\n\tsubtraces\x18\n \x01(\x04\x12\x15\n\rtrace_address\x18\x0b \x03(\x04\x12\x12\n\ntrace_type\x18\x0c \x01(\t\x12\x11\n\tcall_type\x18\r \x01(\t\x12\x10\n\x08trace_id\x18\x0e \x01(\t\x12\x0e\n\x06status\x18\x0f \x01(\x04\x12\x12\n\nblock_hash\x18\x10 \x01(\t\x12\x14\n\x0c\x62lock_number\x18\x11 \x01(\x04\x12\x18\n\x10transaction_hash\x18\x12 \x01(\t\x12\x19\n\x11transaction_index\x18\x13 \x01(\x04\"\xe5\x02\n\x15\x45thereumTokenTransfer\x12\x15\n\rtoken_address\x18\x01 \x01(\t\x12\x14\n\x0c\x66rom_address\x18\x02 \x01(\t\x12\x12\n\nto_address\x18\x03 \x01(\t\x12\r\n\x05value\x18\x04 \x01(\t\x12\x19\n\x11transaction_index\x18\x05 \x01(\x04\x12\x18\n\x10transaction_hash\x18\x06 \x01(\t\x12\x11\n\tlog_index\x18\x07 \x01(\x04\x12\x12\n\nblock_hash\x18\x08 \x01(\t\x12\x14\n\x0c\x62lock_number\x18\t \x01(\x04\x12:\n\x05\x65rc20\x18\x64 \x01(\x0b\x32).coinbase.chainstorage.ERC20TokenTransferH\x00\x12<\n\x06\x65rc721\x18\x65 \x01(\x0b\x32*.coinbase.chainstorage.ERC721TokenTransferH\x00\x42\x10\n\x0etoken_transfer\"M\n\x12\x45RC20TokenTransfer\x12\x14\n\x0c\x66rom_address\x18\x01 \x01(\t\x12\x12\n\nto_address\x18\x02 \x01(\t\x12\r\n\x05value\x18\x03 \x01(\t\"Q\n\x13\x45RC721TokenTransfer\x12\x14\n\x0c\x66rom_address\x18\x01 \x01(\t\x12\x12\n\nto_address\x18\x02 \x01(\t\x12\x10\n\x08token_id\x18\x03 \x01(\t\"2\n\x19\x45thereumAccountStateProof\x12\x15\n\raccount_proof\x18\x01 \x01(\x0c\",\n\x12\x45thereumExtraInput\x12\x16\n\x0e\x65rc20_contract\x18\x01 \x01(\t\"V\n\x1c\x45thereumAccountStateResponse\x12\r\n\x05nonce\x18\x01 \x01(\x04\x12\x14\n\x0cstorage_hash\x18\x02 \x01(\t\x12\x11\n\tcode_hash\x18\x03 \x01(\tB?Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorageb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.chainstorage.blockchain_ethereum_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorage'
+ _globals['_ETHEREUMBLOBDATA']._serialized_start=108
+ _globals['_ETHEREUMBLOBDATA']._serialized_end=290
+ _globals['_POLYGONEXTRADATA']._serialized_start=292
+ _globals['_POLYGONEXTRADATA']._serialized_end=326
+ _globals['_ETHEREUMBLOCK']._serialized_start=329
+ _globals['_ETHEREUMBLOCK']._serialized_end=520
+ _globals['_ETHEREUMWITHDRAWAL']._serialized_start=522
+ _globals['_ETHEREUMWITHDRAWAL']._serialized_end=615
+ _globals['_ETHEREUMHEADER']._serialized_start=618
+ _globals['_ETHEREUMHEADER']._serialized_end=1404
+ _globals['_ETHEREUMTRANSACTIONACCESS']._serialized_start=1406
+ _globals['_ETHEREUMTRANSACTIONACCESS']._serialized_end=1472
+ _globals['_ETHEREUMTRANSACTIONACCESSLIST']._serialized_start=1474
+ _globals['_ETHEREUMTRANSACTIONACCESSLIST']._serialized_end=1576
+ _globals['_ETHEREUMTRANSACTION']._serialized_start=1579
+ _globals['_ETHEREUMTRANSACTION']._serialized_end=2628
+ _globals['_ETHEREUMTRANSACTIONRECEIPT']._serialized_start=2631
+ _globals['_ETHEREUMTRANSACTIONRECEIPT']._serialized_end=3853
+ _globals['_ETHEREUMTRANSACTIONRECEIPT_L1FEEINFO']._serialized_start=3394
+ _globals['_ETHEREUMTRANSACTIONRECEIPT_L1FEEINFO']._serialized_end=3487
+ _globals['_ETHEREUMEVENTLOG']._serialized_start=3856
+ _globals['_ETHEREUMEVENTLOG']._serialized_end=4052
+ _globals['_ETHEREUMTRANSACTIONTRACE']._serialized_start=4055
+ _globals['_ETHEREUMTRANSACTIONTRACE']._serialized_end=4277
+ _globals['_ETHEREUMTRANSACTIONFLATTENEDTRACE']._serialized_start=4280
+ _globals['_ETHEREUMTRANSACTIONFLATTENEDTRACE']._serialized_end=4657
+ _globals['_ETHEREUMTOKENTRANSFER']._serialized_start=4660
+ _globals['_ETHEREUMTOKENTRANSFER']._serialized_end=5017
+ _globals['_ERC20TOKENTRANSFER']._serialized_start=5019
+ _globals['_ERC20TOKENTRANSFER']._serialized_end=5096
+ _globals['_ERC721TOKENTRANSFER']._serialized_start=5098
+ _globals['_ERC721TOKENTRANSFER']._serialized_end=5179
+ _globals['_ETHEREUMACCOUNTSTATEPROOF']._serialized_start=5181
+ _globals['_ETHEREUMACCOUNTSTATEPROOF']._serialized_end=5231
+ _globals['_ETHEREUMEXTRAINPUT']._serialized_start=5233
+ _globals['_ETHEREUMEXTRAINPUT']._serialized_end=5277
+ _globals['_ETHEREUMACCOUNTSTATERESPONSE']._serialized_start=5279
+ _globals['_ETHEREUMACCOUNTSTATERESPONSE']._serialized_end=5365
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_ethereum_pb2_grpc.py b/gen/src/python/coinbase/chainstorage/blockchain_ethereum_pb2_grpc.py
new file mode 100644
index 00000000..7312787d
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_ethereum_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/chainstorage/blockchain_ethereum_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_pb2.py b/gen/src/python/coinbase/chainstorage/blockchain_pb2.py
new file mode 100644
index 00000000..5030d931
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_pb2.py
@@ -0,0 +1,75 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/chainstorage/blockchain.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/chainstorage/blockchain.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+from coinbase.c3.common import common_pb2 as coinbase_dot_c3_dot_common_dot_common__pb2
+from coinbase.crypto.rosetta.types import block_pb2 as coinbase_dot_crypto_dot_rosetta_dot_types_dot_block__pb2
+from coinbase.crypto.rosetta.types import transaction_pb2 as coinbase_dot_crypto_dot_rosetta_dot_types_dot_transaction__pb2
+from coinbase.chainstorage import blockchain_bitcoin_pb2 as coinbase_dot_chainstorage_dot_blockchain__bitcoin__pb2
+from coinbase.chainstorage import blockchain_aptos_pb2 as coinbase_dot_chainstorage_dot_blockchain__aptos__pb2
+from coinbase.chainstorage import blockchain_solana_pb2 as coinbase_dot_chainstorage_dot_blockchain__solana__pb2
+from coinbase.chainstorage import blockchain_rosetta_pb2 as coinbase_dot_chainstorage_dot_blockchain__rosetta__pb2
+from coinbase.chainstorage import blockchain_ethereum_pb2 as coinbase_dot_chainstorage_dot_blockchain__ethereum__pb2
+from coinbase.chainstorage import blockchain_ethereum_beacon_pb2 as coinbase_dot_chainstorage_dot_blockchain__ethereum__beacon__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n&coinbase/chainstorage/blockchain.proto\x12\x15\x63oinbase.chainstorage\x1a\x1fgoogle/protobuf/timestamp.proto\x1a\x1f\x63oinbase/c3/common/common.proto\x1a)coinbase/crypto/rosetta/types/block.proto\x1a/coinbase/crypto/rosetta/types/transaction.proto\x1a.coinbase/chainstorage/blockchain_bitcoin.proto\x1a,coinbase/chainstorage/blockchain_aptos.proto\x1a-coinbase/chainstorage/blockchain_solana.proto\x1a.coinbase/chainstorage/blockchain_rosetta.proto\x1a/coinbase/chainstorage/blockchain_ethereum.proto\x1a\x36\x63oinbase/chainstorage/blockchain_ethereum_beacon.proto\"\x9a\x05\n\x05\x42lock\x12\x32\n\nblockchain\x18\x01 \x01(\x0e\x32\x1e.coinbase.c3.common.Blockchain\x12,\n\x07network\x18\x02 \x01(\x0e\x32\x1b.coinbase.c3.common.Network\x12\x36\n\x08metadata\x18\x03 \x01(\x0b\x32$.coinbase.chainstorage.BlockMetadata\x12H\n\x14transaction_metadata\x18\x04 \x01(\x0b\x32*.coinbase.chainstorage.TransactionMetadata\x12\x34\n\nside_chain\x18\x05 \x01(\x0e\x32 .coinbase.chainstorage.SideChain\x12;\n\x08\x65thereum\x18\x64 \x01(\x0b\x32\'.coinbase.chainstorage.EthereumBlobdataH\x00\x12\x39\n\x07\x62itcoin\x18\x65 \x01(\x0b\x32&.coinbase.chainstorage.BitcoinBlobdataH\x00\x12\x39\n\x07rosetta\x18\x66 \x01(\x0b\x32&.coinbase.chainstorage.RosettaBlobdataH\x00\x12\x37\n\x06solana\x18g \x01(\x0b\x32%.coinbase.chainstorage.SolanaBlobdataH\x00\x12\x35\n\x05\x61ptos\x18h \x01(\x0b\x32$.coinbase.chainstorage.AptosBlobdataH\x00\x12H\n\x0f\x65thereum_beacon\x18i \x01(\x0b\x32-.coinbase.chainstorage.EthereumBeaconBlobdataH\x00\x42\n\n\x08\x62lobdata\"|\n\x0f\x42lockIdentifier\x12\x0c\n\x04hash\x18\x01 \x01(\t\x12\x0e\n\x06height\x18\x02 \x01(\x04\x12\x0b\n\x03tag\x18\x03 \x01(\r\x12\x0f\n\x07skipped\x18\x04 \x01(\x08\x12-\n\ttimestamp\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"\xc7\x02\n\rBlockMetadata\x12\x0b\n\x03tag\x18\x01 \x01(\r\x12\x0c\n\x04hash\x18\x02 \x01(\t\x12\x13\n\x0bparent_hash\x18\x03 \x01(\t\x12\x0e\n\x06height\x18\x04 \x01(\x04\x12\x17\n\x0fobject_key_main\x18\x05 \x01(\t\x12\x15\n\rparent_height\x18\x06 \x01(\x04\x12\x0f\n\x07skipped\x18\x07 \x01(\x08\x12-\n\ttimestamp\x18\x08 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x13\n\x0b\x62yte_offset\x18\t \x01(\x04\x12\x13\n\x0b\x62yte_length\x18\n \x01(\x04\x12\x1b\n\x13uncompressed_length\x18\x0b \x01(\x04\x12?\n\robject_format\x18\x0c \x01(\x0e\x32(.coinbase.chainstorage.BlockObjectFormat\"+\n\x13TransactionMetadata\x12\x14\n\x0ctransactions\x18\x01 \x03(\t\"C\n\x0cRosettaBlock\x12\x33\n\x05\x62lock\x18\x01 \x01(\x0b\x32$.coinbase.crypto.rosetta.types.Block\"\xf6\x05\n\x0bNativeBlock\x12\x32\n\nblockchain\x18\x01 \x01(\x0e\x32\x1e.coinbase.c3.common.Blockchain\x12,\n\x07network\x18\x02 \x01(\x0e\x32\x1b.coinbase.c3.common.Network\x12\x0b\n\x03tag\x18\x03 \x01(\r\x12\x0c\n\x04hash\x18\x04 \x01(\t\x12\x13\n\x0bparent_hash\x18\x05 \x01(\t\x12\x0e\n\x06height\x18\x06 \x01(\x04\x12-\n\ttimestamp\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x18\n\x10num_transactions\x18\x08 \x01(\x04\x12\x15\n\rparent_height\x18\t \x01(\x04\x12\x0f\n\x07skipped\x18\n \x01(\x08\x12\x34\n\nside_chain\x18\x0b \x01(\x0e\x32 .coinbase.chainstorage.SideChain\x12\x38\n\x08\x65thereum\x18\x64 \x01(\x0b\x32$.coinbase.chainstorage.EthereumBlockH\x00\x12\x36\n\x07\x62itcoin\x18\x65 \x01(\x0b\x32#.coinbase.chainstorage.BitcoinBlockH\x00\x12\x37\n\x07rosetta\x18\x66 \x01(\x0b\x32$.coinbase.crypto.rosetta.types.BlockH\x00\x12\x34\n\x06solana\x18g \x01(\x0b\x32\".coinbase.chainstorage.SolanaBlockH\x00\x12\x32\n\x05\x61ptos\x18h \x01(\x0b\x32!.coinbase.chainstorage.AptosBlockH\x00\x12\x39\n\tsolana_v2\x18i \x01(\x0b\x32$.coinbase.chainstorage.SolanaBlockV2H\x00\x12\x45\n\x0f\x65thereum_beacon\x18j \x01(\x0b\x32*.coinbase.chainstorage.EthereumBeaconBlockH\x00\x42\x07\n\x05\x62lock\"\xbd\x04\n\x11NativeTransaction\x12\x32\n\nblockchain\x18\x01 \x01(\x0e\x32\x1e.coinbase.c3.common.Blockchain\x12,\n\x07network\x18\x02 \x01(\x0e\x32\x1b.coinbase.c3.common.Network\x12\x0b\n\x03tag\x18\x03 \x01(\r\x12\x18\n\x10transaction_hash\x18\x04 \x01(\t\x12\x14\n\x0c\x62lock_height\x18\x05 \x01(\x04\x12\x12\n\nblock_hash\x18\x06 \x01(\t\x12\x33\n\x0f\x62lock_timestamp\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12>\n\x08\x65thereum\x18\x64 \x01(\x0b\x32*.coinbase.chainstorage.EthereumTransactionH\x00\x12<\n\x07\x62itcoin\x18\x65 \x01(\x0b\x32).coinbase.chainstorage.BitcoinTransactionH\x00\x12=\n\x07rosetta\x18\x66 \x01(\x0b\x32*.coinbase.crypto.rosetta.types.TransactionH\x00\x12:\n\x06solana\x18g \x01(\x0b\x32(.coinbase.chainstorage.SolanaTransactionH\x00\x12\x38\n\x05\x61ptos\x18h \x01(\x0b\x32\'.coinbase.chainstorage.AptosTransactionH\x00\x42\r\n\x0btransaction\"k\n\x17GetAccountProofResponse\x12\x44\n\x08\x65thereum\x18\x64 \x01(\x0b\x32\x30.coinbase.chainstorage.EthereumAccountStateProofH\x00\x42\n\n\x08response\"\xeb\x01\n\x1bValidateAccountStateRequest\x12R\n\x0b\x61\x63\x63ount_req\x18\x01 \x01(\x0b\x32=.coinbase.chainstorage.InternalGetVerifiedAccountStateRequest\x12\x31\n\x05\x62lock\x18\x02 \x01(\x0b\x32\".coinbase.chainstorage.NativeBlock\x12\x45\n\raccount_proof\x18\x03 \x01(\x0b\x32..coinbase.chainstorage.GetAccountProofResponse\"\xb2\x01\n&InternalGetVerifiedAccountStateRequest\x12\x0f\n\x07\x61\x63\x63ount\x18\x01 \x01(\t\x12\x0b\n\x03tag\x18\x02 \x01(\r\x12\x0e\n\x06height\x18\x03 \x01(\x04\x12\x0c\n\x04hash\x18\x04 \x01(\t\x12=\n\x08\x65thereum\x18\x64 \x01(\x0b\x32).coinbase.chainstorage.EthereumExtraInputH\x00\x42\r\n\x0b\x65xtra_input\"\x84\x01\n\x1cValidateAccountStateResponse\x12\x0f\n\x07\x62\x61lance\x18\x01 \x01(\t\x12G\n\x08\x65thereum\x18\x64 \x01(\x0b\x32\x33.coinbase.chainstorage.EthereumAccountStateResponseH\x00\x42\n\n\x08response\"W\n\x1bValidateRosettaBlockRequest\x12\x38\n\x0cnative_block\x18\x01 \x01(\x0b\x32\".coinbase.chainstorage.NativeBlock*\x8e\x01\n\x11\x42lockObjectFormat\x12+\n\'BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK\x10\x00\x12$\n BLOCK_OBJECT_FORMAT_SINGLE_BLOCK\x10\x00\x12\"\n\x1e\x42LOCK_OBJECT_FORMAT_CSCB_BATCH\x10\x01\x1a\x02\x10\x01*m\n\tSideChain\x12\x12\n\x0eSIDECHAIN_NONE\x10\x00\x12%\n!SIDECHAIN_ETHEREUM_MAINNET_BEACON\x10\x01\x12%\n!SIDECHAIN_ETHEREUM_HOLESKY_BEACON\x10\x02\x42?Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorageb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.chainstorage.blockchain_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorage'
+ _globals['_BLOCKOBJECTFORMAT']._loaded_options = None
+ _globals['_BLOCKOBJECTFORMAT']._serialized_options = b'\020\001'
+ _globals['_BLOCKOBJECTFORMAT']._serialized_start=3846
+ _globals['_BLOCKOBJECTFORMAT']._serialized_end=3988
+ _globals['_SIDECHAIN']._serialized_start=3990
+ _globals['_SIDECHAIN']._serialized_end=4099
+ _globals['_BLOCK']._serialized_start=518
+ _globals['_BLOCK']._serialized_end=1184
+ _globals['_BLOCKIDENTIFIER']._serialized_start=1186
+ _globals['_BLOCKIDENTIFIER']._serialized_end=1310
+ _globals['_BLOCKMETADATA']._serialized_start=1313
+ _globals['_BLOCKMETADATA']._serialized_end=1640
+ _globals['_TRANSACTIONMETADATA']._serialized_start=1642
+ _globals['_TRANSACTIONMETADATA']._serialized_end=1685
+ _globals['_ROSETTABLOCK']._serialized_start=1687
+ _globals['_ROSETTABLOCK']._serialized_end=1754
+ _globals['_NATIVEBLOCK']._serialized_start=1757
+ _globals['_NATIVEBLOCK']._serialized_end=2515
+ _globals['_NATIVETRANSACTION']._serialized_start=2518
+ _globals['_NATIVETRANSACTION']._serialized_end=3091
+ _globals['_GETACCOUNTPROOFRESPONSE']._serialized_start=3093
+ _globals['_GETACCOUNTPROOFRESPONSE']._serialized_end=3200
+ _globals['_VALIDATEACCOUNTSTATEREQUEST']._serialized_start=3203
+ _globals['_VALIDATEACCOUNTSTATEREQUEST']._serialized_end=3438
+ _globals['_INTERNALGETVERIFIEDACCOUNTSTATEREQUEST']._serialized_start=3441
+ _globals['_INTERNALGETVERIFIEDACCOUNTSTATEREQUEST']._serialized_end=3619
+ _globals['_VALIDATEACCOUNTSTATERESPONSE']._serialized_start=3622
+ _globals['_VALIDATEACCOUNTSTATERESPONSE']._serialized_end=3754
+ _globals['_VALIDATEROSETTABLOCKREQUEST']._serialized_start=3756
+ _globals['_VALIDATEROSETTABLOCKREQUEST']._serialized_end=3843
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_pb2_grpc.py b/gen/src/python/coinbase/chainstorage/blockchain_pb2_grpc.py
new file mode 100644
index 00000000..1a065aae
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/chainstorage/blockchain_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_rosetta_pb2.py b/gen/src/python/coinbase/chainstorage/blockchain_rosetta_pb2.py
new file mode 100644
index 00000000..372f7eee
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_rosetta_pb2.py
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/chainstorage/blockchain_rosetta.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/chainstorage/blockchain_rosetta.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n.coinbase/chainstorage/blockchain_rosetta.proto\x12\x15\x63oinbase.chainstorage\"P\n\x0fRosettaBlobdata\x12\x0e\n\x06header\x18\x01 \x01(\x0c\x12\x1a\n\x12other_transactions\x18\x02 \x03(\x0c\x12\x11\n\traw_block\x18\x03 \x01(\x0c\x42?Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorageb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.chainstorage.blockchain_rosetta_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorage'
+ _globals['_ROSETTABLOBDATA']._serialized_start=73
+ _globals['_ROSETTABLOBDATA']._serialized_end=153
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_rosetta_pb2_grpc.py b/gen/src/python/coinbase/chainstorage/blockchain_rosetta_pb2_grpc.py
new file mode 100644
index 00000000..c005a5f6
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_rosetta_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/chainstorage/blockchain_rosetta_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_solana_pb2.py b/gen/src/python/coinbase/chainstorage/blockchain_solana_pb2.py
new file mode 100644
index 00000000..d49cfff5
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_solana_pb2.py
@@ -0,0 +1,174 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/chainstorage/blockchain_solana.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/chainstorage/blockchain_solana.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-coinbase/chainstorage/blockchain_solana.proto\x12\x15\x63oinbase.chainstorage\x1a\x1fgoogle/protobuf/timestamp.proto\" \n\x0eSolanaBlobdata\x12\x0e\n\x06header\x18\x01 \x01(\x0c\"\xb8\x01\n\x0bSolanaBlock\x12\x33\n\x06header\x18\x01 \x01(\x0b\x32#.coinbase.chainstorage.SolanaHeader\x12>\n\x0ctransactions\x18\x02 \x03(\x0b\x32(.coinbase.chainstorage.SolanaTransaction\x12\x34\n\x07rewards\x18\x03 \x03(\x0b\x32#.coinbase.chainstorage.SolanaReward\"\xbc\x01\n\rSolanaBlockV2\x12\x33\n\x06header\x18\x01 \x01(\x0b\x32#.coinbase.chainstorage.SolanaHeader\x12@\n\x0ctransactions\x18\x02 \x03(\x0b\x32*.coinbase.chainstorage.SolanaTransactionV2\x12\x34\n\x07rewards\x18\x03 \x03(\x0b\x32#.coinbase.chainstorage.SolanaReward\"\xa8\x01\n\x0cSolanaHeader\x12\x12\n\nblock_hash\x18\x01 \x01(\t\x12\x1b\n\x13previous_block_hash\x18\x02 \x01(\t\x12\x0c\n\x04slot\x18\x03 \x01(\x04\x12\x13\n\x0bparent_slot\x18\x04 \x01(\x04\x12.\n\nblock_time\x18\x05 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12\x14\n\x0c\x62lock_height\x18\x06 \x01(\x04\"\xba\x01\n\x11SolanaTransaction\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x12@\n\x07payload\x18\x02 \x01(\x0b\x32/.coinbase.chainstorage.SolanaTransactionPayload\x12:\n\x04meta\x18\x03 \x01(\x0b\x32,.coinbase.chainstorage.SolanaTransactionMeta\x12\x0f\n\x07version\x18\x04 \x01(\x05\"\xc0\x01\n\x13SolanaTransactionV2\x12\x16\n\x0etransaction_id\x18\x01 \x01(\t\x12\x42\n\x07payload\x18\x02 \x01(\x0b\x32\x31.coinbase.chainstorage.SolanaTransactionPayloadV2\x12<\n\x04meta\x18\x03 \x01(\x0b\x32..coinbase.chainstorage.SolanaTransactionMetaV2\x12\x0f\n\x07version\x18\x04 \x01(\x05\"\x84\x03\n\x15SolanaTransactionMeta\x12\x0b\n\x03\x65rr\x18\x01 \x01(\t\x12\x0b\n\x03\x66\x65\x65\x18\x02 \x01(\x04\x12\x14\n\x0cpre_balances\x18\x03 \x03(\x04\x12\x15\n\rpost_balances\x18\x04 \x03(\x04\x12\x45\n\x12pre_token_balances\x18\x05 \x03(\x0b\x32).coinbase.chainstorage.SolanaTokenBalance\x12\x46\n\x13post_token_balances\x18\x06 \x03(\x0b\x32).coinbase.chainstorage.SolanaTokenBalance\x12I\n\x12inner_instructions\x18\x07 \x03(\x0b\x32-.coinbase.chainstorage.SolanaInnerInstruction\x12\x14\n\x0clog_messages\x18\x08 \x03(\t\x12\x34\n\x07rewards\x18\t \x03(\x0b\x32#.coinbase.chainstorage.SolanaReward\"\x88\x03\n\x17SolanaTransactionMetaV2\x12\x0b\n\x03\x65rr\x18\x01 \x01(\t\x12\x0b\n\x03\x66\x65\x65\x18\x02 \x01(\x04\x12\x14\n\x0cpre_balances\x18\x03 \x03(\x04\x12\x15\n\rpost_balances\x18\x04 \x03(\x04\x12\x45\n\x12pre_token_balances\x18\x05 \x03(\x0b\x32).coinbase.chainstorage.SolanaTokenBalance\x12\x46\n\x13post_token_balances\x18\x06 \x03(\x0b\x32).coinbase.chainstorage.SolanaTokenBalance\x12K\n\x12inner_instructions\x18\x07 \x03(\x0b\x32/.coinbase.chainstorage.SolanaInnerInstructionV2\x12\x14\n\x0clog_messages\x18\x08 \x03(\t\x12\x34\n\x07rewards\x18\t \x03(\x0b\x32#.coinbase.chainstorage.SolanaReward\"\x88\x01\n\x12SolanaTokenBalance\x12\x15\n\raccount_index\x18\x01 \x01(\x04\x12\x0c\n\x04mint\x18\x02 \x01(\t\x12>\n\x0ctoken_amount\x18\x03 \x01(\x0b\x32(.coinbase.chainstorage.SolanaTokenAmount\x12\r\n\x05owner\x18\x04 \x01(\t\"O\n\x11SolanaTokenAmount\x12\x0e\n\x06\x61mount\x18\x01 \x01(\t\x12\x10\n\x08\x64\x65\x63imals\x18\x02 \x01(\x04\x12\x18\n\x10ui_amount_string\x18\x03 \x01(\t\"g\n\x16SolanaInnerInstruction\x12\r\n\x05index\x18\x01 \x01(\x04\x12>\n\x0cinstructions\x18\x02 \x03(\x0b\x32(.coinbase.chainstorage.SolanaInstruction\"k\n\x18SolanaInnerInstructionV2\x12\r\n\x05index\x18\x01 \x01(\x04\x12@\n\x0cinstructions\x18\x02 \x03(\x0b\x32*.coinbase.chainstorage.SolanaInstructionV2\"\x88\x01\n\x0cSolanaReward\x12\x0e\n\x06pubkey\x18\x01 \x01(\x0c\x12\x10\n\x08lamports\x18\x02 \x01(\x03\x12\x14\n\x0cpost_balance\x18\x03 \x01(\x04\x12\x13\n\x0breward_type\x18\x04 \x01(\t\x12\x14\n\ncommission\x18\x05 \x01(\x04H\x00\x42\x15\n\x13optional_commission\"e\n\x18SolanaTransactionPayload\x12\x12\n\nsignatures\x18\x01 \x03(\t\x12\x35\n\x07message\x18\x02 \x01(\x0b\x32$.coinbase.chainstorage.SolanaMessage\"i\n\x1aSolanaTransactionPayloadV2\x12\x12\n\nsignatures\x18\x01 \x03(\t\x12\x37\n\x07message\x18\x02 \x01(\x0b\x32&.coinbase.chainstorage.SolanaMessageV2\"\xde\x01\n\rSolanaMessage\x12:\n\x06header\x18\x01 \x01(\x0b\x32*.coinbase.chainstorage.SolanaMessageHeader\x12\x19\n\x11recent_block_hash\x18\x03 \x01(\t\x12>\n\x0cinstructions\x18\x04 \x03(\x0b\x32(.coinbase.chainstorage.SolanaInstruction\x12\x36\n\x08\x61\x63\x63ounts\x18\x05 \x03(\x0b\x32$.coinbase.chainstorage.SolanaAccount\"\xf1\x01\n\x0fSolanaMessageV2\x12\x37\n\x0c\x61\x63\x63ount_keys\x18\x01 \x03(\x0b\x32!.coinbase.chainstorage.AccountKey\x12H\n\x15\x61\x64\x64ress_table_lookups\x18\x02 \x03(\x0b\x32).coinbase.chainstorage.AddressTableLookup\x12@\n\x0cinstructions\x18\x03 \x03(\x0b\x32*.coinbase.chainstorage.SolanaInstructionV2\x12\x19\n\x11recent_block_hash\x18\x04 \x01(\t\"N\n\nAccountKey\x12\x0e\n\x06pubkey\x18\x01 \x01(\t\x12\x0e\n\x06signer\x18\x02 \x01(\x08\x12\x0e\n\x06source\x18\x03 \x01(\t\x12\x10\n\x08writable\x18\x04 \x01(\x08\"]\n\x12\x41\x64\x64ressTableLookup\x12\x13\n\x0b\x61\x63\x63ount_key\x18\x01 \x01(\t\x12\x18\n\x10readonly_indexes\x18\x02 \x03(\x04\x12\x18\n\x10writable_indexes\x18\x03 \x03(\x04\"\x84\x01\n\x13SolanaMessageHeader\x12\x1f\n\x17num_required_signatures\x18\x01 \x01(\x04\x12$\n\x1cnum_readonly_signed_accounts\x18\x02 \x01(\x04\x12&\n\x1enum_readonly_unsigned_accounts\x18\x03 \x01(\x04\"w\n\x11SolanaInstruction\x12\x18\n\x10program_id_index\x18\x01 \x01(\x04\x12\x10\n\x08\x61\x63\x63ounts\x18\x02 \x03(\x04\x12\x0c\n\x04\x64\x61ta\x18\x03 \x01(\x0c\x12\x12\n\nprogram_id\x18\x04 \x01(\t\x12\x14\n\x0c\x61\x63\x63ount_keys\x18\x05 \x03(\t\"6\n\x14SolanaRawInstruction\x12\x10\n\x08\x61\x63\x63ounts\x18\x01 \x03(\t\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\"\xec\x07\n\x13SolanaInstructionV2\x12\x35\n\x07program\x18\x01 \x01(\x0e\x32$.coinbase.chainstorage.SolanaProgram\x12\x12\n\nprogram_id\x18\x02 \x01(\t\x12\x46\n\x0fraw_instruction\x18\x64 \x01(\x0b\x32+.coinbase.chainstorage.SolanaRawInstructionH\x00\x12^\n\x1c\x61\x64\x64ress_lookup_table_program\x18\x65 \x01(\x0b\x32\x36.coinbase.chainstorage.SolanaAddressLookupTableProgramH\x00\x12K\n\x12\x62pf_loader_program\x18\x66 \x01(\x0b\x32-.coinbase.chainstorage.SolanaBpfLoaderProgramH\x00\x12\x62\n\x1e\x62pf_upgradeable_loader_program\x18g \x01(\x0b\x32\x38.coinbase.chainstorage.SolanaBpfUpgradeableLoaderProgramH\x00\x12@\n\x0cvote_program\x18h \x01(\x0b\x32(.coinbase.chainstorage.SolanaVoteProgramH\x00\x12\x44\n\x0esystem_program\x18i \x01(\x0b\x32*.coinbase.chainstorage.SolanaSystemProgramH\x00\x12\x42\n\rstake_program\x18j \x01(\x0b\x32).coinbase.chainstorage.SolanaStakeProgramH\x00\x12G\n\x10spl_memo_program\x18k \x01(\x0b\x32+.coinbase.chainstorage.SolanaSplMemoProgramH\x00\x12I\n\x11spl_token_program\x18l \x01(\x0b\x32,.coinbase.chainstorage.SolanaSplTokenProgramH\x00\x12R\n\x16spl_token_2022_program\x18m \x01(\x0b\x32\x30.coinbase.chainstorage.SolanaSplToken2022ProgramH\x00\x12m\n$spl_associated_token_account_program\x18n \x01(\x0b\x32=.coinbase.chainstorage.SolanaSplAssociatedTokenAccountProgramH\x00\x42\x0e\n\x0cprogram_data\"\xf6\x01\n\x1fSolanaAddressLookupTableProgram\x12`\n\x10instruction_type\x18\x01 \x01(\x0e\x32\x46.coinbase.chainstorage.SolanaAddressLookupTableProgram.InstructionType\x12\x42\n\x07unknown\x18\x64 \x01(\x0b\x32/.coinbase.chainstorage.SolanaUnknownInstructionH\x00\"\x1e\n\x0fInstructionType\x12\x0b\n\x07UNKNOWN\x10\x00\x42\r\n\x0binstruction\"\xe4\x01\n\x16SolanaBpfLoaderProgram\x12W\n\x10instruction_type\x18\x01 \x01(\x0e\x32=.coinbase.chainstorage.SolanaBpfLoaderProgram.InstructionType\x12\x42\n\x07unknown\x18\x64 \x01(\x0b\x32/.coinbase.chainstorage.SolanaUnknownInstructionH\x00\"\x1e\n\x0fInstructionType\x12\x0b\n\x07UNKNOWN\x10\x00\x42\r\n\x0binstruction\"\xfa\x01\n!SolanaBpfUpgradeableLoaderProgram\x12\x62\n\x10instruction_type\x18\x01 \x01(\x0e\x32H.coinbase.chainstorage.SolanaBpfUpgradeableLoaderProgram.InstructionType\x12\x42\n\x07unknown\x18\x64 \x01(\x0b\x32/.coinbase.chainstorage.SolanaUnknownInstructionH\x00\"\x1e\n\x0fInstructionType\x12\x0b\n\x07UNKNOWN\x10\x00\x42\r\n\x0binstruction\"\xe4\x04\n\x11SolanaVoteProgram\x12R\n\x10instruction_type\x18\x01 \x01(\x0e\x32\x38.coinbase.chainstorage.SolanaVoteProgram.InstructionType\x12\x42\n\x07unknown\x18\x64 \x01(\x0b\x32/.coinbase.chainstorage.SolanaUnknownInstructionH\x00\x12L\n\ninitialize\x18\x65 \x01(\x0b\x32\x36.coinbase.chainstorage.SolanaVoteInitializeInstructionH\x00\x12@\n\x04vote\x18\x66 \x01(\x0b\x32\x30.coinbase.chainstorage.SolanaVoteVoteInstructionH\x00\x12H\n\x08withdraw\x18g \x01(\x0b\x32\x34.coinbase.chainstorage.SolanaVoteWithdrawInstructionH\x00\x12g\n\x19\x63ompact_update_vote_state\x18h \x01(\x0b\x32\x42.coinbase.chainstorage.SolanaVoteCompactUpdateVoteStateInstructionH\x00\"e\n\x0fInstructionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0e\n\nINITIALIZE\x10\x01\x12\x08\n\x04VOTE\x10\x02\x12\x0c\n\x08WITHDRAW\x10\x03\x12\x1d\n\x19\x43OMPACT_UPDATE_VOTE_STATE\x10\x04\x42\r\n\x0binstruction\"\xa0\x05\n\x13SolanaSystemProgram\x12T\n\x10instruction_type\x18\x01 \x01(\x0e\x32:.coinbase.chainstorage.SolanaSystemProgram.InstructionType\x12\x42\n\x07unknown\x18\x64 \x01(\x0b\x32/.coinbase.chainstorage.SolanaUnknownInstructionH\x00\x12U\n\x0e\x63reate_account\x18\x65 \x01(\x0b\x32;.coinbase.chainstorage.SolanaSystemCreateAccountInstructionH\x00\x12J\n\x08transfer\x18\x66 \x01(\x0b\x32\x36.coinbase.chainstorage.SolanaSystemTransferInstructionH\x00\x12g\n\x18\x63reate_account_with_seed\x18g \x01(\x0b\x32\x43.coinbase.chainstorage.SolanaSystemCreateAccountWithSeedInstructionH\x00\x12\\\n\x12transfer_with_seed\x18h \x01(\x0b\x32>.coinbase.chainstorage.SolanaSystemTransferWithSeedInstructionH\x00\"v\n\x0fInstructionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x12\n\x0e\x43REATE_ACCOUNT\x10\x01\x12\x0c\n\x08TRANSFER\x10\x02\x12\x1c\n\x18\x43REATE_ACCOUNT_WITH_SEED\x10\x03\x12\x16\n\x12TRANSFER_WITH_SEED\x10\x04\x42\r\n\x0binstruction\"\xec\x05\n\x12SolanaStakeProgram\x12S\n\x10instruction_type\x18\x01 \x01(\x0e\x32\x39.coinbase.chainstorage.SolanaStakeProgram.InstructionType\x12\x42\n\x07unknown\x18\x64 \x01(\x0b\x32/.coinbase.chainstorage.SolanaUnknownInstructionH\x00\x12M\n\ninitialize\x18\x65 \x01(\x0b\x32\x37.coinbase.chainstorage.SolanaStakeInitializeInstructionH\x00\x12I\n\x08\x64\x65legate\x18\x66 \x01(\x0b\x32\x35.coinbase.chainstorage.SolanaStakeDelegateInstructionH\x00\x12M\n\ndeactivate\x18g \x01(\x0b\x32\x37.coinbase.chainstorage.SolanaStakeDeactivateInstructionH\x00\x12\x43\n\x05merge\x18h \x01(\x0b\x32\x32.coinbase.chainstorage.SolanaStakeMergeInstructionH\x00\x12\x43\n\x05split\x18i \x01(\x0b\x32\x32.coinbase.chainstorage.SolanaStakeSplitInstructionH\x00\x12I\n\x08withdraw\x18j \x01(\x0b\x32\x35.coinbase.chainstorage.SolanaStakeWithdrawInstructionH\x00\"p\n\x0fInstructionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0e\n\nINITIALIZE\x10\x01\x12\x0c\n\x08\x44\x45LEGATE\x10\x02\x12\x0e\n\nDEACTIVATE\x10\x03\x12\t\n\x05MERGE\x10\x04\x12\t\n\x05SPLIT\x10\x05\x12\x0c\n\x08WITHDRAW\x10\x06\x42\r\n\x0binstruction\"\xde\x01\n\x14SolanaSplMemoProgram\x12U\n\x10instruction_type\x18\x01 \x01(\x0e\x32;.coinbase.chainstorage.SolanaSplMemoProgram.InstructionType\x12?\n\x04memo\x18\x64 \x01(\x0b\x32/.coinbase.chainstorage.SolanaSplMemoInstructionH\x00\"\x1f\n\x0fInstructionType\x12\x0c\n\x08SPL_MEMO\x10\x00\x42\r\n\x0binstruction\"\xce\x04\n\x15SolanaSplTokenProgram\x12V\n\x10instruction_type\x18\x01 \x01(\x0e\x32<.coinbase.chainstorage.SolanaSplTokenProgram.InstructionType\x12\x42\n\x07unknown\x18\x64 \x01(\x0b\x32/.coinbase.chainstorage.SolanaUnknownInstructionH\x00\x12\x63\n\x15get_account_data_size\x18\x65 \x01(\x0b\x32\x42.coinbase.chainstorage.SolanaSplTokenGetAccountDataSizeInstructionH\x00\x12n\n\x1ainitialize_immutable_owner\x18\x66 \x01(\x0b\x32H.coinbase.chainstorage.SolanaSplTokenInitializeImmutableOwnerInstructionH\x00\x12L\n\x08transfer\x18g \x01(\x0b\x32\x38.coinbase.chainstorage.SolanaSplTokenTransferInstructionH\x00\"g\n\x0fInstructionType\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x19\n\x15GET_ACCOUNT_DATA_SIZE\x10\x01\x12\x1e\n\x1aINITIALIZE_IMMUTABLE_OWNER\x10\x02\x12\x0c\n\x08TRANSFER\x10\x03\x42\r\n\x0binstruction\"\xea\x01\n\x19SolanaSplToken2022Program\x12Z\n\x10instruction_type\x18\x01 \x01(\x0e\x32@.coinbase.chainstorage.SolanaSplToken2022Program.InstructionType\x12\x42\n\x07unknown\x18\x64 \x01(\x0b\x32/.coinbase.chainstorage.SolanaUnknownInstructionH\x00\"\x1e\n\x0fInstructionType\x12\x0b\n\x07UNKNOWN\x10\x00\x42\r\n\x0binstruction\"\x84\x02\n&SolanaSplAssociatedTokenAccountProgram\x12g\n\x10instruction_type\x18\x01 \x01(\x0e\x32M.coinbase.chainstorage.SolanaSplAssociatedTokenAccountProgram.InstructionType\x12\x42\n\x07unknown\x18\x64 \x01(\x0b\x32/.coinbase.chainstorage.SolanaUnknownInstructionH\x00\"\x1e\n\x0fInstructionType\x12\x0b\n\x07UNKNOWN\x10\x00\x42\r\n\x0binstruction\"(\n\x18SolanaUnknownInstruction\x12\x0c\n\x04info\x18\x01 \x01(\x0c\"\xbd\x01\n\x1fSolanaVoteInitializeInstruction\x12\x14\n\x0cvote_account\x18\x01 \x01(\t\x12\x13\n\x0brent_sysvar\x18\x02 \x01(\t\x12\x14\n\x0c\x63lock_sysvar\x18\x03 \x01(\t\x12\x0c\n\x04node\x18\x04 \x01(\t\x12\x18\n\x10\x61uthorized_voter\x18\x05 \x01(\t\x12\x1d\n\x15\x61uthorized_withdrawer\x18\x06 \x01(\t\x12\x12\n\ncommission\x18\x07 \x01(\r\"\x94\x02\n\x19SolanaVoteVoteInstruction\x12\x14\n\x0cvote_account\x18\x01 \x01(\t\x12\x1a\n\x12slot_hashes_sysvar\x18\x02 \x01(\t\x12\x14\n\x0c\x63lock_sysvar\x18\x03 \x01(\t\x12\x16\n\x0evote_authority\x18\x04 \x01(\t\x12\x43\n\x04vote\x18\x05 \x01(\x0b\x32\x35.coinbase.chainstorage.SolanaVoteVoteInstruction.Vote\x1aR\n\x04Vote\x12\r\n\x05slots\x18\x01 \x03(\x04\x12\x0c\n\x04hash\x18\x02 \x01(\t\x12-\n\ttimestamp\x18\x03 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"x\n\x1dSolanaVoteWithdrawInstruction\x12\x14\n\x0cvote_account\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x1a\n\x12withdraw_authority\x18\x03 \x01(\t\x12\x10\n\x08lamports\x18\x04 \x01(\x04\"\xbc\x03\n+SolanaVoteCompactUpdateVoteStateInstruction\x12\x14\n\x0cvote_account\x18\x01 \x01(\t\x12\x16\n\x0evote_authority\x18\x02 \x01(\t\x12m\n\x11vote_state_update\x18\x03 \x01(\x0b\x32R.coinbase.chainstorage.SolanaVoteCompactUpdateVoteStateInstruction.VoteStateUpdate\x1a\x33\n\x07Lockout\x12\x1a\n\x12\x63onfirmation_count\x18\x01 \x01(\x04\x12\x0c\n\x04slot\x18\x02 \x01(\x04\x1a\xba\x01\n\x0fVoteStateUpdate\x12\x0c\n\x04hash\x18\x01 \x01(\t\x12\\\n\x08lockouts\x18\x02 \x03(\x0b\x32J.coinbase.chainstorage.SolanaVoteCompactUpdateVoteStateInstruction.Lockout\x12\x0c\n\x04root\x18\x03 \x01(\x04\x12-\n\ttimestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\"{\n$SolanaSystemCreateAccountInstruction\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\x13\n\x0bnew_account\x18\x02 \x01(\t\x12\x10\n\x08lamports\x18\x03 \x01(\x04\x12\r\n\x05space\x18\x04 \x01(\x04\x12\r\n\x05owner\x18\x05 \x01(\t\"X\n\x1fSolanaSystemTransferInstruction\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x10\n\x08lamports\x18\x03 \x01(\x04\"\x9f\x01\n,SolanaSystemCreateAccountWithSeedInstruction\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\x13\n\x0bnew_account\x18\x02 \x01(\t\x12\x0c\n\x04\x62\x61se\x18\x03 \x01(\t\x12\x0c\n\x04seed\x18\x04 \x01(\t\x12\x10\n\x08lamports\x18\x05 \x01(\x04\x12\r\n\x05space\x18\x06 \x01(\x04\x12\r\n\x05owner\x18\x07 \x01(\t\"\xa0\x01\n\'SolanaSystemTransferWithSeedInstruction\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\x13\n\x0bsource_base\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65stination\x18\x03 \x01(\t\x12\x10\n\x08lamports\x18\x04 \x01(\x04\x12\x13\n\x0bsource_seed\x18\x05 \x01(\t\x12\x14\n\x0csource_owner\x18\x06 \x01(\t\"\xec\x02\n SolanaStakeInitializeInstruction\x12\x15\n\rstake_account\x18\x01 \x01(\t\x12\x13\n\x0brent_sysvar\x18\x02 \x01(\t\x12V\n\nauthorized\x18\x03 \x01(\x0b\x32\x42.coinbase.chainstorage.SolanaStakeInitializeInstruction.Authorized\x12N\n\x06lockup\x18\x04 \x01(\x0b\x32>.coinbase.chainstorage.SolanaStakeInitializeInstruction.Lockup\x1a\x30\n\nAuthorized\x12\x0e\n\x06staker\x18\x01 \x01(\t\x12\x12\n\nwithdrawer\x18\x02 \x01(\t\x1a\x42\n\x06Lockup\x12\x16\n\x0eunix_timestamp\x18\x01 \x01(\x03\x12\r\n\x05\x65poch\x18\x02 \x01(\x04\x12\x11\n\tcustodian\x18\x03 \x01(\t\"\xb8\x01\n\x1eSolanaStakeDelegateInstruction\x12\x15\n\rstake_account\x18\x01 \x01(\t\x12\x14\n\x0cvote_account\x18\x02 \x01(\t\x12\x14\n\x0c\x63lock_sysvar\x18\x03 \x01(\t\x12\x1c\n\x14stake_history_sysvar\x18\x04 \x01(\t\x12\x1c\n\x14stake_config_account\x18\x05 \x01(\t\x12\x17\n\x0fstake_authority\x18\x06 \x01(\t\"h\n SolanaStakeDeactivateInstruction\x12\x15\n\rstake_account\x18\x01 \x01(\t\x12\x14\n\x0c\x63lock_sysvar\x18\x02 \x01(\t\x12\x17\n\x0fstake_authority\x18\x03 \x01(\t\"\x8f\x01\n\x1bSolanaStakeMergeInstruction\x12\x13\n\x0b\x64\x65stination\x18\x01 \x01(\t\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x14\n\x0c\x63lock_sysvar\x18\x03 \x01(\t\x12\x1c\n\x14stake_history_sysvar\x18\x04 \x01(\t\x12\x17\n\x0fstake_authority\x18\x05 \x01(\t\"z\n\x1bSolanaStakeSplitInstruction\x12\x15\n\rstake_account\x18\x01 \x01(\t\x12\x19\n\x11new_split_account\x18\x02 \x01(\t\x12\x17\n\x0fstake_authority\x18\x03 \x01(\t\x12\x10\n\x08lamports\x18\x04 \x01(\x04\"\xae\x01\n\x1eSolanaStakeWithdrawInstruction\x12\x15\n\rstake_account\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x14\n\x0c\x63lock_sysvar\x18\x03 \x01(\t\x12\x1c\n\x14stake_history_sysvar\x18\x04 \x01(\t\x12\x1a\n\x12withdraw_authority\x18\x05 \x01(\t\x12\x10\n\x08lamports\x18\x06 \x01(\x04\"(\n\x18SolanaSplMemoInstruction\x12\x0c\n\x04memo\x18\x01 \x01(\t\"T\n+SolanaSplTokenGetAccountDataSizeInstruction\x12\x0c\n\x04mint\x18\x01 \x01(\t\x12\x17\n\x0f\x65xtension_types\x18\x02 \x03(\t\"D\n1SolanaSplTokenInitializeImmutableOwnerInstruction\x12\x0f\n\x07\x61\x63\x63ount\x18\x01 \x01(\t\"k\n!SolanaSplTokenTransferInstruction\x12\x0e\n\x06source\x18\x01 \x01(\t\x12\x13\n\x0b\x64\x65stination\x18\x02 \x01(\t\x12\x11\n\tauthority\x18\x03 \x01(\t\x12\x0e\n\x06\x61mount\x18\x04 \x01(\t\"E\n\rSolanaAccount\x12\x12\n\npublic_key\x18\x01 \x01(\t\x12\x0e\n\x06signer\x18\x02 \x01(\x08\x12\x10\n\x08writable\x18\x03 \x01(\x08*\xe0\x01\n\rSolanaProgram\x12\x07\n\x03RAW\x10\x00\x12\x18\n\x14\x41\x44\x44RESS_LOOKUP_TABLE\x10\x01\x12\x0e\n\nBPF_Loader\x10\x02\x12\x1a\n\x16\x42PF_UPGRADEABLE_Loader\x10\x03\x12\x08\n\x04VOTE\x10\x04\x12\n\n\x06SYSTEM\x10\x05\x12\t\n\x05STAKE\x10\x06\x12\x0c\n\x08SPL_MEMO\x10\x07\x12\r\n\tSPL_TOKEN\x10\x08\x12\x12\n\x0eSPL_TOKEN_2022\x10\t\x12 \n\x1cSPL_ASSOCIATED_TOKEN_ACCOUNT\x10\n\x12\x0c\n\x08UNPARSED\x10\x0b\x42?Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorageb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.chainstorage.blockchain_solana_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'Z=github.com/coinbase/chainstorage/protos/coinbase/chainstorage'
+ _globals['_SOLANAPROGRAM']._serialized_start=11804
+ _globals['_SOLANAPROGRAM']._serialized_end=12028
+ _globals['_SOLANABLOBDATA']._serialized_start=105
+ _globals['_SOLANABLOBDATA']._serialized_end=137
+ _globals['_SOLANABLOCK']._serialized_start=140
+ _globals['_SOLANABLOCK']._serialized_end=324
+ _globals['_SOLANABLOCKV2']._serialized_start=327
+ _globals['_SOLANABLOCKV2']._serialized_end=515
+ _globals['_SOLANAHEADER']._serialized_start=518
+ _globals['_SOLANAHEADER']._serialized_end=686
+ _globals['_SOLANATRANSACTION']._serialized_start=689
+ _globals['_SOLANATRANSACTION']._serialized_end=875
+ _globals['_SOLANATRANSACTIONV2']._serialized_start=878
+ _globals['_SOLANATRANSACTIONV2']._serialized_end=1070
+ _globals['_SOLANATRANSACTIONMETA']._serialized_start=1073
+ _globals['_SOLANATRANSACTIONMETA']._serialized_end=1461
+ _globals['_SOLANATRANSACTIONMETAV2']._serialized_start=1464
+ _globals['_SOLANATRANSACTIONMETAV2']._serialized_end=1856
+ _globals['_SOLANATOKENBALANCE']._serialized_start=1859
+ _globals['_SOLANATOKENBALANCE']._serialized_end=1995
+ _globals['_SOLANATOKENAMOUNT']._serialized_start=1997
+ _globals['_SOLANATOKENAMOUNT']._serialized_end=2076
+ _globals['_SOLANAINNERINSTRUCTION']._serialized_start=2078
+ _globals['_SOLANAINNERINSTRUCTION']._serialized_end=2181
+ _globals['_SOLANAINNERINSTRUCTIONV2']._serialized_start=2183
+ _globals['_SOLANAINNERINSTRUCTIONV2']._serialized_end=2290
+ _globals['_SOLANAREWARD']._serialized_start=2293
+ _globals['_SOLANAREWARD']._serialized_end=2429
+ _globals['_SOLANATRANSACTIONPAYLOAD']._serialized_start=2431
+ _globals['_SOLANATRANSACTIONPAYLOAD']._serialized_end=2532
+ _globals['_SOLANATRANSACTIONPAYLOADV2']._serialized_start=2534
+ _globals['_SOLANATRANSACTIONPAYLOADV2']._serialized_end=2639
+ _globals['_SOLANAMESSAGE']._serialized_start=2642
+ _globals['_SOLANAMESSAGE']._serialized_end=2864
+ _globals['_SOLANAMESSAGEV2']._serialized_start=2867
+ _globals['_SOLANAMESSAGEV2']._serialized_end=3108
+ _globals['_ACCOUNTKEY']._serialized_start=3110
+ _globals['_ACCOUNTKEY']._serialized_end=3188
+ _globals['_ADDRESSTABLELOOKUP']._serialized_start=3190
+ _globals['_ADDRESSTABLELOOKUP']._serialized_end=3283
+ _globals['_SOLANAMESSAGEHEADER']._serialized_start=3286
+ _globals['_SOLANAMESSAGEHEADER']._serialized_end=3418
+ _globals['_SOLANAINSTRUCTION']._serialized_start=3420
+ _globals['_SOLANAINSTRUCTION']._serialized_end=3539
+ _globals['_SOLANARAWINSTRUCTION']._serialized_start=3541
+ _globals['_SOLANARAWINSTRUCTION']._serialized_end=3595
+ _globals['_SOLANAINSTRUCTIONV2']._serialized_start=3598
+ _globals['_SOLANAINSTRUCTIONV2']._serialized_end=4602
+ _globals['_SOLANAADDRESSLOOKUPTABLEPROGRAM']._serialized_start=4605
+ _globals['_SOLANAADDRESSLOOKUPTABLEPROGRAM']._serialized_end=4851
+ _globals['_SOLANAADDRESSLOOKUPTABLEPROGRAM_INSTRUCTIONTYPE']._serialized_start=4806
+ _globals['_SOLANAADDRESSLOOKUPTABLEPROGRAM_INSTRUCTIONTYPE']._serialized_end=4836
+ _globals['_SOLANABPFLOADERPROGRAM']._serialized_start=4854
+ _globals['_SOLANABPFLOADERPROGRAM']._serialized_end=5082
+ _globals['_SOLANABPFLOADERPROGRAM_INSTRUCTIONTYPE']._serialized_start=4806
+ _globals['_SOLANABPFLOADERPROGRAM_INSTRUCTIONTYPE']._serialized_end=4836
+ _globals['_SOLANABPFUPGRADEABLELOADERPROGRAM']._serialized_start=5085
+ _globals['_SOLANABPFUPGRADEABLELOADERPROGRAM']._serialized_end=5335
+ _globals['_SOLANABPFUPGRADEABLELOADERPROGRAM_INSTRUCTIONTYPE']._serialized_start=4806
+ _globals['_SOLANABPFUPGRADEABLELOADERPROGRAM_INSTRUCTIONTYPE']._serialized_end=4836
+ _globals['_SOLANAVOTEPROGRAM']._serialized_start=5338
+ _globals['_SOLANAVOTEPROGRAM']._serialized_end=5950
+ _globals['_SOLANAVOTEPROGRAM_INSTRUCTIONTYPE']._serialized_start=5834
+ _globals['_SOLANAVOTEPROGRAM_INSTRUCTIONTYPE']._serialized_end=5935
+ _globals['_SOLANASYSTEMPROGRAM']._serialized_start=5953
+ _globals['_SOLANASYSTEMPROGRAM']._serialized_end=6625
+ _globals['_SOLANASYSTEMPROGRAM_INSTRUCTIONTYPE']._serialized_start=6492
+ _globals['_SOLANASYSTEMPROGRAM_INSTRUCTIONTYPE']._serialized_end=6610
+ _globals['_SOLANASTAKEPROGRAM']._serialized_start=6628
+ _globals['_SOLANASTAKEPROGRAM']._serialized_end=7376
+ _globals['_SOLANASTAKEPROGRAM_INSTRUCTIONTYPE']._serialized_start=7249
+ _globals['_SOLANASTAKEPROGRAM_INSTRUCTIONTYPE']._serialized_end=7361
+ _globals['_SOLANASPLMEMOPROGRAM']._serialized_start=7379
+ _globals['_SOLANASPLMEMOPROGRAM']._serialized_end=7601
+ _globals['_SOLANASPLMEMOPROGRAM_INSTRUCTIONTYPE']._serialized_start=7555
+ _globals['_SOLANASPLMEMOPROGRAM_INSTRUCTIONTYPE']._serialized_end=7586
+ _globals['_SOLANASPLTOKENPROGRAM']._serialized_start=7604
+ _globals['_SOLANASPLTOKENPROGRAM']._serialized_end=8194
+ _globals['_SOLANASPLTOKENPROGRAM_INSTRUCTIONTYPE']._serialized_start=8076
+ _globals['_SOLANASPLTOKENPROGRAM_INSTRUCTIONTYPE']._serialized_end=8179
+ _globals['_SOLANASPLTOKEN2022PROGRAM']._serialized_start=8197
+ _globals['_SOLANASPLTOKEN2022PROGRAM']._serialized_end=8431
+ _globals['_SOLANASPLTOKEN2022PROGRAM_INSTRUCTIONTYPE']._serialized_start=4806
+ _globals['_SOLANASPLTOKEN2022PROGRAM_INSTRUCTIONTYPE']._serialized_end=4836
+ _globals['_SOLANASPLASSOCIATEDTOKENACCOUNTPROGRAM']._serialized_start=8434
+ _globals['_SOLANASPLASSOCIATEDTOKENACCOUNTPROGRAM']._serialized_end=8694
+ _globals['_SOLANASPLASSOCIATEDTOKENACCOUNTPROGRAM_INSTRUCTIONTYPE']._serialized_start=4806
+ _globals['_SOLANASPLASSOCIATEDTOKENACCOUNTPROGRAM_INSTRUCTIONTYPE']._serialized_end=4836
+ _globals['_SOLANAUNKNOWNINSTRUCTION']._serialized_start=8696
+ _globals['_SOLANAUNKNOWNINSTRUCTION']._serialized_end=8736
+ _globals['_SOLANAVOTEINITIALIZEINSTRUCTION']._serialized_start=8739
+ _globals['_SOLANAVOTEINITIALIZEINSTRUCTION']._serialized_end=8928
+ _globals['_SOLANAVOTEVOTEINSTRUCTION']._serialized_start=8931
+ _globals['_SOLANAVOTEVOTEINSTRUCTION']._serialized_end=9207
+ _globals['_SOLANAVOTEVOTEINSTRUCTION_VOTE']._serialized_start=9125
+ _globals['_SOLANAVOTEVOTEINSTRUCTION_VOTE']._serialized_end=9207
+ _globals['_SOLANAVOTEWITHDRAWINSTRUCTION']._serialized_start=9209
+ _globals['_SOLANAVOTEWITHDRAWINSTRUCTION']._serialized_end=9329
+ _globals['_SOLANAVOTECOMPACTUPDATEVOTESTATEINSTRUCTION']._serialized_start=9332
+ _globals['_SOLANAVOTECOMPACTUPDATEVOTESTATEINSTRUCTION']._serialized_end=9776
+ _globals['_SOLANAVOTECOMPACTUPDATEVOTESTATEINSTRUCTION_LOCKOUT']._serialized_start=9536
+ _globals['_SOLANAVOTECOMPACTUPDATEVOTESTATEINSTRUCTION_LOCKOUT']._serialized_end=9587
+ _globals['_SOLANAVOTECOMPACTUPDATEVOTESTATEINSTRUCTION_VOTESTATEUPDATE']._serialized_start=9590
+ _globals['_SOLANAVOTECOMPACTUPDATEVOTESTATEINSTRUCTION_VOTESTATEUPDATE']._serialized_end=9776
+ _globals['_SOLANASYSTEMCREATEACCOUNTINSTRUCTION']._serialized_start=9778
+ _globals['_SOLANASYSTEMCREATEACCOUNTINSTRUCTION']._serialized_end=9901
+ _globals['_SOLANASYSTEMTRANSFERINSTRUCTION']._serialized_start=9903
+ _globals['_SOLANASYSTEMTRANSFERINSTRUCTION']._serialized_end=9991
+ _globals['_SOLANASYSTEMCREATEACCOUNTWITHSEEDINSTRUCTION']._serialized_start=9994
+ _globals['_SOLANASYSTEMCREATEACCOUNTWITHSEEDINSTRUCTION']._serialized_end=10153
+ _globals['_SOLANASYSTEMTRANSFERWITHSEEDINSTRUCTION']._serialized_start=10156
+ _globals['_SOLANASYSTEMTRANSFERWITHSEEDINSTRUCTION']._serialized_end=10316
+ _globals['_SOLANASTAKEINITIALIZEINSTRUCTION']._serialized_start=10319
+ _globals['_SOLANASTAKEINITIALIZEINSTRUCTION']._serialized_end=10683
+ _globals['_SOLANASTAKEINITIALIZEINSTRUCTION_AUTHORIZED']._serialized_start=10567
+ _globals['_SOLANASTAKEINITIALIZEINSTRUCTION_AUTHORIZED']._serialized_end=10615
+ _globals['_SOLANASTAKEINITIALIZEINSTRUCTION_LOCKUP']._serialized_start=10617
+ _globals['_SOLANASTAKEINITIALIZEINSTRUCTION_LOCKUP']._serialized_end=10683
+ _globals['_SOLANASTAKEDELEGATEINSTRUCTION']._serialized_start=10686
+ _globals['_SOLANASTAKEDELEGATEINSTRUCTION']._serialized_end=10870
+ _globals['_SOLANASTAKEDEACTIVATEINSTRUCTION']._serialized_start=10872
+ _globals['_SOLANASTAKEDEACTIVATEINSTRUCTION']._serialized_end=10976
+ _globals['_SOLANASTAKEMERGEINSTRUCTION']._serialized_start=10979
+ _globals['_SOLANASTAKEMERGEINSTRUCTION']._serialized_end=11122
+ _globals['_SOLANASTAKESPLITINSTRUCTION']._serialized_start=11124
+ _globals['_SOLANASTAKESPLITINSTRUCTION']._serialized_end=11246
+ _globals['_SOLANASTAKEWITHDRAWINSTRUCTION']._serialized_start=11249
+ _globals['_SOLANASTAKEWITHDRAWINSTRUCTION']._serialized_end=11423
+ _globals['_SOLANASPLMEMOINSTRUCTION']._serialized_start=11425
+ _globals['_SOLANASPLMEMOINSTRUCTION']._serialized_end=11465
+ _globals['_SOLANASPLTOKENGETACCOUNTDATASIZEINSTRUCTION']._serialized_start=11467
+ _globals['_SOLANASPLTOKENGETACCOUNTDATASIZEINSTRUCTION']._serialized_end=11551
+ _globals['_SOLANASPLTOKENINITIALIZEIMMUTABLEOWNERINSTRUCTION']._serialized_start=11553
+ _globals['_SOLANASPLTOKENINITIALIZEIMMUTABLEOWNERINSTRUCTION']._serialized_end=11621
+ _globals['_SOLANASPLTOKENTRANSFERINSTRUCTION']._serialized_start=11623
+ _globals['_SOLANASPLTOKENTRANSFERINSTRUCTION']._serialized_end=11730
+ _globals['_SOLANAACCOUNT']._serialized_start=11732
+ _globals['_SOLANAACCOUNT']._serialized_end=11801
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/chainstorage/blockchain_solana_pb2_grpc.py b/gen/src/python/coinbase/chainstorage/blockchain_solana_pb2_grpc.py
new file mode 100644
index 00000000..443a7c31
--- /dev/null
+++ b/gen/src/python/coinbase/chainstorage/blockchain_solana_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/chainstorage/blockchain_solana_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/account_identifer_pb2.py b/gen/src/python/coinbase/crypto/rosetta/types/account_identifer_pb2.py
new file mode 100644
index 00000000..14b3a616
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/account_identifer_pb2.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/crypto/rosetta/types/account_identifer.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/crypto/rosetta/types/account_identifer.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n5coinbase/crypto/rosetta/types/account_identifer.proto\x12\x1d\x63oinbase.crypto.rosetta.types\x1a\x19google/protobuf/any.proto\"\x87\x02\n\x11\x41\x63\x63ountIdentifier\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12H\n\x0bsub_account\x18\x02 \x01(\x0b\x32\x33.coinbase.crypto.rosetta.types.SubAccountIdentifier\x12P\n\x08metadata\x18\x03 \x03(\x0b\x32>.coinbase.crypto.rosetta.types.AccountIdentifier.MetadataEntry\x1a\x45\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\"\xc3\x01\n\x14SubAccountIdentifier\x12\x0f\n\x07\x61\x64\x64ress\x18\x01 \x01(\t\x12S\n\x08metadata\x18\x02 \x03(\x0b\x32\x41.coinbase.crypto.rosetta.types.SubAccountIdentifier.MetadataEntry\x1a\x45\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x42GZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/typesb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.crypto.rosetta.types.account_identifer_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'ZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/types'
+ _globals['_ACCOUNTIDENTIFIER_METADATAENTRY']._loaded_options = None
+ _globals['_ACCOUNTIDENTIFIER_METADATAENTRY']._serialized_options = b'8\001'
+ _globals['_SUBACCOUNTIDENTIFIER_METADATAENTRY']._loaded_options = None
+ _globals['_SUBACCOUNTIDENTIFIER_METADATAENTRY']._serialized_options = b'8\001'
+ _globals['_ACCOUNTIDENTIFIER']._serialized_start=116
+ _globals['_ACCOUNTIDENTIFIER']._serialized_end=379
+ _globals['_ACCOUNTIDENTIFIER_METADATAENTRY']._serialized_start=310
+ _globals['_ACCOUNTIDENTIFIER_METADATAENTRY']._serialized_end=379
+ _globals['_SUBACCOUNTIDENTIFIER']._serialized_start=382
+ _globals['_SUBACCOUNTIDENTIFIER']._serialized_end=577
+ _globals['_SUBACCOUNTIDENTIFIER_METADATAENTRY']._serialized_start=310
+ _globals['_SUBACCOUNTIDENTIFIER_METADATAENTRY']._serialized_end=379
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/account_identifer_pb2_grpc.py b/gen/src/python/coinbase/crypto/rosetta/types/account_identifer_pb2_grpc.py
new file mode 100644
index 00000000..c9e38c21
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/account_identifer_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/crypto/rosetta/types/account_identifer_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/amount_pb2.py b/gen/src/python/coinbase/crypto/rosetta/types/amount_pb2.py
new file mode 100644
index 00000000..adfbb1e8
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/amount_pb2.py
@@ -0,0 +1,48 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/crypto/rosetta/types/amount.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/crypto/rosetta/types/amount.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n*coinbase/crypto/rosetta/types/amount.proto\x12\x1d\x63oinbase.crypto.rosetta.types\x1a\x19google/protobuf/any.proto\"\xe0\x01\n\x06\x41mount\x12\r\n\x05value\x18\x01 \x01(\t\x12\x39\n\x08\x63urrency\x18\x02 \x01(\x0b\x32\'.coinbase.crypto.rosetta.types.Currency\x12\x45\n\x08metadata\x18\x03 \x03(\x0b\x32\x33.coinbase.crypto.rosetta.types.Amount.MetadataEntry\x1a\x45\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\"\xbc\x01\n\x08\x43urrency\x12\x0e\n\x06symbol\x18\x01 \x01(\t\x12\x10\n\x08\x64\x65\x63imals\x18\x02 \x01(\x05\x12G\n\x08metadata\x18\x03 \x03(\x0b\x32\x35.coinbase.crypto.rosetta.types.Currency.MetadataEntry\x1a\x45\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x42GZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/typesb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.crypto.rosetta.types.amount_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'ZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/types'
+ _globals['_AMOUNT_METADATAENTRY']._loaded_options = None
+ _globals['_AMOUNT_METADATAENTRY']._serialized_options = b'8\001'
+ _globals['_CURRENCY_METADATAENTRY']._loaded_options = None
+ _globals['_CURRENCY_METADATAENTRY']._serialized_options = b'8\001'
+ _globals['_AMOUNT']._serialized_start=105
+ _globals['_AMOUNT']._serialized_end=329
+ _globals['_AMOUNT_METADATAENTRY']._serialized_start=260
+ _globals['_AMOUNT_METADATAENTRY']._serialized_end=329
+ _globals['_CURRENCY']._serialized_start=332
+ _globals['_CURRENCY']._serialized_end=520
+ _globals['_CURRENCY_METADATAENTRY']._serialized_start=260
+ _globals['_CURRENCY_METADATAENTRY']._serialized_end=329
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/amount_pb2_grpc.py b/gen/src/python/coinbase/crypto/rosetta/types/amount_pb2_grpc.py
new file mode 100644
index 00000000..f1de77ac
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/amount_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/crypto/rosetta/types/amount_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/block_pb2.py b/gen/src/python/coinbase/crypto/rosetta/types/block_pb2.py
new file mode 100644
index 00000000..eeeed721
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/block_pb2.py
@@ -0,0 +1,46 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/crypto/rosetta/types/block.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/crypto/rosetta/types/block.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2
+from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2
+from coinbase.crypto.rosetta.types import transaction_pb2 as coinbase_dot_crypto_dot_rosetta_dot_types_dot_transaction__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n)coinbase/crypto/rosetta/types/block.proto\x12\x1d\x63oinbase.crypto.rosetta.types\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\x1a/coinbase/crypto/rosetta/types/transaction.proto\"\xba\x03\n\x05\x42lock\x12H\n\x10\x62lock_identifier\x18\x02 \x01(\x0b\x32..coinbase.crypto.rosetta.types.BlockIdentifier\x12O\n\x17parent_block_identifier\x18\x03 \x01(\x0b\x32..coinbase.crypto.rosetta.types.BlockIdentifier\x12-\n\ttimestamp\x18\x04 \x01(\x0b\x32\x1a.google.protobuf.Timestamp\x12@\n\x0ctransactions\x18\x05 \x03(\x0b\x32*.coinbase.crypto.rosetta.types.Transaction\x12\x44\n\x08metadata\x18\x06 \x03(\x0b\x32\x32.coinbase.crypto.rosetta.types.Block.MetadataEntry\x1a\x45\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01J\x04\x08\x01\x10\x02R\x12network_identifier\".\n\x0f\x42lockIdentifier\x12\r\n\x05index\x18\x01 \x01(\x03\x12\x0c\n\x04hash\x18\x02 \x01(\tBGZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/typesb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.crypto.rosetta.types.block_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'ZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/types'
+ _globals['_BLOCK_METADATAENTRY']._loaded_options = None
+ _globals['_BLOCK_METADATAENTRY']._serialized_options = b'8\001'
+ _globals['_BLOCK']._serialized_start=186
+ _globals['_BLOCK']._serialized_end=628
+ _globals['_BLOCK_METADATAENTRY']._serialized_start=533
+ _globals['_BLOCK_METADATAENTRY']._serialized_end=602
+ _globals['_BLOCKIDENTIFIER']._serialized_start=630
+ _globals['_BLOCKIDENTIFIER']._serialized_end=676
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/block_pb2_grpc.py b/gen/src/python/coinbase/crypto/rosetta/types/block_pb2_grpc.py
new file mode 100644
index 00000000..59b58d18
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/block_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/crypto/rosetta/types/block_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/coin_change_pb2.py b/gen/src/python/coinbase/crypto/rosetta/types/coin_change_pb2.py
new file mode 100644
index 00000000..6a595fe1
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/coin_change_pb2.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/crypto/rosetta/types/coin_change.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/crypto/rosetta/types/coin_change.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/coinbase/crypto/rosetta/types/coin_change.proto\x12\x1d\x63oinbase.crypto.rosetta.types\"\xec\x01\n\nCoinChange\x12\x46\n\x0f\x63oin_identifier\x18\x01 \x01(\x0b\x32-.coinbase.crypto.rosetta.types.CoinIdentifier\x12I\n\x0b\x63oin_action\x18\x02 \x01(\x0e\x32\x34.coinbase.crypto.rosetta.types.CoinChange.CoinAction\"K\n\nCoinAction\x12\x1b\n\x17\x43OIN_ACTION_UNSPECIFIED\x10\x00\x12\x10\n\x0c\x43OIN_CREATED\x10\x01\x12\x0e\n\nCOIN_SPENT\x10\x02\"$\n\x0e\x43oinIdentifier\x12\x12\n\nidentifier\x18\x01 \x01(\tBGZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/typesb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.crypto.rosetta.types.coin_change_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'ZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/types'
+ _globals['_COINCHANGE']._serialized_start=83
+ _globals['_COINCHANGE']._serialized_end=319
+ _globals['_COINCHANGE_COINACTION']._serialized_start=244
+ _globals['_COINCHANGE_COINACTION']._serialized_end=319
+ _globals['_COINIDENTIFIER']._serialized_start=321
+ _globals['_COINIDENTIFIER']._serialized_end=357
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/coin_change_pb2_grpc.py b/gen/src/python/coinbase/crypto/rosetta/types/coin_change_pb2_grpc.py
new file mode 100644
index 00000000..203a5ffa
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/coin_change_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/crypto/rosetta/types/coin_change_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/network_identifier_pb2.py b/gen/src/python/coinbase/crypto/rosetta/types/network_identifier_pb2.py
new file mode 100644
index 00000000..736cf3b6
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/network_identifier_pb2.py
@@ -0,0 +1,44 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/crypto/rosetta/types/network_identifier.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/crypto/rosetta/types/network_identifier.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n6coinbase/crypto/rosetta/types/network_identifier.proto\x12\x1d\x63oinbase.crypto.rosetta.types\x1a\x19google/protobuf/any.proto\"\x8d\x01\n\x11NetworkIdentifier\x12\x12\n\nblockchain\x18\x01 \x01(\t\x12\x0f\n\x07network\x18\x02 \x01(\t\x12S\n\x16sub_network_identifier\x18\x03 \x01(\x0b\x32\x33.coinbase.crypto.rosetta.types.SubNetworkIdentifier\"\xc3\x01\n\x14SubNetworkIdentifier\x12\x0f\n\x07network\x18\x01 \x01(\t\x12S\n\x08metadata\x18\x02 \x03(\x0b\x32\x41.coinbase.crypto.rosetta.types.SubNetworkIdentifier.MetadataEntry\x1a\x45\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\x42GZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/typesb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.crypto.rosetta.types.network_identifier_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'ZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/types'
+ _globals['_SUBNETWORKIDENTIFIER_METADATAENTRY']._loaded_options = None
+ _globals['_SUBNETWORKIDENTIFIER_METADATAENTRY']._serialized_options = b'8\001'
+ _globals['_NETWORKIDENTIFIER']._serialized_start=117
+ _globals['_NETWORKIDENTIFIER']._serialized_end=258
+ _globals['_SUBNETWORKIDENTIFIER']._serialized_start=261
+ _globals['_SUBNETWORKIDENTIFIER']._serialized_end=456
+ _globals['_SUBNETWORKIDENTIFIER_METADATAENTRY']._serialized_start=387
+ _globals['_SUBNETWORKIDENTIFIER_METADATAENTRY']._serialized_end=456
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/network_identifier_pb2_grpc.py b/gen/src/python/coinbase/crypto/rosetta/types/network_identifier_pb2_grpc.py
new file mode 100644
index 00000000..60bbd298
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/network_identifier_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/crypto/rosetta/types/network_identifier_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/operation_pb2.py b/gen/src/python/coinbase/crypto/rosetta/types/operation_pb2.py
new file mode 100644
index 00000000..2834ca44
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/operation_pb2.py
@@ -0,0 +1,47 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/crypto/rosetta/types/operation.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/crypto/rosetta/types/operation.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2
+from coinbase.crypto.rosetta.types import account_identifer_pb2 as coinbase_dot_crypto_dot_rosetta_dot_types_dot_account__identifer__pb2
+from coinbase.crypto.rosetta.types import amount_pb2 as coinbase_dot_crypto_dot_rosetta_dot_types_dot_amount__pb2
+from coinbase.crypto.rosetta.types import coin_change_pb2 as coinbase_dot_crypto_dot_rosetta_dot_types_dot_coin__change__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-coinbase/crypto/rosetta/types/operation.proto\x12\x1d\x63oinbase.crypto.rosetta.types\x1a\x19google/protobuf/any.proto\x1a\x35\x63oinbase/crypto/rosetta/types/account_identifer.proto\x1a*coinbase/crypto/rosetta/types/amount.proto\x1a/coinbase/crypto/rosetta/types/coin_change.proto\"\x96\x04\n\tOperation\x12P\n\x14operation_identifier\x18\x01 \x01(\x0b\x32\x32.coinbase.crypto.rosetta.types.OperationIdentifier\x12N\n\x12related_operations\x18\x02 \x03(\x0b\x32\x32.coinbase.crypto.rosetta.types.OperationIdentifier\x12\x0c\n\x04type\x18\x03 \x01(\t\x12\x0e\n\x06status\x18\x04 \x01(\t\x12\x41\n\x07\x61\x63\x63ount\x18\x05 \x01(\x0b\x32\x30.coinbase.crypto.rosetta.types.AccountIdentifier\x12\x35\n\x06\x61mount\x18\x06 \x01(\x0b\x32%.coinbase.crypto.rosetta.types.Amount\x12>\n\x0b\x63oin_change\x18\x07 \x01(\x0b\x32).coinbase.crypto.rosetta.types.CoinChange\x12H\n\x08metadata\x18\x08 \x03(\x0b\x32\x36.coinbase.crypto.rosetta.types.Operation.MetadataEntry\x1a\x45\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\";\n\x13OperationIdentifier\x12\r\n\x05index\x18\x01 \x01(\x03\x12\x15\n\rnetwork_index\x18\x02 \x01(\x03\x42GZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/typesb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.crypto.rosetta.types.operation_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'ZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/types'
+ _globals['_OPERATION_METADATAENTRY']._loaded_options = None
+ _globals['_OPERATION_METADATAENTRY']._serialized_options = b'8\001'
+ _globals['_OPERATION']._serialized_start=256
+ _globals['_OPERATION']._serialized_end=790
+ _globals['_OPERATION_METADATAENTRY']._serialized_start=721
+ _globals['_OPERATION_METADATAENTRY']._serialized_end=790
+ _globals['_OPERATIONIDENTIFIER']._serialized_start=792
+ _globals['_OPERATIONIDENTIFIER']._serialized_end=851
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/operation_pb2_grpc.py b/gen/src/python/coinbase/crypto/rosetta/types/operation_pb2_grpc.py
new file mode 100644
index 00000000..4318eee7
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/operation_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/crypto/rosetta/types/operation_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/transaction_pb2.py b/gen/src/python/coinbase/crypto/rosetta/types/transaction_pb2.py
new file mode 100644
index 00000000..1d3fe1ca
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/transaction_pb2.py
@@ -0,0 +1,50 @@
+# -*- coding: utf-8 -*-
+# Generated by the protocol buffer compiler. DO NOT EDIT!
+# NO CHECKED-IN PROTOBUF GENCODE
+# source: coinbase/crypto/rosetta/types/transaction.proto
+# Protobuf Python Version: 5.29.0
+"""Generated protocol buffer code."""
+from google.protobuf import descriptor as _descriptor
+from google.protobuf import descriptor_pool as _descriptor_pool
+from google.protobuf import runtime_version as _runtime_version
+from google.protobuf import symbol_database as _symbol_database
+from google.protobuf.internal import builder as _builder
+_runtime_version.ValidateProtobufRuntimeVersion(
+ _runtime_version.Domain.PUBLIC,
+ 5,
+ 29,
+ 0,
+ '',
+ 'coinbase/crypto/rosetta/types/transaction.proto'
+)
+# @@protoc_insertion_point(imports)
+
+_sym_db = _symbol_database.Default()
+
+
+from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2
+from coinbase.crypto.rosetta.types import operation_pb2 as coinbase_dot_crypto_dot_rosetta_dot_types_dot_operation__pb2
+from coinbase.crypto.rosetta.types import network_identifier_pb2 as coinbase_dot_crypto_dot_rosetta_dot_types_dot_network__identifier__pb2
+
+
+DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n/coinbase/crypto/rosetta/types/transaction.proto\x12\x1d\x63oinbase.crypto.rosetta.types\x1a\x19google/protobuf/any.proto\x1a-coinbase/crypto/rosetta/types/operation.proto\x1a\x36\x63oinbase/crypto/rosetta/types/network_identifier.proto\"\x85\x03\n\x0bTransaction\x12T\n\x16transaction_identifier\x18\x01 \x01(\x0b\x32\x34.coinbase.crypto.rosetta.types.TransactionIdentifier\x12<\n\noperations\x18\x02 \x03(\x0b\x32(.coinbase.crypto.rosetta.types.Operation\x12O\n\x14related_transactions\x18\x03 \x03(\x0b\x32\x31.coinbase.crypto.rosetta.types.RelatedTransaction\x12J\n\x08metadata\x18\x04 \x03(\x0b\x32\x38.coinbase.crypto.rosetta.types.Transaction.MetadataEntry\x1a\x45\n\rMetadataEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12#\n\x05value\x18\x02 \x01(\x0b\x32\x14.google.protobuf.Any:\x02\x38\x01\"%\n\x15TransactionIdentifier\x12\x0c\n\x04hash\x18\x01 \x01(\t\"\xcb\x02\n\x12RelatedTransaction\x12L\n\x12network_identifier\x18\x01 \x01(\x0b\x32\x30.coinbase.crypto.rosetta.types.NetworkIdentifier\x12T\n\x16transaction_identifier\x18\x02 \x01(\x0b\x32\x34.coinbase.crypto.rosetta.types.TransactionIdentifier\x12N\n\tdirection\x18\x03 \x01(\x0e\x32;.coinbase.crypto.rosetta.types.RelatedTransaction.Direction\"A\n\tDirection\x12\x19\n\x15\x44IRECTION_UNSPECIFIED\x10\x00\x12\x0b\n\x07\x46ORWARD\x10\x01\x12\x0c\n\x08\x42\x41\x43KWARD\x10\x02\x42GZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/typesb\x06proto3')
+
+_globals = globals()
+_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals)
+_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'coinbase.crypto.rosetta.types.transaction_pb2', _globals)
+if not _descriptor._USE_C_DESCRIPTORS:
+ _globals['DESCRIPTOR']._loaded_options = None
+ _globals['DESCRIPTOR']._serialized_options = b'ZEgithub.com/coinbase/chainstorage/protos/coinbase/crypto/rosetta/types'
+ _globals['_TRANSACTION_METADATAENTRY']._loaded_options = None
+ _globals['_TRANSACTION_METADATAENTRY']._serialized_options = b'8\001'
+ _globals['_TRANSACTION']._serialized_start=213
+ _globals['_TRANSACTION']._serialized_end=602
+ _globals['_TRANSACTION_METADATAENTRY']._serialized_start=533
+ _globals['_TRANSACTION_METADATAENTRY']._serialized_end=602
+ _globals['_TRANSACTIONIDENTIFIER']._serialized_start=604
+ _globals['_TRANSACTIONIDENTIFIER']._serialized_end=641
+ _globals['_RELATEDTRANSACTION']._serialized_start=644
+ _globals['_RELATEDTRANSACTION']._serialized_end=975
+ _globals['_RELATEDTRANSACTION_DIRECTION']._serialized_start=910
+ _globals['_RELATEDTRANSACTION_DIRECTION']._serialized_end=975
+# @@protoc_insertion_point(module_scope)
diff --git a/gen/src/python/coinbase/crypto/rosetta/types/transaction_pb2_grpc.py b/gen/src/python/coinbase/crypto/rosetta/types/transaction_pb2_grpc.py
new file mode 100644
index 00000000..affdefa0
--- /dev/null
+++ b/gen/src/python/coinbase/crypto/rosetta/types/transaction_pb2_grpc.py
@@ -0,0 +1,24 @@
+# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
+"""Client and server classes corresponding to protobuf-defined services."""
+import grpc
+import warnings
+
+
+GRPC_GENERATED_VERSION = '1.71.0'
+GRPC_VERSION = grpc.__version__
+_version_not_supported = False
+
+try:
+ from grpc._utilities import first_version_is_lower
+ _version_not_supported = first_version_is_lower(GRPC_VERSION, GRPC_GENERATED_VERSION)
+except ImportError:
+ _version_not_supported = True
+
+if _version_not_supported:
+ raise RuntimeError(
+ f'The grpc package installed is at version {GRPC_VERSION},'
+ + f' but the generated code in coinbase/crypto/rosetta/types/transaction_pb2_grpc.py depends on'
+ + f' grpcio>={GRPC_GENERATED_VERSION}.'
+ + f' Please upgrade your grpc module to grpcio>={GRPC_GENERATED_VERSION}'
+ + f' or downgrade your generated code using grpcio-tools<={GRPC_VERSION}.'
+ )
diff --git a/go.mod b/go.mod
index 3b728fc2..638f614f 100644
--- a/go.mod
+++ b/go.mod
@@ -1,28 +1,37 @@
module github.com/coinbase/chainstorage
-go 1.22.0
+go 1.24.0
+
+toolchain go1.24.11
require (
cloud.google.com/go/firestore v1.14.0
cloud.google.com/go/storage v1.37.0
github.com/VividCortex/ewma v1.2.0
- github.com/aws/aws-sdk-go v1.50.4
+ github.com/aws/aws-sdk-go-v2 v1.36.3
+ github.com/aws/aws-sdk-go-v2/config v1.27.0
+ github.com/aws/aws-sdk-go-v2/credentials v1.17.0
+ github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.13.0
+ github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.0
+ github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.0
+ github.com/aws/aws-sdk-go-v2/service/dynamodb v1.30.0
+ github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2
+ github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.0
+ github.com/aws/aws-sdk-go-v2/service/sqs v1.30.0
github.com/btcsuite/btcd/btcutil v1.1.5
github.com/cenkalti/backoff v2.2.1+incompatible
github.com/cenkalti/backoff/v4 v4.2.1
- github.com/coinbase/rosetta-sdk-go v0.8.3
+ github.com/coinbase/rosetta-sdk-go v0.8.9
github.com/coinbase/rosetta-sdk-go/types v1.0.0
- github.com/ethereum/go-ethereum v1.13.11
+ github.com/ethereum/go-ethereum v1.16.8
github.com/fatih/color v1.16.0
github.com/gagliardetto/solana-go v1.8.4
github.com/go-playground/validator/v10 v10.17.0
github.com/gogo/status v1.1.1
github.com/golang/mock v1.6.0
- github.com/golang/protobuf v1.5.4
- github.com/google/go-cmp v0.6.0
+ github.com/google/go-cmp v0.7.0
github.com/grpc-ecosystem/go-grpc-middleware v1.4.0
github.com/mitchellh/mapstructure v1.5.0
- github.com/opentracing-contrib/go-aws-sdk v0.0.0-20200219142134-2e00fb2121c5
github.com/opentracing/opentracing-go v1.2.0
github.com/pkg/errors v0.9.1
github.com/prysmaticlabs/prysm/v4 v4.1.0
@@ -31,36 +40,76 @@ require (
github.com/smira/go-statsd v1.3.3
github.com/spf13/cobra v1.8.1
github.com/spf13/viper v1.18.2
- github.com/stretchr/testify v1.9.0
+ github.com/stretchr/testify v1.11.1
github.com/uber-go/tally/v4 v4.1.10
github.com/valyala/fasttemplate v1.2.2
- go.temporal.io/api v1.27.0
- go.temporal.io/sdk v1.26.0-rc.2.0.20240214221834-30da688037d1
+ go.temporal.io/api v1.49.1
+ go.temporal.io/sdk v1.35.0
go.temporal.io/sdk/contrib/tally v0.2.0
go.uber.org/atomic v1.11.0
go.uber.org/fx v1.20.1
go.uber.org/mock v0.4.0
go.uber.org/zap v1.26.0
- golang.org/x/crypto v0.32.0
+ golang.org/x/crypto v0.46.0
golang.org/x/exp v0.0.0-20240119083558-1b970713d09a
- golang.org/x/net v0.34.0
- golang.org/x/sync v0.10.0
- golang.org/x/text v0.21.0
- golang.org/x/time v0.5.0
+ golang.org/x/net v0.48.0
+ golang.org/x/sync v0.19.0
+ golang.org/x/text v0.32.0
+ golang.org/x/time v0.9.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028
google.golang.org/api v0.158.0
- google.golang.org/grpc v1.61.0
- google.golang.org/protobuf v1.34.2
+ google.golang.org/grpc v1.79.3
+ google.golang.org/protobuf v1.36.10
gopkg.in/DataDog/dd-trace-go.v1 v1.59.1
gopkg.in/yaml.v2 v2.4.0
logur.dev/adapter/zap v0.5.0
logur.dev/logur v0.17.0
)
+require github.com/nexus-rpc/sdk-go v0.3.0 // indirect
+
+require github.com/lib/pq v1.10.9
+
+require (
+ github.com/SeismicSystems/aes v0.0.0-20251119232201-ff6734fc5e0e
+ github.com/aws/smithy-go v1.22.2
+ github.com/golang/protobuf v1.5.4
+ github.com/pressly/goose/v3 v3.14.0
+)
+
+require (
+ github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 // indirect
+ github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 // indirect
+ github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect
+ github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 // indirect
+ github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.19.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/eventbridge v1.20.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.1 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 // indirect
+ github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 // indirect
+ github.com/aws/aws-sdk-go-v2/service/kinesis v1.18.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sfn v1.19.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sns v1.21.4 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 // indirect
+ github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 // indirect
+ github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect
+ github.com/emicklei/dot v1.6.2 // indirect
+ github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect
+ github.com/ferranbt/fastssz v0.1.4 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.5 // indirect
+ github.com/minio/sha256-simd v1.0.0 // indirect
+ go.opentelemetry.io/auto/sdk v1.2.1 // indirect
+)
+
require (
cloud.google.com/go v0.112.0 // indirect
- cloud.google.com/go/compute v1.23.3 // indirect
- cloud.google.com/go/compute/metadata v0.2.3 // indirect
+ cloud.google.com/go/compute/metadata v0.9.0 // indirect
cloud.google.com/go/iam v1.1.5 // indirect
cloud.google.com/go/longrunning v0.5.4 // indirect
contrib.go.opencensus.io/exporter/stackdriver v0.13.4 // indirect
@@ -75,22 +124,19 @@ require (
github.com/DataDog/sketches-go v1.4.2 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect
- github.com/bits-and-blooms/bitset v1.17.0 // indirect
+ github.com/bits-and-blooms/bitset v1.20.0 // indirect
github.com/blendle/zapdriver v1.3.1 // indirect
- github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd // indirect
- github.com/btcsuite/btcd/btcec/v2 v2.3.2 // indirect
+ github.com/btcsuite/btcd v0.25.0 // indirect
+ github.com/btcsuite/btcd/btcec/v2 v2.3.5 // indirect
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
- github.com/consensys/bavard v0.1.22 // indirect
- github.com/consensys/gnark-crypto v0.14.0 // indirect
+ github.com/consensys/gnark-crypto v0.19.2 // indirect
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect
- github.com/crate-crypto/go-kzg-4844 v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.1.0 // indirect
github.com/dfuse-io/logging v0.0.0-20201110202154-26697de88c79 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ebitengine/purego v0.5.2 // indirect
- github.com/ethereum/c-kzg-4844 v1.0.0 // indirect
github.com/ethereum/go-verkle v0.2.2 // indirect
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
@@ -98,29 +144,29 @@ require (
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gagliardetto/binary v0.7.7 // indirect
github.com/gagliardetto/treeout v0.1.4 // indirect
- github.com/go-logr/logr v1.4.1 // indirect
+ github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
- github.com/gofrs/flock v0.8.1 // indirect
+ github.com/gofrs/flock v0.12.1 // indirect
github.com/gogo/googleapis v1.4.1 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
- github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb // indirect
+ github.com/golang/snappy v1.0.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
- github.com/google/uuid v1.5.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
- github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 // indirect
+ github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.1-vault-5 // indirect
- github.com/holiman/uint256 v1.3.2 // indirect
+ github.com/holiman/uint256 v1.3.2
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
- github.com/klauspost/compress v1.17.11 // indirect
+ github.com/klauspost/compress v1.17.11
github.com/leodido/go-urn v1.2.4 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/magiconair/properties v1.8.7 // indirect
@@ -129,23 +175,21 @@ require (
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
- github.com/mmcloughlin/addchain v0.4.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/mostynb/zstdpool-freelist v0.0.0-20201229113212-927304c0c3b1 // indirect
- github.com/mr-tron/base58 v1.2.0 // indirect
+ github.com/mr-tron/base58 v1.2.0
github.com/naoina/go-stringutil v0.1.0 // indirect
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect
github.com/outcaste-io/ristretto v0.2.3 // indirect
- github.com/pborman/uuid v1.2.1 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
github.com/philhofer/fwd v1.1.2 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
- github.com/prometheus/client_golang v1.14.0 // indirect
+ github.com/prometheus/client_golang v1.15.0
github.com/prometheus/client_model v0.3.0 // indirect
github.com/prometheus/common v0.42.0 // indirect
- github.com/prometheus/procfs v0.9.0 // indirect
+ github.com/prometheus/procfs v0.11.0 // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/robfig/cron v1.2.0 // indirect
github.com/sagikazarmark/locafero v0.4.0 // indirect
@@ -155,11 +199,11 @@ require (
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/spf13/afero v1.11.0 // indirect
github.com/spf13/cast v1.6.0 // indirect
- github.com/spf13/pflag v1.0.5 // indirect
+ github.com/spf13/pflag v1.0.6 // indirect
github.com/streamingfast/logging v0.0.0-20220405224725-2755dab2ce75 // indirect
github.com/stretchr/objx v0.5.2 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
- github.com/supranational/blst v0.3.13 // indirect
+ github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe // indirect
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 // indirect
github.com/thomaso-mirodin/intmath v0.0.0-20160323211736-5dc6d854e46e // indirect
github.com/tidwall/gjson v1.16.0 // indirect
@@ -175,26 +219,26 @@ require (
go.opencensus.io v0.24.0 // indirect
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 // indirect
- go.opentelemetry.io/otel v1.22.0 // indirect
- go.opentelemetry.io/otel/metric v1.22.0 // indirect
- go.opentelemetry.io/otel/trace v1.22.0 // indirect
+ go.opentelemetry.io/otel v1.39.0 // indirect
+ go.opentelemetry.io/otel/metric v1.39.0 // indirect
+ go.opentelemetry.io/otel/trace v1.39.0 // indirect
go.uber.org/dig v1.17.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go4.org/intern v0.0.0-20230525184215-6c62f75575cb // indirect
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
- golang.org/x/oauth2 v0.16.0 // indirect
- golang.org/x/sys v0.29.0 // indirect
- golang.org/x/term v0.28.0 // indirect
- google.golang.org/appengine v1.6.8 // indirect
+ golang.org/x/oauth2 v0.34.0 // indirect
+ golang.org/x/sys v0.39.0 // indirect
+ golang.org/x/term v0.38.0 // indirect
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe // indirect
- google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe // indirect
- google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe // 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
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
inet.af/netaddr v0.0.0-20230525184311-b8eac61e914a // indirect
- rsc.io/tmplfunc v0.0.3 // indirect
)
replace github.com/gogo/protobuf v1.3.3 => github.com/gogo/protobuf v1.3.2
-replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101500.0
+replace github.com/ethereum/go-ethereum => github.com/ethereum-optimism/op-geth v1.101609.0
+
+replace github.com/coinbase/rosetta-sdk-go => github.com/coinbase/mesh-sdk-go v0.8.9
diff --git a/go.sum b/go.sum
index 4246d1c3..a303c153 100644
--- a/go.sum
+++ b/go.sum
@@ -15,10 +15,8 @@ cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnP
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/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk=
-cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI=
-cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY=
-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/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/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
@@ -70,8 +68,10 @@ github.com/Microsoft/go-winio v0.5.0/go.mod h1:JPGBdM1cNvN/6ISo+n8V5iA4v8pBzdOpz
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
-github.com/VictoriaMetrics/fastcache v1.12.2 h1:N0y9ASrJ0F6h0QaC3o6uJb3NIZ9VKLjCM7NQbSmF7WI=
-github.com/VictoriaMetrics/fastcache v1.12.2/go.mod h1:AmC+Nzz1+3G2eCPapF6UcsnkThDcMsQicp4xDukwJYI=
+github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6 h1:1zYrtlhrZ6/b6SAjLSfKzWtdgqK0U+HtH/VcBWh1BaU=
+github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251001021608-1fe7b43fc4d6/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI=
+github.com/SeismicSystems/aes v0.0.0-20251119232201-ff6734fc5e0e h1:46c0e75cmuSvi7DEpshhvGIaFJbegu/mqhBHWwGnHxc=
+github.com/SeismicSystems/aes v0.0.0-20251119232201-ff6734fc5e0e/go.mod h1:pxLMRBExL594NpfDc/kt70zHYNqOI4N0fX7rLT+kTSs=
github.com/VividCortex/ewma v1.2.0 h1:f58SaIzcDXrSy3kWaHNvuJgJ3Nmz59Zji6XoJR/q1ow=
github.com/VividCortex/ewma v1.2.0/go.mod h1:nz4BbCtbLyFDeC9SUHbtcT5644juEuWfUAUnGx7j5l4=
github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII=
@@ -88,9 +88,74 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.22.1/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.23.20/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
-github.com/aws/aws-sdk-go v1.29.5/go.mod h1:1KvfttTE3SPKMpo8g2c6jL3ZKfXtFvKscTgahTma5Xg=
-github.com/aws/aws-sdk-go v1.50.4 h1:jJNhxunBgfjmCSjMZ3INwQ19ZN3RoGEZfgSCUYF/NZw=
-github.com/aws/aws-sdk-go v1.50.4/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk=
+github.com/aws/aws-sdk-go-v2 v1.20.3/go.mod h1:/RfNgGmRxI+iFOB1OeJUyxiU+9s88k3pfHvDagGEp0M=
+github.com/aws/aws-sdk-go-v2 v1.36.3 h1:mJoei2CxPutQVxaATCzDUjcZEjVRdpsiiXi2o38yqWM=
+github.com/aws/aws-sdk-go-v2 v1.36.3/go.mod h1:LLXuLpgzEbD766Z5ECcRmi8AzSwfZItDtmABVkRLGzg=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.13/go.mod h1:gpAbvyDGQFozTEmlTFO8XcQKHzubdq0LzRyJpG6MiXM=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10 h1:zAybnyUQXIZ5mok5Jqwlf58/TFE7uvd3IAsa1aF9cXs=
+github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.10/go.mod h1:qqvMj6gHLR/EXWZw4ZbqlPbQUyenf4h82UQUlKc+l14=
+github.com/aws/aws-sdk-go-v2/config v1.27.0 h1:J5sdGCAHuWKIXLeXiqr8II/adSvetkx0qdZwdbXXpb0=
+github.com/aws/aws-sdk-go-v2/config v1.27.0/go.mod h1:cfh8v69nuSUohNFMbIISP2fhmblGmYEOKs5V53HiHnk=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.0 h1:lMW2x6sKBsiAJrpi1doOXqWFyEPoE886DTb1X0wb7So=
+github.com/aws/aws-sdk-go-v2/credentials v1.17.0/go.mod h1:uT41FIH8cCIxOdUYIL0PYyHlL1NoneDuDSCwg5VE/5o=
+github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.13.0 h1:fGT8LonG+MYsHpcMt5i02nRRiN47e2OfMTUSf9hV+1c=
+github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue v1.13.0/go.mod h1:ZPI4T1e58+Y9oBwn2mMO7HDaTi5ZRswWivzEYSoD1QY=
+github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.0 h1:ggBvO5eiHgVrK8OwCM0U/5vpKx44YefM9B0/pZAqDXI=
+github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression v1.7.0/go.mod h1:PbwHxUgyfI5wzVb7t3EcIoaJjO3qV2kPnF1HRcL3cUY=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0 h1:xWCwjjvVz2ojYTP4kBKUuUh9ZrXfcAXpflhOUUeXg1k=
+github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.15.0/go.mod h1:j3fACuqXg4oMTQOR2yY7m0NmJY0yBK4L4sLsRXq1Ins=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.0 h1:FHVyVIJpOeQZCnYj9EVKTWahb4WDNFEUOKCx/dOUPcM=
+github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.0/go.mod h1:SL/aJzGL0LsQPQ1y2HMNbJGrm/Xh6aVCGq6ki+DLGEw=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.1.40/go.mod h1:5kKmFhLeOVy6pwPDpDNA6/hK/d6URC98pqDDqHgdBx4=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34 h1:ZK5jHhnrioRkUNOc+hOgQKlUL5JeC3S6JgLxtQ+Rm0Q=
+github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.34/go.mod h1:p4VfIceZokChbA9FzMbRGz5OV+lekcVtHlPKEO0gSZY=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.4.34/go.mod h1:RZP0scceAyhMIQ9JvFp7HvkpcgqjL4l/4C+7RAeGbuM=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34 h1:SZwFm17ZUNNg5Np0ioo/gq8Mn6u9w19Mri8DnJ15Jf0=
+github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.34/go.mod h1:dFZsC0BLo346mvKQLWmoJxT+Sjp+qcVR1tRVHQGOH9Q=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU=
+github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.1.3/go.mod h1:jYLMm3Dh0wbeV3lxth5ryks/O2M/omVXWyYm3YcEVqQ=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34 h1:ZNTqv4nIdE/DiBfUUfXcLZ/Spcuz+RjeziUtNJackkM=
+github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.34/go.mod h1:zf7Vcd1ViW7cPqYWEHLHJkS50X0JS2IKz9Cgaj6ugrs=
+github.com/aws/aws-sdk-go-v2/service/dynamodb v1.30.0 h1:rZ2DPklkMHMFGUe1GbtfBJjPa+1M6JUemDntzgQaA7Y=
+github.com/aws/aws-sdk-go-v2/service/dynamodb v1.30.0/go.mod h1:H6ktm/kjq2KtbGwnVFMAyOkOwcFfoD0P+SpneVqaa5o=
+github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.19.0 h1:/U4z6jbdY9nO9ZL0PNjxp9460GcIrAldxkYov2JbuI0=
+github.com/aws/aws-sdk-go-v2/service/dynamodbstreams v1.19.0/go.mod h1:0FgUg08+1knEoYHo0pa8ogm7D9sjH79lHnRzCNGk/6Q=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.93.2 h1:c6a19AjfhEXKlEX63cnlWtSQ4nzENihHZOG0I3wH6BE=
+github.com/aws/aws-sdk-go-v2/service/ec2 v1.93.2/go.mod h1:VX22JN3HQXDtQ3uS4h4TtM+K11vydq58tpHTlsm8TL8=
+github.com/aws/aws-sdk-go-v2/service/eventbridge v1.20.4 h1:G18wotYZxZ0A5tkqKv6FHCjsF86UQrqNHy5LS+T7JWM=
+github.com/aws/aws-sdk-go-v2/service/eventbridge v1.20.4/go.mod h1:XlbY5AGZhlipCdhRorT18/HEThKAxo51hMmhixreJoM=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3 h1:eAh2A4b5IzM/lum78bZ590jy36+d/aFLgKF/4Vd1xPE=
+github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.12.3/go.mod h1:0yKJC/kb8sAnmlYa6Zs3QVYqaC8ug2AbnNChv5Ox3uA=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0 h1:lguz0bmOoGzozP9XfRJR1QIayEYo+2vP/No3OfLF0pU=
+github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.7.0/go.mod h1:iu6FSzgt+M2/x3Dk8zhycdIcHjEFb36IS8HVUVFoMg0=
+github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.1 h1:QEot4yoGf6KGY2hAJe7IIP5x51pyRv4cs/x/aKcXMck=
+github.com/aws/aws-sdk-go-v2/service/internal/endpoint-discovery v1.9.1/go.mod h1:FVivjmCWEidMuFguqtnXZGoJK/MN+EtoCSEZMEcpGhc=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15 h1:dM9/92u2F1JbDaGooxTq18wmmFzbJRfXfVfy96/1CXM=
+github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.12.15/go.mod h1:SwFBy2vjtA0vZbjjaFtfN045boopadnoVPhu4Fv66vY=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15 h1:moLQUoVq91LiqT1nbvzDukyqAlCv89ZmwaHw/ZFlFZg=
+github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.18.15/go.mod h1:ZH34PJUc8ApjBIfgQCFvkWcUDBtl/WTD+uiYHjd8igA=
+github.com/aws/aws-sdk-go-v2/service/kinesis v1.18.4 h1:UohaQds+Puk9BEbvncXkZduIGYImxohbFpVmSoymXck=
+github.com/aws/aws-sdk-go-v2/service/kinesis v1.18.4/go.mod h1:HnjgmL8TNmYtGcrA3N6EeCnDvlX6CteCdUbZ1wV8QWQ=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2 h1:tWUG+4wZqdMl/znThEk9tcCy8tTMxq8dW0JTgamohrY=
+github.com/aws/aws-sdk-go-v2/service/s3 v1.79.2/go.mod h1:U5SNqwhXB3Xe6F47kXvWihPl/ilGaEDe8HD/50Z9wxc=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.0 h1:Xf3s55N9cqKvFK6D70zCXvXXN4ZovTCy7glL+gUhLEc=
+github.com/aws/aws-sdk-go-v2/service/secretsmanager v1.28.0/go.mod h1:RA3ERghFSivbTf0Sbsxv/grUuLMcyAjm0F/PylJMmEs=
+github.com/aws/aws-sdk-go-v2/service/sfn v1.19.4 h1:yIyFY2kbCOoHvuivf9minqnP2RLYJgmvQRYxakIb2oI=
+github.com/aws/aws-sdk-go-v2/service/sfn v1.19.4/go.mod h1:uWCH4ATwNrkRO40j8Dmy7u/Y1/BVWgCM+YjBNYZeOro=
+github.com/aws/aws-sdk-go-v2/service/sns v1.21.4 h1:Asj098jPfIZYzAbk4xVFwVBGij5hgMcli0d+5Pe4aZA=
+github.com/aws/aws-sdk-go-v2/service/sns v1.21.4/go.mod h1:bbB779DXXOnPXvB7F3dP7AjuV1Eyr7fNyrA058ExuzY=
+github.com/aws/aws-sdk-go-v2/service/sqs v1.30.0 h1:YaTVomKPwEuzYnJS/bV9xdZ//GO0j3B1exwP5Mqi1qY=
+github.com/aws/aws-sdk-go-v2/service/sqs v1.30.0/go.mod h1:4phHwV34rLb+56VjTud2w7hBu5Q57YIKFIA8Httzljg=
+github.com/aws/aws-sdk-go-v2/service/sso v1.19.0 h1:u6OkVDxtBPnxPkZ9/63ynEe+8kHbtS5IfaC4PzVxzWM=
+github.com/aws/aws-sdk-go-v2/service/sso v1.19.0/go.mod h1:YqbU3RS/pkDVu+v+Nwxvn0i1WB0HkNWEePWbmODEbbs=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 h1:6DL0qu5+315wbsAEEmzK+P9leRwNbkp+lGjPC+CEvb8=
+github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0/go.mod h1:olUAyg+FaoFaL/zFaeQQONjOZ9HXoxgvI/c7mQTYz7M=
+github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 h1:cjTRjh700H36MQ8M0LnDn33W3JmwC77mdxIIyPWCdpM=
+github.com/aws/aws-sdk-go-v2/service/sts v1.27.0/go.mod h1:nXfOBMWPokIbOY+Gi7a1psWMSvskUCemZzI+SMB7Akc=
+github.com/aws/smithy-go v1.14.2/go.mod h1:Tg+OJXh4MB2R/uN61Ko2f6hTZwB/ZYGOtib8J3gBHzA=
+github.com/aws/smithy-go v1.22.2 h1:6D9hW43xKFrRx/tXXfAlIZc4JI+yQe6snnWcQyxSyLQ=
+github.com/aws/smithy-go v1.22.2/go.mod h1:irrKGvNn1InZwb2d7fkIRNucdfwR8R+Ts3wxYa/cJHg=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A=
github.com/benbjohnson/clock v1.3.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -99,19 +164,20 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
-github.com/bits-and-blooms/bitset v1.17.0 h1:1X2TS7aHz1ELcC0yU1y2stUs/0ig5oMU6STFZGrhvHI=
-github.com/bits-and-blooms/bitset v1.17.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
+github.com/bits-and-blooms/bitset v1.20.0 h1:2F+rfL86jE2d/bmw7OhqUg2Sj/1rURkBn3MdfoPyRVU=
+github.com/bits-and-blooms/bitset v1.20.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
github.com/bketelsen/crypt v0.0.3-0.20200106085610-5cbc8cc4026c/go.mod h1:MKsuJmJgSg28kpZDP6UIiPt0e0Oz0kqKNGyRaWEPv84=
github.com/blendle/zapdriver v1.3.1 h1:C3dydBOWYRiOk+B8X9IVZ5IOe+7cl+tGOexN4QqHfpE=
github.com/blendle/zapdriver v1.3.1/go.mod h1:mdXfREi6u5MArG4j9fewC+FGnXaBR+T4Ox4J2u4eHCc=
github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
-github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd h1:js1gPwhcFflTZ7Nzl7WHaOTlTr5hIrR4n1NM4v9n4Kw=
github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
+github.com/btcsuite/btcd v0.25.0 h1:JPbjwvHGpSywBRuorFFqTjaVP4y6Qw69XJ1nQ6MyWJM=
+github.com/btcsuite/btcd v0.25.0/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE=
github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
-github.com/btcsuite/btcd/btcec/v2 v2.3.2 h1:5n0X6hX0Zk+6omWcihdYvdAlGf2DfasC0GMf7DClJ3U=
-github.com/btcsuite/btcd/btcec/v2 v2.3.2/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04=
+github.com/btcsuite/btcd/btcec/v2 v2.3.5 h1:dpAlnAwmT1yIBm3exhT1/8iUSD98RDJM5vqJVQDQLiU=
+github.com/btcsuite/btcd/btcec/v2 v2.3.5/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ=
github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8=
@@ -148,16 +214,14 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101 h1:7To3pQ+pZo0i3dsWEbinPNFs5gPSBOsJtx3wTT94VBY=
-github.com/cncf/xds/go v0.0.0-20231109132714-523115ebc101/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
-github.com/coinbase/rosetta-sdk-go v0.8.3 h1:IYqd+Ser5NVh0s7p8p2Ir82iCvi75E1l0NH2H4NEr0Y=
-github.com/coinbase/rosetta-sdk-go v0.8.3/go.mod h1:ChOHc+BNq7zqJDDkui0DA124GOvlAiRbdgAc1U9GMDQ=
+github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
+github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
+github.com/coinbase/mesh-sdk-go v0.8.9 h1:4paJktpDY7e5ghWSnSa5QHOXDdKTSlSwDZzbm1JT2tI=
+github.com/coinbase/mesh-sdk-go v0.8.9/go.mod h1:xIu+9M4EN/WkAy/H67lP8iu+/Fy3Wbyihmv8L+XacWM=
github.com/coinbase/rosetta-sdk-go/types v1.0.0 h1:jpVIwLcPoOeCR6o1tU+Xv7r5bMONNbHU7MuEHboiFuA=
github.com/coinbase/rosetta-sdk-go/types v1.0.0/go.mod h1:eq7W2TMRH22GTW0N0beDnN931DW0/WOI1R2sdHNHG4c=
-github.com/consensys/bavard v0.1.22 h1:Uw2CGvbXSZWhqK59X0VG/zOjpTFuOMcPLStrp1ihI0A=
-github.com/consensys/bavard v0.1.22/go.mod h1:k/zVjHHC4B+PQy1Pg7fgvG3ALicQw540Crag8qx+dZs=
-github.com/consensys/gnark-crypto v0.14.0 h1:DDBdl4HaBtdQsq/wfMwJvZNE80sHidrK3Nfrefatm0E=
-github.com/consensys/gnark-crypto v0.14.0/go.mod h1:CU4UijNPsHawiVGNxe9co07FkzCeWHHrb1li/n1XoU0=
+github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80=
+github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0=
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
@@ -165,10 +229,10 @@ github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7
github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA=
github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
+github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg=
+github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg=
github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM=
-github.com/crate-crypto/go-kzg-4844 v1.1.0 h1:EN/u9k2TF6OWSHrCCDBBU6GLNMq88OspHHlMnHfoyU4=
-github.com/crate-crypto/go-kzg-4844 v1.1.0/go.mod h1:JolLjpSff1tCCJKaJx4psrlEdlXuJEC996PL3tTAFks=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt9U=
github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
@@ -197,18 +261,23 @@ github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+m
github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw=
github.com/ebitengine/purego v0.5.2 h1:r2MQEtkGzZ4LRtFZVAg5bjYKnUbxxloaeuGxH0t7qfs=
github.com/ebitengine/purego v0.5.2/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
+github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A=
+github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s=
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.9-0.20201210154907-fd9021fe5dad/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.14.0 h1:hbG2kr4RuFj222B6+7T83thSPqLjwBIfQawTkC++2HA=
+github.com/envoyproxy/go-control-plane/envoy v1.36.0 h1:yg/JjO5E7ubRyKX3m07GF3reDNEnfOboJ0QySbH736g=
+github.com/envoyproxy/go-control-plane/envoy v1.36.0/go.mod h1:ty89S1YCCVruQAm9OtKeEkQLTb+Lkz0k8v9W0Oxsv98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
-github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA=
-github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE=
-github.com/ethereum-optimism/op-geth v1.101500.0 h1:pe/bYceb/w26Kz31/GGlFBrFQjQOuFHm8o2MV5W7n0g=
-github.com/ethereum-optimism/op-geth v1.101500.0/go.mod h1:OMpyVMMy5zpAAHlR5s/aGbXRk+7cIKczUEIJj54APbY=
-github.com/ethereum/c-kzg-4844 v1.0.0 h1:0X1LBXxaEtYD9xsyj9B9ctQEZIpnvVDeoBx8aHEwTNA=
-github.com/ethereum/c-kzg-4844 v1.0.0/go.mod h1:VewdlzQmpT5QSrVhbBuGoCdFJkpaJlO1aQputP83wc0=
+github.com/envoyproxy/protoc-gen-validate v1.3.0 h1:TvGH1wof4H33rezVKWSpqKz5NXWg5VPuZ0uONDT6eb4=
+github.com/envoyproxy/protoc-gen-validate v1.3.0/go.mod h1:HvYl7zwPa5mffgyeTUHA9zHIH36nmrm7oCbo4YKoSWA=
+github.com/ethereum-optimism/op-geth v1.101609.0 h1:r1z9bEXkm/7p54Y5Z5jMQ7H3HYExg1kqj0if1adOwjM=
+github.com/ethereum-optimism/op-geth v1.101609.0/go.mod h1:3YphRrN5/TvRp9VGy5rfA6l6rVR6IAsgSJNPLbIg66E=
+github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s=
+github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs=
github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8=
github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk=
github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw=
@@ -219,6 +288,8 @@ github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
+github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY=
+github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@@ -245,8 +316,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
-github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
-github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+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-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
@@ -260,10 +331,9 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74=
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
-github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
-github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw=
-github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU=
+github.com/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E=
+github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0=
github.com/gogo/googleapis v0.0.0-20180223154316-0cd9801be74a/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s=
github.com/gogo/googleapis v1.4.1 h1:1Yx4Myt7BxzvUr5ldGSbwYiZG6t9wGBZ+8/fX3Wvtq0=
github.com/gogo/googleapis v1.4.1/go.mod h1:2lpHqI5OcWCtVElxXnPt+s8oJvMpySlOyM6xDCrzib4=
@@ -308,8 +378,8 @@ 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.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
-github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb h1:PBC98N2aIaM3XXiurYmW7fx4GZkL8feAMVq7nEjURHk=
-github.com/golang/snappy v0.0.5-0.20220116011046-fa5810519dcb/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
+github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
+github.com/golang/snappy v1.0.0/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/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
@@ -322,8 +392,9 @@ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
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.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
-github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.5.8/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/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -341,12 +412,11 @@ github.com/google/pprof v0.0.0-20230817174616-7a8ec2ada47b/go.mod h1:czg5+yv1E0Z
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o=
github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw=
-github.com/google/subcommands v1.2.0/go.mod h1:ZjhPrFU+Olkh9WazFPsl27BQ4UPiG37m3yTrtFlrHVk=
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
-github.com/google/uuid v1.5.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.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
@@ -364,8 +434,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vb
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 h1:asbCHRVmodnJTuQ3qamDwqVOIjwqUPTYmYuemVOx+Ys=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0/go.mod h1:ggCgvZ2r7uOoQjOyu2Y1NhHmEPPzzuhWgcza5M1Ji1I=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@@ -420,6 +490,8 @@ github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/X
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
@@ -428,6 +500,7 @@ github.com/klauspost/compress v1.11.4/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYs
github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.17.11 h1:In6xLpyWOi1+C7tXUUWv2ot1QvBjxevKAaI6IXrJmUc=
github.com/klauspost/compress v1.17.11/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0=
+github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg=
github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -447,6 +520,8 @@ github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzW
github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c=
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
@@ -483,9 +558,6 @@ github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:F
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
-github.com/mmcloughlin/addchain v0.4.0 h1:SobOdjm2xLj1KkXN5/n0xTIWyZA2+s99UCY1iPfkHRY=
-github.com/mmcloughlin/addchain v0.4.0/go.mod h1:A86O+tHqZLMNO4w6ZZ4FlVQEadcoqkyU72HC5wJ4RlU=
-github.com/mmcloughlin/profile v0.1.1/go.mod h1:IhHD7q1ooxgwTgjxQYkACGA77oFTDdFVejUS1/tS/qU=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -504,6 +576,8 @@ github.com/naoina/go-stringutil v0.1.0 h1:rCUeRUHjBjGTSHl0VC00jUPLz8/F9dDzYI70Hz
github.com/naoina/go-stringutil v0.1.0/go.mod h1:XJ2SJL9jCtBh+P9q5btrd/Ylo8XwT/h1USek5+NqSA0=
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416 h1:shk/vn9oCoOTmwcouEdwIeOtOGA/ELRUw/GwvxwfT+0=
github.com/naoina/toml v0.1.2-0.20170918210437-9fafd6967416/go.mod h1:NBIhNtsFMo3G2szEBne+bO4gS192HuIYRqfvOWb4i1E=
+github.com/nexus-rpc/sdk-go v0.3.0 h1:Y3B0kLYbMhd4C2u00kcYajvmOrfozEtTV/nHSnV57jA=
+github.com/nexus-rpc/sdk-go v0.3.0/go.mod h1:TpfkM2Cw0Rlk9drGkoiSMpFqflKTiQLWUNyKJjF8mKQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E=
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
@@ -518,15 +592,12 @@ github.com/onsi/gomega v1.4.1/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
-github.com/opentracing-contrib/go-aws-sdk v0.0.0-20200219142134-2e00fb2121c5 h1:8gNn+RDTGdzFfb9p9n78SdnQ+JGkCB14xctljUdzv8g=
-github.com/opentracing-contrib/go-aws-sdk v0.0.0-20200219142134-2e00fb2121c5/go.mod h1:yI4m7klRqSRjgM761T6LsPJCQVqh+Ujaab2isxw+0GM=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs=
github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc=
github.com/outcaste-io/ristretto v0.2.3 h1:AK4zt/fJ76kjlYObOeNwh4T3asEuaCmp26pOvUOL9w0=
github.com/outcaste-io/ristretto v0.2.3/go.mod h1:W8HywhmtlopSB1jeMg3JtdIhf+DYkLAr0VN/s4+MHac=
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
-github.com/pborman/uuid v1.2.1 h1:+ZZIw58t/ozdjRaXh/3awHfmWRbzYxJoAdNJxe/3pvw=
github.com/pborman/uuid v1.2.1/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
@@ -537,17 +608,21 @@ github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+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/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/pressly/goose/v3 v3.14.0 h1:gNrFLLDF+fujdq394rcdYK3WPxp3VKWifTajlZwInJM=
+github.com/pressly/goose/v3 v3.14.0/go.mod h1:uwSpREK867PbIsdE9GS6pRk1LUPB7gwMkmvk9/hbIMA=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
-github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw=
-github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y=
+github.com/prometheus/client_golang v1.15.0 h1:5fCgGYogn0hFdhyhLbw7hEsWxufKtY9klyvdNfFlFhM=
+github.com/prometheus/client_golang v1.15.0/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk=
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
@@ -566,15 +641,17 @@ github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7z
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
-github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI=
-github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY=
+github.com/prometheus/procfs v0.11.0 h1:5EAgkfkMl659uZPbe9AS2N68a7Cc1TJbPEuGzFuRbyk=
+github.com/prometheus/procfs v0.11.0/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
github.com/prysmaticlabs/fastssz v0.0.0-20221107182844-78142813af44 h1:c3p3UzV4vFA7xaCDphnDWOjpxcadrQ26l5b+ypsvyxo=
github.com/prysmaticlabs/fastssz v0.0.0-20221107182844-78142813af44/go.mod h1:MA5zShstUwCQaE9faGHgCGvEWUbG87p4SAXINhmCkvg=
-github.com/prysmaticlabs/gohashtree v0.0.3-alpha h1:1EVinCWdb3Lorq7xn8DYQHf48nCcdAM3Vb18KsFlRWY=
-github.com/prysmaticlabs/gohashtree v0.0.3-alpha/go.mod h1:4pWaT30XoEx1j8KNJf3TV+E3mQkaufn7mf+jRNb/Fuk=
+github.com/prysmaticlabs/gohashtree v0.0.4-beta h1:H/EbCuXPeTV3lpKeXGPpEV9gsUpkqOOVnWapUyeWro4=
+github.com/prysmaticlabs/gohashtree v0.0.4-beta/go.mod h1:BFdtALS+Ffhg3lGQIHv9HDWuHS8cTvHZzrHWxwOtGOs=
github.com/prysmaticlabs/prysm/v4 v4.1.0 h1:fJWyCzeDgAD/4RGxqnZN0StrFQgZ0MXjpGSWkipV9zw=
github.com/prysmaticlabs/prysm/v4 v4.1.0/go.mod h1:+o907dc4mwEE0wJkQ8RrzCroC+q2WCzdCLtikwonw8c=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052 h1:Qp27Idfgi6ACvFQat5+VJvlYToylpM/hcyLBI3WaKPA=
github.com/richardartoul/molecule v1.0.1-0.20221107223329-32cfee06a052/go.mod h1:uvX/8buq8uVeiZiFht+0lqSLBHF+uGV8BrTv8W/SIwk=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
@@ -587,8 +664,8 @@ github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzG
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
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.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
-github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
+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/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
@@ -633,8 +710,9 @@ github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM=
github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y=
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
+github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.7.1/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ=
@@ -659,13 +737,13 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
-github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+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/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
-github.com/supranational/blst v0.3.13 h1:AYeSxdOMacwu7FBmpfloBz5pbFXDmJL33RuwnKtmTjk=
-github.com/supranational/blst v0.3.13/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
+github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe h1:nbdqkIGOGfUAD54q1s2YBcBz/WcsxCO9HUQ4aGV5hUw=
+github.com/supranational/blst v0.3.16-0.20250831170142-f48500c1fdbe/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw=
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc=
github.com/teris-io/shortid v0.0.0-20171029131806-771a37caa5cf/go.mod h1:M8agBzgqHIhgj7wEn9/0hJUZcrvt9VY+Ln+S1I5Mha0=
github.com/teris-io/shortid v0.0.0-20201117134242-e59966efd125 h1:3SNcvBmEPE1YlB1JpVZouslJpI3GBNoiqW7+wb0Rz7w=
@@ -727,25 +805,29 @@ 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.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/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0 h1:UNQQKPfTDe1J81ViolILjTKPr9WetKW6uei2hFgJmFs=
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.47.0/go.mod h1:r9vWsPS/3AQItv3OSlEJ/E4mbrhUbbw18meOjArPtKQ=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0 h1:sv9kVfal0MK0wBMCOGr+HeJm9v803BkJxGrk2au7j08=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.47.0/go.mod h1:SK2UL73Zy1quvRPonmOmRDiWk1KBV3LyIeeIxcEApWw=
-go.opentelemetry.io/otel v1.22.0 h1:xS7Ku+7yTFvDfDraDIJVpw7XPyuHlB9MCiqqX5mcJ6Y=
-go.opentelemetry.io/otel v1.22.0/go.mod h1:eoV4iAi3Ea8LkAEI9+GFT44O6T/D0GWAVFyZVCC6pMI=
-go.opentelemetry.io/otel/metric v1.22.0 h1:lypMQnGyJYeuYPhOM/bgjbFM6WE44W1/T45er4d8Hhg=
-go.opentelemetry.io/otel/metric v1.22.0/go.mod h1:evJGjVpZv0mQ5QBRJoBF64yMuOf4xCWdXjK8pzFvliY=
-go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8=
-go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E=
-go.opentelemetry.io/otel/trace v1.22.0 h1:Hg6pPujv0XG9QaVbGOBVHunyuLcCC3jN7WEhPx83XD0=
-go.opentelemetry.io/otel/trace v1.22.0/go.mod h1:RbbHXVqKES9QhzZq/fE5UnOSILqRt40a21sPw2He1xo=
+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/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.temporal.io/api v1.5.0/go.mod h1:BqKxEJJYdxb5dqf0ODfzfMxh8UEQ5L3zKS51FiIYYkA=
-go.temporal.io/api v1.27.0 h1:M7a7p3A/gIKEMAYVBQD+/2hfzh/hC0410393TwD20Ko=
-go.temporal.io/api v1.27.0/go.mod h1:iASB2zPPR+FtFKn5w7/hF7AG2dkvkW7TTMAqL06tz0g=
+go.temporal.io/api v1.49.1 h1:CdiIohibamF4YP9k261DjrzPVnuomRoh1iC//gZ1puA=
+go.temporal.io/api v1.49.1/go.mod h1:iaxoP/9OXMJcQkETTECfwYq4cw/bj4nwov8b3ZLVnXM=
go.temporal.io/sdk v1.12.0/go.mod h1:lSp3lH1lI0TyOsus0arnO3FYvjVXBZGi/G7DjnAnm6o=
-go.temporal.io/sdk v1.26.0-rc.2.0.20240214221834-30da688037d1 h1:TJAj59PR+Ek0Z1dQSBx50MDxPeQsMZdaRl71w6QK3VU=
-go.temporal.io/sdk v1.26.0-rc.2.0.20240214221834-30da688037d1/go.mod h1:HDr8fIWJ/HF8dJwTPgOayI8PYB5WoVIxUMjzE78M2ng=
+go.temporal.io/sdk v1.35.0 h1:lRNAQ5As9rLgYa7HBvnmKyzxLcdElTuoFJ0FXM/AsLQ=
+go.temporal.io/sdk v1.35.0/go.mod h1:1q5MuLc2MEJ4lneZTHJzpVebW2oZnyxoIOWX3oFVebw=
go.temporal.io/sdk/contrib/tally v0.2.0 h1:XnTJIQcjOv+WuCJ1u8Ve2nq+s2H4i/fys34MnWDRrOo=
go.temporal.io/sdk/contrib/tally v0.2.0/go.mod h1:1kpSuCms/tHeJQDPuuKkaBsMqfHnIIRnCtUYlPNXxuE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -761,8 +843,9 @@ go.uber.org/fx v1.20.1 h1:zVwVQGS8zYvhh9Xxcu4w1M6ESyeMzebzj2NbSayZ4Mk=
go.uber.org/fx v1.20.1/go.mod h1:iSYNbHf2y55acNCwCXKx7LbWb5WG1Bnue5RDXz1OREg=
go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ=
-go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A=
go.uber.org/goleak v1.2.1/go.mod h1:qlT2yGI9QafXHhZZLxlSuNsMw3FFLxBr+tBRlmO1xH4=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
@@ -799,8 +882,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220214200702-86341886e292/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
-golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc=
-golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc=
+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-20190121172915-509febef88a4/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=
@@ -837,6 +920,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
+golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
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=
@@ -874,15 +959,15 @@ golang.org/x/net v0.0.0-20210913180222-943fd674d43e/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.3.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE=
-golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
-golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
+golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
+golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
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.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ=
-golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o=
+golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
+golang.org/x/oauth2 v0.34.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=
@@ -895,8 +980,8 @@ golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/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.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
-golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
+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-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -952,17 +1037,18 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
-golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+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-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA=
-golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg=
-golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
+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=
@@ -974,14 +1060,14 @@ 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.5.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.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
-golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
+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-20210723032227-1f47c861a9ac/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
-golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
-golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
+golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
+golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/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=
@@ -1027,12 +1113,16 @@ golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ=
+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-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
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=
@@ -1053,8 +1143,6 @@ google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7
google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
-google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
-google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/genproto v0.0.0-20180518175338-11a468237815/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
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=
@@ -1081,10 +1169,10 @@ google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEY
google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY=
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe h1:USL2DhxfgRchafRvt/wYyyQNzwgL7ZiURcozOE/Pkvo=
google.golang.org/genproto v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:cc8bqMqtv9gMOr0zHg2Vzff5ULhhL2IXP4sbcn32Dro=
-google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe h1:0poefMBYvYbs7g5UkjS6HcxBPaTRAmznle9jnxYoAI8=
-google.golang.org/genproto/googleapis/api v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe h1:bQnxqljG/wqi4NTXu2+DJ3n7APcEA882QZ1JvhQAq9o=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20240125205218-1f4bbc51befe/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s=
+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.12.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
@@ -1102,8 +1190,8 @@ google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTp
google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34=
-google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
-google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
+google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
+google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
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=
@@ -1117,8 +1205,8 @@ google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp0
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.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
-google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
+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/DataDog/dd-trace-go.v1 v1.59.1 h1:rXPzybNgv7r9dHCLlQqcThiAL5q2gvAQgFYYjpgwR/k=
gopkg.in/DataDog/dd-trace-go.v1 v1.59.1/go.mod h1:/4wpnyM3/ncN1CY1kqIm84mw8N/QVcHeckn13u8ZJis=
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
@@ -1165,8 +1253,26 @@ logur.dev/adapter/zap v0.5.0/go.mod h1:fpjTeoSkN05hrUviBkIe/u0CKWTh1PBxWQLLFgnWh
logur.dev/logur v0.16.1/go.mod h1:DyA5B+b6WjjCcnpE1+HGtTLh2lXooxRq+JmAwXMRK08=
logur.dev/logur v0.17.0 h1:lwFZk349ZBY7KhonJFLshP/VhfFa6BxOjHxNnPHnEyc=
logur.dev/logur v0.17.0/go.mod h1:DyA5B+b6WjjCcnpE1+HGtTLh2lXooxRq+JmAwXMRK08=
+lukechampine.com/uint128 v1.3.0 h1:cDdUVfRwDUDovz610ABgFD17nXD4/uDgVHl2sC3+sbo=
+lukechampine.com/uint128 v1.3.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.41.0 h1:QoR1Sn3YWlmA1T4vLaKZfawdVtSiGx8H+cEojbC7v1Q=
+modernc.org/cc/v3 v3.41.0/go.mod h1:Ni4zjJYJ04CDOhG7dn640WGfwBzfE0ecX8TyMB0Fv0Y=
+modernc.org/ccgo/v3 v3.16.14 h1:af6KNtFgsVmnDYrWk3PQCS9XT6BXe7o3ZFJKkIKvXNQ=
+modernc.org/ccgo/v3 v3.16.14/go.mod h1:mPDSujUIaTNWQSG4eqKw+atqLOEbma6Ncsa94WbC9zo=
+modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM=
+modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o=
+modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sqlite v1.24.0 h1:EsClRIWHGhLTCX44p+Ri/JLD+vFGo0QGjasg2/F9TlI=
+modernc.org/sqlite v1.24.0/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk=
+modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
+modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
-rsc.io/tmplfunc v0.0.3 h1:53XFQh69AfOa8Tw0Jm7t+GV7KZhOi6jzsCzTtKbMvzU=
-rsc.io/tmplfunc v0.0.3/go.mod h1:AG3sTPzElb1Io3Yg4voV9AGZJuleGAwaVRxL9M49PhA=
diff --git a/internal/aws/config.go b/internal/aws/config.go
index 9f69120f..43e70688 100644
--- a/internal/aws/config.go
+++ b/internal/aws/config.go
@@ -1,9 +1,14 @@
package aws
import (
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/credentials"
+ "context"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/credentials"
"go.uber.org/fx"
+ "golang.org/x/xerrors"
+ awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go-v2/aws"
"github.com/coinbase/chainstorage/internal/utils/fxparams"
)
@@ -15,15 +20,34 @@ type (
}
)
-func NewConfig(params ConfigParams) *aws.Config {
- cfg := &aws.Config{
- Region: aws.String(params.Config.AWS.Region),
- Retryer: newCustomRetryer(),
- }
+func NewConfig(params ConfigParams) (aws.Config, error) {
+ ctx := context.Background()
+
+ var opts []func(*config.LoadOptions) error
+ opts = append(opts, config.WithRegion(params.Config.AWS.Region))
+ opts = append(opts, config.WithRetryer(newCustomRetryer))
+
if params.Config.AWS.IsLocalStack {
- cfg.Credentials = credentials.NewStaticCredentials("THESE", "ARE", "IGNORED")
- cfg.S3ForcePathStyle = aws.Bool(true)
- cfg.Endpoint = aws.String("http://localhost:4566")
+ opts = append(opts, config.WithCredentialsProvider(
+ credentials.NewStaticCredentialsProvider("THESE", "ARE", "IGNORED"),
+ ))
+ opts = append(opts, config.WithEndpointResolverWithOptions(
+ aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
+ return aws.Endpoint{
+ URL: "http://localstack:4566",
+ HostnameImmutable: true,
+ }, nil
+ }),
+ ))
}
- return cfg
+
+ cfg, err := config.LoadDefaultConfig(ctx, opts...)
+ if err != nil {
+ return aws.Config{}, xerrors.Errorf("failed to load AWS config: %w", err)
+ }
+
+ // Add DataDog tracing middleware
+ awstrace.AppendMiddleware(&cfg)
+
+ return cfg, nil
}
diff --git a/internal/aws/module.go b/internal/aws/module.go
index 81a3f5bb..17f91016 100644
--- a/internal/aws/module.go
+++ b/internal/aws/module.go
@@ -5,6 +5,5 @@ import (
)
var Module = fx.Options(
- fx.Provide(NewSession),
fx.Provide(NewConfig),
)
diff --git a/internal/aws/retryer.go b/internal/aws/retryer.go
index 492032ae..a14a2149 100644
--- a/internal/aws/retryer.go
+++ b/internal/aws/retryer.go
@@ -4,48 +4,40 @@ import (
"strings"
"time"
- "github.com/aws/aws-sdk-go/aws/client"
- "github.com/aws/aws-sdk-go/aws/request"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/aws/retry"
)
-type customRetryer struct {
- client.DefaultRetryer
-}
-
const (
// Use the same configurations as the DynamoDB client.
- // Ref: https://github.com/aws/aws-sdk-go/blob/6fcfde5b3429cb00ea9dfc16157299cc6ec0bcaf/service/dynamodb/customizations.go#L33
maxRetries = 10
minRetryDelay = 50 * time.Millisecond
+ maxRetryDelay = 5 * time.Second
)
-var _ request.Retryer = &customRetryer{}
-
-func newCustomRetryer() request.Retryer {
- return &customRetryer{
- DefaultRetryer: client.DefaultRetryer{
- NumMaxRetries: maxRetries,
- MinRetryDelay: minRetryDelay,
- },
- }
+// newCustomRetryer creates a custom retryer for AWS SDK v2.
+func newCustomRetryer() aws.Retryer {
+ return retry.NewStandard(func(o *retry.StandardOptions) {
+ o.MaxAttempts = maxRetries
+ o.MaxBackoff = maxRetryDelay
+ o.Retryables = append(o.Retryables, connectionResetRetryable{})
+ })
}
-// isErrReadConnectionReset returns true if the underlying error is a read connection reset error.
+// connectionResetRetryable implements retry.IsErrorRetryable for connection reset errors.
+type connectionResetRetryable struct{}
+
+// IsErrorRetryable returns true if the error is a read connection reset error.
//
// A read connection reset error is thrown when the SDK is unable to read the response of an underlying API request
-// due to a connection reset. The DefaultRetryer does not treat this error as a retryable error since the SDK has no
+// due to a connection reset. The default retryer does not treat this error as a retryable error since the SDK has no
// knowledge about whether the given operation is idempotent or whether it would be safe to retry.
-// Ref: https://github.com/aws/aws-sdk-go/pull/2926#issuecomment-553637658.
//
// In this project all operations with S3 or DynamoDB (read, write, list) are considered idempotent,
// and therefore this error can be treated as retryable.
-func isErrReadConnectionReset(err error) bool {
- // The error string must match the one in
- // https://github.com/aws/aws-sdk-go/blob/main/aws/request/connection_reset_error.go.
- return err != nil && strings.Contains(err.Error(), "read: connection reset")
-}
-
-// ShouldRetry overrides the implementation defined in client.DefaultRetryer.
-func (sr *customRetryer) ShouldRetry(r *request.Request) bool {
- return sr.DefaultRetryer.ShouldRetry(r) || isErrReadConnectionReset(r.Error)
+func (connectionResetRetryable) IsErrorRetryable(err error) aws.Ternary {
+ if err != nil && strings.Contains(err.Error(), "read: connection reset") {
+ return aws.TrueTernary
+ }
+ return aws.UnknownTernary
}
diff --git a/internal/aws/retryer_test.go b/internal/aws/retryer_test.go
index 17b798ca..191cd551 100644
--- a/internal/aws/retryer_test.go
+++ b/internal/aws/retryer_test.go
@@ -2,38 +2,33 @@ package aws
import (
"errors"
- "net"
"testing"
- "github.com/aws/aws-sdk-go/aws/awserr"
- "github.com/aws/aws-sdk-go/aws/client"
- "github.com/aws/aws-sdk-go/aws/request"
+ "github.com/aws/aws-sdk-go-v2/aws"
"github.com/stretchr/testify/require"
)
-var errReadConnectionReset = awserr.New(
- request.ErrCodeRequestError,
- "send request failed",
- &net.OpError{
- Op: "read",
- Err: errors.New("connection reset"),
- },
-)
-
-func TestDefaultRetryer(t *testing.T) {
+func TestConnectionResetRetryable_IsRetryable(t *testing.T) {
require := require.New(t)
- req := &request.Request{
- Error: errReadConnectionReset,
- }
- retryer := client.DefaultRetryer{NumMaxRetries: 3}
- require.False(retryer.ShouldRetry(req))
+
+ retryable := connectionResetRetryable{}
+
+ // Test connection reset error is retryable
+ resetErr := errors.New("read: connection reset")
+ require.Equal(aws.TrueTernary, retryable.IsErrorRetryable(resetErr))
+
+ // Test other errors are unknown (let other retryables decide)
+ otherErr := errors.New("some other error")
+ require.Equal(aws.UnknownTernary, retryable.IsErrorRetryable(otherErr))
+
+ // Test nil error
+ require.Equal(aws.UnknownTernary, retryable.IsErrorRetryable(nil))
}
-func TestCustomRetryer(t *testing.T) {
+func TestNewCustomRetryer(t *testing.T) {
require := require.New(t)
- req := &request.Request{
- Error: errReadConnectionReset,
- }
+
retryer := newCustomRetryer()
- require.True(retryer.ShouldRetry(req))
+ require.NotNil(retryer)
+ require.Equal(maxRetries, retryer.MaxAttempts())
}
diff --git a/internal/aws/session.go b/internal/aws/session.go
deleted file mode 100644
index 3fe8e6ab..00000000
--- a/internal/aws/session.go
+++ /dev/null
@@ -1,30 +0,0 @@
-package aws
-
-import (
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/session"
- "go.uber.org/fx"
- "golang.org/x/xerrors"
- awstrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/aws/aws-sdk-go/aws"
-
- "github.com/coinbase/chainstorage/internal/utils/fxparams"
-)
-
-type (
- SessionParams struct {
- fx.In
- fxparams.Params
- AWSConfig *aws.Config
- }
-)
-
-func NewSession(params SessionParams) (*session.Session, error) {
- awsSession, err := session.NewSession(params.AWSConfig)
- if err != nil {
- return nil, xerrors.Errorf("failed to create AWS session: %w", err)
- }
-
- // wrap aws session for tracing requests and responses
- awsSession = awstrace.WrapSession(awsSession)
- return awsSession, nil
-}
diff --git a/internal/blockchain/client/bitcoin/bitcoin.go b/internal/blockchain/client/bitcoin/bitcoin.go
index 0e68ab5e..7661d2f8 100644
--- a/internal/blockchain/client/bitcoin/bitcoin.go
+++ b/internal/blockchain/client/bitcoin/bitcoin.go
@@ -3,10 +3,12 @@ package bitcoin
import (
"context"
"encoding/json"
+ "strconv"
"time"
"github.com/go-playground/validator/v10"
"go.uber.org/zap"
+ "golang.org/x/sync/errgroup"
"golang.org/x/xerrors"
"github.com/coinbase/chainstorage/internal/blockchain/client/internal"
@@ -19,17 +21,42 @@ import (
)
type (
+ // rpcMethods holds the RPC method definitions for a Bitcoin-family client.
+ // Use newRPCMethods() for defaults, or pass rpcMethodsOverride to customize.
+ rpcMethods struct {
+ getBlockByHash *jsonrpc.RequestMethod
+ getBlockHash *jsonrpc.RequestMethod
+ getRawTransaction *jsonrpc.RequestMethod
+ getBlockCount *jsonrpc.RequestMethod
+ }
+
+ rpcMethodName string
+
+ rpcMethodsOverride map[rpcMethodName]*jsonrpc.RequestMethod
+
bitcoinClient struct {
- config *config.Config
- logger *zap.Logger
- client jsonrpc.Client
- validate *validator.Validate
+ config *config.Config
+ logger *zap.Logger
+ client jsonrpc.Client
+ validate *validator.Validate
+ methods rpcMethods
+ preserveRawInputTransactions bool
+ // Optional hook to customize getrawtransaction RPC params.
+ // Args: (txid, current block hash). Returns: RPC call params.
+ // nil means default behavior: jsonrpc.Params{txid, 1}
+ getRawTxParams func(txid string, blockHash string) jsonrpc.Params
}
bitcoinBlockHeaderResultHolder struct {
header *bitcoin.BitcoinBlockLit // Use the light version for faster parsing.
rawJson json.RawMessage // Store the raw message in blob storage.
}
+
+ // parsedInputTx holds a parsed input transaction with an O(1) vout lookup map.
+ parsedInputTx struct {
+ tx *bitcoin.BitcoinInputTransactionLit
+ voutMap map[uint64]*bitcoin.BitcoinTransactionOutput
+ }
)
const (
@@ -38,37 +65,83 @@ const (
// If verbosity is 1, returns a json object without full transaction data
bitcoinBlockMetadataVerbosity = 1
+ // Maximum number of concurrent batch RPC calls when fetching input transactions.
+ fetchInputTxConcurrency = 3
+
// err code defined by bitcoin.
// reference: https://github.com/bitcoin/bitcoin/blob/89d148c8c65b3e6b6a8fb8b722efb4b6a7d0a375/src/rpc/protocol.h#L23-L87
bitcoinErrCodeInvalidAddressOrKey = -5
bitcoinErrCodeInvalidParameter = -8
bitcoinErrMessageBlockNotFound = "Block not found"
bitcoinErrMessageBlockOutOfRange = "Block height out of range"
+)
- // batch size
- bitcoinGetInputTransactionsBatchSize = 100
+const (
+ rpcMethodGetBlockByHash rpcMethodName = "getBlockByHash"
+ rpcMethodGetBlockHash rpcMethodName = "getBlockHash"
+ rpcMethodGetRawTransaction rpcMethodName = "getRawTransaction"
+ rpcMethodGetBlockCount rpcMethodName = "getBlockCount"
)
var _ internal.Client = (*bitcoinClient)(nil)
+// Default RPC method definitions shared across all Bitcoin-family chains.
var (
- bitcoinGetBlockByHashMethod = &jsonrpc.RequestMethod{
- Name: "getblock",
- Timeout: time.Second * 10,
+ defaultGetBlockByHash = &jsonrpc.RequestMethod{Name: "getblock", Timeout: 10 * time.Second}
+ defaultGetBlockHash = &jsonrpc.RequestMethod{Name: "getblockhash", Timeout: 5 * time.Second}
+ defaultGetRawTransaction = &jsonrpc.RequestMethod{Name: "getrawtransaction", Timeout: 30 * time.Second}
+ defaultGetBlockCount = &jsonrpc.RequestMethod{Name: "getblockcount", Timeout: 5 * time.Second}
+)
+
+func defaultRPCMethods() rpcMethods {
+ return rpcMethods{
+ getBlockByHash: defaultGetBlockByHash,
+ getBlockHash: defaultGetBlockHash,
+ getRawTransaction: defaultGetRawTransaction,
+ getBlockCount: defaultGetBlockCount,
}
- bitcoinGetBlockHashMethod = &jsonrpc.RequestMethod{
- Name: "getblockhash",
- Timeout: time.Second * 5,
+}
+
+func newRPCMethods(overrides ...rpcMethodsOverride) rpcMethods {
+ methods := defaultRPCMethods()
+ for _, override := range overrides {
+ for methodName, method := range override {
+ if method == nil {
+ continue
+ }
+ switch methodName {
+ case rpcMethodGetBlockByHash:
+ methods.getBlockByHash = method
+ case rpcMethodGetBlockHash:
+ methods.getBlockHash = method
+ case rpcMethodGetRawTransaction:
+ methods.getRawTransaction = method
+ case rpcMethodGetBlockCount:
+ methods.getBlockCount = method
+ }
+ }
}
- bitcoinGetRawTransactionMethod = &jsonrpc.RequestMethod{
- Name: "getrawtransaction",
- Timeout: time.Second * 30,
+
+ return methods
+}
+
+// rpcMethodsOverrideFromConfig builds an rpcMethodsOverride from configurable
+// timeout values in ClientConfig. Zero-value durations are skipped, preserving
+// the compiled defaults.
+func rpcMethodsOverrideFromConfig(cfg *config.Config) rpcMethodsOverride {
+ override := make(rpcMethodsOverride)
+ clientCfg := cfg.Chain.Client
+ if clientCfg.RpcTimeoutGetBlock > 0 {
+ override[rpcMethodGetBlockByHash] = &jsonrpc.RequestMethod{Name: defaultGetBlockByHash.Name, Timeout: clientCfg.RpcTimeoutGetBlock}
}
- bitcoinGetBlockCountMethod = &jsonrpc.RequestMethod{
- Name: "getblockcount",
- Timeout: time.Second * 5,
+ if clientCfg.RpcTimeoutGetRawTx > 0 {
+ override[rpcMethodGetRawTransaction] = &jsonrpc.RequestMethod{Name: defaultGetRawTransaction.Name, Timeout: clientCfg.RpcTimeoutGetRawTx}
}
-)
+ if clientCfg.RpcTimeoutGetBlockHash > 0 {
+ override[rpcMethodGetBlockHash] = &jsonrpc.RequestMethod{Name: defaultGetBlockHash.Name, Timeout: clientCfg.RpcTimeoutGetBlockHash}
+ }
+ return override
+}
func NewBitcoinClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
return internal.NewJsonrpcClientFactory(params, func(client jsonrpc.Client) internal.Client {
@@ -78,6 +151,7 @@ func NewBitcoinClientFactory(params internal.JsonrpcClientParams) internal.Clien
logger: logger,
client: client,
validate: validator.New(),
+ methods: newRPCMethods(rpcMethodsOverrideFromConfig(params.Config)),
}
})
}
@@ -101,13 +175,13 @@ func (b *bitcoinClient) BatchGetBlockMetadata(ctx context.Context, tag uint32, f
}
}
- responses, err := b.client.BatchCall(ctx, bitcoinGetBlockByHashMethod, params)
+ responses, err := b.client.BatchCall(ctx, b.methods.getBlockByHash, params)
if err != nil {
return nil, xerrors.Errorf("failed to get block for blockhashes: %w", err)
}
if len(responses) != numBlocks {
- return nil, xerrors.Errorf("missing blocks in BatchCall to %s", bitcoinGetBlockByHashMethod.Name)
+ return nil, xerrors.Errorf("missing blocks in BatchCall to %s", b.methods.getBlockByHash.Name)
}
results := make([]*api.BlockMetadata, len(responses))
@@ -145,7 +219,7 @@ func (b *bitcoinClient) GetBlockByHeight(ctx context.Context, tag uint32, height
ctx = internal.ContextWithOptions(ctx, opts...)
params := jsonrpc.Params{height}
- response, err := b.client.Call(ctx, bitcoinGetBlockHashMethod, params)
+ response, err := b.client.Call(ctx, b.methods.getBlockHash, params)
if err != nil {
var rpcErr *jsonrpc.RPCError
if xerrors.As(err, &rpcErr) &&
@@ -181,7 +255,7 @@ func (b *bitcoinClient) GetBlockByHash(ctx context.Context, tag uint32, height u
bitcoinBlockVerbosity,
}
- response, err := b.client.Call(ctx, bitcoinGetBlockByHashMethod, params)
+ response, err := b.client.Call(ctx, b.methods.getBlockByHash, params)
if err != nil {
var rpcErr *jsonrpc.RPCError
if xerrors.As(err, &rpcErr) &&
@@ -208,7 +282,7 @@ func (b *bitcoinClient) GetBlockByHash(ctx context.Context, tag uint32, height u
func (b *bitcoinClient) GetLatestHeight(ctx context.Context) (uint64, error) {
params := jsonrpc.Params{}
- response, err := b.client.Call(ctx, bitcoinGetBlockCountMethod, params)
+ response, err := b.client.Call(ctx, b.methods.getBlockCount, params)
if err != nil {
return 0, xerrors.Errorf("failed to get the height of the most-work fully-validated chain: %w", err)
}
@@ -255,7 +329,7 @@ func (b *bitcoinClient) getBlockFromHeader(
) (*api.Block, error) {
blockHash := headerResult.header.Hash.Value()
- inputTransactionsData, err := b.getInputTransactions(ctx, headerResult.header)
+ inputTransactionsData, rawInputTransactionsMap, err := b.getInputTransactions(ctx, headerResult.header)
if err != nil {
return nil, xerrors.Errorf("failed to get previous transactions for block %s: %w", blockHash, err)
}
@@ -265,6 +339,14 @@ func (b *bitcoinClient) getBlockFromHeader(
inputTransactions[i] = &api.RepeatedBytes{Data: data}
}
+ blobdata := &api.BitcoinBlobdata{
+ Header: headerResult.rawJson,
+ InputTransactions: inputTransactions,
+ }
+ if b.preserveRawInputTransactions {
+ blobdata.RawInputTransactions = rawInputTransactionsMap
+ }
+
block := &api.Block{
Blockchain: b.config.Chain.Blockchain,
Network: b.config.Chain.Network,
@@ -277,110 +359,230 @@ func (b *bitcoinClient) getBlockFromHeader(
Timestamp: utils.ToTimestamp(int64(headerResult.header.Time.Value())),
},
Blobdata: &api.Block_Bitcoin{
- Bitcoin: &api.BitcoinBlobdata{
- Header: headerResult.rawJson,
- InputTransactions: inputTransactions,
- },
+ Bitcoin: blobdata,
},
}
return block, nil
}
-// get raw transaction data for input transactions of a block
-// if a block contains transactions A (input txs: A1, A2), B (input txs: B1, B2), C (input txs: C1),
+// getInputTransactions fetches and processes input transactions for a block.
+// Returns per-transaction filtered input data and the raw transaction map.
+//
+// For a block with transactions A (inputs: A1, A2), B (inputs: B1, B2), C (inputs: C1),
// the results will be:
-// [[transaction_data_of_A1, transaction_data_of_A2], [transaction_data_of_B1, transaction_data_of_B2], [transaction_data_of_C1]]
-// For individual transaction data in the results, they are serialized bytes of BitcoinInputTransactionLit
+// [[data_of_A1, data_of_A2], [data_of_B1, data_of_B2], [data_of_C1]]
+// Each entry is serialized bytes of BitcoinInputTransactionLit with a single filtered vout.
func (b *bitcoinClient) getInputTransactions(
ctx context.Context,
header *bitcoin.BitcoinBlockLit,
-) ([][][]byte, error) {
+) ([][][]byte, map[string][]byte, error) {
transactions := header.Transactions
blockHash := header.Hash.Value()
- var inputTransactionIDs []string
- // TODO: dedupe for inputTransactionIDs
+ inputTransactionIDs := collectInputTransactionIDs(transactions)
+
+ inputTransactionsMap, err := b.fetchInputTransactions(ctx, inputTransactionIDs, blockHash)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ parsedCache, err := parseInputTransactions(inputTransactionsMap)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ results, err := buildInputTransactionResults(transactions, parsedCache, blockHash)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return results, inputTransactionsMap, nil
+}
+
+// collectInputTransactionIDs extracts deduplicated input transaction IDs
+// from all block transactions, preserving first-seen order.
+func collectInputTransactionIDs(transactions []*bitcoin.BitcoinTransactionLit) []string {
+ seen := make(map[string]bool)
+ var ids []string
for _, tx := range transactions {
for _, input := range tx.Inputs {
- inputTransactionID := input.Identifier.Value()
+ txID := input.Identifier.Value()
// coinbase transaction does not have txid
- if inputTransactionID != "" {
- inputTransactionIDs = append(inputTransactionIDs, inputTransactionID)
+ if txID != "" && !seen[txID] {
+ seen[txID] = true
+ ids = append(ids, txID)
}
}
}
+ return ids
+}
+// fetchInputTransactions fetches raw transaction data for the given IDs via
+// concurrent batched getrawtransaction RPC calls. Returns a map of txid -> raw JSON.
+func (b *bitcoinClient) fetchInputTransactions(
+ ctx context.Context,
+ inputTransactionIDs []string,
+ blockHash string,
+) (map[string][]byte, error) {
+ opts := internal.OptionsFromContext(ctx)
+ txBatchSize := b.config.Chain.Client.TxBatchSize
numTransactions := len(inputTransactionIDs)
- inputTransactionsMap := make(map[string][]byte, numTransactions)
+ opts.RecordHeartbeat(ctx, "fetchInputTx.begin", numTransactions)
- // batch of batchCalls to getrawtransaction in order to fetch input transaction data
- for batchStart := 0; batchStart < numTransactions; batchStart += bitcoinGetInputTransactionsBatchSize {
- batchEnd := batchStart + bitcoinGetInputTransactionsBatchSize
- if batchEnd > numTransactions {
- batchEnd = numTransactions
- }
+ b.logger.Debug(
+ "getting input transactions>>>",
+ zap.Int("numTransactions", numTransactions),
+ zap.Int("txBatchSize", txBatchSize),
+ )
+
+ // Calculate number of batches and pre-allocate per-batch result slices.
+ numBatches := (numTransactions + txBatchSize - 1) / txBatchSize
+ batchResults := make([][]*jsonrpc.Response, numBatches)
+
+ g, ctx := errgroup.WithContext(ctx)
+ g.SetLimit(fetchInputTxConcurrency)
+
+ for batchIdx := 0; batchIdx < numBatches; batchIdx++ {
+ batchStart := batchIdx * txBatchSize
+ batchEnd := min(batchStart+txBatchSize, numTransactions)
+ idx := batchIdx
batchParams := make([]jsonrpc.Params, batchEnd-batchStart)
for i, transactionID := range inputTransactionIDs[batchStart:batchEnd] {
- batchParams[i] = jsonrpc.Params{
- transactionID,
- true,
+ if b.getRawTxParams != nil {
+ batchParams[i] = b.getRawTxParams(transactionID, blockHash)
+ } else {
+ batchParams[i] = jsonrpc.Params{transactionID, true}
}
}
- batchResponses, err := b.client.BatchCall(ctx, bitcoinGetRawTransactionMethod, batchParams)
- if err != nil {
- return nil, xerrors.Errorf(
- "failed to call %s for subset of (blockHash=%s, startTransactionID=%v, batchSize=%v): %w",
- bitcoinGetRawTransactionMethod.Name,
- blockHash,
- inputTransactionIDs[batchStart],
- batchEnd-batchStart,
- err,
+ g.Go(func() error {
+ responses, err := b.client.BatchCall(
+ ctx,
+ b.methods.getRawTransaction,
+ batchParams,
+ jsonrpc.WithOnAttempt(func(ctx context.Context, attempt int) {
+ opts.RecordHeartbeat(ctx, "fetchInputTx.batch", idx, attempt)
+ }),
)
+ if err != nil {
+ return xerrors.Errorf(
+ "failed to call %s for subset of (blockHash=%s, startTransactionID=%v, batchSize=%v): %w",
+ b.methods.getRawTransaction.Name,
+ blockHash,
+ inputTransactionIDs[batchStart],
+ batchEnd-batchStart,
+ err,
+ )
+ }
+ batchResults[idx] = responses
+ opts.RecordHeartbeat(ctx, "fetchInputTx.batch.done", idx)
+ return nil
+ })
+ }
+
+ if err := g.Wait(); err != nil {
+ return nil, err
+ }
+ opts.RecordHeartbeat(ctx, "fetchInputTx.done", numBatches)
+
+ // Merge batch results into a single map.
+ result := make(map[string][]byte, numTransactions)
+ for batchIdx, responses := range batchResults {
+ batchStart := batchIdx * txBatchSize
+ for respIdx, resp := range responses {
+ result[inputTransactionIDs[batchStart+respIdx]] = resp.Result
}
+ }
+
+ return result, nil
+}
- for respIndex, resp := range batchResponses {
- transactionID := inputTransactionIDs[batchStart+respIndex]
- inputTransactionsMap[transactionID] = resp.Result
+// parseInputTransactions unmarshals, validates, and indexes raw input
+// transactions. Each unique txid is parsed once and its vouts are indexed
+// by N for O(1) lookup. Replaces reflection-based validator.Struct with
+// direct checks to avoid per-call reflection overhead.
+func parseInputTransactions(rawMap map[string][]byte) (map[string]*parsedInputTx, error) {
+ parsed := make(map[string]*parsedInputTx, len(rawMap))
+ for txID, rawData := range rawMap {
+ var tx bitcoin.BitcoinInputTransactionLit
+ if err := json.Unmarshal(rawData, &tx); err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal input transaction %s: %w", txID, err)
}
+ if tx.TxId.Value() == "" {
+ return nil, xerrors.Errorf("failed to validate input transaction %s: txid is required", txID)
+ }
+ if len(tx.Vout) == 0 {
+ return nil, xerrors.Errorf("failed to validate input transaction %s: vout must have at least 1 element", txID)
+ }
+ vm := make(map[uint64]*bitcoin.BitcoinTransactionOutput, len(tx.Vout))
+ for _, o := range tx.Vout {
+ vm[o.N.Value()] = o
+ }
+ parsed[txID] = &parsedInputTx{tx: &tx, voutMap: vm}
}
+ return parsed, nil
+}
+// buildInputTransactionResults assembles per-vin filtered results with
+// marshal caching to avoid redundant work for shared (txid, vout) pairs.
+func buildInputTransactionResults(
+ transactions []*bitcoin.BitcoinTransactionLit,
+ parsedCache map[string]*parsedInputTx,
+ blockHash string,
+) ([][][]byte, error) {
+ // Build results, caching marshalled (txid, voutIndex) pairs.
+ marshalCache := make(map[string][]byte)
results := make([][][]byte, len(transactions))
for index, tx := range transactions {
- var inputTransactions [][]byte
if index == 0 {
// coinbase transaction
- inputTransactions = make([][]byte, 0)
- } else {
- inputTransactions = make([][]byte, len(tx.Inputs))
-
- for inputIndex, input := range tx.Inputs {
- inputID := input.Identifier.Value()
- rawTransaction, ok := inputTransactionsMap[inputID]
- if !ok {
- return nil, xerrors.Errorf(
- "input transaction id not found in map (blockHash=%s, transactionID=%v, inputTransactionID=%v)",
- blockHash,
- tx.Identifier,
- inputID,
- )
- }
-
- data, err := b.processInputTransactionRawData(rawTransaction, input.Vout.Value())
- if err != nil {
- return nil, xerrors.Errorf(
- "error processing input transaction data (blockHash=%s, transactionID=%v, inputTransactionID=%v): %w",
- blockHash,
- tx.Identifier,
- inputID,
- err,
- )
- }
-
- inputTransactions[inputIndex] = data
+ results[index] = make([][]byte, 0)
+ continue
+ }
+
+ inputTransactions := make([][]byte, len(tx.Inputs))
+ for inputIndex, input := range tx.Inputs {
+ inputID := input.Identifier.Value()
+ voutIdx := input.Vout.Value()
+ cacheKey := inputID + ":" + strconv.FormatUint(voutIdx, 10)
+
+ if cached, ok := marshalCache[cacheKey]; ok {
+ inputTransactions[inputIndex] = cached
+ continue
+ }
+
+ parsed, ok := parsedCache[inputID]
+ if !ok {
+ return nil, xerrors.Errorf(
+ "input transaction id not found in map (blockHash=%s, transactionID=%v, inputTransactionID=%v)",
+ blockHash, tx.Identifier, inputID,
+ )
+ }
+
+ output, ok := parsed.voutMap[voutIdx]
+ if !ok {
+ return nil, xerrors.Errorf(
+ "vout not found (blockHash=%s, transactionID=%v, inputTransactionID=%v, voutIndex=%d)",
+ blockHash, tx.Identifier, inputID, voutIdx,
+ )
+ }
+
+ filtered := bitcoin.BitcoinInputTransactionLit{
+ TxId: parsed.tx.TxId,
+ Vout: []*bitcoin.BitcoinTransactionOutput{output},
}
+ data, err := json.Marshal(&filtered)
+ if err != nil {
+ return nil, xerrors.Errorf(
+ "failed to marshal filtered input transaction (blockHash=%s, inputTransactionID=%v, voutIndex=%d): %w",
+ blockHash, inputID, voutIdx, err,
+ )
+ }
+
+ marshalCache[cacheKey] = data
+ inputTransactions[inputIndex] = data
}
results[index] = inputTransactions
}
@@ -395,13 +597,13 @@ func (b *bitcoinClient) getBlockHashesByHeights(ctx context.Context, from uint64
params[i] = jsonrpc.Params{height}
}
- responses, err := b.client.BatchCall(ctx, bitcoinGetBlockHashMethod, params)
+ responses, err := b.client.BatchCall(ctx, b.methods.getBlockHash, params)
if err != nil {
return nil, xerrors.Errorf("failed to get block hashes for heights: %w", err)
}
if numBlocks != len(responses) {
- return nil, xerrors.Errorf("missing block hashes in BatchCall to %s", bitcoinGetBlockHashMethod.Name)
+ return nil, xerrors.Errorf("missing block hashes in BatchCall to %s", b.methods.getBlockHash.Name)
}
blockHashes := make([]string, len(responses))
@@ -414,39 +616,3 @@ func (b *bitcoinClient) getBlockHashesByHeights(ctx context.Context, from uint64
}
return blockHashes, nil
}
-
-func (b *bitcoinClient) processInputTransactionRawData(data json.RawMessage, voutIndex uint64) ([]byte, error) {
- var transaction bitcoin.BitcoinInputTransactionLit
- if err := json.Unmarshal(data, &transaction); err != nil {
- return nil, xerrors.Errorf("failed to unmarshal data into bitcoin input transaction lite: %w", err)
- }
-
- if err := b.validate.Struct(transaction); err != nil {
- return nil, xerrors.Errorf("failed to validate bitcoin input transaction lite %+v: %w", transaction, err)
- }
-
- if voutIndex >= uint64(len(transaction.Vout)) {
- return nil, xerrors.Errorf("vout index out of bound (index=%d, len=%d)", voutIndex, len(transaction.Vout))
- }
-
- var output *bitcoin.BitcoinTransactionOutput
- for _, o := range transaction.Vout {
- if voutIndex == uint64(o.N) {
- output = o
- break
- }
- }
-
- if output == nil {
- return nil, xerrors.Errorf("vout not found for index: %d", voutIndex)
- }
-
- // ignore other outputs
- transaction.Vout = []*bitcoin.BitcoinTransactionOutput{output}
-
- result, err := json.Marshal(&transaction)
- if err != nil {
- return nil, xerrors.Errorf("failed to marshal input transaction data %v: %w", transaction, err)
- }
- return result, nil
-}
diff --git a/internal/blockchain/client/bitcoin/bitcoin_test.go b/internal/blockchain/client/bitcoin/bitcoin_test.go
index 5783004e..ce0d4a09 100644
--- a/internal/blockchain/client/bitcoin/bitcoin_test.go
+++ b/internal/blockchain/client/bitcoin/bitcoin_test.go
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"testing"
+ "time"
"github.com/stretchr/testify/suite"
"go.uber.org/fx"
@@ -15,6 +16,7 @@ import (
jsonrpcmocks "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc/mocks"
"github.com/coinbase/chainstorage/internal/blockchain/parser"
"github.com/coinbase/chainstorage/internal/blockchain/restapi"
+ "github.com/coinbase/chainstorage/internal/config"
"github.com/coinbase/chainstorage/internal/dlq"
"github.com/coinbase/chainstorage/internal/utils/fixtures"
"github.com/coinbase/chainstorage/internal/utils/testapp"
@@ -130,6 +132,39 @@ type bitcoinClientTestSuite struct {
client internal.Client
}
+func TestRpcMethodsOverrideFromConfig(t *testing.T) {
+ t.Run("NoOverrides", func(t *testing.T) {
+ require := testutil.Require(t)
+ cfg := &config.Config{}
+ methods := newRPCMethods(rpcMethodsOverrideFromConfig(cfg))
+ require.Equal(defaultGetBlockByHash.Timeout, methods.getBlockByHash.Timeout)
+ require.Equal(defaultGetRawTransaction.Timeout, methods.getRawTransaction.Timeout)
+ require.Equal(defaultGetBlockHash.Timeout, methods.getBlockHash.Timeout)
+ })
+
+ t.Run("OverrideGetRawTx", func(t *testing.T) {
+ require := testutil.Require(t)
+ cfg := &config.Config{}
+ cfg.Chain.Client.RpcTimeoutGetRawTx = 60 * time.Second
+ methods := newRPCMethods(rpcMethodsOverrideFromConfig(cfg))
+ require.Equal(60*time.Second, methods.getRawTransaction.Timeout)
+ require.Equal(defaultGetBlockByHash.Timeout, methods.getBlockByHash.Timeout)
+ require.Equal(defaultGetBlockHash.Timeout, methods.getBlockHash.Timeout)
+ })
+
+ t.Run("OverrideAll", func(t *testing.T) {
+ require := testutil.Require(t)
+ cfg := &config.Config{}
+ cfg.Chain.Client.RpcTimeoutGetBlock = 20 * time.Second
+ cfg.Chain.Client.RpcTimeoutGetRawTx = 60 * time.Second
+ cfg.Chain.Client.RpcTimeoutGetBlockHash = 10 * time.Second
+ methods := newRPCMethods(rpcMethodsOverrideFromConfig(cfg))
+ require.Equal(20*time.Second, methods.getBlockByHash.Timeout)
+ require.Equal(60*time.Second, methods.getRawTransaction.Timeout)
+ require.Equal(10*time.Second, methods.getBlockHash.Timeout)
+ })
+}
+
func TestBitcoinClientTestSuite(t *testing.T) {
suite.Run(t, new(bitcoinClientTestSuite))
}
@@ -184,7 +219,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHeight() {
Result: json.RawMessage(btcFixtureGetBlockHashResponse),
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockHash, jsonrpc.Params{
btcFixtureBlockHeight,
},
).Return(getBlockHashResponse, nil)
@@ -196,7 +231,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHeight() {
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -227,7 +262,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHeight() {
)
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetRawTransactionMethod, expectedParams,
+ gomock.Any(), defaultGetRawTransaction, expectedParams, gomock.Any(),
).Return(getRawTransactionResponse, nil)
block, err := s.client.GetBlockByHeight(context.Background(), btcTag, btcFixtureBlockHeight)
@@ -268,7 +303,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetGenesisBlockByHeight() {
Result: json.RawMessage(btcFixtureGenesisBlockHashResponse),
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockHash, jsonrpc.Params{
uint64(0),
},
).Return(getBlockHashResponse, nil)
@@ -278,7 +313,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetGenesisBlockByHeight() {
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureGenesisBlockHash,
bitcoinBlockVerbosity,
},
@@ -309,7 +344,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHeight_BlockOutOfRa
},
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockHash, jsonrpc.Params{
btcFixtureBlockHeight,
},
).Return(nil, getBlockHashResponse.Error)
@@ -324,7 +359,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHeight_BlockNotFoun
Result: json.RawMessage(btcFixtureGetBlockHashResponse),
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockHash, jsonrpc.Params{
btcFixtureBlockHeight,
},
).Return(getBlockHashResponse, nil)
@@ -336,7 +371,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHeight_BlockNotFoun
},
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -357,7 +392,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash() {
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -388,7 +423,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash() {
)
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetRawTransactionMethod, expectedParams,
+ gomock.Any(), defaultGetRawTransaction, expectedParams, gomock.Any(),
).Return(getRawTransactionResponse, nil)
block, err := s.client.GetBlockByHash(context.Background(), btcTag, btcFixtureBlockHeight, btcFixtureBlockHash)
@@ -431,7 +466,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_BlockNotFound(
},
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -452,7 +487,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_NoPreviousBloc
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -473,7 +508,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_NoHeight() {
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -494,7 +529,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_NoHash() {
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -515,7 +550,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_InconsistentHa
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -536,7 +571,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_GetInputTransa
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -550,7 +585,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_GetInputTransa
)
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetRawTransactionMethod, expectedParams,
+ gomock.Any(), defaultGetRawTransaction, expectedParams, gomock.Any(),
).Return(nil, xerrors.Errorf("error making http requests"))
block, err := s.client.GetBlockByHash(context.Background(), btcTag, btcFixtureBlockHeight, btcFixtureBlockHash)
@@ -568,7 +603,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_GetRawTransact
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -595,7 +630,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_GetRawTransact
)
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetRawTransactionMethod, expectedParams,
+ gomock.Any(), defaultGetRawTransaction, expectedParams, gomock.Any(),
).Return(getRawTransactionResponse, xerrors.Errorf("error making http requests"))
block, err := s.client.GetBlockByHash(context.Background(), btcTag, btcFixtureBlockHeight, btcFixtureBlockHash)
@@ -613,7 +648,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_NoTransactions
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -634,7 +669,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_InputTransacti
Error: nil,
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockByHashMethod, jsonrpc.Params{
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
btcFixtureBlockHash,
bitcoinBlockVerbosity,
},
@@ -660,7 +695,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_InputTransacti
)
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetRawTransactionMethod, expectedParams,
+ gomock.Any(), defaultGetRawTransaction, expectedParams, gomock.Any(),
).Return(getRawTransactionResponse, nil)
block, err := s.client.GetBlockByHash(context.Background(), btcTag, btcFixtureBlockHeight, btcFixtureBlockHash)
@@ -673,7 +708,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetLatestHeight() {
Result: json.RawMessage(btcFixtureGetBlockCountResponse),
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockCountMethod, jsonrpc.Params{},
+ gomock.Any(), defaultGetBlockCount, jsonrpc.Params{},
).Return(getBlockCountResponse, nil)
height, err := s.client.GetLatestHeight(context.Background())
@@ -689,7 +724,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetLatestHeight_ResponseError
},
}
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockCountMethod, jsonrpc.Params{},
+ gomock.Any(), defaultGetBlockCount, jsonrpc.Params{},
).Return(getBlockCountResponse, nil)
height, err := s.client.GetLatestHeight(context.Background())
@@ -699,7 +734,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_GetLatestHeight_ResponseError
func (s *bitcoinClientTestSuite) TestBitcoinClient_GetLatestHeight_CallError() {
s.rpcClient.EXPECT().Call(
- gomock.Any(), bitcoinGetBlockCountMethod, jsonrpc.Params{},
+ gomock.Any(), defaultGetBlockCount, jsonrpc.Params{},
).Return(nil, xerrors.Errorf("error making http request"))
height, err := s.client.GetLatestHeight(context.Background())
@@ -727,7 +762,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange(
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockHashMethod, getBlockHashesParams,
+ gomock.Any(), defaultGetBlockHash, getBlockHashesParams,
).Return(getBlockHashesResponses, nil)
hashes := []string{
@@ -756,7 +791,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange(
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockByHashMethod, getBlocksParams,
+ gomock.Any(), defaultGetBlockByHash, getBlocksParams,
).Return(getBlocksResponses, nil)
metadata, err := s.client.BatchGetBlockMetadata(context.Background(), btcTag, 101, 104)
@@ -783,7 +818,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockHashMethod, getBlockHashesParams,
+ gomock.Any(), defaultGetBlockHash, getBlockHashesParams,
).Return(nil, xerrors.Errorf("error making http request"))
metadata, err := s.client.BatchGetBlockMetadata(context.Background(), btcTag, 101, 104)
@@ -811,7 +846,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockHashMethod, getBlockHashesParams,
+ gomock.Any(), defaultGetBlockHash, getBlockHashesParams,
).Return(getBlockHashesResponses, nil)
hashes := []string{
@@ -828,7 +863,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockByHashMethod, getBlocksParams,
+ gomock.Any(), defaultGetBlockByHash, getBlocksParams,
).Return(nil, xerrors.Errorf("error getting blocks"))
metadata, err := s.client.BatchGetBlockMetadata(context.Background(), btcTag, 101, 104)
@@ -853,7 +888,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockHashMethod, getBlockHashesParams,
+ gomock.Any(), defaultGetBlockHash, getBlockHashesParams,
).Return(getBlockHashesResponses, nil)
metadata, err := s.client.BatchGetBlockMetadata(context.Background(), btcTag, 101, 104)
@@ -881,7 +916,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockHashMethod, getBlockHashesParams,
+ gomock.Any(), defaultGetBlockHash, getBlockHashesParams,
).Return(getBlockHashesResponses, nil)
hashes := []string{
@@ -907,7 +942,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockByHashMethod, getBlocksParams,
+ gomock.Any(), defaultGetBlockByHash, getBlocksParams,
).Return(getBlocksResponses, nil)
metadata, err := s.client.BatchGetBlockMetadata(context.Background(), btcTag, 101, 104)
@@ -935,7 +970,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockHashMethod, getBlockHashesParams,
+ gomock.Any(), defaultGetBlockHash, getBlockHashesParams,
).Return(getBlockHashesResponses, nil)
hashes := []string{
@@ -964,7 +999,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockByHashMethod, getBlocksParams,
+ gomock.Any(), defaultGetBlockByHash, getBlocksParams,
).Return(getBlocksResponses, nil)
metadata, err := s.client.BatchGetBlockMetadata(context.Background(), btcTag, 101, 104)
@@ -992,7 +1027,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockHashMethod, getBlockHashesParams,
+ gomock.Any(), defaultGetBlockHash, getBlockHashesParams,
).Return(getBlockHashesResponses, nil)
hashes := []string{
@@ -1021,7 +1056,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockByHashMethod, getBlocksParams,
+ gomock.Any(), defaultGetBlockByHash, getBlocksParams,
).Return(getBlocksResponses, nil)
metadata, err := s.client.BatchGetBlockMetadata(context.Background(), btcTag, 101, 104)
@@ -1049,7 +1084,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockHashMethod, getBlockHashesParams,
+ gomock.Any(), defaultGetBlockHash, getBlockHashesParams,
).Return(getBlockHashesResponses, nil)
hashes := []string{
@@ -1078,7 +1113,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockByHashMethod, getBlocksParams,
+ gomock.Any(), defaultGetBlockByHash, getBlocksParams,
).Return(getBlocksResponses, nil)
metadata, err := s.client.BatchGetBlockMetadata(context.Background(), btcTag, 101, 104)
@@ -1106,7 +1141,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockHashMethod, getBlockHashesParams,
+ gomock.Any(), defaultGetBlockHash, getBlockHashesParams,
).Return(getBlockHashesResponses, nil)
hashes := []string{
@@ -1135,7 +1170,7 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
}
s.rpcClient.EXPECT().BatchCall(
- gomock.Any(), bitcoinGetBlockByHashMethod, getBlocksParams,
+ gomock.Any(), defaultGetBlockByHash, getBlocksParams,
).Return(getBlocksResponses, nil)
metadata, err := s.client.BatchGetBlockMetadata(context.Background(), btcTag, 101, 104)
@@ -1143,6 +1178,207 @@ func (s *bitcoinClientTestSuite) TestBitcoinClient_BatchGetBlockMetadataByRange_
s.Nil(metadata)
}
+// TestBitcoinClient_GetBlockByHash_SharedParentTxid verifies that when multiple
+// inputs across different transactions reference the same parent txid (but different
+// vout indices), the parent is only fetched once and each input gets the correct
+// filtered vout in its output.
+func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_SharedParentTxid() {
+ blockHash := "00000000000000000001blockhash"
+ parentTxA := "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111"
+ parentTxB := "bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222bbbb2222"
+
+ // Block: coinbase + tx1 (input: parentA:0) + tx2 (inputs: parentA:1, parentB:0)
+ blockJSON := json.RawMessage(`{
+ "hash": "` + blockHash + `",
+ "previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000",
+ "height": 100,
+ "time": 1629306034,
+ "tx": [
+ {"txid": "coinbasetx", "vin": [{"coinbase": "00"}]},
+ {"txid": "tx1", "vin": [{"txid": "` + parentTxA + `", "vout": 0}]},
+ {"txid": "tx2", "vin": [{"txid": "` + parentTxA + `", "vout": 1}, {"txid": "` + parentTxB + `", "vout": 0}]}
+ ]
+ }`)
+
+ // parentA has 2 vouts
+ parentARespJSON := json.RawMessage(`{
+ "txid": "` + parentTxA + `",
+ "vout": [
+ {"value": 1.0, "n": 0, "scriptPubKey": {"asm": "a0", "hex": "a0", "type": "pubkeyhash", "address": "addrA0"}},
+ {"value": 2.0, "n": 1, "scriptPubKey": {"asm": "a1", "hex": "a1", "type": "pubkeyhash", "address": "addrA1"}}
+ ]
+ }`)
+ // parentB has 1 vout
+ parentBRespJSON := json.RawMessage(`{
+ "txid": "` + parentTxB + `",
+ "vout": [
+ {"value": 3.0, "n": 0, "scriptPubKey": {"asm": "b0", "hex": "b0", "type": "pubkeyhash", "address": "addrB0"}}
+ ]
+ }`)
+
+ s.rpcClient.EXPECT().Call(
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{blockHash, bitcoinBlockVerbosity},
+ ).Return(&jsonrpc.Response{Result: blockJSON}, nil)
+
+ // Only 2 unique parent txids should be fetched (parentA, parentB) in one batch
+ s.rpcClient.EXPECT().BatchCall(
+ gomock.Any(), defaultGetRawTransaction,
+ []jsonrpc.Params{{parentTxA, true}, {parentTxB, true}}, gomock.Any(),
+ ).Return([]*jsonrpc.Response{
+ {Result: parentARespJSON},
+ {Result: parentBRespJSON},
+ }, nil)
+
+ block, err := s.client.GetBlockByHash(context.Background(), btcTag, 100, blockHash)
+ s.NoError(err)
+
+ blobdata := block.GetBitcoin()
+ s.NotNil(blobdata)
+ s.Equal(3, len(blobdata.InputTransactions))
+
+ // tx[0] = coinbase -> empty
+ s.Equal(0, len(blobdata.InputTransactions[0].Data))
+
+ // tx[1] = 1 input (parentA:0) -> 1 entry with vout[0]
+ s.Equal(1, len(blobdata.InputTransactions[1].Data))
+ s.JSONEq(`{"txid":"`+parentTxA+`","vout":[{"value":1.0,"n":0,"scriptPubKey":{"asm":"a0","hex":"a0","reqSigs":0,"type":"pubkeyhash","addresses":null,"address":"addrA0"}}]}`,
+ string(blobdata.InputTransactions[1].Data[0]))
+
+ // tx[2] = 2 inputs (parentA:1, parentB:0)
+ s.Equal(2, len(blobdata.InputTransactions[2].Data))
+ s.JSONEq(`{"txid":"`+parentTxA+`","vout":[{"value":2.0,"n":1,"scriptPubKey":{"asm":"a1","hex":"a1","reqSigs":0,"type":"pubkeyhash","addresses":null,"address":"addrA1"}}]}`,
+ string(blobdata.InputTransactions[2].Data[0]))
+ s.JSONEq(`{"txid":"`+parentTxB+`","vout":[{"value":3.0,"n":0,"scriptPubKey":{"asm":"b0","hex":"b0","reqSigs":0,"type":"pubkeyhash","addresses":null,"address":"addrB0"}}]}`,
+ string(blobdata.InputTransactions[2].Data[1]))
+}
+
+// TestBitcoinClient_GetBlockByHash_DuplicateTxidVout verifies that when two inputs
+// in different transactions reference the exact same (txid, vout) pair, both get
+// identical output data.
+func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_DuplicateTxidVout() {
+ blockHash := "00000000000000000002blockhash"
+ parentTxA := "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111"
+
+ blockJSON := json.RawMessage(`{
+ "hash": "` + blockHash + `",
+ "previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000",
+ "height": 200,
+ "time": 1629306034,
+ "tx": [
+ {"txid": "coinbasetx", "vin": [{"coinbase": "00"}]},
+ {"txid": "tx1", "vin": [{"txid": "` + parentTxA + `", "vout": 0}]},
+ {"txid": "tx2", "vin": [{"txid": "` + parentTxA + `", "vout": 0}]}
+ ]
+ }`)
+
+ parentARespJSON := json.RawMessage(`{
+ "txid": "` + parentTxA + `",
+ "vout": [
+ {"value": 5.0, "n": 0, "scriptPubKey": {"asm": "a0", "hex": "a0", "type": "pubkeyhash", "address": "addrA0"}}
+ ]
+ }`)
+
+ s.rpcClient.EXPECT().Call(
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{blockHash, bitcoinBlockVerbosity},
+ ).Return(&jsonrpc.Response{Result: blockJSON}, nil)
+
+ s.rpcClient.EXPECT().BatchCall(
+ gomock.Any(), defaultGetRawTransaction,
+ []jsonrpc.Params{{parentTxA, true}}, gomock.Any(),
+ ).Return([]*jsonrpc.Response{
+ {Result: parentARespJSON},
+ }, nil)
+
+ block, err := s.client.GetBlockByHash(context.Background(), btcTag, 200, blockHash)
+ s.NoError(err)
+
+ blobdata := block.GetBitcoin()
+ s.Equal(3, len(blobdata.InputTransactions))
+ s.Equal(0, len(blobdata.InputTransactions[0].Data))
+ s.Equal(1, len(blobdata.InputTransactions[1].Data))
+ s.Equal(1, len(blobdata.InputTransactions[2].Data))
+
+ // Both should produce identical output
+ s.JSONEq(string(blobdata.InputTransactions[1].Data[0]), string(blobdata.InputTransactions[2].Data[0]))
+ s.JSONEq(`{"txid":"`+parentTxA+`","vout":[{"value":5.0,"n":0,"scriptPubKey":{"asm":"a0","hex":"a0","reqSigs":0,"type":"pubkeyhash","addresses":null,"address":"addrA0"}}]}`,
+ string(blobdata.InputTransactions[1].Data[0]))
+}
+
+// TestBitcoinClient_GetBlockByHash_OutputShapeAndOrder verifies the strict contract:
+// - results outer length == len(transactions)
+// - coinbase (index 0) always has empty Data
+// - each input maps to one entry with exactly one vout
+// - entry order matches vin order within each transaction
+func (s *bitcoinClientTestSuite) TestBitcoinClient_GetBlockByHash_OutputShapeAndOrder() {
+ blockHash := "00000000000000000003blockhash"
+ parentTxA := "aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111aaaa1111"
+
+ // tx2 has 3 inputs from the same parent, referencing vouts 2, 0, 1 (out of order)
+ blockJSON := json.RawMessage(`{
+ "hash": "` + blockHash + `",
+ "previousblockhash": "0000000000000000000000000000000000000000000000000000000000000000",
+ "height": 300,
+ "time": 1629306034,
+ "tx": [
+ {"txid": "coinbasetx", "vin": [{"coinbase": "00"}]},
+ {"txid": "tx2", "vin": [
+ {"txid": "` + parentTxA + `", "vout": 2},
+ {"txid": "` + parentTxA + `", "vout": 0},
+ {"txid": "` + parentTxA + `", "vout": 1}
+ ]}
+ ]
+ }`)
+
+ parentARespJSON := json.RawMessage(`{
+ "txid": "` + parentTxA + `",
+ "vout": [
+ {"value": 10.0, "n": 0, "scriptPubKey": {"asm": "v0", "hex": "v0", "type": "pubkeyhash", "address": "addr0"}},
+ {"value": 20.0, "n": 1, "scriptPubKey": {"asm": "v1", "hex": "v1", "type": "pubkeyhash", "address": "addr1"}},
+ {"value": 30.0, "n": 2, "scriptPubKey": {"asm": "v2", "hex": "v2", "type": "pubkeyhash", "address": "addr2"}}
+ ]
+ }`)
+
+ s.rpcClient.EXPECT().Call(
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{blockHash, bitcoinBlockVerbosity},
+ ).Return(&jsonrpc.Response{Result: blockJSON}, nil)
+
+ s.rpcClient.EXPECT().BatchCall(
+ gomock.Any(), defaultGetRawTransaction,
+ []jsonrpc.Params{{parentTxA, true}}, gomock.Any(),
+ ).Return([]*jsonrpc.Response{
+ {Result: parentARespJSON},
+ }, nil)
+
+ block, err := s.client.GetBlockByHash(context.Background(), btcTag, 300, blockHash)
+ s.NoError(err)
+
+ blobdata := block.GetBitcoin()
+ s.Require().Equal(2, len(blobdata.InputTransactions))
+
+ // coinbase
+ s.Equal(0, len(blobdata.InputTransactions[0].Data))
+
+ // tx2: 3 inputs, order must match vin order (vout 2, 0, 1)
+ s.Require().Equal(3, len(blobdata.InputTransactions[1].Data))
+
+ // Each entry must have exactly 1 vout
+ for i, data := range blobdata.InputTransactions[1].Data {
+ var parsed map[string]json.RawMessage
+ s.NoError(json.Unmarshal(data, &parsed))
+ var vouts []json.RawMessage
+ s.NoError(json.Unmarshal(parsed["vout"], &vouts))
+ s.Equal(1, len(vouts), "entry %d should have exactly 1 vout", i)
+ }
+
+ // Verify order: first entry is vout 2 (value 30), second is vout 0 (value 10), third is vout 1 (value 20)
+ s.JSONEq(`{"txid":"`+parentTxA+`","vout":[{"value":30.0,"n":2,"scriptPubKey":{"asm":"v2","hex":"v2","reqSigs":0,"type":"pubkeyhash","addresses":null,"address":"addr2"}}]}`,
+ string(blobdata.InputTransactions[1].Data[0]))
+ s.JSONEq(`{"txid":"`+parentTxA+`","vout":[{"value":10.0,"n":0,"scriptPubKey":{"asm":"v0","hex":"v0","reqSigs":0,"type":"pubkeyhash","addresses":null,"address":"addr0"}}]}`,
+ string(blobdata.InputTransactions[1].Data[1]))
+ s.JSONEq(`{"txid":"`+parentTxA+`","vout":[{"value":20.0,"n":1,"scriptPubKey":{"asm":"v1","hex":"v1","reqSigs":0,"type":"pubkeyhash","addresses":null,"address":"addr1"}}]}`,
+ string(blobdata.InputTransactions[1].Data[2]))
+}
+
func testModule(client *jsonrpcmocks.MockClient) fx.Option {
return fx.Options(
internal.Module,
diff --git a/internal/blockchain/client/bitcoin/dash.go b/internal/blockchain/client/bitcoin/dash.go
new file mode 100644
index 00000000..08b15c4b
--- /dev/null
+++ b/internal/blockchain/client/bitcoin/dash.go
@@ -0,0 +1,23 @@
+package bitcoin
+
+import (
+ "github.com/go-playground/validator/v10"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+ "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
+ "github.com/coinbase/chainstorage/internal/utils/log"
+)
+
+func NewDashClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ return internal.NewJsonrpcClientFactory(params, func(client jsonrpc.Client) internal.Client {
+ logger := log.WithPackage(params.Logger)
+ return &bitcoinClient{
+ config: params.Config,
+ logger: logger,
+ client: client,
+ validate: validator.New(),
+ methods: newRPCMethods(rpcMethodsOverrideFromConfig(params.Config)),
+ preserveRawInputTransactions: true,
+ }
+ })
+}
diff --git a/internal/blockchain/client/bitcoin/dash_test.go b/internal/blockchain/client/bitcoin/dash_test.go
new file mode 100644
index 00000000..55a98d6a
--- /dev/null
+++ b/internal/blockchain/client/bitcoin/dash_test.go
@@ -0,0 +1,251 @@
+package bitcoin
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/fx"
+ "go.uber.org/mock/gomock"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+ "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
+ jsonrpcmocks "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc/mocks"
+ "github.com/coinbase/chainstorage/internal/utils/fixtures"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+)
+
+type dashClientTestSuite struct {
+ suite.Suite
+
+ ctrl *gomock.Controller
+ testapp testapp.TestApp
+ rpcClient *jsonrpcmocks.MockClient
+ client internal.Client
+}
+
+func TestDashClientTestSuite(t *testing.T) {
+ suite.Run(t, new(dashClientTestSuite))
+}
+
+func (s *dashClientTestSuite) SetupTest() {
+ s.ctrl = gomock.NewController(s.T())
+ s.rpcClient = jsonrpcmocks.NewMockClient(s.ctrl)
+
+ var result internal.ClientParams
+ s.testapp = testapp.New(
+ s.T(),
+ Module,
+ testModule(s.rpcClient),
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_DASH, common.Network_NETWORK_DASH_MAINNET),
+ fx.Populate(&result),
+ )
+
+ s.client = result.Master
+ s.NotNil(s.client)
+}
+
+func (s *dashClientTestSuite) TearDownTest() {
+ s.testapp.Close()
+ s.ctrl.Finish()
+}
+
+func (s *dashClientTestSuite) TestDashClient_GetBlockByHeight() {
+ require := testutil.Require(s.T())
+
+ getBlockHashResponse := &jsonrpc.Response{
+ Result: json.RawMessage(btcFixtureGetBlockHashResponse),
+ }
+ s.rpcClient.EXPECT().Call(
+ gomock.Any(), defaultGetBlockHash, jsonrpc.Params{
+ btcFixtureBlockHeight,
+ },
+ ).Return(getBlockHashResponse, nil)
+
+ response, err := fixtures.ReadFile("client/bitcoin/btc_getblockresponse.json")
+ require.NoError(err)
+ getBlockByHashResponse := &jsonrpc.Response{
+ Result: response,
+ Error: nil,
+ }
+ s.rpcClient.EXPECT().Call(
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
+ btcFixtureBlockHash,
+ bitcoinBlockVerbosity,
+ },
+ ).Return(getBlockByHashResponse, nil)
+
+ resp1, err := fixtures.ReadFile("client/bitcoin/btc_getinputtx1_resp.json")
+ require.NoError(err)
+ resp2, err := fixtures.ReadFile("client/bitcoin/btc_getinputtx2_resp.json")
+ require.NoError(err)
+ resp3, err := fixtures.ReadFile("client/bitcoin/btc_getinputtx3_resp.json")
+ require.NoError(err)
+ getRawTransactionResponse := []*jsonrpc.Response{
+ {Result: resp1},
+ {Result: resp2},
+ {Result: resp3},
+ }
+ var expectedParams []jsonrpc.Params
+ for _, id := range btcFixtureInputTransactionIDs {
+ expectedParams = append(
+ expectedParams,
+ jsonrpc.Params{id, true},
+ )
+ }
+ s.rpcClient.EXPECT().BatchCall(
+ gomock.Any(), defaultGetRawTransaction, expectedParams, gomock.Any(),
+ ).Return(getRawTransactionResponse, nil)
+
+ block, err := s.client.GetBlockByHeight(context.Background(), btcTag, btcFixtureBlockHeight)
+ s.NoError(err)
+
+ s.Equal(common.Blockchain_BLOCKCHAIN_DASH, block.Blockchain)
+ s.Equal(common.Network_NETWORK_DASH_MAINNET, block.Network)
+ s.Equal(btcTag, block.Metadata.Tag)
+ s.Equal(btcFixtureBlockHash, block.Metadata.Hash)
+ s.Equal(btcFixturePreviousBlockHash, block.Metadata.ParentHash)
+ s.Equal(btcFixtureBlockHeight, block.Metadata.Height)
+ s.Equal(btcFixtureBlockTime, block.Metadata.Timestamp.GetSeconds())
+
+ blobdata := block.GetBitcoin()
+ s.NotNil(blobdata)
+ s.NotNil(blobdata.Header)
+
+ // Verify processed input transactions (same as bitcoin)
+ s.NotNil(blobdata.InputTransactions)
+ s.Equal(3, len(blobdata.InputTransactions))
+ s.Equal(0, len(blobdata.InputTransactions[0].Data))
+ s.Equal(1, len(blobdata.InputTransactions[1].Data))
+ s.Equal(2, len(blobdata.InputTransactions[2].Data))
+
+ data1, err := fixtures.ReadFile("client/bitcoin/btc_getinputtx1_data.json")
+ require.NoError(err)
+ s.JSONEq(string(data1), string(blobdata.InputTransactions[1].Data[0]))
+
+ data2, err := fixtures.ReadFile("client/bitcoin/btc_getinputtx2_data.json")
+ require.NoError(err)
+ s.JSONEq(string(data2), string(blobdata.InputTransactions[2].Data[0]))
+
+ data3, err := fixtures.ReadFile("client/bitcoin/btc_getinputtx3_data.json")
+ require.NoError(err)
+ s.JSONEq(string(data3), string(blobdata.InputTransactions[2].Data[1]))
+
+ // Verify raw input transactions (Dash-specific: full raw JSON preserved)
+ s.NotNil(blobdata.RawInputTransactions)
+ s.Equal(3, len(blobdata.RawInputTransactions))
+ // Each entry is the full getrawtransaction response keyed by txid
+ s.JSONEq(string(resp1), string(blobdata.RawInputTransactions[btcFixtureInputTransactionIDs[0]]))
+ s.JSONEq(string(resp2), string(blobdata.RawInputTransactions[btcFixtureInputTransactionIDs[1]]))
+ s.JSONEq(string(resp3), string(blobdata.RawInputTransactions[btcFixtureInputTransactionIDs[2]]))
+}
+
+func (s *dashClientTestSuite) TestDashClient_GetGenesisBlockByHeight() {
+ getBlockHashResponse := &jsonrpc.Response{
+ Result: json.RawMessage(btcFixtureGenesisBlockHashResponse),
+ }
+ s.rpcClient.EXPECT().Call(
+ gomock.Any(), defaultGetBlockHash, jsonrpc.Params{
+ uint64(0),
+ },
+ ).Return(getBlockHashResponse, nil)
+
+ getBlockByHashResponse := &jsonrpc.Response{
+ Result: json.RawMessage(btcFixtureGetGenesisBlockResponse),
+ Error: nil,
+ }
+ s.rpcClient.EXPECT().Call(
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
+ btcFixtureGenesisBlockHash,
+ bitcoinBlockVerbosity,
+ },
+ ).Return(getBlockByHashResponse, nil)
+
+ block, err := s.client.GetBlockByHeight(context.Background(), btcTag, 0)
+ s.NoError(err)
+
+ s.Equal(common.Blockchain_BLOCKCHAIN_DASH, block.Blockchain)
+ s.Equal(common.Network_NETWORK_DASH_MAINNET, block.Network)
+
+ blobdata := block.GetBitcoin()
+ s.NotNil(blobdata)
+ s.NotNil(blobdata.Header)
+ s.Equal(1, len(blobdata.InputTransactions))
+ s.Equal(0, len(blobdata.RawInputTransactions))
+}
+
+func (s *dashClientTestSuite) TestDashClient_GetBlockByHash() {
+ require := testutil.Require(s.T())
+
+ response, err := fixtures.ReadFile("client/bitcoin/btc_getblockresponse.json")
+ require.NoError(err)
+ getBlockByHashResponse := &jsonrpc.Response{
+ Result: response,
+ Error: nil,
+ }
+ s.rpcClient.EXPECT().Call(
+ gomock.Any(), defaultGetBlockByHash, jsonrpc.Params{
+ btcFixtureBlockHash,
+ bitcoinBlockVerbosity,
+ },
+ ).Return(getBlockByHashResponse, nil)
+
+ resp1, err := fixtures.ReadFile("client/bitcoin/btc_getinputtx1_resp.json")
+ require.NoError(err)
+ resp2, err := fixtures.ReadFile("client/bitcoin/btc_getinputtx2_resp.json")
+ require.NoError(err)
+ resp3, err := fixtures.ReadFile("client/bitcoin/btc_getinputtx3_resp.json")
+ require.NoError(err)
+ getRawTransactionResponse := []*jsonrpc.Response{
+ {Result: resp1},
+ {Result: resp2},
+ {Result: resp3},
+ }
+ var expectedParams []jsonrpc.Params
+ for _, id := range btcFixtureInputTransactionIDs {
+ expectedParams = append(
+ expectedParams,
+ jsonrpc.Params{id, true},
+ )
+ }
+ s.rpcClient.EXPECT().BatchCall(
+ gomock.Any(), defaultGetRawTransaction, expectedParams, gomock.Any(),
+ ).Return(getRawTransactionResponse, nil)
+
+ block, err := s.client.GetBlockByHash(context.Background(), btcTag, btcFixtureBlockHeight, btcFixtureBlockHash)
+ s.NoError(err)
+
+ s.Equal(common.Blockchain_BLOCKCHAIN_DASH, block.Blockchain)
+ s.Equal(common.Network_NETWORK_DASH_MAINNET, block.Network)
+ s.Equal(btcTag, block.Metadata.Tag)
+ s.Equal(btcFixtureBlockHash, block.Metadata.Hash)
+
+ blobdata := block.GetBitcoin()
+ s.NotNil(blobdata)
+ s.NotNil(blobdata.Header)
+ s.NotNil(blobdata.InputTransactions)
+ s.Equal(3, len(blobdata.InputTransactions))
+
+ // Verify raw input transactions are preserved
+ s.NotNil(blobdata.RawInputTransactions)
+ s.Equal(3, len(blobdata.RawInputTransactions))
+ s.JSONEq(string(resp1), string(blobdata.RawInputTransactions[btcFixtureInputTransactionIDs[0]]))
+ s.JSONEq(string(resp2), string(blobdata.RawInputTransactions[btcFixtureInputTransactionIDs[1]]))
+ s.JSONEq(string(resp3), string(blobdata.RawInputTransactions[btcFixtureInputTransactionIDs[2]]))
+}
+
+func (s *dashClientTestSuite) TestDashClient_GetLatestHeight() {
+ getBlockCountResponse := &jsonrpc.Response{
+ Result: json.RawMessage(btcFixtureGetBlockCountResponse),
+ }
+ s.rpcClient.EXPECT().Call(
+ gomock.Any(), defaultGetBlockCount, jsonrpc.Params{},
+ ).Return(getBlockCountResponse, nil)
+
+ height, err := s.client.GetLatestHeight(context.Background())
+ s.NoError(err)
+ s.Equal(uint64(697413), height)
+}
diff --git a/internal/blockchain/client/bitcoin/module.go b/internal/blockchain/client/bitcoin/module.go
index 17733363..8855bbf4 100644
--- a/internal/blockchain/client/bitcoin/module.go
+++ b/internal/blockchain/client/bitcoin/module.go
@@ -7,4 +7,12 @@ var Module = fx.Options(
Name: "bitcoin",
Target: NewBitcoinClientFactory,
}),
+ fx.Provide(fx.Annotated{
+ Name: "dash",
+ Target: NewDashClientFactory,
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "zcash",
+ Target: NewZcashClientFactory,
+ }),
)
diff --git a/internal/blockchain/client/bitcoin/zcash.go b/internal/blockchain/client/bitcoin/zcash.go
new file mode 100644
index 00000000..7e5f6a30
--- /dev/null
+++ b/internal/blockchain/client/bitcoin/zcash.go
@@ -0,0 +1,31 @@
+package bitcoin
+
+import (
+ "time"
+
+ "github.com/go-playground/validator/v10"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+ "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
+ "github.com/coinbase/chainstorage/internal/utils/log"
+)
+
+func NewZcashClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ return internal.NewJsonrpcClientFactory(params, func(client jsonrpc.Client) internal.Client {
+ logger := log.WithPackage(params.Logger)
+ return &bitcoinClient{
+ config: params.Config,
+ logger: logger,
+ client: client,
+ validate: validator.New(),
+ methods: newRPCMethods(
+ rpcMethodsOverride{rpcMethodGetRawTransaction: &jsonrpc.RequestMethod{Name: defaultGetRawTransaction.Name, Timeout: 10 * time.Second}},
+ rpcMethodsOverrideFromConfig(params.Config),
+ ),
+ preserveRawInputTransactions: true,
+ getRawTxParams: func(txid string, _ string) jsonrpc.Params {
+ return jsonrpc.Params{txid, 1}
+ },
+ }
+ })
+}
diff --git a/internal/blockchain/client/ethereum/abstract.go b/internal/blockchain/client/ethereum/abstract.go
new file mode 100644
index 00000000..671787a7
--- /dev/null
+++ b/internal/blockchain/client/ethereum/abstract.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+)
+
+func NewAbstractClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ // Reuse the Ethereum client factory since it is an EVM chain.
+ return NewEthereumClientFactory(params)
+}
diff --git a/internal/blockchain/client/ethereum/ethereum.go b/internal/blockchain/client/ethereum/ethereum.go
index 2afe1d7e..2304ab5f 100644
--- a/internal/blockchain/client/ethereum/ethereum.go
+++ b/internal/blockchain/client/ethereum/ethereum.go
@@ -14,6 +14,7 @@ import (
"github.com/uber-go/tally/v4"
"go.uber.org/zap"
"golang.org/x/xerrors"
+ "google.golang.org/protobuf/types/known/timestamppb"
"github.com/coinbase/chainstorage/internal/blockchain/client/internal"
"github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
@@ -34,12 +35,18 @@ type (
config *config.Config
logger *zap.Logger
client jsonrpc.Client
+ tracer EthereumBlockTracer
dlq dlq.DLQ
validate *validator.Validate
metrics *ethereumClientMetrics
nodeType types.EthereumNodeType
traceType types.TraceType
commitmentLevel types.CommitmentLevel
+ timestampInMs bool // If true, raw timestamps are in milliseconds and need to be converted to seconds
+ }
+
+ EthereumBlockTracer interface {
+ getBlockTraces(ctx context.Context, tag uint32, block *ethereum.EthereumBlockLit) ([][]byte, error)
}
EthereumClientOption func(client *EthereumClient)
@@ -47,6 +54,7 @@ type (
ethereumClientMetrics struct {
traceBlockSuccessCounter tally.Counter
traceBlockServerErrorCounter tally.Counter
+ traceBlockFallbackCounter tally.Counter
traceBlockExecutionTimeoutCounter tally.Counter
traceBlockFakeCounter tally.Counter
traceTransactionFakeCounter tally.Counter
@@ -104,6 +112,13 @@ const (
optimismWhitelistError = "TypeError: cannot read property 'toString' of undefined in server-side tracer function 'result'"
optimismFakeTraceError = "Creating fake trace for failed transaction."
unfinalizedDataError = "cannot query unfinalized data"
+
+ // On Arbitrum Nitro based chains, a transaction terminated by ArbOS before EVM execution
+ // (e.g. included by the sequencer but failed pre-checks) produces zero call frames, while
+ // go-ethereum's callTracer requires exactly one top-level frame per transaction.
+ // See https://github.com/OffchainLabs/go-ethereum/blob/master/eth/tracers/native/call.go
+ incorrectTopLevelCallsError = "incorrect number of top-level calls"
+ robinhoodFakeTraceError = "Creating fake trace for transaction terminated by ArbOS before EVM execution."
)
var ethBlacklistedRanges = map[common.Network][]types.BlockRange{
@@ -282,6 +297,21 @@ func WithEthereumCommitmentLevel(commitmentLevel types.CommitmentLevel) Ethereum
}
}
+func WithEthereumTimestampInMs() EthereumClientOption {
+ return func(client *EthereumClient) {
+ client.timestampInMs = true
+ }
+}
+
+// toTimestamp converts a raw block timestamp to a protobuf Timestamp.
+// If timestampInMs is true, preserves millisecond precision in the Nanos field.
+func (c *EthereumClient) toTimestamp(rawTimestamp int64) *timestamppb.Timestamp {
+ if c.timestampInMs {
+ return utils.ToTimestampFromMs(rawTimestamp)
+ }
+ return utils.ToTimestamp(rawTimestamp)
+}
+
func newEthereumClientMetrics(scope tally.Scope) *ethereumClientMetrics {
scope = scope.SubScope(subScope)
@@ -297,6 +327,9 @@ func newEthereumClientMetrics(scope tally.Scope) *ethereumClientMetrics {
traceBlockFakeCounter := scope.
Tagged(map[string]string{resultType: "fake_trace"}).
Counter(traceBlockCounter)
+ traceBlockFallbackCounter := scope.
+ Tagged(map[string]string{resultType: "fallback"}).
+ Counter(traceBlockCounter)
traceTransactionSuccessCounter := scope.
Tagged(map[string]string{resultType: "success"}).
@@ -317,6 +350,7 @@ func newEthereumClientMetrics(scope tally.Scope) *ethereumClientMetrics {
traceTransactionSuccessCounter: traceTransactionSuccessCounter,
traceTransactionIgnoredCounter: traceTransactionIgnoredCounter,
traceBlockFakeCounter: traceBlockFakeCounter,
+ traceBlockFallbackCounter: traceBlockFallbackCounter,
traceTransactionFakeCounter: traceTransactionFakeCounter,
transactionReceiptFakeCounter: transactionReceiptFakeCounter,
}
@@ -408,7 +442,7 @@ func (c *EthereumClient) batchGetBlockMetadata(ctx context.Context, tag uint32,
ParentHeight: internal.GetParentHeight(height),
Hash: headerResult.header.Hash.Value(),
ParentHash: headerResult.header.ParentHash.Value(),
- Timestamp: utils.ToTimestamp(int64(headerResult.header.Timestamp.Value())),
+ Timestamp: c.toTimestamp(int64(headerResult.header.Timestamp.Value())),
}
}
@@ -490,8 +524,13 @@ func (c *EthereumClient) getBlockFromHeader(ctx context.Context, tag uint32, hea
if err != nil {
return nil, xerrors.Errorf("failed to fetch transaction receipts for block %v: %w", height, err)
}
-
- transactionTraces, err := c.getBlockTraces(ctx, tag, headerResult.header)
+ var tracer EthereumBlockTracer
+ if c.tracer != nil {
+ tracer = c.tracer
+ } else {
+ tracer = c
+ }
+ transactionTraces, err := tracer.getBlockTraces(ctx, tag, headerResult.header)
if err != nil {
return nil, xerrors.Errorf("failed to fetch traces for block %v: %w", height, err)
}
@@ -510,7 +549,7 @@ func (c *EthereumClient) getBlockFromHeader(ctx context.Context, tag uint32, hea
ParentHeight: internal.GetParentHeight(headerResult.header.Number.Value()),
Hash: headerResult.header.Hash.Value(),
ParentHash: headerResult.header.ParentHash.Value(),
- Timestamp: utils.ToTimestamp(int64(headerResult.header.Timestamp.Value())),
+ Timestamp: c.toTimestamp(int64(headerResult.header.Timestamp.Value())),
},
Blobdata: &api.Block_Ethereum{
Ethereum: &api.EthereumBlobdata{
@@ -680,8 +719,13 @@ func (c *EthereumClient) getBlockTransactionReceipts(ctx context.Context, block
numTransactions := len(block.Transactions)
responses := make([]*jsonrpc.Response, numTransactions)
- for batchStart := 0; batchStart < numTransactions; batchStart += ethBlockTransactionReceiptBatchSize {
- batchEnd := batchStart + ethBlockTransactionReceiptBatchSize
+ receiptBatchSize := c.config.Chain.Client.TxBatchSize
+ if receiptBatchSize <= 0 {
+ receiptBatchSize = ethBlockTransactionReceiptBatchSize
+ }
+
+ for batchStart := 0; batchStart < numTransactions; batchStart += receiptBatchSize {
+ batchEnd := batchStart + receiptBatchSize
if batchEnd > numTransactions {
batchEnd = numTransactions
}
@@ -852,6 +896,20 @@ func (c *EthereumClient) getBlockTraces(ctx context.Context, tag uint32, block *
return response, nil
})
if err != nil {
+ if c.config.Blockchain() == common.Blockchain_BLOCKCHAIN_ROBINHOOD && isIncorrectTopLevelCallsError(err) {
+ // A transaction terminated by ArbOS before EVM execution produces zero call
+ // frames, which fails the whole block-level callTracer request. Fall back to
+ // per-transaction tracing so the poisoned transaction gets a fake trace via
+ // processFailedTransactionTrace while the rest keep their real traces.
+ c.logger.Warn(
+ "block trace failed with incorrect number of top-level calls; falling back to transaction traces",
+ zap.Uint64("height", height),
+ zap.String("hash", hash),
+ )
+ c.metrics.traceBlockFallbackCounter.Inc(1)
+ return c.getTransactionTraces(ctx, tag, block)
+ }
+
c.metrics.traceBlockServerErrorCounter.Inc(1)
return nil, err
}
@@ -1234,8 +1292,13 @@ func (c *EthereumClient) UpgradeBlock(ctx context.Context, block *api.Block, new
if err != nil {
return nil, xerrors.Errorf("failed to fetch header result for block %v: %w", height, err)
}
-
- transactionTraces, err := c.getBlockTraces(ctx, newTag, headerResult.header)
+ var tracer EthereumBlockTracer
+ if c.tracer != nil {
+ tracer = c.tracer
+ } else {
+ tracer = c
+ }
+ transactionTraces, err := tracer.getBlockTraces(ctx, newTag, headerResult.header)
if err != nil {
return nil, xerrors.Errorf("failed to fetch traces for block %v: %w", height, err)
}
@@ -1346,8 +1409,27 @@ func (c *EthereumClient) isServerError(err error) bool {
return false
}
+// isIncorrectTopLevelCallsError matches the deterministic go-ethereum callTracer error
+// returned when a transaction yields a number of top-level call frames other than one.
+func isIncorrectTopLevelCallsError(err error) bool {
+ var rpcErr *jsonrpc.RPCError
+ return xerrors.As(err, &rpcErr) && rpcErr.Code == -32000 && rpcErr.Message == incorrectTopLevelCallsError
+}
+
func (c *EthereumClient) processFailedTransactionTrace(err error) ([]byte, bool, error) {
processed := false
+ if c.config.Blockchain() == common.Blockchain_BLOCKCHAIN_ROBINHOOD && isIncorrectTopLevelCallsError(err) {
+ // The transaction was terminated by ArbOS before EVM execution (e.g. included by the
+ // sequencer but failed pre-checks), so no call frames exist. Substitute a fake trace;
+ // the receipt already records the failed status.
+ byteTrace, processErr := json.Marshal(ethereum.EthereumTransactionTrace{Error: robinhoodFakeTraceError})
+ processed = true
+ if processErr != nil {
+ return nil, processed, xerrors.Errorf("failed to marshal fake trace for robinhood transaction: %w", processErr)
+ }
+ c.metrics.traceTransactionFakeCounter.Inc(1)
+ return byteTrace, processed, nil
+ }
if c.config.Blockchain() == common.Blockchain_BLOCKCHAIN_OPTIMISM {
// If an Optimism transaction is failed with the following error: "Fail with error
// 'deployer address not whitelisted:'", we need to create a fake trace
diff --git a/internal/blockchain/client/ethereum/ethereum_test.go b/internal/blockchain/client/ethereum/ethereum_test.go
index 4c372d82..88645ee5 100644
--- a/internal/blockchain/client/ethereum/ethereum_test.go
+++ b/internal/blockchain/client/ethereum/ethereum_test.go
@@ -21,6 +21,7 @@ import (
jsonrpcmocks "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc/mocks"
"github.com/coinbase/chainstorage/internal/blockchain/parser"
"github.com/coinbase/chainstorage/internal/blockchain/restapi"
+ "github.com/coinbase/chainstorage/internal/config"
"github.com/coinbase/chainstorage/internal/dlq"
"github.com/coinbase/chainstorage/internal/utils/fixtures"
"github.com/coinbase/chainstorage/internal/utils/retry"
@@ -289,6 +290,67 @@ func TestEthereumClient_GetBlockByHeight(t *testing.T) {
require.Nil(blobdata.Uncles)
}
+func TestEthereumClient_GetBlockByHeight_WithTxBatchSize(t *testing.T) {
+ require := testutil.Require(t)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ rpcClient := jsonrpcmocks.NewMockClient(ctrl)
+
+ blockResponse := &jsonrpc.Response{
+ Result: json.RawMessage(fixtureBlock),
+ }
+ rpcClient.EXPECT().Call(
+ gomock.Any(), ethGetBlockByNumberMethod, jsonrpc.Params{
+ "0xacc290",
+ true,
+ },
+ ).Return(blockResponse, nil)
+
+ // The fixture block has 2 transactions. With tx_batch_size set to 1,
+ // the receipts must be fetched via 2 batch calls of 1 receipt each.
+ rpcClient.EXPECT().BatchCall(
+ gomock.Any(), ethGetTransactionReceiptMethod, gomock.Any(),
+ ).DoAndReturn(func(ctx context.Context, method *jsonrpc.RequestMethod, batchParams []jsonrpc.Params, opts ...jsonrpc.Option) ([]*jsonrpc.Response, error) {
+ require.Equal(1, len(batchParams))
+ return []*jsonrpc.Response{
+ {Result: json.RawMessage(fixtureReceipt)},
+ }, nil
+ }).Times(2)
+
+ traceResponse := &jsonrpc.Response{
+ Result: json.RawMessage(fixtureBlockTrace),
+ }
+ rpcClient.EXPECT().Call(
+ gomock.Any(), ethTraceBlockByHashMethod, gomock.Any(),
+ ).Return(traceResponse, nil)
+
+ cfg, err := config.New()
+ require.NoError(err)
+ cfg.Chain.Client.TxBatchSize = 1
+
+ var result internal.ClientParams
+ app := testapp.New(
+ t,
+ Module,
+ testModule(rpcClient),
+ testapp.WithConfig(cfg),
+ fx.Populate(&result),
+ )
+ defer app.Close()
+
+ client := result.Master
+ require.NotNil(client)
+
+ block, err := client.GetBlockByHeight(context.Background(), tag, ethereumHeight)
+ require.NoError(err)
+
+ blobdata := block.GetEthereum()
+ require.NotNil(blobdata)
+ require.Equal(2, len(blobdata.TransactionReceipts))
+}
+
func TestEthereumClient_GetBlockByHeightWithoutTransactions(t *testing.T) {
require := testutil.Require(t)
diff --git a/internal/blockchain/client/ethereum/ethereumclassic.go b/internal/blockchain/client/ethereum/ethereumclassic.go
new file mode 100644
index 00000000..e304a5b9
--- /dev/null
+++ b/internal/blockchain/client/ethereum/ethereumclassic.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+)
+
+func NewEthereumClassicClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ // Ethereum Classic shares the same data schema as Ethereum since it is an EVM chain.
+ return NewEthereumClientFactory(params)
+}
diff --git a/internal/blockchain/client/ethereum/megaeth.go b/internal/blockchain/client/ethereum/megaeth.go
new file mode 100644
index 00000000..50f09cdc
--- /dev/null
+++ b/internal/blockchain/client/ethereum/megaeth.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+)
+
+func NewMegaethClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ // Reuse the Ethereum client factory since it is an EVM chain.
+ return NewEthereumClientFactory(params)
+}
diff --git a/internal/blockchain/client/ethereum/module.go b/internal/blockchain/client/ethereum/module.go
index 2c1d65e6..95a73c9b 100644
--- a/internal/blockchain/client/ethereum/module.go
+++ b/internal/blockchain/client/ethereum/module.go
@@ -39,5 +39,45 @@ var Module = fx.Options(
Name: "polygon",
Target: NewPolygonClientFactory,
}),
+ fx.Provide(fx.Annotated{
+ Name: "tron",
+ Target: NewTronClientFactory,
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "story",
+ Target: NewStoryClientFactory,
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "ethereumclassic",
+ Target: NewEthereumClassicClientFactory,
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "plasma",
+ Target: NewPlasmaClientFactory,
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "monad",
+ Target: NewMonadClientFactory,
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "abstract",
+ Target: NewAbstractClientFactory,
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "megaeth",
+ Target: NewMegaethClientFactory,
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "seismic",
+ Target: NewSeismicClientFactory,
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "tempo",
+ Target: NewTempoClientFactory,
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "robinhood",
+ Target: NewRobinhoodClientFactory,
+ }),
beacon.Module,
)
diff --git a/internal/blockchain/client/ethereum/monad.go b/internal/blockchain/client/ethereum/monad.go
new file mode 100644
index 00000000..fdd1f907
--- /dev/null
+++ b/internal/blockchain/client/ethereum/monad.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+)
+
+func NewMonadClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ // Reuse the Ethereum client factory since it is an EVM chain.
+ return NewEthereumClientFactory(params)
+}
diff --git a/internal/blockchain/client/ethereum/plasma.go b/internal/blockchain/client/ethereum/plasma.go
new file mode 100644
index 00000000..f4e2281c
--- /dev/null
+++ b/internal/blockchain/client/ethereum/plasma.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+)
+
+func NewPlasmaClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ // Plasma shares the same data schema as Ethereum since it is an EVM chain.
+ return NewEthereumClientFactory(params)
+}
diff --git a/internal/blockchain/client/ethereum/robinhood.go b/internal/blockchain/client/ethereum/robinhood.go
new file mode 100644
index 00000000..6ef8e162
--- /dev/null
+++ b/internal/blockchain/client/ethereum/robinhood.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+)
+
+func NewRobinhoodClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ // Robinhood Chain is an Arbitrum Nitro based EVM L2 and shares the same data schema as Ethereum.
+ return NewEthereumClientFactory(params)
+}
diff --git a/internal/blockchain/client/ethereum/robinhood_test.go b/internal/blockchain/client/ethereum/robinhood_test.go
new file mode 100644
index 00000000..e6099d58
--- /dev/null
+++ b/internal/blockchain/client/ethereum/robinhood_test.go
@@ -0,0 +1,177 @@
+package ethereum
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+
+ "go.uber.org/fx"
+ "go.uber.org/mock/gomock"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+ "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
+ jsonrpcmocks "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc/mocks"
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/ethereum"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+)
+
+const (
+ // Transaction hashes embedded in fixtureBlock.
+ robinhoodTestTxHash0 = "0xbaa42c87b7c764c548fa37e61e9764415fd4a79d7e073d4f92a456698002016b"
+ robinhoodTestTxHash1 = "0xf5365847bff6e48d0c6bc23eee276343d2987efd9876c3c1bf597225e3d69991"
+)
+
+func newRobinhoodTestClient(t *testing.T, rpcClient *jsonrpcmocks.MockClient) (internal.Client, func()) {
+ var result internal.ClientParams
+ app := testapp.New(
+ t,
+ Module,
+ testModule(rpcClient),
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_ROBINHOOD, common.Network_NETWORK_ROBINHOOD_MAINNET),
+ fx.Populate(&result),
+ )
+ return result.Master, app.Close
+}
+
+// A block containing a transaction terminated by ArbOS before EVM execution cannot be traced
+// at the block level ("incorrect number of top-level calls"). The client should fall back to
+// per-transaction tracing and substitute a fake trace for the poisoned transaction.
+func TestRobinhoodClient_GetBlockByHeight_TopLevelCallsFallback(t *testing.T) {
+ require := testutil.Require(t)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ rpcClient := jsonrpcmocks.NewMockClient(ctrl)
+
+ rpcClient.EXPECT().Call(
+ gomock.Any(), ethGetBlockByNumberMethod, jsonrpc.Params{"0xacc290", true},
+ ).Return(&jsonrpc.Response{Result: json.RawMessage(fixtureBlock)}, nil)
+
+ rpcClient.EXPECT().BatchCall(
+ gomock.Any(), ethGetTransactionReceiptMethod, gomock.Any(),
+ ).Return([]*jsonrpc.Response{
+ {Result: json.RawMessage(fixtureReceipt)},
+ {Result: json.RawMessage(fixtureReceipt)},
+ }, nil)
+
+ rpcErr := &jsonrpc.RPCError{Code: -32000, Message: incorrectTopLevelCallsError}
+ rpcClient.EXPECT().Call(
+ gomock.Any(), ethTraceBlockByHashMethod, gomock.Any(),
+ ).Return(nil, rpcErr)
+
+ rpcClient.EXPECT().Call(
+ gomock.Any(), ethTraceTransactionMethod, gomock.Any(),
+ ).Times(2).DoAndReturn(func(ctx context.Context, method *jsonrpc.RequestMethod, params jsonrpc.Params, opts ...jsonrpc.Option) (*jsonrpc.Response, error) {
+ txHash, ok := params[0].(string)
+ require.True(ok)
+ switch txHash {
+ case robinhoodTestTxHash0:
+ return &jsonrpc.Response{Result: json.RawMessage(fixtureTransactionTrace)}, nil
+ case robinhoodTestTxHash1:
+ return nil, rpcErr
+ default:
+ t.Fatalf("unexpected transaction hash: %v", txHash)
+ return nil, nil
+ }
+ })
+
+ client, closeApp := newRobinhoodTestClient(t, rpcClient)
+ defer closeApp()
+
+ block, err := client.GetBlockByHeight(context.Background(), tag, ethereumHeight)
+ require.NoError(err)
+ require.NotNil(block)
+
+ blobdata := block.GetEthereum()
+ require.NotNil(blobdata)
+ require.Equal(2, len(blobdata.TransactionTraces))
+
+ // The healthy transaction keeps its real trace.
+ require.Equal(json.RawMessage(fixtureTransactionTrace), json.RawMessage(blobdata.TransactionTraces[0]))
+
+ // The poisoned transaction gets the fake trace, and its wire format is exactly
+ // {"error": robinhoodFakeTraceError} so the parser sees a failed trace.
+ expectedStub, err := json.Marshal(ethereum.EthereumTransactionTrace{Error: robinhoodFakeTraceError})
+ require.NoError(err)
+ require.Equal(expectedStub, blobdata.TransactionTraces[1])
+
+ var stub map[string]any
+ require.NoError(json.Unmarshal(blobdata.TransactionTraces[1], &stub))
+ require.Equal(robinhoodFakeTraceError, stub["error"])
+}
+
+// Other block-level trace errors must still fail: the fallback only applies to the exact
+// "incorrect number of top-level calls" error.
+func TestRobinhoodClient_GetBlockByHeight_OtherTraceError_NoFallback(t *testing.T) {
+ require := testutil.Require(t)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ rpcClient := jsonrpcmocks.NewMockClient(ctrl)
+
+ rpcClient.EXPECT().Call(
+ gomock.Any(), ethGetBlockByNumberMethod, jsonrpc.Params{"0xacc290", true},
+ ).Return(&jsonrpc.Response{Result: json.RawMessage(fixtureBlock)}, nil)
+
+ rpcClient.EXPECT().BatchCall(
+ gomock.Any(), ethGetTransactionReceiptMethod, gomock.Any(),
+ ).Return([]*jsonrpc.Response{
+ {Result: json.RawMessage(fixtureReceipt)},
+ {Result: json.RawMessage(fixtureReceipt)},
+ }, nil).AnyTimes()
+
+ rpcClient.EXPECT().Call(
+ gomock.Any(), ethTraceBlockByHashMethod, gomock.Any(),
+ ).Return(nil, &jsonrpc.RPCError{Code: -32000, Message: "some other error"})
+
+ client, closeApp := newRobinhoodTestClient(t, rpcClient)
+ defer closeApp()
+
+ _, err := client.GetBlockByHeight(context.Background(), tag, ethereumHeight)
+ require.Error(err)
+ require.Contains(err.Error(), "some other error")
+}
+
+// Non-Robinhood chains must not fall back: the same error on Ethereum still fails the block,
+// and no per-transaction tracing is attempted.
+func TestEthereumClient_GetBlockByHeight_TopLevelCallsError_NoFallback(t *testing.T) {
+ require := testutil.Require(t)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ rpcClient := jsonrpcmocks.NewMockClient(ctrl)
+
+ rpcClient.EXPECT().Call(
+ gomock.Any(), ethGetBlockByNumberMethod, jsonrpc.Params{"0xacc290", true},
+ ).Return(&jsonrpc.Response{Result: json.RawMessage(fixtureBlock)}, nil)
+
+ rpcClient.EXPECT().BatchCall(
+ gomock.Any(), ethGetTransactionReceiptMethod, gomock.Any(),
+ ).Return([]*jsonrpc.Response{
+ {Result: json.RawMessage(fixtureReceipt)},
+ {Result: json.RawMessage(fixtureReceipt)},
+ }, nil).AnyTimes()
+
+ // No expectation for ethTraceTransactionMethod: any per-transaction call would fail the test.
+ rpcClient.EXPECT().Call(
+ gomock.Any(), ethTraceBlockByHashMethod, gomock.Any(),
+ ).Return(nil, &jsonrpc.RPCError{Code: -32000, Message: incorrectTopLevelCallsError})
+
+ var result internal.ClientParams
+ app := testapp.New(
+ t,
+ Module,
+ testModule(rpcClient),
+ fx.Populate(&result),
+ )
+ defer app.Close()
+
+ _, err := result.Master.GetBlockByHeight(context.Background(), tag, ethereumHeight)
+ require.Error(err)
+ require.Contains(err.Error(), incorrectTopLevelCallsError)
+}
diff --git a/internal/blockchain/client/ethereum/seismic.go b/internal/blockchain/client/ethereum/seismic.go
new file mode 100644
index 00000000..139e6e91
--- /dev/null
+++ b/internal/blockchain/client/ethereum/seismic.go
@@ -0,0 +1,11 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+)
+
+func NewSeismicClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ // Shares the same data schema as Ethereum since it is an EVM chain.
+ // Seismic timestamps are in milliseconds, so we convert them to seconds.
+ return NewEthereumClientFactory(params, WithEthereumTimestampInMs())
+}
diff --git a/internal/blockchain/client/ethereum/story.go b/internal/blockchain/client/ethereum/story.go
new file mode 100644
index 00000000..df4eb6ae
--- /dev/null
+++ b/internal/blockchain/client/ethereum/story.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+)
+
+func NewStoryClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ // Story shares the same data schema as Ethereum since it is an EVM chain.
+ return NewEthereumClientFactory(params)
+}
diff --git a/internal/blockchain/client/ethereum/story_test.go b/internal/blockchain/client/ethereum/story_test.go
new file mode 100644
index 00000000..59dd7210
--- /dev/null
+++ b/internal/blockchain/client/ethereum/story_test.go
@@ -0,0 +1 @@
+package ethereum
diff --git a/internal/blockchain/client/ethereum/tempo.go b/internal/blockchain/client/ethereum/tempo.go
new file mode 100644
index 00000000..46927a22
--- /dev/null
+++ b/internal/blockchain/client/ethereum/tempo.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+)
+
+func NewTempoClientFactory(params internal.JsonrpcClientParams) internal.ClientFactory {
+ // Reuse the Ethereum client factory since it is an EVM chain.
+ return NewEthereumClientFactory(params)
+}
diff --git a/internal/blockchain/client/ethereum/tron.go b/internal/blockchain/client/ethereum/tron.go
new file mode 100644
index 00000000..50f214ee
--- /dev/null
+++ b/internal/blockchain/client/ethereum/tron.go
@@ -0,0 +1,308 @@
+package ethereum
+
+import (
+ "context"
+ "encoding/json"
+ "net/http"
+ "time"
+
+ "github.com/go-playground/validator/v10"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+ "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/ethereum"
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/ethereum/types"
+ "github.com/coinbase/chainstorage/internal/blockchain/restapi"
+ "github.com/coinbase/chainstorage/internal/dlq"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ "github.com/coinbase/chainstorage/internal/utils/log"
+ "github.com/coinbase/chainstorage/internal/utils/retry"
+)
+
+type (
+ TronClient struct {
+ *EthereumClient
+ additionalClient restapi.Client
+ }
+
+ TronClientParams struct {
+ fx.In
+ fxparams.Params
+ MasterClient jsonrpc.Client `name:"master"`
+ SlaveClient jsonrpc.Client `name:"slave"`
+ ValidatorClient jsonrpc.Client `name:"validator"`
+ ConsensusClient jsonrpc.Client `name:"consensus"`
+ AdditionalClient restapi.Client `name:"additional"`
+ DLQ dlq.DLQ
+ }
+
+ tronApiClientFactory struct {
+ masterClient jsonrpc.Client
+ slaveClient jsonrpc.Client
+ validatorClient jsonrpc.Client
+ consensusClient jsonrpc.Client
+ clientFactory TronApiClientFactoryFn
+ }
+
+ TronApiClientFactoryFn func(client jsonrpc.Client) internal.Client
+)
+
+type tronBlockNumRequestData struct {
+ Num uint64 `json:"num"`
+}
+
+var tronTxInfoMethod = &restapi.RequestMethod{
+ Name: "GetTransactionInfoByBlockNum",
+ ParamsPath: "/wallet/gettransactioninfobyblocknum", // No parameter URls
+ Timeout: 6 * time.Second,
+ HTTPMethod: http.MethodPost,
+}
+
+var tronBlockTxMethod = &restapi.RequestMethod{
+ Name: "GetBlockByNum",
+ ParamsPath: "/wallet/getblockbynum",
+ Timeout: 6 * time.Second,
+ HTTPMethod: http.MethodPost,
+}
+
+func NewTronApiClientFactory(params TronClientParams, clientFactory TronApiClientFactoryFn) internal.ClientFactory {
+ return &tronApiClientFactory{
+ masterClient: params.MasterClient,
+ slaveClient: params.SlaveClient,
+ validatorClient: params.ValidatorClient,
+ consensusClient: params.ConsensusClient,
+ clientFactory: clientFactory,
+ }
+}
+
+func (f *tronApiClientFactory) Master() internal.Client {
+ return f.clientFactory(f.masterClient)
+}
+
+func (f *tronApiClientFactory) Slave() internal.Client {
+ return f.clientFactory(f.slaveClient)
+
+}
+
+func (f *tronApiClientFactory) Validator() internal.Client {
+ return f.clientFactory(f.validatorClient)
+
+}
+
+func (f *tronApiClientFactory) Consensus() internal.Client {
+ return f.clientFactory(f.consensusClient)
+}
+
+// Tron shares the same data schema as Ethereum since it is an EVM chain, but we retrive trace from another restapi Client which independent from the main jsonrpc client.
+// So it need to create a new factory for TronClient and set the additionalClient to the restapi client.
+func NewTronClientFactory(params TronClientParams) internal.ClientFactory {
+ return NewTronApiClientFactory(params, func(client jsonrpc.Client) internal.Client {
+ logger := log.WithPackage(params.Logger)
+ ethClient := &EthereumClient{
+ config: params.Config,
+ logger: logger,
+ client: client,
+ dlq: params.DLQ,
+ validate: validator.New(),
+ metrics: newEthereumClientMetrics(params.Metrics),
+ nodeType: types.EthereumNodeType_ARCHIVAL,
+ traceType: types.TraceType_GETH,
+ commitmentLevel: types.CommitmentLevelLatest,
+ }
+ result := &TronClient{
+ EthereumClient: ethClient,
+ additionalClient: params.AdditionalClient,
+ }
+ result.tracer = result
+ return result
+ })
+}
+
+func (c *TronClient) makeTronHttpCall(ctx context.Context, httpMethod *restapi.RequestMethod, requestData tronBlockNumRequestData) ([]byte, error) {
+ postData, err := json.Marshal(requestData)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to Marshal Tron requestData: %w", err)
+ }
+ response, err := c.additionalClient.Call(ctx, httpMethod, postData)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to call Tron API: %w", err)
+ }
+
+ // Validate that the response is valid JSON to detect truncated responses
+ if len(response) > 0 && !json.Valid(response) {
+ // Log the truncated response for debugging
+ preview := string(response)
+ if len(preview) > 500 {
+ preview = preview[:500] + "..."
+ }
+ c.logger.Warn(
+ "received invalid/truncated JSON response from Tron API",
+ zap.String("method", httpMethod.Name),
+ zap.Uint64("block_num", requestData.Num),
+ zap.Int("response_length", len(response)),
+ zap.String("response_preview", preview),
+ )
+ return nil, retry.Retryable(xerrors.Errorf("received invalid/truncated JSON response from %s (length=%d): response is not valid JSON", httpMethod.Name, len(response)))
+ }
+
+ return response, nil
+}
+
+func (c *TronClient) getBlockTxByNum(ctx context.Context, blockNumber uint64) ([]byte, error) {
+ requestData := tronBlockNumRequestData{
+ Num: blockNumber,
+ }
+ result, err := c.makeTronHttpCall(ctx, tronBlockTxMethod, requestData)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get Tron block: %w", err)
+ }
+ return result, nil
+}
+
+func (c *TronClient) getBlockTxInfoByNum(ctx context.Context, blockNumber uint64) ([]byte, error) {
+ requestData := tronBlockNumRequestData{
+ Num: blockNumber,
+ }
+ response, err := c.makeTronHttpCall(ctx, tronTxInfoMethod, requestData)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get Tron transaction info: %w", err)
+ }
+ return response, nil
+}
+
+func (c *TronClient) getBlockTraces(ctx context.Context, tag uint32, block *ethereum.EthereumBlockLit) ([][]byte, error) {
+ blockNumber := block.Number.Value()
+
+ // Get block transactions to extract types
+ blockTxData, err := c.getBlockTxByNum(ctx, blockNumber)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get block transactions: %w", err)
+ }
+
+ // Get transaction info
+ txInfoResponse, err := c.getBlockTxInfoByNum(ctx, blockNumber)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get transaction info: %w", err)
+ }
+
+ // Parse block data to extract transaction types by txID
+ txTypeMap, err := c.extractTransactionTypes(blockTxData)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to extract transaction types: %w", err)
+ }
+
+ // Merge txInfo with transaction types
+ results, err := c.mergeTxInfoWithTypes(txInfoResponse, txTypeMap)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to merge txInfo with types: %w", err)
+ }
+
+ return results, nil
+}
+
+// mergeTxInfoWithTypes parses txInfo response and adds transaction types based on txID matching
+func (c *TronClient) mergeTxInfoWithTypes(txInfoResponse []byte, txTypeMap map[string]string) ([][]byte, error) {
+ // Parse txInfo response as array
+ var txInfoArray []json.RawMessage
+ if err := json.Unmarshal(txInfoResponse, &txInfoArray); err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal TronTxInfo: %w", err)
+ }
+
+ // Merge each txInfo with its corresponding type
+ results := make([][]byte, 0, len(txInfoArray))
+ for _, txInfoBytes := range txInfoArray {
+ // Parse txInfo as map to allow dynamic field addition
+ var txInfo map[string]interface{}
+ if err := json.Unmarshal(txInfoBytes, &txInfo); err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal txInfo: %w", err)
+ }
+
+ // Extract txID from txInfo (every transaction must have txID)
+ txID, ok := txInfo["id"].(string)
+ if !ok {
+ return nil, xerrors.Errorf("txInfo id is not a string or is missing: %+v", txInfo)
+ }
+ // Add transaction type if found
+ if txType, exists := txTypeMap[txID]; exists {
+ txInfo["type"] = txType
+ }
+
+ // Re-serialize the modified txInfo
+ modifiedBytes, err := json.Marshal(txInfo)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to marshal modified txInfo: %w", err)
+ }
+
+ results = append(results, modifiedBytes)
+ }
+
+ return results, nil
+}
+
+// extractTransactionTypes extracts transaction types from block data, indexed by txID
+func (c *TronClient) extractTransactionTypes(blockTxData []byte) (map[string]string, error) {
+ if len(blockTxData) == 0 {
+ return make(map[string]string), nil
+ }
+
+ // Parse the block data
+ var blockData map[string]interface{}
+ if err := json.Unmarshal(blockTxData, &blockData); err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal block data: %w", err)
+ }
+
+ txTypeMap := make(map[string]string)
+
+ // Extract transactions array
+ transactions, ok := blockData["transactions"].([]any)
+ if !ok {
+ return txTypeMap, nil // No transactions in block
+ }
+ // Extract txID (every transaction must have txID)
+ for _, tx := range transactions {
+ txMap, ok := tx.(map[string]any)
+ if !ok {
+ return nil, xerrors.Errorf("failed to assert transaction as map[string]interface{}: %+v", tx)
+ }
+
+ txID, ok := txMap["txID"].(string)
+ if !ok {
+ return nil, xerrors.Errorf("transaction is missing txID or it's not a string: %+v", txMap)
+ }
+
+ rawDataVal, ok := txMap["raw_data"]
+ if !ok {
+ continue // Or return an error if raw_data is always expected
+ }
+ rawData, ok := rawDataVal.(map[string]any)
+ if !ok {
+ return nil, xerrors.Errorf("raw_data is not a map: %+v", rawDataVal)
+ }
+
+ contractsVal, ok := rawData["contract"]
+ if !ok {
+ continue
+ }
+ contracts, ok := contractsVal.([]any)
+ if !ok || len(contracts) == 0 {
+ continue
+ }
+
+ contract, ok := contracts[0].(map[string]any)
+ if !ok {
+ return nil, xerrors.Errorf("contract is not a map: %+v", contracts[0])
+ }
+
+ txType, ok := contract["type"].(string)
+ if !ok {
+ return nil, xerrors.Errorf("contract type is not a string: %+v", contract["type"])
+ }
+
+ txTypeMap[txID] = txType
+ }
+
+ return txTypeMap, nil
+}
diff --git a/internal/blockchain/client/ethereum/tron_test.go b/internal/blockchain/client/ethereum/tron_test.go
new file mode 100644
index 00000000..6c4de285
--- /dev/null
+++ b/internal/blockchain/client/ethereum/tron_test.go
@@ -0,0 +1,305 @@
+package ethereum
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/fx"
+ "go.uber.org/mock/gomock"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/client/internal"
+ "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
+ jsonrpcmocks "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc/mocks"
+ "github.com/coinbase/chainstorage/internal/blockchain/parser"
+ "github.com/coinbase/chainstorage/internal/blockchain/restapi"
+ restapimocks "github.com/coinbase/chainstorage/internal/blockchain/restapi/mocks"
+ "github.com/coinbase/chainstorage/internal/dlq"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+)
+
+type (
+ tronClientTestSuite struct {
+ suite.Suite
+
+ ctrl *gomock.Controller
+ app testapp.TestApp
+ rpcClient *jsonrpcmocks.MockClient
+ restClient *restapimocks.MockClient
+ client internal.Client
+ }
+)
+
+const (
+ tronTestTag = uint32(2)
+ // tronTestHeight = uint64(10000)
+ tronTestHeight = ethereumHeight
+ fixtureBlockTxInfoResponse = `
+ [
+ {
+ "blockNumber": 11322000,
+ "contractResult": [
+ ""
+ ],
+ "blockTimeStamp": 1725466323000,
+ "receipt": {
+ "net_usage": 268
+ },
+ "id": "baa42c87b7c764c548fa37e61e9764415fd4a79d7e073d4f92a456698002016b"
+ },
+ {
+ "id": "f5365847bff6e48d0c6bc23eee276343d2987efd9876c3c1bf597225e3d69991",
+ "blockNumber": 11322000,
+ "internal_transactions": [
+ {
+ "hash": "27b42aff06882822a0c84211121e5f98c06a9b074ee84a085c998397b8b2da3a",
+ "caller_address": "4158baea0b354f7b333b3b1563c849e979ae4e2002",
+ "transferTo_address": "41eed9e56a5cddaa15ef0c42984884a8afcf1bdebb",
+ "callValueInfo": [
+ {}
+ ],
+ "note": "63616c6c"
+ },
+ {
+ "hash": "3e2b8ca208f6c899afdc74b772a4504cdd6704bbeff6d34045351c9ad83f478d",
+ "caller_address": "4158baea0b354f7b333b3b1563c849e979ae4e2002",
+ "transferTo_address": "41f5a6eae2fb24b0bda6288e346982fc14e094c19a",
+ "callValueInfo": [
+ {
+ "callValue": 405000000
+ }
+ ],
+ "note": "63616c6c"
+ }
+ ]
+ }
+ ]
+ `
+ fixtureBlockTxResponse = `
+ {
+ "blockID": "000000000408a36cd32a8e674045e96f895b7708b85fa5141f3c8fd92eb497a8",
+ "block_header": {
+ "raw_data": {
+ "number": 67674988,
+ "txTrieRoot": "08203d3094277ae2bfc9981bde29400a87ab5bc6b9aa807ce42d96a2de5ea109",
+ "witness_address": "417f5e5aca5332ce5e18414d7f85bb62097cefa453",
+ "parentHash": "000000000408a36b033d241520fd155cb0351a27bce043ddb7799ec2790ca1ee",
+ "version": 31,
+ "timestamp": 1733675346000
+ },
+ "witness_signature": "241ea0cb69f7e3dd1436c03c557f2b4a005f6ca315d968a11c1115324f795f2875883db4c91e74a0638c9100a4ced4196080fdaf6077881936636e6169d0fbb801"
+ },
+ "transactions": [
+ {
+ "ret": [
+ {
+ "contractRet": "SUCCESS"
+ }
+ ],
+ "signature": [
+ "e93160d1df484382923db34f02ca196a7dbbd7342948214a739fdf4b96896cfc2ca4d9b706c2393c3f83d269f31901aee43ae8e50a04579b6636c03c321da8e000"
+ ],
+ "txID": "0xbaa42c87b7c764c548fa37e61e9764415fd4a79d7e073d4f92a456698002016b",
+ "raw_data": {
+ "contract": [
+ {
+ "parameter": {
+ "value": {
+ "amount": 6,
+ "owner_address": "4174d7980a2a3e48e3a863365542e92ab8d646e0aa",
+ "to_address": "41c3d34597fb01e25d4d8e7ef34ccdd0f05bf85473"
+ },
+ "type_url": "type.googleapis.com/protocol.TransferContract"
+ },
+ "type": "TransferContract"
+ }
+ ],
+ "ref_block_bytes": "a358",
+ "ref_block_hash": "c773a99ac91938c6",
+ "expiration": 1733675400000,
+ "timestamp": 1733675342465
+ },
+ "raw_data_hex": "0a02a3582208c773a99ac91938c640c0f6ecb8ba325a65080112610a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412300a154174d7980a2a3e48e3a863365542e92ab8d646e0aa121541c3d34597fb01e25d4d8e7ef34ccdd0f05bf8547318067081b5e9b8ba32"
+ },
+ {
+ "ret": [
+ {
+ "contractRet": "SUCCESS"
+ }
+ ],
+ "signature": [
+ "27a73675483a50fa52972e7c5ffe33e931ffad1a037a79c3ff2ab97cf08315b557a723d3fc87d5a1e81165775fdfba4b14c7aab31ccd74d06d36bcdd4615f18a1c"
+ ],
+ "txID": "f5365847bff6e48d0c6bc23eee276343d2987efd9876c3c1bf597225e3d69991",
+ "raw_data": {
+ "contract": [
+ {
+ "parameter": {
+ "value": {
+ "balance": 248167143,
+ "receiver_address": "414c614b77e81392450d88f9e339481843abae0e34",
+ "owner_address": "410611d6c1d784930f53979f06f10306251919e1ea"
+ },
+ "type_url": "type.googleapis.com/protocol.UnDelegateResourceContract"
+ },
+ "type": "UnDelegateResourceContract"
+ }
+ ],
+ "ref_block_bytes": "a343",
+ "ref_block_hash": "92d31415885cc680",
+ "expiration": 1733761442497,
+ "fee_limit": 30000000,
+ "timestamp": 1733675342497
+ },
+ "raw_data_hex": "0a02a343220892d31415885cc68040c1c5f0e1ba325a72083a126e0a37747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e556e44656c65676174655265736f75726365436f6e747261637412330a15410611d6c1d784930f53979f06f10306251919e1ea18e7f5aa762215414c614b77e81392450d88f9e339481843abae0e3470a1b5e9b8ba3290018087a70e"
+ }
+ ]
+ }
+ `
+)
+
+func TestTronClientTestSuite(t *testing.T) {
+ suite.Run(t, new(tronClientTestSuite))
+}
+
+func (s *tronClientTestSuite) SetupTest() {
+ s.ctrl = gomock.NewController(s.T())
+ s.rpcClient = jsonrpcmocks.NewMockClient(s.ctrl)
+ s.restClient = restapimocks.NewMockClient(s.ctrl)
+
+ var result internal.ClientParams
+ s.app = testapp.New(
+ s.T(),
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_TRON, common.Network_NETWORK_TRON_MAINNET),
+ Module,
+ // jsonrpc.Module,
+ // restapi.Module,
+ testTronApiModule(s.rpcClient, s.restClient),
+ fx.Populate(&result),
+ )
+
+ s.client = result.Master
+ s.NotNil(s.client)
+}
+
+func (s *tronClientTestSuite) TearDownTest() {
+ s.app.Close()
+ s.ctrl.Finish()
+}
+
+func testTronApiModule(rpcClient *jsonrpcmocks.MockClient, restClient *restapimocks.MockClient) fx.Option {
+ return fx.Options(
+ internal.Module,
+ fx.Provide(fx.Annotated{
+ Name: "master",
+ Target: func() jsonrpc.Client { return rpcClient },
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "slave",
+ Target: func() jsonrpc.Client { return rpcClient },
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "validator",
+ Target: func() jsonrpc.Client { return rpcClient },
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "consensus",
+ Target: func() jsonrpc.Client { return rpcClient },
+ }),
+ fx.Provide(fx.Annotated{
+ Name: "additional",
+ Target: func() restapi.Client { return restClient },
+ }),
+ fx.Provide(dlq.NewNop),
+ fx.Provide(parser.NewNop),
+ )
+}
+
+func (s *tronClientTestSuite) TestTronClient_New() {
+ require := testutil.Require(s.T())
+
+ var tronClientResult TronClientParams
+ var clientResutl internal.ClientParams
+ app := testapp.New(
+ s.T(),
+ Module,
+ internal.Module,
+ jsonrpc.Module,
+ restapi.Module,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_TRON, common.Network_NETWORK_TRON_MAINNET),
+ fx.Provide(dlq.NewNop),
+ fx.Provide(parser.NewNop),
+ fx.Populate(&tronClientResult),
+ fx.Populate(&clientResutl),
+ )
+ defer app.Close()
+
+ require.NotNil(s.client)
+ require.NotNil(tronClientResult.AdditionalClient)
+ s.NotNil(clientResutl.Master)
+ s.NotNil(clientResutl.Slave)
+ s.NotNil(clientResutl.Validator)
+ s.NotNil(clientResutl.Consensus)
+}
+
+func (s *tronClientTestSuite) TestTronClient_GetBlockByHeight() {
+ require := testutil.Require(s.T())
+ // mock block jsonrpc request --------------------
+ blockResponse := &jsonrpc.Response{
+ Result: json.RawMessage(fixtureBlock),
+ }
+ s.rpcClient.EXPECT().Call(
+ gomock.Any(), ethGetBlockByNumberMethod, jsonrpc.Params{
+ "0xacc290",
+ true,
+ },
+ ).Return(blockResponse, nil)
+ // mock TxReceipt jsonrpc request --------------------
+ receiptResponse := []*jsonrpc.Response{
+ {Result: json.RawMessage(fixtureReceipt)},
+ {Result: json.RawMessage(fixtureReceipt)},
+ }
+ s.rpcClient.EXPECT().BatchCall(
+ gomock.Any(), ethGetTransactionReceiptMethod, gomock.Any(),
+ ).Return(receiptResponse, nil)
+
+ // mock BlockTxInfo restapi request --------------------
+ blockTxInfoPostData := tronBlockNumRequestData{Num: ethereumHeight}
+ postData, _ := json.Marshal(blockTxInfoPostData)
+ txr := json.RawMessage(fixtureBlockTxInfoResponse)
+ s.restClient.EXPECT().Call(gomock.Any(), tronTxInfoMethod, postData).Return(txr, nil)
+
+ // mock BlockTx restapi request --------------------
+ blockTxPostData := tronBlockNumRequestData{Num: ethereumHeight}
+ postData, _ = json.Marshal(blockTxPostData)
+ txr = json.RawMessage(fixtureBlockTxResponse)
+ s.restClient.EXPECT().Call(gomock.Any(), tronBlockTxMethod, postData).Return(txr, nil)
+
+ block, err := s.client.GetBlockByHeight(context.Background(), tronTestTag, tronTestHeight)
+ require.NoError(err)
+ require.Equal(common.Blockchain_BLOCKCHAIN_TRON, block.Blockchain)
+ require.Equal(common.Network_NETWORK_TRON_MAINNET, block.Network)
+
+ metadata := block.Metadata
+ require.NotNil(metadata)
+ require.Equal(ethereumHash, metadata.Hash)
+ require.Equal(ethereumParentHash, metadata.ParentHash)
+ require.Equal(ethereumHeight, metadata.Height)
+ require.Equal(ethereumParentHeight, metadata.ParentHeight)
+ require.Equal(tronTestTag, metadata.Tag)
+
+ blobdata := block.GetEthereum()
+ require.NotNil(blobdata)
+ require.NotNil(blobdata.Header)
+ require.Equal(2, len(blobdata.TransactionReceipts))
+ require.NotNil(blobdata.TransactionTraces)
+ require.Equal(2, len(blobdata.TransactionTraces))
+ require.NotNil(blobdata.TransactionTraces[0])
+ require.NotNil(blobdata.TransactionTraces[1])
+ require.Nil(blobdata.Uncles)
+}
+
+// TODO: add test case for TronClient.getBlockTraces
diff --git a/internal/blockchain/client/internal/client.go b/internal/blockchain/client/internal/client.go
index 4a0d9c19..d04657d3 100644
--- a/internal/blockchain/client/internal/client.go
+++ b/internal/blockchain/client/internal/client.go
@@ -54,22 +54,34 @@ type (
Params struct {
fx.In
fxparams.Params
- Parser parser.Parser
- Bitcoin ClientFactory `name:"bitcoin" optional:"true"`
- Bsc ClientFactory `name:"bsc" optional:"true"`
- Ethereum ClientFactory `name:"ethereum" optional:"true"`
- Rosetta ClientFactory `name:"rosetta" optional:"true"`
- Solana ClientFactory `name:"solana" optional:"true"`
- Polygon ClientFactory `name:"polygon" optional:"true"`
- Avacchain ClientFactory `name:"avacchain" optional:"true"`
- Arbitrum ClientFactory `name:"arbitrum" optional:"true"`
- Optimism ClientFactory `name:"optimism" optional:"true"`
- Base ClientFactory `name:"base" optional:"true"`
- Fantom ClientFactory `name:"fantom" optional:"true"`
- Aptos ClientFactory `name:"aptos" optional:"true"`
- EthereumBeacon ClientFactory `name:"ethereum/beacon" optional:"true"`
- CosmosStaking ClientFactory `name:"cosmos/staking" optional:"true"`
- CardanoStaking ClientFactory `name:"cardano/staking" optional:"true"`
+ Parser parser.Parser
+ Bitcoin ClientFactory `name:"bitcoin" optional:"true"`
+ Bsc ClientFactory `name:"bsc" optional:"true"`
+ Ethereum ClientFactory `name:"ethereum" optional:"true"`
+ Rosetta ClientFactory `name:"rosetta" optional:"true"`
+ Solana ClientFactory `name:"solana" optional:"true"`
+ Polygon ClientFactory `name:"polygon" optional:"true"`
+ Avacchain ClientFactory `name:"avacchain" optional:"true"`
+ Arbitrum ClientFactory `name:"arbitrum" optional:"true"`
+ Optimism ClientFactory `name:"optimism" optional:"true"`
+ Base ClientFactory `name:"base" optional:"true"`
+ Fantom ClientFactory `name:"fantom" optional:"true"`
+ Aptos ClientFactory `name:"aptos" optional:"true"`
+ EthereumBeacon ClientFactory `name:"ethereum/beacon" optional:"true"`
+ CosmosStaking ClientFactory `name:"cosmos/staking" optional:"true"`
+ CardanoStaking ClientFactory `name:"cardano/staking" optional:"true"`
+ Tron ClientFactory `name:"tron" optional:"true"`
+ Story ClientFactory `name:"story" optional:"true"`
+ EthereumClassic ClientFactory `name:"ethereumclassic" optional:"true"`
+ Plasma ClientFactory `name:"plasma" optional:"true"`
+ Monad ClientFactory `name:"monad" optional:"true"`
+ Abstract ClientFactory `name:"abstract" optional:"true"`
+ Megaeth ClientFactory `name:"megaeth" optional:"true"`
+ Seismic ClientFactory `name:"seismic" optional:"true"`
+ Dash ClientFactory `name:"dash" optional:"true"`
+ Zcash ClientFactory `name:"zcash" optional:"true"`
+ Tempo ClientFactory `name:"tempo" optional:"true"`
+ Robinhood ClientFactory `name:"robinhood" optional:"true"`
}
ClientParams struct {
@@ -92,6 +104,7 @@ type (
ClientOptions struct {
BestEffort bool
+ Heartbeat func(ctx context.Context, details ...any)
}
)
@@ -111,8 +124,12 @@ func NewClient(params Params) (Result, error) {
sidechain := params.Config.Chain.Sidechain
if sidechain == api.SideChain_SIDECHAIN_NONE {
switch blockchain {
- case common.Blockchain_BLOCKCHAIN_BITCOIN:
+ case common.Blockchain_BLOCKCHAIN_BITCOIN, common.Blockchain_BLOCKCHAIN_BITCOINCASH, common.Blockchain_BLOCKCHAIN_LITECOIN, common.Blockchain_BLOCKCHAIN_DOGECOIN:
factory = params.Bitcoin
+ case common.Blockchain_BLOCKCHAIN_ZCASH:
+ factory = params.Zcash
+ case common.Blockchain_BLOCKCHAIN_DASH:
+ factory = params.Dash
case common.Blockchain_BLOCKCHAIN_BSC:
factory = params.Bsc
case common.Blockchain_BLOCKCHAIN_ETHEREUM:
@@ -133,6 +150,26 @@ func NewClient(params Params) (Result, error) {
factory = params.Base
case common.Blockchain_BLOCKCHAIN_APTOS:
factory = params.Aptos
+ case common.Blockchain_BLOCKCHAIN_TRON:
+ factory = params.Tron
+ case common.Blockchain_BLOCKCHAIN_STORY:
+ factory = params.Story
+ case common.Blockchain_BLOCKCHAIN_ETHEREUMCLASSIC:
+ factory = params.EthereumClassic
+ case common.Blockchain_BLOCKCHAIN_PLASMA:
+ factory = params.Plasma
+ case common.Blockchain_BLOCKCHAIN_MONAD:
+ factory = params.Monad
+ case common.Blockchain_BLOCKCHAIN_ABSTRACT:
+ factory = params.Abstract
+ case common.Blockchain_BLOCKCHAIN_MEGAETH:
+ factory = params.Megaeth
+ case common.Blockchain_BLOCKCHAIN_SEISMIC:
+ factory = params.Seismic
+ case common.Blockchain_BLOCKCHAIN_TEMPO:
+ factory = params.Tempo
+ case common.Blockchain_BLOCKCHAIN_ROBINHOOD:
+ factory = params.Robinhood
default:
if params.Config.IsRosetta() {
factory = params.Rosetta
@@ -199,6 +236,21 @@ func WithBestEffort() ClientOption {
}
}
+// WithHeartbeat sets a heartbeat callback on the client options.
+// The callback is invoked during long-running operations to signal progress.
+func WithHeartbeat(fn func(ctx context.Context, details ...any)) ClientOption {
+ return func(options *ClientOptions) {
+ options.Heartbeat = fn
+ }
+}
+
+// RecordHeartbeat calls the heartbeat callback if one is set.
+func (o *ClientOptions) RecordHeartbeat(ctx context.Context, details ...any) {
+ if o != nil && o.Heartbeat != nil {
+ o.Heartbeat(ctx, details...)
+ }
+}
+
func defaultClientOptions() *ClientOptions {
return &ClientOptions{
BestEffort: false,
diff --git a/internal/blockchain/client/module.go b/internal/blockchain/client/module.go
index 399f625f..0ceed80b 100644
--- a/internal/blockchain/client/module.go
+++ b/internal/blockchain/client/module.go
@@ -1,6 +1,8 @@
package client
import (
+ "context"
+
"go.uber.org/fx"
"github.com/coinbase/chainstorage/internal/blockchain/client/aptos"
@@ -34,3 +36,7 @@ var (
func WithBestEffort() ClientOption {
return internal.WithBestEffort()
}
+
+func WithHeartbeat(fn func(ctx context.Context, details ...any)) ClientOption {
+ return internal.WithHeartbeat(fn)
+}
diff --git a/internal/blockchain/client/rosetta/rosetta_test.go b/internal/blockchain/client/rosetta/rosetta_test.go
index 0e9495e9..92c872a1 100644
--- a/internal/blockchain/client/rosetta/rosetta_test.go
+++ b/internal/blockchain/client/rosetta/rosetta_test.go
@@ -15,6 +15,7 @@ import (
"github.com/coinbase/chainstorage/internal/blockchain/client/internal"
"github.com/coinbase/chainstorage/internal/blockchain/parser"
+ rosettaparser "github.com/coinbase/chainstorage/internal/blockchain/parser/rosetta"
"github.com/coinbase/chainstorage/internal/blockchain/restapi"
restapimocks "github.com/coinbase/chainstorage/internal/blockchain/restapi/mocks"
"github.com/coinbase/chainstorage/internal/config"
@@ -209,17 +210,20 @@ func TestGetRosettaBlock(t *testing.T) {
require.Equal(tag, rawBlock.Metadata.Tag)
require.Equal(testutil.MustTimestamp("2021-08-04T18:51:21Z"), rawBlock.Metadata.Timestamp)
- var rosettaParser parser.Parser
+ // The rosetta native parser is instantiated directly because dogecoin is
+ // now dispatched to the bitcoin parser; this test only verifies that the
+ // raw block produced by the rosetta client round-trips through the
+ // generic rosetta parser.
+ var rosettaParser parser.NativeParser
app := testapp.New(
t,
testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_DOGECOIN, common.Network_NETWORK_DOGECOIN_MAINNET),
- Module,
- parser.Module,
+ fx.Provide(rosettaparser.NewRosettaNativeParser),
fx.Populate(&rosettaParser),
)
defer app.Close()
- nativeBlock, err := rosettaParser.ParseNativeBlock(context.Background(), rawBlock)
+ nativeBlock, err := rosettaParser.ParseBlock(context.Background(), rawBlock)
require.NoError(err)
require.NotNil(nativeBlock)
require.Equal(common.Blockchain_BLOCKCHAIN_DOGECOIN, nativeBlock.Blockchain)
diff --git a/internal/blockchain/endpoints/client.go b/internal/blockchain/endpoints/client.go
index 3783ee5d..1accf5fb 100644
--- a/internal/blockchain/endpoints/client.go
+++ b/internal/blockchain/endpoints/client.go
@@ -1,6 +1,7 @@
package endpoints
import (
+ "context"
"fmt"
"net/http"
"strings"
@@ -10,13 +11,40 @@ import (
"github.com/coinbase/chainstorage/internal/utils/ratelimiter"
)
+type requestWeightContextKey struct{}
+
+// WithRequestWeight annotates ctx with the number of provider-side requests
+// the enclosed HTTP request represents, e.g. the number of calls inside a
+// JSON-RPC batch request. The weight is only consumed by endpoints configured
+// with rps_count_batch; all other endpoints keep charging one token per HTTP
+// request.
+func WithRequestWeight(ctx context.Context, weight int) context.Context {
+ return context.WithValue(ctx, requestWeightContextKey{}, weight)
+}
+
+// RequestWeightFromContext returns the weight set by WithRequestWeight.
+// It returns 1 when the weight is unset or non-positive, since every HTTP
+// request costs at least one provider-side request.
+func RequestWeightFromContext(ctx context.Context) int {
+ weight, ok := ctx.Value(requestWeightContextKey{}).(int)
+ if !ok || weight <= 0 {
+ return 1
+ }
+
+ return weight
+}
+
func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if len(rt.headers) > 0 {
for key, val := range rt.headers {
req.Header.Set(key, val)
}
}
- err := rt.rateLimiter.WaitN(req.Context(), 1)
+ weight := 1
+ if rt.countBatch {
+ weight = RequestWeightFromContext(req.Context())
+ }
+ err := rt.rateLimiter.WaitWeight(req.Context(), weight)
if err != nil {
return nil, xerrors.Errorf("failed to wait for rate limiting: %w", err)
}
@@ -24,7 +52,7 @@ func (rt *roundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
return res, err
}
-func WrapHTTPClient(client *http.Client, headers map[string]string, rps int) *http.Client {
+func WrapHTTPClient(client *http.Client, headers map[string]string, rps int, rpsCountBatch bool) *http.Client {
transport := client.Transport
if transport == nil {
transport = http.DefaultTransport
@@ -34,6 +62,7 @@ func WrapHTTPClient(client *http.Client, headers map[string]string, rps int) *ht
base: transport,
headers: headers,
rateLimiter: ratelimiter.New(rps),
+ countBatch: rpsCountBatch,
}
return client
diff --git a/internal/blockchain/endpoints/client_test.go b/internal/blockchain/endpoints/client_test.go
index 7ab68705..2125b6b5 100644
--- a/internal/blockchain/endpoints/client_test.go
+++ b/internal/blockchain/endpoints/client_test.go
@@ -1,11 +1,79 @@
package endpoints
import (
+ "context"
+ "net/http"
"testing"
+ "time"
"github.com/coinbase/chainstorage/internal/utils/testutil"
)
+type stubRoundTripper struct {
+ calls int
+}
+
+func (s *stubRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+ s.calls++
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Body: http.NoBody,
+ }, nil
+}
+
+func TestRequestWeightFromContext(t *testing.T) {
+ require := testutil.Require(t)
+
+ // Unset and non-positive weights are clamped to 1.
+ require.Equal(1, RequestWeightFromContext(context.Background()))
+ require.Equal(1, RequestWeightFromContext(WithRequestWeight(context.Background(), 0)))
+ require.Equal(1, RequestWeightFromContext(WithRequestWeight(context.Background(), -3)))
+ require.Equal(7, RequestWeightFromContext(WithRequestWeight(context.Background(), 7)))
+}
+
+func TestRoundTripperRequestWeight(t *testing.T) {
+ require := testutil.Require(t)
+
+ newRequest := func(weight int) *http.Request {
+ ctx := WithRequestWeight(context.Background(), weight)
+ req, err := http.NewRequestWithContext(ctx, http.MethodPost, "http://localhost", nil)
+ require.NoError(err)
+ return req
+ }
+
+ // countBatch disabled: a weighted request charges a single token, so it
+ // passes instantly even though the weight exceeds the burst.
+ stub := &stubRoundTripper{}
+ client := WrapHTTPClient(&http.Client{Transport: stub}, nil, 20, false)
+ start := time.Now()
+ resp, err := client.Transport.RoundTrip(newRequest(25))
+ require.NoError(err)
+ require.Equal(http.StatusOK, resp.StatusCode)
+ require.True(time.Since(start) < 200*time.Millisecond, "elapsed=%v", time.Since(start))
+ require.Equal(1, stub.calls)
+
+ // countBatch enabled: the same request charges 25 tokens against a bucket
+ // of 20, so at least 5 tokens must be waited for at 20 rps (>= 250ms).
+ stub = &stubRoundTripper{}
+ client = WrapHTTPClient(&http.Client{Transport: stub}, nil, 20, true)
+ start = time.Now()
+ resp, err = client.Transport.RoundTrip(newRequest(25))
+ require.NoError(err)
+ require.Equal(http.StatusOK, resp.StatusCode)
+ require.True(time.Since(start) >= 200*time.Millisecond, "elapsed=%v", time.Since(start))
+ require.Equal(1, stub.calls)
+
+ // countBatch enabled without a weight in the context: defaults to 1.
+ stub = &stubRoundTripper{}
+ client = WrapHTTPClient(&http.Client{Transport: stub}, nil, 20, true)
+ req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, "http://localhost", nil)
+ require.NoError(err)
+ start = time.Now()
+ _, err = client.Transport.RoundTrip(req)
+ require.NoError(err)
+ require.True(time.Since(start) < 200*time.Millisecond, "elapsed=%v", time.Since(start))
+}
+
func TestGetCookieNames(t *testing.T) {
tests := []struct {
cookies string
diff --git a/internal/blockchain/endpoints/endpoint_provider.go b/internal/blockchain/endpoints/endpoint_provider.go
index cc8abe24..8413bc73 100644
--- a/internal/blockchain/endpoints/endpoint_provider.go
+++ b/internal/blockchain/endpoints/endpoint_provider.go
@@ -5,6 +5,8 @@ import (
"fmt"
"math/rand"
"net/http"
+ "os"
+ "regexp"
"github.com/uber-go/tally/v4"
"go.uber.org/fx"
@@ -50,10 +52,11 @@ type (
EndpointProviderResult struct {
fx.Out
- Master EndpointProvider `name:"master"`
- Slave EndpointProvider `name:"slave"`
- Validator EndpointProvider `name:"validator"`
- Consensus EndpointProvider `name:"consensus"`
+ Master EndpointProvider `name:"master"`
+ Slave EndpointProvider `name:"slave"`
+ Validator EndpointProvider `name:"validator"`
+ Consensus EndpointProvider `name:"consensus"`
+ Additional EndpointProvider `name:"additional"`
}
)
@@ -73,15 +76,17 @@ type (
)
const (
- masterEndpointGroupName = "master"
- slaveEndpointGroupName = "slave"
- validatorEndpointGroupName = "validator"
- consensusEndpointGroupName = "consensus"
- contextKeyFailover = "failover:"
+ masterEndpointGroupName = "master"
+ slaveEndpointGroupName = "slave"
+ validatorEndpointGroupName = "validator"
+ consensusEndpointGroupName = "consensus"
+ contextKeyFailover = "failover:"
+ additionalEndpointGroupName = "additional"
)
var (
ErrFailoverUnavailable = xerrors.New("no endpoint is available for failover")
+ placeholderVarRE = regexp.MustCompile(`\{([A-Za-z_][A-Za-z0-9_]*)\}`)
)
func NewEndpointProvider(params EndpointProviderParams) (EndpointProviderResult, error) {
@@ -116,12 +121,16 @@ func NewEndpointProvider(params EndpointProviderParams) (EndpointProviderResult,
return EndpointProviderResult{}, xerrors.Errorf("failed to create consensus endpoint provider with slave endpoints: %w", err)
}
}
-
+ additional, err := newEndpointProvider(logger, params.Config, scope, ¶ms.Config.Chain.Client.Additional.EndpointGroup, additionalEndpointGroupName)
+ if err != nil {
+ return EndpointProviderResult{}, xerrors.Errorf("failed to create additional endpoint provider: %w", err)
+ }
return EndpointProviderResult{
- Master: master,
- Slave: slave,
- Validator: validator,
- Consensus: consensus,
+ Master: master,
+ Slave: slave,
+ Validator: validator,
+ Consensus: consensus,
+ Additional: additional,
}, nil
}
@@ -207,6 +216,15 @@ func newEndpoint(
endpoint *config.Endpoint,
endpointConfig *config.EndpointConfig,
) (*Endpoint, error) {
+ // Expand URL placeholders before it is used anywhere else (e.g. sticky session cookie hash).
+ if endpoint.Url != "" {
+ expanded, err := expandEndpointURL(endpoint.Url)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to expand endpoint url for %q: %w", endpoint.Name, err)
+ }
+ endpoint.Url = expanded
+ }
+
var opts []ClientOption
//TODO: check if this is still needed
@@ -263,6 +281,10 @@ func newEndpoint(
opts = append(opts, withRPS(endpoint.RPS))
}
+ if endpoint.RPSCountBatch {
+ opts = append(opts, withRPSCountBatch(true))
+ }
+
client, err := newHTTPClient(opts...)
if err != nil {
return nil, xerrors.Errorf("failed to create http client for %v: %w", endpoint.Name, err)
@@ -286,6 +308,38 @@ func newEndpoint(
}, nil
}
+// expandEndpointURL expands placeholders in the URL using environment variables.
+//
+// Supported formats:
+// - {VAR}: custom syntax (e.g. {URL_TOKEN})
+//
+// It returns an error if any placeholder is left unresolved.
+func expandEndpointURL(raw string) (string, error) {
+ missing := make(map[string]struct{})
+ expanded := placeholderVarRE.ReplaceAllStringFunc(raw, func(m string) string {
+ sub := placeholderVarRE.FindStringSubmatch(m)
+ // sub is always [full, group1] when regex matches, but keep it defensive.
+ if len(sub) != 2 {
+ return m
+ }
+
+ key := sub[1]
+ val, ok := os.LookupEnv(key)
+ if !ok {
+ missing[key] = struct{}{}
+ return m
+ }
+ return val
+ })
+
+ if len(missing) > 0 {
+ // Keep the expanded URL in the error for debugging (it will still contain {VAR} markers).
+ return "", xerrors.Errorf("endpoint url contains unresolved placeholder(s): %v", expanded)
+ }
+
+ return expanded, nil
+}
+
func getStickySessionValue(cfg *config.Config) string {
headerValue := fmt.Sprintf("%v-%v-%v", consts.ServiceName, cfg.Chain.Network.GetName(), cfg.Env())
return utils.GenerateSha256HashString(headerValue)
diff --git a/internal/blockchain/endpoints/endpoint_provider_test.go b/internal/blockchain/endpoints/endpoint_provider_test.go
index 4c165cc3..d029c579 100644
--- a/internal/blockchain/endpoints/endpoint_provider_test.go
+++ b/internal/blockchain/endpoints/endpoint_provider_test.go
@@ -2,6 +2,7 @@ package endpoints
import (
"context"
+ "errors"
"fmt"
"math"
"net/http/cookiejar"
@@ -14,7 +15,6 @@ import (
"go.uber.org/fx/fxtest"
"go.uber.org/zap"
"go.uber.org/zap/zaptest"
- "golang.org/x/xerrors"
"github.com/coinbase/chainstorage/internal/config"
"github.com/coinbase/chainstorage/internal/utils/testutil"
@@ -73,6 +73,47 @@ func TestEndpointProvider(t *testing.T) {
}
}
+func TestEndpointProvider_UrlExpandEnv_Curly(t *testing.T) {
+ require := testutil.Require(t)
+
+ t.Setenv("URL_TOKEN", "abc")
+
+ logger := zaptest.NewLogger(t)
+ cfg, err := config.New()
+ require.NoError(err)
+
+ endpointGroup := &config.EndpointGroup{
+ Endpoints: []config.Endpoint{
+ {Name: "e1", Url: "https://example.com/v2/{URL_TOKEN}", Weight: 1},
+ },
+ }
+
+ ctx := context.Background()
+ provider, err := newEndpointProvider(logger, cfg, tally.NoopScope, endpointGroup, "master")
+ require.NoError(err)
+
+ pick, err := provider.GetEndpoint(ctx)
+ require.NoError(err)
+ require.Equal("https://example.com/v2/abc", pick.Config.Url)
+}
+
+func TestEndpointProvider_UrlExpandEnv_MissingVar(t *testing.T) {
+ require := testutil.Require(t)
+
+ logger := zaptest.NewLogger(t)
+ cfg, err := config.New()
+ require.NoError(err)
+
+ endpointGroup := &config.EndpointGroup{
+ Endpoints: []config.Endpoint{
+ {Name: "e1", Url: "https://example.com/v2/{MISSING_TOKEN}", Weight: 1},
+ },
+ }
+
+ _, err = newEndpointProvider(logger, cfg, tally.NoopScope, endpointGroup, "master")
+ require.ErrorContains(err, "failed to expand endpoint url for \"e1\": endpoint url contains unresolved placeholder(s): https://example.com/v2/{MISSING_TOKEN}")
+}
+
func TestEndpointProvider_WithFailover(t *testing.T) {
require := testutil.Require(t)
@@ -212,7 +253,7 @@ func TestEndpointProvider_EmptyEndpoints(t *testing.T) {
_, err = ep.WithFailoverContext(ctx)
require.Equal([]string{"foo"}, getActiveEndpoints(ctx, ep))
require.Error(err)
- require.True(xerrors.Is(err, ErrFailoverUnavailable))
+ require.True(errors.Is(err, ErrFailoverUnavailable))
}
func TestEndpointProvider_StickySessionCookieHash(t *testing.T) {
diff --git a/internal/blockchain/endpoints/http.go b/internal/blockchain/endpoints/http.go
index f39d4165..b9d4ba01 100644
--- a/internal/blockchain/endpoints/http.go
+++ b/internal/blockchain/endpoints/http.go
@@ -27,6 +27,7 @@ type (
headers map[string]string
rootCAs *x509.CertPool
rps int
+ rpsCountBatch bool
}
// stickySessionJar implements passive cookie affinity.
@@ -49,6 +50,7 @@ type (
logger *zap.Logger
headers map[string]string
rateLimiter *ratelimiter.RateLimiter
+ countBatch bool
}
)
@@ -98,7 +100,7 @@ func newDefaultClient(options *clientOptions) (*http.Client, error) {
Timeout: options.timeout,
Transport: transport,
}
- wrapClient := WrapHTTPClient(client, options.headers, options.rps)
+ wrapClient := WrapHTTPClient(client, options.headers, options.rps, options.rpsCountBatch)
return wrapClient, nil
}
@@ -129,7 +131,7 @@ func newStickyClient(options *clientOptions) (*http.Client, error) {
headers[key] = val
}
- wrapClient := WrapHTTPClient(client, headers, options.rps)
+ wrapClient := WrapHTTPClient(client, headers, options.rps, options.rpsCountBatch)
return wrapClient, nil
}
@@ -204,3 +206,13 @@ func withRPS(rps int) ClientOption {
opts.rps = rps
}
}
+
+// withRPSCountBatch makes the endpoint's rate limiter charge one token per
+// call inside a JSON-RPC batch request (see WithRequestWeight). It has no
+// effect unless withRPS is also set, since no rate limiter is created
+// otherwise.
+func withRPSCountBatch(countBatch bool) ClientOption {
+ return func(opts *clientOptions) {
+ opts.rpsCountBatch = countBatch
+ }
+}
diff --git a/internal/blockchain/jsonrpc/client.go b/internal/blockchain/jsonrpc/client.go
index 258dd19e..eceb6523 100644
--- a/internal/blockchain/jsonrpc/client.go
+++ b/internal/blockchain/jsonrpc/client.go
@@ -80,13 +80,16 @@ type (
Name string
Timeout time.Duration
}
+)
+type (
Params []any
Option func(opts *options)
options struct {
allowsRPCError bool
+ onAttempt func(ctx context.Context, attempt int)
}
)
@@ -141,6 +144,15 @@ func WithAllowsRPCError() Option {
}
}
+// WithOnAttempt registers a callback invoked at the start of each attempt
+// (including retries). This is useful for recording heartbeats so that
+// long-running retry loops do not appear idle to the caller.
+func WithOnAttempt(fn func(ctx context.Context, attempt int)) Option {
+ return func(opts *options) {
+ opts.onAttempt = fn
+ }
+}
+
func newClient(params ClientParams, endpointProvider endpoints.EndpointProvider) (Client, error) {
logger := log.WithPackage(params.Logger)
@@ -188,7 +200,13 @@ func (c *clientImpl) Call(ctx context.Context, method *RequestMethod, params Par
}
var response *Response
+ attempt := 0
if err := c.wrap(ctx, method.Name, endpoint.Name, []Params{params}, func(ctx context.Context) error {
+ if options.onAttempt != nil {
+ options.onAttempt(ctx, attempt)
+ }
+ attempt++
+
response = new(Response)
if err := c.makeHTTPRequest(ctx, method.Timeout, endpoint, request, response); err != nil {
return xerrors.Errorf("failed to make http request (method=%v, params=%v, endpoint=%v): %w", method, params, endpoint.Name, err)
@@ -212,6 +230,11 @@ func (c *clientImpl) BatchCall(ctx context.Context, method *RequestMethod, batch
opt(&options)
}
+ // Annotate the context before entering the retry wrapper so that every
+ // attempt's HTTP request carries the batch size, allowing endpoints with
+ // rps_count_batch to charge one rate-limit token per batched call.
+ ctx = endpoints.WithRequestWeight(ctx, len(batchParams))
+
endpoint, err := c.endpointProvider.GetEndpoint(ctx)
if err != nil {
return nil, xerrors.Errorf("failed to get endpoint for request: %w", err)
@@ -230,7 +253,13 @@ func (c *clientImpl) BatchCall(ctx context.Context, method *RequestMethod, batch
}
finalBatchResponses := make([]*Response, len(batchParams))
+ attempt := 0
if err := c.wrap(ctx, method.Name, endpoint.Name, batchParams, func(ctx context.Context) error {
+ if options.onAttempt != nil {
+ options.onAttempt(ctx, attempt)
+ }
+ attempt++
+
var batchResponses []Response
if err := c.makeHTTPRequest(ctx, method.Timeout, endpoint, batchRequests, &batchResponses); err != nil {
return xerrors.Errorf(
diff --git a/internal/blockchain/jsonrpc/client_test.go b/internal/blockchain/jsonrpc/client_test.go
index 87af767e..b11f4193 100644
--- a/internal/blockchain/jsonrpc/client_test.go
+++ b/internal/blockchain/jsonrpc/client_test.go
@@ -14,6 +14,7 @@ import (
"go.uber.org/mock/gomock"
"golang.org/x/xerrors"
+ "github.com/coinbase/chainstorage/internal/blockchain/endpoints"
"github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
jsonrpcmocks "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc/mocks"
"github.com/coinbase/chainstorage/internal/config"
@@ -675,6 +676,111 @@ func TestBatchCall_DuplicateID(t *testing.T) {
require.Contains(err.Error(), "missing response")
}
+func TestBatchCall_RequestWeight(t *testing.T) {
+ require := testutil.Require(t)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ httpClient := jsonrpcmocks.NewMockHTTPClient(ctrl)
+
+ // Capture the request weight seen by the transport on every attempt,
+ // including the first one and the retry.
+ var weights []int
+ attempts := 0
+ httpClient.EXPECT().Do(gomock.Any()).Times(2).
+ DoAndReturn(func(req *http.Request) (*http.Response, error) {
+ weights = append(weights, endpoints.RequestWeightFromContext(req.Context()))
+ attempts += 1
+ body := `
+ [
+ {"jsonrpc":"2.0","id":0,"result":{"hash": "0xabcd"}},
+ {"jsonrpc":"2.0","id":1,"result":{"hash": "0xabce"}},
+ {"jsonrpc":"2.0","id":2,"result":{"hash": "0xabcf"}}
+ ]`
+ if attempts == 1 {
+ // Return a null response to trigger a retry.
+ body = `
+ [
+ {"jsonrpc":"2.0","id":0,"result":{"hash": "0xabcd"}},
+ {"jsonrpc":"2.0","id":1,"result":{"hash": "0xabce"}},
+ {"jsonrpc":"2.0","id":2,"result":null}
+ ]`
+ }
+
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Body: ioutil.NopCloser(strings.NewReader(body)),
+ }, nil
+ })
+
+ var params clientParams
+ app := testapp.New(
+ t,
+ withDummyEndpoints(),
+ fx.Provide(jsonrpc.New),
+ fx.Provide(func() jsonrpc.HTTPClient {
+ return httpClient
+ }),
+ fx.Populate(¶ms),
+ )
+ defer app.Close()
+
+ client := params.Master
+ require.NotNil(client)
+ batchParams := []jsonrpc.Params{
+ {"0x1234"},
+ {"0x1235"},
+ {"0x1236"},
+ }
+ _, err := client.BatchCall(context.Background(),
+ &jsonrpc.RequestMethod{Name: "hello", Timeout: time.Duration(5)},
+ batchParams)
+ require.NoError(err)
+ require.Equal([]int{3, 3}, weights)
+}
+
+func TestCall_RequestWeightDefault(t *testing.T) {
+ require := testutil.Require(t)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ httpClient := jsonrpcmocks.NewMockHTTPClient(ctrl)
+
+ // A single call must not carry a batch weight: the transport sees the
+ // default weight of 1.
+ var weight int
+ httpClient.EXPECT().Do(gomock.Any()).
+ DoAndReturn(func(req *http.Request) (*http.Response, error) {
+ weight = endpoints.RequestWeightFromContext(req.Context())
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Body: ioutil.NopCloser(strings.NewReader(`{"jsonrpc":"2.0","id":0,"result":{"hash": "0xabcd"}}`)),
+ }, nil
+ })
+
+ var params clientParams
+ app := testapp.New(
+ t,
+ withDummyEndpoints(),
+ fx.Provide(jsonrpc.New),
+ fx.Provide(func() jsonrpc.HTTPClient {
+ return httpClient
+ }),
+ fx.Populate(¶ms),
+ )
+ defer app.Close()
+
+ client := params.Master
+ require.NotNil(client)
+ _, err := client.Call(context.Background(),
+ &jsonrpc.RequestMethod{Name: "hello", Timeout: time.Duration(5)},
+ jsonrpc.Params{"0x1234"})
+ require.NoError(err)
+ require.Equal(1, weight)
+}
+
func TestBatchCall_RetryNullResponse(t *testing.T) {
require := testutil.Require(t)
diff --git a/internal/blockchain/parser/aptos/aptos_native.go b/internal/blockchain/parser/aptos/aptos_native.go
index 2d4b22d8..c2fb9e3b 100644
--- a/internal/blockchain/parser/aptos/aptos_native.go
+++ b/internal/blockchain/parser/aptos/aptos_native.go
@@ -535,7 +535,7 @@ func (v *AptosSignature) UnmarshalJSON(input []byte) error {
return nil
}
-func (p *aptosNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+func (p *aptosNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block, opts ...internal.ParseOption) (*api.NativeBlock, error) {
metadata := rawBlock.GetMetadata()
if metadata == nil {
return nil, xerrors.New("metadata not found")
diff --git a/internal/blockchain/parser/bitcoin/bitcoin_native.go b/internal/blockchain/parser/bitcoin/bitcoin_native.go
index 7ec6d911..6b96a48a 100644
--- a/internal/blockchain/parser/bitcoin/bitcoin_native.go
+++ b/internal/blockchain/parser/bitcoin/bitcoin_native.go
@@ -18,6 +18,7 @@ import (
"github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
"github.com/coinbase/chainstorage/internal/utils/log"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
)
@@ -35,6 +36,13 @@ const (
bitcoinScriptTypeNullData string = "nulldata"
bitcoinScriptTypeWitnessUnknown string = "witness_unknown"
bitcoinScriptTypeWitnessV1Taproot string = "witness_v1_taproot"
+ bitcoinScriptTypeAnchor string = "anchor"
+ // TODO, Create litecoin parser for LTC address
+ bitcoinScriptTypeMwebPegin string = "witness_mweb_pegin"
+ bitcoinScriptTypeMwebHogaddr string = "witness_mweb_hogaddr"
+ // BCH-specific: p2s (pay-to-script) introduced in BCHN upgrade 12
+ // Covers bare scripts <= 201 bytes that were previously classified as nonstandard
+ bitcoinScriptTypeBCHScript string = "script"
)
type (
@@ -43,6 +51,14 @@ type (
BitcoinHexString string
BitcoinQuantity uint64
BitcoinDecimalQuantity float64
+ // BitcoinNonce handles nonce fields that can be either a JSON number (Bitcoin)
+ // or a hex string (Zcash Equihash). String nonces are stored as-is; numeric
+ // nonces are converted to uint64.
+ BitcoinNonce struct {
+ value uint64
+ strValue string
+ isString bool
+ }
// BitcoinBlock https://developer.bitcoin.org/reference/rpc/getblock.html
BitcoinBlock struct {
@@ -57,7 +73,7 @@ type (
Tx []*BitcoinTransaction `json:"tx" validate:"required,min=1,dive,required"`
Time BitcoinQuantity `json:"time" validate:"required"`
MedianTime BitcoinQuantity `json:"mediantime"`
- Nonce BitcoinQuantity `json:"nonce"`
+ Nonce BitcoinNonce `json:"nonce"`
Bits BitcoinHexString `json:"bits"`
Difficulty BitcoinDecimalQuantity `json:"difficulty"`
ChainWork BitcoinHexString `json:"chainwork"`
@@ -76,8 +92,8 @@ type (
Weight BitcoinQuantity `json:"weight"`
Version BitcoinQuantity `json:"version"`
LockTime BitcoinQuantity `json:"locktime"`
- Vin []*BitcoinTransactionInput `json:"vin" validate:"required,min=1,dive,required"`
- Vout []*BitcoinTransactionOutput `json:"vout" validate:"required,min=1,dive,required"`
+ Vin []*BitcoinTransactionInput `json:"vin" validate:"required,dive,required"`
+ Vout []*BitcoinTransactionOutput `json:"vout" validate:"required,dive,required"`
BlockHash BitcoinHexString `json:"blockhash"`
BlockTime BitcoinQuantity `json:"blocktime"`
Time BitcoinQuantity `json:"time"`
@@ -149,11 +165,38 @@ type (
bitcoinNativeParserImpl struct {
logger *zap.Logger
validate *validator.Validate
+ // preprocessTx runs per-transaction after JSON unmarshal but
+ // before validation. Chains that backfill fields omitted by
+ // their RPC (e.g. tx.Hash on Dash/Zcash) wire this hook. Nil
+ // means no preprocessing. Both ParseBlock and the streaming
+ // path apply this hook identically so their outputs match.
+ preprocessTx func(*BitcoinTransaction)
+ // txFilter classifies a raw JSON transaction as keep-or-skip.
+ // Used by Zcash to drop shielded-only transactions from the
+ // native output. The filter runs over raw JSON so it can
+ // inspect fields (e.g. shielded outputs) that are not parsed
+ // into the shared BitcoinTransaction struct. Nil means "keep
+ // all transactions".
+ txFilter func(rawTxJSON json.RawMessage) (bool, error)
+ // p2pkhVersionByte is the chain-specific version byte used for P2PKH
+ // base58check encoding when deriving addresses from P2PK scripts.
+ // Bitcoin: 0x00, Dash: 0x4c, etc.
+ p2pkhVersionByte byte
}
)
var _ internal.NativeParser = (*bitcoinNativeParserImpl)(nil)
+// backfillTxHashSingle is a preprocessTx hook for non-SegWit chains
+// (e.g. Dash, Zcash) whose getblock RPC omits tx.hash. Because these
+// chains have no witness data, txid and hash are equivalent, so we
+// can safely copy one to the other.
+func backfillTxHashSingle(tx *BitcoinTransaction) {
+ if tx.Hash == "" {
+ tx.Hash = tx.TxId
+ }
+}
+
var pubKeyScriptRegexp = regexp.MustCompile("^([[:xdigit:]]*) OP_CHECKSIG$")
func validateBitcoinScriptPubKey(sl validator.StructLevel) {
@@ -190,25 +233,48 @@ func validateBitcoinScriptPubKey(sl validator.StructLevel) {
}
}
// Types that we expect to be able to parse address for
- case bitcoinScriptTypePubKeyHash, bitcoinScriptTypeScriptHash, bitcoinScriptTypeWitnessV0PubKeyHash, bitcoinScriptTypeWitnessV0ScriptHash, bitcoinScriptTypeWitnessUnknown, bitcoinScriptTypeWitnessV1Taproot:
+ // https://github.com/bitcoin/bitcoin/commit/455fca86cfada1823aa28615b5683f9dc73dbb9a
+ case bitcoinScriptTypePubKeyHash, bitcoinScriptTypeScriptHash, bitcoinScriptTypeWitnessV0PubKeyHash, bitcoinScriptTypeWitnessV0ScriptHash, bitcoinScriptTypeWitnessUnknown, bitcoinScriptTypeWitnessV1Taproot, bitcoinScriptTypeAnchor:
if len(address) == 0 {
sl.ReportError(address, "Address[main]", "Address[main]", "bspk_a", "")
- }
+ } // Types that we expect to be able to parse address for
+ case bitcoinScriptTypeMwebPegin, bitcoinScriptTypeMwebHogaddr:
+ // https://github.com/litecoin-project/litecoin/blob/cd1660afaf5b31a80e797668b12b5b3933844842/src/script/standard.cpp#L60
+ // BCH p2s: bare scripts that don't match standard templates; may or may not have an address
+ case bitcoinScriptTypeBCHScript:
default:
sl.ReportError(address, "Address[unsupported]", "Address[unsupported]", "bspk_as", "")
}
}
+// validateBitcoinTransactionVinVout enforces min=1 on Vin and Vout at the
+// struct-level so that chains like Zcash can opt out by not registering it.
+func validateBitcoinTransactionVinVout(sl validator.StructLevel) {
+ tx := sl.Current().Interface().(BitcoinTransaction)
+ if len(tx.Vin) == 0 {
+ sl.ReportError(tx.Vin, "Vin", "Vin", "min", "1")
+ }
+ if len(tx.Vout) == 0 {
+ sl.ReportError(tx.Vout, "Vout", "Vout", "min", "1")
+ }
+}
+
func NewBitcoinNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
v := validator.New()
v.RegisterStructValidation(validateBitcoinScriptPubKey, BitcoinScriptPubKey{})
+ v.RegisterStructValidation(validateBitcoinTransactionVinVout, BitcoinTransaction{})
+ var p2pkhVersionByte byte = 0x00
+ if params.Config.Chain.Blockchain == common.Blockchain_BLOCKCHAIN_DOGECOIN {
+ p2pkhVersionByte = 0x1e
+ }
return &bitcoinNativeParserImpl{
- logger: log.WithPackage(params.Logger),
- validate: v,
+ logger: log.WithPackage(params.Logger),
+ validate: v,
+ p2pkhVersionByte: p2pkhVersionByte,
}, nil
}
-func (b *bitcoinNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+func (b *bitcoinNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block, opts ...internal.ParseOption) (*api.NativeBlock, error) {
metadata := rawBlock.GetMetadata()
if metadata == nil {
return nil, xerrors.New("metadata not found")
@@ -219,17 +285,41 @@ func (b *bitcoinNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.
return nil, xerrors.New("bitcoin blobdata not found")
}
+ optView := internal.ResolveParseOptions(opts)
+
+ rawHeader := blobdata.GetHeader()
+
var block BitcoinBlock
- if err := json.Unmarshal(blobdata.GetHeader(), &block); err != nil {
+ if err := json.Unmarshal(rawHeader, &block); err != nil {
return nil, xerrors.Errorf("failed to parse bitcoin block with %+v: %w", metadata, err)
}
+ // Apply per-tx preprocessing (e.g. Dash/Zcash hash backfill) before
+ // validation so the validator sees the normalized form.
+ if b.preprocessTx != nil {
+ for _, tx := range block.Tx {
+ b.preprocessTx(tx)
+ }
+ }
+
if err := b.validateStruct(block); err != nil {
return nil, xerrors.Errorf("failed to validate bitcoin block %+v: %w", metadata, err)
}
+ // If a chain-specific filter is set (Zcash shielded-tx drop),
+ // compute a keep-mask by classifying the raw tx JSON from the
+ // header. Streaming applies the same filter per-tx as it decodes.
+ var keepMask []bool
+ if b.txFilter != nil {
+ mask, err := b.buildTxKeepMask(rawHeader, len(block.Tx))
+ if err != nil {
+ return nil, xerrors.Errorf("failed to build tx keep mask for %+v: %w", metadata, err)
+ }
+ keepMask = mask
+ }
+
header := block.GetApiBitcoinHeader()
- transactions, err := b.parseTransactions(blobdata, block.Tx)
+ transactions, err := b.parseTransactions(blobdata, block.Tx, keepMask, optView)
if err != nil {
return nil, xerrors.Errorf("parseTransactions failed for %+v: %w", metadata, err)
}
@@ -253,44 +343,113 @@ func (b *bitcoinNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.
}, nil
}
+// buildTxKeepMask applies b.txFilter to each raw tx JSON and returns a
+// boolean per tx: true = keep, false = drop. The raw tx bytes are
+// pulled from the block header JSON so the filter can inspect fields
+// not present in the shared BitcoinTransaction struct (e.g. Zcash
+// shielded outputs).
+func (b *bitcoinNativeParserImpl) buildTxKeepMask(rawHeader []byte, expectedTxCount int) ([]bool, error) {
+ var filterBlock struct {
+ Tx []json.RawMessage `json:"tx"`
+ }
+ if err := json.Unmarshal(rawHeader, &filterBlock); err != nil {
+ return nil, xerrors.Errorf("failed to parse raw header for tx filter: %w", err)
+ }
+ if len(filterBlock.Tx) != expectedTxCount {
+ return nil, xerrors.Errorf("tx count mismatch between parsed block and filter view (parsed=%d filter=%d)", expectedTxCount, len(filterBlock.Tx))
+ }
+ mask := make([]bool, expectedTxCount)
+ for i, raw := range filterBlock.Tx {
+ keep, err := b.txFilter(raw)
+ if err != nil {
+ return nil, xerrors.Errorf("tx filter failed at [%d]: %w", i, err)
+ }
+ mask[i] = keep
+ }
+ return mask, nil
+}
+
func (b *bitcoinNativeParserImpl) GetTransaction(ctx context.Context, nativeBlock *api.NativeBlock, transactionHash string) (*api.NativeTransaction, error) {
return nil, internal.ErrNotImplemented
}
-func (b *bitcoinNativeParserImpl) parseTransactions(
- data *api.BitcoinBlobdata, rawTransactions []*BitcoinTransaction,
-) ([]*api.BitcoinTransaction, error) {
+// buildInputMetadataMap builds a lookup table from input transaction data in
+// blobdata. Each entry maps a transaction ID to its referenced outputs, which
+// are used later to resolve input values and compute fees.
+//
+// Used by the non-streaming ParseBlock path. Streaming callers prefer
+// buildMetadataForGroup which scopes the allocation to one tx's worth
+// of prev-outs.
+func (b *bitcoinNativeParserImpl) buildInputMetadataMap(
+ data *api.BitcoinBlobdata, optView internal.ParseOptionsView,
+) (map[string][]*api.BitcoinTransactionOutput, error) {
metadataMap := make(map[string][]*api.BitcoinTransactionOutput)
for i, rawTransaction := range data.GetInputTransactions() {
- for j, input := range rawTransaction.GetData() {
- var inputTx BitcoinInputTransactionLit
- if err := json.Unmarshal(input, &inputTx); err != nil {
- return nil, xerrors.Errorf("failed to parse input transaction on [%d][%d]: %w", i, j, err)
- }
+ if err := b.appendGroupToMap(rawTransaction, optView, metadataMap); err != nil {
+ return nil, xerrors.Errorf("group [%d]: %w", i, err)
+ }
+ }
+ return metadataMap, nil
+}
- if err := b.validateStruct(inputTx); err != nil {
- return nil, xerrors.Errorf("failed to validate bitcoin input transaction %v: %w", inputTx.TxId, err)
- }
+// buildMetadataForGroup builds a metadata lookup map for one tx's
+// prev-output transactions. Scoping the map to a single tx's worth
+// lets the streaming parser drop both the group bytes and the map
+// after each tx is yielded, keeping peak memory O(largest tx's
+// prev-outs) instead of O(all prev-outs).
+func (b *bitcoinNativeParserImpl) buildMetadataForGroup(
+ group *api.RepeatedBytes, optView internal.ParseOptionsView,
+) (map[string][]*api.BitcoinTransactionOutput, error) {
+ if group == nil {
+ return nil, nil
+ }
+ metadataMap := make(map[string][]*api.BitcoinTransactionOutput, len(group.GetData()))
+ if err := b.appendGroupToMap(group, optView, metadataMap); err != nil {
+ return nil, err
+ }
+ return metadataMap, nil
+}
- if len(inputTx.Vout) != 1 {
- return nil, xerrors.Errorf("unexpected length of input transaction's output (expected=1, len=%d)", len(inputTx.Vout))
- }
+// appendGroupToMap decodes every prev-tx JSON in the group and appends
+// its single vout entry to metadataMap under the prev-tx's txid.
+func (b *bitcoinNativeParserImpl) appendGroupToMap(
+ group *api.RepeatedBytes,
+ optView internal.ParseOptionsView,
+ metadataMap map[string][]*api.BitcoinTransactionOutput,
+) error {
+ for j, input := range group.GetData() {
+ var inputTx BitcoinInputTransactionLit
+ if err := json.Unmarshal(input, &inputTx); err != nil {
+ return xerrors.Errorf("failed to parse input transaction [%d]: %w", j, err)
+ }
- inputTxId := inputTx.TxId.Value()
- if _, ok := metadataMap[inputTxId]; !ok {
- metadataMap[inputTxId] = make([]*api.BitcoinTransactionOutput, 0)
- }
+ if err := b.validateStruct(inputTx); err != nil {
+ return xerrors.Errorf("failed to validate bitcoin input transaction %v: %w", inputTx.TxId, err)
+ }
- outputTx, err := inputTx.Vout[0].ToApiBitcoinTransactionOutput()
- if err != nil {
- return nil, xerrors.Errorf("failed to convert to transaction output: %w", err)
- }
+ if len(inputTx.Vout) != 1 {
+ return xerrors.Errorf("unexpected length of input transaction's output (expected=1, len=%d)", len(inputTx.Vout))
+ }
- metadataMap[inputTxId] = append(metadataMap[inputTxId], outputTx)
+ inputTxId := inputTx.TxId.Value()
+ outputTx, err := inputTx.Vout[0].ToApiBitcoinTransactionOutput(b.p2pkhVersionByte, optView)
+ if err != nil {
+ return xerrors.Errorf("failed to convert to transaction output: %w", err)
}
+ metadataMap[inputTxId] = append(metadataMap[inputTxId], outputTx)
}
+ return nil
+}
- transactions, err := b.parseApiBitcoinTransactions(rawTransactions, metadataMap)
+func (b *bitcoinNativeParserImpl) parseTransactions(
+ data *api.BitcoinBlobdata, rawTransactions []*BitcoinTransaction, keepMask []bool, optView internal.ParseOptionsView,
+) ([]*api.BitcoinTransaction, error) {
+ metadataMap, err := b.buildInputMetadataMap(data, optView)
+ if err != nil {
+ return nil, err
+ }
+
+ transactions, err := b.parseApiBitcoinTransactions(rawTransactions, keepMask, metadataMap, b.p2pkhVersionByte, optView)
if err != nil {
return nil, err
}
@@ -299,18 +458,20 @@ func (b *bitcoinNativeParserImpl) parseTransactions(
}
func (b *bitcoinNativeParserImpl) parseApiBitcoinTransactions(
- rawTransactions []*BitcoinTransaction, metadataMap map[string][]*api.BitcoinTransactionOutput,
+ rawTransactions []*BitcoinTransaction, keepMask []bool,
+ metadataMap map[string][]*api.BitcoinTransactionOutput, p2pkhVersionByte byte, optView internal.ParseOptionsView,
) ([]*api.BitcoinTransaction, error) {
- transactions := make([]*api.BitcoinTransaction, len(rawTransactions))
+ transactions := make([]*api.BitcoinTransaction, 0, len(rawTransactions))
for i, rawTx := range rawTransactions {
- transaction, err := rawTx.ToApiBitcoinTransaction(i, metadataMap)
+ if keepMask != nil && !keepMask[i] {
+ continue
+ }
+ transaction, err := rawTx.ToApiBitcoinTransaction(i, metadataMap, p2pkhVersionByte, optView)
if err != nil {
return nil, err
}
-
- transactions[i] = transaction
+ transactions = append(transactions, transaction)
}
-
return transactions, nil
}
@@ -340,6 +501,7 @@ func (b *BitcoinBlock) GetApiBitcoinHeader() *api.BitcoinHeader {
Time: b.Time.Value(),
MedianTime: b.MedianTime.Value(),
Nonce: b.Nonce.Value(),
+ RawNonce: b.Nonce.RawValue(),
Bits: b.Bits.Value(),
Difficulty: b.Difficulty.String(),
ChainWork: b.ChainWork.Value(),
@@ -352,13 +514,13 @@ func (b *BitcoinBlock) GetApiBitcoinHeader() *api.BitcoinHeader {
}
}
-func (t *BitcoinTransaction) ToApiBitcoinTransaction(index int, metadataMap map[string][]*api.BitcoinTransactionOutput) (*api.BitcoinTransaction, error) {
- vin, err := parseVin(t.Vin, metadataMap)
+func (t *BitcoinTransaction) ToApiBitcoinTransaction(index int, metadataMap map[string][]*api.BitcoinTransactionOutput, p2pkhVersionByte byte, optView internal.ParseOptionsView) (*api.BitcoinTransaction, error) {
+ vin, err := parseVin(t.Vin, metadataMap, optView)
if err != nil {
return nil, err
}
- vout, err := parseVout(t.Vout)
+ vout, err := parseVout(t.Vout, p2pkhVersionByte, optView)
if err != nil {
return nil, err
}
@@ -383,8 +545,13 @@ func (t *BitcoinTransaction) ToApiBitcoinTransaction(index int, metadataMap map[
fee = inputValue - outputValue
}
+ hexValue := t.Hex.Value()
+ if optView.SkipScripts() {
+ hexValue = ""
+ }
+
return &api.BitcoinTransaction{
- Hex: t.Hex.Value(),
+ Hex: hexValue,
TransactionId: t.TxId.Value(),
Hash: t.Hash.Value(),
Size: t.Size.Value(),
@@ -407,10 +574,10 @@ func (t *BitcoinTransaction) ToApiBitcoinTransaction(index int, metadataMap map[
}, nil
}
-func parseVin(vin []*BitcoinTransactionInput, metadataMap map[string][]*api.BitcoinTransactionOutput) ([]*api.BitcoinTransactionInput, error) {
+func parseVin(vin []*BitcoinTransactionInput, metadataMap map[string][]*api.BitcoinTransactionOutput, optView internal.ParseOptionsView) ([]*api.BitcoinTransactionInput, error) {
inputs := make([]*api.BitcoinTransactionInput, len(vin))
for i, inputTx := range vin {
- input := inputTx.ToApiBitcoinTransactionInput(uint64(i))
+ input := inputTx.ToApiBitcoinTransactionInput(uint64(i), optView)
// Note that there is no transaction ID for coinbase transaction
if input.TransactionId != "" && metadataMap != nil {
outputs, ok := metadataMap[input.TransactionId]
@@ -436,9 +603,12 @@ func parseVin(vin []*BitcoinTransactionInput, metadataMap map[string][]*api.Bitc
return inputs, nil
}
-func (i *BitcoinTransactionInput) ToApiBitcoinTransactionInput(index uint64) *api.BitcoinTransactionInput {
- scriptSig := i.ScriptSig.ToApiBitcoinScriptSignature()
- witnesses := hexStringsToStrings(i.TxInWitness)
+func (i *BitcoinTransactionInput) ToApiBitcoinTransactionInput(index uint64, optView internal.ParseOptionsView) *api.BitcoinTransactionInput {
+ scriptSig := i.ScriptSig.ToApiBitcoinScriptSignature(optView)
+ var witnesses []string
+ if !optView.SkipWitnesses() {
+ witnesses = hexStringsToStrings(i.TxInWitness)
+ }
return &api.BitcoinTransactionInput{
Coinbase: i.Coinbase.Value(),
TransactionId: i.TxId.Value(),
@@ -463,10 +633,10 @@ func hexStringsToStrings(hexStrings []BitcoinHexString) []string {
return result
}
-func parseVout(vout []*BitcoinTransactionOutput) ([]*api.BitcoinTransactionOutput, error) {
+func parseVout(vout []*BitcoinTransactionOutput, p2pkhVersionByte byte, optView internal.ParseOptionsView) ([]*api.BitcoinTransactionOutput, error) {
outputs := make([]*api.BitcoinTransactionOutput, len(vout))
for i, outputTx := range vout {
- output, err := outputTx.ToApiBitcoinTransactionOutput()
+ output, err := outputTx.ToApiBitcoinTransactionOutput(p2pkhVersionByte, optView)
if err != nil {
return nil, xerrors.Errorf("failed to convert transaction output: %w", err)
}
@@ -477,13 +647,13 @@ func parseVout(vout []*BitcoinTransactionOutput) ([]*api.BitcoinTransactionOutpu
return outputs, nil
}
-func (o *BitcoinTransactionOutput) ToApiBitcoinTransactionOutput() (*api.BitcoinTransactionOutput, error) {
+func (o *BitcoinTransactionOutput) ToApiBitcoinTransactionOutput(p2pkhVersionByte byte, optView internal.ParseOptionsView) (*api.BitcoinTransactionOutput, error) {
value, err := btcutil.NewAmount(o.Value.Value())
if err != nil {
return nil, xerrors.Errorf("failed to convert value %v to btc amount: %w", o.Value.Value(), err)
}
- scriptPubKey, err := o.ScriptPubKey.ToApiBitcoinScriptPublicKey()
+ scriptPubKey, err := o.ScriptPubKey.ToApiBitcoinScriptPublicKey(p2pkhVersionByte, optView)
if err != nil {
return nil, xerrors.Errorf("failed to convert script public key: %w", err)
}
@@ -495,7 +665,7 @@ func (o *BitcoinTransactionOutput) ToApiBitcoinTransactionOutput() (*api.Bitcoin
}, nil
}
-func (k *BitcoinScriptPubKey) ToApiBitcoinScriptPublicKey() (*api.BitcoinScriptPublicKey, error) {
+func (k *BitcoinScriptPubKey) ToApiBitcoinScriptPublicKey(p2pkhVersionByte byte, optView internal.ParseOptionsView) (*api.BitcoinScriptPublicKey, error) {
if k == nil {
return nil, nil
}
@@ -516,19 +686,27 @@ func (k *BitcoinScriptPubKey) ToApiBitcoinScriptPublicKey() (*api.BitcoinScriptP
Address: k.Address.Value(),
}
- transformedScriptPublicKey, err := transformScriptPublicKey(scriptPublicKey)
+ transformedScriptPublicKey, err := transformScriptPublicKey(scriptPublicKey, p2pkhVersionByte)
if err != nil {
return nil, err
}
+ if optView.SkipScripts() {
+ // Drop the assembly/hex after any address derivation that needs them.
+ transformedScriptPublicKey.Assembly = ""
+ transformedScriptPublicKey.Hex = ""
+ }
+
return transformedScriptPublicKey, nil
}
-func (s *BitcoinScriptSig) ToApiBitcoinScriptSignature() *api.BitcoinScriptSignature {
+func (s *BitcoinScriptSig) ToApiBitcoinScriptSignature(optView internal.ParseOptionsView) *api.BitcoinScriptSignature {
if s == nil {
return nil
}
-
+ if optView.SkipScripts() {
+ return &api.BitcoinScriptSignature{}
+ }
return &api.BitcoinScriptSignature{
Assembly: s.Asm.Value(),
Hex: s.Hex.Value(),
@@ -551,6 +729,39 @@ func (v BitcoinQuantity) Value() uint64 {
return uint64(v)
}
+func (v *BitcoinNonce) UnmarshalJSON(input []byte) error {
+ // Try as number first (Bitcoin, Litecoin, etc.)
+ var num uint64
+ if err := json.Unmarshal(input, &num); err == nil {
+ v.value = num
+ v.isString = false
+ return nil
+ }
+
+ // Fall back to string (Zcash Equihash nonce)
+ var s string
+ if err := json.Unmarshal(input, &s); err != nil {
+ return err
+ }
+ v.strValue = s
+ v.isString = true
+ return nil
+}
+
+func (v BitcoinNonce) Value() uint64 {
+ return v.value
+}
+
+// RawValue returns the nonce as a string, preserving the original format.
+// For numeric nonces (Bitcoin), returns the decimal string representation.
+// For hex string nonces (Zcash), returns the original hex string.
+func (v BitcoinNonce) RawValue() string {
+ if v.isString {
+ return v.strValue
+ }
+ return fmt.Sprintf("%d", v.value)
+}
+
func (v BitcoinDecimalQuantity) String() string {
return fmt.Sprintf("%.2f", v)
}
@@ -577,7 +788,7 @@ func (v *BitcoinTransactionLit) UnmarshalJSON(input []byte) error {
}
func transformScriptPublicKey(
- scriptPubKey *api.BitcoinScriptPublicKey,
+ scriptPubKey *api.BitcoinScriptPublicKey, p2pkhVersionByte byte,
) (*api.BitcoinScriptPublicKey, error) {
switch scriptPubKey.Type {
case bitcoinScriptTypePubKey:
@@ -586,7 +797,7 @@ func transformScriptPublicKey(
}
// Attempt to fall back to parsing the address from the script.
- account, err := parseAccountFromPubKeyScript(scriptPubKey)
+ account, err := parseAccountFromPubKeyScript(scriptPubKey, p2pkhVersionByte)
if err != nil {
return nil, err
}
@@ -599,7 +810,7 @@ func transformScriptPublicKey(
}
func parseAccountFromPubKeyScript(
- scriptPubKey *api.BitcoinScriptPublicKey,
+ scriptPubKey *api.BitcoinScriptPublicKey, p2pkhVersionByte byte,
) (string, error) {
if scriptPubKey.Type != bitcoinScriptTypePubKey {
return "", xerrors.New("not of type pubkey")
@@ -620,7 +831,7 @@ func parseAccountFromPubKeyScript(
_, _ = sha256Hash.Write(pubKey)
ripemd160Hash := ripemd160.New()
_, _ = ripemd160Hash.Write(sha256Hash.Sum(nil))
- address := base58.CheckEncode(ripemd160Hash.Sum(nil), 0)
+ address := base58.CheckEncode(ripemd160Hash.Sum(nil), p2pkhVersionByte)
return address, nil
}
diff --git a/internal/blockchain/parser/bitcoin/bitcoin_native_bench_test.go b/internal/blockchain/parser/bitcoin/bitcoin_native_bench_test.go
new file mode 100644
index 00000000..83b94205
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/bitcoin_native_bench_test.go
@@ -0,0 +1,99 @@
+package bitcoin
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "testing"
+
+ "go.uber.org/fx"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+)
+
+// BenchmarkBitcoinParseBlock measures memory + allocations for a single
+// ParseBlock call on a witness-heavy mainnet block (15MB fixture, block
+// 731379). Sub-benchmarks vary which ParseOptions are set. Run with:
+//
+// go test -run=^$ -bench=BenchmarkBitcoinParseBlock -benchmem \
+// ./internal/blockchain/parser/bitcoin/
+func BenchmarkBitcoinParseBlock(b *testing.B) {
+ var parser internal.NativeParser
+ app := testapp.New(b,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_BITCOIN, common.Network_NETWORK_BITCOIN_MAINNET),
+ fx.Provide(NewBitcoinNativeParser),
+ fx.Populate(&parser),
+ )
+ defer app.Close()
+
+ rawBlock, err := testutil.LoadRawBlock("parser/bitcoin/raw_block_731379.json")
+ if err != nil {
+ b.Fatalf("LoadRawBlock: %v", err)
+ }
+
+ ctx := context.Background()
+
+ cases := []struct {
+ name string
+ opts []internal.ParseOption
+ }{
+ {"baseline", nil},
+ {"skip_scripts", []internal.ParseOption{internal.WithSkipScripts()}},
+ {"skip_witnesses", []internal.ParseOption{internal.WithSkipWitnesses()}},
+ {"skip_scripts_witnesses", []internal.ParseOption{internal.WithSkipScripts(), internal.WithSkipWitnesses()}},
+ }
+
+ for _, c := range cases {
+ b.Run(c.name, func(b *testing.B) {
+ b.ReportAllocs()
+ b.ResetTimer()
+ for i := 0; i < b.N; i++ {
+ if _, err := parser.ParseBlock(ctx, rawBlock, c.opts...); err != nil {
+ b.Fatalf("ParseBlock: %v", err)
+ }
+ }
+ })
+ }
+}
+
+// BenchmarkBitcoinStreamBlockIter measures StreamBlockIter on the same
+// 15MB witness-heavy mainnet block. The loop drops each transaction
+// reference immediately so allocations-per-op reflect streaming overhead
+// rather than aggregate block size.
+func BenchmarkBitcoinStreamBlockIter(b *testing.B) {
+ var parser internal.NativeParser
+ app := testapp.New(b,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_BITCOIN, common.Network_NETWORK_BITCOIN_MAINNET),
+ fx.Provide(NewBitcoinNativeParser),
+ fx.Populate(&parser),
+ )
+ defer app.Close()
+
+ impl := parser.(*bitcoinNativeParserImpl)
+
+ rawBlock, err := testutil.LoadRawBlock("parser/bitcoin/raw_block_731379.json")
+ if err != nil {
+ b.Fatalf("LoadRawBlock: %v", err)
+ }
+
+ headerBytes := rawBlock.GetBitcoin().GetHeader()
+ loadGroup := NewInMemoryInputTxGroupLoader(rawBlock.GetBitcoin().GetInputTransactions())
+ ctx := context.Background()
+
+ b.ResetTimer()
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ opener := func() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(headerBytes)), nil
+ }
+ stream := impl.StreamBlockIter(ctx, opener, loadGroup)
+ for _, err := range stream.Transactions() {
+ if err != nil {
+ b.Fatalf("stream: %v", err)
+ }
+ }
+ }
+}
diff --git a/internal/blockchain/parser/bitcoin/bitcoin_native_options_test.go b/internal/blockchain/parser/bitcoin/bitcoin_native_options_test.go
new file mode 100644
index 00000000..72b49a92
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/bitcoin_native_options_test.go
@@ -0,0 +1,120 @@
+package bitcoin
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/fx"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+)
+
+func newBitcoinParser(t testing.TB) (internal.NativeParser, testapp.TestApp) {
+ t.Helper()
+ var parser internal.NativeParser
+ app := testapp.New(t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_BITCOIN, common.Network_NETWORK_BITCOIN_MAINNET),
+ fx.Provide(NewBitcoinNativeParser),
+ fx.Populate(&parser),
+ )
+ return parser, app
+}
+
+// TestBitcoinParseBlockOptions verifies that each ParseOption produces the
+// expected field-level changes, and that the protobuf-serialized output
+// shrinks meaningfully when options are used.
+func TestBitcoinParseBlockOptions(t *testing.T) {
+ require := require.New(t)
+
+ parser, app := newBitcoinParser(t)
+ defer app.Close()
+
+ rawBlock, err := testutil.LoadRawBlock("parser/bitcoin/raw_block_731379.json")
+ require.NoError(err)
+
+ ctx := context.Background()
+
+ base, err := parser.ParseBlock(ctx, rawBlock)
+ require.NoError(err)
+ require.NotNil(base)
+
+ noScripts, err := parser.ParseBlock(ctx, rawBlock, internal.WithSkipScripts())
+ require.NoError(err)
+
+ noWitnesses, err := parser.ParseBlock(ctx, rawBlock, internal.WithSkipWitnesses())
+ require.NoError(err)
+
+ noBoth, err := parser.ParseBlock(ctx, rawBlock, internal.WithSkipScripts(), internal.WithSkipWitnesses())
+ require.NoError(err)
+
+ // Field-level assertions.
+ for i, tx := range noScripts.GetBitcoin().GetTransactions() {
+ require.Empty(tx.Hex, "tx[%d] Hex should be empty with WithSkipScripts", i)
+ for j, in := range tx.Inputs {
+ if in.ScriptSignature != nil {
+ require.Empty(in.ScriptSignature.Assembly, "tx[%d].Inputs[%d].ScriptSignature.Assembly", i, j)
+ require.Empty(in.ScriptSignature.Hex, "tx[%d].Inputs[%d].ScriptSignature.Hex", i, j)
+ }
+ }
+ for j, out := range tx.Outputs {
+ if out.ScriptPublicKey != nil {
+ require.Empty(out.ScriptPublicKey.Assembly, "tx[%d].Outputs[%d].ScriptPublicKey.Assembly", i, j)
+ require.Empty(out.ScriptPublicKey.Hex, "tx[%d].Outputs[%d].ScriptPublicKey.Hex", i, j)
+ }
+ }
+ }
+
+ for i, tx := range noWitnesses.GetBitcoin().GetTransactions() {
+ for j, in := range tx.Inputs {
+ require.Empty(in.TransactionInputWitnesses, "tx[%d].Inputs[%d].TransactionInputWitnesses", i, j)
+ }
+ }
+
+ // Serialized-size comparison: the output with options dropped should be
+ // meaningfully smaller than baseline.
+ baseSize := proto.Size(base)
+ noScriptsSize := proto.Size(noScripts)
+ noWitnessesSize := proto.Size(noWitnesses)
+ noBothSize := proto.Size(noBoth)
+
+ t.Logf("serialized NativeBlock sizes (bytes): baseline=%d skip_scripts=%d (-%d%%) skip_witnesses=%d (-%d%%) skip_both=%d (-%d%%)",
+ baseSize,
+ noScriptsSize, pct(baseSize, noScriptsSize),
+ noWitnessesSize, pct(baseSize, noWitnessesSize),
+ noBothSize, pct(baseSize, noBothSize),
+ )
+
+ require.Less(noScriptsSize, baseSize, "WithSkipScripts should shrink serialized NativeBlock")
+ require.Less(noWitnessesSize, baseSize, "WithSkipWitnesses should shrink serialized NativeBlock")
+ require.LessOrEqual(noBothSize, noScriptsSize)
+ require.LessOrEqual(noBothSize, noWitnessesSize)
+
+ // Other fields stay intact.
+ require.Equal(base.Hash, noScripts.Hash)
+ require.Equal(base.Height, noScripts.Height)
+ require.Equal(len(base.GetBitcoin().GetTransactions()), len(noScripts.GetBitcoin().GetTransactions()))
+
+ // Address derivation still works when scripts are skipped: any tx output
+ // whose baseline carries a non-empty address should still have it with
+ // SkipScripts, because Assembly is preserved just long enough for the
+ // pubkey-script derivation path.
+ for i, tx := range base.GetBitcoin().GetTransactions() {
+ for j, out := range tx.Outputs {
+ baseAddr := out.GetScriptPublicKey().GetAddress()
+ skipAddr := noScripts.GetBitcoin().GetTransactions()[i].Outputs[j].GetScriptPublicKey().GetAddress()
+ require.Equal(baseAddr, skipAddr, "tx[%d].Outputs[%d] address should survive SkipScripts", i, j)
+ }
+ }
+}
+
+func pct(base, cur int) int {
+ if base <= 0 {
+ return 0
+ }
+ return (base - cur) * 100 / base
+}
diff --git a/internal/blockchain/parser/bitcoin/bitcoin_native_stream.go b/internal/blockchain/parser/bitcoin/bitcoin_native_stream.go
new file mode 100644
index 00000000..11f88a52
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/bitcoin_native_stream.go
@@ -0,0 +1,424 @@
+package bitcoin
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "io"
+ "iter"
+ "sync"
+
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// InputTxGroupLoader returns the prev-output transactions
+// corresponding to the inputs of the i-th block transaction. The
+// returned RepeatedBytes may be nil for txs with no inputs (e.g.
+// coinbase) or when no input_transactions are available for the
+// block.
+//
+// Callers may implement this against in-memory data (adapting
+// *api.BitcoinBlobdata.InputTransactions) or against a spooled copy
+// of the proto bytes (per-group seek+proto.Unmarshal). The lazy
+// spool-based implementation is what drops parser peak RAM from
+// ~O(block) to ~O(largest tx's input_transactions) on zcash.
+//
+// Alias of internal.BitcoinInputTxGroupLoader so the streamer
+// interface and its bitcoin-family implementation share the same
+// underlying type.
+type InputTxGroupLoader = internal.BitcoinInputTxGroupLoader
+
+// NewInMemoryInputTxGroupLoader adapts the legacy in-memory
+// []*api.RepeatedBytes layout to the loader interface. Memory
+// characteristics are unchanged vs directly passing the blobdata.
+func NewInMemoryInputTxGroupLoader(groups []*api.RepeatedBytes) InputTxGroupLoader {
+ return func(i int) (*api.RepeatedBytes, error) {
+ if i < 0 || i >= len(groups) {
+ return nil, nil
+ }
+ return groups[i], nil
+ }
+}
+
+// BlockStream is a bitcoin-package alias for internal.BitcoinNativeStream.
+// The stream is an iterator-based view over a bitcoin-family block.
+//
+// Ownership of backing resources (spool file, decompressors) lives
+// above this type — see internal.NativeStreamedBlock (returned by
+// Parser.ParseStreamNative) for the full contract that includes Close.
+//
+// # Header() and the "free after iteration" optimization
+//
+// Header() can be called before, during, or after Transactions()
+// iteration. The cost depends on the order of operations:
+//
+// - Call Header() AFTER Transactions() has been fully consumed: free.
+// The header is captured as a side effect of the streaming parse
+// (we already had to reconstitute it to return from the underlying
+// decode pass). Subsequent Header() calls return the cached value.
+//
+// - Call Header() BEFORE (or WITHOUT) iterating: a dedicated
+// header-only pass over the block's JSON is performed once. The
+// pass walks top-level fields and skips the "tx" array via
+// token-depth tracking without decoding its elements, then
+// reconstitutes a BitcoinHeader from the remaining fields. The
+// result is cached.
+//
+// - Call Header() DURING iteration (e.g. mid-range inside the loop):
+// treated as "before" — the dedicated pass runs. Avoid this if
+// possible.
+//
+// Callers that need both transactions and header should iterate first
+// and read the header after the range loop to avoid the extra pass.
+//
+// BlockStream is not safe for concurrent use across goroutines. The
+// typical single-goroutine range-loop pattern is always safe.
+type BlockStream = internal.BitcoinNativeStream
+
+// errIterStopped is an internal sentinel used to unwind the decode pass
+// when the iterator consumer breaks. It is not exposed to callers.
+var errIterStopped = errors.New("bitcoin: block stream iteration stopped by consumer")
+
+type bitcoinBlockStream struct {
+ ctx context.Context
+ parser *bitcoinNativeParserImpl
+ loadGroup InputTxGroupLoader
+ opts []internal.ParseOption
+ openReader func() (io.ReadCloser, error)
+
+ mu sync.Mutex
+ header *api.BitcoinHeader
+ headerErr error
+ headerSet bool
+}
+
+// StreamBlockIter returns a BlockStream that consumes block JSON via
+// the supplied reader factory. The factory may be called more than
+// once — once for each Transactions() iteration and at most once more
+// for an early Header() call. It must return a freshly-positioned
+// reader on each call.
+//
+// loadGroup resolves prev-output transactions for each block tx. It
+// is invoked once per tx during iteration; callers that populate
+// InputTransactions in memory can use NewInMemoryInputTxGroupLoader,
+// while the disk-spooled DownloadStreamBitcoin path hands in a
+// loader that seeks into the spool file on demand. The loader's
+// return value for each call is allowed to be GC'd as soon as the
+// tx is yielded, so peak parser memory is bounded by the largest
+// single tx's prev-output bytes.
+func (b *bitcoinNativeParserImpl) StreamBlockIter(
+ ctx context.Context,
+ openReader func() (io.ReadCloser, error),
+ loadGroup InputTxGroupLoader,
+ opts ...internal.ParseOption,
+) BlockStream {
+ return &bitcoinBlockStream{
+ ctx: ctx,
+ parser: b,
+ loadGroup: loadGroup,
+ opts: opts,
+ openReader: openReader,
+ }
+}
+
+func (s *bitcoinBlockStream) Transactions() iter.Seq2[*api.BitcoinTransaction, error] {
+ return func(yield func(*api.BitcoinTransaction, error) bool) {
+ if s.openReader == nil {
+ yield(nil, xerrors.New("nil openReader"))
+ return
+ }
+
+ r, err := s.openReader()
+ if err != nil {
+ yield(nil, xerrors.Errorf("failed to open block reader: %w", err))
+ return
+ }
+ defer func() { _ = r.Close() }()
+
+ emit := func(tx *api.BitcoinTransaction) error {
+ if !yield(tx, nil) {
+ return errIterStopped
+ }
+ return nil
+ }
+
+ hdr, err := s.parser.decodeBlockStream(s.ctx, r, s.loadGroup, emit, s.opts...)
+ switch {
+ case errors.Is(err, errIterStopped):
+ // Consumer broke the range. Nothing to report; defers clean up.
+ case err != nil:
+ yield(nil, err)
+ default:
+ // Full consumption: cache header for a free subsequent Header() call.
+ s.mu.Lock()
+ if !s.headerSet {
+ s.header = hdr
+ s.headerSet = true
+ }
+ s.mu.Unlock()
+ }
+ }
+}
+
+func (s *bitcoinBlockStream) Header() (*api.BitcoinHeader, error) {
+ s.mu.Lock()
+ if s.headerSet {
+ defer s.mu.Unlock()
+ return s.header, s.headerErr
+ }
+ s.mu.Unlock()
+
+ // Slow path: dedicated header-only scan.
+ r, err := s.openReader()
+ if err != nil {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.headerSet = true
+ s.headerErr = xerrors.Errorf("failed to open block reader for header scan: %w", err)
+ return nil, s.headerErr
+ }
+ defer func() { _ = r.Close() }()
+
+ hdr, err := parseBitcoinHeaderSkippingTx(r)
+
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ // Another caller may have raced to populate the header via iteration.
+ // In that case keep the iteration-derived value (it is equivalent).
+ if !s.headerSet {
+ s.header = hdr
+ s.headerErr = err
+ s.headerSet = true
+ }
+ return s.header, s.headerErr
+}
+
+// decodeBlockStream decodes a bitcoin-family block JSON from r, invoking
+// emit once per transaction as the stream progresses. Only a single
+// BitcoinTransaction is held in memory at a time (plus a small buffer of
+// the non-"tx" header fields), so peak parser memory is
+// O(largest_tx + header) rather than O(block).
+//
+// The returned header is available only after all transactions are
+// emitted, because tx may precede or follow header fields in JSON.
+func (b *bitcoinNativeParserImpl) decodeBlockStream(
+ ctx context.Context,
+ r io.Reader,
+ loadGroup InputTxGroupLoader,
+ emit func(*api.BitcoinTransaction) error,
+ opts ...internal.ParseOption,
+) (*api.BitcoinHeader, error) {
+ optView := internal.ResolveParseOptions(opts)
+
+ dec := json.NewDecoder(r)
+ openTok, err := dec.Token()
+ if err != nil {
+ return nil, xerrors.Errorf("failed to read opening token: %w", err)
+ }
+ if d, ok := openTok.(json.Delim); !ok || d != '{' {
+ return nil, xerrors.Errorf("expected '{' at block start, got %v", openTok)
+ }
+
+ headerFields := make(map[string]json.RawMessage)
+ txIdx := 0
+
+ for dec.More() {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ keyTok, err := dec.Token()
+ if err != nil {
+ return nil, xerrors.Errorf("failed to read key token: %w", err)
+ }
+ key, ok := keyTok.(string)
+ if !ok {
+ return nil, xerrors.Errorf("expected string key, got %v", keyTok)
+ }
+
+ if key == "tx" {
+ openArr, err := dec.Token()
+ if err != nil {
+ return nil, xerrors.Errorf("failed to read tx opening: %w", err)
+ }
+ if d, ok := openArr.(json.Delim); !ok || d != '[' {
+ return nil, xerrors.Errorf("expected '[' for tx, got %v", openArr)
+ }
+ for dec.More() {
+ // When a chain-specific txFilter is set (e.g. Zcash
+ // shielded-tx drop), decode as raw JSON first so
+ // the filter can inspect fields not present in the
+ // shared BitcoinTransaction struct. Otherwise decode
+ // directly into BitcoinTransaction to avoid a
+ // two-stage decode.
+ //
+ // txIdx is always incremented (including for
+ // filtered txs) to preserve the source position as
+ // the tx's index, matching ParseBlock.
+ var rawTx BitcoinTransaction
+ if b.txFilter != nil {
+ var rawMsg json.RawMessage
+ if err := dec.Decode(&rawMsg); err != nil {
+ return nil, xerrors.Errorf("failed to decode tx[%d]: %w", txIdx, err)
+ }
+ keep, err := b.txFilter(rawMsg)
+ if err != nil {
+ return nil, xerrors.Errorf("tx filter failed at [%d]: %w", txIdx, err)
+ }
+ if !keep {
+ txIdx++
+ continue
+ }
+ if err := json.Unmarshal(rawMsg, &rawTx); err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal tx[%d]: %w", txIdx, err)
+ }
+ } else {
+ if err := dec.Decode(&rawTx); err != nil {
+ return nil, xerrors.Errorf("failed to decode tx[%d]: %w", txIdx, err)
+ }
+ }
+
+ // Chain-specific normalization (e.g. Dash/Zcash
+ // hash backfill) before conversion.
+ if b.preprocessTx != nil {
+ b.preprocessTx(&rawTx)
+ }
+
+ // Lazy: load only this tx's prev-output group,
+ // build a per-tx metadata map, and let it go
+ // out of scope immediately after parsing.
+ var metadataMap map[string][]*api.BitcoinTransactionOutput
+ if loadGroup != nil {
+ group, err := loadGroup(txIdx)
+ if err != nil {
+ return nil, xerrors.Errorf("load input tx group [%d]: %w", txIdx, err)
+ }
+ metadataMap, err = b.buildMetadataForGroup(group, optView)
+ if err != nil {
+ return nil, xerrors.Errorf("build metadata for tx[%d]: %w", txIdx, err)
+ }
+ }
+
+ apiTx, err := rawTx.ToApiBitcoinTransaction(txIdx, metadataMap, b.p2pkhVersionByte, optView)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to convert tx[%d]: %w", txIdx, err)
+ }
+ if err := emit(apiTx); err != nil {
+ return nil, err
+ }
+ txIdx++
+ }
+ if _, err := dec.Token(); err != nil {
+ return nil, xerrors.Errorf("failed to read tx closing: %w", err)
+ }
+ continue
+ }
+
+ // Header fields are small; buffer as json.RawMessage and reassemble
+ // into a BitcoinBlock at the end. This is well under 1 KB total.
+ var raw json.RawMessage
+ if err := dec.Decode(&raw); err != nil {
+ return nil, xerrors.Errorf("failed to decode header field %q: %w", key, err)
+ }
+ headerFields[key] = raw
+ }
+ if _, err := dec.Token(); err != nil {
+ return nil, xerrors.Errorf("failed to read block closing: %w", err)
+ }
+
+ // Reconstitute the header. Remove "tx" so validation does not require it
+ // to be present here; transactions were already streamed.
+ delete(headerFields, "tx")
+ headerJSON, err := json.Marshal(headerFields)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to marshal header fields: %w", err)
+ }
+ var block BitcoinBlock
+ if err := json.Unmarshal(headerJSON, &block); err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal header: %w", err)
+ }
+ return block.GetApiBitcoinHeader(), nil
+}
+
+// parseBitcoinHeaderSkippingTx performs a one-pass JSON scan over r,
+// buffering every top-level field except "tx" (which is skipped via
+// token-depth tracking — no transaction values are allocated). The
+// remaining fields are reassembled into a BitcoinBlock struct and the
+// derived header is returned.
+func parseBitcoinHeaderSkippingTx(r io.Reader) (*api.BitcoinHeader, error) {
+ dec := json.NewDecoder(r)
+ openTok, err := dec.Token()
+ if err != nil {
+ return nil, xerrors.Errorf("read opening token: %w", err)
+ }
+ if d, ok := openTok.(json.Delim); !ok || d != '{' {
+ return nil, xerrors.Errorf("expected '{' at block start, got %v", openTok)
+ }
+
+ headerFields := make(map[string]json.RawMessage)
+ for dec.More() {
+ keyTok, err := dec.Token()
+ if err != nil {
+ return nil, xerrors.Errorf("read key token: %w", err)
+ }
+ key, ok := keyTok.(string)
+ if !ok {
+ return nil, xerrors.Errorf("expected string key, got %v", keyTok)
+ }
+ if key == "tx" {
+ if err := skipJSONValue(dec); err != nil {
+ return nil, xerrors.Errorf("skip tx array: %w", err)
+ }
+ continue
+ }
+ var raw json.RawMessage
+ if err := dec.Decode(&raw); err != nil {
+ return nil, xerrors.Errorf("decode header field %q: %w", key, err)
+ }
+ headerFields[key] = raw
+ }
+ if _, err := dec.Token(); err != nil {
+ return nil, xerrors.Errorf("read closing token: %w", err)
+ }
+
+ headerJSON, err := json.Marshal(headerFields)
+ if err != nil {
+ return nil, xerrors.Errorf("marshal header fields: %w", err)
+ }
+ var block BitcoinBlock
+ if err := json.Unmarshal(headerJSON, &block); err != nil {
+ return nil, xerrors.Errorf("unmarshal header: %w", err)
+ }
+ return block.GetApiBitcoinHeader(), nil
+}
+
+// skipJSONValue consumes exactly one JSON value (scalar or container)
+// from dec's current position, without allocating any sub-values.
+func skipJSONValue(dec *json.Decoder) error {
+ depth := 0
+ for {
+ tok, err := dec.Token()
+ if err != nil {
+ return err
+ }
+ d, ok := tok.(json.Delim)
+ if !ok {
+ if depth == 0 {
+ // Scalar at the top level — we're done.
+ return nil
+ }
+ continue
+ }
+ switch d {
+ case '[', '{':
+ depth++
+ case ']', '}':
+ depth--
+ if depth == 0 {
+ return nil
+ }
+ }
+ }
+}
diff --git a/internal/blockchain/parser/bitcoin/bitcoin_native_stream_parity_test.go b/internal/blockchain/parser/bitcoin/bitcoin_native_stream_parity_test.go
new file mode 100644
index 00000000..75ff40b4
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/bitcoin_native_stream_parity_test.go
@@ -0,0 +1,157 @@
+package bitcoin
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/fx"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/utils/fixtures"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// TestBitcoinFamilyStreamParseParity runs ParseBlock and StreamBlockIter
+// against the same fixture for every bitcoin-family chain and asserts
+// their transaction lists match exactly. This is the load-bearing test
+// that guarantees streaming honors chain-specific hooks (preprocessTx,
+// txFilter) — without it, overrides like Dash's hash backfill or
+// Zcash's shielded-tx filter can silently diverge from ParseBlock.
+func TestBitcoinFamilyStreamParseParity(t *testing.T) {
+ cases := []struct {
+ name string
+ blockchain common.Blockchain
+ network common.Network
+ newParser any // fx.Provide constructor
+ rawBlock func(t *testing.T) *api.Block
+ }{
+ {
+ name: "Bitcoin",
+ blockchain: common.Blockchain_BLOCKCHAIN_BITCOIN,
+ network: common.Network_NETWORK_BITCOIN_MAINNET,
+ newParser: NewBitcoinNativeParser,
+ rawBlock: func(t *testing.T) *api.Block {
+ rawBlock, err := testutil.LoadRawBlock("parser/bitcoin/raw_block_731379.json")
+ require.NoError(t, err)
+ return rawBlock
+ },
+ },
+ {
+ name: "Dash",
+ blockchain: common.Blockchain_BLOCKCHAIN_DASH,
+ network: common.Network_NETWORK_DASH_MAINNET,
+ newParser: NewDashNativeParser,
+ rawBlock: func(t *testing.T) *api.Block {
+ header, err := fixtures.ReadFile("parser/bitcoin/dash_get_block_pubkey_case.json")
+ require.NoError(t, err)
+ return &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_DASH,
+ Network: common.Network_NETWORK_DASH_MAINNET,
+ Metadata: bitcoinMetadata,
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{Header: header},
+ },
+ }
+ },
+ },
+ {
+ name: "Zcash",
+ blockchain: common.Blockchain_BLOCKCHAIN_ZCASH,
+ network: common.Network_NETWORK_ZCASH_MAINNET,
+ newParser: NewZcashNativeParser,
+ rawBlock: func(t *testing.T) *api.Block {
+ // Coinbase + shielded-only (no transparent txs with
+ // external inputs, avoids needing InputTransactions
+ // fixture plumbing). Exercises isTransparentZcashTx
+ // filter: coinbase kept, shielded dropped → 1 tx in
+ // output. Also exercises Dash/Zcash-style hash
+ // backfill since we omit the "hash" field.
+ return &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ZCASH,
+ Network: common.Network_NETWORK_ZCASH_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 1, Hash: "blockhash", ParentHash: "parenthash",
+ Height: 1, ParentHeight: 0,
+ },
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: []byte(`{
+ "hash":"blockhash",
+ "height":1,
+ "time":1,
+ "nTx":2,
+ "previousblockhash":"parenthash",
+ "tx":[
+ {
+ "txid":"coinbase-tx",
+ "vin":[{"coinbase":"01","sequence":4294967295}],
+ "vout":[{"value":12.5,"n":0,"scriptPubKey":{"asm":"OP_DUP OP_HASH160 1111111111111111111111111111111111111111 OP_EQUALVERIFY OP_CHECKSIG","hex":"76a914111111111111111111111111111111111111111188ac","type":"pubkeyhash","address":"t1coinbase"}}]
+ },
+ {
+ "txid":"privacy-tx",
+ "vin":[],
+ "vout":[],
+ "vShieldedSpend":[{"dummy":"value"}]
+ }
+ ]
+ }`),
+ },
+ },
+ }
+ },
+ },
+ }
+
+ for _, tc := range cases {
+ tc := tc
+ t.Run(tc.name, func(t *testing.T) {
+ require := require.New(t)
+
+ var parser internal.NativeParser
+ app := testapp.New(t,
+ testapp.WithBlockchainNetwork(tc.blockchain, tc.network),
+ fx.Provide(tc.newParser),
+ fx.Populate(&parser),
+ )
+ defer app.Close()
+
+ rawBlock := tc.rawBlock(t)
+
+ ctx := context.Background()
+ baseline, err := parser.ParseBlock(ctx, rawBlock)
+ require.NoError(err, "ParseBlock must succeed on fixture")
+ baseTxs := baseline.GetBitcoin().GetTransactions()
+
+ streamer, ok := parser.(internal.BitcoinStreamer)
+ require.True(ok, "parser %T must implement BitcoinStreamer", parser)
+
+ headerBytes := rawBlock.GetBitcoin().GetHeader()
+ opener := func() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(headerBytes)), nil
+ }
+ loadGroup := NewInMemoryInputTxGroupLoader(rawBlock.GetBitcoin().GetInputTransactions())
+ stream := streamer.StreamBlockIter(ctx, opener, loadGroup)
+
+ var streamed []*api.BitcoinTransaction
+ for tx, iterErr := range stream.Transactions() {
+ require.NoError(iterErr)
+ streamed = append(streamed, tx)
+ }
+
+ require.Equal(len(baseTxs), len(streamed), "tx count must match between ParseBlock and StreamBlockIter")
+ for i, baseTx := range baseTxs {
+ s := streamed[i]
+ require.Equal(baseTx.TransactionId, s.TransactionId, "%s: tx[%d] id mismatch", tc.name, i)
+ require.Equal(baseTx.Hash, s.Hash, "%s: tx[%d] hash mismatch (catches Dash/Zcash backfill gap)", tc.name, i)
+ require.Equal(baseTx.InputCount, s.InputCount, "%s: tx[%d] InputCount mismatch", tc.name, i)
+ require.Equal(baseTx.OutputCount, s.OutputCount, "%s: tx[%d] OutputCount mismatch", tc.name, i)
+ }
+ })
+ }
+}
diff --git a/internal/blockchain/parser/bitcoin/bitcoin_native_stream_prod_parity_test.go b/internal/blockchain/parser/bitcoin/bitcoin_native_stream_prod_parity_test.go
new file mode 100644
index 00000000..c7e5ee0c
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/bitcoin_native_stream_prod_parity_test.go
@@ -0,0 +1,723 @@
+package bitcoin
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "math/rand"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "path/filepath"
+ "runtime"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
+ storage_utils "github.com/coinbase/chainstorage/internal/storage/utils"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// TestProdParityBitcoinFamily downloads random blocks from the prod
+// S3 buckets for each bitcoin-family chain and asserts that the
+// full legacy pipeline (Download → proto.Unmarshal → ParseBlock)
+// and the full streaming pipeline (DownloadStream → walker → iter)
+// produce identical transaction lists.
+//
+// Timing and memory measurements cover the ENTIRE pipeline for each
+// path — read + decompress + parse/stream. A local httptest server
+// serves cached compressed blocks to the real downloader so the
+// measurements include the production code path rather than just
+// the final parse step.
+//
+// Gated by env var CHAINSTORAGE_PROD_PARITY=1 so it does not run in
+// default `go test` sweeps. Requires AWS_PROFILE=cipherowl-prod (or
+// equivalent credentials) and network access to S3 for the initial
+// cache population.
+//
+// Blocks are cached on disk under CHAINSTORAGE_PROD_PARITY_CACHE_DIR
+// (default ~/data/chainstorage/{chain}/) as their compressed bytes,
+// so re-runs skip the S3 fetch. Deterministic height selection via
+// seed makes the same cache reusable across runs.
+//
+// Tuning knobs:
+//
+// CHAINSTORAGE_PROD_PARITY_COUNT=1000 (blocks per chain, default 1000)
+// CHAINSTORAGE_PROD_PARITY_WORKERS=20 (concurrent downloads; forced to 1 when PROFILE_MEM=1)
+// CHAINSTORAGE_PROD_PARITY_SEED=42 (deterministic height selection)
+// CHAINSTORAGE_PROD_PARITY_CHAINS=bitcoin,dash,zcash
+// CHAINSTORAGE_PROD_PARITY_CACHE_DIR=~/data/chainstorage
+// CHAINSTORAGE_PROD_PARITY_PROFILE_MEM=1 (measure per-block peak heap for both paths; runs serially)
+func TestProdParityBitcoinFamily(t *testing.T) {
+ if os.Getenv("CHAINSTORAGE_PROD_PARITY") != "1" {
+ t.Skip("set CHAINSTORAGE_PROD_PARITY=1 (and AWS_PROFILE=cipherowl-prod) to run")
+ }
+
+ count := envInt("CHAINSTORAGE_PROD_PARITY_COUNT", 1000)
+ workers := envInt("CHAINSTORAGE_PROD_PARITY_WORKERS", 20)
+ seed := envInt("CHAINSTORAGE_PROD_PARITY_SEED", 42)
+ profileMem := os.Getenv("CHAINSTORAGE_PROD_PARITY_PROFILE_MEM") == "1"
+ if profileMem {
+ // Per-block heap measurement requires serial execution:
+ // HeapAlloc is process-global and concurrent workers pollute
+ // each other's peak readings.
+ workers = 1
+ }
+ cacheDir := os.Getenv("CHAINSTORAGE_PROD_PARITY_CACHE_DIR")
+ if cacheDir == "" {
+ home, err := os.UserHomeDir()
+ if err != nil {
+ t.Fatalf("resolve home dir: %v", err)
+ }
+ cacheDir = filepath.Join(home, "data", "chainstorage")
+ }
+
+ allChains := []prodChain{
+ {
+ name: "bitcoin",
+ blockchain: common.Blockchain_BLOCKCHAIN_BITCOIN,
+ network: common.Network_NETWORK_BITCOIN_MAINNET,
+ bucket: "co-chainstorage-bitcoin-mainnet-prod",
+ keyPrefix: "BLOCKCHAIN_BITCOIN/NETWORK_BITCOIN_MAINNET/2/",
+ fileExt: ".gzip",
+ maxHeight: 945_818,
+ compression: api.Compression_GZIP,
+ newParser: NewBitcoinNativeParser,
+ },
+ {
+ name: "dash",
+ blockchain: common.Blockchain_BLOCKCHAIN_DASH,
+ network: common.Network_NETWORK_DASH_MAINNET,
+ bucket: "co-chainstorage-dash-mainnet-prod",
+ keyPrefix: "BLOCKCHAIN_DASH/NETWORK_DASH_MAINNET/1/",
+ fileExt: ".zstd",
+ maxHeight: 2_457_707,
+ compression: api.Compression_ZSTD,
+ newParser: NewDashNativeParser,
+ },
+ {
+ name: "zcash",
+ blockchain: common.Blockchain_BLOCKCHAIN_ZCASH,
+ network: common.Network_NETWORK_ZCASH_MAINNET,
+ bucket: "co-chainstorage-zcash-mainnet-prod",
+ keyPrefix: "BLOCKCHAIN_ZCASH/NETWORK_ZCASH_MAINNET/1/",
+ fileExt: ".zstd",
+ maxHeight: 3_313_706,
+ compression: api.Compression_ZSTD,
+ newParser: NewZcashNativeParser,
+ },
+ }
+
+ selected := envChains(os.Getenv("CHAINSTORAGE_PROD_PARITY_CHAINS"), allChains)
+
+ ctx := context.Background()
+ awsCfg, err := awsconfig.LoadDefaultConfig(ctx)
+ if err != nil {
+ t.Fatalf("load aws config: %v", err)
+ }
+ s3client := s3.NewFromConfig(awsCfg)
+
+ for _, c := range selected {
+ c := c
+ chainCacheDir := filepath.Join(cacheDir, c.name)
+ if err := os.MkdirAll(chainCacheDir, 0o755); err != nil {
+ t.Fatalf("mkdir cache: %v", err)
+ }
+ t.Run(c.name, func(t *testing.T) {
+ runProdParity(t, ctx, s3client, c, chainCacheDir, count, workers, seed, profileMem)
+ })
+ }
+}
+
+type prodChain struct {
+ name string
+ blockchain common.Blockchain
+ network common.Network
+ bucket string
+ keyPrefix string // ends with "/"
+ fileExt string // ".gzip" or ".zstd"
+ maxHeight int
+ compression api.Compression
+ newParser any // fx.Provide constructor
+}
+
+func runProdParity(t *testing.T, ctx context.Context, s3client *s3.Client, c prodChain, cacheDir string, count, workers, seed int, profileMem bool) {
+ var parser internal.NativeParser
+ var cfg *config.Config
+ app := testapp.New(t,
+ testapp.WithBlockchainNetwork(c.blockchain, c.network),
+ fx.Provide(c.newParser),
+ fx.Populate(&parser),
+ fx.Populate(&cfg),
+ )
+ defer app.Close()
+
+ streamer, ok := parser.(internal.BitcoinStreamer)
+ if !ok {
+ t.Fatalf("%s parser does not implement BitcoinStreamer", c.name)
+ }
+
+ // Local httptest server serving compressed blocks by filename.
+ // Each block's BlockFile.FileUrl points here so the real
+ // downloader exercises the full HTTP+decompress path.
+ srv := httptest.NewServer(http.FileServer(http.Dir(cacheDir)))
+ defer srv.Close()
+
+ dl := downloader.NewBlockDownloader(downloader.BlockDownloaderParams{
+ Params: fxparams.Params{Logger: zap.NewNop(), Config: cfg},
+ HttpClient: srv.Client(),
+ })
+
+ rng := rand.New(rand.NewSource(int64(seed)))
+ heights := make([]int, count)
+ for i := range heights {
+ heights[i] = 1 + rng.Intn(c.maxHeight)
+ }
+
+ // Ensure every sampled height is present on disk up front, so
+ // the pipeline-timing worker loop never has to talk to S3. The
+ // pre-fetch is serial + I/O-bound; the actual measurement runs
+ // afterwards against only the local cache.
+ prefetchHeights(ctx, t, s3client, c, cacheDir, heights)
+
+ type job struct {
+ height int
+ fileName string
+ }
+ jobs := make(chan job, len(heights))
+ for _, h := range heights {
+ entry, err := findCachedFile(cacheDir, h, c.fileExt)
+ if err != nil {
+ continue
+ }
+ jobs <- job{height: h, fileName: entry}
+ }
+ close(jobs)
+
+ var (
+ attempted atomic.Int64
+ mismatches atomic.Int64
+ mu sync.Mutex
+ failures []parityFailure
+ samples []blockSample
+ )
+
+ var wg sync.WaitGroup
+ for w := 0; w < workers; w++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for j := range jobs {
+ attempted.Add(1)
+ bf := &api.BlockFile{
+ Tag: 1,
+ Height: uint64(j.height),
+ Compression: c.compression,
+ FileUrl: srv.URL + "/" + j.fileName,
+ }
+ var (
+ sample blockSample
+ err error
+ )
+ if profileMem {
+ sample = measureFullPipelineProfile(dl, parser, streamer, bf, j.height)
+ } else {
+ sample, err = measureFullPipelineParity(dl, parser, streamer, bf, j.height)
+ if err != nil {
+ mismatches.Add(1)
+ mu.Lock()
+ failures = append(failures, parityFailure{
+ height: j.height,
+ key: j.fileName,
+ err: err,
+ })
+ mu.Unlock()
+ continue
+ }
+ }
+ mu.Lock()
+ samples = append(samples, sample)
+ mu.Unlock()
+ }
+ }()
+ }
+ wg.Wait()
+
+ t.Logf("[%s] attempted=%d mismatches=%d (full pipeline timing: read + decompress + parse/stream)",
+ c.name, attempted.Load(), mismatches.Load())
+
+ reportSamples(t, c.name, samples, profileMem)
+
+ if len(failures) > 0 {
+ for i, f := range failures {
+ if i >= 10 {
+ t.Logf(" ... and %d more failures", len(failures)-10)
+ break
+ }
+ t.Logf(" FAIL height=%d file=%s err=%v", f.height, f.key, f.err)
+ }
+ t.Fatalf("[%s] %d parity mismatches", c.name, mismatches.Load())
+ }
+}
+
+type parityFailure struct {
+ height int
+ key string // local file name (within chain cache dir)
+ err error
+}
+
+// blockSample records per-block full-pipeline timing + (optionally)
+// memory metrics for one block.
+type blockSample struct {
+ height int
+ txCount int
+ rawSize int // proto.Size of the *api.Block after unmarshal
+ parseTime time.Duration
+ streamTime time.Duration
+ parsePeak uint64 // bytes; populated only when profileMem=true
+ streamPeak uint64 // bytes; populated only when profileMem=true
+}
+
+// prefetchHeights downloads any missing blocks to disk serially via
+// the S3 client. Returns when all heights are either cached or known
+// missing (404-equivalent).
+func prefetchHeights(ctx context.Context, t *testing.T, s3client *s3.Client, c prodChain, cacheDir string, heights []int) {
+ t.Helper()
+ var fetched, cached, missing int
+ for _, h := range heights {
+ name, err := findCachedFile(cacheDir, h, c.fileExt)
+ if err == nil && name != "" {
+ cached++
+ continue
+ }
+ if downloaded := s3DownloadAndCache(ctx, s3client, c, cacheDir, h); downloaded {
+ fetched++
+ } else {
+ missing++
+ }
+ }
+ t.Logf("[%s] prefetch: cached=%d fetched=%d missing=%d", c.name, cached, fetched, missing)
+}
+
+// findCachedFile returns the bare filename (no dir) for height's
+// cached object, if present. Returns "" if the expected file doesn't
+// exist.
+func findCachedFile(cacheDir string, height int, fileExt string) (string, error) {
+ name := strconv.Itoa(height) + fileExt
+ path := filepath.Join(cacheDir, name)
+ st, err := os.Stat(path)
+ if err != nil {
+ return "", err
+ }
+ if st.IsDir() {
+ return "", fmt.Errorf("unexpected directory at %s", path)
+ }
+ return name, nil
+}
+
+// s3DownloadAndCache fetches the block at height from S3 and persists
+// it under cacheDir using the {height}{ext} filename convention.
+// Returns true when a block was successfully downloaded.
+func s3DownloadAndCache(ctx context.Context, s3client *s3.Client, c prodChain, cacheDir string, height int) bool {
+ prefix := c.keyPrefix + strconv.Itoa(height) + "/"
+ list, err := s3client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{
+ Bucket: aws.String(c.bucket),
+ Prefix: aws.String(prefix),
+ MaxKeys: aws.Int32(2),
+ })
+ if err != nil || len(list.Contents) == 0 {
+ return false
+ }
+ key := *list.Contents[0].Key
+ obj, err := s3client.GetObject(ctx, &s3.GetObjectInput{
+ Bucket: aws.String(c.bucket),
+ Key: aws.String(key),
+ })
+ if err != nil {
+ return false
+ }
+ defer obj.Body.Close()
+ compressedBytes, err := io.ReadAll(obj.Body)
+ if err != nil {
+ return false
+ }
+ localPath := filepath.Join(cacheDir, strconv.Itoa(height)+c.fileExt)
+ tmpPath := localPath + ".tmp"
+ if err := os.WriteFile(tmpPath, compressedBytes, 0o644); err != nil {
+ return false
+ }
+ return os.Rename(tmpPath, localPath) == nil
+}
+
+// measureFullPipelineParity times both full pipelines on a block and
+// asserts their transaction lists match. Used in the concurrent mode
+// (workers > 1).
+//
+// Legacy pipeline: dl.Download → parser.ParseBlock
+// Streaming path: dl.DownloadStream → WalkBitcoinEnvelope → StreamBlockIter
+//
+// Both timers cover HTTP read + decompression + parse/stream. Parity
+// comparison happens outside the streaming timer.
+func measureFullPipelineParity(dl downloader.BlockDownloader, parser internal.NativeParser, streamer internal.BitcoinStreamer, bf *api.BlockFile, height int) (blockSample, error) {
+ ctx := context.Background()
+ sample := blockSample{height: height}
+
+ // --- Legacy pipeline ---
+ legacyStart := time.Now()
+ rawBlock, err := dl.Download(ctx, bf)
+ if err != nil {
+ return sample, fmt.Errorf("Download: %w", err)
+ }
+ baseline, err := parser.ParseBlock(ctx, rawBlock)
+ sample.parseTime = time.Since(legacyStart)
+ if err != nil {
+ return sample, fmt.Errorf("ParseBlock: %w", err)
+ }
+ sample.rawSize = proto.Size(rawBlock)
+ baseTxs := baseline.GetBitcoin().GetTransactions()
+
+ // --- Streaming pipeline ---
+ streamStart := time.Now()
+ spooled, err := dl.DownloadStream(ctx, bf)
+ if err != nil {
+ return sample, fmt.Errorf("DownloadStream: %w", err)
+ }
+ streamed, iterErr := runStreamOverSpool(ctx, streamer, spooled)
+ sample.streamTime = time.Since(streamStart)
+ spooled.Close()
+ if iterErr != nil {
+ return sample, iterErr
+ }
+ sample.txCount = len(streamed)
+
+ if len(baseTxs) != len(streamed) {
+ return sample, fmt.Errorf("tx count: ParseBlock=%d StreamBlockIter=%d", len(baseTxs), len(streamed))
+ }
+ for i, b := range baseTxs {
+ s := streamed[i]
+ if b.TransactionId != s.TransactionId {
+ return sample, fmt.Errorf("tx[%d] id: %q vs %q", i, b.TransactionId, s.TransactionId)
+ }
+ if b.Hash != s.Hash {
+ return sample, fmt.Errorf("tx[%d] hash: %q vs %q", i, b.Hash, s.Hash)
+ }
+ if b.InputCount != s.InputCount {
+ return sample, fmt.Errorf("tx[%d] InputCount: %d vs %d", i, b.InputCount, s.InputCount)
+ }
+ if b.OutputCount != s.OutputCount {
+ return sample, fmt.Errorf("tx[%d] OutputCount: %d vs %d", i, b.OutputCount, s.OutputCount)
+ }
+ }
+ return sample, nil
+}
+
+// measureFullPipelineProfile times and measures peak heap delta for
+// both pipelines in isolation. The streaming path DOES NOT accumulate
+// transactions — this reflects the intrinsic streaming peak-RAM
+// profile a production consumer would see, not the collection-for-
+// parity overhead.
+//
+// Must be called serially: runtime.MemStats is process-global.
+func measureFullPipelineProfile(dl downloader.BlockDownloader, parser internal.NativeParser, streamer internal.BitcoinStreamer, bf *api.BlockFile, height int) blockSample {
+ ctx := context.Background()
+ sample := blockSample{height: height}
+
+ // --- Legacy pipeline ---
+ //
+ // Scoped inside a closure so rawBlock + baseline drop out of
+ // scope when it returns. Without the closure, local-var refs
+ // are still live on the goroutine stack when we call
+ // runtime.GC() below, which would prevent reclamation before
+ // the streaming measurement starts.
+ parseOK := func() bool {
+ runtime.GC()
+ runtime.GC()
+ parseBaseline := heapAlloc()
+ parsePeak := newHeapPeak()
+ parsePeak.start()
+ legacyStart := time.Now()
+ rawBlock, err := dl.Download(ctx, bf)
+ if err != nil {
+ parsePeak.stop()
+ return false
+ }
+ baseline, err := parser.ParseBlock(ctx, rawBlock)
+ sample.parseTime = time.Since(legacyStart)
+ parsePeak.stop()
+ sample.parsePeak = peakDelta(parsePeak.peak, parseBaseline)
+ if err != nil {
+ return false
+ }
+ sample.rawSize = proto.Size(rawBlock)
+ if bc := baseline.GetBitcoin(); bc != nil {
+ sample.txCount = len(bc.GetTransactions())
+ }
+ return true
+ }()
+ if !parseOK {
+ return sample
+ }
+
+ // Force GC now that the closure's locals are out of scope, so
+ // streaming's peak measurement starts from a clean baseline.
+ runtime.GC()
+ runtime.GC()
+
+ // --- Streaming pipeline (no tx accumulation) ---
+ streamBaseline := heapAlloc()
+ streamPeak := newHeapPeak()
+ streamPeak.start()
+ streamStart := time.Now()
+ spooled, err := dl.DownloadStream(ctx, bf)
+ if err != nil {
+ streamPeak.stop()
+ return sample
+ }
+ _ = runStreamOverSpoolDiscard(ctx, streamer, spooled)
+ sample.streamTime = time.Since(streamStart)
+ streamPeak.stop()
+ sample.streamPeak = peakDelta(streamPeak.peak, streamBaseline)
+ _ = spooled.Close()
+
+ return sample
+}
+
+// runStreamOverSpool walks the spooled bitcoin envelope, wires
+// seek-based loaders, and collects emitted transactions for parity
+// comparison.
+func runStreamOverSpool(ctx context.Context, streamer internal.BitcoinStreamer, spooled *downloader.SpooledBlock) ([]*api.BitcoinTransaction, error) {
+ opener, loadGroup, closeFn, err := spoolLoaders(spooled)
+ if err != nil {
+ return nil, err
+ }
+ defer closeFn()
+ stream := streamer.StreamBlockIter(ctx, opener, loadGroup)
+ var out []*api.BitcoinTransaction
+ for tx, iterErr := range stream.Transactions() {
+ if iterErr != nil {
+ return nil, fmt.Errorf("iter: %w", iterErr)
+ }
+ out = append(out, tx)
+ }
+ return out, nil
+}
+
+// runStreamOverSpoolDiscard walks the spool + iterates without
+// retaining transactions. Used in memory-profile mode so peak heap
+// reflects the intrinsic streaming profile.
+func runStreamOverSpoolDiscard(ctx context.Context, streamer internal.BitcoinStreamer, spooled *downloader.SpooledBlock) error {
+ opener, loadGroup, closeFn, err := spoolLoaders(spooled)
+ if err != nil {
+ return err
+ }
+ defer closeFn()
+ stream := streamer.StreamBlockIter(ctx, opener, loadGroup)
+ for _, iterErr := range stream.Transactions() {
+ if iterErr != nil {
+ return fmt.Errorf("iter: %w", iterErr)
+ }
+ }
+ return nil
+}
+
+// spoolLoaders walks the spool once, caches a single file handle,
+// and returns pread-based loaders. Mirrors the internal parser's
+// optimized loader implementation so this test measures the
+// production code path. Callers MUST invoke the returned close fn.
+func spoolLoaders(spooled *downloader.SpooledBlock) (func() (io.ReadCloser, error), func(int) (*api.RepeatedBytes, error), func() error, error) {
+ r, err := spooled.Open()
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("open spool: %w", err)
+ }
+ _, chunks, walkErr := api.WalkBitcoinEnvelope(r)
+ r.Close()
+ if walkErr != nil {
+ return nil, nil, nil, fmt.Errorf("walk: %w", walkErr)
+ }
+
+ // One handle for the whole iterator lifetime; ReadAt/pread keeps
+ // the loaders syscall-light under many-tx blocks.
+ handle, err := spooled.Open()
+ if err != nil {
+ return nil, nil, nil, fmt.Errorf("open spool for loaders: %w", err)
+ }
+ readerAt, ok := handle.(io.ReaderAt)
+ if !ok {
+ handle.Close()
+ return nil, nil, nil, fmt.Errorf("spool reader not io.ReaderAt")
+ }
+
+ openHeader := func() (io.ReadCloser, error) {
+ return io.NopCloser(io.NewSectionReader(readerAt, chunks.Header.Offset, chunks.Header.Length)), nil
+ }
+ groups := chunks.InputTransactionsGroups
+ loadGroup := func(i int) (*api.RepeatedBytes, error) {
+ if i < 0 || i >= len(groups) {
+ return nil, nil
+ }
+ ref := groups[i]
+ b := make([]byte, ref.Length)
+ if _, err := readerAt.ReadAt(b, ref.Offset); err != nil && err != io.EOF {
+ return nil, fmt.Errorf("readat group [%d]: %w", i, err)
+ }
+ rb := &api.RepeatedBytes{}
+ if err := proto.Unmarshal(b, rb); err != nil {
+ return nil, err
+ }
+ return rb, nil
+ }
+ return openHeader, loadGroup, handle.Close, nil
+}
+
+// reportSamples prints aggregate timing (and memory) stats for a
+// chain's parity run.
+func reportSamples(t *testing.T, chain string, samples []blockSample, profileMem bool) {
+ if len(samples) == 0 {
+ return
+ }
+
+ parseTimes := make([]time.Duration, len(samples))
+ streamTimes := make([]time.Duration, len(samples))
+ for i, s := range samples {
+ parseTimes[i] = s.parseTime
+ streamTimes[i] = s.streamTime
+ }
+ sort.Slice(parseTimes, func(i, j int) bool { return parseTimes[i] < parseTimes[j] })
+ sort.Slice(streamTimes, func(i, j int) bool { return streamTimes[i] < streamTimes[j] })
+
+ t.Logf("[%s] full-pipeline timing (n=%d)", chain, len(samples))
+ t.Logf(" Download+ParseBlock: mean=%s p50=%s p95=%s p99=%s max=%s",
+ meanDur(parseTimes), pctDur(parseTimes, 0.50),
+ pctDur(parseTimes, 0.95), pctDur(parseTimes, 0.99), parseTimes[len(parseTimes)-1])
+ t.Logf(" DownloadStream+StreamIter: mean=%s p50=%s p95=%s p99=%s max=%s",
+ meanDur(streamTimes), pctDur(streamTimes, 0.50),
+ pctDur(streamTimes, 0.95), pctDur(streamTimes, 0.99), streamTimes[len(streamTimes)-1])
+
+ // Top-5 largest blocks by raw size.
+ bySize := make([]blockSample, len(samples))
+ copy(bySize, samples)
+ sort.Slice(bySize, func(i, j int) bool { return bySize[i].rawSize > bySize[j].rawSize })
+ top := 5
+ if len(bySize) < top {
+ top = len(bySize)
+ }
+ t.Logf("[%s] top-%d largest blocks (by proto.Size):", chain, top)
+ for i := 0; i < top; i++ {
+ s := bySize[i]
+ base := fmt.Sprintf(" h=%d size=%s txs=%d parse=%s stream=%s",
+ s.height, humanBytes(uint64(s.rawSize)), s.txCount, s.parseTime, s.streamTime)
+ if profileMem {
+ base += fmt.Sprintf(" parsePeak=%s streamPeak=%s",
+ humanBytes(s.parsePeak), humanBytes(s.streamPeak))
+ }
+ t.Logf("%s", base)
+ }
+
+ if profileMem {
+ parsePeaks := make([]uint64, len(samples))
+ streamPeaks := make([]uint64, len(samples))
+ for i, s := range samples {
+ parsePeaks[i] = s.parsePeak
+ streamPeaks[i] = s.streamPeak
+ }
+ sort.Slice(parsePeaks, func(i, j int) bool { return parsePeaks[i] < parsePeaks[j] })
+ sort.Slice(streamPeaks, func(i, j int) bool { return streamPeaks[i] < streamPeaks[j] })
+ t.Logf("[%s] full-pipeline peak heap (n=%d)", chain, len(samples))
+ t.Logf(" Download+ParseBlock: p50=%s p95=%s p99=%s max=%s",
+ humanBytes(pctU64(parsePeaks, 0.50)), humanBytes(pctU64(parsePeaks, 0.95)),
+ humanBytes(pctU64(parsePeaks, 0.99)), humanBytes(parsePeaks[len(parsePeaks)-1]))
+ t.Logf(" DownloadStream+StreamIter: p50=%s p95=%s p99=%s max=%s",
+ humanBytes(pctU64(streamPeaks, 0.50)), humanBytes(pctU64(streamPeaks, 0.95)),
+ humanBytes(pctU64(streamPeaks, 0.99)), humanBytes(streamPeaks[len(streamPeaks)-1]))
+ }
+}
+
+func meanDur(ds []time.Duration) time.Duration {
+ var total time.Duration
+ for _, d := range ds {
+ total += d
+ }
+ return total / time.Duration(len(ds))
+}
+
+func pctDur(sorted []time.Duration, p float64) time.Duration {
+ if len(sorted) == 0 {
+ return 0
+ }
+ idx := int(float64(len(sorted)-1) * p)
+ return sorted[idx]
+}
+
+func pctU64(sorted []uint64, p float64) uint64 {
+ if len(sorted) == 0 {
+ return 0
+ }
+ idx := int(float64(len(sorted)-1) * p)
+ return sorted[idx]
+}
+
+func peakDelta(peak, baseline uint64) uint64 {
+ if peak > baseline {
+ return peak - baseline
+ }
+ return 0
+}
+
+func envInt(name string, def int) int {
+ if v := os.Getenv(name); v != "" {
+ if n, err := strconv.Atoi(v); err == nil {
+ return n
+ }
+ }
+ return def
+}
+
+func envChains(spec string, all []prodChain) []prodChain {
+ if spec == "" {
+ return all
+ }
+ want := make(map[string]bool)
+ for _, n := range strings.Split(spec, ",") {
+ want[strings.TrimSpace(n)] = true
+ }
+ var out []prodChain
+ for _, c := range all {
+ if want[c.name] {
+ out = append(out, c)
+ }
+ }
+ return out
+}
+
+// Unused in the new full-pipeline flow; kept so `decodeCompressed`
+// has one caller to satisfy the linter if future tests want it.
+var _ = decodeCompressed
+
+func decodeCompressed(data []byte, compression api.Compression) (*api.Block, error) {
+ decoded, err := storage_utils.Decompress(data, compression)
+ if err != nil {
+ return nil, fmt.Errorf("decompress: %w", err)
+ }
+ var block api.Block
+ if err := proto.Unmarshal(decoded, &block); err != nil {
+ return nil, fmt.Errorf("unmarshal: %w", err)
+ }
+ return &block, nil
+}
diff --git a/internal/blockchain/parser/bitcoin/bitcoin_native_stream_test.go b/internal/blockchain/parser/bitcoin/bitcoin_native_stream_test.go
new file mode 100644
index 00000000..3f383fcc
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/bitcoin_native_stream_test.go
@@ -0,0 +1,300 @@
+package bitcoin
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "io"
+ "strings"
+ "sync/atomic"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/fx"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func decoderAt(s string) *json.Decoder {
+ return json.NewDecoder(strings.NewReader(s))
+}
+
+// newStreamIterParser wires a bitcoin native parser and returns it as the
+// concrete impl so tests can reach StreamBlockIter.
+func newStreamIterParser(t *testing.T) (*bitcoinNativeParserImpl, testapp.TestApp) {
+ t.Helper()
+ var parser internal.NativeParser
+ app := testapp.New(t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_BITCOIN, common.Network_NETWORK_BITCOIN_MAINNET),
+ fx.Provide(NewBitcoinNativeParser),
+ fx.Populate(&parser),
+ )
+ return parser.(*bitcoinNativeParserImpl), app
+}
+
+// openerFor returns an openReader closure over the given bytes plus a
+// counter that records how many times the opener was invoked.
+func openerFor(data []byte) (func() (io.ReadCloser, error), *int32) {
+ var calls int32
+ return func() (io.ReadCloser, error) {
+ atomic.AddInt32(&calls, 1)
+ return io.NopCloser(bytes.NewReader(data)), nil
+ }, &calls
+}
+
+// TestBlockStreamIter_IterateThenHeader exercises the free path: Header()
+// is called after iteration, so no extra reader open.
+func TestBlockStreamIter_IterateThenHeader(t *testing.T) {
+ require := require.New(t)
+ parser, app := newStreamIterParser(t)
+ defer app.Close()
+
+ rawBlock, err := testutil.LoadRawBlock("parser/bitcoin/raw_block_731379.json")
+ require.NoError(err)
+ headerJSON := rawBlock.GetBitcoin().GetHeader()
+
+ baseline, err := parser.ParseBlock(context.Background(), rawBlock)
+ require.NoError(err)
+
+ opener, calls := openerFor(headerJSON)
+ stream := parser.StreamBlockIter(context.Background(), opener, NewInMemoryInputTxGroupLoader(rawBlock.GetBitcoin().GetInputTransactions()))
+
+ var count int
+ for tx, err := range stream.Transactions() {
+ require.NoError(err)
+ require.NotNil(tx)
+ count++
+ }
+ require.Equal(len(baseline.GetBitcoin().GetTransactions()), count)
+
+ // Header is free now: no new opener call.
+ before := atomic.LoadInt32(calls)
+ header, err := stream.Header()
+ require.NoError(err)
+ require.Equal(atomic.LoadInt32(calls), before, "Header after iteration must not open the reader again")
+
+ require.Equal(baseline.GetBitcoin().GetHeader().Hash, header.Hash)
+ require.Equal(baseline.GetBitcoin().GetHeader().Height, header.Height)
+ require.Equal(baseline.GetBitcoin().GetHeader().MerkleRoot, header.MerkleRoot)
+
+ // Calling Header() again hits the cache — still no new opens.
+ header2, err := stream.Header()
+ require.NoError(err)
+ require.Equal(atomic.LoadInt32(calls), before)
+ require.Equal(header.Hash, header2.Hash)
+}
+
+// TestBlockStreamIter_HeaderBeforeIter exercises the slow path: Header()
+// is called first, paying for a dedicated header-only scan.
+func TestBlockStreamIter_HeaderBeforeIter(t *testing.T) {
+ require := require.New(t)
+ parser, app := newStreamIterParser(t)
+ defer app.Close()
+
+ rawBlock, err := testutil.LoadRawBlock("parser/bitcoin/raw_block_731379.json")
+ require.NoError(err)
+ headerJSON := rawBlock.GetBitcoin().GetHeader()
+
+ baseline, err := parser.ParseBlock(context.Background(), rawBlock)
+ require.NoError(err)
+
+ opener, calls := openerFor(headerJSON)
+ stream := parser.StreamBlockIter(context.Background(), opener, NewInMemoryInputTxGroupLoader(rawBlock.GetBitcoin().GetInputTransactions()))
+
+ header, err := stream.Header()
+ require.NoError(err)
+ require.Equal(int32(1), atomic.LoadInt32(calls), "Header-first path opens the reader once for the scan")
+ require.Equal(baseline.GetBitcoin().GetHeader().Hash, header.Hash)
+ require.Equal(baseline.GetBitcoin().GetHeader().Height, header.Height)
+ require.Equal(baseline.GetBitcoin().GetHeader().MerkleRoot, header.MerkleRoot)
+
+ // A second Header() call hits the cache; no extra open.
+ header2, err := stream.Header()
+ require.NoError(err)
+ require.Equal(int32(1), atomic.LoadInt32(calls))
+ require.Equal(header.Hash, header2.Hash)
+
+ // Iteration opens the reader a second time (full pass), yields all txs.
+ var count int
+ for tx, iterErr := range stream.Transactions() {
+ require.NoError(iterErr)
+ require.NotNil(tx)
+ count++
+ }
+ require.Equal(int32(2), atomic.LoadInt32(calls), "Iteration opens reader again after header-only scan")
+ require.Equal(len(baseline.GetBitcoin().GetTransactions()), count)
+}
+
+// TestBlockStreamIter_HeaderOnly exercises pure header retrieval with
+// no iteration.
+func TestBlockStreamIter_HeaderOnly(t *testing.T) {
+ require := require.New(t)
+ parser, app := newStreamIterParser(t)
+ defer app.Close()
+
+ rawBlock, err := testutil.LoadRawBlock("parser/bitcoin/raw_block_731379.json")
+ require.NoError(err)
+ headerJSON := rawBlock.GetBitcoin().GetHeader()
+
+ baseline, err := parser.ParseBlock(context.Background(), rawBlock)
+ require.NoError(err)
+
+ opener, calls := openerFor(headerJSON)
+ stream := parser.StreamBlockIter(context.Background(), opener, NewInMemoryInputTxGroupLoader(rawBlock.GetBitcoin().GetInputTransactions()))
+
+ header, err := stream.Header()
+ require.NoError(err)
+ require.Equal(int32(1), atomic.LoadInt32(calls))
+ require.Equal(baseline.GetBitcoin().GetHeader().Hash, header.Hash)
+ require.Equal(baseline.GetBitcoin().GetHeader().Height, header.Height)
+}
+
+// TestBlockStreamIter_BreakEarly confirms that early termination does not
+// cache a header (iteration was partial, so we re-scan).
+func TestBlockStreamIter_BreakEarly(t *testing.T) {
+ require := require.New(t)
+ parser, app := newStreamIterParser(t)
+ defer app.Close()
+
+ rawBlock, err := testutil.LoadRawBlock("parser/bitcoin/raw_block_731379.json")
+ require.NoError(err)
+ headerJSON := rawBlock.GetBitcoin().GetHeader()
+
+ opener, calls := openerFor(headerJSON)
+ stream := parser.StreamBlockIter(context.Background(), opener, NewInMemoryInputTxGroupLoader(rawBlock.GetBitcoin().GetInputTransactions()))
+
+ var count int
+ for tx, iterErr := range stream.Transactions() {
+ require.NoError(iterErr)
+ require.NotNil(tx)
+ count++
+ if count == 5 {
+ break
+ }
+ }
+ require.Equal(5, count)
+ require.Equal(int32(1), atomic.LoadInt32(calls))
+
+ // Header was NOT captured (iteration was partial); slow path runs.
+ header, err := stream.Header()
+ require.NoError(err)
+ require.NotNil(header)
+ require.Equal(int32(2), atomic.LoadInt32(calls), "break-early iteration does not cache header; Header() pays for a scan")
+}
+
+// TestBlockStreamIter_ParityWithParseBlock checks field-by-field equivalence
+// on all transactions between the iter path and ParseBlock.
+func TestBlockStreamIter_ParityWithParseBlock(t *testing.T) {
+ require := require.New(t)
+ parser, app := newStreamIterParser(t)
+ defer app.Close()
+
+ rawBlock, err := testutil.LoadRawBlock("parser/bitcoin/raw_block_731379.json")
+ require.NoError(err)
+ headerJSON := rawBlock.GetBitcoin().GetHeader()
+
+ baseline, err := parser.ParseBlock(context.Background(), rawBlock)
+ require.NoError(err)
+
+ opener, _ := openerFor(headerJSON)
+ stream := parser.StreamBlockIter(context.Background(), opener, NewInMemoryInputTxGroupLoader(rawBlock.GetBitcoin().GetInputTransactions()))
+
+ var streamed []*api.BitcoinTransaction
+ for tx, iterErr := range stream.Transactions() {
+ require.NoError(iterErr)
+ streamed = append(streamed, tx)
+ }
+
+ baseTxs := baseline.GetBitcoin().GetTransactions()
+ require.Equal(len(baseTxs), len(streamed))
+ for i := range baseTxs {
+ require.Equal(baseTxs[i].TransactionId, streamed[i].TransactionId)
+ require.Equal(baseTxs[i].Hash, streamed[i].Hash)
+ require.Equal(baseTxs[i].InputCount, streamed[i].InputCount)
+ require.Equal(baseTxs[i].OutputCount, streamed[i].OutputCount)
+ require.Equal(baseTxs[i].Fee, streamed[i].Fee)
+ }
+}
+
+// TestBlockStreamIter_OptionsPassthrough confirms that ParseOption args
+// are forwarded into the underlying StreamBlock.
+func TestBlockStreamIter_OptionsPassthrough(t *testing.T) {
+ require := require.New(t)
+ parser, app := newStreamIterParser(t)
+ defer app.Close()
+
+ rawBlock, err := testutil.LoadRawBlock("parser/bitcoin/raw_block_731379.json")
+ require.NoError(err)
+ headerJSON := rawBlock.GetBitcoin().GetHeader()
+
+ opener, _ := openerFor(headerJSON)
+ stream := parser.StreamBlockIter(context.Background(), opener, NewInMemoryInputTxGroupLoader(rawBlock.GetBitcoin().GetInputTransactions()),
+ internal.WithSkipScripts(), internal.WithSkipWitnesses())
+
+ for tx, iterErr := range stream.Transactions() {
+ require.NoError(iterErr)
+ require.Empty(tx.Hex, "WithSkipScripts should empty tx.Hex")
+ for _, in := range tx.Inputs {
+ if in.ScriptSignature != nil {
+ require.Empty(in.ScriptSignature.Assembly)
+ require.Empty(in.ScriptSignature.Hex)
+ }
+ require.Empty(in.TransactionInputWitnesses, "WithSkipWitnesses")
+ }
+ for _, out := range tx.Outputs {
+ if out.ScriptPublicKey != nil {
+ require.Empty(out.ScriptPublicKey.Assembly)
+ require.Empty(out.ScriptPublicKey.Hex)
+ }
+ }
+ }
+
+ header, err := stream.Header()
+ require.NoError(err)
+ require.NotEmpty(header.Hash)
+}
+
+// TestSkipJSONValue covers the JSON-depth skipper used by
+// parseBitcoinHeaderSkippingTx.
+func TestSkipJSONValue(t *testing.T) {
+ cases := []struct {
+ name string
+ in string
+ }{
+ {"scalar_string", `"foo"`},
+ {"scalar_number", `42`},
+ {"scalar_bool", `true`},
+ {"scalar_null", `null`},
+ {"empty_array", `[]`},
+ {"empty_object", `{}`},
+ {"flat_array", `[1, 2, 3]`},
+ {"nested_object", `{"a": 1, "b": {"c": 2}, "d": [3, 4, {"e": 5}]}`},
+ {"deeply_nested", `[[[[1, 2, 3]]]]`},
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ // Wrap the value in an envelope so we can verify the decoder
+ // is positioned correctly after skipJSONValue.
+ in := `{"skip":` + c.in + `,"keep":"sentinel"}`
+ dec := decoderAt(in)
+
+ // Consume '{' and "skip".
+ _, err := dec.Token()
+ require.NoError(t, err)
+ _, err = dec.Token()
+ require.NoError(t, err)
+
+ require.NoError(t, skipJSONValue(dec))
+
+ // The next key should be "keep".
+ tok, err := dec.Token()
+ require.NoError(t, err)
+ require.Equal(t, "keep", tok)
+ })
+ }
+}
diff --git a/internal/blockchain/parser/bitcoin/bitcoin_native_test.go b/internal/blockchain/parser/bitcoin/bitcoin_native_test.go
index d4feb30e..d9ebe458 100644
--- a/internal/blockchain/parser/bitcoin/bitcoin_native_test.go
+++ b/internal/blockchain/parser/bitcoin/bitcoin_native_test.go
@@ -190,10 +190,45 @@ func (s *bitcoinNativeParserTestSuite) TestParseBitcoinBlockPubKey() {
nativeBlock, err := s.parser.ParseBlock(context.Background(), block)
require.NoError(err)
require.Equal(bitcoinScriptTypePubKey, nativeBlock.GetBitcoin().Transactions[0].Outputs[0].ScriptPublicKey.Type)
+ require.Equal("1ta5GhKyZCWdvYkzv3AM5YT55VGpgQU6p", nativeBlock.GetBitcoin().Transactions[0].Outputs[0].ScriptPublicKey.Address)
require.Equal(bitcoinScriptTypePubKeyHash, nativeBlock.GetBitcoin().Transactions[0].Outputs[1].ScriptPublicKey.Type)
require.Equal("1LVnx7af5JPzW41vVURfbmxs3ENEzPcEqF", nativeBlock.GetBitcoin().Transactions[0].Outputs[1].ScriptPublicKey.Address)
}
+func TestParseDogecoinBlockPubKey(t *testing.T) {
+ require := testutil.Require(t)
+
+ var parser internal.NativeParser
+ app := testapp.New(t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_DOGECOIN, common.Network_NETWORK_DOGECOIN_MAINNET),
+ fx.Provide(NewBitcoinNativeParser),
+ fx.Populate(&parser),
+ )
+ defer app.Close()
+ require.NotNil(parser)
+
+ header, err := fixtures.ReadFile("parser/bitcoin/get_block_pubkey_case.json")
+ require.NoError(err)
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_DOGECOIN,
+ Network: common.Network_NETWORK_DOGECOIN_MAINNET,
+ Metadata: bitcoinMetadata,
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: header,
+ },
+ },
+ }
+
+ nativeBlock, err := parser.ParseBlock(context.Background(), block)
+ require.NoError(err)
+ require.Equal(bitcoinScriptTypePubKey, nativeBlock.GetBitcoin().Transactions[0].Outputs[0].ScriptPublicKey.Type)
+ // The P2PK output has no address in the raw data, so the address is derived
+ // from the public key using Dogecoin's P2PKH version byte (0x1e).
+ require.Equal("D62fcXdyGy6oAvjMjW2itqi3xDDaFDhNS8", nativeBlock.GetBitcoin().Transactions[0].Outputs[0].ScriptPublicKey.Address)
+}
+
func (s *bitcoinNativeParserTestSuite) TestParseBitcoinBlockPubKeyInputNonCoinbase() {
require := testutil.Require(s.T())
@@ -693,3 +728,51 @@ func TestParseBitcoinTransactionLit(t *testing.T) {
})
}
}
+
+type bitcoinCashNativeParserTestSuite struct {
+ suite.Suite
+ app testapp.TestApp
+ parser internal.NativeParser
+}
+
+func TestBitcoinCashNativeParserTestSuite(t *testing.T) {
+ suite.Run(t, new(bitcoinCashNativeParserTestSuite))
+}
+
+func (s *bitcoinCashNativeParserTestSuite) SetupTest() {
+ s.app = testapp.New(s.T(),
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_BITCOINCASH, common.Network_NETWORK_BITCOINCASH_MAINNET),
+ fx.Provide(NewBitcoinNativeParser),
+ fx.Populate(&s.parser),
+ )
+ s.NotNil(s.parser)
+}
+
+func (s *bitcoinCashNativeParserTestSuite) TearDownTest() {
+ s.app.Close()
+}
+
+func (s *bitcoinCashNativeParserTestSuite) TestParseBitcoinCashBlock_ScriptType() {
+ require := testutil.Require(s.T())
+
+ block, err := testutil.LoadRawBlock("parser/bitcoin/bch_raw_block_940884.json")
+ require.NoError(err)
+
+ nativeBlock, err := s.parser.ParseBlock(context.Background(), block)
+ require.NoError(err)
+ require.Equal(common.Blockchain_BLOCKCHAIN_BITCOINCASH, nativeBlock.Blockchain)
+ require.Equal(common.Network_NETWORK_BITCOINCASH_MAINNET, nativeBlock.Network)
+ require.Equal(uint32(2), nativeBlock.Tag)
+ require.Equal(uint64(940884), nativeBlock.Height)
+
+ actual := nativeBlock.GetBitcoin()
+ require.NotNil(actual)
+ require.Equal(132, len(actual.Transactions))
+
+ // The coinbase transaction (tx[0]) has a "script" type output,
+ // a BCH-specific p2s type introduced in BCHN upgrade 12.
+ coinbaseTx := actual.Transactions[0]
+ require.True(coinbaseTx.IsCoinbase)
+ require.Equal("script", coinbaseTx.Outputs[0].ScriptPublicKey.Type)
+ require.Empty(coinbaseTx.Outputs[0].ScriptPublicKey.Address)
+}
diff --git a/internal/blockchain/parser/bitcoin/dash_native.go b/internal/blockchain/parser/bitcoin/dash_native.go
new file mode 100644
index 00000000..6088f7fd
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/dash_native.go
@@ -0,0 +1,19 @@
+package bitcoin
+
+import (
+ "github.com/go-playground/validator/v10"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/utils/log"
+)
+
+func NewDashNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ v := validator.New()
+ v.RegisterStructValidation(validateBitcoinScriptPubKey, BitcoinScriptPubKey{})
+ return &bitcoinNativeParserImpl{
+ logger: log.WithPackage(params.Logger),
+ validate: v,
+ preprocessTx: backfillTxHashSingle,
+ p2pkhVersionByte: 0x4c,
+ }, nil
+}
diff --git a/internal/blockchain/parser/bitcoin/dash_native_test.go b/internal/blockchain/parser/bitcoin/dash_native_test.go
new file mode 100644
index 00000000..95da5aca
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/dash_native_test.go
@@ -0,0 +1,171 @@
+package bitcoin
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/fx"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/utils/fixtures"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type dashNativeParserTestSuite struct {
+ suite.Suite
+ app testapp.TestApp
+ parser internal.NativeParser
+}
+
+func TestDashNativeParserTestSuite(t *testing.T) {
+ suite.Run(t, new(dashNativeParserTestSuite))
+}
+
+func (s *dashNativeParserTestSuite) SetupTest() {
+ s.app = testapp.New(s.T(),
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_DASH, common.Network_NETWORK_DASH_MAINNET),
+ fx.Provide(NewDashNativeParser),
+ fx.Populate(&s.parser),
+ )
+ s.NotNil(s.parser)
+}
+
+func (s *dashNativeParserTestSuite) TearDownTest() {
+ s.app.Close()
+}
+
+func (s *dashNativeParserTestSuite) TestParseDashBlock_PubKeyAddress() {
+ require := testutil.Require(s.T())
+
+ // Dash P2PK outputs omit the "address" field; the parser must derive it
+ // using Dash's P2PKH version byte (0x4c) to produce an X-prefixed address.
+ header, err := fixtures.ReadFile("parser/bitcoin/dash_get_block_pubkey_case.json")
+ require.NoError(err)
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_DASH,
+ Network: common.Network_NETWORK_DASH_MAINNET,
+ Metadata: bitcoinMetadata,
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: header,
+ },
+ },
+ }
+
+ nativeBlock, err := s.parser.ParseBlock(context.Background(), block)
+ require.NoError(err)
+
+ output := nativeBlock.GetBitcoin().Transactions[0].Outputs[0]
+ require.Equal(bitcoinScriptTypePubKey, output.ScriptPublicKey.Type)
+ // The derived address must use Dash's version byte (0x4c), producing an X-prefix.
+ require.Equal("XbaQuXMDwGR6ns9LroMPCcEEuR4xr8L6ew", output.ScriptPublicKey.Address)
+}
+
+func (s *dashNativeParserTestSuite) TestParseDashBlock_AllowsMissingHash() {
+ require := testutil.Require(s.T())
+
+ // This fixture has transactions without the "hash" field.
+ // Bitcoin parser rejects it, but Dash parser should accept it
+ // because the preprocessor fills Hash from TxId before validation.
+ header, err := fixtures.ReadFile("parser/bitcoin/errcases/get_block_err_txnohash.json")
+ require.NoError(err)
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_DASH,
+ Network: common.Network_NETWORK_DASH_MAINNET,
+ Metadata: bitcoinMetadata,
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: header,
+ },
+ },
+ }
+
+ _, err = s.parser.ParseBlock(context.Background(), block)
+ // The block may still fail for other reasons (e.g., missing input transactions),
+ // but it should NOT fail with validation error for Hash.
+ if err != nil {
+ require.NotContains(err.Error(), "Key: 'BitcoinBlock.Tx[0].Hash'")
+ }
+}
+
+func (s *dashNativeParserTestSuite) TestParseDashBlock_RejectsEmptyTxIdAndHash() {
+ require := testutil.Require(s.T())
+
+ // When both TxId and Hash are missing, the preprocessor cannot fill Hash,
+ // so validation must reject it.
+ header, err := fixtures.ReadFile("parser/bitcoin/errcases/get_block_err_txnoid.json")
+ require.NoError(err)
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_DASH,
+ Network: common.Network_NETWORK_DASH_MAINNET,
+ Metadata: bitcoinMetadata,
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: header,
+ },
+ },
+ }
+
+ _, err = s.parser.ParseBlock(context.Background(), block)
+ require.Error(err)
+ require.Contains(err.Error(), "failed to validate bitcoin block")
+}
+
+func (s *dashNativeParserTestSuite) TestParseDashBlock_StillValidatesRequiredFields() {
+ require := testutil.Require(s.T())
+
+ tests := []struct {
+ name string
+ file string
+ messages []string
+ }{
+ {
+ name: "DashBlock_requires_hash",
+ file: "parser/bitcoin/errcases/get_block_err_nohash.json",
+ messages: []string{
+ "failed to validate bitcoin block",
+ "Key: 'BitcoinBlock.Hash'",
+ },
+ },
+ {
+ name: "DashBlock_requires_time",
+ file: "parser/bitcoin/errcases/get_block_err_notime.json",
+ messages: []string{
+ "failed to validate bitcoin block",
+ "Key: 'BitcoinBlock.Time'",
+ },
+ },
+ }
+
+ for _, test := range tests {
+ require.NotContains(test.name, " ")
+ s.T().Run(test.name, func(t *testing.T) {
+ header, err := fixtures.ReadFile(test.file)
+ require.NoError(err)
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_DASH,
+ Network: common.Network_NETWORK_DASH_MAINNET,
+ Metadata: bitcoinMetadata,
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: header,
+ },
+ },
+ }
+
+ _, err = s.parser.ParseBlock(context.Background(), block)
+ require.Error(err)
+ for _, msg := range test.messages {
+ require.Contains(err.Error(), msg)
+ }
+ })
+ }
+}
diff --git a/internal/blockchain/parser/bitcoin/module.go b/internal/blockchain/parser/bitcoin/module.go
index ac5fccd5..33afadb0 100644
--- a/internal/blockchain/parser/bitcoin/module.go
+++ b/internal/blockchain/parser/bitcoin/module.go
@@ -11,4 +11,12 @@ var Module = fx.Options(
SetCheckerFactory(NewBitcoinChecker).
SetRosettaParserFactory(NewBitcoinRosettaParser).
Build(),
+ internal.NewParserBuilder("dash", NewDashNativeParser).
+ SetCheckerFactory(NewBitcoinChecker).
+ SetRosettaParserFactory(NewBitcoinRosettaParser).
+ Build(),
+ internal.NewParserBuilder("zcash", NewZcashNativeParser).
+ SetCheckerFactory(NewBitcoinChecker).
+ SetRosettaParserFactory(NewBitcoinRosettaParser).
+ Build(),
)
diff --git a/internal/blockchain/parser/bitcoin/zcash_large_bench_test.go b/internal/blockchain/parser/bitcoin/zcash_large_bench_test.go
new file mode 100644
index 00000000..e1d0fd6a
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/zcash_large_bench_test.go
@@ -0,0 +1,201 @@
+package bitcoin
+
+import (
+ "context"
+ "fmt"
+ "os"
+ "runtime"
+ "strings"
+ "testing"
+ "time"
+
+ "go.uber.org/fx"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// TestZcashLargeBlockBench runs a one-shot ParseBlock benchmark on a real
+// pre-downloaded, pre-decompressed zcash block.
+//
+// Gated by env var CHAINSTORAGE_BENCH_ZCASH_BLOCK (path to the .pb file)
+// so it does not run in the default `go test` sweep. Optional:
+//
+// CHAINSTORAGE_BENCH_OPTS="skipScripts,skipWitnesses,hexAsBytes"
+//
+// The .pb file is the protobuf-serialized api.Block after zstd decompress
+// — i.e. the shape the blob downloader's proto.Unmarshal expects.
+//
+// Example:
+//
+// CHAINSTORAGE_BENCH_ZCASH_BLOCK=/tmp/zcash_bench/block_322102.pb \
+// go test -v -run=TestZcashLargeBlockBench -timeout=30m \
+// ./internal/blockchain/parser/bitcoin/
+func TestZcashLargeBlockBench(t *testing.T) {
+ path := os.Getenv("CHAINSTORAGE_BENCH_ZCASH_BLOCK")
+ if path == "" {
+ t.Skip("set CHAINSTORAGE_BENCH_ZCASH_BLOCK to run")
+ }
+
+ var parser internal.NativeParser
+ app := testapp.New(t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_ZCASH, common.Network_NETWORK_ZCASH_MAINNET),
+ fx.Provide(NewZcashNativeParser),
+ fx.Populate(&parser),
+ )
+ defer app.Close()
+
+ t.Logf("loading %s", path)
+
+ // Read + Unmarshal inside a closure so the compressed `data`
+ // slice (multi-GB) goes out of scope before the GC below —
+ // otherwise its reference stays live on the goroutine stack
+ // and inflates the subsequent measurements.
+ var rawBlock api.Block
+ func() {
+ t0 := time.Now()
+ data, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("ReadFile: %v", err)
+ }
+ t.Logf("read: %.2f MB in %s", mbf(len(data)), time.Since(t0))
+
+ t0 = time.Now()
+ if err := proto.Unmarshal(data, &rawBlock); err != nil {
+ t.Fatalf("proto.Unmarshal: %v", err)
+ }
+ t.Logf("proto.Unmarshal api.Block envelope: %s", time.Since(t0))
+ }()
+ runtime.GC()
+ runtime.GC()
+
+ blob := rawBlock.GetBitcoin()
+ if blob == nil {
+ t.Fatalf("no bitcoin blobdata")
+ }
+ t.Logf("header JSON size: %.2f MB", mbf(len(blob.GetHeader())))
+ t.Logf("input tx groups: %d", len(blob.GetInputTransactions()))
+
+ opts := buildBenchOpts(t, os.Getenv("CHAINSTORAGE_BENCH_OPTS"))
+
+ ctx := context.Background()
+
+ // ----- ParseBlock -----
+ t.Logf("\n=== ParseBlock ===")
+ runtime.GC()
+ runtime.GC()
+ before := heapAlloc()
+ peak := newHeapPeak()
+ peak.start()
+ parseStart := time.Now()
+ nb, err := parser.ParseBlock(ctx, &rawBlock, opts...)
+ parseElapsed := time.Since(parseStart)
+ peak.stop()
+ if err != nil {
+ t.Fatalf("ParseBlock: %v", err)
+ }
+ after := heapAlloc()
+ t.Logf(" elapsed: %s", parseElapsed)
+ t.Logf(" tx count: %d", nb.NumTransactions)
+ t.Logf(" heap before: %s", humanBytes(before))
+ t.Logf(" heap after: %s", humanBytes(after))
+ t.Logf(" heap peak: %s (+%s)", humanBytes(peak.peak), humanBytes(subU(peak.peak, before)))
+ t.Logf(" persistent NB size: %s", humanBytes(uint64(proto.Size(nb))))
+}
+
+func buildBenchOpts(t *testing.T, s string) []internal.ParseOption {
+ if s == "" {
+ return nil
+ }
+ var out []internal.ParseOption
+ for _, name := range strings.Split(s, ",") {
+ switch strings.TrimSpace(name) {
+ case "":
+ case "skipScripts":
+ out = append(out, internal.WithSkipScripts())
+ case "skipWitnesses":
+ out = append(out, internal.WithSkipWitnesses())
+ case "skipShielded":
+ out = append(out, internal.WithSkipShielded())
+ default:
+ t.Fatalf("unknown bench opt %q", name)
+ }
+ }
+ return out
+}
+
+func mbf(n int) float64 { return float64(n) / 1e6 }
+
+func heapAlloc() uint64 {
+ var ms runtime.MemStats
+ runtime.ReadMemStats(&ms)
+ return ms.HeapAlloc
+}
+
+func subU(a, b uint64) uint64 {
+ if a < b {
+ return 0
+ }
+ return a - b
+}
+
+func humanBytes(b uint64) string {
+ const unit = 1024
+ if b < unit {
+ return fmt.Sprintf("%d B", b)
+ }
+ div, exp := uint64(unit), 0
+ for n := b / unit; n >= unit; n /= unit {
+ div *= unit
+ exp++
+ }
+ return fmt.Sprintf("%.2f %cB", float64(b)/float64(div), "KMGTPE"[exp])
+}
+
+type heapPeakTracker struct {
+ peak uint64
+ stopCh chan struct{}
+ doneCh chan struct{}
+}
+
+func newHeapPeak() *heapPeakTracker { return &heapPeakTracker{} }
+
+func (p *heapPeakTracker) start() {
+ p.stopCh = make(chan struct{})
+ p.doneCh = make(chan struct{})
+ // Capture an initial snapshot so short ops that finish before the
+ // first tick still record a baseline peak.
+ if h := heapAlloc(); h > p.peak {
+ p.peak = h
+ }
+ go func() {
+ defer close(p.doneCh)
+ // 5ms tick catches ~10ms+ operations cleanly; sub-ms blocks
+ // may still read as baseline (fine — no peak to observe).
+ tick := time.NewTicker(5 * time.Millisecond)
+ defer tick.Stop()
+ for {
+ select {
+ case <-p.stopCh:
+ return
+ case <-tick.C:
+ if h := heapAlloc(); h > p.peak {
+ p.peak = h
+ }
+ }
+ }
+ }()
+}
+
+func (p *heapPeakTracker) stop() {
+ close(p.stopCh)
+ <-p.doneCh
+ // Final snapshot so we don't miss a peak that hit between the
+ // last tick and the stop signal.
+ if h := heapAlloc(); h > p.peak {
+ p.peak = h
+ }
+}
diff --git a/internal/blockchain/parser/bitcoin/zcash_native.go b/internal/blockchain/parser/bitcoin/zcash_native.go
new file mode 100644
index 00000000..29723b1a
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/zcash_native.go
@@ -0,0 +1,90 @@
+package bitcoin
+
+import (
+ "bytes"
+ "encoding/json"
+
+ "github.com/go-playground/validator/v10"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/utils/log"
+)
+
+// zcashTxForFilter is a lightweight view over a raw transaction used by
+// the shielded-tx filter. Raw JSON fields are kept opaque so we can
+// classify without allocating into the shared BitcoinTransaction struct.
+type zcashTxForFilter map[string]json.RawMessage
+
+func NewZcashNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ v := validator.New()
+ v.RegisterStructValidation(validateBitcoinScriptPubKey, BitcoinScriptPubKey{})
+ // Note: validateBitcoinTransactionVinVout is intentionally NOT registered.
+ // Zcash shielded transactions can have empty vin (shielded-to-transparent)
+ // or empty vout (transparent-to-shielded) and must pass struct validation
+ // even though the subsequent txFilter will drop them from the output.
+ return &bitcoinNativeParserImpl{
+ logger: log.WithPackage(params.Logger),
+ validate: v,
+ preprocessTx: backfillTxHashSingle,
+ txFilter: isTransparentZcashTx,
+ }, nil
+}
+
+// isTransparentZcashTx returns true only when the transaction has both
+// vin and vout. Coinbase transactions are always kept. Matches the
+// bitcoinNativeParserImpl.txFilter signature so it can be wired
+// directly.
+func isTransparentZcashTx(txRaw json.RawMessage) (bool, error) {
+ var tx zcashTxForFilter
+ if err := json.Unmarshal(txRaw, &tx); err != nil {
+ return false, err
+ }
+
+ coinbase, err := isZcashCoinbaseTx(tx)
+ if err != nil {
+ return false, err
+ }
+ if coinbase {
+ return true, nil
+ }
+
+ return hasMeaningfulJSONField(tx["vin"]) && hasMeaningfulJSONField(tx["vout"]), nil
+}
+
+// isZcashCoinbaseTx checks whether a transaction is a coinbase by inspecting
+// the first vin entry for a "coinbase" field.
+func isZcashCoinbaseTx(tx zcashTxForFilter) (bool, error) {
+ vinRaw, ok := tx["vin"]
+ if !ok || !hasMeaningfulJSONField(vinRaw) {
+ return false, nil
+ }
+
+ var vin []json.RawMessage
+ if err := json.Unmarshal(vinRaw, &vin); err != nil {
+ return false, err
+ }
+ if len(vin) == 0 {
+ return false, nil
+ }
+
+ var firstVin map[string]json.RawMessage
+ if err := json.Unmarshal(vin[0], &firstVin); err != nil {
+ return false, err
+ }
+
+ return hasMeaningfulJSONField(firstVin["coinbase"]), nil
+}
+
+func hasMeaningfulJSONField(raw json.RawMessage) bool {
+ trimmed := bytes.TrimSpace(raw)
+ if len(trimmed) == 0 {
+ return false
+ }
+
+ switch string(trimmed) {
+ case "null", "[]", "{}":
+ return false
+ default:
+ return true
+ }
+}
diff --git a/internal/blockchain/parser/bitcoin/zcash_native_test.go b/internal/blockchain/parser/bitcoin/zcash_native_test.go
new file mode 100644
index 00000000..5b0fb274
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/zcash_native_test.go
@@ -0,0 +1,285 @@
+package bitcoin
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/fx"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestZcashNativeParser_FiltersPrivacyTransactionsAndPreservesOriginalIndex(t *testing.T) {
+ require := require.New(t)
+ parser, app := newZcashNativeParser(t)
+ defer app.Close()
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ZCASH,
+ Network: common.Network_NETWORK_ZCASH_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Hash: "blockhash",
+ ParentHash: "parenthash",
+ Height: 1,
+ ParentHeight: 0,
+ },
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: []byte(`{
+ "hash":"blockhash",
+ "height":1,
+ "time":1,
+ "nTx":3,
+ "previousblockhash":"parenthash",
+ "tx":[
+ {
+ "txid":"coinbase-tx",
+ "hash":"coinbase-tx",
+ "vin":[{"coinbase":"01","sequence":4294967295}],
+ "vout":[
+ {
+ "value":12.5,
+ "n":0,
+ "scriptPubKey":{
+ "asm":"OP_DUP OP_HASH160 1111111111111111111111111111111111111111 OP_EQUALVERIFY OP_CHECKSIG",
+ "hex":"76a914111111111111111111111111111111111111111188ac",
+ "type":"pubkeyhash",
+ "address":"t1coinbase"
+ }
+ }
+ ]
+ },
+ {
+ "txid":"privacy-tx",
+ "hash":"privacy-tx",
+ "vin":[],
+ "vout":[],
+ "vShieldedSpend":[{"dummy":"value"}]
+ },
+ {
+ "txid":"transparent-tx",
+ "hash":"transparent-tx",
+ "vin":[
+ {
+ "txid":"prevtx",
+ "vout":0,
+ "scriptSig":{"asm":"","hex":""},
+ "sequence":4294967295
+ }
+ ],
+ "vout":[
+ {
+ "value":0.999,
+ "n":0,
+ "scriptPubKey":{
+ "asm":"OP_DUP OP_HASH160 2222222222222222222222222222222222222222 OP_EQUALVERIFY OP_CHECKSIG",
+ "hex":"76a914222222222222222222222222222222222222222288ac",
+ "type":"pubkeyhash",
+ "address":"t1transparent"
+ }
+ }
+ ]
+ }
+ ]
+ }`),
+ InputTransactions: []*api.RepeatedBytes{
+ {Data: [][]byte{}},
+ {Data: [][]byte{}},
+ {
+ Data: [][]byte{
+ []byte(`{
+ "txid":"prevtx",
+ "vout":[
+ {
+ "value":1.0,
+ "n":0,
+ "scriptPubKey":{
+ "asm":"OP_DUP OP_HASH160 3333333333333333333333333333333333333333 OP_EQUALVERIFY OP_CHECKSIG",
+ "hex":"76a914333333333333333333333333333333333333333388ac",
+ "type":"pubkeyhash",
+ "address":"t1prevout"
+ }
+ }
+ ]
+ }`),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ nativeBlock, err := parser.ParseBlock(context.Background(), block)
+ require.NoError(err)
+ require.Equal(uint64(2), nativeBlock.NumTransactions)
+
+ bitcoinBlock := nativeBlock.GetBitcoin()
+ require.NotNil(bitcoinBlock)
+ require.Len(bitcoinBlock.Transactions, 2)
+ require.Equal(uint64(3), bitcoinBlock.Header.NumberOfTransactions)
+ require.Equal(uint64(0), bitcoinBlock.Transactions[0].Index)
+ require.Equal(uint64(2), bitcoinBlock.Transactions[1].Index)
+}
+
+func TestZcashNativeParser_FiltersTransactionsMissingEitherVinOrVout(t *testing.T) {
+ require := require.New(t)
+ parser, app := newZcashNativeParser(t)
+ defer app.Close()
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ZCASH,
+ Network: common.Network_NETWORK_ZCASH_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Hash: "blockhash",
+ ParentHash: "parenthash",
+ Height: 1,
+ ParentHeight: 0,
+ },
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: []byte(`{
+ "hash":"blockhash",
+ "height":1,
+ "time":1,
+ "nTx":2,
+ "previousblockhash":"parenthash",
+ "tx":[
+ {
+ "txid":"coinbase-tx",
+ "hash":"coinbase-tx",
+ "vin":[{"coinbase":"01","sequence":4294967295}],
+ "vout":[
+ {
+ "value":12.5,
+ "n":0,
+ "scriptPubKey":{
+ "asm":"OP_DUP OP_HASH160 1111111111111111111111111111111111111111 OP_EQUALVERIFY OP_CHECKSIG",
+ "hex":"76a914111111111111111111111111111111111111111188ac",
+ "type":"pubkeyhash",
+ "address":"t1coinbase"
+ }
+ }
+ ]
+ },
+ {
+ "txid":"privacy-tx",
+ "hash":"privacy-tx",
+ "vin":[
+ {
+ "txid":"prevtx",
+ "vout":0,
+ "scriptSig":{"asm":"","hex":""},
+ "sequence":4294967295
+ }
+ ],
+ "vout":[],
+ "valueBalance":"0.0"
+ }
+ ]
+ }`),
+ InputTransactions: []*api.RepeatedBytes{
+ {Data: [][]byte{}},
+ {
+ Data: [][]byte{
+ []byte(`{
+ "txid":"prevtx",
+ "vout":[
+ {
+ "value":1.0,
+ "n":0,
+ "scriptPubKey":{
+ "asm":"OP_DUP OP_HASH160 3333333333333333333333333333333333333333 OP_EQUALVERIFY OP_CHECKSIG",
+ "hex":"76a914333333333333333333333333333333333333333388ac",
+ "type":"pubkeyhash",
+ "address":"t1prevout"
+ }
+ }
+ ]
+ }`),
+ },
+ },
+ },
+ },
+ },
+ }
+
+ nativeBlock, err := parser.ParseBlock(context.Background(), block)
+ require.NoError(err)
+
+ bitcoinBlock := nativeBlock.GetBitcoin()
+ require.NotNil(bitcoinBlock)
+ require.Equal(uint64(1), nativeBlock.NumTransactions)
+ require.Len(bitcoinBlock.Transactions, 1)
+ require.Equal("coinbase-tx", bitcoinBlock.Transactions[0].TransactionId)
+}
+
+func TestZcashNativeParser_ParseOptionsPassthrough(t *testing.T) {
+ require := require.New(t)
+ parser, app := newZcashNativeParser(t)
+ defer app.Close()
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ZCASH,
+ Network: common.Network_NETWORK_ZCASH_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 1, Hash: "blockhash", ParentHash: "parenthash",
+ Height: 1, ParentHeight: 0,
+ },
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: []byte(`{
+ "hash":"blockhash","height":1,"time":1,"nTx":1,
+ "previousblockhash":"parenthash",
+ "tx":[{
+ "txid":"coinbase-tx","hash":"coinbase-tx",
+ "vin":[{"coinbase":"01","sequence":4294967295}],
+ "vout":[{"value":12.5,"n":0,"scriptPubKey":{
+ "asm":"OP_DUP OP_HASH160 1111111111111111111111111111111111111111 OP_EQUALVERIFY OP_CHECKSIG",
+ "hex":"76a914111111111111111111111111111111111111111188ac",
+ "type":"pubkeyhash","address":"t1coinbase"
+ }}]
+ }],
+ "vShieldedSpend":[{"dummy":"value"}]
+ }`),
+ InputTransactions: []*api.RepeatedBytes{{Data: [][]byte{}}},
+ },
+ },
+ }
+
+ // All three options applied; should not break zcash parse.
+ nb, err := parser.ParseBlock(context.Background(),
+ block,
+ internal.WithSkipScripts(),
+ internal.WithSkipWitnesses(),
+ internal.WithSkipShielded(),
+ )
+ require.NoError(err)
+ require.NotNil(nb.GetBitcoin())
+ require.Equal(uint64(1), nb.NumTransactions)
+
+ tx := nb.GetBitcoin().Transactions[0]
+ require.Empty(tx.Hex)
+ for _, out := range tx.Outputs {
+ require.Empty(out.ScriptPublicKey.Assembly)
+ require.Empty(out.ScriptPublicKey.Hex)
+ }
+}
+
+func newZcashNativeParser(t *testing.T) (internal.NativeParser, testapp.TestApp) {
+ t.Helper()
+
+ var parser internal.NativeParser
+ app := testapp.New(t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_ZCASH, common.Network_NETWORK_ZCASH_MAINNET),
+ fx.Provide(NewZcashNativeParser),
+ fx.Populate(&parser),
+ )
+
+ return parser, app
+}
diff --git a/internal/blockchain/parser/bitcoin/zcash_stream_bench_test.go b/internal/blockchain/parser/bitcoin/zcash_stream_bench_test.go
new file mode 100644
index 00000000..5f137a53
--- /dev/null
+++ b/internal/blockchain/parser/bitcoin/zcash_stream_bench_test.go
@@ -0,0 +1,292 @@
+package bitcoin
+
+import (
+ "context"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "os"
+ "runtime"
+ "testing"
+ "time"
+
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// benchSeekedReader wraps a ReadCloser with a LimitReader view.
+type benchSeekedReader struct {
+ io.ReadCloser
+ limit io.Reader
+}
+
+func (b *benchSeekedReader) Read(p []byte) (int, error) { return b.limit.Read(p) }
+
+func protoSize(m proto.Message) int { return proto.Size(m) }
+
+// TestZcashLargeBlockBench_Streaming runs the new end-to-end streaming
+// path on a large zcash block: HTTP GET (served from a local httptest
+// Server backed by the compressed .zstd file) → DownloadStream
+// (disk-spool + decompress + proto.Unmarshal) → StreamBlockIter
+// (iter.Seq2 consumption).
+//
+// Gated by env var CHAINSTORAGE_BENCH_ZCASH_ZSTD. Optional:
+//
+// CHAINSTORAGE_BENCH_OPTS="skipScripts,skipWitnesses,skipShielded"
+//
+// Example:
+//
+// CHAINSTORAGE_BENCH_ZCASH_ZSTD=/tmp/zcash_bench/block_322102.zstd \
+// go test -v -run=TestZcashLargeBlockBench_Streaming -count=1 -timeout=30m \
+// ./internal/blockchain/parser/bitcoin/
+func TestZcashLargeBlockBench_Streaming(t *testing.T) {
+ zstdPath := os.Getenv("CHAINSTORAGE_BENCH_ZCASH_ZSTD")
+ if zstdPath == "" {
+ t.Skip("set CHAINSTORAGE_BENCH_ZCASH_ZSTD to the compressed .zstd file to run")
+ }
+
+ st, err := os.Stat(zstdPath)
+ if err != nil {
+ t.Fatalf("stat %s: %v", zstdPath, err)
+ }
+ t.Logf("serving: %s (%.2f MB compressed)", zstdPath, float64(st.Size())/1e6)
+
+ // Serve via an in-process httptest server. Use ServeFile so the
+ // compressed bytes stream from disk without buffering 700+ MB in RAM
+ // on the server side.
+ srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ http.ServeFile(w, r, zstdPath)
+ }))
+ defer srv.Close()
+
+ // Build the parser via fx.
+ var parser internal.NativeParser
+ var cfg *config.Config
+ app := testapp.New(t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_ZCASH, common.Network_NETWORK_ZCASH_MAINNET),
+ fx.Provide(NewZcashNativeParser),
+ fx.Populate(&parser),
+ fx.Populate(&cfg),
+ )
+ defer app.Close()
+
+ // Build the downloader directly, wiring in the httptest client so
+ // the test stays in-process and no TLS validation kicks in.
+ dl := downloader.NewBlockDownloader(downloader.BlockDownloaderParams{
+ Params: fxparams.Params{Logger: zap.NewNop(), Config: cfg},
+ HttpClient: srv.Client(),
+ })
+
+ bitcoinImpl, ok := parser.(internal.BitcoinStreamer)
+ if !ok {
+ t.Fatalf("parser %T does not implement BitcoinStreamer", parser)
+ }
+
+ opts := buildBenchOpts(t, os.Getenv("CHAINSTORAGE_BENCH_OPTS"))
+
+ bf := &api.BlockFile{
+ Tag: 1,
+ FileUrl: srv.URL,
+ Compression: api.Compression_ZSTD,
+ }
+
+ ctx := context.Background()
+
+ runtime.GC()
+ runtime.GC()
+ before := heapAlloc()
+ peak := newHeapPeak()
+ peak.start()
+
+ var (
+ txCount int
+ headerHash string
+ headerHeight uint64
+ headerCostStart time.Time
+ headerCost time.Duration
+ )
+ start := time.Now()
+ dlStart := time.Now()
+ spooled, err := dl.DownloadStream(ctx, bf)
+ if err != nil {
+ t.Fatalf("DownloadStream: %v", err)
+ }
+ defer spooled.Close()
+ dlElapsed := time.Since(dlStart)
+
+ // Walk the proto envelope once to collect chunk offsets.
+ walkStart := time.Now()
+ r, err := spooled.Open()
+ if err != nil {
+ t.Fatalf("open spool: %v", err)
+ }
+ _, chunks, walkErr := api.WalkBitcoinEnvelope(r)
+ r.Close()
+ if walkErr != nil {
+ t.Fatalf("walk bitcoin envelope: %v", walkErr)
+ }
+ walkElapsed := time.Since(walkStart)
+
+ openHeader := func() (io.ReadCloser, error) {
+ f, err := spooled.Open()
+ if err != nil {
+ return nil, err
+ }
+ if _, err := f.(*os.File).Seek(chunks.Header.Offset, io.SeekStart); err != nil {
+ f.Close()
+ return nil, err
+ }
+ return &benchSeekedReader{ReadCloser: f, limit: io.LimitReader(f, chunks.Header.Length)}, nil
+ }
+ groups := chunks.InputTransactionsGroups
+ loadGroup := func(i int) (*api.RepeatedBytes, error) {
+ if i < 0 || i >= len(groups) {
+ return nil, nil
+ }
+ ref := groups[i]
+ f, err := spooled.Open()
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ if _, err := f.(*os.File).Seek(ref.Offset, io.SeekStart); err != nil {
+ return nil, err
+ }
+ b := make([]byte, ref.Length)
+ if _, err := io.ReadFull(f, b); err != nil {
+ return nil, err
+ }
+ rb := &api.RepeatedBytes{}
+ if err := proto.Unmarshal(b, rb); err != nil {
+ return nil, err
+ }
+ return rb, nil
+ }
+
+ iterStart := time.Now()
+ bstream := bitcoinImpl.StreamBlockIter(ctx, openHeader, loadGroup, opts...)
+ for tx, err := range bstream.Transactions() {
+ if err != nil {
+ t.Fatalf("iterate tx: %v", err)
+ }
+ txCount++
+ _ = tx
+ }
+ iterElapsed := time.Since(iterStart)
+
+ // Header after iteration should be "free" per the BlockStream contract.
+ headerCostStart = time.Now()
+ h, err := bstream.Header()
+ headerCost = time.Since(headerCostStart)
+ if err != nil {
+ t.Fatalf("Header: %v", err)
+ }
+ headerHash = h.GetHash()
+ headerHeight = h.GetHeight()
+ elapsed := time.Since(start)
+ peak.stop()
+ after := heapAlloc()
+
+ t.Logf("\n=== Phase 2 streaming (DownloadStreamBitcoin -> StreamBlockIter) ===")
+ t.Logf("total elapsed: %s", elapsed)
+ t.Logf(" download+spool: %s", dlElapsed)
+ t.Logf(" walk envelope: %s", walkElapsed)
+ t.Logf(" iter transactions: %s", iterElapsed)
+ t.Logf("tx count (visited): %d", txCount)
+ t.Logf("Header() cost: %s (free after iteration)", headerCost)
+ t.Logf("heap before: %s", humanBytes(before))
+ t.Logf("heap after: %s", humanBytes(after))
+ t.Logf("heap peak: %s (+%s)", humanBytes(peak.peak), humanBytes(subU(peak.peak, before)))
+ t.Logf("block hash: %s", headerHash)
+ t.Logf("block height: %d", headerHeight)
+
+ // ----- Non-streaming baseline: Download + ParseBlock -----
+ // Drop all iter-derived references so GC can reclaim before the
+ // next measurement.
+ runtime.GC()
+ runtime.GC()
+ before2 := heapAlloc()
+ peak2 := newHeapPeak()
+ peak2.start()
+
+ start2 := time.Now()
+ dl2Start := time.Now()
+ rawBlock, err := dl.Download(ctx, bf)
+ if err != nil {
+ t.Fatalf("Download: %v", err)
+ }
+ dl2Elapsed := time.Since(dl2Start)
+ parse2Start := time.Now()
+ nb, err := parser.ParseBlock(ctx, rawBlock, opts...)
+ if err != nil {
+ t.Fatalf("ParseBlock: %v", err)
+ }
+ parse2Elapsed := time.Since(parse2Start)
+ elapsed2 := time.Since(start2)
+ peak2.stop()
+ after2 := heapAlloc()
+
+ t.Logf("\n=== Legacy baseline (Download -> ParseBlock) ===")
+ t.Logf("total elapsed: %s", elapsed2)
+ t.Logf(" download+unmarshal: %s", dl2Elapsed)
+ t.Logf(" parse block: %s", parse2Elapsed)
+ t.Logf("tx count: %d", nb.NumTransactions)
+ t.Logf("heap before: %s", humanBytes(before2))
+ t.Logf("heap after: %s", humanBytes(after2))
+ t.Logf("heap peak: %s (+%s)", humanBytes(peak2.peak), humanBytes(subU(peak2.peak, before2)))
+ t.Logf("persistent NB size: %s", humanBytes(uint64(protoSize(nb))))
+
+ // ----- Phase 2 chain-agnostic: DownloadStream (generic walker) + ParseBlock -----
+ nb = nil
+ runtime.GC()
+ runtime.GC()
+ before3 := heapAlloc()
+ peak3 := newHeapPeak()
+ peak3.start()
+
+ start3 := time.Now()
+ spooled3, err := dl.DownloadStream(ctx, bf)
+ if err != nil {
+ t.Fatalf("DownloadStream: %v", err)
+ }
+ spoolReader, err := spooled3.Open()
+ if err != nil {
+ t.Fatalf("open spool: %v", err)
+ }
+ block3, err := api.WalkBlockEnvelope(spoolReader)
+ spoolReader.Close()
+ if err != nil {
+ t.Fatalf("WalkBlockEnvelope: %v", err)
+ }
+ nb3, err := parser.ParseBlock(ctx, block3, opts...)
+ spooled3.Close()
+ if err != nil {
+ t.Fatalf("ParseBlock: %v", err)
+ }
+ elapsed3 := time.Since(start3)
+ peak3.stop()
+ after3 := heapAlloc()
+
+ t.Logf("\n=== Phase 2 generic (DownloadStream -> WalkBlockEnvelope -> ParseBlock) ===")
+ t.Logf("total elapsed: %s", elapsed3)
+ t.Logf("tx count: %d", nb3.NumTransactions)
+ t.Logf("heap before: %s", humanBytes(before3))
+ t.Logf("heap after: %s", humanBytes(after3))
+ t.Logf("heap peak: %s (+%s)", humanBytes(peak3.peak), humanBytes(subU(peak3.peak, before3)))
+ t.Logf("persistent NB size: %s", humanBytes(uint64(protoSize(nb3))))
+
+ t.Logf("\n=== Peak heap summary ===")
+ t.Logf("legacy (Download): %s", humanBytes(subU(peak2.peak, before2)))
+ t.Logf("Phase 2 generic (DownloadStream): %s", humanBytes(subU(peak3.peak, before3)))
+ t.Logf("Phase 2 bitcoin (DownloadStream+iter): %s", humanBytes(subU(peak.peak, before)))
+}
diff --git a/internal/blockchain/parser/ethereum/abstract_native.go b/internal/blockchain/parser/ethereum/abstract_native.go
new file mode 100644
index 00000000..a7be06e8
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/abstract_native.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewAbstractNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ // Reuse the Ethereum native parser since its an EVM chain.
+ return NewEthereumNativeParser(params, opts...)
+}
diff --git a/internal/blockchain/parser/ethereum/abstract_validato.go b/internal/blockchain/parser/ethereum/abstract_validato.go
new file mode 100644
index 00000000..228820fc
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/abstract_validato.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewAbstractValidator(params internal.ParserParams) internal.TrustlessValidator {
+ // Reuse the same implementation as Ethereum.
+ return NewEthereumValidator(params)
+}
diff --git a/internal/blockchain/parser/ethereum/beacon/native.go b/internal/blockchain/parser/ethereum/beacon/native.go
index e3bb118f..982288b6 100644
--- a/internal/blockchain/parser/ethereum/beacon/native.go
+++ b/internal/blockchain/parser/ethereum/beacon/native.go
@@ -258,7 +258,7 @@ func NewNativeParser(params internal.ParserParams, opts ...internal.ParserFactor
}, nil
}
-func (p *nativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+func (p *nativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block, opts ...internal.ParseOption) (*api.NativeBlock, error) {
metadata := rawBlock.GetMetadata()
if metadata == nil {
return nil, xerrors.New("metadata not found")
diff --git a/internal/blockchain/parser/ethereum/ethereum_native.go b/internal/blockchain/parser/ethereum/ethereum_native.go
index 6dabbf44..017d335a 100644
--- a/internal/blockchain/parser/ethereum/ethereum_native.go
+++ b/internal/blockchain/parser/ethereum/ethereum_native.go
@@ -21,6 +21,7 @@ import (
"github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
"github.com/coinbase/chainstorage/internal/config"
"github.com/coinbase/chainstorage/internal/utils/log"
+ "github.com/coinbase/chainstorage/internal/utils/utils"
"github.com/coinbase/chainstorage/protos/coinbase/c3/common"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
)
@@ -237,19 +238,36 @@ type (
Output EthereumHexString `json:"output"`
}
+ // receiptIndexMismatchTolerator decides whether a receipt/transaction TransactionIndex
+ // mismatch (when hash/blockHash/blockNumber already match) is acceptable for a given chain.
+ // The default parser has none (strict). Chains with known data quirks inject one via
+ // WithReceiptIndexMismatchTolerator to keep their special-case knowledge out of this file.
+ receiptIndexMismatchTolerator func(
+ transaction *api.EthereumTransaction,
+ receipt *api.EthereumTransactionReceipt,
+ transactions []*api.EthereumTransaction,
+ txHashCounts map[string]int,
+ ) bool
+
ethereumNativeParserImpl struct {
- Logger *zap.Logger
- validate *validator.Validate
- nodeType types.EthereumNodeType
- traceType types.TraceType
- config *config.Config
- metrics *ethereumNativeParserMetrics
+ Logger *zap.Logger
+ validate *validator.Validate
+ nodeType types.EthereumNodeType
+ traceType types.TraceType
+ config *config.Config
+ metrics *ethereumNativeParserMetrics
+ src20Parser SRC20TokenTransferParser // Optional, only Seismic sets this
+ timestampInMs bool // If true, raw timestamps are in milliseconds and need to be converted to seconds
+ receiptIndexMismatchTolerator receiptIndexMismatchTolerator // Optional, only chains with duplicate-tx quirks (e.g. MegaETH) set this
}
ethereumParserOptions struct {
- nodeType types.EthereumNodeType
- traceType types.TraceType
- checksumAddress bool
+ nodeType types.EthereumNodeType
+ traceType types.TraceType
+ checksumAddress bool
+ src20Parser SRC20TokenTransferParser
+ timestampInMs bool
+ receiptIndexMismatchTolerator receiptIndexMismatchTolerator
}
nestedParityTrace struct {
@@ -288,6 +306,7 @@ const (
parseFailure = "parse_failure"
arbitrumNITROUpgradeBlockNumber = 22_207_818
+ tronNoncePlaceHolder = "0x0000000000000000"
)
func (v EthereumHexString) MarshalJSON() ([]byte, error) {
@@ -331,7 +350,7 @@ func (v *EthereumQuantity) UnmarshalJSON(input []byte) error {
return xerrors.Errorf("failed to unmarshal EthereumQuantity into string: %w", err)
}
- if s == "" {
+ if s == "" || s == tronNoncePlaceHolder {
*v = 0
return nil
}
@@ -448,12 +467,16 @@ func NewEthereumNativeParser(params internal.ParserParams, opts ...internal.Pars
}
return ðereumNativeParserImpl{
- Logger: log.WithPackage(params.Logger),
- validate: validator.New(),
- nodeType: options.nodeType,
- traceType: options.traceType,
- config: params.Config,
- metrics: newEthereumNativeParserMetrics(params.Metrics),
+ Logger: log.WithPackage(params.Logger),
+ validate: validator.New(),
+ nodeType: options.nodeType,
+ traceType: options.traceType,
+ config: params.Config,
+ metrics: newEthereumNativeParserMetrics(params.Metrics),
+ src20Parser: options.src20Parser,
+ timestampInMs: options.timestampInMs,
+
+ receiptIndexMismatchTolerator: options.receiptIndexMismatchTolerator,
}, nil
}
@@ -492,7 +515,36 @@ func WithEthereumChecksumAddress() internal.ParserFactoryOption {
}
}
-func (p *ethereumNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+// WithSRC20Parser sets the SRC20 token transfer parser for Seismic chain.
+func WithSRC20Parser(parser SRC20TokenTransferParser) internal.ParserFactoryOption {
+ return func(options any) {
+ if v, ok := options.(*ethereumParserOptions); ok {
+ v.src20Parser = parser
+ }
+ }
+}
+
+// WithTimestampInMs indicates that raw block timestamps are in milliseconds and should be converted to seconds.
+func WithTimestampInMs() internal.ParserFactoryOption {
+ return func(options any) {
+ if v, ok := options.(*ethereumParserOptions); ok {
+ v.timestampInMs = true
+ }
+ }
+}
+
+// WithReceiptIndexMismatchTolerator installs a chain-specific policy that decides whether a
+// receipt/transaction TransactionIndex mismatch is acceptable. Used by chains whose nodes can
+// return blocks that break positional receipt/tx index pairing (e.g. MegaETH duplicate tx hashes).
+func WithReceiptIndexMismatchTolerator(tolerator receiptIndexMismatchTolerator) internal.ParserFactoryOption {
+ return func(options any) {
+ if v, ok := options.(*ethereumParserOptions); ok {
+ v.receiptIndexMismatchTolerator = tolerator
+ }
+ }
+}
+
+func (p *ethereumNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block, opts ...internal.ParseOption) (*api.NativeBlock, error) {
metadata := rawBlock.GetMetadata()
if metadata == nil {
return nil, xerrors.New("metadata not found")
@@ -523,15 +575,46 @@ func (p *ethereumNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api
return nil, xerrors.Errorf("unexpected number of transaction receipts: expected=%v actual=%v", numTransactions, len(transactionReceipts))
}
+ // txHashCounts is only needed by a chain-specific tolerator (see
+ // WithReceiptIndexMismatchTolerator); the default strict path skips the allocation.
+ var txHashCounts map[string]int
+ if p.receiptIndexMismatchTolerator != nil {
+ txHashCounts = make(map[string]int, len(transactions))
+ for _, t := range transactions {
+ txHashCounts[t.Hash]++
+ }
+ }
+
for i, transactionReceipt := range transactionReceipts {
transaction := transactions[i]
- if transactionReceipt.TransactionHash != transaction.Hash ||
- transactionReceipt.TransactionIndex != transaction.Index ||
+ hashOrBlockMismatch := transactionReceipt.TransactionHash != transaction.Hash ||
transactionReceipt.BlockHash != header.Hash ||
- transactionReceipt.BlockNumber != header.Number {
+ transactionReceipt.BlockNumber != header.Number
+ indexMismatch := transactionReceipt.TransactionIndex != transaction.Index
+
+ tolerable := indexMismatch && !hashOrBlockMismatch &&
+ p.receiptIndexMismatchTolerator != nil &&
+ p.receiptIndexMismatchTolerator(transaction, transactionReceipt, transactions, txHashCounts)
+
+ if hashOrBlockMismatch || (indexMismatch && !tolerable) {
return nil, xerrors.Errorf("unexpected transaction receipt: transactionReceipt={%+v} transaction={%+v} block={%+v}", transactionReceipt, transaction, header)
}
+
+ if tolerable {
+ receiptPointsToHash := ""
+ if transactionReceipt.TransactionIndex < uint64(len(transactions)) {
+ receiptPointsToHash = transactions[transactionReceipt.TransactionIndex].Hash
+ }
+ p.Logger.Warn("tolerating receipt/tx index mismatch by chain-specific policy",
+ zap.String("blockchain", p.config.Chain.Blockchain.String()),
+ zap.String("transaction_hash", transaction.Hash),
+ zap.Uint64("block_number", header.Number),
+ zap.Uint64("expected_transaction_index", transaction.Index),
+ zap.Uint64("receipt_transaction_index", transactionReceipt.TransactionIndex),
+ zap.String("receipt_points_to_transaction_hash", receiptPointsToHash),
+ )
+ }
}
} else {
transactionReceipts = make([]*api.EthereumTransactionReceipt, numTransactions)
@@ -570,14 +653,26 @@ func (p *ethereumNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api
if numTransactions != len(tokenTransfers) {
return nil, xerrors.Errorf("unexpected number of token transfers: expected=%v actual=%v", numTransactions, len(tokenTransfers))
}
-
transactionToFlattenedTracesMap := make(map[string][]*api.EthereumTransactionFlattenedTrace, 0)
if isParityTrace {
- if err := p.parseTransactionFlattenedParityTraces(blobdata, transactionToFlattenedTracesMap); err != nil {
- return nil, xerrors.Errorf("failed to parse transaction parity traces: %w", err)
+ if p.config.Blockchain() != common.Blockchain_BLOCKCHAIN_TRON {
+ if err := p.parseTransactionFlattenedParityTraces(blobdata, transactionToFlattenedTracesMap); err != nil {
+ return nil, xerrors.Errorf("failed to parse transaction parity traces: %w", err)
+ }
+ }
+ }
+ // post process block data for Tron data, convert hash and account address, and set flattened traces
+ if p.config.Blockchain() == common.Blockchain_BLOCKCHAIN_TRON {
+ if err := postProcessTronBlock(
+ blobdata,
+ header,
+ transactions,
+ transactionReceipts,
+ tokenTransfers,
+ transactionToFlattenedTracesMap); err != nil {
+ return nil, xerrors.Errorf("failed to post process tron block: %w", err)
}
}
-
for i, transaction := range transactions {
transaction.Receipt = transactionReceipts[i]
transaction.TokenTransfers = tokenTransfers[i]
@@ -606,8 +701,8 @@ func (p *ethereumNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api
Blockchain: rawBlock.Blockchain,
Network: rawBlock.Network,
Tag: metadata.Tag,
- Hash: metadata.Hash,
- ParentHash: metadata.ParentHash,
+ Hash: header.Hash,
+ ParentHash: header.ParentHash,
Height: metadata.Height,
ParentHeight: metadata.ParentHeight,
Timestamp: header.Timestamp,
@@ -646,6 +741,15 @@ func (p *ethereumNativeParserImpl) GetTransaction(ctx context.Context, nativeBlo
return nil, internal.ErrNotImplemented
}
+// toTimestamp converts a raw block timestamp to a protobuf Timestamp.
+// If timestampInMs is true, preserves millisecond precision in the Nanos field.
+func (p *ethereumNativeParserImpl) toTimestamp(rawTimestamp int64) *timestamp.Timestamp {
+ if p.timestampInMs {
+ return utils.ToTimestampFromMs(rawTimestamp)
+ }
+ return utils.ToTimestamp(rawTimestamp)
+}
+
func (p *ethereumNativeParserImpl) parseHeader(data []byte) (*api.EthereumHeader, []*api.EthereumTransaction, error) {
var block EthereumBlock
if err := json.Unmarshal(data, &block); err != nil {
@@ -674,7 +778,7 @@ func (p *ethereumNativeParserImpl) parseHeader(data []byte) (*api.EthereumHeader
Index: transaction.Index.Value(),
Value: transaction.Value.Value(),
Type: transaction.Type.Value(),
- BlockTimestamp: ×tamp.Timestamp{Seconds: int64(block.Timestamp.Value())},
+ BlockTimestamp: p.toTimestamp(int64(block.Timestamp.Value())),
V: transaction.V.Value(),
S: transaction.S.Value(),
R: transaction.R.Value(),
@@ -772,7 +876,7 @@ func (p *ethereumNativeParserImpl) parseHeader(data []byte) (*api.EthereumHeader
Hash: block.Hash.Value(),
ParentHash: block.ParentHash.Value(),
Number: block.Number.Value(),
- Timestamp: ×tamp.Timestamp{Seconds: int64(block.Timestamp.Value())},
+ Timestamp: p.toTimestamp(int64(block.Timestamp.Value())),
Transactions: transactionHashes,
Nonce: block.Nonce.Value(),
Sha3Uncles: block.Sha3Uncles.Value(),
@@ -897,7 +1001,7 @@ func (p *ethereumNativeParserImpl) parseTransactionReceipts(blobdata *api.Ethere
}
// Field effectiveGasPrice is added to the eth_getTransactionReceipt call for EIP-1559.
- // Pre-London, it is equal to the transaction’s gasPrice.
+ // Pre-London, it is equal to the transaction's gasPrice.
// Post-London, it is equal to the actual gas price paid for inclusion.
// Since it's hard to backfill all old blocks, set `effectiveGasPrice` as gasPrice for Pre-London blocks.
// Ref: https://hackmd.io/@timbeiko/1559-json-rpc
@@ -1244,7 +1348,6 @@ func (p *ethereumNativeParserImpl) parseTokenTransfers(transactionReceipts []*ap
if len(eventLog.Topics) == 3 && eventLog.Topics[0] == TransferEventTopic {
// Parse ERC-20 token
// https://ethereum.org/en/developers/docs/standards/tokens/erc-20/
-
tokenTransfer, err := p.parseERC20TokenTransfer(eventLog)
if err != nil {
return nil, xerrors.Errorf("failed to parse erc20 token transfer: %w", err)
@@ -1262,6 +1365,17 @@ func (p *ethereumNativeParserImpl) parseTokenTransfers(transactionReceipts []*ap
if tokenTransfer != nil {
tokenTransfers = append(tokenTransfers, tokenTransfer)
}
+ } else if p.src20Parser != nil && len(eventLog.Topics) == 4 && eventLog.Topics[0] == SRCTransferEventTopic {
+ // Parse SRC-20 token (Seismic only)
+ // Outer topic check avoids unnecessary function calls
+ // SRCTransferEventTopic is exported from seismic_native.go
+ tokenTransfer, err := p.src20Parser.ParseSRC20TokenTransfer(eventLog)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to parse src20 token transfer: %w", err)
+ }
+ if tokenTransfer != nil {
+ tokenTransfers = append(tokenTransfers, tokenTransfer)
+ }
}
}
diff --git a/internal/blockchain/parser/ethereum/ethereum_native_test.go b/internal/blockchain/parser/ethereum/ethereum_native_test.go
index c17918be..119d84da 100644
--- a/internal/blockchain/parser/ethereum/ethereum_native_test.go
+++ b/internal/blockchain/parser/ethereum/ethereum_native_test.go
@@ -6,6 +6,7 @@ import (
"fmt"
"math"
"math/big"
+ "strings"
"testing"
"github.com/golang/protobuf/ptypes/timestamp"
@@ -2382,6 +2383,166 @@ func TestParseEthereumBlock_InvalidTransactionReceipt(t *testing.T) {
require.Contains(err.Error(), "unexpected transaction receipt")
}
+// TestParseMegaethBlock_DuplicateTransactionReceiptIndex covers the MegaETH-only relaxation
+// for blocks that list the same tx hash multiple times (e.g. mainnet block 10004831). The
+// node returns one canonical receipt (single transactionIndex) for every occurrence, so the
+// positional receipt/tx index check disagrees even though the receipt is correct. The
+// relaxation is scoped strictly: MegaETH only, hash must still match, and the mismatch must be
+// a genuine duplicate-hash artifact.
+func TestParseMegaethBlock_DuplicateTransactionReceiptIndex(t *testing.T) {
+ const (
+ blockHash = ethereumHash
+ dupTxHash = "0xe67071db25331ea3a92a4e28b516c95f2d5b62b68329b70386c19e00807f51d8"
+ otherTxHash = "0x7422d3f183f198a4645d60deae5bebb6cd44f12df2c2e24b813ddd1c58da62c3"
+ )
+
+ type txSpec struct {
+ hash string
+ index string
+ }
+
+ buildBlock := func(blockchain common.Blockchain, network common.Network, txs []txSpec, receipts []txSpec) *api.Block {
+ txJSON := func(s txSpec) string {
+ return fmt.Sprintf(`{
+ "blockHash": "%s",
+ "blockNumber": "0xacc290",
+ "from": "0x4823cc90c145fd6a16ab7668043dbba5ce79cdfc",
+ "gas": "0x15f90",
+ "gasPrice": "0x22cee4f700",
+ "hash": "%s",
+ "input": "0x",
+ "nonce": "0x1f0",
+ "to": "0xdac17f958d2ee523a2206206994597c13d831ec7",
+ "transactionIndex": "%s",
+ "type": "0x0",
+ "value": "0xa"
+ }`, blockHash, s.hash, s.index)
+ }
+ receiptJSON := func(s txSpec) []byte {
+ return []byte(fmt.Sprintf(`{
+ "transactionHash": "%s",
+ "transactionIndex": "%s",
+ "blockHash": "%s",
+ "blockNumber": "0xacc290",
+ "from": "0x98265d92b016df8758f361fb8d2f9a813c82494a",
+ "to": "0x7a250d5630b4cf539739df2c5dacb4c659f2488d",
+ "cumulativeGasUsed": "0xbca58c",
+ "gasUsed": "0x1b889",
+ "logsBloom": "0x00",
+ "logs": [],
+ "status": "0x1",
+ "type": "0x0"
+ }`, s.hash, s.index, blockHash))
+ }
+ traceJSON := []byte(`{
+ "type": "CALL",
+ "from": "0x4823cc90c145fd6a16ab7668043dbba5ce79cdfc",
+ "to": "0xdac17f958d2ee523a2206206994597c13d831ec7",
+ "value": "0x0",
+ "gas": "0x10b1c",
+ "gasUsed": "0x4c91",
+ "input": "0x",
+ "output": "0x"
+ }`)
+
+ txsJSON := make([]string, len(txs))
+ for i, s := range txs {
+ txsJSON[i] = txJSON(s)
+ }
+ header := []byte(fmt.Sprintf(`{
+ "hash": "%s",
+ "parentHash": "%s",
+ "number": "0xacc290",
+ "timestamp": "0x5fbd2fb9",
+ "transactions": [%s]
+ }`, blockHash, ethereumParentHash, strings.Join(txsJSON, ",")))
+
+ rawReceipts := make([][]byte, len(receipts))
+ for i, s := range receipts {
+ rawReceipts[i] = receiptJSON(s)
+ }
+ rawTraces := make([][]byte, len(txs))
+ for i := range txs {
+ rawTraces[i] = traceJSON
+ }
+
+ return &api.Block{
+ Blockchain: blockchain,
+ Network: network,
+ Metadata: ethereumMetadata,
+ Blobdata: &api.Block_Ethereum{
+ Ethereum: &api.EthereumBlobdata{
+ Header: header,
+ TransactionReceipts: rawReceipts,
+ TransactionTraces: rawTraces,
+ },
+ },
+ }
+ }
+
+ newParser := func(t *testing.T, blockchain common.Blockchain, network common.Network) (internal.Parser, func()) {
+ var parser internal.Parser
+ app := testapp.New(
+ t,
+ Module,
+ internal.Module,
+ testapp.WithBlockchainNetwork(blockchain, network),
+ fx.Populate(&parser),
+ )
+ return parser, app.Close
+ }
+
+ t.Run("megaeth duplicate hash is tolerated", func(t *testing.T) {
+ require := testutil.Require(t)
+ // Same hash at index 0 and 1; the node returns the canonical receipt (index 1) for both.
+ block := buildBlock(
+ common.Blockchain_BLOCKCHAIN_MEGAETH,
+ common.Network_NETWORK_MEGAETH_MAINNET,
+ []txSpec{{dupTxHash, "0x0"}, {dupTxHash, "0x1"}},
+ []txSpec{{dupTxHash, "0x1"}, {dupTxHash, "0x1"}},
+ )
+ parser, cleanup := newParser(t, common.Blockchain_BLOCKCHAIN_MEGAETH, common.Network_NETWORK_MEGAETH_MAINNET)
+ defer cleanup()
+
+ nativeBlock, err := parser.ParseNativeBlock(context.Background(), block)
+ require.NoError(err)
+ require.NotNil(nativeBlock)
+ })
+
+ t.Run("ethereum with identical data still fails", func(t *testing.T) {
+ require := testutil.Require(t)
+ block := buildBlock(
+ common.Blockchain_BLOCKCHAIN_ETHEREUM,
+ common.Network_NETWORK_ETHEREUM_MAINNET,
+ []txSpec{{dupTxHash, "0x0"}, {dupTxHash, "0x1"}},
+ []txSpec{{dupTxHash, "0x1"}, {dupTxHash, "0x1"}},
+ )
+ parser, cleanup := newParser(t, common.Blockchain_BLOCKCHAIN_ETHEREUM, common.Network_NETWORK_ETHEREUM_MAINNET)
+ defer cleanup()
+
+ _, err := parser.ParseNativeBlock(context.Background(), block)
+ require.Error(err)
+ require.Contains(err.Error(), "unexpected transaction receipt")
+ })
+
+ t.Run("megaeth non-duplicate index mismatch still fails", func(t *testing.T) {
+ require := testutil.Require(t)
+ // Two distinct hashes: the index mismatch is NOT a duplicate-hash artifact, so it must fail.
+ block := buildBlock(
+ common.Blockchain_BLOCKCHAIN_MEGAETH,
+ common.Network_NETWORK_MEGAETH_MAINNET,
+ []txSpec{{dupTxHash, "0x0"}, {otherTxHash, "0x1"}},
+ []txSpec{{dupTxHash, "0x1"}, {otherTxHash, "0x1"}},
+ )
+ parser, cleanup := newParser(t, common.Blockchain_BLOCKCHAIN_MEGAETH, common.Network_NETWORK_MEGAETH_MAINNET)
+ defer cleanup()
+
+ _, err := parser.ParseNativeBlock(context.Background(), block)
+ require.Error(err)
+ require.Contains(err.Error(), "unexpected transaction receipt")
+ })
+}
+
func TestParseEthereumBlock_PostLondon(t *testing.T) {
require := testutil.Require(t)
diff --git a/internal/blockchain/parser/ethereum/ethereumclassic_native.go b/internal/blockchain/parser/ethereum/ethereumclassic_native.go
new file mode 100644
index 00000000..a26fca66
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/ethereumclassic_native.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewEthereumClassicNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ // Ethereum Classic shares the same data schema as Ethereum since its an EVM chain.
+ return NewEthereumNativeParser(params, opts...)
+}
diff --git a/internal/blockchain/parser/ethereum/ethereumclassic_validator.go b/internal/blockchain/parser/ethereum/ethereumclassic_validator.go
new file mode 100644
index 00000000..2b2834d5
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/ethereumclassic_validator.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewEthereumClassicValidator(params internal.ParserParams) internal.TrustlessValidator {
+ // Reuse the same implementation as Ethereum.
+ return NewEthereumValidator(params)
+}
diff --git a/internal/blockchain/parser/ethereum/megaeth_native.go b/internal/blockchain/parser/ethereum/megaeth_native.go
new file mode 100644
index 00000000..ee584481
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/megaeth_native.go
@@ -0,0 +1,45 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func NewMegaethNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ // Reuse the Ethereum native parser since its an EVM chain, but tolerate MegaETH's
+ // duplicate-tx-hash quirk in receipt/tx index validation (see below).
+ opts = append(opts, WithReceiptIndexMismatchTolerator(tolerateMegaethDuplicateReceiptIndex))
+ return NewEthereumNativeParser(params, opts...)
+}
+
+// tolerateMegaethDuplicateReceiptIndex reports whether a receipt/transaction TransactionIndex
+// mismatch is explained by the same tx hash appearing multiple times in a MegaETH block (e.g.
+// mainnet block 10004831). A MegaETH block can list one tx hash at several indices;
+// eth_getTransactionReceipt then returns the single canonical receipt (one index) for every
+// occurrence, so positional receipt/tx pairing disagrees on index even though the receipt is
+// correct. The caller has already confirmed hash/blockHash/blockNumber match.
+//
+// The receipt is intentionally NOT mutated: the node only has one receipt for the duplicated
+// hash, so the canonical index is the honest value. As a result, for the duplicated tx the
+// flattened transaction's TransactionIndex (its block position) may differ from the
+// receipt/event-log/token-transfer TransactionIndex (the node's canonical index).
+func tolerateMegaethDuplicateReceiptIndex(
+ transaction *api.EthereumTransaction,
+ receipt *api.EthereumTransactionReceipt,
+ transactions []*api.EthereumTransaction,
+ txHashCounts map[string]int,
+) bool {
+ // (a) The hash must genuinely be duplicated in this block (position-independent).
+ if txHashCounts[transaction.Hash] <= 1 {
+ return false
+ }
+ // (b) The receipt's index must point at a real slot that holds the SAME hash —
+ // i.e. the canonical occurrence — not an arbitrary/garbage index. Compare in
+ // uint64 BEFORE converting to int to avoid a 64->int overflow producing a
+ // negative/wrapped index.
+ if receipt.TransactionIndex >= uint64(len(transactions)) {
+ return false
+ }
+ idx := int(receipt.TransactionIndex)
+ return transactions[idx].Hash == transaction.Hash
+}
diff --git a/internal/blockchain/parser/ethereum/megaeth_validator.go b/internal/blockchain/parser/ethereum/megaeth_validator.go
new file mode 100644
index 00000000..fea61285
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/megaeth_validator.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewMegaethValidator(params internal.ParserParams) internal.TrustlessValidator {
+ // Reuse the same implementation as Ethereum.
+ return NewEthereumValidator(params)
+}
diff --git a/internal/blockchain/parser/ethereum/module.go b/internal/blockchain/parser/ethereum/module.go
index 8290ee84..de1d1ed5 100644
--- a/internal/blockchain/parser/ethereum/module.go
+++ b/internal/blockchain/parser/ethereum/module.go
@@ -36,5 +36,35 @@ var Module = fx.Options(
Build(),
internal.NewParserBuilder("fantom", NewFantomNativeParser).
Build(),
+ internal.NewParserBuilder("tron", NewTronNativeParser).
+ SetValidatorFactory(NewBaseValidator).
+ Build(),
+ internal.NewParserBuilder("story", NewStoryNativeParser).
+ SetValidatorFactory(NewStoryValidator).
+ Build(),
+ internal.NewParserBuilder("ethereumclassic", NewEthereumClassicNativeParser).
+ SetValidatorFactory(NewEthereumClassicValidator).
+ Build(),
+ internal.NewParserBuilder("plasma", NewPlasmaNativeParser).
+ SetValidatorFactory(NewPlasmaValidator).
+ Build(),
+ internal.NewParserBuilder("monad", NewMonadNativeParser).
+ SetValidatorFactory(NewMonadValidator).
+ Build(),
+ internal.NewParserBuilder("abstract", NewAbstractNativeParser).
+ SetValidatorFactory(NewAbstractValidator).
+ Build(),
+ internal.NewParserBuilder("megaeth", NewMegaethNativeParser).
+ SetValidatorFactory(NewMegaethValidator).
+ Build(),
+ internal.NewParserBuilder("seismic", NewSeismicNativeParser).
+ SetValidatorFactory(NewSeismicValidator).
+ Build(),
+ internal.NewParserBuilder("tempo", NewTempoNativeParser).
+ SetValidatorFactory(NewTempoValidator).
+ Build(),
+ internal.NewParserBuilder("robinhood", NewRobinhoodNativeParser).
+ SetValidatorFactory(NewRobinhoodValidator).
+ Build(),
beacon.Module,
)
diff --git a/internal/blockchain/parser/ethereum/monad_native.go b/internal/blockchain/parser/ethereum/monad_native.go
new file mode 100644
index 00000000..8fb74372
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/monad_native.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewMonadNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ // Reuse the Ethereum native parser since its an EVM chain.
+ return NewEthereumNativeParser(params, opts...)
+}
diff --git a/internal/blockchain/parser/ethereum/monad_validator.go b/internal/blockchain/parser/ethereum/monad_validator.go
new file mode 100644
index 00000000..c62cac5d
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/monad_validator.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewMonadValidator(params internal.ParserParams) internal.TrustlessValidator {
+ // Reuse the same implementation as Ethereum.
+ return NewEthereumValidator(params)
+}
diff --git a/internal/blockchain/parser/ethereum/plasma_native.go b/internal/blockchain/parser/ethereum/plasma_native.go
new file mode 100644
index 00000000..c05818a4
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/plasma_native.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewPlasmaNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ // Plasma shares the same data schema as Ethereum since its an EVM chain.
+ return NewEthereumNativeParser(params, opts...)
+}
diff --git a/internal/blockchain/parser/ethereum/plasma_validator.go b/internal/blockchain/parser/ethereum/plasma_validator.go
new file mode 100644
index 00000000..87453d63
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/plasma_validator.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewPlasmaValidator(params internal.ParserParams) internal.TrustlessValidator {
+ // Reuse the same implementation as Ethereum.
+ return NewEthereumValidator(params)
+}
diff --git a/internal/blockchain/parser/ethereum/robinhood_native.go b/internal/blockchain/parser/ethereum/robinhood_native.go
new file mode 100644
index 00000000..e41ebad9
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/robinhood_native.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewRobinhoodNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ // Reuse the Ethereum native parser since its an EVM chain.
+ return NewEthereumNativeParser(params, opts...)
+}
diff --git a/internal/blockchain/parser/ethereum/robinhood_validator.go b/internal/blockchain/parser/ethereum/robinhood_validator.go
new file mode 100644
index 00000000..8074232c
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/robinhood_validator.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewRobinhoodValidator(params internal.ParserParams) internal.TrustlessValidator {
+ // Reuse the same implementation as Ethereum.
+ return NewEthereumValidator(params)
+}
diff --git a/internal/blockchain/parser/ethereum/seismic_native.go b/internal/blockchain/parser/ethereum/seismic_native.go
new file mode 100644
index 00000000..3a157227
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/seismic_native.go
@@ -0,0 +1,182 @@
+package ethereum
+
+import (
+ "crypto/cipher"
+ "encoding/hex"
+ "os"
+ "strings"
+
+ "github.com/SeismicSystems/aes"
+ "github.com/ethereum/go-ethereum/accounts/abi"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+const (
+ // SRCTransferEventTopic is the event signature for SRC20 Transfer events.
+ // Exported for use in ethereum_native.go for outer topic check.
+ SRCTransferEventTopic = "0x80ffa007a69623ef13594f5e8178eee6c4ef2d0cba74c08329e879f695b7d3f6"
+
+ src20Abi = `[
+ {
+ "type": "event",
+ "name": "Approval",
+ "inputs": [
+ {"name": "owner", "type": "address", "indexed": true, "internalType": "address"},
+ {"name": "spender", "type": "address", "indexed": true, "internalType": "address"},
+ {"name": "encryptKeyHash", "type": "bytes32", "indexed": true, "internalType": "bytes32"},
+ {"name": "encryptedAmount", "type": "bytes", "indexed": false, "internalType": "bytes"}
+ ],
+ "anonymous": false
+ },
+ {
+ "type": "event",
+ "name": "Transfer",
+ "inputs": [
+ {"name": "from", "type": "address", "indexed": true, "internalType": "address"},
+ {"name": "to", "type": "address", "indexed": true, "internalType": "address"},
+ {"name": "encryptKeyHash", "type": "bytes32", "indexed": true, "internalType": "bytes32"},
+ {"name": "encryptedAmount", "type": "bytes", "indexed": false, "internalType": "bytes"}
+ ],
+ "anonymous": false
+ }
+ ]`
+)
+
+// SRC20TokenTransferParser handles parsing of Seismic's encrypted SRC20 token transfers.
+// Interface defined for testability and mock support.
+type SRC20TokenTransferParser interface {
+ ParseSRC20TokenTransfer(eventLog *api.EthereumEventLog) (*api.EthereumTokenTransfer, error)
+}
+
+// seismicSRC20Parser implements SRC20TokenTransferParser for Seismic chain.
+type seismicSRC20Parser struct {
+ src20ABI *abi.ABI
+ aesGCM cipher.AEAD
+ keyHash string
+}
+
+// NewSeismicSRC20Parser creates a new SRC20 token transfer parser.
+// aesKeyHex should be a hex-encoded AES key (with or without "0x" prefix).
+func NewSeismicSRC20Parser(aesKeyHex string) (SRC20TokenTransferParser, error) {
+ if aesKeyHex == "" {
+ return nil, xerrors.New("SRC20 AES key is required")
+ }
+
+ // Parse ABI
+ contractAbi, err := abi.JSON(strings.NewReader(src20Abi))
+ if err != nil {
+ return nil, xerrors.Errorf("failed to parse SRC20 ABI: %w", err)
+ }
+
+ // Decode AES key
+ aesKey, err := hex.DecodeString(strings.TrimPrefix(aesKeyHex, "0x"))
+ if err != nil {
+ return nil, xerrors.Errorf("failed to decode AES key: %w", err)
+ }
+
+ // Create AES-GCM cipher
+ aesGCM, err := aes.CreateAESGCM(aesKey)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create AES-GCM cipher: %w", err)
+ }
+
+ return &seismicSRC20Parser{
+ src20ABI: &contractAbi,
+ aesGCM: aesGCM,
+ keyHash: aes.Keccak256Hash(aesKey).Hex(),
+ }, nil
+}
+
+func (p *seismicSRC20Parser) ParseSRC20TokenTransfer(eventLog *api.EthereumEventLog) (*api.EthereumTokenTransfer, error) {
+ // Defensive check: although the outer layer already checks the topic, we verify here for safety
+ // and the key hash is the last topic, only keep those have same key hash with hash of the AES key
+ if len(eventLog.Topics) != 4 || eventLog.Topics[0] != SRCTransferEventTopic || eventLog.Topics[3] != p.keyHash {
+ return nil, nil
+ }
+
+ // Parse event data
+ var transferEvent struct {
+ EncryptedAmount []byte
+ }
+
+ logData, err := hex.DecodeString(strings.TrimPrefix(eventLog.Data, "0x"))
+ if err != nil {
+ return nil, xerrors.Errorf("failed to decode log data: %w", err)
+ }
+
+ if err := p.src20ABI.UnpackIntoInterface(&transferEvent, "Transfer", logData); err != nil {
+ return nil, xerrors.Errorf("failed to unpack Transfer event: %w", err)
+ }
+
+ value, err := aes.DecryptAESGCM(transferEvent.EncryptedAmount, p.aesGCM)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to decrypt amount: %w", err)
+ }
+ // Clean addresses from indexed topics
+ tokenAddress, err := internal.CleanAddress(eventLog.Address)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to clean token address for src20: %w", err)
+ }
+ fromAddress, err := internal.CleanAddress(eventLog.Topics[1])
+ if err != nil {
+ return nil, xerrors.Errorf("failed to clean from address for src20: %w", err)
+ }
+ toAddress, err := internal.CleanAddress(eventLog.Topics[2])
+ if err != nil {
+ return nil, xerrors.Errorf("failed to clean to address for src20: %w", err)
+ }
+
+ valueStr := value.String()
+
+ return &api.EthereumTokenTransfer{
+ TokenAddress: tokenAddress,
+ FromAddress: fromAddress,
+ ToAddress: toAddress,
+ Value: valueStr,
+ TransactionHash: eventLog.TransactionHash,
+ TransactionIndex: eventLog.TransactionIndex,
+ LogIndex: eventLog.LogIndex,
+ BlockHash: eventLog.BlockHash,
+ BlockNumber: eventLog.BlockNumber,
+ TokenTransfer: &api.EthereumTokenTransfer_Src20{
+ Src20: &api.SRC20TokenTransfer{
+ FromAddress: fromAddress,
+ ToAddress: toAddress,
+ Value: valueStr,
+ },
+ },
+ }, nil
+}
+
+// NewSeismicNativeParser creates a new Seismic native parser.
+// It extends the Ethereum parser with SRC20 token transfer parsing capability.
+func NewSeismicNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ // Get AES key from config, with fallback to environment variable.
+ // This keeps config.New() generic while handling Seismic-specific logic here.
+ aesKey := ""
+ if params.Config.Chain.CustomParams != nil {
+ aesKey = params.Config.Chain.CustomParams["src20_aes_key"]
+ }
+ if aesKey == "" {
+ aesKey = os.Getenv("SRC20_AES_KEY")
+ }
+
+ // If AES key is available, create SRC20 parser
+ if aesKey != "" {
+ src20Parser, err := NewSeismicSRC20Parser(aesKey)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create SRC20 parser: %w", err)
+ }
+ opts = append(opts, WithSRC20Parser(src20Parser))
+ } else {
+ params.Logger.Warn("SRC20_AES_KEY is not configured for Seismic parser; SRC20 token transfers will be skipped.")
+ }
+
+ // Seismic timestamps are in milliseconds, convert to seconds.
+ opts = append(opts, WithTimestampInMs())
+
+ return NewEthereumNativeParser(params, opts...)
+}
diff --git a/internal/blockchain/parser/ethereum/seismic_native_test.go b/internal/blockchain/parser/ethereum/seismic_native_test.go
new file mode 100644
index 00000000..0bf25bca
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/seismic_native_test.go
@@ -0,0 +1,270 @@
+package ethereum
+
+import (
+ "crypto/rand"
+ "encoding/hex"
+ "math/big"
+ "testing"
+
+ seismicAes "github.com/SeismicSystems/aes"
+ "github.com/ethereum/go-ethereum/accounts/abi"
+ "github.com/stretchr/testify/require"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// testAESKeyHex is a 256-bit test AES key (hex-encoded, no 0x prefix).
+const testAESKeyHex = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+
+// encryptTestAmount encrypts a big.Int value using AES-GCM, returning the
+// ABI-encoded log data (hex string with 0x prefix) matching the Transfer event layout.
+func encryptTestAmount(t *testing.T, aesKeyHex string, amount *big.Int) string {
+ t.Helper()
+
+ aesKey, err := hex.DecodeString(aesKeyHex)
+ require.NoError(t, err)
+
+ aesGCM, err := seismicAes.CreateAESGCM(aesKey)
+ require.NoError(t, err)
+
+ // Pad amount to 32 bytes (uint256)
+ plaintext := amount.Bytes()
+ padded := make([]byte, 32)
+ copy(padded[32-len(plaintext):], plaintext)
+
+ // Encrypt: ciphertext || nonce (12 bytes)
+ nonce := make([]byte, seismicAes.NONCE_LENGTH)
+ _, err = rand.Read(nonce)
+ require.NoError(t, err)
+
+ ciphertext := aesGCM.Seal(nil, nonce, padded, nil)
+ // Format expected by DecryptAESGCM: ciphertext || nonce
+ encrypted := append(ciphertext, nonce...)
+
+ // ABI-encode as bytes (dynamic type)
+ bytesType, err := abi.NewType("bytes", "", nil)
+ require.NoError(t, err)
+
+ args := abi.Arguments{{Type: bytesType}}
+ encoded, err := args.Pack(encrypted)
+ require.NoError(t, err)
+
+ return "0x" + hex.EncodeToString(encoded)
+}
+
+// testKeyHash returns the keccak256 hash of the test AES key, matching what the parser computes.
+func testKeyHash(t *testing.T) string {
+ t.Helper()
+ aesKey, err := hex.DecodeString(testAESKeyHex)
+ require.NoError(t, err)
+ return seismicAes.Keccak256Hash(aesKey).Hex()
+}
+
+func TestParseSRC20TokenTransfer(t *testing.T) {
+ parser, err := NewSeismicSRC20Parser(testAESKeyHex)
+ require.NoError(t, err)
+
+ keyHash := testKeyHash(t)
+
+ t.Run("ValidTransfer", func(t *testing.T) {
+ amount := big.NewInt(1000000)
+ data := encryptTestAmount(t, testAESKeyHex, amount)
+
+ eventLog := &api.EthereumEventLog{
+ Topics: []string{
+ SRCTransferEventTopic,
+ "0x000000000000000000000000aabbccddee11223344556677889900aabbccddee",
+ "0x00000000000000000000000011223344556677889900aabbccddeeff00112233",
+ keyHash,
+ },
+ Data: data,
+ Address: "0x0000000000000000000000001234567890abcdef1234567890abcdef12345678",
+ TransactionHash: "0xdeadbeef",
+ TransactionIndex: 5,
+ LogIndex: 3,
+ BlockHash: "0xblockhash",
+ BlockNumber: 100,
+ }
+
+ transfer, err := parser.ParseSRC20TokenTransfer(eventLog)
+ require.NoError(t, err)
+ require.NotNil(t, transfer)
+
+ require.Equal(t, "0x1234567890abcdef1234567890abcdef12345678", transfer.TokenAddress)
+ require.Equal(t, "0xaabbccddee11223344556677889900aabbccddee", transfer.FromAddress)
+ require.Equal(t, "0x11223344556677889900aabbccddeeff00112233", transfer.ToAddress)
+ require.Equal(t, amount.String(), transfer.Value)
+ require.Equal(t, "0xdeadbeef", transfer.TransactionHash)
+ require.Equal(t, uint64(5), transfer.TransactionIndex)
+ require.Equal(t, uint64(3), transfer.LogIndex)
+ require.Equal(t, "0xblockhash", transfer.BlockHash)
+ require.Equal(t, uint64(100), transfer.BlockNumber)
+
+ src20 := transfer.GetSrc20()
+ require.NotNil(t, src20)
+ require.Equal(t, "0xaabbccddee11223344556677889900aabbccddee", src20.FromAddress)
+ require.Equal(t, "0x11223344556677889900aabbccddeeff00112233", src20.ToAddress)
+ require.Equal(t, amount.String(), src20.Value)
+ })
+
+ t.Run("ZeroAmount", func(t *testing.T) {
+ amount := big.NewInt(0)
+ data := encryptTestAmount(t, testAESKeyHex, amount)
+
+ eventLog := &api.EthereumEventLog{
+ Topics: []string{
+ SRCTransferEventTopic,
+ "0x000000000000000000000000aabbccddee11223344556677889900aabbccddee",
+ "0x00000000000000000000000011223344556677889900aabbccddeeff00112233",
+ keyHash,
+ },
+ Data: data,
+ Address: "0x0000000000000000000000001234567890abcdef1234567890abcdef12345678",
+ }
+
+ transfer, err := parser.ParseSRC20TokenTransfer(eventLog)
+ require.NoError(t, err)
+ require.NotNil(t, transfer)
+ require.Equal(t, "0", transfer.Value)
+ })
+
+ t.Run("LargeAmount", func(t *testing.T) {
+ // Use a large uint256-range value
+ amount := new(big.Int)
+ amount.SetString("115792089237316195423570985008687907853269984665640564039457584007913129639935", 10) // max uint256
+ data := encryptTestAmount(t, testAESKeyHex, amount)
+
+ eventLog := &api.EthereumEventLog{
+ Topics: []string{
+ SRCTransferEventTopic,
+ "0x000000000000000000000000aabbccddee11223344556677889900aabbccddee",
+ "0x00000000000000000000000011223344556677889900aabbccddeeff00112233",
+ keyHash,
+ },
+ Data: data,
+ Address: "0x0000000000000000000000001234567890abcdef1234567890abcdef12345678",
+ }
+
+ transfer, err := parser.ParseSRC20TokenTransfer(eventLog)
+ require.NoError(t, err)
+ require.NotNil(t, transfer)
+ require.Equal(t, amount.String(), transfer.Value)
+ })
+
+ t.Run("WrongTopicCount_TooFew", func(t *testing.T) {
+ eventLog := &api.EthereumEventLog{
+ Topics: []string{
+ SRCTransferEventTopic,
+ "0x000000000000000000000000aabbccddee11223344556677889900aabbccddee",
+ },
+ }
+
+ transfer, err := parser.ParseSRC20TokenTransfer(eventLog)
+ require.NoError(t, err)
+ require.Nil(t, transfer)
+ })
+
+ t.Run("WrongEventTopic", func(t *testing.T) {
+ eventLog := &api.EthereumEventLog{
+ Topics: []string{
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", // ERC20 Transfer topic
+ "0x000000000000000000000000aabbccddee11223344556677889900aabbccddee",
+ "0x00000000000000000000000011223344556677889900aabbccddeeff00112233",
+ keyHash,
+ },
+ }
+
+ transfer, err := parser.ParseSRC20TokenTransfer(eventLog)
+ require.NoError(t, err)
+ require.Nil(t, transfer)
+ })
+
+ t.Run("WrongKeyHash", func(t *testing.T) {
+ eventLog := &api.EthereumEventLog{
+ Topics: []string{
+ SRCTransferEventTopic,
+ "0x000000000000000000000000aabbccddee11223344556677889900aabbccddee",
+ "0x00000000000000000000000011223344556677889900aabbccddeeff00112233",
+ "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef",
+ },
+ }
+
+ transfer, err := parser.ParseSRC20TokenTransfer(eventLog)
+ require.NoError(t, err)
+ require.Nil(t, transfer)
+ })
+
+ t.Run("EmptyTopics", func(t *testing.T) {
+ eventLog := &api.EthereumEventLog{
+ Topics: []string{},
+ }
+
+ transfer, err := parser.ParseSRC20TokenTransfer(eventLog)
+ require.NoError(t, err)
+ require.Nil(t, transfer)
+ })
+
+ t.Run("InvalidLogData", func(t *testing.T) {
+ eventLog := &api.EthereumEventLog{
+ Topics: []string{
+ SRCTransferEventTopic,
+ "0x000000000000000000000000aabbccddee11223344556677889900aabbccddee",
+ "0x00000000000000000000000011223344556677889900aabbccddeeff00112233",
+ keyHash,
+ },
+ Data: "0xnotvalidhex",
+ Address: "0x0000000000000000000000001234567890abcdef1234567890abcdef12345678",
+ }
+
+ transfer, err := parser.ParseSRC20TokenTransfer(eventLog)
+ require.Error(t, err)
+ require.Nil(t, transfer)
+ })
+
+ t.Run("AddressTooShort", func(t *testing.T) {
+ amount := big.NewInt(500)
+ data := encryptTestAmount(t, testAESKeyHex, amount)
+
+ eventLog := &api.EthereumEventLog{
+ Topics: []string{
+ SRCTransferEventTopic,
+ "0xshort",
+ "0x00000000000000000000000011223344556677889900aabbccddeeff00112233",
+ keyHash,
+ },
+ Data: data,
+ Address: "0x0000000000000000000000001234567890abcdef1234567890abcdef12345678",
+ }
+
+ transfer, err := parser.ParseSRC20TokenTransfer(eventLog)
+ require.Error(t, err)
+ require.Nil(t, transfer)
+ })
+}
+
+func TestNewSeismicSRC20Parser(t *testing.T) {
+ t.Run("ValidKey", func(t *testing.T) {
+ parser, err := NewSeismicSRC20Parser(testAESKeyHex)
+ require.NoError(t, err)
+ require.NotNil(t, parser)
+ })
+
+ t.Run("ValidKeyWith0xPrefix", func(t *testing.T) {
+ parser, err := NewSeismicSRC20Parser("0x" + testAESKeyHex)
+ require.NoError(t, err)
+ require.NotNil(t, parser)
+ })
+
+ t.Run("EmptyKey", func(t *testing.T) {
+ parser, err := NewSeismicSRC20Parser("")
+ require.Error(t, err)
+ require.Nil(t, parser)
+ require.Contains(t, err.Error(), "SRC20 AES key is required")
+ })
+
+ t.Run("InvalidHexKey", func(t *testing.T) {
+ parser, err := NewSeismicSRC20Parser("not-a-hex-key")
+ require.Error(t, err)
+ require.Nil(t, parser)
+ })
+}
diff --git a/internal/blockchain/parser/ethereum/seismic_validator.go b/internal/blockchain/parser/ethereum/seismic_validator.go
new file mode 100644
index 00000000..265343b4
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/seismic_validator.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewSeismicValidator(params internal.ParserParams) internal.TrustlessValidator {
+ // Reuse the same implementation as Ethereum.
+ return NewEthereumValidator(params)
+}
diff --git a/internal/blockchain/parser/ethereum/story_native.go b/internal/blockchain/parser/ethereum/story_native.go
new file mode 100644
index 00000000..e46de482
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/story_native.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewStoryNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ // Story shares the same data schema as Ethereum since its an EVM chain.
+ return NewEthereumNativeParser(params, opts...)
+}
diff --git a/internal/blockchain/parser/ethereum/story_validator.go b/internal/blockchain/parser/ethereum/story_validator.go
new file mode 100644
index 00000000..3a8d38a4
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/story_validator.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewStoryValidator(params internal.ParserParams) internal.TrustlessValidator {
+ // Reuse the same implementation as Ethereum.
+ return NewEthereumValidator(params)
+}
diff --git a/internal/blockchain/parser/ethereum/story_validator_test.go b/internal/blockchain/parser/ethereum/story_validator_test.go
new file mode 100644
index 00000000..dc62f195
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/story_validator_test.go
@@ -0,0 +1,3 @@
+package ethereum
+
+//TODO - Implement the testing for the story validator
diff --git a/internal/blockchain/parser/ethereum/tempo_native.go b/internal/blockchain/parser/ethereum/tempo_native.go
new file mode 100644
index 00000000..e20d779c
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/tempo_native.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewTempoNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ // Reuse the Ethereum native parser since its an EVM chain.
+ return NewEthereumNativeParser(params, opts...)
+}
diff --git a/internal/blockchain/parser/ethereum/tempo_validator.go b/internal/blockchain/parser/ethereum/tempo_validator.go
new file mode 100644
index 00000000..024f5c94
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/tempo_validator.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewTempoValidator(params internal.ParserParams) internal.TrustlessValidator {
+ // Reuse the same implementation as Ethereum.
+ return NewEthereumValidator(params)
+}
diff --git a/internal/blockchain/parser/ethereum/tron_native.go b/internal/blockchain/parser/ethereum/tron_native.go
new file mode 100644
index 00000000..4261ba75
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/tron_native.go
@@ -0,0 +1,385 @@
+package ethereum
+
+import (
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "strconv"
+ "strings"
+
+ "golang.org/x/xerrors"
+
+ "github.com/mr-tron/base58"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/ethereum/types"
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// contractTypeMap maps contract type strings to their corresponding enum values
+var TronContractTypeMap = map[string]uint64{
+ "AccountCreateContract": 0,
+ "TransferContract": 1,
+ "TransferAssetContract": 2,
+ "VoteAssetContract": 3,
+ "VoteWitnessContract": 4,
+ "WitnessCreateContract": 5,
+ "AssetIssueContract": 6,
+ "WitnessUpdateContract": 8,
+ "ParticipateAssetIssueContract": 9,
+ "AccountUpdateContract": 10,
+ "FreezeBalanceContract": 11,
+ "UnfreezeBalanceContract": 12,
+ "WithdrawBalanceContract": 13,
+ "UnfreezeAssetContract": 14,
+ "UpdateAssetContract": 15,
+ "ProposalCreateContract": 16,
+ "ProposalApproveContract": 17,
+ "ProposalDeleteContract": 18,
+ "SetAccountIdContract": 19,
+ "CustomContract": 20,
+ "CreateSmartContract": 30,
+ "TriggerSmartContract": 31,
+ "GetContract": 32,
+ "UpdateSettingContract": 33,
+ "ExchangeCreateContract": 41,
+ "ExchangeInjectContract": 42,
+ "ExchangeWithdrawContract": 43,
+ "ExchangeTransactionContract": 44,
+ "UpdateEnergyLimitContract": 45,
+ "AccountPermissionUpdateContract": 46,
+ "ClearABIContract": 48,
+ "UpdateBrokerageContract": 49,
+ "ShieldedTransferContract": 51,
+ "MarketSellAssetContract": 52,
+ "MarketCancelOrderContract": 53,
+ "FreezeBalanceV2Contract": 54,
+ "UnfreezeBalanceV2Contract": 55,
+ "WithdrawExpireUnfreezeContract": 56,
+ "DelegateResourceContract": 57,
+ "UnDelegateResourceContract": 58,
+ "CancelAllUnfreezeV2Contract": 59,
+}
+
+var TronTraceCallTypeMap = map[string]bool{
+ "CALL": true,
+ "CREATE": true,
+ "CREATE2": true,
+ "STATICCALL": true,
+ "CALLCODE": true,
+ "DELEGATECALL": true,
+}
+
+const TronContractTypeUnknown = 999
+
+type TronCallValueInfo struct {
+ CallValue int64 `json:"callValue"`
+ TokenId string `json:"tokenId"`
+}
+
+type TronTransactionInfo struct {
+ InternalTransactions []TronInternalTransaction `json:"internal_transactions"`
+ Id string `json:"id"`
+ BlockNumber uint64 `json:"blockNumber"`
+ TransactionHash string `json:"transactionHash"`
+ Fee uint64 `json:"fee"`
+ Receipt TronReceipt `json:"receipt"`
+ Type string `json:"type"`
+}
+
+type TronReceipt struct {
+ Result string `json:"result"`
+ // Bandwidth is represented as either net_fee or net_usage, only one will exist in the response
+ NetFee uint64 `json:"net_fee"`
+ NetUsage uint64 `json:"net_usage"`
+ EnergyUsage uint64 `json:"energy_usage"`
+ EnergyFee uint64 `json:"energy_fee"`
+ OriginEnergyUsage uint64 `json:"origin_energy_usage"`
+ EnergyUsageTotal uint64 `json:"energy_usage_total"`
+ EnergyPenaltyTotal uint64 `json:"energy_penalty_total"`
+}
+
+type TronInternalTransaction struct {
+ Hash string `json:"hash"`
+ CallerAddress string `json:"caller_address"`
+ TransferToAddress string `json:"transferTo_address"`
+ CallValueInfo []TronCallValueInfo `json:"callValueInfo"`
+ Note string `json:"note"`
+ Rejected bool `json:"rejected"`
+}
+
+func NewTronNativeParser(params internal.ParserParams, opts ...internal.ParserFactoryOption) (internal.NativeParser, error) {
+ // Tron shares the same data schema as Ethereum since its an EVM chain except skip trace data
+ opts = append(opts, WithEthereumNodeType(types.EthereumNodeType_ARCHIVAL), WithTraceType(types.TraceType_PARITY))
+ return NewEthereumNativeParser(params, opts...)
+}
+
+func convertInternalTransactionToTrace(itx *TronInternalTransaction) *api.EthereumTransactionFlattenedTrace {
+ // only keep native values, ignore TRC10 token values
+ var nativeTokenValue int64
+ for _, callValueInfoItem := range itx.CallValueInfo {
+ if callValueInfoItem.TokenId == "" {
+ // If TokenId is empty, it means this is a native token transfer
+ nativeTokenValue += callValueInfoItem.CallValue
+ }
+ }
+ var note string
+ noteBytes, err := hex.DecodeString(itx.Note)
+ if err != nil {
+ note = ""
+ } else {
+ note = string(noteBytes)
+ }
+ rawType := strings.ToUpper(note)
+ trace := &api.EthereumTransactionFlattenedTrace{
+ Type: rawType,
+ TraceType: rawType,
+ From: hexToTronAddress(itx.CallerAddress),
+ To: hexToTronAddress(itx.TransferToAddress),
+ Value: strconv.FormatInt(nativeTokenValue, 10),
+ TraceId: itx.Hash,
+ CallValueInfo: convertTronCallValueInfo(itx.CallValueInfo),
+ }
+
+ if TronTraceCallTypeMap[rawType] {
+ trace.CallType = rawType
+ trace.TraceType = "CALL"
+ }
+ if itx.Rejected {
+ trace.Error = "Internal transaction is executed failed"
+ trace.Status = 0
+ } else {
+ trace.Status = 1
+ }
+
+ return trace
+}
+
+func convertTronCallValueInfo(callValueInfo []TronCallValueInfo) []*api.CallValueInfo {
+ result := make([]*api.CallValueInfo, len(callValueInfo))
+ for i, info := range callValueInfo {
+ result[i] = &api.CallValueInfo{
+ TokenId: info.TokenId,
+ CallValue: info.CallValue,
+ }
+ }
+ return result
+}
+
+func parseTronTxInfo(
+ blobData *api.EthereumBlobdata,
+ header *api.EthereumHeader,
+ transactionToFlattenedTracesMap map[string][]*api.EthereumTransactionFlattenedTrace,
+ txReceipts []*api.EthereumTransactionReceipt,
+ transactions []*api.EthereumTransaction,
+) error {
+ if len(blobData.TransactionTraces) == 0 {
+ return nil
+ }
+
+ // Ensure we have matching number of receipts and traces
+ if len(blobData.TransactionTraces) != len(txReceipts) {
+ return xerrors.Errorf(
+ "mismatch between number of transaction traces (%d) and receipts (%d)",
+ len(blobData.TransactionTraces),
+ len(txReceipts),
+ )
+ }
+
+ for txIndex, rawTxInfo := range blobData.TransactionTraces {
+ var txInfo TronTransactionInfo
+ if err := json.Unmarshal(rawTxInfo, &txInfo); err != nil {
+ return xerrors.Errorf("failed to parse transaction trace at index %d: %w", txIndex, err)
+ }
+
+ traceTransactionHash := txInfo.Id
+ txIdx := uint64(txIndex)
+ fee := txInfo.Fee
+ receipt := txInfo.Receipt
+ // 1. enreach txReceipt with fee and net_fee (Bandwidth)fields from transactionInfo.receipt
+ txReceipt := txReceipts[txIndex]
+ if fee != 0 {
+ txReceipt.OptionalFee = &api.EthereumTransactionReceipt_Fee{
+ Fee: uint64(fee),
+ }
+ }
+ if receipt.NetFee != 0 {
+ txReceipt.OptionalNetFee = &api.EthereumTransactionReceipt_NetFee{
+ NetFee: uint64(receipt.NetFee),
+ }
+ }
+ if receipt.NetUsage != 0 {
+ txReceipt.OptionalNetUsage = &api.EthereumTransactionReceipt_NetUsage{
+ NetUsage: uint64(receipt.NetUsage),
+ }
+ }
+ if receipt.EnergyUsage != 0 {
+ txReceipt.OptionalEnergyUsage = &api.EthereumTransactionReceipt_EnergyUsage{
+ EnergyUsage: uint64(receipt.EnergyUsage),
+ }
+ }
+ if receipt.EnergyFee != 0 {
+ txReceipt.OptionalEnergyFee = &api.EthereumTransactionReceipt_EnergyFee{
+ EnergyFee: uint64(receipt.EnergyFee),
+ }
+ }
+ if receipt.OriginEnergyUsage != 0 {
+ txReceipt.OptionalOriginEnergyUsage = &api.EthereumTransactionReceipt_OriginEnergyUsage{
+ OriginEnergyUsage: uint64(receipt.OriginEnergyUsage),
+ }
+ }
+ if receipt.EnergyUsageTotal != 0 {
+ txReceipt.OptionalEnergyUsageTotal = &api.EthereumTransactionReceipt_EnergyUsageTotal{
+ EnergyUsageTotal: uint64(receipt.EnergyUsageTotal),
+ }
+ }
+ if receipt.EnergyPenaltyTotal != 0 {
+ txReceipt.OptionalEnergyPenaltyTotal = &api.EthereumTransactionReceipt_EnergyPenaltyTotal{
+ EnergyPenaltyTotal: uint64(receipt.EnergyPenaltyTotal),
+ }
+ }
+
+ // 2. mapping internalTransactions to trace
+ internalTxs := txInfo.InternalTransactions
+ traces := make([]*api.EthereumTransactionFlattenedTrace, len(internalTxs))
+ for i, internalTx := range internalTxs {
+ trace := convertInternalTransactionToTrace(&internalTx)
+ trace.BlockHash = toTronHash(header.Hash)
+ trace.BlockNumber = header.Number
+ trace.TransactionHash = traceTransactionHash
+ trace.TransactionIndex = txIdx
+ traces[i] = trace
+ }
+ transactionToFlattenedTracesMap[traceTransactionHash] = traces
+
+ // add type to transaction
+ tx := transactions[txIndex]
+ if typeValue, exists := TronContractTypeMap[txInfo.Type]; exists {
+ tx.Type = typeValue
+ } else {
+ // If type is not found in map, set to 999 as default or log warning
+ tx.Type = TronContractTypeUnknown
+ }
+ }
+ return nil
+}
+
+func toTronHash(hexHash string) string {
+ // if hexHash == "" {
+ // return ""
+ // }
+ // // Normalize the hash by ensuring it's lowercase and removing 0x prefix
+ // hexHash = strings.ToLower(hexHash)
+ return strings.Replace(hexHash, "0x", "", -1)
+}
+
+func hexToTronAddress(hexAddress string) string {
+ if hexAddress == "" {
+ return ""
+ }
+
+ // Ensure consistent format by cleaning the hex address
+ hexAddress = strings.ToLower(hexAddress)
+ if strings.HasPrefix(hexAddress, "0x") {
+ hexAddress = "41" + hexAddress[2:]
+ } else if !strings.HasPrefix(hexAddress, "41") {
+ hexAddress = "41" + hexAddress
+ }
+
+ // Decode hex string to bytes
+ rawBytes, err := hex.DecodeString(hexAddress)
+ if err != nil {
+ // If unable to decode, return the original address to avoid data loss
+ return hexAddress
+ }
+
+ // Compute double SHA-256 checksum
+ hash1 := sha256.Sum256(rawBytes)
+ hash2 := sha256.Sum256(hash1[:])
+ checksum := hash2[:4] // First 4 bytes as checksum
+ // Append checksum to the raw bytes
+ fullBytes := append(rawBytes, checksum...)
+ // Base58Check encode
+ tronAddress := base58.Encode(fullBytes)
+
+ return tronAddress
+}
+
+func convertTokenTransfer(data *api.EthereumTokenTransfer) {
+ data.TokenAddress = hexToTronAddress(data.TokenAddress)
+ data.FromAddress = hexToTronAddress(data.FromAddress)
+ data.ToAddress = hexToTronAddress(data.ToAddress)
+
+ data.TransactionHash = toTronHash(data.TransactionHash)
+ data.BlockHash = toTronHash(data.BlockHash)
+
+ switch v := data.TokenTransfer.(type) {
+ case *api.EthereumTokenTransfer_Erc20:
+ if v.Erc20 != nil {
+ v.Erc20.FromAddress = hexToTronAddress(v.Erc20.FromAddress)
+ v.Erc20.ToAddress = hexToTronAddress(v.Erc20.ToAddress)
+ }
+ case *api.EthereumTokenTransfer_Erc721:
+ if v.Erc721 != nil {
+ v.Erc721.FromAddress = hexToTronAddress(v.Erc721.FromAddress)
+ v.Erc721.ToAddress = hexToTronAddress(v.Erc721.ToAddress)
+ }
+ }
+}
+
+func postProcessTronBlock(
+ blobData *api.EthereumBlobdata,
+ header *api.EthereumHeader,
+ transactions []*api.EthereumTransaction,
+ txReceipts []*api.EthereumTransactionReceipt,
+ tokenTransfers [][]*api.EthereumTokenTransfer,
+ transactionToFlattenedTracesMap map[string][]*api.EthereumTransactionFlattenedTrace,
+) error {
+ if err := parseTronTxInfo(blobData, header, transactionToFlattenedTracesMap, txReceipts, transactions); err != nil {
+ return xerrors.Errorf("failed to parse transaction parity traces: %w", err)
+ }
+
+ header.Hash = toTronHash(header.Hash)
+ header.ParentHash = toTronHash(header.ParentHash)
+ header.TransactionsRoot = toTronHash(header.TransactionsRoot)
+ header.Miner = hexToTronAddress(header.Miner)
+
+ for i := range header.Transactions {
+ header.Transactions[i] = toTronHash(header.Transactions[i])
+ }
+
+ for _, tx := range transactions {
+ tx.BlockHash = toTronHash(tx.BlockHash)
+ tx.Hash = toTronHash(tx.Hash)
+ if tx.From != "" {
+ tx.From = hexToTronAddress(tx.From)
+ }
+ if tx.To != "" {
+ tx.To = hexToTronAddress(tx.To)
+ }
+ }
+
+ for _, txR := range txReceipts {
+ txR.TransactionHash = toTronHash(txR.TransactionHash)
+ txR.BlockHash = toTronHash(txR.BlockHash)
+ if txR.From != "" {
+ txR.From = hexToTronAddress(txR.From)
+ }
+ if txR.To != "" {
+ txR.To = hexToTronAddress(txR.To)
+ }
+ if txR.Logs != nil {
+ for _, txLog := range txR.Logs {
+ txLog.TransactionHash = toTronHash(txLog.TransactionHash)
+ txLog.BlockHash = toTronHash(txLog.BlockHash)
+ txLog.Address = hexToTronAddress(txLog.Address)
+ }
+ }
+ }
+ for _, txTokenTransfers := range tokenTransfers {
+ for _, tokenTransfer := range txTokenTransfers {
+ convertTokenTransfer(tokenTransfer)
+ }
+ }
+ return nil
+}
diff --git a/internal/blockchain/parser/ethereum/tron_native_test.go b/internal/blockchain/parser/ethereum/tron_native_test.go
new file mode 100644
index 00000000..d914e1e8
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/tron_native_test.go
@@ -0,0 +1,590 @@
+package ethereum
+
+import (
+ "context"
+ "encoding/json"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/fx"
+ "go.uber.org/mock/gomock"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+ "github.com/coinbase/chainstorage/internal/utils/fixtures"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type tronParserTestSuite struct {
+ suite.Suite
+
+ ctrl *gomock.Controller
+ testapp testapp.TestApp
+ parser internal.Parser
+}
+
+func TestTronParserTestSuite(t *testing.T) {
+ // t.Skip()
+ suite.Run(t, new(tronParserTestSuite))
+}
+
+func (s *tronParserTestSuite) SetupTest() {
+ s.ctrl = gomock.NewController(s.T())
+
+ var parser internal.Parser
+ s.testapp = testapp.New(
+ s.T(),
+ Module,
+ internal.Module,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_TRON, common.Network_NETWORK_TRON_MAINNET),
+ fx.Populate(&parser),
+ )
+
+ s.parser = parser
+ s.NotNil(s.parser)
+}
+
+func (s *tronParserTestSuite) TearDownTest() {
+ s.testapp.Close()
+ s.ctrl.Finish()
+}
+
+func (s *tronParserTestSuite) TestParseTronBlock() {
+ require := testutil.Require(s.T())
+
+ fixtureHeader := fixtures.MustReadFile("parser/tron/raw_block_header.json")
+
+ rawReceipts, err := s.fixtureParsingHelper("parser/tron/raw_block_tx_receipt.json")
+ require.NoError(err)
+
+ rawTraces, err := s.fixtureParsingHelper("parser/tron/raw_block_trace_tx_info.json")
+ require.NoError(err)
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_TRON,
+ Network: common.Network_NETWORK_TRON_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 2,
+ Hash: "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ ParentHash: "0x0000000004034f5b43c5934257b3d1f1a313bba4af0a4dd2f778fda9e641b615",
+ Height: 0x4034f5c,
+ },
+ Blobdata: &api.Block_Ethereum{
+ Ethereum: &api.EthereumBlobdata{
+ Header: fixtureHeader,
+ TransactionReceipts: rawReceipts,
+ TransactionTraces: rawTraces,
+ },
+ },
+ }
+
+ expectedHeader := &api.EthereumHeader{
+ Hash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ ParentHash: "0000000004034f5b43c5934257b3d1f1a313bba4af0a4dd2f778fda9e641b615",
+ Number: 0x4034F5C,
+ Timestamp: ×tamppb.Timestamp{Seconds: 1732627338},
+ Transactions: []string{
+ "d581afa9158fbed69fb10d6a2245ad45d912a3da03ff24d59f3d2f6df6fd9529",
+ "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ },
+ Nonce: "0x0000000000000000",
+ Sha3Uncles: "0x0000000000000000000000000000000000000000000000000000000000000000",
+ LogsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ TransactionsRoot: "d270690faa58558c2b03ae600334f71f9d5a0ad42d7313852fb3742e8576eec9",
+ StateRoot: "0x",
+ ReceiptsRoot: "0x0000000000000000000000000000000000000000000000000000000000000000",
+ Miner: "TNeEwWHXLLUgEtfzTnYN8wtVenGxuMzZCE",
+ TotalDifficulty: "0",
+ ExtraData: "0x",
+ Size: 0x1a366,
+ GasLimit: 0x2b3b43dc6,
+ GasUsed: 0xb1006d,
+ MixHash: "0x0000000000000000000000000000000000000000000000000000000000000000",
+ OptionalBaseFeePerGas: &api.EthereumHeader_BaseFeePerGas{
+ BaseFeePerGas: uint64(0),
+ },
+ }
+ expectedFlattenedTraces := []*api.EthereumTransactionFlattenedTrace{
+ {
+ Type: "CALL",
+ From: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ To: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ Value: "200",
+ TraceType: "CALL",
+ CallType: "CALL",
+ TraceId: "499bdbdfaae021dd510c70b433bc48d88d8ca6e0b7aee13ce6d726114e365aaf",
+ Status: 1,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 0x4034F5C,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 1,
+ CallValueInfo: []*api.CallValueInfo{
+ {
+ CallValue: 100,
+ },
+ {
+ CallValue: 100,
+ },
+ },
+ },
+ {
+ Type: "CALL",
+ From: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ To: "TXA2WjFc5f86deJcZZCdbdpkpUTKTA3VDM",
+ Value: "1000",
+ TraceType: "CALL",
+ CallType: "CALL",
+ TraceId: "997225b56440a9bd172f05f44a663830b72093a12502551cda99b0bc7c60cbc1",
+ Status: 1,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 0x4034F5C,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 1,
+ CallValueInfo: []*api.CallValueInfo{
+ {
+ TokenId: "1004777",
+ CallValue: 1000000000000000,
+ },
+ {
+ CallValue: 1000,
+ },
+ },
+ },
+ {
+ Type: "CALL",
+ From: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ To: "TXA2WjFc5f86deJcZZCdbdpkpUTKTA3VDM",
+ Value: "0",
+ TraceType: "CALL",
+ CallType: "CALL",
+ TraceId: "7ac8dd16dede5c512330f5033c8fd6f5390d742aa51b805f805098109eb54fe9",
+ Status: 1,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 0x4034F5C,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 1,
+ CallValueInfo: []*api.CallValueInfo{
+ {
+ TokenId: "1004777",
+ CallValue: 1000,
+ },
+ {
+ TokenId: "1004777",
+ CallValue: 100,
+ },
+ },
+ },
+ {
+ Type: "CALL",
+ From: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ To: "TU3kjFuhtEo42tsCBtfYUAZxoqQ4yuSLQ5",
+ Value: "100000",
+ TraceType: "CALL",
+ CallType: "CALL",
+ TraceId: "cf6f699d9bdae8aa25fae310a06bb60a29a7812548cf3c1d83c737fd1a22c0ee",
+ Status: 1,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 0x4034F5C,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 1,
+ CallValueInfo: []*api.CallValueInfo{
+ {
+ TokenId: "1004777",
+ CallValue: 100,
+ },
+ {
+ CallValue: 100000,
+ },
+ },
+ },
+ {
+ Type: "CALL",
+ From: "TU3kjFuhtEo42tsCBtfYUAZxoqQ4yuSLQ5",
+ To: "TU3kjFuhtEo42tsCBtfYUAZxoqQ4yuSLQ5",
+ Value: "0",
+ TraceType: "CALL",
+ CallType: "CALL",
+ TraceId: "95787b9a6558c7b6b624d0c1bece9723a7f4c3d414010b6ac105ae5f5aebffbc",
+ Status: 1,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 0x4034F5C,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 1,
+ CallValueInfo: []*api.CallValueInfo{
+ {},
+ },
+ },
+ {
+ Type: "UNDELEGATERESOURCEOFENERGY",
+ From: "TU3kjFuhtEo42tsCBtfYUAZxoqQ4yuSLQ5",
+ To: "TGzjkw66CtL49eKiQFDwJDuXG9HSQd69p2",
+ Value: "822996311610",
+ TraceType: "UNDELEGATERESOURCEOFENERGY",
+ CallType: "",
+ TraceId: "14526162e31d969ef0dca9b902d51ecc0ffab87dc936dce62022f368119043af",
+ Status: 1,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 0x4034F5C,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 1,
+ CallValueInfo: []*api.CallValueInfo{
+ {
+ CallValue: 822994311610,
+ },
+ {
+ CallValue: 2000000,
+ },
+ },
+ },
+ {
+ Type: "CALL",
+ From: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ To: "TXA2WjFc5f86deJcZZCdbdpkpUTKTA3VDM",
+ Value: "0",
+ TraceType: "CALL",
+ CallType: "CALL",
+ TraceId: "8e088220a26ca8d794786e78096e71259cf8744cccdc4f07a8129aa8ee29bb98",
+ Status: 1,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 0x4034F5C,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 1,
+ CallValueInfo: []*api.CallValueInfo{
+ {},
+ },
+ },
+ {
+ Type: "CALL",
+ From: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ To: "TNXC2YCSxhdxsVqhqu3gYZYme6n4i6T1C1",
+ Value: "1424255258",
+ TraceType: "CALL",
+ CallType: "CALL",
+ TraceId: "83b1d41ba953aab4da6e474147f647599ea53bb3213306897127b57e85ddd1ca",
+ Status: 1,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 0x4034F5C,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 1,
+ CallValueInfo: []*api.CallValueInfo{
+ {
+ CallValue: 1424255258,
+ },
+ },
+ },
+ }
+
+ expectedTransactions := []*api.EthereumTransaction{
+ {
+ Hash: "d581afa9158fbed69fb10d6a2245ad45d912a3da03ff24d59f3d2f6df6fd9529",
+ From: "TDQFomPihdhP8Jzr2LMpdcXgg9qxKfZZmD",
+ To: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
+ Index: 0,
+ Type: 1,
+ Receipt: &api.EthereumTransactionReceipt{
+ TransactionHash: "d581afa9158fbed69fb10d6a2245ad45d912a3da03ff24d59f3d2f6df6fd9529",
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 67325788,
+ From: "TDQFomPihdhP8Jzr2LMpdcXgg9qxKfZZmD",
+ To: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
+ CumulativeGasUsed: 130285,
+ GasUsed: 130285,
+ LogsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ EffectiveGasPrice: 210,
+ OptionalStatus: &api.EthereumTransactionReceipt_Status{Status: 1},
+ TransactionIndex: 0,
+ OptionalNetUsage: &api.EthereumTransactionReceipt_NetUsage{
+ NetUsage: 345,
+ },
+ OptionalEnergyUsage: &api.EthereumTransactionReceipt_EnergyUsage{
+ EnergyUsage: 130285,
+ },
+ OptionalEnergyUsageTotal: &api.EthereumTransactionReceipt_EnergyUsageTotal{
+ EnergyUsageTotal: 130285,
+ },
+ OptionalEnergyPenaltyTotal: &api.EthereumTransactionReceipt_EnergyPenaltyTotal{
+ EnergyPenaltyTotal: 100635,
+ },
+ Logs: []*api.EthereumEventLog{
+ {
+ TransactionHash: "d581afa9158fbed69fb10d6a2245ad45d912a3da03ff24d59f3d2f6df6fd9529",
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 67325788,
+ Address: "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t",
+ Data: "0x0000000000000000000000000000000000000000000000000000000000027165",
+ TransactionIndex: 0,
+ LogIndex: 0,
+ Topics: []string{
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x00000000000000000000000025a51e3e65287539b8d4eb559cbca4488a08bb00",
+ "0x0000000000000000000000009dc5da2b3c502661c8448ba88bacf7f0b22272ad",
+ },
+ },
+ },
+ },
+ },
+ {
+ Hash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ From: "TNXC2YCSxhdxsVqhqu3gYZYme6n4i6T1C1",
+ To: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ Index: 69,
+ Type: 2,
+ Receipt: &api.EthereumTransactionReceipt{
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 67325788,
+ From: "TNXC2YCSxhdxsVqhqu3gYZYme6n4i6T1C1",
+ To: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ CumulativeGasUsed: 1432695,
+ GasUsed: 74135,
+ LogsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ EffectiveGasPrice: 210,
+ OptionalStatus: &api.EthereumTransactionReceipt_Status{Status: 1},
+ TransactionIndex: 69,
+ OptionalFee: &api.EthereumTransactionReceipt_Fee{
+ Fee: 379,
+ },
+ OptionalNetFee: &api.EthereumTransactionReceipt_NetFee{
+ NetFee: 379,
+ },
+ OptionalEnergyUsage: &api.EthereumTransactionReceipt_EnergyUsage{
+ EnergyUsage: 68976,
+ },
+ OptionalOriginEnergyUsage: &api.EthereumTransactionReceipt_OriginEnergyUsage{
+ OriginEnergyUsage: 5159,
+ },
+ OptionalEnergyUsageTotal: &api.EthereumTransactionReceipt_EnergyUsageTotal{
+ EnergyUsageTotal: 74135,
+ },
+ Logs: []*api.EthereumEventLog{
+ {
+ LogIndex: 16,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 69,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 67325788,
+ Address: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ Data: "0x00000000000000000000000000000000000000000000000000000001f9873bc7000000000000000000000000000000000000000000000000093732ae413feb69000000000000000000000000000000000000000000000000093732b42dd59ebe0000000000000000000000000000000000000000000000000000801f33d9f651000000000000000000000000000000000000000000000000000000000036b158",
+ Topics: []string{
+ "0xda6e3523d5765dedff9534b488c7e508318178571c144293451989755e9379e7",
+ "0x0000000000000000000000000000000000000000000000000000000000000001",
+ },
+ },
+ {
+ LogIndex: 17,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 69,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 67325788,
+ Address: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ Data: "0x000000000000000000000000000000000000000000000000093732a856669e8f000000000000000000000000000000000000000000000000093732b42dd59ebe000000000000000000000000000000000000000000000000000000bf9e4899ba000000000000000000000000000000000000000000000000000000000000a3810000000000000000000000000000000000000000000000000000000000000000",
+ Topics: []string{
+ "0x74fed619850adf4ba83cfb92b9566b424e3de6de4d9a7adc3b1909ea58421a55",
+ "0x00000000000000000000000089ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "0x0000000000000000000000004d12f87c18a914dddbc2b27f378ad126a79b76b6",
+ "0x0000000000000000000000000000000000000000000000000000000000000001",
+ },
+ },
+ {
+ LogIndex: 18,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 69,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 67325788,
+ // Address: "0xc60a6f5c81431c97ed01b61698b6853557f3afd4",
+ Address: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ Data: "0x000000000000000000000000000000000000000000000000000000bf9e4899ba",
+ Topics: []string{
+ "0xf2def54ec5eba61fd8f18d019c7beaf6a47df317fb798b3263ad69ec227c9261",
+ "0x00000000000000000000000089ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "0x0000000000000000000000004d12f87c18a914dddbc2b27f378ad126a79b76b6",
+ "0x0000000000000000000000000000000000000000000000000000000000000001",
+ },
+ },
+ {
+ LogIndex: 19,
+ TransactionHash: "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ TransactionIndex: 69,
+ BlockHash: "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ BlockNumber: 67325788,
+ Address: "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd",
+ Data: "0x000000000000000000000000000000000000000000000000000000bf9e4899ba0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000c032ffd0000000000000000000000000000000000000000000000000000000054e4691a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000093732b42dd59ebe",
+ Topics: []string{
+ "0xf7e21d5bf17851f93ab7bda7e390841620f59dfbe9d86add32824f33bd40d3f5",
+ "0x00000000000000000000000089ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "0x0000000000000000000000004d12f87c18a914dddbc2b27f378ad126a79b76b6",
+ },
+ },
+ },
+ },
+ },
+ }
+
+ nativeBlock, err := s.parser.ParseNativeBlock(context.Background(), block)
+ require.NoError(err)
+ require.Equal(common.Blockchain_BLOCKCHAIN_TRON, nativeBlock.Blockchain)
+ require.Equal(common.Network_NETWORK_TRON_MAINNET, nativeBlock.Network)
+ actualBlock := nativeBlock.GetEthereum()
+ require.NotNil(actualBlock)
+ require.Equal(expectedHeader, actualBlock.Header)
+
+ require.Equal(2, len(actualBlock.Transactions))
+
+ require.Equal(8, len(actualBlock.Transactions[1].FlattenedTraces))
+
+ tx := actualBlock.Transactions[1]
+ for i, trace := range tx.FlattenedTraces {
+ trace_i := expectedFlattenedTraces[i]
+ require.Equal(trace_i.Type, trace.Type)
+ require.Equal(trace_i.From, trace.From)
+ require.Equal(trace_i.To, trace.To)
+ require.Equal(trace_i.Value, trace.Value)
+ require.Equal(trace_i.TraceType, trace.TraceType)
+ require.Equal(trace_i.CallType, trace.CallType)
+ require.Equal(trace_i.TraceId, trace.TraceId)
+ require.Equal(trace_i.Status, trace.Status)
+ require.Equal(trace_i.BlockHash, trace.BlockHash)
+ require.Equal(trace_i.BlockNumber, trace.BlockNumber)
+ require.Equal(trace_i.TransactionHash, trace.TransactionHash)
+ require.Equal(trace_i.TransactionIndex, trace.TransactionIndex)
+ }
+ require.Equal(tx.FlattenedTraces, expectedFlattenedTraces)
+
+ for i, tx := range actualBlock.Transactions {
+ expected_tx := expectedTransactions[i]
+ require.Equal(expected_tx.Hash, tx.Hash)
+ require.Equal(expected_tx.From, tx.From)
+ require.Equal(expected_tx.To, tx.To)
+ require.Equal(expected_tx.Index, tx.Index)
+ require.Equal(expected_tx.Type, tx.Type)
+ require.Equal(expected_tx.Receipt.From, tx.Receipt.From)
+ require.Equal(expected_tx.Receipt.To, tx.Receipt.To)
+ require.Equal(expected_tx.Receipt.TransactionHash, tx.Receipt.TransactionHash)
+ require.Equal(expected_tx.Receipt.TransactionIndex, tx.Receipt.TransactionIndex)
+ require.Equal(expected_tx.Receipt.BlockHash, tx.Receipt.BlockHash)
+ require.Equal(expected_tx.Receipt.BlockNumber, tx.Receipt.BlockNumber)
+ require.Equal(expected_tx.Receipt.CumulativeGasUsed, tx.Receipt.CumulativeGasUsed)
+ require.Equal(expected_tx.Receipt.GasUsed, tx.Receipt.GasUsed)
+ require.Equal(expected_tx.Receipt.LogsBloom, tx.Receipt.LogsBloom)
+ require.Equal(expected_tx.Receipt.EffectiveGasPrice, tx.Receipt.EffectiveGasPrice)
+ require.Equal(expected_tx.Receipt.Logs, tx.Receipt.Logs)
+
+ if expected_tx.Receipt.GetOptionalFee() != nil {
+ require.NotNil(tx.Receipt.GetOptionalFee())
+ require.Equal(expected_tx.Receipt.GetFee(), tx.Receipt.GetFee())
+ } else {
+ require.Nil(tx.Receipt.GetOptionalFee())
+ }
+ if expected_tx.Receipt.GetOptionalNetFee() != nil {
+ require.NotNil(tx.Receipt.GetOptionalNetFee())
+ require.Equal(expected_tx.Receipt.GetNetFee(), tx.Receipt.GetNetFee())
+ } else {
+ require.Nil(tx.Receipt.GetOptionalNetFee())
+ }
+ if expected_tx.Receipt.GetOptionalNetUsage() != nil {
+ require.NotNil(tx.Receipt.GetOptionalNetUsage())
+ require.Equal(expected_tx.Receipt.GetNetUsage(), tx.Receipt.GetNetUsage())
+ } else {
+ require.Nil(tx.Receipt.GetOptionalNetUsage())
+ }
+ if expected_tx.Receipt.GetOptionalEnergyUsage() != nil {
+ require.NotNil(tx.Receipt.GetOptionalEnergyUsage())
+ require.Equal(expected_tx.Receipt.GetEnergyUsage(), tx.Receipt.GetEnergyUsage())
+ } else {
+ require.Nil(tx.Receipt.GetOptionalEnergyUsage())
+ }
+ if expected_tx.Receipt.GetOptionalEnergyUsageTotal() != nil {
+ require.NotNil(tx.Receipt.GetOptionalEnergyUsageTotal())
+ require.Equal(expected_tx.Receipt.GetEnergyUsageTotal(), tx.Receipt.GetEnergyUsageTotal())
+ } else {
+ require.Nil(tx.Receipt.GetOptionalEnergyUsageTotal())
+ }
+ if expected_tx.Receipt.GetOptionalEnergyPenaltyTotal() != nil {
+ require.NotNil(tx.Receipt.GetOptionalEnergyPenaltyTotal())
+ require.Equal(expected_tx.Receipt.GetEnergyPenaltyTotal(), tx.Receipt.GetEnergyPenaltyTotal())
+ } else {
+ require.Nil(tx.Receipt.GetOptionalEnergyPenaltyTotal())
+ }
+ if expected_tx.Receipt.GetOptionalOriginEnergyUsage() != nil {
+ require.NotNil(tx.Receipt.GetOptionalOriginEnergyUsage())
+ require.Equal(expected_tx.Receipt.GetOriginEnergyUsage(), tx.Receipt.GetOriginEnergyUsage())
+ } else {
+ require.Nil(tx.Receipt.GetOptionalOriginEnergyUsage())
+ }
+ if expected_tx.Receipt.GetOptionalNetUsage() != nil {
+ require.NotNil(tx.Receipt.GetOptionalNetUsage())
+ require.Equal(expected_tx.Receipt.GetNetUsage(), tx.Receipt.GetNetUsage())
+ } else {
+ require.Nil(tx.Receipt.GetOptionalNetUsage())
+ }
+ }
+}
+
+func (s *tronParserTestSuite) fixtureParsingHelper(filePath string) ([][]byte, error) {
+
+ fixtureParityTrace, _ := fixtures.ReadFile(filePath)
+
+ var tmpItems []json.RawMessage
+ err := json.Unmarshal(fixtureParityTrace, &tmpItems)
+
+ items := make([][]byte, len(tmpItems))
+ for i, item := range tmpItems {
+ items[i] = item
+ }
+ return items, err
+}
+
+func (s *tronParserTestSuite) TestToTronHash() {
+ require := testutil.Require(s.T())
+
+ testCases := []struct {
+ input string
+ expected string
+ comment string
+ }{
+ {"0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87", "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87", "with 0x prefix"},
+ {"0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87", "0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87", "without 0x prefix"},
+ {"0xABCDEF1234567890", "ABCDEF1234567890", "uppercase hex"},
+ {"", "", "empth string"},
+ {"0x", "", "only 0x prefix"},
+ }
+
+ for _, tc := range testCases {
+ result := toTronHash(tc.input)
+ require.Equal(tc.expected, result, tc.comment)
+ }
+}
+
+func (s *tronParserTestSuite) TestHexToTronAddress() {
+ require := testutil.Require(s.T())
+ testCases := []struct {
+ input string
+ expected string
+ comment string
+ }{
+ {"0x8b0359acac03bac62cbf89c4b787cb10b3c3f513", "TNeEwWHXLLUgEtfzTnYN8wtVenGxuMzZCE", "with 0x prefix"},
+ {"0xc60a6f5c81431c97ed01b61698b6853557f3afd4", "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd", "with 0x prefix"},
+ {"0x4d12f87c18a914dddbc2b27f378ad126a79b76b6", "TGzjkw66CtL49eKiQFDwJDuXG9HSQd69p2", "with 0x prefix"},
+ {"0xe8667633c747066c70672c58207cc745a9860527", "TXA2WjFc5f86deJcZZCdbdpkpUTKTA3VDM", "with 0x prefix"},
+ {"0x89ae01b878dffc8088222adf1fb08ebadfeea53a", "TNXC2YCSxhdxsVqhqu3gYZYme6n4i6T1C1", "with 0x prefix"},
+
+ {"418b0359acac03bac62cbf89c4b787cb10b3c3f513", "TNeEwWHXLLUgEtfzTnYN8wtVenGxuMzZCE", "without 0x but have 41 prefix"},
+ {"41c60a6f5c81431c97ed01b61698b6853557f3afd4", "TU2MJ5Veik1LRAgjeSzEdvmDYx7mefJZvd", "without 0x but have 41 prefix"},
+ {"414d12f87c18a914dddbc2b27f378ad126a79b76b6", "TGzjkw66CtL49eKiQFDwJDuXG9HSQd69p2", "without 0x but have 41 prefix"},
+ {"41e8667633c747066c70672c58207cc745a9860527", "TXA2WjFc5f86deJcZZCdbdpkpUTKTA3VDM", "without 0x but have 41 prefix"},
+ {"4189ae01b878dffc8088222adf1fb08ebadfeea53a", "TNXC2YCSxhdxsVqhqu3gYZYme6n4i6T1C1", "without 0x but have 41 prefix"},
+
+ {"c64e69acde1c7b16c2a3efcdbbdaa96c3644c2b3", "TU3kjFuhtEo42tsCBtfYUAZxoqQ4yuSLQ5", "without 0x and 41 prefix"},
+ {"a614f803b6fd780986a42c78ec9c7f77e6ded13c", "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t", "without 0x and 41 prefix"},
+
+ {"", "", "empty string"},
+ }
+
+ for _, tc := range testCases {
+ result := hexToTronAddress(tc.input)
+ require.Equal(tc.expected, result, tc.comment)
+ }
+}
diff --git a/internal/blockchain/parser/ethereum/tron_validator.go b/internal/blockchain/parser/ethereum/tron_validator.go
new file mode 100644
index 00000000..2ee7ab45
--- /dev/null
+++ b/internal/blockchain/parser/ethereum/tron_validator.go
@@ -0,0 +1,10 @@
+package ethereum
+
+import (
+ "github.com/coinbase/chainstorage/internal/blockchain/parser/internal"
+)
+
+func NewTronValidator(params internal.ParserParams) internal.TrustlessValidator {
+ // Reuse the same implementation as Ethereum.
+ return NewEthereumValidator(params)
+}
diff --git a/internal/blockchain/parser/internal/interceptors.go b/internal/blockchain/parser/internal/interceptors.go
index 8a09a3c2..4e6d4e9d 100644
--- a/internal/blockchain/parser/internal/interceptors.go
+++ b/internal/blockchain/parser/internal/interceptors.go
@@ -8,6 +8,7 @@ import (
"go.uber.org/zap"
"golang.org/x/xerrors"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
"github.com/coinbase/chainstorage/internal/utils/instrument"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
)
@@ -62,11 +63,11 @@ func newInstrumentWithResult[T any](method string, scope tally.Scope, logger *za
)
}
-func (i *instrumentInterceptor) ParseNativeBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+func (i *instrumentInterceptor) ParseNativeBlock(ctx context.Context, rawBlock *api.Block, opts ...ParseOption) (*api.NativeBlock, error) {
return i.instrumentParseNativeBlock.Instrument(
ctx,
func(ctx context.Context) (*api.NativeBlock, error) {
- return i.parser.ParseNativeBlock(ctx, rawBlock)
+ return i.parser.ParseNativeBlock(ctx, rawBlock, opts...)
},
instrument.WithLoggerFields(
zap.Reflect("block", rawBlock.Metadata),
@@ -150,3 +151,11 @@ func (i *instrumentInterceptor) ValidateRosettaBlock(ctx context.Context, req *a
),
)
}
+
+// ParseStreamNative passes through to the wrapped parser. Streaming
+// instrumentation (per-tx counters, per-block duration) would require
+// hooking the iterator; for now the call is passed through unwrapped
+// so consumers see the real stream object.
+func (i *instrumentInterceptor) ParseStreamNative(ctx context.Context, spooled *downloader.SpooledBlock, opts ...ParseOption) (NativeStreamedBlock, error) {
+ return i.parser.ParseStreamNative(ctx, spooled, opts...)
+}
diff --git a/internal/blockchain/parser/internal/options.go b/internal/blockchain/parser/internal/options.go
new file mode 100644
index 00000000..6aa557f7
--- /dev/null
+++ b/internal/blockchain/parser/internal/options.go
@@ -0,0 +1,65 @@
+package internal
+
+// parseOptions is the internal state bag that concrete ParseOption values
+// mutate. It is unexported so only this package can implement the
+// ParseOption interface.
+type parseOptions struct {
+ skipScripts bool
+ skipWitnesses bool
+ skipShielded bool
+}
+
+// ParseOption configures a ParseNativeBlock / ParseBlock call. Callers
+// construct options via the With* helpers; the interface is sealed so
+// only this package can define new options.
+type ParseOption interface {
+ applyParseOption(*parseOptions)
+}
+
+type parseOptionFunc func(*parseOptions)
+
+func (f parseOptionFunc) applyParseOption(o *parseOptions) { f(o) }
+
+// WithSkipScripts asks the parser to omit bitcoin-family script bytes
+// (ScriptSignature.{Assembly,Hex} and ScriptPublicKey.{Assembly,Hex})
+// and the ethereum transaction Input field from its output. Address
+// derivation that requires the script assembly (e.g. P2PK) may fall
+// back to empty output when this option is set.
+func WithSkipScripts() ParseOption {
+ return parseOptionFunc(func(o *parseOptions) { o.skipScripts = true })
+}
+
+// WithSkipWitnesses asks the parser to omit
+// BitcoinTransactionInput.TransactionInputWitnesses from its output.
+func WithSkipWitnesses() ParseOption {
+ return parseOptionFunc(func(o *parseOptions) { o.skipWitnesses = true })
+}
+
+// WithSkipShielded asks the bitcoin-family parser to discard zcash
+// shielded transaction fields (joinsplits, vShieldedSpend,
+// vShieldedOutput, vJoinSplit, saplingBundle, orchardBundle) during
+// JSON decoding so no Go strings are allocated for them.
+func WithSkipShielded() ParseOption {
+ return parseOptionFunc(func(o *parseOptions) { o.skipShielded = true })
+}
+
+// ResolveParseOptions collapses a variadic option list into a concrete
+// view that parser implementations in sibling packages can read.
+func ResolveParseOptions(opts []ParseOption) ParseOptionsView {
+ var p parseOptions
+ for _, opt := range opts {
+ if opt != nil {
+ opt.applyParseOption(&p)
+ }
+ }
+ return ParseOptionsView{p: p}
+}
+
+// ParseOptionsView is the read-only view of a resolved option set.
+type ParseOptionsView struct {
+ p parseOptions
+}
+
+func (v ParseOptionsView) SkipScripts() bool { return v.p.skipScripts }
+func (v ParseOptionsView) SkipWitnesses() bool { return v.p.skipWitnesses }
+func (v ParseOptionsView) SkipShielded() bool { return v.p.skipShielded }
diff --git a/internal/blockchain/parser/internal/parser.go b/internal/blockchain/parser/internal/parser.go
index 945b488c..f0f6ea05 100644
--- a/internal/blockchain/parser/internal/parser.go
+++ b/internal/blockchain/parser/internal/parser.go
@@ -2,10 +2,15 @@ package internal
import (
"context"
+ "io"
+ "iter"
+ "os"
"go.uber.org/fx"
"golang.org/x/xerrors"
+ "google.golang.org/protobuf/proto"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
"github.com/coinbase/chainstorage/internal/utils/fxparams"
"github.com/coinbase/chainstorage/internal/utils/log"
"github.com/coinbase/chainstorage/protos/coinbase/c3/common"
@@ -14,7 +19,7 @@ import (
type (
Parser interface {
- ParseNativeBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error)
+ ParseNativeBlock(ctx context.Context, rawBlock *api.Block, opts ...ParseOption) (*api.NativeBlock, error)
GetNativeTransaction(ctx context.Context, nativeBlock *api.NativeBlock, transactionHash string) (*api.NativeTransaction, error)
ParseRosettaBlock(ctx context.Context, rawBlock *api.Block) (*api.RosettaBlock, error)
CompareNativeBlocks(ctx context.Context, height uint64, expectedBlock, actualBlock *api.NativeBlock) error
@@ -24,10 +29,22 @@ type (
ValidateAccountState(ctx context.Context, req *api.ValidateAccountStateRequest) (*api.ValidateAccountStateResponse, error)
// ValidateRosettaBlock Given other block source (native, etc), validates whether transaction operations show the correct balance transfer.
ValidateRosettaBlock(ctx context.Context, req *api.ValidateRosettaBlockRequest, actualRosettaBlock *api.RosettaBlock) error
+ // ParseStreamNative returns a chain-agnostic NativeStreamedBlock
+ // over the spooled bytes. Dispatches per chain internally:
+ // for bitcoin-family chains the returned block's GetBitcoin()
+ // is populated; for other (or unimplemented) chains all
+ // Get() accessors return nil and callers should fall
+ // back to GetBlock + ParseNativeBlock.
+ //
+ // Ownership of the SpooledBlock transfers to the returned
+ // stream — calling stream.Close() also closes the spool. On
+ // error, the caller retains ownership and must close the
+ // spool itself.
+ ParseStreamNative(ctx context.Context, spooled *downloader.SpooledBlock, opts ...ParseOption) (NativeStreamedBlock, error)
}
NativeParser interface {
- ParseBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error)
+ ParseBlock(ctx context.Context, rawBlock *api.Block, opts ...ParseOption) (*api.NativeBlock, error)
GetTransaction(ctx context.Context, nativeBlock *api.NativeBlock, transactionHash string) (*api.NativeTransaction, error)
}
@@ -45,22 +62,34 @@ type (
Params struct {
fx.In
fxparams.Params
- Aleo ParserFactory `name:"aleo" optional:"true"`
- Bitcoin ParserFactory `name:"bitcoin" optional:"true"`
- Bsc ParserFactory `name:"bsc" optional:"true"`
- Ethereum ParserFactory `name:"ethereum" optional:"true"`
- Rosetta ParserFactory `name:"rosetta" optional:"true"`
- Solana ParserFactory `name:"solana" optional:"true"`
- Polygon ParserFactory `name:"polygon" optional:"true"`
- Avacchain ParserFactory `name:"avacchain" optional:"true"`
- Arbitrum ParserFactory `name:"arbitrum" optional:"true"`
- Optimism ParserFactory `name:"optimism" optional:"true"`
- Fantom ParserFactory `name:"fantom" optional:"true"`
- Base ParserFactory `name:"base" optional:"true"`
- Aptos ParserFactory `name:"aptos" optional:"true"`
- EthereumBeacon ParserFactory `name:"ethereum/beacon" optional:"true"`
- CosmosStaking ParserFactory `name:"cosmos/staking" optional:"true"`
- CardanoStaking ParserFactory `name:"cardano/staking" optional:"true"`
+ Aleo ParserFactory `name:"aleo" optional:"true"`
+ Bitcoin ParserFactory `name:"bitcoin" optional:"true"`
+ Bsc ParserFactory `name:"bsc" optional:"true"`
+ Ethereum ParserFactory `name:"ethereum" optional:"true"`
+ Rosetta ParserFactory `name:"rosetta" optional:"true"`
+ Solana ParserFactory `name:"solana" optional:"true"`
+ Polygon ParserFactory `name:"polygon" optional:"true"`
+ Avacchain ParserFactory `name:"avacchain" optional:"true"`
+ Arbitrum ParserFactory `name:"arbitrum" optional:"true"`
+ Optimism ParserFactory `name:"optimism" optional:"true"`
+ Fantom ParserFactory `name:"fantom" optional:"true"`
+ Base ParserFactory `name:"base" optional:"true"`
+ Aptos ParserFactory `name:"aptos" optional:"true"`
+ EthereumBeacon ParserFactory `name:"ethereum/beacon" optional:"true"`
+ CosmosStaking ParserFactory `name:"cosmos/staking" optional:"true"`
+ CardanoStaking ParserFactory `name:"cardano/staking" optional:"true"`
+ Tron ParserFactory `name:"tron" optional:"true"`
+ Story ParserFactory `name:"story" optional:"true"`
+ EthereumClassic ParserFactory `name:"ethereumclassic" optional:"true"`
+ Plasma ParserFactory `name:"plasma" optional:"true"`
+ Monad ParserFactory `name:"monad" optional:"true"`
+ Abstract ParserFactory `name:"abstract" optional:"true"`
+ Megaeth ParserFactory `name:"megaeth" optional:"true"`
+ Seismic ParserFactory `name:"seismic" optional:"true"`
+ Dash ParserFactory `name:"dash" optional:"true"`
+ Zcash ParserFactory `name:"zcash" optional:"true"`
+ Tempo ParserFactory `name:"tempo" optional:"true"`
+ Robinhood ParserFactory `name:"robinhood" optional:"true"`
}
ParserParams struct {
@@ -82,8 +111,12 @@ func NewParser(params Params) (Parser, error) {
sidechain := params.Config.Chain.Sidechain
if sidechain == api.SideChain_SIDECHAIN_NONE {
switch blockchain {
- case common.Blockchain_BLOCKCHAIN_BITCOIN:
+ case common.Blockchain_BLOCKCHAIN_BITCOIN, common.Blockchain_BLOCKCHAIN_BITCOINCASH, common.Blockchain_BLOCKCHAIN_LITECOIN, common.Blockchain_BLOCKCHAIN_DOGECOIN:
factory = params.Bitcoin
+ case common.Blockchain_BLOCKCHAIN_ZCASH:
+ factory = params.Zcash
+ case common.Blockchain_BLOCKCHAIN_DASH:
+ factory = params.Dash
case common.Blockchain_BLOCKCHAIN_BSC:
factory = params.Bsc
case common.Blockchain_BLOCKCHAIN_ETHEREUM:
@@ -104,6 +137,26 @@ func NewParser(params Params) (Parser, error) {
factory = params.Fantom
case common.Blockchain_BLOCKCHAIN_APTOS:
factory = params.Aptos
+ case common.Blockchain_BLOCKCHAIN_TRON:
+ factory = params.Tron
+ case common.Blockchain_BLOCKCHAIN_STORY:
+ factory = params.Story
+ case common.Blockchain_BLOCKCHAIN_ETHEREUMCLASSIC:
+ factory = params.EthereumClassic
+ case common.Blockchain_BLOCKCHAIN_PLASMA:
+ factory = params.Plasma
+ case common.Blockchain_BLOCKCHAIN_MONAD:
+ factory = params.Monad
+ case common.Blockchain_BLOCKCHAIN_ABSTRACT:
+ factory = params.Abstract
+ case common.Blockchain_BLOCKCHAIN_MEGAETH:
+ factory = params.Megaeth
+ case common.Blockchain_BLOCKCHAIN_SEISMIC:
+ factory = params.Seismic
+ case common.Blockchain_BLOCKCHAIN_TEMPO:
+ factory = params.Tempo
+ case common.Blockchain_BLOCKCHAIN_ROBINHOOD:
+ factory = params.Robinhood
default:
if params.Config.IsRosetta() {
factory = params.Rosetta
@@ -131,8 +184,8 @@ func NewParser(params Params) (Parser, error) {
return parser, nil
}
-func (p *parserImpl) ParseNativeBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
- return p.nativeParser.ParseBlock(ctx, rawBlock)
+func (p *parserImpl) ParseNativeBlock(ctx context.Context, rawBlock *api.Block, opts ...ParseOption) (*api.NativeBlock, error) {
+ return p.nativeParser.ParseBlock(ctx, rawBlock, opts...)
}
func (p *parserImpl) GetNativeTransaction(ctx context.Context, nativeBlock *api.NativeBlock, transactionHash string) (*api.NativeTransaction, error) {
@@ -158,3 +211,174 @@ func (p *parserImpl) ValidateAccountState(ctx context.Context, req *api.Validate
func (p *parserImpl) ValidateRosettaBlock(ctx context.Context, req *api.ValidateRosettaBlockRequest, actualRosettaBlock *api.RosettaBlock) error {
return p.checker.ValidateRosettaBlock(ctx, req, actualRosettaBlock)
}
+
+func (p *parserImpl) ParseStreamNative(ctx context.Context, spooled *downloader.SpooledBlock, opts ...ParseOption) (NativeStreamedBlock, error) {
+ if spooled == nil {
+ return nil, xerrors.New("nil spooled block")
+ }
+
+ // Skipped blocks have no payload. Return an empty native stream
+ // carrying just the metadata synthesized from BlockFile. If the
+ // underlying chain is bitcoin-family, the skipped-bitcoin inner
+ // stream yields no transactions; otherwise GetBitcoin() is nil.
+ if spooled.BlockFile.GetSkipped() {
+ metadata := &api.BlockMetadata{
+ Tag: spooled.BlockFile.Tag,
+ Height: spooled.BlockFile.Height,
+ Skipped: true,
+ }
+ wrapper := &nativeStreamedBlock{metadata: metadata, spooled: spooled}
+ if _, ok := p.nativeParser.(BitcoinStreamer); ok {
+ wrapper.bitcoin = skippedBitcoinNativeStream{}
+ }
+ return wrapper, nil
+ }
+
+ // Bitcoin-family: walk the envelope, wire loaders, delegate to
+ // the native parser's StreamBlockIter.
+ if streamer, ok := p.nativeParser.(BitcoinStreamer); ok {
+ return buildBitcoinNativeStreamedBlock(ctx, spooled, streamer, opts...)
+ }
+
+ // No chain-specific streaming available. Return metadata-only
+ // wrapper; caller falls back to GetBlock + ParseNativeBlock.
+ r, err := spooled.Open()
+ if err != nil {
+ return nil, xerrors.Errorf("open spool for metadata walk: %w", err)
+ }
+ defer func() { _ = r.Close() }()
+ block, err := api.WalkBlockEnvelope(r)
+ if err != nil {
+ return nil, xerrors.Errorf("walk block envelope: %w", err)
+ }
+ return &nativeStreamedBlock{
+ metadata: block.GetMetadata(),
+ spooled: spooled,
+ }, nil
+}
+
+// nativeStreamedBlock is the chain-agnostic NativeStreamedBlock
+// implementation. Per-chain inner streams are populated when the
+// configured chain's native parser supports streaming; otherwise the
+// corresponding Get() returns nil.
+type nativeStreamedBlock struct {
+ metadata *api.BlockMetadata
+ spooled *downloader.SpooledBlock
+ // spoolHandle is a cached file handle over spooled used by the
+ // bitcoin loaders' ReadAt calls (so per-tx seek+read doesn't
+ // pay an Open syscall). Closed on Close() alongside the spool.
+ // Populated only for bitcoin-family streams.
+ spoolHandle io.Closer
+ bitcoin BitcoinNativeStream
+ // ethereum is always nil today; added when the ethereum streaming
+ // walker lands.
+ ethereum EthereumNativeStream
+}
+
+func (n *nativeStreamedBlock) GetMetadata() *api.BlockMetadata { return n.metadata }
+func (n *nativeStreamedBlock) Close() error {
+ if n.spoolHandle != nil {
+ _ = n.spoolHandle.Close()
+ }
+ return n.spooled.Close()
+}
+func (n *nativeStreamedBlock) GetBitcoin() BitcoinNativeStream {
+ if n == nil {
+ return nil
+ }
+ return n.bitcoin
+}
+func (n *nativeStreamedBlock) GetEthereum() EthereumNativeStream {
+ if n == nil {
+ return nil
+ }
+ return n.ethereum
+}
+
+// skippedBitcoinNativeStream is the bitcoin inner stream returned for
+// skipped bitcoin-family blocks. Yields no transactions and reports
+// "no header" on Header().
+type skippedBitcoinNativeStream struct{}
+
+func (skippedBitcoinNativeStream) Transactions() iter.Seq2[*api.BitcoinTransaction, error] {
+ return func(yield func(*api.BitcoinTransaction, error) bool) {}
+}
+func (skippedBitcoinNativeStream) Header() (*api.BitcoinHeader, error) {
+ return nil, xerrors.New("skipped block has no header")
+}
+
+// buildBitcoinNativeStreamedBlock walks the bitcoin proto envelope,
+// wires lazy seek-based loaders over the spool, and delegates to the
+// native parser's StreamBlockIter. The returned NativeStreamedBlock
+// has GetBitcoin() populated and GetEthereum() nil.
+func buildBitcoinNativeStreamedBlock(ctx context.Context, spooled *downloader.SpooledBlock, streamer BitcoinStreamer, opts ...ParseOption) (NativeStreamedBlock, error) {
+ r, err := spooled.Open()
+ if err != nil {
+ return nil, xerrors.Errorf("open spool for walk: %w", err)
+ }
+ block, chunks, walkErr := api.WalkBitcoinEnvelope(r)
+ _ = r.Close()
+ if walkErr != nil {
+ return nil, xerrors.Errorf("walk bitcoin envelope: %w", walkErr)
+ }
+ if chunks.Header.Length == 0 && chunks.Header.Offset == 0 {
+ return nil, xerrors.Errorf("bitcoin block at height %d has no header", spooled.BlockFile.GetHeight())
+ }
+
+ // Cache a single *os.File handle over the spool so header and
+ // per-tx loaders use pread (ReadAt) instead of repeated
+ // Open+Seek+Read+Close. For a many-tx block this cuts ~4
+ // syscalls per tx × N txs into one open + N preads, making a
+ // meaningful difference under concurrent workloads.
+ handle, err := spooled.Open()
+ if err != nil {
+ return nil, xerrors.Errorf("open spool for loaders: %w", err)
+ }
+ readerAt, ok := handle.(io.ReaderAt)
+ if !ok {
+ _ = handle.Close()
+ return nil, xerrors.Errorf("spool reader does not support ReadAt")
+ }
+
+ openHeader := func() (io.ReadCloser, error) {
+ return io.NopCloser(io.NewSectionReader(readerAt, chunks.Header.Offset, chunks.Header.Length)), nil
+ }
+
+ groups := chunks.InputTransactionsGroups
+ loadGroup := func(i int) (*api.RepeatedBytes, error) {
+ if i < 0 || i >= len(groups) {
+ return nil, nil
+ }
+ ref := groups[i]
+ b := make([]byte, ref.Length)
+ if _, err := readerAt.ReadAt(b, ref.Offset); err != nil && err != io.EOF {
+ return nil, xerrors.Errorf("read input tx group [%d]: %w", i, err)
+ }
+ rb := &api.RepeatedBytes{}
+ if err := proto.Unmarshal(b, rb); err != nil {
+ return nil, xerrors.Errorf("unmarshal input tx group [%d]: %w", i, err)
+ }
+ return rb, nil
+ }
+
+ inner := streamer.StreamBlockIter(ctx, openHeader, loadGroup, opts...)
+ return &nativeStreamedBlock{
+ metadata: block.GetMetadata(),
+ spooled: spooled,
+ spoolHandle: handle,
+ bitcoin: inner,
+ }, nil
+}
+
+// seekedHeaderReader is an io.ReadCloser that reads up to `limit`
+// bytes from `f` (an io.ReadCloser) and closes `f` on Close.
+type seekedHeaderReader struct {
+ f io.ReadCloser
+ limit io.Reader
+}
+
+func (r *seekedHeaderReader) Read(p []byte) (int, error) { return r.limit.Read(p) }
+func (r *seekedHeaderReader) Close() error { return r.f.Close() }
+
+// Ensure os.File satisfies the reader contract we seek against.
+var _ io.ReadSeekCloser = (*os.File)(nil)
diff --git a/internal/blockchain/parser/internal/parser_builder.go b/internal/blockchain/parser/internal/parser_builder.go
index d5b9abb2..3aa8d1ea 100644
--- a/internal/blockchain/parser/internal/parser_builder.go
+++ b/internal/blockchain/parser/internal/parser_builder.go
@@ -79,7 +79,13 @@ func (f *parserFactoryImpl) NewParser() (Parser, error) {
return nil, xerrors.Errorf("failed to create native parser: %w", err)
}
- rosettaParser, err := f.rosettaParserFactory(f.params, nativeParser)
+ rosettaParserFactory := f.rosettaParserFactory
+ if !f.params.Config.Chain.Feature.RosettaParser {
+ // The chain opted out of the rosetta parser (e.g. dogecoin), even if
+ // the parser it shares with other chains registers a rosetta factory.
+ rosettaParserFactory = NewNotImplementedRosettaParser
+ }
+ rosettaParser, err := rosettaParserFactory(f.params, nativeParser)
if err != nil {
return nil, xerrors.Errorf("failed to create rosetta parser: %w", err)
}
diff --git a/internal/blockchain/parser/internal/stream.go b/internal/blockchain/parser/internal/stream.go
new file mode 100644
index 00000000..0ce43516
--- /dev/null
+++ b/internal/blockchain/parser/internal/stream.go
@@ -0,0 +1,73 @@
+package internal
+
+import (
+ "context"
+ "io"
+ "iter"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// NativeStreamedBlock is the chain-agnostic interface returned by
+// Parser.ParseStreamNative. It exposes block metadata plus a Close
+// method that releases backing resources (most notably the
+// decompressed spool file in the downloader layer), and nil-checkable
+// per-chain accessors that mirror *api.NativeBlock's oneof shape.
+//
+// Callers pick the accessor matching the chain their code handles. If
+// the configured chain does not yet support streaming (or if the
+// block's chain does not match), all Get() accessors return
+// nil and callers should fall back to GetBlock + ParseNativeBlock.
+type NativeStreamedBlock interface {
+ // GetMetadata returns the block metadata from the downloaded envelope.
+ GetMetadata() *api.BlockMetadata
+ // Close releases backing resources. Safe to call multiple times.
+ Close() error
+ // GetBitcoin returns a bitcoin-family stream view, or nil when the
+ // configured chain is not bitcoin-family.
+ GetBitcoin() BitcoinNativeStream
+ // GetEthereum returns an ethereum stream view. Always nil today;
+ // will be populated once the ethereum streaming walker lands.
+ GetEthereum() EthereumNativeStream
+}
+
+// BitcoinNativeStream is the bitcoin-family iterator view. Yielded
+// transactions match the output of Parser.ParseNativeBlock's
+// Block.Bitcoin.Transactions exactly (parity enforced by
+// bitcoin_native_stream_parity_test).
+//
+// See the bitcoin parser package for the "free after iteration"
+// Header() ordering contract.
+type BitcoinNativeStream interface {
+ // Transactions yields each decoded transaction in the block.
+ Transactions() iter.Seq2[*api.BitcoinTransaction, error]
+ // Header returns the block header, lazily populated.
+ Header() (*api.BitcoinHeader, error)
+}
+
+// EthereumNativeStream is the ethereum iterator view. Stub interface
+// until the ethereum streaming walker is implemented; today
+// NativeStreamedBlock.GetEthereum() always returns nil.
+type EthereumNativeStream interface {
+ Transactions() iter.Seq2[*api.EthereumTransaction, error]
+ Header() (*api.EthereumHeader, error)
+}
+
+// BitcoinInputTxGroupLoader returns the prev-output transactions for
+// the i-th block transaction. See bitcoin.InputTxGroupLoader for
+// details.
+type BitcoinInputTxGroupLoader func(i int) (*api.RepeatedBytes, error)
+
+// BitcoinStreamer identifies bitcoin-family NativeParser implementations
+// that support streaming via StreamBlockIter. The generic Parser impl
+// type-asserts its underlying NativeParser to BitcoinStreamer; non-
+// bitcoin chains don't implement this and fall back to leaving
+// NativeStreamedBlock.GetBitcoin() nil.
+type BitcoinStreamer interface {
+ StreamBlockIter(
+ ctx context.Context,
+ openReader func() (io.ReadCloser, error),
+ loadGroup BitcoinInputTxGroupLoader,
+ opts ...ParseOption,
+ ) BitcoinNativeStream
+}
diff --git a/internal/blockchain/parser/internal/utils.go b/internal/blockchain/parser/internal/utils.go
index 9689d474..4a30d337 100644
--- a/internal/blockchain/parser/internal/utils.go
+++ b/internal/blockchain/parser/internal/utils.go
@@ -11,6 +11,7 @@ import (
"github.com/ethereum/go-ethereum/common/hexutil"
"golang.org/x/xerrors"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
)
@@ -27,7 +28,7 @@ func NewNop() Parser {
return nopParser{}
}
-func (p nopParser) ParseNativeBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+func (p nopParser) ParseNativeBlock(ctx context.Context, rawBlock *api.Block, opts ...ParseOption) (*api.NativeBlock, error) {
return &api.NativeBlock{}, nil
}
@@ -55,6 +56,10 @@ func (p nopParser) ValidateRosettaBlock(ctx context.Context, req *api.ValidateRo
return nil
}
+func (p nopParser) ParseStreamNative(ctx context.Context, spooled *downloader.SpooledBlock, opts ...ParseOption) (NativeStreamedBlock, error) {
+ return nil, xerrors.New("nopParser does not support streaming")
+}
+
// ValidateChain checks if the chain is continuous.
func ValidateChain(blocks []*api.BlockMetadata, lastBlock *api.BlockMetadata) error {
for _, currBlock := range blocks {
diff --git a/internal/blockchain/parser/mocks/mocks.go b/internal/blockchain/parser/mocks/mocks.go
index e620d02e..c3e38148 100644
--- a/internal/blockchain/parser/mocks/mocks.go
+++ b/internal/blockchain/parser/mocks/mocks.go
@@ -13,6 +13,8 @@ import (
context "context"
reflect "reflect"
+ parser "github.com/coinbase/chainstorage/internal/blockchain/parser"
+ downloader "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
chainstorage "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
gomock "go.uber.org/mock/gomock"
)
@@ -70,18 +72,23 @@ func (mr *MockParserMockRecorder) GetNativeTransaction(arg0, arg1, arg2 any) *go
}
// ParseNativeBlock mocks base method.
-func (m *MockParser) ParseNativeBlock(arg0 context.Context, arg1 *chainstorage.Block) (*chainstorage.NativeBlock, error) {
+func (m *MockParser) ParseNativeBlock(arg0 context.Context, arg1 *chainstorage.Block, arg2 ...parser.ParseOption) (*chainstorage.NativeBlock, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ParseNativeBlock", arg0, arg1)
+ varargs := []any{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ParseNativeBlock", varargs...)
ret0, _ := ret[0].(*chainstorage.NativeBlock)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ParseNativeBlock indicates an expected call of ParseNativeBlock.
-func (mr *MockParserMockRecorder) ParseNativeBlock(arg0, arg1 any) *gomock.Call {
+func (mr *MockParserMockRecorder) ParseNativeBlock(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseNativeBlock", reflect.TypeOf((*MockParser)(nil).ParseNativeBlock), arg0, arg1)
+ varargs := append([]any{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseNativeBlock", reflect.TypeOf((*MockParser)(nil).ParseNativeBlock), varargs...)
}
// ParseRosettaBlock mocks base method.
@@ -141,3 +148,23 @@ func (mr *MockParserMockRecorder) ValidateRosettaBlock(arg0, arg1, arg2 any) *go
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateRosettaBlock", reflect.TypeOf((*MockParser)(nil).ValidateRosettaBlock), arg0, arg1, arg2)
}
+
+// ParseStreamNative mocks base method.
+func (m *MockParser) ParseStreamNative(arg0 context.Context, arg1 *downloader.SpooledBlock, arg2 ...parser.ParseOption) (parser.NativeStreamedBlock, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ParseStreamNative", varargs...)
+ ret0, _ := ret[0].(parser.NativeStreamedBlock)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// ParseStreamNative indicates an expected call of ParseStreamNative.
+func (mr *MockParserMockRecorder) ParseStreamNative(arg0, arg1 any, arg2 ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseStreamNative", reflect.TypeOf((*MockParser)(nil).ParseStreamNative), varargs...)
+}
diff --git a/internal/blockchain/parser/module.go b/internal/blockchain/parser/module.go
index cc70d614..41eded02 100644
--- a/internal/blockchain/parser/module.go
+++ b/internal/blockchain/parser/module.go
@@ -16,7 +16,34 @@ import (
type (
Parser = internal.Parser
+ // NativeParser is re-exported so callers outside this package can
+ // instantiate a chain-specific native parser directly without importing
+ // the internal subpackage.
+ NativeParser = internal.NativeParser
+
ParityCheckFailedError = internal.ParityCheckFailedError
+
+ // ParseOption is re-exported so callers outside this package (including
+ // the SDK) can construct options without importing the internal
+ // subpackage directly.
+ ParseOption = internal.ParseOption
+
+ // NativeStreamedBlock is the chain-agnostic container returned
+ // by Parser.ParseStreamNative. Re-exported so SDK consumers can
+ // reference it without importing internal.
+ NativeStreamedBlock = internal.NativeStreamedBlock
+
+ // BitcoinNativeStream is the bitcoin-family iterator view.
+ // Exposed via NativeStreamedBlock.GetBitcoin().
+ BitcoinNativeStream = internal.BitcoinNativeStream
+
+ // EthereumNativeStream is the ethereum iterator view. Stub
+ // interface until the ethereum streaming walker lands.
+ EthereumNativeStream = internal.EthereumNativeStream
+
+ // BitcoinInputTxGroupLoader is re-exported so SDK callers can
+ // type their consumer lambdas without importing internal.
+ BitcoinInputTxGroupLoader = internal.BitcoinInputTxGroupLoader
)
var (
@@ -24,6 +51,11 @@ var (
ErrNotImplemented = internal.ErrNotImplemented
ErrNotFound = internal.ErrNotFound
ErrInvalidParameters = internal.ErrInvalidParameters
+
+ // Parse options (see internal/options.go for behavior).
+ WithSkipScripts = internal.WithSkipScripts
+ WithSkipWitnesses = internal.WithSkipWitnesses
+ WithSkipShielded = internal.WithSkipShielded
)
var Module = fx.Options(
diff --git a/internal/blockchain/parser/rosetta/rosetta_native.go b/internal/blockchain/parser/rosetta/rosetta_native.go
index f5dd5a2a..db66ddc3 100644
--- a/internal/blockchain/parser/rosetta/rosetta_native.go
+++ b/internal/blockchain/parser/rosetta/rosetta_native.go
@@ -23,7 +23,7 @@ func NewRosettaNativeParser(params internal.ParserParams, opts ...internal.Parse
}, nil
}
-func (p *rosettaNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+func (p *rosettaNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block, opts ...internal.ParseOption) (*api.NativeBlock, error) {
metadata := rawBlock.GetMetadata()
if metadata == nil {
return nil, xerrors.New("metadata not found")
diff --git a/internal/blockchain/parser/rosetta/rosetta_native_test.go b/internal/blockchain/parser/rosetta/rosetta_native_test.go
index 7466b7e8..be09555c 100644
--- a/internal/blockchain/parser/rosetta/rosetta_native_test.go
+++ b/internal/blockchain/parser/rosetta/rosetta_native_test.go
@@ -273,18 +273,19 @@ func TestParseRosettaNativeBlock(t *testing.T) {
},
}
- var parser internal.Parser
+ // The rosetta parser is instantiated directly because no blockchain is
+ // currently dispatched to it; the fixtures are dogecoin-flavored sample data.
+ var parser internal.NativeParser
app := testapp.New(
t,
testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_DOGECOIN, common.Network_NETWORK_DOGECOIN_MAINNET),
- Module,
- internal.Module,
+ fx.Provide(NewRosettaNativeParser),
fx.Populate(&parser),
)
defer app.Close()
require.NotNil(parser)
- nativeBlock, err := parser.ParseNativeBlock(context.Background(), block)
+ nativeBlock, err := parser.ParseBlock(context.Background(), block)
require.NoError(err)
require.Equal(common.Blockchain_BLOCKCHAIN_DOGECOIN, nativeBlock.Blockchain)
require.Equal(common.Network_NETWORK_DOGECOIN_MAINNET, nativeBlock.Network)
@@ -314,18 +315,17 @@ func TestParseRosettaNativeBlock_Nil(t *testing.T) {
},
}
- var parser internal.Parser
+ var parser internal.NativeParser
app := testapp.New(
t,
testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_DOGECOIN, common.Network_NETWORK_DOGECOIN_MAINNET),
- Module,
- internal.Module,
+ fx.Provide(NewRosettaNativeParser),
fx.Populate(&parser),
)
defer app.Close()
require.NotNil(parser)
- nativeBlock, err := parser.ParseNativeBlock(context.Background(), block)
+ nativeBlock, err := parser.ParseBlock(context.Background(), block)
require.NoError(err)
require.Equal(common.Blockchain_BLOCKCHAIN_DOGECOIN, nativeBlock.Blockchain)
require.Equal(common.Network_NETWORK_DOGECOIN_MAINNET, nativeBlock.Network)
diff --git a/internal/blockchain/parser/rosetta/rosetta_rosetta_test.go b/internal/blockchain/parser/rosetta/rosetta_rosetta_test.go
index 6a3d83a7..0c2bc6e5 100644
--- a/internal/blockchain/parser/rosetta/rosetta_rosetta_test.go
+++ b/internal/blockchain/parser/rosetta/rosetta_rosetta_test.go
@@ -148,18 +148,20 @@ func TestParseRosettaRosettaBlockFromNative(t *testing.T) {
},
}
- var parser internal.Parser
+ // The rosetta parser is instantiated directly because no blockchain is
+ // currently dispatched to it; the fixtures are dogecoin-flavored sample data.
+ var parser internal.RosettaParser
app := testapp.New(
t,
testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_DOGECOIN, common.Network_NETWORK_DOGECOIN_MAINNET),
- Module,
- internal.Module,
+ fx.Provide(NewRosettaNativeParser),
+ fx.Provide(NewRosettaRosettaParser),
fx.Populate(&parser),
)
defer app.Close()
require.NotNil(parser)
- actual, err := parser.ParseRosettaBlock(context.Background(), block)
+ actual, err := parser.ParseBlock(context.Background(), block)
require.NoError(err)
require.NotNil(actual)
require.Equal(expected, actual)
@@ -179,18 +181,18 @@ func TestParseRosettaRosettaBlock_Nil(t *testing.T) {
},
}
- var parser internal.Parser
+ var parser internal.RosettaParser
app := testapp.New(
t,
testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_DOGECOIN, common.Network_NETWORK_DOGECOIN_MAINNET),
- Module,
- internal.Module,
+ fx.Provide(NewRosettaNativeParser),
+ fx.Provide(NewRosettaRosettaParser),
fx.Populate(&parser),
)
defer app.Close()
require.NotNil(parser)
- actual, err := parser.ParseRosettaBlock(context.Background(), block)
+ actual, err := parser.ParseBlock(context.Background(), block)
require.Nil(actual)
require.NotNil(err)
require.Contains(err.Error(), "not found")
diff --git a/internal/blockchain/parser/solana/solana_native.go b/internal/blockchain/parser/solana/solana_native.go
index 6560fbb5..77ba6975 100644
--- a/internal/blockchain/parser/solana/solana_native.go
+++ b/internal/blockchain/parser/solana/solana_native.go
@@ -221,7 +221,7 @@ func NewSolanaNativeParser(params internal.ParserParams, opts ...internal.Parser
}, nil
}
-func (p *solanaNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+func (p *solanaNativeParserImpl) ParseBlock(ctx context.Context, rawBlock *api.Block, opts ...internal.ParseOption) (*api.NativeBlock, error) {
metadata := rawBlock.GetMetadata()
if metadata == nil {
return nil, xerrors.New("metadata not found")
diff --git a/internal/blockchain/restapi/client.go b/internal/blockchain/restapi/client.go
index 2b27f4f7..b7e246cd 100644
--- a/internal/blockchain/restapi/client.go
+++ b/internal/blockchain/restapi/client.go
@@ -46,15 +46,17 @@ type (
Slave endpoints.EndpointProvider `name:"slave"`
Validator endpoints.EndpointProvider `name:"validator"`
Consensus endpoints.EndpointProvider `name:"consensus"`
+ Additional endpoints.EndpointProvider `name:"additional"`
HTTPClient HTTPClient `optional:"true"` // Injected by unit test.
}
ClientResult struct {
fx.Out
- Master Client `name:"master"`
- Slave Client `name:"slave"`
- Validator Client `name:"validator"`
- Consensus Client `name:"consensus"`
+ Master Client `name:"master"`
+ Slave Client `name:"slave"`
+ Validator Client `name:"validator"`
+ Consensus Client `name:"consensus"`
+ Additional Client `name:"additional"`
}
HTTPError struct {
@@ -66,6 +68,7 @@ type (
// The 'Name' is just used for annotation.
// For example, in Aptos, the 'ParamsPath' for block 1 will be: "/blocks/by_height/1?with_transactions=true".
RequestMethod struct {
+ HTTPMethod string
Name string
ParamsPath string
Timeout time.Duration
@@ -106,12 +109,16 @@ func New(params ClientParams) (ClientResult, error) {
if err != nil {
return ClientResult{}, xerrors.Errorf("failed to create consensus client: %w", err)
}
-
+ additional, err := newClient(params, params.Additional)
+ if err != nil {
+ return ClientResult{}, xerrors.Errorf("failed to create additional client: %w", err)
+ }
return ClientResult{
- Master: master,
- Slave: slave,
- Validator: validator,
- Consensus: consensus,
+ Master: master,
+ Slave: slave,
+ Validator: validator,
+ Consensus: consensus,
+ Additional: additional,
}, nil
}
@@ -171,9 +178,7 @@ func (c *clientImpl) makeHTTPRequest(ctx context.Context, method *RequestMethod,
ctx, cancel := context.WithTimeout(ctx, method.Timeout)
defer cancel()
-
- // TODO: will handle both GET and POST.
- request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, bytes.NewReader(requestBody))
+ request, err := http.NewRequestWithContext(ctx, method.HTTPMethod, url, bytes.NewReader(requestBody))
if err != nil {
err = c.sanitizedError(err)
return nil, xerrors.Errorf("failed to create request: %w", err)
@@ -201,6 +206,19 @@ func (c *clientImpl) makeHTTPRequest(ctx context.Context, method *RequestMethod,
return nil, retry.Retryable(xerrors.Errorf("failed to read http response: %w", err))
}
+ // Check for incomplete response based on Content-Length header
+ expectedLen := response.ContentLength
+ if expectedLen > 0 && int64(len(responseBody)) != expectedLen {
+ c.logger.Warn(
+ "incomplete http response detected",
+ zap.String("method", method.Name),
+ zap.Int64("expected_bytes", expectedLen),
+ zap.Int("actual_bytes", len(responseBody)),
+ zap.String("response_preview", truncateString(string(responseBody), 200)),
+ )
+ return nil, retry.Retryable(xerrors.Errorf("incomplete http response: received %d bytes, expected %d bytes", len(responseBody), expectedLen))
+ }
+
if response.StatusCode != http.StatusOK {
errHTTP := xerrors.Errorf("received http error: %w", &HTTPError{
Code: response.StatusCode,
@@ -263,3 +281,14 @@ func (c *clientImpl) sanitizedError(err error) error {
}
return err
}
+
+// truncateString truncates a string to the specified max length, adding "..." if truncated.
+func truncateString(s string, maxLen int) string {
+ if len(s) <= maxLen {
+ return s
+ }
+ if maxLen <= 3 {
+ return s[:maxLen]
+ }
+ return s[:maxLen-3] + "..."
+}
diff --git a/internal/blockchain/restapi/client_test.go b/internal/blockchain/restapi/client_test.go
index e28be5a4..4586bed0 100644
--- a/internal/blockchain/restapi/client_test.go
+++ b/internal/blockchain/restapi/client_test.go
@@ -112,7 +112,7 @@ func TestCall_RequestError(t *testing.T) {
require.Nil(response)
require.Error(err)
- require.Contains(err.Error(), "method=&{hello path 5ns}")
+ require.Contains(err.Error(), "method=&{ hello path 5ns}")
require.Contains(err.Error(), "requestBody=[]")
require.Contains(err.Error(), "endpoint=node_name")
@@ -165,7 +165,7 @@ func TestCall_RequestError_FailedWithRetry(t *testing.T) {
require.Nil(response)
require.Error(err)
- require.Contains(err.Error(), "method=&{hello path 5ns}")
+ require.Contains(err.Error(), "method=&{ hello path 5ns}")
require.Contains(err.Error(), "requestBody=[]")
require.Contains(err.Error(), "endpoint=node_name")
@@ -181,6 +181,54 @@ func TestCall_RequestError_FailedWithRetry(t *testing.T) {
require.Equal("block_not_found", errOut.ErrorCode)
}
+func TestCall_RequestMethod(t *testing.T) {
+ require := testutil.Require(t)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ httpClient := restapimocks.NewMockHTTPClient(ctrl)
+ // Construct a REST API response with request method in body
+ httpClient.EXPECT().Do(gomock.Any()).DoAndReturn(func(req *http.Request) (*http.Response, error) {
+ method := req.Method
+ body := ioutil.NopCloser(strings.NewReader(`{"method": "` + method + `"}`))
+ return &http.Response{
+ StatusCode: http.StatusOK,
+ Body: body,
+ }, nil
+ }).Times(2)
+
+ var params clientParams
+ app := testapp.New(
+ t,
+ withDummyEndpoints(),
+ fx.Provide(restapi.New),
+ fx.Provide(func() restapi.HTTPClient {
+ return httpClient
+ }),
+ fx.Populate(¶ms),
+ )
+ defer app.Close()
+
+ client := params.Master
+ require.NotNil(client)
+
+ methods := []string{http.MethodGet, http.MethodPost}
+ for _, method := range methods {
+ response, err := client.Call(context.Background(),
+ &restapi.RequestMethod{Name: "hello", ParamsPath: "path", HTTPMethod: method, Timeout: time.Duration(5)},
+ nil)
+ require.NoError(err)
+ // assert the right method
+ var responseData map[string]string
+ err = json.Unmarshal(response, &responseData)
+ require.NoError(err)
+ require.NotEmpty(responseData)
+ require.Equal(method, responseData["method"])
+
+ }
+}
+
func TestCall_RequestError_SucceededAfterRetries(t *testing.T) {
require := testutil.Require(t)
@@ -263,7 +311,7 @@ func TestCall_RequestError_WithCustomizedAttempts(t *testing.T) {
require.Error(err)
require.Nil(response)
- require.Contains(err.Error(), "method=&{hello path 5ns}")
+ require.Contains(err.Error(), "method=&{ hello path 5ns}")
require.Contains(err.Error(), "requestBody=[]")
require.Contains(err.Error(), "endpoint=node_name")
diff --git a/internal/cadence/runtime.go b/internal/cadence/runtime.go
index ac4f9e67..e024ddea 100644
--- a/internal/cadence/runtime.go
+++ b/internal/cadence/runtime.go
@@ -7,7 +7,9 @@ import (
"net"
"time"
+ filterpb "go.temporal.io/api/filter/v1"
"go.temporal.io/api/serviceerror"
+ workflowpb "go.temporal.io/api/workflow/v1"
"go.temporal.io/api/workflowservice/v1"
"go.temporal.io/sdk/activity"
"go.temporal.io/sdk/client"
@@ -41,7 +43,7 @@ type (
TerminateWorkflow(ctx context.Context, workflowID string, runID string, reason string) error
OnStart(ctx context.Context) error
OnStop(ctx context.Context) error
- ListOpenWorkflows(ctx context.Context, namespace string, maxPageSize int32) (*workflowservice.ListOpenWorkflowExecutionsResponse, error)
+ ListOpenWorkflows(ctx context.Context, namespace string, maxPageSize int32, workflowType string) (*workflowservice.ListOpenWorkflowExecutionsResponse, error)
}
RuntimeParams struct {
@@ -57,6 +59,8 @@ type (
namespaceClient client.NamespaceClient
workers []worker.Worker
}
+
+ listOpenWorkflowsFn func(ctx context.Context, request *workflowservice.ListOpenWorkflowExecutionsRequest) (*workflowservice.ListOpenWorkflowExecutionsResponse, error)
)
func NewRuntime(params RuntimeParams) (Runtime, error) {
@@ -69,35 +73,9 @@ func NewRuntime(params RuntimeParams) (Runtime, error) {
runtimeLogger := logur.LoggerToKV(zapadapter.New(logger))
address := params.Config.Cadence.Address
- tlsConfig := params.Config.Cadence.TLSConfig
- connectionOptions := client.ConnectionOptions{}
- if tlsConfig.Enabled && params.Config.Env() != config.EnvLocal {
- host, _, err := net.SplitHostPort(address)
- if err != nil {
- return nil, xerrors.Errorf("failed to parse address (%v): %w", address, err)
- }
-
- connectionOptions.TLS = &tls.Config{
- MinVersion: tls.VersionTLS12,
- ServerName: host,
- InsecureSkipVerify: !tlsConfig.ValidateHostname,
- }
-
- if tlsConfig.CertificateAuthority != "" {
- caCertPool := x509.NewCertPool()
- if !caCertPool.AppendCertsFromPEM([]byte(tlsConfig.CertificateAuthority)) {
- return nil, xerrors.Errorf("failed to parse CA certificate: %v", tlsConfig.CertificateAuthority)
- }
- connectionOptions.TLS.RootCAs = caCertPool
- }
-
- if tlsConfig.ClientCertificate != "" && tlsConfig.ClientPrivateKey != "" {
- clientCert, err := tls.X509KeyPair([]byte(tlsConfig.ClientCertificate), []byte(tlsConfig.ClientPrivateKey))
- if err != nil {
- return nil, xerrors.Errorf("failed to parse client certificate or key (%v): %w", tlsConfig.ClientCertificate, err)
- }
- connectionOptions.TLS.Certificates = []tls.Certificate{clientCert}
- }
+ connectionOptions, err := newConnectionOptions(params.Config.Cadence, params.Config.Env())
+ if err != nil {
+ return nil, err
}
options := client.Options{
@@ -123,12 +101,7 @@ func NewRuntime(params RuntimeParams) (Runtime, error) {
workers[i] = worker.New(
workflowClient,
workerConfig.TaskList,
- worker.Options{
- // Enable this option to allow worker to process sessions. Defaults to false.
- EnableSessionWorker: true,
- // If set defines maximum amount of time that workflow task will be allowed to run. Defaults to 1 sec.
- DeadlockDetectionTimeout: 2 * time.Second,
- },
+ newWorkerOptions(workerConfig),
)
}
@@ -144,15 +117,99 @@ func NewRuntime(params RuntimeParams) (Runtime, error) {
}
-func (r *runtimeImpl) ListOpenWorkflows(ctx context.Context, namespace string, maxPageSize int32) (*workflowservice.ListOpenWorkflowExecutionsResponse, error) {
- openWorkflows, err := r.workflowClient.ListOpenWorkflow(ctx, &workflowservice.ListOpenWorkflowExecutionsRequest{
- Namespace: namespace,
- MaximumPageSize: maxPageSize,
- })
- if err != nil {
- return nil, xerrors.Errorf("failed to get open workflows: %w", err)
+func newConnectionOptions(cadenceConfig config.CadenceConfig, env config.Env) (client.ConnectionOptions, error) {
+ connectionOptions := client.ConnectionOptions{}
+ if cadenceConfig.KeepAliveTime > 0 {
+ connectionOptions.KeepAliveTime = cadenceConfig.KeepAliveTime
+ }
+ if cadenceConfig.KeepAliveTimeout > 0 {
+ connectionOptions.KeepAliveTimeout = cadenceConfig.KeepAliveTimeout
+ }
+
+ tlsConfig := cadenceConfig.TLSConfig
+ if tlsConfig.Enabled && env != config.EnvLocal {
+ host, _, err := net.SplitHostPort(cadenceConfig.Address)
+ if err != nil {
+ return client.ConnectionOptions{}, xerrors.Errorf("failed to parse address (%v): %w", cadenceConfig.Address, err)
+ }
+
+ connectionOptions.TLS = &tls.Config{
+ MinVersion: tls.VersionTLS12,
+ ServerName: host,
+ InsecureSkipVerify: !tlsConfig.ValidateHostname,
+ }
+
+ if tlsConfig.CertificateAuthority != "" {
+ caCertPool := x509.NewCertPool()
+ if !caCertPool.AppendCertsFromPEM([]byte(tlsConfig.CertificateAuthority)) {
+ return client.ConnectionOptions{}, xerrors.Errorf("failed to parse CA certificate: %v", tlsConfig.CertificateAuthority)
+ }
+ connectionOptions.TLS.RootCAs = caCertPool
+ }
+
+ if tlsConfig.ClientCertificate != "" && tlsConfig.ClientPrivateKey != "" {
+ clientCert, err := tls.X509KeyPair([]byte(tlsConfig.ClientCertificate), []byte(tlsConfig.ClientPrivateKey))
+ if err != nil {
+ return client.ConnectionOptions{}, xerrors.Errorf("failed to parse client certificate or key (%v): %w", tlsConfig.ClientCertificate, err)
+ }
+ connectionOptions.TLS.Certificates = []tls.Certificate{clientCert}
+ }
+ }
+
+ return connectionOptions, nil
+}
+
+func newWorkerOptions(workerConfig config.WorkerConfig) worker.Options {
+ options := worker.Options{
+ // Enable this option to allow worker to process sessions. Defaults to false.
+ EnableSessionWorker: true,
+ // If set defines maximum amount of time that workflow task will be allowed to run. Defaults to 1 sec.
+ DeadlockDetectionTimeout: 2 * time.Second,
+ }
+ if workerConfig.MaxConcurrentActivityExecutionSize > 0 {
+ options.MaxConcurrentActivityExecutionSize = workerConfig.MaxConcurrentActivityExecutionSize
+ }
+
+ return options
+}
+
+func (r *runtimeImpl) ListOpenWorkflows(ctx context.Context, namespace string, maxPageSize int32, workflowType string) (*workflowservice.ListOpenWorkflowExecutionsResponse, error) {
+ return listOpenWorkflowExecutions(ctx, namespace, maxPageSize, workflowType, r.workflowClient.ListOpenWorkflow)
+}
+
+func listOpenWorkflowExecutions(
+ ctx context.Context,
+ namespace string,
+ maxPageSize int32,
+ workflowType string,
+ listFn listOpenWorkflowsFn,
+) (*workflowservice.ListOpenWorkflowExecutionsResponse, error) {
+ var executions []*workflowpb.WorkflowExecutionInfo
+ var nextPageToken []byte
+ for {
+ request := &workflowservice.ListOpenWorkflowExecutionsRequest{
+ Namespace: namespace,
+ MaximumPageSize: maxPageSize,
+ NextPageToken: nextPageToken,
+ }
+ if workflowType != "" {
+ request.Filters = &workflowservice.ListOpenWorkflowExecutionsRequest_TypeFilter{
+ TypeFilter: &filterpb.WorkflowTypeFilter{Name: workflowType},
+ }
+ }
+
+ openWorkflows, err := listFn(ctx, request)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get open workflows: %w", err)
+ }
+ if openWorkflows != nil {
+ executions = append(executions, openWorkflows.Executions...)
+ nextPageToken = openWorkflows.GetNextPageToken()
+ }
+ if len(nextPageToken) == 0 {
+ return &workflowservice.ListOpenWorkflowExecutionsResponse{Executions: executions}, nil
+ }
}
- return openWorkflows, nil
}
func (r *runtimeImpl) RegisterWorkflow(w any, options workflow.RegisterOptions) {
diff --git a/internal/cadence/runtime_test.go b/internal/cadence/runtime_test.go
new file mode 100644
index 00000000..b27c87d5
--- /dev/null
+++ b/internal/cadence/runtime_test.go
@@ -0,0 +1,184 @@
+package cadence
+
+import (
+ "context"
+ "crypto/tls"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ commonpb "go.temporal.io/api/common/v1"
+ workflowpb "go.temporal.io/api/workflow/v1"
+ "go.temporal.io/api/workflowservice/v1"
+
+ "github.com/coinbase/chainstorage/internal/config"
+)
+
+func TestNewConnectionOptionsLeavesTemporalKeepAliveUnsetByDefault(t *testing.T) {
+ require := require.New(t)
+
+ options, err := newConnectionOptions(
+ config.CadenceConfig{
+ Address: "temporal.example.com:7233",
+ },
+ config.EnvProduction,
+ )
+
+ require.NoError(err)
+ require.Zero(options.KeepAliveTime)
+ require.Zero(options.KeepAliveTimeout)
+ require.False(options.DisableKeepAliveCheck)
+ require.False(options.DisableKeepAlivePermitWithoutStream)
+ require.Nil(options.TLS)
+}
+
+func TestNewConnectionOptionsAppliesConfiguredTemporalKeepAlive(t *testing.T) {
+ require := require.New(t)
+
+ options, err := newConnectionOptions(
+ config.CadenceConfig{
+ Address: "temporal.example.com:7233",
+ KeepAliveTime: 10 * time.Second,
+ KeepAliveTimeout: 30 * time.Second,
+ },
+ config.EnvProduction,
+ )
+
+ require.NoError(err)
+ require.Equal(10*time.Second, options.KeepAliveTime)
+ require.Equal(30*time.Second, options.KeepAliveTimeout)
+ require.False(options.DisableKeepAliveCheck)
+ require.False(options.DisableKeepAlivePermitWithoutStream)
+ require.Nil(options.TLS)
+}
+
+func TestNewConnectionOptionsPreservesTLSConfig(t *testing.T) {
+ require := require.New(t)
+
+ options, err := newConnectionOptions(
+ config.CadenceConfig{
+ Address: "temporal.example.com:7233",
+ KeepAliveTime: 10 * time.Second,
+ KeepAliveTimeout: 30 * time.Second,
+ TLSConfig: config.CadenceTLSConfig{
+ Enabled: true,
+ ValidateHostname: true,
+ },
+ },
+ config.EnvProduction,
+ )
+
+ require.NoError(err)
+ require.NotNil(options.TLS)
+ require.Equal(uint16(tls.VersionTLS12), options.TLS.MinVersion)
+ require.Equal("temporal.example.com", options.TLS.ServerName)
+ require.False(options.TLS.InsecureSkipVerify)
+ require.Equal(10*time.Second, options.KeepAliveTime)
+ require.Equal(30*time.Second, options.KeepAliveTimeout)
+}
+
+func TestNewConnectionOptionsSuppressesTLSConfigForLocalEnv(t *testing.T) {
+ require := require.New(t)
+
+ options, err := newConnectionOptions(
+ config.CadenceConfig{
+ Address: "temporal.example.com:7233",
+ TLSConfig: config.CadenceTLSConfig{
+ Enabled: true,
+ },
+ },
+ config.EnvLocal,
+ )
+
+ require.NoError(err)
+ require.Nil(options.TLS)
+}
+
+func TestNewConnectionOptionsRejectsInvalidCA(t *testing.T) {
+ require := require.New(t)
+
+ _, err := newConnectionOptions(
+ config.CadenceConfig{
+ Address: "temporal.example.com:7233",
+ TLSConfig: config.CadenceTLSConfig{
+ Enabled: true,
+ CertificateAuthority: "not-pem",
+ },
+ },
+ config.EnvProduction,
+ )
+
+ require.ErrorContains(err, "failed to parse CA certificate")
+}
+
+func TestNewWorkerOptionsPreservesDefaultActivityConcurrency(t *testing.T) {
+ require := require.New(t)
+
+ options := newWorkerOptions(config.WorkerConfig{TaskList: "default"})
+
+ require.True(options.EnableSessionWorker)
+ require.Equal(2*time.Second, options.DeadlockDetectionTimeout)
+ require.Zero(options.MaxConcurrentActivityExecutionSize)
+}
+
+func TestNewWorkerOptionsAppliesActivityConcurrencyLimit(t *testing.T) {
+ require := require.New(t)
+
+ options := newWorkerOptions(config.WorkerConfig{
+ TaskList: "batch_consolidator",
+ MaxConcurrentActivityExecutionSize: 1,
+ })
+
+ require.True(options.EnableSessionWorker)
+ require.Equal(2*time.Second, options.DeadlockDetectionTimeout)
+ require.Equal(1, options.MaxConcurrentActivityExecutionSize)
+}
+
+func TestListOpenWorkflowExecutionsPaginatesAndAppliesTypeFilter(t *testing.T) {
+ require := require.New(t)
+ ctx := context.Background()
+ calls := make([]*workflowservice.ListOpenWorkflowExecutionsRequest, 0, 2)
+ responses := []*workflowservice.ListOpenWorkflowExecutionsResponse{
+ {
+ Executions: []*workflowpb.WorkflowExecutionInfo{
+ {
+ Execution: &commonpb.WorkflowExecution{WorkflowId: "custom-manual-workflow-id-1"},
+ Type: &commonpb.WorkflowType{Name: "workflow.batch_consolidator"},
+ },
+ },
+ NextPageToken: []byte("page-2"),
+ },
+ {
+ Executions: []*workflowpb.WorkflowExecutionInfo{
+ {
+ Execution: &commonpb.WorkflowExecution{WorkflowId: "custom-manual-workflow-id-2"},
+ Type: &commonpb.WorkflowType{Name: "workflow.batch_consolidator"},
+ },
+ },
+ },
+ }
+
+ response, err := listOpenWorkflowExecutions(
+ ctx,
+ "chainstorage-prod",
+ 100,
+ "workflow.batch_consolidator",
+ func(ctx context.Context, request *workflowservice.ListOpenWorkflowExecutionsRequest) (*workflowservice.ListOpenWorkflowExecutionsResponse, error) {
+ calls = append(calls, request)
+ return responses[len(calls)-1], nil
+ },
+ )
+
+ require.NoError(err)
+ require.Len(calls, 2)
+ require.Equal("chainstorage-prod", calls[0].GetNamespace())
+ require.Equal(int32(100), calls[0].GetMaximumPageSize())
+ require.Empty(calls[0].GetNextPageToken())
+ require.Equal("workflow.batch_consolidator", calls[0].GetTypeFilter().GetName())
+ require.Equal([]byte("page-2"), calls[1].GetNextPageToken())
+ require.Equal("workflow.batch_consolidator", calls[1].GetTypeFilter().GetName())
+ require.Len(response.GetExecutions(), 2)
+ require.Equal("custom-manual-workflow-id-1", response.GetExecutions()[0].GetExecution().GetWorkflowId())
+ require.Equal("custom-manual-workflow-id-2", response.GetExecutions()[1].GetExecution().GetWorkflowId())
+ require.Empty(response.GetNextPageToken())
+}
diff --git a/internal/cadence/test_runtime.go b/internal/cadence/test_runtime.go
index dc8c564e..a1fa8b82 100644
--- a/internal/cadence/test_runtime.go
+++ b/internal/cadence/test_runtime.go
@@ -102,7 +102,7 @@ func (r *testRuntime) TerminateWorkflow(ctx context.Context, workflowID string,
return nil
}
-func (t *testRuntime) ListOpenWorkflows(ctx context.Context, namespace string, maxPageSize int32) (*workflowservice.ListOpenWorkflowExecutionsResponse, error) {
+func (t *testRuntime) ListOpenWorkflows(ctx context.Context, namespace string, maxPageSize int32, workflowType string) (*workflowservice.ListOpenWorkflowExecutionsResponse, error) {
return nil, nil
}
diff --git a/internal/config/config.go b/internal/config/config.go
index 6b30220b..0a8926d1 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -40,6 +40,7 @@ type (
SLA SLAConfig `mapstructure:"sla"`
FunctionalTest FunctionalTestConfig `mapstructure:"functional_test"`
StatsD *StatsDConfig `mapstructure:"statsd"`
+ Prometheus *PrometheusConfig `mapstructure:"prometheus"`
namespace string
env Env
@@ -51,9 +52,10 @@ type (
DLQType DLQType `mapstructure:"dlq"`
}
- BlobStorageType int32
- MetaStorageType int32
- DLQType int32
+ BlobStorageType int32
+ MetaStorageType int32
+ DLQType int32
+ ConsolidationMode string
ChainConfig struct {
Blockchain common.Blockchain `mapstructure:"blockchain" validate:"required"`
@@ -69,15 +71,23 @@ type (
IrreversibleDistance uint64 `mapstructure:"irreversible_distance" validate:"required"`
Rosetta RosettaConfig `mapstructure:"rosetta"`
BlockTime time.Duration `mapstructure:"block_time" validate:"required"`
+ // CustomParams stores chain-specific custom parameters as key-value pairs.
+ // For example, Seismic uses "src20_aes_key" for encrypted token transfer parsing.
+ CustomParams map[string]string `mapstructure:"custom_params"`
}
ClientConfig struct {
- Master JSONRPCConfig `mapstructure:"master"`
- Slave JSONRPCConfig `mapstructure:"slave"`
- Validator JSONRPCConfig `mapstructure:"validator"`
- Consensus JSONRPCConfig `mapstructure:"consensus"`
- Retry ClientRetryConfig `mapstructure:"retry"`
- HttpTimeout time.Duration `mapstructure:"http_timeout"`
+ Master JSONRPCConfig `mapstructure:"master"`
+ Slave JSONRPCConfig `mapstructure:"slave"`
+ Validator JSONRPCConfig `mapstructure:"validator"`
+ Consensus JSONRPCConfig `mapstructure:"consensus"`
+ Additional JSONRPCConfig `mapstructure:"additional"`
+ Retry ClientRetryConfig `mapstructure:"retry"`
+ HttpTimeout time.Duration `mapstructure:"http_timeout"`
+ TxBatchSize int `mapstructure:"tx_batch_size"`
+ RpcTimeoutGetBlock time.Duration `mapstructure:"rpc_timeout_get_block"`
+ RpcTimeoutGetRawTx time.Duration `mapstructure:"rpc_timeout_get_raw_tx"`
+ RpcTimeoutGetBlockHash time.Duration `mapstructure:"rpc_timeout_get_block_hash"`
}
JSONRPCConfig struct {
@@ -114,15 +124,33 @@ type (
}
AwsConfig struct {
- Region string `mapstructure:"region" validate:"required"`
- Bucket string `mapstructure:"bucket" validate:"required"`
- DynamoDB DynamoDBConfig `mapstructure:"dynamodb" validate:"required"`
- IsLocalStack bool `mapstructure:"local_stack"`
- IsResetLocal bool `mapstructure:"reset_local"`
- PresignedUrlExpiration time.Duration `mapstructure:"presigned_url_expiration" validate:"required"`
- DLQ SQSConfig `mapstructure:"dlq"`
- Storage StorageConfig `mapstructure:"storage"`
- AWSAccount AWSAccount `mapstructure:"aws_account" validate:"required"`
+ Region string `mapstructure:"region" validate:"required"`
+ Bucket string `mapstructure:"bucket" validate:"required"`
+ Postgres *PostgresConfig `mapstructure:"postgres" validate:"required_without=DynamoDB"`
+ DynamoDB *DynamoDBConfig `mapstructure:"dynamodb" validate:"required_without=Postgres"`
+ IsLocalStack bool `mapstructure:"local_stack"`
+ IsResetLocal bool `mapstructure:"reset_local"`
+ PresignedUrlExpiration time.Duration `mapstructure:"presigned_url_expiration" validate:"required"`
+ DLQ SQSConfig `mapstructure:"dlq"`
+ Storage StorageConfig `mapstructure:"storage"`
+ AWSAccount AWSAccount `mapstructure:"aws_account" validate:"required"`
+ }
+
+ PostgresConfig struct {
+ Host string `mapstructure:"host" validate:"required"`
+ Port int `mapstructure:"port" validate:"required"`
+ Database string `mapstructure:"database" validate:"required"`
+ User string `mapstructure:"user"`
+ Password string `mapstructure:"password"`
+ SSLMode string `mapstructure:"ssl_mode" validate:"required"`
+ MaxConnections int `mapstructure:"max_connections"`
+ MinConnections int `mapstructure:"min_connections"`
+ MaxIdleTime time.Duration `mapstructure:"max_idle_time"`
+ MaxLifetime time.Duration `mapstructure:"max_lifetime"`
+ ConnectTimeout time.Duration `mapstructure:"connect_timeout"`
+ StatementTimeout time.Duration `mapstructure:"statement_timeout"`
+ Schema string `mapstructure:"schema"`
+ TablePrefix string `mapstructure:"table_prefix"`
}
GcpConfig struct {
@@ -149,10 +177,12 @@ type (
}
CadenceConfig struct {
- Address string `mapstructure:"address" validate:"required"`
- Domain string `mapstructure:"domain" validate:"required"`
- RetentionPeriod int32 `mapstructure:"retention_period" validate:"required"`
- TLSConfig CadenceTLSConfig `mapstructure:"tls" validate:"required"`
+ Address string `mapstructure:"address" validate:"required"`
+ Domain string `mapstructure:"domain" validate:"required"`
+ RetentionPeriod int32 `mapstructure:"retention_period" validate:"required"`
+ KeepAliveTime time.Duration `mapstructure:"keep_alive_time"`
+ KeepAliveTimeout time.Duration `mapstructure:"keep_alive_timeout"`
+ TLSConfig CadenceTLSConfig `mapstructure:"tls" validate:"required"`
}
CadenceTLSConfig struct {
@@ -164,19 +194,23 @@ type (
}
WorkflowsConfig struct {
- Workers []WorkerConfig `mapstructure:"workers"`
- Backfiller BackfillerWorkflowConfig `mapstructure:"backfiller"`
- Poller PollerWorkflowConfig `mapstructure:"poller"`
- Benchmarker BenchmarkerWorkflowConfig `mapstructure:"benchmarker"`
- Monitor MonitorWorkflowConfig `mapstructure:"monitor"`
- Streamer StreamerWorkflowConfig `mapstructure:"streamer"`
- CrossValidator CrossValidatorWorkflowConfig `mapstructure:"cross_validator"`
- EventBackfiller EventBackfillerWorkflowConfig `mapstructure:"event_backfiller"`
- Replicator ReplicatorWorkflowConfig `mapstructure:"replicator"`
+ Workers []WorkerConfig `mapstructure:"workers"`
+ Backfiller BackfillerWorkflowConfig `mapstructure:"backfiller"`
+ Poller PollerWorkflowConfig `mapstructure:"poller"`
+ Benchmarker BenchmarkerWorkflowConfig `mapstructure:"benchmarker"`
+ Monitor MonitorWorkflowConfig `mapstructure:"monitor"`
+ Streamer StreamerWorkflowConfig `mapstructure:"streamer"`
+ CrossValidator CrossValidatorWorkflowConfig `mapstructure:"cross_validator"`
+ EventBackfiller EventBackfillerWorkflowConfig `mapstructure:"event_backfiller"`
+ Replicator ReplicatorWorkflowConfig `mapstructure:"replicator"`
+ Migrator MigratorWorkflowConfig `mapstructure:"migrator"`
+ BatchConsolidator BatchConsolidatorWorkflowConfig `mapstructure:"batch_consolidator"`
+ SingleBlockRetention SingleBlockRetentionWorkflowConfig `mapstructure:"single_block_retention"`
}
WorkerConfig struct {
- TaskList string `mapstructure:"task_list"`
+ TaskList string `mapstructure:"task_list"`
+ MaxConcurrentActivityExecutionSize int `mapstructure:"max_concurrent_activity_execution_size"`
}
WorkflowConfig struct {
@@ -281,6 +315,30 @@ type (
BackoffInterval time.Duration `mapstructure:"backoff_interval"`
}
+ MigratorWorkflowConfig struct {
+ WorkflowConfig `mapstructure:",squash"`
+ BatchSize uint64 `mapstructure:"batch_size" validate:"required"`
+ MiniBatchSize uint64 `mapstructure:"mini_batch_size"`
+ CheckpointSize uint64 `mapstructure:"checkpoint_size" validate:"required"`
+ Parallelism int `mapstructure:"parallelism"`
+ BackoffInterval time.Duration `mapstructure:"backoff_interval"`
+ ContinuousSync bool `mapstructure:"continuous_sync"`
+ SyncInterval time.Duration `mapstructure:"sync_interval"`
+ AutoResume bool `mapstructure:"auto_resume"`
+ }
+
+ BatchConsolidatorWorkflowConfig struct {
+ WorkflowConfig `mapstructure:",squash"`
+ BatchSize uint64 `mapstructure:"batch_size" validate:"required"`
+ CheckpointSize uint64 `mapstructure:"checkpoint_size" validate:"required,gtfield=BatchSize"`
+ MaxBlocks uint64 `mapstructure:"max_blocks"`
+ }
+
+ SingleBlockRetentionWorkflowConfig struct {
+ WorkflowConfig `mapstructure:",squash"`
+ MaxObjectRanges int `mapstructure:"max_object_ranges" validate:"required,gt=0,lte=250"`
+ }
+
RosettaConfig struct {
Blockchain string `mapstructure:"blockchain"`
Network string `mapstructure:"network" validate:"required_with=Blockchain"`
@@ -315,6 +373,10 @@ type (
Weight uint8 `json:"weight"`
ExtraUrls map[string]string `json:"extra_urls"`
RPS int `json:"rps"`
+ // RPSCountBatch makes the rate limiter charge one token per call inside
+ // a JSON-RPC batch request, for providers whose rate limit counts each
+ // batched call individually. Has no effect unless RPS is set.
+ RPSCountBatch bool `json:"rps_count_batch"`
}
EndpointConfig struct {
@@ -357,16 +419,54 @@ type (
}
CronConfig struct {
- BlockRangeSize uint64 `mapstructure:"block_range_size" validate:"required"`
- DisableDLQProcessor bool `mapstructure:"disable_dlq_processor"`
- DisablePollingCanary bool `mapstructure:"disable_polling_canary"`
- DisableStreamingCanary bool `mapstructure:"disable_streaming_canary"`
- DisableNodeCanary bool `mapstructure:"disable_node_canary"`
- DisableWorkflowStatus bool `mapstructure:"disable_workflow_status"`
+ BlockRangeSize uint64 `mapstructure:"block_range_size" validate:"required"`
+ BatchConsolidator BatchConsolidatorCronConfig `mapstructure:"batch_consolidator"`
+ DisableDLQProcessor bool `mapstructure:"disable_dlq_processor"`
+ DisablePollingCanary bool `mapstructure:"disable_polling_canary"`
+ DisableStreamingCanary bool `mapstructure:"disable_streaming_canary"`
+ DisableNodeCanary bool `mapstructure:"disable_node_canary"`
+ DisableWorkflowStatus bool `mapstructure:"disable_workflow_status"`
+ }
+
+ BatchConsolidatorCronConfig struct {
+ Enabled bool `mapstructure:"enabled"`
+ Spec string `mapstructure:"spec"`
+ Parallelism int64 `mapstructure:"parallelism"`
+ WorkflowParallelism int `mapstructure:"workflow_parallelism"`
+ DelayStartDuration time.Duration `mapstructure:"delay_start_duration"`
+ StartHeight uint64 `mapstructure:"start_height"`
}
StorageConfig struct {
- DataCompression api.Compression `mapstructure:"data_compression"`
+ DataCompression api.Compression `mapstructure:"data_compression"`
+ Consolidation ConsolidationConfig `mapstructure:"consolidation"`
+ }
+
+ ConsolidationConfig struct {
+ Enabled bool `mapstructure:"enabled"`
+ Mode ConsolidationMode `mapstructure:"mode"`
+ Codec api.Compression `mapstructure:"codec"`
+ CodecLevel int `mapstructure:"codec_level"`
+ ZstdLongDistanceWindowLog *int `mapstructure:"zstd_long_distance_window_log"`
+ MaxCompressedBytes uint64 `mapstructure:"max_compressed_bytes"`
+ MaxUncompressedBytes uint64 `mapstructure:"max_uncompressed_bytes"`
+ MaxBlocks uint64 `mapstructure:"max_blocks"`
+ CompressionChunkBlocks uint64 `mapstructure:"compression_chunk_blocks"`
+ MaxChunkCompressedBytes *uint64 `mapstructure:"max_chunk_compressed_bytes"`
+ MaxChunkUncompressedBytes *uint64 `mapstructure:"max_chunk_uncompressed_bytes"`
+ FlushInterval time.Duration `mapstructure:"flush_interval"`
+ ShadowTimeout time.Duration `mapstructure:"shadow_timeout"`
+ MaxInflightRawBlocks uint64 `mapstructure:"max_inflight_raw_blocks"`
+ MemoryBudgetBytes *uint64 `mapstructure:"memory_budget_bytes"`
+ LocalSpillDir string `mapstructure:"local_spill_dir"`
+ LocalSpillMaxBytes *uint64 `mapstructure:"local_spill_max_bytes"`
+ ShardSize uint64 `mapstructure:"shard_size"`
+ MultipartThreshold uint64 `mapstructure:"multipart_threshold"`
+ ReadShadowFirst bool `mapstructure:"read_shadow_first"`
+ PromotionGateHeight *uint64 `mapstructure:"promotion_gate_height"`
+ SafePromotionLag *uint64 `mapstructure:"safe_promotion_lag"`
+ SingleBlockObjectRetention time.Duration `mapstructure:"single_block_object_retention"`
+ SingleBlockShadowWriteAfterSyncerCutover bool `mapstructure:"single_block_shadow_write_after_syncer_cutover"`
}
SLAConfig struct {
@@ -417,6 +517,24 @@ type (
Prefix string `mapstructure:"prefix"`
}
+ PrometheusConfig struct {
+ // Port is the port to listen on for the metrics server.
+ Port int `mapstructure:"port" validate:"required"`
+ // MetricsPath is the path to listen on for the metrics server.
+ MetricsPath string `mapstructure:"metrics_path"`
+ // Namespace is the namespace for the metrics.
+ Namespace string `mapstructure:"namespace"`
+ // GlobalLabels are labels that are applied to all metrics.
+ GlobalLabels map[string]string `mapstructure:"global_labels"`
+ // DefaultHistogramBuckets are the default buckets for histogram metrics
+ // if not specified in HistogramBuckets.
+ DefaultHistogramBuckets []float64 `mapstructure:"default_histogram_buckets"`
+ // HistogramBuckets are custom buckets for specific histogram metrics.
+ // This allows for more granular control over the histogram buckets on a
+ // per-metric basis.
+ HistogramBuckets map[string][]float64 `mapstructure:"histogram_buckets"`
+ }
+
ConfigOption func(options *configOptions)
Env string
@@ -469,6 +587,7 @@ var (
"UNSPECIFIED": 0,
"DYNAMODB": 1,
"FIRESTORE": 2,
+ "POSTGRES": 3,
}
DLQType_value = map[string]int32{
@@ -505,11 +624,23 @@ const (
MetaStorageType_UNSPECIFIED MetaStorageType = 0
MetaStorageType_DYNAMODB MetaStorageType = 1
MetaStorageType_FIRESTORE MetaStorageType = 2
+ MetaStorageType_POSTGRES MetaStorageType = 3
DLQType_UNSPECIFIED DLQType = 0
DLQType_SQS DLQType = 1
DLQType_FIRESTORE DLQType = 2
+ ConsolidationModeSingleBlockOnly ConsolidationMode = "single_block_only"
+ ConsolidationModeShadowDualWrite ConsolidationMode = "shadow_dual_write"
+ ConsolidationModeAutoConsolidate ConsolidationMode = "auto_consolidate"
+ // Historical backfill is the manual catch-up mode. It remains separate from
+ // auto_consolidate, which is the normal serial cron mode.
+ ConsolidationModeHistoricalBackfill ConsolidationMode = "historical_backfill"
+ ConsolidationModePromoteFinalized ConsolidationMode = "promote_finalized"
+ ConsolidationModeRepairExistingCSCB ConsolidationMode = "repair_existing_cscb"
+ ConsolidationModeSyncerConsolidatedPrimary ConsolidationMode = "syncer_consolidated_primary"
+ DefaultSingleBlockObjectRetention = 72 * time.Hour
+
AWSAccountDevelopment AWSAccount = "development"
AWSAccountProduction AWSAccount = "production"
@@ -523,6 +654,20 @@ const (
tagSidechain = "sidechain"
)
+func (m ConsolidationMode) IsAutoConsolidate() bool {
+ return m == ConsolidationModeAutoConsolidate
+}
+
+func (m ConsolidationMode) IsShadowWrite() bool {
+ return m == ConsolidationModeShadowDualWrite ||
+ m == ConsolidationModeAutoConsolidate ||
+ m == ConsolidationModeHistoricalBackfill
+}
+
+func (m ConsolidationMode) IsRepairExistingCSCB() bool {
+ return m == ConsolidationModeRepairExistingCSCB
+}
+
func New(opts ...ConfigOption) (*Config, error) {
validate := validator.New()
@@ -569,6 +714,34 @@ func New(opts ...ConfigOption) (*Config, error) {
v.SetDefault("aws.local_stack", true)
v.SetDefault("aws.reset_local", true)
}
+ v.SetDefault("chain.client.tx_batch_size", 100)
+ v.SetDefault("cron.batch_consolidator.enabled", false)
+ v.SetDefault("cron.batch_consolidator.spec", "@every 30m")
+ v.SetDefault("cron.batch_consolidator.parallelism", 1)
+ v.SetDefault("cron.batch_consolidator.delay_start_duration", "1m")
+ v.SetDefault("aws.storage.consolidation.enabled", false)
+ v.SetDefault("aws.storage.consolidation.mode", string(ConsolidationModeSingleBlockOnly))
+ v.SetDefault("aws.storage.consolidation.codec", "ZSTD")
+ v.SetDefault("aws.storage.consolidation.codec_level", 6)
+ v.SetDefault("aws.storage.consolidation.max_compressed_bytes", 2147483648)
+ v.SetDefault("aws.storage.consolidation.max_uncompressed_bytes", 137438953472)
+ v.SetDefault("aws.storage.consolidation.max_blocks", 1000)
+ v.SetDefault("aws.storage.consolidation.compression_chunk_blocks", 10)
+ v.SetDefault("aws.storage.consolidation.flush_interval", "1m")
+ v.SetDefault("aws.storage.consolidation.shadow_timeout", "30s")
+ v.SetDefault("aws.storage.consolidation.max_inflight_raw_blocks", 4)
+ v.SetDefault("aws.storage.consolidation.memory_budget_bytes", 268435456)
+ v.SetDefault("aws.storage.consolidation.local_spill_dir", "/tmp/chainstorage-cscb")
+ v.SetDefault("aws.storage.consolidation.shard_size", 10000)
+ v.SetDefault("aws.storage.consolidation.multipart_threshold", 134217728)
+ v.SetDefault("aws.storage.consolidation.read_shadow_first", false)
+ v.SetDefault("aws.storage.consolidation.single_block_object_retention", DefaultSingleBlockObjectRetention.String())
+ if err := v.BindEnv("aws.storage.consolidation.promotion_gate_height"); err != nil {
+ return nil, xerrors.Errorf("failed to bind promotion_gate_height env: %w", err)
+ }
+ if err := v.BindEnv("aws.storage.consolidation.safe_promotion_lag"); err != nil {
+ return nil, xerrors.Errorf("failed to bind safe_promotion_lag env: %w", err)
+ }
// Read the data in base.yml
if err := v.ReadConfig(configReader); err != nil {
@@ -584,6 +757,7 @@ func New(opts ...ConfigOption) (*Config, error) {
if err := mergeInConfig(v, configOpts, envSecrets); err != nil {
return nil, xerrors.Errorf("failed to merge in %v config: %w", envSecrets, err)
}
+ applyDeprecatedConsolidationAliases(v)
if err := v.Unmarshal(&cfg, viper.DecodeHook(mapstructure.ComposeDecodeHookFunc(
mapstructure.TextUnmarshallerHookFunc(),
@@ -599,12 +773,18 @@ func New(opts ...ConfigOption) (*Config, error) {
))); err != nil {
return nil, xerrors.Errorf("failed to unmarshal config: %w", err)
}
+ if cfg.AWS.Storage.Consolidation.Mode == deprecatedConsolidationModeBeforeSingleBlockRename {
+ cfg.AWS.Storage.Consolidation.Mode = ConsolidationModeSingleBlockOnly
+ }
cfg.setDerivedConfigs(reflect.ValueOf(&cfg))
if err := validate.Struct(&cfg); err != nil {
return nil, xerrors.Errorf("failed to validate config: %w", err)
}
+ if err := cfg.validateConsolidationConfig(); err != nil {
+ return nil, xerrors.Errorf("failed to validate consolidation config: %w", err)
+ }
if cfg.Chain.Blockchain != common.Blockchain_BLOCKCHAIN_ETHEREUM || cfg.Chain.Network != common.Network_NETWORK_ETHEREUM_MAINNET {
// Zero-value blockTag is reserved as an alias of the stable blockTag.
@@ -621,6 +801,135 @@ func New(opts ...ConfigOption) (*Config, error) {
return &cfg, nil
}
+const (
+ consolidationSingleBlockObjectRetentionKey = "aws.storage.consolidation.single_block_object_retention"
+ consolidationSingleBlockShadowWriteAfterSyncerCutoverKey = "aws.storage.consolidation.single_block_shadow_write_after_syncer_cutover"
+
+ // Deprecated aliases are accepted only at the configuration boundary so a
+ // rolling deployment can consume the previous production configuration.
+ deprecatedConsolidationModeBeforeSingleBlockRename ConsolidationMode = "legacy_only"
+ deprecatedConsolidationObjectRetentionKeyBeforeSingleBlockRename = "aws.storage.consolidation.legacy_object_retention"
+ deprecatedConsolidationShadowWriteKeyBeforeSingleBlockRename = "aws.storage.consolidation.legacy_shadow_write_after_syncer_cutover"
+)
+
+func applyDeprecatedConsolidationAliases(v *viper.Viper) {
+ applyDeprecatedConfigAlias(
+ v,
+ consolidationSingleBlockObjectRetentionKey,
+ deprecatedConsolidationObjectRetentionKeyBeforeSingleBlockRename,
+ )
+ applyDeprecatedConfigAlias(
+ v,
+ consolidationSingleBlockShadowWriteAfterSyncerCutoverKey,
+ deprecatedConsolidationShadowWriteKeyBeforeSingleBlockRename,
+ )
+}
+
+func applyDeprecatedConfigAlias(v *viper.Viper, canonicalKey string, deprecatedKey string) {
+ canonicalEnv := "CHAINSTORAGE_" + strings.ToUpper(strings.ReplaceAll(canonicalKey, ".", "_"))
+ if _, ok := os.LookupEnv(canonicalEnv); ok {
+ return
+ }
+
+ deprecatedEnv := "CHAINSTORAGE_" + strings.ToUpper(strings.ReplaceAll(deprecatedKey, ".", "_"))
+ if value, ok := os.LookupEnv(deprecatedEnv); ok {
+ v.Set(canonicalKey, value)
+ return
+ }
+
+ if !v.InConfig(canonicalKey) && v.InConfig(deprecatedKey) {
+ v.Set(canonicalKey, v.Get(deprecatedKey))
+ }
+}
+
+func (c *Config) validateConsolidationConfig() error {
+ consolidation := c.AWS.Storage.Consolidation
+ switch consolidation.Mode {
+ case ConsolidationModeSingleBlockOnly,
+ ConsolidationModeShadowDualWrite,
+ ConsolidationModeAutoConsolidate,
+ ConsolidationModeHistoricalBackfill,
+ ConsolidationModePromoteFinalized,
+ ConsolidationModeSyncerConsolidatedPrimary:
+ default:
+ return xerrors.Errorf("invalid consolidation mode %q", consolidation.Mode)
+ }
+
+ switch consolidation.Codec {
+ case api.Compression_GZIP, api.Compression_ZSTD:
+ default:
+ return xerrors.Errorf("invalid consolidation codec %v", consolidation.Codec)
+ }
+
+ if !consolidation.Enabled && !consolidation.ReadShadowFirst {
+ return nil
+ }
+
+ switch c.StorageType.BlobStorageType {
+ case BlobStorageType_UNSPECIFIED, BlobStorageType_S3:
+ default:
+ return xerrors.Errorf("consolidation requires S3 blob storage, got %v", c.StorageType.BlobStorageType)
+ }
+ if c.StorageType.MetaStorageType != MetaStorageType_POSTGRES {
+ return xerrors.Errorf("consolidation requires Postgres meta storage until DynamoDB shadow placement and guarded promotion are implemented, got %v", c.StorageType.MetaStorageType)
+ }
+
+ if !consolidation.Enabled {
+ return nil
+ }
+
+ if consolidation.CodecLevel <= 0 {
+ return xerrors.New("consolidation codec_level must be positive")
+ }
+ if consolidation.MaxCompressedBytes == 0 {
+ return xerrors.New("consolidation max_compressed_bytes must be positive")
+ }
+ if consolidation.MaxUncompressedBytes == 0 {
+ return xerrors.New("consolidation max_uncompressed_bytes must be positive")
+ }
+ if consolidation.MaxBlocks == 0 {
+ return xerrors.New("consolidation max_blocks must be positive")
+ }
+ if consolidation.CompressionChunkBlocks == 0 {
+ return xerrors.New("consolidation compression_chunk_blocks must be positive")
+ }
+ if consolidation.ShardSize == 0 {
+ return xerrors.New("consolidation shard_size must be positive")
+ }
+ if consolidation.MultipartThreshold == 0 {
+ return xerrors.New("consolidation multipart_threshold must be positive")
+ }
+ if consolidation.MaxInflightRawBlocks == 0 {
+ return xerrors.New("consolidation max_inflight_raw_blocks must be positive")
+ }
+ if consolidation.MemoryBudgetBytes != nil && *consolidation.MemoryBudgetBytes == 0 {
+ return xerrors.New("consolidation memory_budget_bytes must be positive when set")
+ }
+ if consolidation.LocalSpillMaxBytes != nil && *consolidation.LocalSpillMaxBytes == 0 {
+ return xerrors.New("consolidation local_spill_max_bytes must be positive when set")
+ }
+ if consolidation.FlushInterval <= 0 {
+ return xerrors.New("consolidation flush_interval must be positive")
+ }
+ if consolidation.ShadowTimeout <= 0 {
+ return xerrors.New("consolidation shadow_timeout must be positive")
+ }
+ if consolidation.SingleBlockObjectRetention <= 0 {
+ return xerrors.New("consolidation single_block_object_retention must be positive")
+ }
+ if consolidation.Mode == ConsolidationModePromoteFinalized && consolidation.SafePromotionLag == nil {
+ return xerrors.New("consolidation promote_finalized requires safe_promotion_lag")
+ }
+ if consolidation.SafePromotionLag != nil && *consolidation.SafePromotionLag < c.Chain.IrreversibleDistance {
+ return xerrors.Errorf(
+ "consolidation safe_promotion_lag(%d) must be at least irreversible_distance(%d)",
+ *consolidation.SafePromotionLag,
+ c.Chain.IrreversibleDistance,
+ )
+ }
+ return nil
+}
+
func GetEnv() Env {
awsAccount := AWSAccount(os.Getenv(EnvVarEnvironment))
env, ok := AWSAccountEnvMap[awsAccount]
@@ -1045,7 +1354,11 @@ func stringToCompressionHookFunc() mapstructure.DecodeHookFunc {
return data, nil
}
- return api.Compression_value[data.(string)], nil
+ v, ok := api.Compression_value[strings.ToUpper(data.(string))]
+ if !ok {
+ return nil, xerrors.Errorf("invalid compression type: %v", data)
+ }
+ return v, nil
}
}
diff --git a/internal/config/config_test.go b/internal/config/config_test.go
index a189f59a..5873576e 100644
--- a/internal/config/config_test.go
+++ b/internal/config/config_test.go
@@ -8,6 +8,8 @@ import (
"testing"
"time"
+ "github.com/go-playground/validator/v10"
+
"github.com/coinbase/chainstorage/internal/config"
"github.com/coinbase/chainstorage/internal/utils/fixtures"
"github.com/coinbase/chainstorage/internal/utils/testapp"
@@ -123,6 +125,8 @@ func TestValidateConfigs(t *testing.T) {
require.GreaterOrEqual(cfg.Chain.EventTag.Latest, cfg.Chain.EventTag.Stable)
require.Equal(api.Compression_GZIP, cfg.AWS.Storage.DataCompression)
+ require.Equal(expectedConsolidationConfig(cfg), cfg.AWS.Storage.Consolidation)
+ require.Equal(5*time.Minute, cfg.Workflows.BatchConsolidator.ActivityHeartbeatTimeout)
require.Equal(fmt.Sprintf("chainstorage-%v", normalizedConfigName), cfg.Cadence.Domain)
@@ -138,6 +142,23 @@ func TestValidateConfigs(t *testing.T) {
})
}
+func TestSolanaProductionCadenceKeepAliveConfig(t *testing.T) {
+ require := testutil.Require(t)
+
+ cfg, err := config.New(
+ config.WithEnvironment(config.EnvProduction),
+ config.WithBlockchain(common.Blockchain_BLOCKCHAIN_SOLANA),
+ config.WithNetwork(common.Network_NETWORK_SOLANA_MAINNET),
+ )
+
+ require.NoError(err)
+ require.Equal(10*time.Second, cfg.Cadence.KeepAliveTime)
+ require.Equal(30*time.Second, cfg.Cadence.KeepAliveTimeout)
+ require.True(cfg.AWS.Storage.Consolidation.ReadShadowFirst)
+ require.Equal(config.BlobStorageType_S3, cfg.StorageType.BlobStorageType)
+ require.Equal(config.MetaStorageType_POSTGRES, cfg.StorageType.MetaStorageType)
+}
+
func TestDerivedConfigValues(t *testing.T) {
testapp.TestAllConfigs(t, func(t *testing.T, cfg *config.Config) {
require := testutil.Require(t)
@@ -145,23 +166,49 @@ func TestDerivedConfigValues(t *testing.T) {
normalizedConfigName := strings.ReplaceAll(configName, "_", "-")
// Verify template derived configs.
- dynamoDB := config.DynamoDBConfig{
- BlockTable: fmt.Sprintf("example_chainstorage_blocks_%v", configName),
- EventTable: cfg.AWS.DynamoDB.EventTable,
- EventTableHeightIndex: cfg.AWS.DynamoDB.EventTableHeightIndex,
- VersionedEventTable: fmt.Sprintf("example_chainstorage_versioned_block_events_%v", configName),
- VersionedEventTableBlockIndex: fmt.Sprintf("example_chainstorage_versioned_block_events_by_block_id_%v", configName),
- TransactionTable: cfg.AWS.DynamoDB.TransactionTable,
- // Skip DynamoDB.Arn verification
- Arn: "",
+ var dynamoDBPtr *config.DynamoDBConfig
+ if cfg.AWS.DynamoDB != nil {
+ dynamoDB := config.DynamoDBConfig{
+ BlockTable: fmt.Sprintf("example_chainstorage_blocks_%v", configName),
+ EventTable: cfg.AWS.DynamoDB.EventTable,
+ EventTableHeightIndex: cfg.AWS.DynamoDB.EventTableHeightIndex,
+ VersionedEventTable: fmt.Sprintf("example_chainstorage_versioned_block_events_%v", configName),
+ VersionedEventTableBlockIndex: fmt.Sprintf("example_chainstorage_versioned_block_events_by_block_id_%v", configName),
+ TransactionTable: cfg.AWS.DynamoDB.TransactionTable,
+ // Skip DynamoDB.Arn verification
+ Arn: "",
+ }
+ dynamoDBPtr = &dynamoDB
+ }
+
+ var postgresPtr *config.PostgresConfig
+ if cfg.AWS.Postgres != nil {
+ postgres := config.PostgresConfig{
+ Host: cfg.AWS.Postgres.Host,
+ Port: cfg.AWS.Postgres.Port,
+ Database: cfg.AWS.Postgres.Database,
+ User: cfg.AWS.Postgres.User,
+ Password: cfg.AWS.Postgres.Password,
+ SSLMode: cfg.AWS.Postgres.SSLMode,
+ MaxConnections: cfg.AWS.Postgres.MaxConnections,
+ MinConnections: cfg.AWS.Postgres.MinConnections,
+ MaxIdleTime: cfg.AWS.Postgres.MaxIdleTime,
+ MaxLifetime: cfg.AWS.Postgres.MaxLifetime,
+ ConnectTimeout: cfg.AWS.Postgres.ConnectTimeout,
+ StatementTimeout: cfg.AWS.Postgres.StatementTimeout,
+ Schema: cfg.AWS.Postgres.Schema,
+ TablePrefix: cfg.AWS.Postgres.TablePrefix,
+ }
+ postgresPtr = &postgres
}
expectedAWS := config.AwsConfig{
Region: "us-east-1",
Bucket: fmt.Sprintf("example-chainstorage-%v-%v", normalizedConfigName, cfg.AwsEnv()),
- DynamoDB: dynamoDB,
- IsLocalStack: true,
- IsResetLocal: true,
+ DynamoDB: dynamoDBPtr,
+ Postgres: postgresPtr,
+ IsLocalStack: cfg.AWS.IsLocalStack,
+ IsResetLocal: cfg.AWS.IsResetLocal,
PresignedUrlExpiration: 30 * time.Minute,
DLQ: config.SQSConfig{
Name: fmt.Sprintf("example_chainstorage_blocks_%v_dlq", configName),
@@ -171,14 +218,26 @@ func TestDerivedConfigValues(t *testing.T) {
},
Storage: config.StorageConfig{
DataCompression: getDataCompressionType(cfg),
+ Consolidation: defaultConsolidationConfig(),
},
// Skip AWS account verification
AWSAccount: cfg.AWS.AWSAccount,
}
+ expectedAWS.Storage.Consolidation = expectedConsolidationConfig(cfg)
require.Equal(expectedAWS, cfg.AWS)
})
}
+func expectedConsolidationConfig(cfg *config.Config) config.ConsolidationConfig {
+ consolidation := defaultConsolidationConfig()
+ if cfg.Env() == config.EnvProduction &&
+ cfg.Chain.Blockchain == common.Blockchain_BLOCKCHAIN_SOLANA &&
+ cfg.Chain.Network == common.Network_NETWORK_SOLANA_MAINNET {
+ consolidation.ReadShadowFirst = true
+ }
+ return consolidation
+}
+
func TestAWSAccountEnvParsing(t *testing.T) {
for _, testConfig := range testapp.TestConfigs {
namespace := testConfig.Namespace
@@ -573,6 +632,45 @@ func TestEndpointGroupWithExtraUrls(t *testing.T) {
require.Equal(expectedEndpointGroup, endpointGroup)
}
+func TestEndpointGroupWithRPSCountBatch(t *testing.T) {
+ require := testutil.Require(t)
+
+ text := `
+ {
+ "endpoints": [
+ {
+ "name": "quicknode",
+ "url": "testUrl",
+ "weight": 1,
+ "rps": 300,
+ "rps_count_batch": true
+ }
+ ]
+ }`
+
+ var endpointGroup config.EndpointGroup
+ require.NoError(endpointGroup.UnmarshalText([]byte(text)))
+ require.Equal(300, endpointGroup.Endpoints[0].RPS)
+ require.True(endpointGroup.Endpoints[0].RPSCountBatch)
+
+ // Defaults to false when unset.
+ text = `
+ {
+ "endpoints": [
+ {
+ "name": "quicknode",
+ "url": "testUrl",
+ "weight": 1,
+ "rps": 300
+ }
+ ]
+ }`
+
+ var endpointGroupDefault config.EndpointGroup
+ require.NoError(endpointGroupDefault.UnmarshalText([]byte(text)))
+ require.False(endpointGroupDefault.Endpoints[0].RPSCountBatch)
+}
+
func TestEndpointGroup(t *testing.T) {
tests := []struct {
fixture string
@@ -820,6 +918,171 @@ func TestEndpointGroup_Error(t *testing.T) {
}
}
+func TestConsolidationEnabledRejectsDynamoDB(t *testing.T) {
+ require := testutil.Require(t)
+
+ require.NoError(os.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_ENABLED", "true"))
+ require.NoError(os.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_MODE", string(config.ConsolidationModeShadowDualWrite)))
+ defer os.Unsetenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_ENABLED")
+ defer os.Unsetenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_MODE")
+
+ _, err := config.New()
+ require.Error(err)
+ require.Contains(err.Error(), "requires Postgres meta storage")
+}
+
+func TestConsolidationReadShadowFirstRejectsDynamoDB(t *testing.T) {
+ require := testutil.Require(t)
+
+ require.NoError(os.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_READ_SHADOW_FIRST", "true"))
+ defer os.Unsetenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_READ_SHADOW_FIRST")
+
+ _, err := config.New()
+ require.Error(err)
+ require.Contains(err.Error(), "requires Postgres meta storage")
+}
+
+func TestConsolidationAutoConsolidateModeAccepted(t *testing.T) {
+ require := testutil.Require(t)
+
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_MODE", string(config.ConsolidationModeAutoConsolidate))
+
+ cfg, err := config.New()
+ require.NoError(err)
+ require.Equal(config.ConsolidationModeAutoConsolidate, cfg.AWS.Storage.Consolidation.Mode)
+}
+
+func TestConsolidationHistoricalBackfillModeAcceptedAsDeprecatedAlias(t *testing.T) {
+ require := testutil.Require(t)
+
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_MODE", string(config.ConsolidationModeHistoricalBackfill))
+
+ cfg, err := config.New()
+ require.NoError(err)
+ require.Equal(config.ConsolidationModeHistoricalBackfill, cfg.AWS.Storage.Consolidation.Mode)
+}
+
+func TestConsolidationSingleBlockObjectRetentionConfig(t *testing.T) {
+ require := testutil.Require(t)
+
+ cfg, err := config.New()
+ require.NoError(err)
+ require.Equal(config.DefaultSingleBlockObjectRetention, cfg.AWS.Storage.Consolidation.SingleBlockObjectRetention)
+
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_SINGLE_BLOCK_OBJECT_RETENTION", "24h")
+ cfg, err = config.New()
+ require.NoError(err)
+ require.Equal(24*time.Hour, cfg.AWS.Storage.Consolidation.SingleBlockObjectRetention)
+}
+
+func TestDeprecatedConsolidationConfigAliases(t *testing.T) {
+ require := testutil.Require(t)
+
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_MODE", "legacy_only")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_LEGACY_OBJECT_RETENTION", "24h")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_LEGACY_SHADOW_WRITE_AFTER_SYNCER_CUTOVER", "true")
+
+ cfg, err := config.New()
+ require.NoError(err)
+ require.Equal(config.ConsolidationModeSingleBlockOnly, cfg.AWS.Storage.Consolidation.Mode)
+ require.Equal(24*time.Hour, cfg.AWS.Storage.Consolidation.SingleBlockObjectRetention)
+ require.True(cfg.AWS.Storage.Consolidation.SingleBlockShadowWriteAfterSyncerCutover)
+}
+
+func TestCanonicalConsolidationConfigTakesPrecedenceOverDeprecatedAliases(t *testing.T) {
+ require := testutil.Require(t)
+
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_SINGLE_BLOCK_OBJECT_RETENTION", "48h")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_LEGACY_OBJECT_RETENTION", "24h")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_SINGLE_BLOCK_SHADOW_WRITE_AFTER_SYNCER_CUTOVER", "false")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_LEGACY_SHADOW_WRITE_AFTER_SYNCER_CUTOVER", "true")
+
+ cfg, err := config.New()
+ require.NoError(err)
+ require.Equal(48*time.Hour, cfg.AWS.Storage.Consolidation.SingleBlockObjectRetention)
+ require.False(cfg.AWS.Storage.Consolidation.SingleBlockShadowWriteAfterSyncerCutover)
+}
+
+func TestConsolidationSingleBlockObjectRetentionMustBePositiveWhenEnabled(t *testing.T) {
+ require := testutil.Require(t)
+
+ t.Setenv("CHAINSTORAGE_STORAGE_TYPE_META", "POSTGRES")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_ENABLED", "true")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_MODE", string(config.ConsolidationModeAutoConsolidate))
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_SINGLE_BLOCK_OBJECT_RETENTION", "0s")
+
+ _, err := config.New()
+ require.Error(err)
+ require.Contains(err.Error(), "single_block_object_retention must be positive")
+}
+
+func TestConsolidationPromoteFinalizedRequiresSafePromotionLag(t *testing.T) {
+ tests := []struct {
+ name string
+ env map[string]string
+ expectedErr string
+ }{
+ {
+ name: "missing safe promotion lag",
+ env: map[string]string{
+ "CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_PROMOTION_GATE_HEIGHT": "100000",
+ },
+ expectedErr: "requires safe_promotion_lag",
+ },
+ {
+ name: "safe promotion lag below irreversible distance",
+ env: map[string]string{
+ "CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_PROMOTION_GATE_HEIGHT": "100000",
+ "CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_SAFE_PROMOTION_LAG": "1",
+ },
+ expectedErr: "must be at least irreversible_distance",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ require := testutil.Require(t)
+ t.Setenv("CHAINSTORAGE_STORAGE_TYPE_META", "POSTGRES")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_ENABLED", "true")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_MODE", string(config.ConsolidationModePromoteFinalized))
+ for k, v := range test.env {
+ t.Setenv(k, v)
+ }
+
+ _, err := config.New()
+ require.Error(err)
+ require.Contains(err.Error(), test.expectedErr)
+ })
+ }
+}
+
+func TestConsolidationPromoteFinalizedAllowsMissingPromotionGate(t *testing.T) {
+ require := testutil.Require(t)
+
+ t.Setenv("CHAINSTORAGE_STORAGE_TYPE_META", "POSTGRES")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_ENABLED", "true")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_MODE", string(config.ConsolidationModePromoteFinalized))
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_SAFE_PROMOTION_LAG", "100000")
+
+ cfg, err := config.New()
+ require.NoError(err)
+ require.Nil(cfg.AWS.Storage.Consolidation.PromotionGateHeight)
+ require.NotNil(cfg.AWS.Storage.Consolidation.SafePromotionLag)
+}
+
+func TestConsolidationPromoteFinalizedRejectsDynamoDB(t *testing.T) {
+ require := testutil.Require(t)
+
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_ENABLED", "true")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_MODE", string(config.ConsolidationModePromoteFinalized))
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_PROMOTION_GATE_HEIGHT", "100000")
+ t.Setenv("CHAINSTORAGE_AWS_STORAGE_CONSOLIDATION_SAFE_PROMOTION_LAG", "100000")
+
+ _, err := config.New()
+ require.Error(err)
+ require.Contains(err.Error(), "requires Postgres meta storage")
+}
+
func TestParseConfigName(t *testing.T) {
tests := []struct {
configName string
@@ -907,6 +1170,29 @@ func getDataCompressionType(cfg *config.Config) api.Compression {
return api.Compression_GZIP
}
+func defaultConsolidationConfig() config.ConsolidationConfig {
+ memoryBudgetBytes := uint64(268435456)
+ return config.ConsolidationConfig{
+ Enabled: false,
+ Mode: config.ConsolidationModeSingleBlockOnly,
+ Codec: api.Compression_ZSTD,
+ CodecLevel: 6,
+ MaxCompressedBytes: 2147483648,
+ MaxUncompressedBytes: 137438953472,
+ MaxBlocks: 1000,
+ CompressionChunkBlocks: 10,
+ FlushInterval: time.Minute,
+ ShadowTimeout: 30 * time.Second,
+ MaxInflightRawBlocks: 4,
+ MemoryBudgetBytes: &memoryBudgetBytes,
+ LocalSpillDir: "/tmp/chainstorage-cscb",
+ ShardSize: 10000,
+ MultipartThreshold: 134217728,
+ ReadShadowFirst: false,
+ SingleBlockObjectRetention: config.DefaultSingleBlockObjectRetention,
+ }
+}
+
func TestUseFailoverEndpoints(t *testing.T) {
testapp.TestAllConfigs(t, func(t *testing.T, cfg *config.Config) {
require := testutil.Require(t)
@@ -1069,3 +1355,97 @@ func TestDefaultHttpTimeout(t *testing.T) {
require.NoError(err)
require.Equal(0*time.Second, cfg.Chain.Client.HttpTimeout)
}
+
+func TestDefaultTxBatchSize(t *testing.T) {
+ require := testutil.Require(t)
+
+ cfg, err := config.New()
+ require.NoError(err)
+ require.Equal(100, cfg.Chain.Client.TxBatchSize)
+}
+
+func TestValidateAWSstorageConfig(t *testing.T) {
+ // Test that Postgres validation is skipped when Postgres is nil
+ require := testutil.Require(t)
+
+ cfg, err := config.New()
+ require.NoError(err)
+
+ // Test 1: DynamoDB config when Postgres is nil
+ cfg.AWS.Postgres = nil
+ cfg.AWS.DynamoDB = &config.DynamoDBConfig{
+ BlockTable: "block_table",
+ VersionedEventTable: "versioned_event_table",
+ VersionedEventTableBlockIndex: "versioned_event_table_block_index",
+ }
+
+ // Validate the config - this should NOT fail even though Postgres is nil
+ // and its fields (Host, Port, Database, SSLMode) are marked as required
+ validate := validator.New()
+ err = validate.Struct(&cfg.AWS)
+ require.NoError(err, "Validation should not fail when Postgres is nil")
+
+ require.Equal("block_table", cfg.AWS.DynamoDB.BlockTable)
+ require.Equal("versioned_event_table", cfg.AWS.DynamoDB.VersionedEventTable)
+ require.Equal("versioned_event_table_block_index", cfg.AWS.DynamoDB.VersionedEventTableBlockIndex)
+ cfg.AWS.DynamoDB = &config.DynamoDBConfig{
+ BlockTable: "block_table",
+ VersionedEventTable: "versioned_event_table",
+ VersionedEventTableBlockIndex: "versioned_event_table_block_index",
+ }
+ require.Equal("block_table", cfg.AWS.DynamoDB.BlockTable)
+ require.Equal("versioned_event_table", cfg.AWS.DynamoDB.VersionedEventTable)
+ require.Equal("versioned_event_table_block_index", cfg.AWS.DynamoDB.VersionedEventTableBlockIndex)
+ // Test 2: Postgres config when DynamoDB is nil - with valid config
+ cfg.AWS.DynamoDB = nil
+ cfg.AWS.Postgres = &config.PostgresConfig{
+ Host: "localhost",
+ Port: 5432,
+ Database: "chainstorage",
+ SSLMode: "disable",
+ }
+ err = validate.Struct(&cfg.AWS)
+ require.NoError(err, "Validation should pass with valid Postgres config")
+ require.Equal("localhost", cfg.AWS.Postgres.Host)
+ require.Equal(5432, cfg.AWS.Postgres.Port)
+ require.Equal("chainstorage", cfg.AWS.Postgres.Database)
+ require.Equal("disable", cfg.AWS.Postgres.SSLMode)
+
+ // Test 3: Postgres config with missing required fields should fail validation
+ cfg.AWS.DynamoDB = nil
+ cfg.AWS.Postgres = &config.PostgresConfig{
+ Host: "localhost",
+ Port: 5432,
+ // Missing Database and SSLMode which are required
+ }
+ err = validate.Struct(&cfg.AWS)
+ require.Error(err, "Validation should fail when Postgres is not nil but has missing required fields")
+ // Test 4: Both configs valid when both are not nil
+ cfg.AWS.DynamoDB = &config.DynamoDBConfig{
+ BlockTable: "block_table",
+ VersionedEventTable: "versioned_event_table",
+ VersionedEventTableBlockIndex: "versioned_event_table_block_index",
+ }
+ cfg.AWS.Postgres = &config.PostgresConfig{
+ Host: "localhost",
+ Port: 5432,
+ Database: "chainstorage",
+ SSLMode: "disable",
+ }
+ err = validate.Struct(&cfg.AWS)
+ require.NoError(err, "Validation should pass when both configs are valid")
+ require.Equal("block_table", cfg.AWS.DynamoDB.BlockTable)
+ require.Equal("versioned_event_table", cfg.AWS.DynamoDB.VersionedEventTable)
+ require.Equal("versioned_event_table_block_index", cfg.AWS.DynamoDB.VersionedEventTableBlockIndex)
+ require.Equal("localhost", cfg.AWS.Postgres.Host)
+ require.Equal(5432, cfg.AWS.Postgres.Port)
+ require.Equal("chainstorage", cfg.AWS.Postgres.Database)
+ require.Equal("disable", cfg.AWS.Postgres.SSLMode)
+
+ // Test 5: Both configs nil - validation should FAIL
+ // because at least one storage backend is required (required_without validation)
+ cfg.AWS.DynamoDB = nil
+ cfg.AWS.Postgres = nil
+ err = validate.Struct(&cfg.AWS)
+ require.Error(err, "Validation should fail when both configs are nil - at least one storage backend is required")
+}
diff --git a/internal/config/worker_config_test.go b/internal/config/worker_config_test.go
new file mode 100644
index 00000000..212db6ed
--- /dev/null
+++ b/internal/config/worker_config_test.go
@@ -0,0 +1,30 @@
+package config
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/spf13/viper"
+ "github.com/stretchr/testify/require"
+)
+
+func TestWorkerConfigDecodesActivityConcurrencyLimit(t *testing.T) {
+ require := require.New(t)
+
+ v := viper.New()
+ v.SetConfigType("yaml")
+ err := v.ReadConfig(strings.NewReader(`
+workflows:
+ workers:
+ - task_list: batch_consolidator
+ max_concurrent_activity_execution_size: 1
+`))
+ require.NoError(err)
+
+ var cfg Config
+ err = v.Unmarshal(&cfg)
+ require.NoError(err)
+ require.Len(cfg.Workflows.Workers, 1)
+ require.Equal("batch_consolidator", cfg.Workflows.Workers[0].TaskList)
+ require.Equal(1, cfg.Workflows.Workers[0].MaxConcurrentActivityExecutionSize)
+}
diff --git a/internal/cron/batch_consolidator.go b/internal/cron/batch_consolidator.go
new file mode 100644
index 00000000..bbadbd1b
--- /dev/null
+++ b/internal/cron/batch_consolidator.go
@@ -0,0 +1,458 @@
+package cron
+
+import (
+ "context"
+ "fmt"
+ "strings"
+ "time"
+
+ "go.temporal.io/api/serviceerror"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ "github.com/coinbase/chainstorage/internal/utils/log"
+ "github.com/coinbase/chainstorage/internal/workflow"
+)
+
+type (
+ BatchConsolidatorTaskParams struct {
+ fx.In
+ fxparams.Params
+ Config *config.Config
+ Runtime cadence.Runtime
+ MetaStorage metastorage.MetaStorage
+ BatchConsolidator *workflow.BatchConsolidator
+ }
+
+ batchConsolidatorTask struct {
+ config *config.Config
+ logger *zap.Logger
+ runtime cadence.Runtime
+ metaStorage metastorage.MetaStorage
+ batchConsolidator *workflow.BatchConsolidator
+ }
+)
+
+const (
+ autoConsolidateSuffix = "auto_consolidate"
+ batchConsolidatorOpenPageSize = 1000
+ defaultBatchConsolidatorCronSpec = "@every 30m"
+ maxBatchConsolidatorWorkflowParallelism = 20
+)
+
+func NewBatchConsolidator(params BatchConsolidatorTaskParams) (Task, error) {
+ return &batchConsolidatorTask{
+ config: params.Config,
+ logger: log.WithPackage(params.Logger),
+ runtime: params.Runtime,
+ metaStorage: params.MetaStorage,
+ batchConsolidator: params.BatchConsolidator,
+ }, nil
+}
+
+func (t *batchConsolidatorTask) Name() string {
+ return "batch_consolidator"
+}
+
+func (t *batchConsolidatorTask) Spec() string {
+ spec := t.config.Cron.BatchConsolidator.Spec
+ if spec == "" {
+ return defaultBatchConsolidatorCronSpec
+ }
+ return spec
+}
+
+func (t *batchConsolidatorTask) Parallelism() int64 {
+ parallelism := t.config.Cron.BatchConsolidator.Parallelism
+ if parallelism <= 0 {
+ return 1
+ }
+ return parallelism
+}
+
+func (t *batchConsolidatorTask) Enabled() bool {
+ return t.config.Cron.BatchConsolidator.Enabled
+}
+
+func (t *batchConsolidatorTask) DelayStartDuration() time.Duration {
+ return t.config.Cron.BatchConsolidator.DelayStartDuration
+}
+
+func (t *batchConsolidatorTask) Run(ctx context.Context) error {
+ consolidation := t.config.AWS.Storage.Consolidation
+ if !consolidation.Enabled {
+ return xerrors.New("batch_consolidator cron requires aws.storage.consolidation.enabled=true")
+ }
+ if consolidation.Mode != config.ConsolidationModeAutoConsolidate {
+ return xerrors.Errorf("batch_consolidator cron requires consolidation mode %q, got %q", config.ConsolidationModeAutoConsolidate, consolidation.Mode)
+ }
+ if consolidation.MaxBlocks == 0 {
+ return xerrors.New("batch_consolidator cron requires aws.storage.consolidation.max_blocks to be positive")
+ }
+ if consolidation.ShardSize == 0 {
+ return xerrors.New("batch_consolidator cron requires aws.storage.consolidation.shard_size to be positive")
+ }
+ if consolidation.MaxBlocks > consolidation.ShardSize {
+ return xerrors.Errorf(
+ "batch_consolidator cron consolidation max_blocks(%d) must be <= shard_size(%d)",
+ consolidation.MaxBlocks,
+ consolidation.ShardSize,
+ )
+ }
+
+ cronConfig := t.config.Cron.BatchConsolidator
+ if cronConfig.WorkflowParallelism < 0 {
+ return xerrors.Errorf("batch_consolidator cron workflow_parallelism(%d) must be non-negative", cronConfig.WorkflowParallelism)
+ }
+ if cronConfig.WorkflowParallelism > maxBatchConsolidatorWorkflowParallelism {
+ return xerrors.Errorf(
+ "batch_consolidator cron workflow_parallelism(%d) exceeds max(%d)",
+ cronConfig.WorkflowParallelism,
+ maxBatchConsolidatorWorkflowParallelism,
+ )
+ }
+
+ workflowID := t.autoConsolidateWorkflowID()
+ openWorkflowID, open, err := t.openBatchConsolidatorWorkflow(ctx)
+ if err != nil {
+ return err
+ }
+ if open {
+ t.logger.Info(
+ "batch_consolidator cron skipped because a batch_consolidator workflow is already open",
+ zap.String("open_workflow_id", openWorkflowID),
+ zap.String("auto_workflow_id", workflowID),
+ )
+ return nil
+ }
+
+ tag := t.config.GetEffectiveBlockTag(0)
+ searchStart := cronConfig.StartHeight
+ cursorHeight, cursorFound, err := t.metaStorage.GetBlockConsolidationCursor(ctx, metastorage.BatchConsolidatorAutoConsolidateCursor, tag)
+ if err != nil {
+ return xerrors.Errorf("failed to get auto_consolidate cursor: %w", err)
+ }
+ if cursorFound && cursorHeight > searchStart {
+ searchStart = cursorHeight
+ }
+ latest, err := t.metaStorage.GetLatestBlock(ctx, tag)
+ if err != nil {
+ return xerrors.Errorf("failed to get latest block for batch_consolidator cron: %w", err)
+ }
+ if latest == nil {
+ return xerrors.New("latest block not found for batch_consolidator cron")
+ }
+ searchEnd, safeHeight, ok := batchConsolidatorCronSafeEndHeight(latest.GetHeight(), t.config.Chain.IrreversibleDistance)
+ if !ok {
+ t.logger.Info(
+ "batch_consolidator cron has no irreversible consolidation range",
+ zap.Uint32("tag", tag),
+ zap.Uint64("latest_height", latest.GetHeight()),
+ zap.Uint64("irreversible_distance", t.config.Chain.IrreversibleDistance),
+ )
+ return nil
+ }
+ if searchEnd <= searchStart {
+ t.logger.Info(
+ "batch_consolidator cron safe range is below configured start height",
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", searchStart),
+ zap.Uint64("configured_start_height", cronConfig.StartHeight),
+ zap.Bool("cursor_found", cursorFound),
+ zap.Uint64("cursor_height", cursorHeight),
+ zap.Uint64("safe_end_height", searchEnd),
+ zap.Uint64("latest_height", latest.GetHeight()),
+ zap.Uint64("safe_consolidation_height", safeHeight),
+ )
+ return nil
+ }
+
+ if cursorFound {
+ repairScanStart := cronConfig.StartHeight
+ if cursorHeight > consolidation.ShardSize && cursorHeight-consolidation.ShardSize > repairScanStart {
+ repairScanStart = cursorHeight - consolidation.ShardSize
+ }
+ if repairScanStart < searchStart {
+ repairStart, repairFound, err := t.firstAutoConsolidateWindowStart(
+ ctx,
+ tag,
+ repairScanStart,
+ searchStart,
+ cronConfig.StartHeight,
+ consolidation.MaxBlocks,
+ consolidation.ShardSize,
+ )
+ if err != nil {
+ return err
+ }
+ if repairFound {
+ searchStart = repairStart
+ }
+ }
+ } else {
+ bootstrapStart, bootstrapFound, err := t.firstAutoConsolidateWindowStart(
+ ctx,
+ tag,
+ cronConfig.StartHeight,
+ searchEnd,
+ cronConfig.StartHeight,
+ consolidation.MaxBlocks,
+ consolidation.ShardSize,
+ )
+ if err != nil {
+ return err
+ }
+ if !bootstrapFound {
+ cursorEnd, cursorEndFound := batchConsolidatorCronRangeEnd(
+ cronConfig.StartHeight,
+ searchEnd,
+ consolidation.MaxBlocks,
+ consolidation.ShardSize,
+ )
+ if !cursorEndFound {
+ t.logger.Info(
+ "batch_consolidator cron found no missing consolidation shadow and no full window to bootstrap cursor",
+ zap.Uint32("tag", tag),
+ zap.Uint64("configured_start_height", cronConfig.StartHeight),
+ zap.Uint64("safe_end_height", searchEnd),
+ zap.Uint64("latest_height", latest.GetHeight()),
+ zap.Uint64("safe_consolidation_height", safeHeight),
+ zap.Uint64("consolidation_max_blocks", consolidation.MaxBlocks),
+ )
+ return nil
+ }
+ if err := t.verifyAutoConsolidateCoverage(ctx, tag, cronConfig.StartHeight, cursorEnd); err != nil {
+ t.logger.Info(
+ "batch_consolidator cron skipped cursor bootstrap because already consolidated range did not pass coverage verification",
+ zap.Uint32("tag", tag),
+ zap.Uint64("configured_start_height", cronConfig.StartHeight),
+ zap.Uint64("cursor_height", cursorEnd),
+ zap.Error(err),
+ )
+ return nil
+ }
+ if err := t.metaStorage.SetBlockConsolidationCursor(ctx, metastorage.BatchConsolidatorAutoConsolidateCursor, tag, cursorEnd); err != nil {
+ return xerrors.Errorf("failed to bootstrap auto_consolidate cursor to height %d: %w", cursorEnd, err)
+ }
+ t.logger.Info(
+ "batch_consolidator cron bootstrapped cursor from already consolidated range",
+ zap.Uint32("tag", tag),
+ zap.Uint64("configured_start_height", cronConfig.StartHeight),
+ zap.Uint64("cursor_height", cursorEnd),
+ zap.Uint64("safe_end_height", searchEnd),
+ zap.Uint64("latest_height", latest.GetHeight()),
+ zap.Uint64("safe_consolidation_height", safeHeight),
+ )
+ return nil
+ }
+ searchStart = bootstrapStart
+ }
+
+ normalizedStart := batchConsolidatorCronFullWindowStartAtOrAfter(searchStart, consolidation.MaxBlocks, consolidation.ShardSize)
+ if normalizedStart != searchStart {
+ t.logger.Info(
+ "batch_consolidator cron advanced start height to next full consolidation window",
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", searchStart),
+ zap.Uint64("normalized_start_height", normalizedStart),
+ zap.Uint64("consolidation_max_blocks", consolidation.MaxBlocks),
+ zap.Uint64("shard_size", consolidation.ShardSize),
+ )
+ searchStart = normalizedStart
+ }
+
+ startHeight := searchStart
+ endHeight, found := batchConsolidatorCronRangeEnd(
+ searchStart,
+ searchEnd,
+ consolidation.MaxBlocks,
+ consolidation.ShardSize,
+ )
+ if !found {
+ t.logger.Info(
+ "batch_consolidator cron found no full consolidation window",
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", searchStart),
+ zap.Uint64("configured_start_height", cronConfig.StartHeight),
+ zap.Bool("cursor_found", cursorFound),
+ zap.Uint64("cursor_height", cursorHeight),
+ zap.Uint64("end_height", searchEnd),
+ zap.Uint64("latest_height", latest.GetHeight()),
+ zap.Uint64("safe_consolidation_height", safeHeight),
+ zap.Uint64("consolidation_max_blocks", consolidation.MaxBlocks),
+ )
+ return nil
+ }
+
+ request := &workflow.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ }
+ if cronConfig.WorkflowParallelism > 0 {
+ request.Parallelism = cronConfig.WorkflowParallelism
+ }
+ workflowCtx := workflow.WithWorkflowID(ctx, workflowID)
+ run, err := t.batchConsolidator.Execute(workflowCtx, request)
+ if err != nil {
+ if isWorkflowAlreadyStarted(err) {
+ t.logger.Info("batch_consolidator cron skipped because auto consolidation workflow was already started", zap.String("workflow_id", workflowID))
+ return nil
+ }
+ return xerrors.Errorf("failed to start batch_consolidator cron workflow: %w", err)
+ }
+ t.logger.Info(
+ "started batch_consolidator cron workflow",
+ zap.String("workflow_id", workflowID),
+ zap.String("run_id", run.GetRunID()),
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", startHeight),
+ zap.Uint64("end_height", endHeight),
+ zap.Uint64("configured_start_height", cronConfig.StartHeight),
+ zap.Bool("cursor_found", cursorFound),
+ zap.Uint64("cursor_height", cursorHeight),
+ zap.Uint64("latest_height", latest.GetHeight()),
+ zap.Uint64("safe_consolidation_height", safeHeight),
+ zap.Int("workflow_parallelism", request.Parallelism),
+ )
+ return nil
+}
+
+func (t *batchConsolidatorTask) autoConsolidateWorkflowID() string {
+ return fmt.Sprintf("%s/%s", t.config.Workflows.BatchConsolidator.WorkflowIdentity, autoConsolidateSuffix)
+}
+
+func (t *batchConsolidatorTask) firstAutoConsolidateWindowStart(
+ ctx context.Context,
+ tag uint32,
+ searchStart uint64,
+ searchEnd uint64,
+ lowerBound uint64,
+ consolidationWindowBlocks uint64,
+ shardSize uint64,
+) (uint64, bool, error) {
+ if searchEnd <= searchStart {
+ return 0, false, nil
+ }
+ missingHeight, found, err := t.metaStorage.GetFirstBlockMissingConsolidationShadow(ctx, tag, searchStart, searchEnd)
+ if err != nil {
+ return 0, false, xerrors.Errorf("failed to get first block missing consolidation shadow: %w", err)
+ }
+ if !found {
+ return 0, false, nil
+ }
+ windowStart := batchConsolidatorCronObjectWindowStart(missingHeight, consolidationWindowBlocks, shardSize)
+ if windowStart < lowerBound {
+ windowStart = batchConsolidatorCronFullWindowStartAtOrAfter(lowerBound, consolidationWindowBlocks, shardSize)
+ }
+ if windowStart >= searchEnd {
+ return 0, false, nil
+ }
+ return windowStart, true, nil
+}
+
+func (t *batchConsolidatorTask) verifyAutoConsolidateCoverage(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) error {
+ if endHeight <= startHeight {
+ return xerrors.Errorf("invalid auto_consolidate coverage range [%d, %d)", startHeight, endHeight)
+ }
+ stats, err := t.metaStorage.GetBlockConsolidationShadowStats(ctx, tag, startHeight, endHeight)
+ if err != nil {
+ return xerrors.Errorf("failed to get auto_consolidate coverage stats for range [%d, %d): %w", startHeight, endHeight, err)
+ }
+ eligibleBlocks := uint64(0)
+ shadowBlocks := uint64(0)
+ if stats != nil {
+ eligibleBlocks = stats.EligibleBlocks
+ shadowBlocks = stats.Blocks
+ }
+ if shadowBlocks != eligibleBlocks {
+ return xerrors.Errorf("auto_consolidate coverage mismatch for range [%d, %d): expected %d eligible blocks, got %d shadow blocks", startHeight, endHeight, eligibleBlocks, shadowBlocks)
+ }
+ return nil
+}
+
+func (t *batchConsolidatorTask) openBatchConsolidatorWorkflow(ctx context.Context) (string, bool, error) {
+ workflowIdentity := t.config.Workflows.BatchConsolidator.WorkflowIdentity
+ openWorkflows, err := t.runtime.ListOpenWorkflows(ctx, t.config.Cadence.Domain, batchConsolidatorOpenPageSize, workflowIdentity)
+ if err != nil {
+ return "", false, xerrors.Errorf("failed to list open workflows for batch_consolidator cron: %w", err)
+ }
+ if openWorkflows == nil {
+ return "", false, nil
+ }
+ for _, wf := range openWorkflows.Executions {
+ if wf.GetType().GetName() == workflowIdentity {
+ workflowID := wf.GetExecution().GetWorkflowId()
+ return workflowID, true, nil
+ }
+ }
+ return "", false, nil
+}
+
+func batchConsolidatorCronSafeEndHeight(latestHeight uint64, irreversibleDistance uint64) (uint64, uint64, bool) {
+ if latestHeight < irreversibleDistance {
+ return 0, 0, false
+ }
+ safeHeight := latestHeight - irreversibleDistance
+ if safeHeight == ^uint64(0) {
+ return safeHeight, safeHeight, true
+ }
+ return safeHeight + 1, safeHeight, true
+}
+
+func batchConsolidatorCronRangeEnd(startHeight uint64, safeEndHeight uint64, consolidationWindowBlocks uint64, shardSize uint64) (uint64, bool) {
+ if safeEndHeight <= startHeight || consolidationWindowBlocks == 0 || shardSize == 0 {
+ return 0, false
+ }
+ rangeLimit := safeEndHeight
+ endHeight := startHeight
+ for {
+ nextEnd := endHeight + consolidationWindowBlocks
+ if nextEnd < endHeight || nextEnd > rangeLimit || nextEnd > batchConsolidatorCronShardEnd(endHeight, shardSize) {
+ break
+ }
+ endHeight = nextEnd
+ }
+ if endHeight == startHeight {
+ return 0, false
+ }
+ return endHeight, true
+}
+
+func batchConsolidatorCronObjectWindowStart(height uint64, consolidationWindowBlocks uint64, shardSize uint64) uint64 {
+ if consolidationWindowBlocks == 0 || shardSize == 0 {
+ return height
+ }
+ shardStart := (height / shardSize) * shardSize
+ offset := height - shardStart
+ return shardStart + (offset/consolidationWindowBlocks)*consolidationWindowBlocks
+}
+
+func batchConsolidatorCronFullWindowStartAtOrAfter(startHeight uint64, consolidationWindowBlocks uint64, shardSize uint64) uint64 {
+ if consolidationWindowBlocks == 0 || shardSize == 0 {
+ return startHeight
+ }
+ windowEnd := startHeight + consolidationWindowBlocks
+ if windowEnd >= startHeight && windowEnd <= batchConsolidatorCronShardEnd(startHeight, shardSize) {
+ return startHeight
+ }
+ return batchConsolidatorCronShardEnd(startHeight, shardSize)
+}
+
+func batchConsolidatorCronShardEnd(height uint64, shardSize uint64) uint64 {
+ return (height/shardSize + 1) * shardSize
+}
+
+func isWorkflowAlreadyStarted(err error) bool {
+ var alreadyStarted *serviceerror.WorkflowExecutionAlreadyStarted
+ if xerrors.As(err, &alreadyStarted) {
+ return true
+ }
+ return strings.Contains(strings.ToLower(err.Error()), "already started")
+}
diff --git a/internal/cron/batch_consolidator_test.go b/internal/cron/batch_consolidator_test.go
new file mode 100644
index 00000000..6d6bc48d
--- /dev/null
+++ b/internal/cron/batch_consolidator_test.go
@@ -0,0 +1,513 @@
+package cron
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "github.com/uber-go/tally/v4"
+ commonpb "go.temporal.io/api/common/v1"
+ workflowpb "go.temporal.io/api/workflow/v1"
+ "go.temporal.io/api/workflowservice/v1"
+ "go.temporal.io/sdk/activity"
+ "go.temporal.io/sdk/client"
+ "go.temporal.io/sdk/workflow"
+ "go.uber.org/mock/gomock"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zaptest"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ metastoragemocks "github.com/coinbase/chainstorage/internal/storage/metastorage/mocks"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ "github.com/coinbase/chainstorage/internal/utils/timesource"
+ workflowpkg "github.com/coinbase/chainstorage/internal/workflow"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ batchConsolidatorCronRuntime struct {
+ logger *zap.Logger
+ openWorkflowID []string
+ requestedWorkflowType string
+ executions []batchConsolidatorCronExecution
+ }
+
+ batchConsolidatorCronExecution struct {
+ options client.StartWorkflowOptions
+ request any
+ }
+
+ batchConsolidatorCronRun struct {
+ id string
+ runID string
+ }
+)
+
+var _ cadence.Runtime = (*batchConsolidatorCronRuntime)(nil)
+
+func TestBatchConsolidatorCronStartsFullWindowAutoConsolidateWorkflow(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 5_000}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(1_000), uint64(4_901)).
+ Return(uint64(3_500), true, nil)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Len(t, runtime.executions, 1)
+ require.Equal(t, "workflow.batch_consolidator/auto_consolidate", runtime.executions[0].options.ID)
+ require.Equal(t, "default", runtime.executions[0].options.TaskQueue)
+ require.Equal(t, &workflowpkg.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: tag,
+ StartHeight: 3_000,
+ EndHeight: 4_000,
+ }, runtime.executions[0].request)
+}
+
+func TestBatchConsolidatorCronRunsToNearestSafeFullWindow(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 7_000
+ cfg.Chain.IrreversibleDistance = 32
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 20_500}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(7_000), uint64(20_469)).
+ Return(uint64(7_000), true, nil)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Len(t, runtime.executions, 1)
+ require.Equal(t, &workflowpkg.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: tag,
+ StartHeight: 7_000,
+ EndHeight: 20_000,
+ }, runtime.executions[0].request)
+}
+
+func TestBatchConsolidatorCronPassesWorkflowParallelism(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+ cfg.Cron.BatchConsolidator.WorkflowParallelism = 20
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 5_000}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(1_000), uint64(4_901)).
+ Return(uint64(3_500), true, nil)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Len(t, runtime.executions, 1)
+ require.Equal(t, &workflowpkg.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: tag,
+ StartHeight: 3_000,
+ EndHeight: 4_000,
+ Parallelism: 20,
+ }, runtime.executions[0].request)
+}
+
+func TestBatchConsolidatorCronWaitsForFullConsolidationWindow(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 2_098}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(1_000), uint64(1_999)).
+ Return(uint64(1_000), true, nil)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Empty(t, runtime.executions)
+}
+
+func TestBatchConsolidatorCronUsesPersistedCursor(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(10_000), true, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 12_000}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(1_000), uint64(10_000)).
+ Return(uint64(0), false, nil)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Len(t, runtime.executions, 1)
+ require.Equal(t, &workflowpkg.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: tag,
+ StartHeight: 10_000,
+ EndHeight: 11_000,
+ }, runtime.executions[0].request)
+}
+
+func TestBatchConsolidatorCronNormalizesCursorShardTail(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(9_500), true, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 12_000}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(1_000), uint64(9_500)).
+ Return(uint64(0), false, nil)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Len(t, runtime.executions, 1)
+ require.Equal(t, &workflowpkg.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: tag,
+ StartHeight: 10_000,
+ EndHeight: 11_000,
+ }, runtime.executions[0].request)
+}
+
+func TestBatchConsolidatorCronRepairsWithinCursorLookback(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(20_000), true, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 25_000}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(10_000), uint64(20_000)).
+ Return(uint64(18_500), true, nil)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Len(t, runtime.executions, 1)
+ require.Equal(t, &workflowpkg.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: tag,
+ StartHeight: 18_000,
+ EndHeight: 24_000,
+ }, runtime.executions[0].request)
+}
+
+func TestBatchConsolidatorCronDoesNotCapAutoConsolidateAtPromotionGate(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ gateHeight := uint64(1_600)
+ cfg.AWS.Storage.Consolidation.PromotionGateHeight = &gateHeight
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 5_000}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(1_000), uint64(4_901)).
+ Return(uint64(1_000), true, nil)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Len(t, runtime.executions, 1)
+ require.Equal(t, &workflowpkg.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: tag,
+ StartHeight: 1_000,
+ EndHeight: 4_000,
+ }, runtime.executions[0].request)
+}
+
+func TestBatchConsolidatorCronWaitsWhenCursorHasNoFullNewWindow(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(1_000), true, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 2_000}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+ Times(0)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Empty(t, runtime.executions)
+}
+
+func TestBatchConsolidatorCronBootstrapsCursorWhenNoMissingShadow(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 12_000}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(1_000), uint64(11_901)).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetBlockConsolidationShadowStats(gomock.Any(), tag, uint64(1_000), uint64(11_000)).
+ Return(&metastorage.ConsolidationShadowStats{Objects: 10, Blocks: 10_000, EligibleBlocks: 10_000}, nil)
+ metaStorage.EXPECT().
+ SetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag, uint64(11_000)).
+ Return(nil)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Empty(t, runtime.executions)
+}
+
+func TestBatchConsolidatorCronBootstrapsCursorWithSkippedBlocks(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 12_000}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(1_000), uint64(11_901)).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetBlockConsolidationShadowStats(gomock.Any(), tag, uint64(1_000), uint64(11_000)).
+ Return(&metastorage.ConsolidationShadowStats{Objects: 10, Blocks: 9_990, EligibleBlocks: 9_990}, nil)
+ metaStorage.EXPECT().
+ SetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag, uint64(11_000)).
+ Return(nil)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Empty(t, runtime.executions)
+}
+
+func TestBatchConsolidatorCronDoesNotBootstrapCursorWithoutFullCoverage(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ tag := cfg.GetEffectiveBlockTag(0)
+ metaStorage.EXPECT().
+ GetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, tag).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), tag).
+ Return(&api.BlockMetadata{Tag: tag, Height: 12_000}, nil)
+ metaStorage.EXPECT().
+ GetFirstBlockMissingConsolidationShadow(gomock.Any(), tag, uint64(1_000), uint64(11_901)).
+ Return(uint64(0), false, nil)
+ metaStorage.EXPECT().
+ GetBlockConsolidationShadowStats(gomock.Any(), tag, uint64(1_000), uint64(11_000)).
+ Return(&metastorage.ConsolidationShadowStats{Objects: 9, Blocks: 9_999, EligibleBlocks: 10_000}, nil)
+ metaStorage.EXPECT().
+ SetBlockConsolidationCursor(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+ Times(0)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Empty(t, runtime.executions)
+}
+
+func TestBatchConsolidatorCronSkipsWhenBatchConsolidatorWorkflowIsAlreadyOpen(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ runtime.openWorkflowID = []string{"custom-manual-promote-workflow-id"}
+ cfg.Cron.BatchConsolidator.StartHeight = 1_000
+
+ metaStorage.EXPECT().GetBlockConsolidationCursor(gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
+ metaStorage.EXPECT().GetLatestBlock(gomock.Any(), gomock.Any()).Times(0)
+ metaStorage.EXPECT().GetFirstBlockMissingConsolidationShadow(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
+
+ require.NoError(t, task.Run(context.Background()))
+ require.Equal(t, "workflow.batch_consolidator", runtime.requestedWorkflowType)
+ require.Empty(t, runtime.executions)
+}
+
+func TestBatchConsolidatorCronRequiresAutoConsolidateMode(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.AWS.Storage.Consolidation.Mode = config.ConsolidationModePromoteFinalized
+
+ metaStorage.EXPECT().GetBlockConsolidationCursor(gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
+ metaStorage.EXPECT().GetLatestBlock(gomock.Any(), gomock.Any()).Times(0)
+ metaStorage.EXPECT().GetFirstBlockMissingConsolidationShadow(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
+
+ err := task.Run(context.Background())
+ require.Error(t, err)
+ require.Contains(t, err.Error(), `batch_consolidator cron requires consolidation mode "auto_consolidate", got "promote_finalized"`)
+ require.Empty(t, runtime.executions)
+}
+
+func TestBatchConsolidatorCronRejectsInvalidWorkflowParallelism(t *testing.T) {
+ task, runtime, metaStorage, cfg, ctrl := newBatchConsolidatorCronTask(t)
+ defer ctrl.Finish()
+ cfg.Cron.BatchConsolidator.WorkflowParallelism = maxBatchConsolidatorWorkflowParallelism + 1
+
+ metaStorage.EXPECT().GetBlockConsolidationCursor(gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
+ metaStorage.EXPECT().GetLatestBlock(gomock.Any(), gomock.Any()).Times(0)
+ metaStorage.EXPECT().GetFirstBlockMissingConsolidationShadow(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(0)
+
+ err := task.Run(context.Background())
+ require.Error(t, err)
+ require.Contains(t, err.Error(), "workflow_parallelism(21) exceeds max(20)")
+ require.Empty(t, runtime.executions)
+}
+
+func newBatchConsolidatorCronTask(t *testing.T) (*batchConsolidatorTask, *batchConsolidatorCronRuntime, *metastoragemocks.MockMetaStorage, *config.Config, *gomock.Controller) {
+ t.Helper()
+ cfg, err := config.New()
+ require.NoError(t, err)
+ cfg.Chain.BlockTag.Stable = 2
+ cfg.Chain.BlockTag.Latest = 2
+ cfg.Chain.IrreversibleDistance = 100
+ cfg.Cron.BatchConsolidator.Enabled = true
+ cfg.AWS.Storage.Consolidation.Enabled = true
+ cfg.AWS.Storage.Consolidation.Mode = config.ConsolidationModeAutoConsolidate
+ cfg.AWS.Storage.Consolidation.MaxBlocks = 1_000
+ safeLag := uint64(100)
+ cfg.AWS.Storage.Consolidation.SafePromotionLag = &safeLag
+
+ logger := zaptest.NewLogger(t)
+ runtime := &batchConsolidatorCronRuntime{logger: logger}
+ batchConsolidator := workflowpkg.NewBatchConsolidator(workflowpkg.BatchConsolidatorParams{
+ Params: fxparams.Params{
+ Config: cfg,
+ Logger: logger,
+ Metrics: tally.NoopScope,
+ },
+ Runtime: runtime,
+ })
+ ctrl := gomock.NewController(t)
+ metaStorage := metastoragemocks.NewMockMetaStorage(ctrl)
+ task, err := NewBatchConsolidator(BatchConsolidatorTaskParams{
+ Params: fxparams.Params{
+ Config: cfg,
+ Logger: logger,
+ Metrics: tally.NoopScope,
+ },
+ Config: cfg,
+ Runtime: runtime,
+ MetaStorage: metaStorage,
+ BatchConsolidator: batchConsolidator,
+ })
+ require.NoError(t, err)
+ return task.(*batchConsolidatorTask), runtime, metaStorage, cfg, ctrl
+}
+
+func (r *batchConsolidatorCronRuntime) RegisterWorkflow(w any, options workflow.RegisterOptions) {}
+
+func (r *batchConsolidatorCronRuntime) RegisterActivity(a any, options activity.RegisterOptions) {}
+
+func (r *batchConsolidatorCronRuntime) ExecuteWorkflow(ctx context.Context, options client.StartWorkflowOptions, workflow any, request any) (client.WorkflowRun, error) {
+ r.executions = append(r.executions, batchConsolidatorCronExecution{
+ options: options,
+ request: request,
+ })
+ return batchConsolidatorCronRun{id: options.ID, runID: "test-run-id"}, nil
+}
+
+func (r *batchConsolidatorCronRuntime) ExecuteActivity(ctx workflow.Context, activity any, request any, response any) error {
+ panic("ExecuteActivity is not used by batch_consolidator cron tests")
+}
+
+func (r *batchConsolidatorCronRuntime) GetLogger(ctx workflow.Context) *zap.Logger {
+ return r.logger
+}
+
+func (r *batchConsolidatorCronRuntime) GetMetricsHandler(ctx workflow.Context) client.MetricsHandler {
+ return client.MetricsNopHandler
+}
+
+func (r *batchConsolidatorCronRuntime) GetActivityLogger(ctx context.Context) *zap.Logger {
+ return r.logger
+}
+
+func (r *batchConsolidatorCronRuntime) GetTimeSource(ctx workflow.Context) timesource.TimeSource {
+ return timesource.NewRealTimeSource()
+}
+
+func (r *batchConsolidatorCronRuntime) TerminateWorkflow(ctx context.Context, workflowID string, runID string, reason string) error {
+ return nil
+}
+
+func (r *batchConsolidatorCronRuntime) OnStart(ctx context.Context) error {
+ return nil
+}
+
+func (r *batchConsolidatorCronRuntime) OnStop(ctx context.Context) error {
+ return nil
+}
+
+func (r *batchConsolidatorCronRuntime) ListOpenWorkflows(ctx context.Context, namespace string, maxPageSize int32, workflowType string) (*workflowservice.ListOpenWorkflowExecutionsResponse, error) {
+ r.requestedWorkflowType = workflowType
+ executions := make([]*workflowpb.WorkflowExecutionInfo, 0, len(r.openWorkflowID))
+ for _, workflowID := range r.openWorkflowID {
+ executions = append(executions, &workflowpb.WorkflowExecutionInfo{
+ Execution: &commonpb.WorkflowExecution{WorkflowId: workflowID},
+ Type: &commonpb.WorkflowType{Name: workflowType},
+ })
+ }
+ return &workflowservice.ListOpenWorkflowExecutionsResponse{Executions: executions}, nil
+}
+
+func (r batchConsolidatorCronRun) GetID() string {
+ return r.id
+}
+
+func (r batchConsolidatorCronRun) GetRunID() string {
+ return r.runID
+}
+
+func (r batchConsolidatorCronRun) Get(ctx context.Context, valuePtr any) error {
+ return nil
+}
+
+func (r batchConsolidatorCronRun) GetWithOptions(ctx context.Context, valuePtr any, options client.WorkflowRunGetOptions) error {
+ return nil
+}
diff --git a/internal/cron/dlq_processor.go b/internal/cron/dlq_processor.go
index 08b13e52..62c13a2f 100644
--- a/internal/cron/dlq_processor.go
+++ b/internal/cron/dlq_processor.go
@@ -22,19 +22,19 @@ type (
DLQProcessorTaskParams struct {
fx.In
fxparams.Params
- DLQ dlq.DLQ
- BlockchainClient client.Client `name:"slave"`
- BlobStorage blobstorage.BlobStorage
- MetaStorage metastorage.MetaStorage
+ DLQ dlq.DLQ
+ BlockchainClient client.Client `name:"slave"`
+ SingleBlockUploader blobstorage.SingleBlockUploader
+ MetaStorage metastorage.MetaStorage
}
dlqProcessorTask struct {
- enabled bool
- logger *zap.Logger
- dlq dlq.DLQ
- blockchainClient client.Client
- blobStorage blobstorage.BlobStorage
- metaStorage metastorage.MetaStorage
+ enabled bool
+ logger *zap.Logger
+ dlq dlq.DLQ
+ blockchainClient client.Client
+ singleBlockUploader blobstorage.SingleBlockUploader
+ metaStorage metastorage.MetaStorage
}
)
@@ -44,12 +44,12 @@ var (
func NewDLQProcessor(params DLQProcessorTaskParams) Task {
return &dlqProcessorTask{
- enabled: !params.Config.Cron.DisableDLQProcessor,
- logger: log.WithPackage(params.Logger),
- dlq: params.DLQ,
- blockchainClient: params.BlockchainClient,
- blobStorage: params.BlobStorage,
- metaStorage: params.MetaStorage,
+ enabled: !params.Config.Cron.DisableDLQProcessor,
+ logger: log.WithPackage(params.Logger),
+ dlq: params.DLQ,
+ blockchainClient: params.BlockchainClient,
+ singleBlockUploader: params.SingleBlockUploader,
+ metaStorage: params.MetaStorage,
}
}
@@ -146,11 +146,10 @@ func (t *dlqProcessorTask) processFailedTransactionTrace(ctx context.Context, me
objectKey := metadata.ObjectKeyMain
compression := storage_utils.GetCompressionType(objectKey)
- _, err = t.blobStorage.Upload(ctx, block, compression)
+ _, err = t.singleBlockUploader.Upload(ctx, block, compression)
if err != nil {
return xerrors.Errorf("failed to upload to blob store with compression type %v: %w", compression.String(), err)
}
-
// Note that the block is already persisted in meta storage.
// Since the S3 object key stays the same, we don't need to persist the block in meta storage again.
diff --git a/internal/cron/module.go b/internal/cron/module.go
index 1291b415..ad80d977 100644
--- a/internal/cron/module.go
+++ b/internal/cron/module.go
@@ -25,4 +25,8 @@ var Module = fx.Options(
Group: "task",
Target: NewWorkflowStatus,
}),
+ fx.Provide(fx.Annotated{
+ Group: "task",
+ Target: NewBatchConsolidator,
+ }),
)
diff --git a/internal/cron/workflow_status.go b/internal/cron/workflow_status.go
index 0c491cd4..14b70975 100644
--- a/internal/cron/workflow_status.go
+++ b/internal/cron/workflow_status.go
@@ -155,7 +155,7 @@ func (t *workflowStatusTask) Run(ctx context.Context) error {
}
func (t *workflowStatusTask) markOpenWorkflows(ctx context.Context, expectedWorkflowMap map[string]bool) error {
- openWorkflows, err := t.runtime.ListOpenWorkflows(ctx, t.config.Cadence.Domain, maxPageSize)
+ openWorkflows, err := t.runtime.ListOpenWorkflows(ctx, t.config.Cadence.Domain, maxPageSize, "")
if err != nil {
return xerrors.Errorf("failed to get open workflows: %w", err)
}
diff --git a/internal/dlq/sqs/dlq.go b/internal/dlq/sqs/dlq.go
index 075db64b..987b0905 100644
--- a/internal/dlq/sqs/dlq.go
+++ b/internal/dlq/sqs/dlq.go
@@ -6,8 +6,9 @@ import (
"strconv"
"time"
- "github.com/aws/aws-sdk-go/aws/session"
- "github.com/aws/aws-sdk-go/service/sqs"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/sqs"
+ "github.com/aws/aws-sdk-go-v2/service/sqs/types"
"go.uber.org/fx"
"go.uber.org/zap"
"golang.org/x/xerrors"
@@ -17,7 +18,6 @@ import (
"github.com/coinbase/chainstorage/internal/utils/fxparams"
"github.com/coinbase/chainstorage/internal/utils/instrument"
"github.com/coinbase/chainstorage/internal/utils/log"
- "github.com/coinbase/chainstorage/internal/utils/pointer"
)
type (
@@ -27,13 +27,13 @@ type (
DLQParams struct {
fx.In
fxparams.Params
- Session *session.Session
+ AWSConfig aws.Config
}
dlqImpl struct {
config *config.Config
logger *zap.Logger
- client *sqs.SQS
+ client *sqs.Client
queueURL string
instrumentSendMessage instrument.Instrument
instrumentResendMessage instrument.Instrument
@@ -61,10 +61,11 @@ const (
topicAttributeDateType = "String"
retriesAttributeName = "retries"
retriesAttributeDataType = "Number"
+ sentTimestampAttribute = "SentTimestamp"
)
func New(params DLQParams) (DLQ, error) {
- client := sqs.New(params.Session)
+ client := sqs.NewFromConfig(params.AWSConfig)
metrics := params.Metrics.SubScope("dlq").Tagged(map[string]string{
"storage_type": "sqs",
})
@@ -78,12 +79,12 @@ func New(params DLQParams) (DLQ, error) {
instrumentDeleteMessage: instrument.New(metrics, "delete_message"),
}
if params.Config.AWS.IsLocalStack {
- if err := impl.resetLocalResources(); err != nil {
+ if err := impl.resetLocalResources(context.Background()); err != nil {
return nil, xerrors.Errorf("failed to reset local resources: %w", err)
}
}
- if err := impl.initQueueURL(); err != nil {
+ if err := impl.initQueueURL(context.Background()); err != nil {
return nil, xerrors.Errorf("failed to init queue url: %w", err)
}
return impl, nil
@@ -98,18 +99,18 @@ func (q *dlqImpl) SendMessage(ctx context.Context, message *Message) error {
messageBody := string(body)
input := &sqs.SendMessageInput{
- QueueUrl: pointer.Ref(q.queueURL),
- MessageAttributes: map[string]*sqs.MessageAttributeValue{
+ QueueUrl: aws.String(q.queueURL),
+ MessageAttributes: map[string]types.MessageAttributeValue{
topicAttributeName: {
- DataType: pointer.Ref(topicAttributeDateType),
- StringValue: pointer.Ref(message.Topic),
+ DataType: aws.String(topicAttributeDateType),
+ StringValue: aws.String(message.Topic),
},
},
- MessageBody: pointer.Ref(messageBody),
- DelaySeconds: pointer.Ref(q.config.AWS.DLQ.DelaySecs),
+ MessageBody: aws.String(messageBody),
+ DelaySeconds: int32(q.config.AWS.DLQ.DelaySecs),
}
- if _, err := q.client.SendMessageWithContext(ctx, input); err != nil {
+ if _, err := q.client.SendMessage(ctx, input); err != nil {
return xerrors.Errorf("failed to send message: %w", err)
}
@@ -133,29 +134,29 @@ func (q *dlqImpl) ResendMessage(ctx context.Context, message *Message) error {
// Increment the retries counter and resend the same data.
retries := strconv.Itoa(message.Retries + 1)
input := &sqs.SendMessageInput{
- QueueUrl: pointer.Ref(q.queueURL),
- MessageAttributes: map[string]*sqs.MessageAttributeValue{
+ QueueUrl: aws.String(q.queueURL),
+ MessageAttributes: map[string]types.MessageAttributeValue{
topicAttributeName: {
- DataType: pointer.Ref(topicAttributeDateType),
- StringValue: pointer.Ref(message.Topic),
+ DataType: aws.String(topicAttributeDateType),
+ StringValue: aws.String(message.Topic),
},
retriesAttributeName: {
- DataType: pointer.Ref(retriesAttributeDataType),
- StringValue: pointer.Ref(retries),
+ DataType: aws.String(retriesAttributeDataType),
+ StringValue: aws.String(retries),
},
},
- MessageBody: pointer.Ref(messageBody),
- DelaySeconds: pointer.Ref(q.config.AWS.DLQ.DelaySecs),
+ MessageBody: aws.String(messageBody),
+ DelaySeconds: int32(q.config.AWS.DLQ.DelaySecs),
}
- if _, err := q.client.SendMessageWithContext(ctx, input); err != nil {
+ if _, err := q.client.SendMessage(ctx, input); err != nil {
return xerrors.Errorf("failed to send message: %w", err)
}
// Delete the original message.
- if _, err := q.client.DeleteMessageWithContext(ctx, &sqs.DeleteMessageInput{
- QueueUrl: pointer.Ref(q.queueURL),
- ReceiptHandle: pointer.Ref(message.ReceiptHandle),
+ if _, err := q.client.DeleteMessage(ctx, &sqs.DeleteMessageInput{
+ QueueUrl: aws.String(q.queueURL),
+ ReceiptHandle: aws.String(message.ReceiptHandle),
}); err != nil {
return xerrors.Errorf("failed to delete message: %w", err)
}
@@ -174,18 +175,18 @@ func (q *dlqImpl) ReceiveMessage(ctx context.Context) (*Message, error) {
var message *Message
if err := q.instrumentReceiveMessage.Instrument(ctx, func(ctx context.Context) error {
input := &sqs.ReceiveMessageInput{
- QueueUrl: pointer.Ref(q.queueURL),
- VisibilityTimeout: pointer.Ref(q.config.AWS.DLQ.VisibilityTimeoutSecs),
- AttributeNames: []*string{
- pointer.Ref(sqs.MessageSystemAttributeNameSentTimestamp),
+ QueueUrl: aws.String(q.queueURL),
+ VisibilityTimeout: int32(q.config.AWS.DLQ.VisibilityTimeoutSecs),
+ AttributeNames: []types.QueueAttributeName{
+ types.QueueAttributeName(sentTimestampAttribute),
},
- MessageAttributeNames: []*string{
- pointer.Ref(topicAttributeName),
- pointer.Ref(retriesAttributeName),
+ MessageAttributeNames: []string{
+ topicAttributeName,
+ retriesAttributeName,
},
}
- output, err := q.client.ReceiveMessageWithContext(ctx, input)
+ output, err := q.client.ReceiveMessage(ctx, input)
if err != nil {
return xerrors.Errorf("failed to receive message: %w", err)
}
@@ -200,24 +201,24 @@ func (q *dlqImpl) ReceiveMessage(ctx context.Context) (*Message, error) {
}
outputMessage := output.Messages[0]
- receiptHandle := pointer.Deref(outputMessage.ReceiptHandle)
+ receiptHandle := aws.ToString(outputMessage.ReceiptHandle)
- sentTimestampAttribute := pointer.Deref(outputMessage.Attributes[sqs.MessageSystemAttributeNameSentTimestamp])
- sentTimestampEpoch, err := strconv.ParseInt(sentTimestampAttribute, 10, 64)
+ sentTimestampAttr := outputMessage.Attributes[sentTimestampAttribute]
+ sentTimestampEpoch, err := strconv.ParseInt(sentTimestampAttr, 10, 64)
if err != nil {
- return xerrors.Errorf("failed to parse sent timestamp: %v", sentTimestampAttribute)
+ return xerrors.Errorf("failed to parse sent timestamp: %v", sentTimestampAttr)
}
sentTimestamp := time.Unix(sentTimestampEpoch/1000, 0)
topicAttribute := outputMessage.MessageAttributes[topicAttributeName]
- if topicAttribute == nil {
+ if topicAttribute.StringValue == nil {
return xerrors.Errorf("topic not found: %v", outputMessage)
}
- topic := pointer.Deref(topicAttribute.StringValue)
+ topic := aws.ToString(topicAttribute.StringValue)
var retries int
- if attr := outputMessage.MessageAttributes[retriesAttributeName]; attr != nil {
- if v, err := strconv.Atoi(pointer.Deref(attr.StringValue)); err == nil {
+ if attr := outputMessage.MessageAttributes[retriesAttributeName]; attr.StringValue != nil {
+ if v, err := strconv.Atoi(aws.ToString(attr.StringValue)); err == nil {
retries = v
}
}
@@ -233,7 +234,7 @@ func (q *dlqImpl) ReceiveMessage(ctx context.Context) (*Message, error) {
}
if data != nil {
- body := []byte(pointer.Deref(outputMessage.Body))
+ body := []byte(aws.ToString(outputMessage.Body))
if err := json.Unmarshal(body, data); err != nil {
return xerrors.Errorf("failed to unmarshal message: %w", err)
}
@@ -263,9 +264,9 @@ func (q *dlqImpl) ReceiveMessage(ctx context.Context) (*Message, error) {
func (q *dlqImpl) DeleteMessage(ctx context.Context, message *Message) error {
return q.instrumentDeleteMessage.Instrument(ctx, func(ctx context.Context) error {
- if _, err := q.client.DeleteMessageWithContext(ctx, &sqs.DeleteMessageInput{
- QueueUrl: pointer.Ref(q.queueURL),
- ReceiptHandle: pointer.Ref(message.ReceiptHandle),
+ if _, err := q.client.DeleteMessage(ctx, &sqs.DeleteMessageInput{
+ QueueUrl: aws.String(q.queueURL),
+ ReceiptHandle: aws.String(message.ReceiptHandle),
}); err != nil {
return xerrors.Errorf("failed to delete message: %w", err)
}
@@ -279,21 +280,21 @@ func (q *dlqImpl) DeleteMessage(ctx context.Context, message *Message) error {
})
}
-func (q *dlqImpl) initQueueURL() error {
+func (q *dlqImpl) initQueueURL(ctx context.Context) error {
queueURLInput := &sqs.GetQueueUrlInput{
- QueueName: pointer.Ref(q.config.AWS.DLQ.Name),
+ QueueName: aws.String(q.config.AWS.DLQ.Name),
}
if q.config.AWS.DLQ.OwnerAccountId != "" {
- queueURLInput.SetQueueOwnerAWSAccountId(q.config.AWS.DLQ.OwnerAccountId)
+ queueURLInput.QueueOwnerAWSAccountId = aws.String(q.config.AWS.DLQ.OwnerAccountId)
}
- output, err := q.client.GetQueueUrl(queueURLInput)
+ output, err := q.client.GetQueueUrl(ctx, queueURLInput)
if err != nil {
return xerrors.Errorf("failed to get queue url (name=%v): %w", q.config.AWS.DLQ.Name, err)
}
- queueURL := pointer.Deref(output.QueueUrl)
+ queueURL := aws.ToString(output.QueueUrl)
if queueURL == "" {
return xerrors.New("empty queue url")
}
diff --git a/internal/dlq/sqs/dlq_local.go b/internal/dlq/sqs/dlq_local.go
index 523c2d23..29512548 100644
--- a/internal/dlq/sqs/dlq_local.go
+++ b/internal/dlq/sqs/dlq_local.go
@@ -1,27 +1,29 @@
package sqs
import (
- "github.com/aws/aws-sdk-go/aws/awserr"
- "github.com/aws/aws-sdk-go/service/sqs"
+ "context"
+ "errors"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/sqs"
+ "github.com/aws/aws-sdk-go-v2/service/sqs/types"
"go.uber.org/zap"
"golang.org/x/xerrors"
-
- "github.com/coinbase/chainstorage/internal/utils/pointer"
)
-func (q *dlqImpl) resetLocalResources() error {
+func (q *dlqImpl) resetLocalResources(ctx context.Context) error {
q.logger.Info("initializing dlq")
- if err := q.initQueueURL(); err != nil {
- var aerr awserr.Error
- if !xerrors.As(err, &aerr) || aerr.Code() != sqs.ErrCodeQueueDoesNotExist {
+ if err := q.initQueueURL(ctx); err != nil {
+ var queueNotExist *types.QueueDoesNotExist
+ if !errors.As(err, &queueNotExist) {
return xerrors.Errorf("failed to init queue url: %w", err)
}
}
if q.config.AWS.IsResetLocal && q.queueURL != "" {
- if _, err := q.client.DeleteQueue(&sqs.DeleteQueueInput{
- QueueUrl: pointer.Ref(q.queueURL),
+ if _, err := q.client.DeleteQueue(ctx, &sqs.DeleteQueueInput{
+ QueueUrl: aws.String(q.queueURL),
}); err != nil {
return xerrors.Errorf("failed to delete queue: %w", err)
}
@@ -31,8 +33,8 @@ func (q *dlqImpl) resetLocalResources() error {
}
if q.queueURL == "" {
- output, err := q.client.CreateQueue(&sqs.CreateQueueInput{
- QueueName: pointer.Ref(q.config.AWS.DLQ.Name),
+ output, err := q.client.CreateQueue(ctx, &sqs.CreateQueueInput{
+ QueueName: aws.String(q.config.AWS.DLQ.Name),
})
if err != nil {
return xerrors.Errorf("failed to create queue: %w", err)
diff --git a/internal/gateway/rest_client.go b/internal/gateway/rest_client.go
index e2fafffe..b33720b8 100644
--- a/internal/gateway/rest_client.go
+++ b/internal/gateway/rest_client.go
@@ -219,6 +219,15 @@ func (c *restClient) GetVerifiedAccountState(ctx context.Context, in *api.GetVer
return &response, nil
}
+func (c *restClient) GetBlockByTimestamp(ctx context.Context, in *api.GetBlockByTimestampRequest, opts ...grpc.CallOption) (*api.GetBlockByTimestampResponse, error) {
+ var response api.GetBlockByTimestampResponse
+ if err := c.makeRequest(ctx, "GetBlockByTimestamp", in, &response); err != nil {
+ return nil, xerrors.Errorf("failed to make request: %w", err)
+ }
+
+ return &response, nil
+}
+
func (c *restClient) makeRequest(ctx context.Context, method string, request proto.Message, response proto.Message) error {
return c.retry.Retry(ctx, func(ctx context.Context) error {
marshaler := protojson.MarshalOptions{}
@@ -237,6 +246,7 @@ func (c *restClient) makeRequest(ctx context.Context, method string, request pro
httpRequest.Header.Set("Accept", "application/json")
if c.authHeader != "" && c.authToken != "" {
httpRequest.Header.Set(c.authHeader, c.authToken)
+ httpRequest.Header.Set("cb-nft-api-token", c.authToken)
}
c.logger.Debug(
diff --git a/internal/s3/client.go b/internal/s3/client.go
index 7f212c4e..d7a431b7 100644
--- a/internal/s3/client.go
+++ b/internal/s3/client.go
@@ -1,12 +1,12 @@
package s3
import (
- "github.com/aws/aws-sdk-go/aws/session"
- "github.com/aws/aws-sdk-go/service/s3"
- "github.com/aws/aws-sdk-go/service/s3/s3iface"
- "github.com/aws/aws-sdk-go/service/s3/s3manager"
- "github.com/aws/aws-sdk-go/service/s3/s3manager/s3manageriface"
- otaws "github.com/opentracing-contrib/go-aws-sdk"
+ "context"
+ "io"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
"go.uber.org/fx"
"golang.org/x/xerrors"
@@ -14,18 +14,40 @@ import (
)
type (
- Downloader = s3manageriface.DownloaderAPI
- Uploader = s3manageriface.UploaderAPI
- Client = s3iface.S3API
+ // Client interface for S3 operations
+ Client interface {
+ GetObject(ctx context.Context, params *s3.GetObjectInput, optFns ...func(*s3.Options)) (*s3.GetObjectOutput, error)
+ PutObject(ctx context.Context, params *s3.PutObjectInput, optFns ...func(*s3.Options)) (*s3.PutObjectOutput, error)
+ HeadObject(ctx context.Context, params *s3.HeadObjectInput, optFns ...func(*s3.Options)) (*s3.HeadObjectOutput, error)
+ HeadBucket(ctx context.Context, params *s3.HeadBucketInput, optFns ...func(*s3.Options)) (*s3.HeadBucketOutput, error)
+ GetBucketPolicy(ctx context.Context, params *s3.GetBucketPolicyInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error)
+ GetBucketLifecycleConfiguration(ctx context.Context, params *s3.GetBucketLifecycleConfigurationInput, optFns ...func(*s3.Options)) (*s3.GetBucketLifecycleConfigurationOutput, error)
+ CreateBucket(ctx context.Context, params *s3.CreateBucketInput, optFns ...func(*s3.Options)) (*s3.CreateBucketOutput, error)
+ DeleteBucket(ctx context.Context, params *s3.DeleteBucketInput, optFns ...func(*s3.Options)) (*s3.DeleteBucketOutput, error)
+ ListObjectsV2(ctx context.Context, params *s3.ListObjectsV2Input, optFns ...func(*s3.Options)) (*s3.ListObjectsV2Output, error)
+ ListObjectVersions(ctx context.Context, params *s3.ListObjectVersionsInput, optFns ...func(*s3.Options)) (*s3.ListObjectVersionsOutput, error)
+ DeleteObject(ctx context.Context, params *s3.DeleteObjectInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectOutput, error)
+ DeleteObjects(ctx context.Context, params *s3.DeleteObjectsInput, optFns ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error)
+ }
+
+ // Downloader interface for S3 download operations
+ Downloader interface {
+ Download(ctx context.Context, w io.WriterAt, input *s3.GetObjectInput, options ...func(*manager.Downloader)) (n int64, err error)
+ }
+
+ // Uploader interface for S3 upload operations
+ Uploader interface {
+ Upload(ctx context.Context, input *s3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error)
+ }
S3 struct {
- Session *session.Session
+ Config aws.Config
}
S3Params struct {
fx.In
fxparams.Params
- Session *session.Session
+ AWSConfig aws.Config
}
ClientParams struct {
@@ -42,22 +64,20 @@ func NewS3(params S3Params) (*S3, error) {
}
return &S3{
- Session: params.Session,
+ Config: params.AWSConfig,
}, nil
}
func NewUploader(params ClientParams) Uploader {
- return s3manager.NewUploader(params.S3.Session)
+ client := s3.NewFromConfig(params.S3.Config)
+ return manager.NewUploader(client)
}
func NewDownloader(params ClientParams) Downloader {
- return s3manager.NewDownloader(params.S3.Session)
+ client := s3.NewFromConfig(params.S3.Config)
+ return manager.NewDownloader(client)
}
func NewClient(params ClientParams) Client {
- s3Client := s3.New(params.S3.Session)
- // this is optional as we already have traces at session level
- // but since we have the client, we might as well add for extra information at client level
- otaws.AddOTHandlers(s3Client.Client)
- return s3Client
+ return s3.NewFromConfig(params.S3.Config)
}
diff --git a/internal/s3/client_local.go b/internal/s3/client_local.go
index a82e4bc1..8e3d03ef 100644
--- a/internal/s3/client_local.go
+++ b/internal/s3/client_local.go
@@ -1,79 +1,98 @@
package s3
import (
+ "context"
+ "errors"
"fmt"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/awserr"
- "github.com/aws/aws-sdk-go/aws/session"
- "github.com/aws/aws-sdk-go/service/s3"
- "github.com/aws/aws-sdk-go/service/s3/s3iface"
- "github.com/aws/aws-sdk-go/service/s3/s3manager"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
"go.uber.org/zap"
"golang.org/x/xerrors"
)
func resetLocalResources(params S3Params) error {
- return initBucket(params.Logger, params.Config.AWS.Bucket, params.Session, params.Config.AWS.IsResetLocal)
+ return initBucket(params.Logger, params.Config.AWS.Bucket, params.AWSConfig, params.Config.AWS.IsResetLocal)
}
-func initBucket(log *zap.Logger, bucket string, awsSession *session.Session, reset bool) error {
- s3Client := s3.New(awsSession)
+func initBucket(log *zap.Logger, bucket string, cfg aws.Config, reset bool) error {
+ ctx := context.Background()
+ s3Client := s3.NewFromConfig(cfg)
log.Debug(fmt.Sprintf("Initializing bucket %s", bucket))
if reset {
- err := deleteBucketIfExists(log, bucket, s3Client)
+ err := deleteBucketIfExists(ctx, log, bucket, s3Client)
if err != nil {
return err
}
}
- return createBucketIfNotExists(bucket, s3Client)
+ return createBucketIfNotExists(ctx, bucket, s3Client)
}
-func deleteBucketIfExists(log *zap.Logger, bucket string, s3Client s3iface.S3API) error {
+func deleteBucketIfExists(ctx context.Context, log *zap.Logger, bucket string, s3Client Client) error {
input := &s3.HeadBucketInput{
Bucket: aws.String(bucket),
}
- _, err := s3Client.HeadBucket(input)
+ _, err := s3Client.HeadBucket(ctx, input)
if err != nil {
- if awsErr, ok := err.(awserr.Error); ok {
- if awsErr.Code() == "NotFound" {
- return nil
- }
+ var notFound *types.NotFound
+ if errors.As(err, ¬Found) {
+ return nil
}
return xerrors.Errorf("failed to get bucket %s: %w", bucket, err)
}
log.Info(fmt.Sprintf("bucket %s exists, deleting files...", bucket))
- iter := s3manager.NewDeleteListIterator(s3Client, &s3.ListObjectsInput{
+
+ // List and delete all objects
+ paginator := s3.NewListObjectsV2Paginator(s3Client.(*s3.Client), &s3.ListObjectsV2Input{
Bucket: aws.String(bucket),
})
- if err := s3manager.NewBatchDeleteWithClient(s3Client).Delete(aws.BackgroundContext(), iter); err != nil {
- return xerrors.Errorf("failed to delete object under bucket %s: %w", bucket, err)
+ for paginator.HasMorePages() {
+ page, err := paginator.NextPage(ctx)
+ if err != nil {
+ return xerrors.Errorf("failed to list objects in bucket %s: %w", bucket, err)
+ }
+
+ if len(page.Contents) > 0 {
+ objectIds := make([]types.ObjectIdentifier, len(page.Contents))
+ for i, obj := range page.Contents {
+ objectIds[i] = types.ObjectIdentifier{Key: obj.Key}
+ }
+
+ _, err = s3Client.DeleteObjects(ctx, &s3.DeleteObjectsInput{
+ Bucket: aws.String(bucket),
+ Delete: &types.Delete{Objects: objectIds},
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to delete objects under bucket %s: %w", bucket, err)
+ }
+ }
}
log.Info(fmt.Sprintf("deleting bucket %s...", bucket))
deleteInput := &s3.DeleteBucketInput{
Bucket: aws.String(bucket),
}
- _, err = s3Client.DeleteBucket(deleteInput)
+ _, err = s3Client.DeleteBucket(ctx, deleteInput)
if err != nil {
return xerrors.Errorf("failed to delete bucket %s: %w", bucket, err)
}
return nil
}
-func createBucketIfNotExists(bucket string, s3Client s3iface.S3API) error {
+func createBucketIfNotExists(ctx context.Context, bucket string, s3Client Client) error {
input := &s3.CreateBucketInput{
Bucket: aws.String(bucket),
}
- _, err := s3Client.CreateBucket(input)
+ _, err := s3Client.CreateBucket(ctx, input)
if err != nil {
- if awsErr, ok := err.(awserr.Error); ok {
- if awsErr.Code() == s3.ErrCodeBucketAlreadyExists {
- return nil
- }
+ var bucketExists *types.BucketAlreadyExists
+ var bucketOwned *types.BucketAlreadyOwnedByYou
+ if errors.As(err, &bucketExists) || errors.As(err, &bucketOwned) {
+ return nil
}
return xerrors.Errorf("failed to create bucket %s: %w", bucket, err)
diff --git a/internal/s3/mocks/mocks.go b/internal/s3/mocks/mocks.go
index 18a76db1..7aa5d988 100644
--- a/internal/s3/mocks/mocks.go
+++ b/internal/s3/mocks/mocks.go
@@ -14,9 +14,8 @@ import (
io "io"
reflect "reflect"
- request "github.com/aws/aws-sdk-go/aws/request"
- s3 "github.com/aws/aws-sdk-go/service/s3"
- s3manager "github.com/aws/aws-sdk-go/service/s3/s3manager"
+ manager "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
+ s3 "github.com/aws/aws-sdk-go-v2/service/s3"
gomock "go.uber.org/mock/gomock"
)
@@ -44,43 +43,23 @@ func (m *MockDownloader) EXPECT() *MockDownloaderMockRecorder {
}
// Download mocks base method.
-func (m *MockDownloader) Download(arg0 io.WriterAt, arg1 *s3.GetObjectInput, arg2 ...func(*s3manager.Downloader)) (int64, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "Download", varargs...)
- ret0, _ := ret[0].(int64)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// Download indicates an expected call of Download.
-func (mr *MockDownloaderMockRecorder) Download(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockDownloader)(nil).Download), varargs...)
-}
-
-// DownloadWithContext mocks base method.
-func (m *MockDownloader) DownloadWithContext(arg0 context.Context, arg1 io.WriterAt, arg2 *s3.GetObjectInput, arg3 ...func(*s3manager.Downloader)) (int64, error) {
+func (m *MockDownloader) Download(arg0 context.Context, arg1 io.WriterAt, arg2 *s3.GetObjectInput, arg3 ...func(*manager.Downloader)) (int64, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1, arg2}
for _, a := range arg3 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DownloadWithContext", varargs...)
+ ret := m.ctrl.Call(m, "Download", varargs...)
ret0, _ := ret[0].(int64)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DownloadWithContext indicates an expected call of DownloadWithContext.
-func (mr *MockDownloaderMockRecorder) DownloadWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
+// Download indicates an expected call of Download.
+func (mr *MockDownloaderMockRecorder) Download(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadWithContext", reflect.TypeOf((*MockDownloader)(nil).DownloadWithContext), varargs...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockDownloader)(nil).Download), varargs...)
}
// MockUploader is a mock of Uploader interface.
@@ -107,43 +86,23 @@ func (m *MockUploader) EXPECT() *MockUploaderMockRecorder {
}
// Upload mocks base method.
-func (m *MockUploader) Upload(arg0 *s3manager.UploadInput, arg1 ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0}
- for _, a := range arg1 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "Upload", varargs...)
- ret0, _ := ret[0].(*s3manager.UploadOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// Upload indicates an expected call of Upload.
-func (mr *MockUploaderMockRecorder) Upload(arg0 any, arg1 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0}, arg1...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*MockUploader)(nil).Upload), varargs...)
-}
-
-// UploadWithContext mocks base method.
-func (m *MockUploader) UploadWithContext(arg0 context.Context, arg1 *s3manager.UploadInput, arg2 ...func(*s3manager.Uploader)) (*s3manager.UploadOutput, error) {
+func (m *MockUploader) Upload(arg0 context.Context, arg1 *s3.PutObjectInput, arg2 ...func(*manager.Uploader)) (*manager.UploadOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "UploadWithContext", varargs...)
- ret0, _ := ret[0].(*s3manager.UploadOutput)
+ ret := m.ctrl.Call(m, "Upload", varargs...)
+ ret0, _ := ret[0].(*manager.UploadOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// UploadWithContext indicates an expected call of UploadWithContext.
-func (mr *MockUploaderMockRecorder) UploadWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// Upload indicates an expected call of Upload.
+func (mr *MockUploaderMockRecorder) Upload(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadWithContext", reflect.TypeOf((*MockUploader)(nil).UploadWithContext), varargs...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*MockUploader)(nil).Upload), varargs...)
}
// MockClient is a mock of Client interface.
@@ -169,5282 +128,242 @@ func (m *MockClient) EXPECT() *MockClientMockRecorder {
return m.recorder
}
-// AbortMultipartUpload mocks base method.
-func (m *MockClient) AbortMultipartUpload(arg0 *s3.AbortMultipartUploadInput) (*s3.AbortMultipartUploadOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "AbortMultipartUpload", arg0)
- ret0, _ := ret[0].(*s3.AbortMultipartUploadOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// AbortMultipartUpload indicates an expected call of AbortMultipartUpload.
-func (mr *MockClientMockRecorder) AbortMultipartUpload(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AbortMultipartUpload", reflect.TypeOf((*MockClient)(nil).AbortMultipartUpload), arg0)
-}
-
-// AbortMultipartUploadRequest mocks base method.
-func (m *MockClient) AbortMultipartUploadRequest(arg0 *s3.AbortMultipartUploadInput) (*request.Request, *s3.AbortMultipartUploadOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "AbortMultipartUploadRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.AbortMultipartUploadOutput)
- return ret0, ret1
-}
-
-// AbortMultipartUploadRequest indicates an expected call of AbortMultipartUploadRequest.
-func (mr *MockClientMockRecorder) AbortMultipartUploadRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AbortMultipartUploadRequest", reflect.TypeOf((*MockClient)(nil).AbortMultipartUploadRequest), arg0)
-}
-
-// AbortMultipartUploadWithContext mocks base method.
-func (m *MockClient) AbortMultipartUploadWithContext(arg0 context.Context, arg1 *s3.AbortMultipartUploadInput, arg2 ...request.Option) (*s3.AbortMultipartUploadOutput, error) {
+// CreateBucket mocks base method.
+func (m *MockClient) CreateBucket(arg0 context.Context, arg1 *s3.CreateBucketInput, arg2 ...func(*s3.Options)) (*s3.CreateBucketOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "AbortMultipartUploadWithContext", varargs...)
- ret0, _ := ret[0].(*s3.AbortMultipartUploadOutput)
+ ret := m.ctrl.Call(m, "CreateBucket", varargs...)
+ ret0, _ := ret[0].(*s3.CreateBucketOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// AbortMultipartUploadWithContext indicates an expected call of AbortMultipartUploadWithContext.
-func (mr *MockClientMockRecorder) AbortMultipartUploadWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// CreateBucket indicates an expected call of CreateBucket.
+func (mr *MockClientMockRecorder) CreateBucket(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AbortMultipartUploadWithContext", reflect.TypeOf((*MockClient)(nil).AbortMultipartUploadWithContext), varargs...)
-}
-
-// CompleteMultipartUpload mocks base method.
-func (m *MockClient) CompleteMultipartUpload(arg0 *s3.CompleteMultipartUploadInput) (*s3.CompleteMultipartUploadOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CompleteMultipartUpload", arg0)
- ret0, _ := ret[0].(*s3.CompleteMultipartUploadOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// CompleteMultipartUpload indicates an expected call of CompleteMultipartUpload.
-func (mr *MockClientMockRecorder) CompleteMultipartUpload(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteMultipartUpload", reflect.TypeOf((*MockClient)(nil).CompleteMultipartUpload), arg0)
-}
-
-// CompleteMultipartUploadRequest mocks base method.
-func (m *MockClient) CompleteMultipartUploadRequest(arg0 *s3.CompleteMultipartUploadInput) (*request.Request, *s3.CompleteMultipartUploadOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CompleteMultipartUploadRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.CompleteMultipartUploadOutput)
- return ret0, ret1
-}
-
-// CompleteMultipartUploadRequest indicates an expected call of CompleteMultipartUploadRequest.
-func (mr *MockClientMockRecorder) CompleteMultipartUploadRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteMultipartUploadRequest", reflect.TypeOf((*MockClient)(nil).CompleteMultipartUploadRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucket", reflect.TypeOf((*MockClient)(nil).CreateBucket), varargs...)
}
-// CompleteMultipartUploadWithContext mocks base method.
-func (m *MockClient) CompleteMultipartUploadWithContext(arg0 context.Context, arg1 *s3.CompleteMultipartUploadInput, arg2 ...request.Option) (*s3.CompleteMultipartUploadOutput, error) {
+// DeleteBucket mocks base method.
+func (m *MockClient) DeleteBucket(arg0 context.Context, arg1 *s3.DeleteBucketInput, arg2 ...func(*s3.Options)) (*s3.DeleteBucketOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "CompleteMultipartUploadWithContext", varargs...)
- ret0, _ := ret[0].(*s3.CompleteMultipartUploadOutput)
+ ret := m.ctrl.Call(m, "DeleteBucket", varargs...)
+ ret0, _ := ret[0].(*s3.DeleteBucketOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// CompleteMultipartUploadWithContext indicates an expected call of CompleteMultipartUploadWithContext.
-func (mr *MockClientMockRecorder) CompleteMultipartUploadWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// DeleteBucket indicates an expected call of DeleteBucket.
+func (mr *MockClientMockRecorder) DeleteBucket(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CompleteMultipartUploadWithContext", reflect.TypeOf((*MockClient)(nil).CompleteMultipartUploadWithContext), varargs...)
-}
-
-// CopyObject mocks base method.
-func (m *MockClient) CopyObject(arg0 *s3.CopyObjectInput) (*s3.CopyObjectOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CopyObject", arg0)
- ret0, _ := ret[0].(*s3.CopyObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// CopyObject indicates an expected call of CopyObject.
-func (mr *MockClientMockRecorder) CopyObject(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyObject", reflect.TypeOf((*MockClient)(nil).CopyObject), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucket", reflect.TypeOf((*MockClient)(nil).DeleteBucket), varargs...)
}
-// CopyObjectRequest mocks base method.
-func (m *MockClient) CopyObjectRequest(arg0 *s3.CopyObjectInput) (*request.Request, *s3.CopyObjectOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CopyObjectRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.CopyObjectOutput)
- return ret0, ret1
-}
-
-// CopyObjectRequest indicates an expected call of CopyObjectRequest.
-func (mr *MockClientMockRecorder) CopyObjectRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyObjectRequest", reflect.TypeOf((*MockClient)(nil).CopyObjectRequest), arg0)
-}
-
-// CopyObjectWithContext mocks base method.
-func (m *MockClient) CopyObjectWithContext(arg0 context.Context, arg1 *s3.CopyObjectInput, arg2 ...request.Option) (*s3.CopyObjectOutput, error) {
+// DeleteObject mocks base method.
+func (m *MockClient) DeleteObject(arg0 context.Context, arg1 *s3.DeleteObjectInput, arg2 ...func(*s3.Options)) (*s3.DeleteObjectOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "CopyObjectWithContext", varargs...)
- ret0, _ := ret[0].(*s3.CopyObjectOutput)
+ ret := m.ctrl.Call(m, "DeleteObject", varargs...)
+ ret0, _ := ret[0].(*s3.DeleteObjectOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// CopyObjectWithContext indicates an expected call of CopyObjectWithContext.
-func (mr *MockClientMockRecorder) CopyObjectWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// DeleteObject indicates an expected call of DeleteObject.
+func (mr *MockClientMockRecorder) DeleteObject(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CopyObjectWithContext", reflect.TypeOf((*MockClient)(nil).CopyObjectWithContext), varargs...)
-}
-
-// CreateBucket mocks base method.
-func (m *MockClient) CreateBucket(arg0 *s3.CreateBucketInput) (*s3.CreateBucketOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateBucket", arg0)
- ret0, _ := ret[0].(*s3.CreateBucketOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// CreateBucket indicates an expected call of CreateBucket.
-func (mr *MockClientMockRecorder) CreateBucket(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucket", reflect.TypeOf((*MockClient)(nil).CreateBucket), arg0)
-}
-
-// CreateBucketRequest mocks base method.
-func (m *MockClient) CreateBucketRequest(arg0 *s3.CreateBucketInput) (*request.Request, *s3.CreateBucketOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateBucketRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.CreateBucketOutput)
- return ret0, ret1
-}
-
-// CreateBucketRequest indicates an expected call of CreateBucketRequest.
-func (mr *MockClientMockRecorder) CreateBucketRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucketRequest", reflect.TypeOf((*MockClient)(nil).CreateBucketRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObject", reflect.TypeOf((*MockClient)(nil).DeleteObject), varargs...)
}
-// CreateBucketWithContext mocks base method.
-func (m *MockClient) CreateBucketWithContext(arg0 context.Context, arg1 *s3.CreateBucketInput, arg2 ...request.Option) (*s3.CreateBucketOutput, error) {
+// DeleteObjects mocks base method.
+func (m *MockClient) DeleteObjects(arg0 context.Context, arg1 *s3.DeleteObjectsInput, arg2 ...func(*s3.Options)) (*s3.DeleteObjectsOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "CreateBucketWithContext", varargs...)
- ret0, _ := ret[0].(*s3.CreateBucketOutput)
+ ret := m.ctrl.Call(m, "DeleteObjects", varargs...)
+ ret0, _ := ret[0].(*s3.DeleteObjectsOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// CreateBucketWithContext indicates an expected call of CreateBucketWithContext.
-func (mr *MockClientMockRecorder) CreateBucketWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// DeleteObjects indicates an expected call of DeleteObjects.
+func (mr *MockClientMockRecorder) DeleteObjects(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBucketWithContext", reflect.TypeOf((*MockClient)(nil).CreateBucketWithContext), varargs...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjects", reflect.TypeOf((*MockClient)(nil).DeleteObjects), varargs...)
}
-// CreateMultipartUpload mocks base method.
-func (m *MockClient) CreateMultipartUpload(arg0 *s3.CreateMultipartUploadInput) (*s3.CreateMultipartUploadOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateMultipartUpload", arg0)
- ret0, _ := ret[0].(*s3.CreateMultipartUploadOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// CreateMultipartUpload indicates an expected call of CreateMultipartUpload.
-func (mr *MockClientMockRecorder) CreateMultipartUpload(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMultipartUpload", reflect.TypeOf((*MockClient)(nil).CreateMultipartUpload), arg0)
-}
-
-// CreateMultipartUploadRequest mocks base method.
-func (m *MockClient) CreateMultipartUploadRequest(arg0 *s3.CreateMultipartUploadInput) (*request.Request, *s3.CreateMultipartUploadOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateMultipartUploadRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.CreateMultipartUploadOutput)
- return ret0, ret1
-}
-
-// CreateMultipartUploadRequest indicates an expected call of CreateMultipartUploadRequest.
-func (mr *MockClientMockRecorder) CreateMultipartUploadRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMultipartUploadRequest", reflect.TypeOf((*MockClient)(nil).CreateMultipartUploadRequest), arg0)
-}
-
-// CreateMultipartUploadWithContext mocks base method.
-func (m *MockClient) CreateMultipartUploadWithContext(arg0 context.Context, arg1 *s3.CreateMultipartUploadInput, arg2 ...request.Option) (*s3.CreateMultipartUploadOutput, error) {
+// GetBucketLifecycleConfiguration mocks base method.
+func (m *MockClient) GetBucketLifecycleConfiguration(arg0 context.Context, arg1 *s3.GetBucketLifecycleConfigurationInput, arg2 ...func(*s3.Options)) (*s3.GetBucketLifecycleConfigurationOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "CreateMultipartUploadWithContext", varargs...)
- ret0, _ := ret[0].(*s3.CreateMultipartUploadOutput)
+ ret := m.ctrl.Call(m, "GetBucketLifecycleConfiguration", varargs...)
+ ret0, _ := ret[0].(*s3.GetBucketLifecycleConfigurationOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// CreateMultipartUploadWithContext indicates an expected call of CreateMultipartUploadWithContext.
-func (mr *MockClientMockRecorder) CreateMultipartUploadWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// GetBucketLifecycleConfiguration indicates an expected call of GetBucketLifecycleConfiguration.
+func (mr *MockClientMockRecorder) GetBucketLifecycleConfiguration(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateMultipartUploadWithContext", reflect.TypeOf((*MockClient)(nil).CreateMultipartUploadWithContext), varargs...)
-}
-
-// CreateSession mocks base method.
-func (m *MockClient) CreateSession(arg0 *s3.CreateSessionInput) (*s3.CreateSessionOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateSession", arg0)
- ret0, _ := ret[0].(*s3.CreateSessionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// CreateSession indicates an expected call of CreateSession.
-func (mr *MockClientMockRecorder) CreateSession(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSession", reflect.TypeOf((*MockClient)(nil).CreateSession), arg0)
-}
-
-// CreateSessionRequest mocks base method.
-func (m *MockClient) CreateSessionRequest(arg0 *s3.CreateSessionInput) (*request.Request, *s3.CreateSessionOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateSessionRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.CreateSessionOutput)
- return ret0, ret1
-}
-
-// CreateSessionRequest indicates an expected call of CreateSessionRequest.
-func (mr *MockClientMockRecorder) CreateSessionRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSessionRequest", reflect.TypeOf((*MockClient)(nil).CreateSessionRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLifecycleConfiguration", reflect.TypeOf((*MockClient)(nil).GetBucketLifecycleConfiguration), varargs...)
}
-// CreateSessionWithContext mocks base method.
-func (m *MockClient) CreateSessionWithContext(arg0 context.Context, arg1 *s3.CreateSessionInput, arg2 ...request.Option) (*s3.CreateSessionOutput, error) {
+// GetBucketPolicy mocks base method.
+func (m *MockClient) GetBucketPolicy(arg0 context.Context, arg1 *s3.GetBucketPolicyInput, arg2 ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "CreateSessionWithContext", varargs...)
- ret0, _ := ret[0].(*s3.CreateSessionOutput)
+ ret := m.ctrl.Call(m, "GetBucketPolicy", varargs...)
+ ret0, _ := ret[0].(*s3.GetBucketPolicyOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// CreateSessionWithContext indicates an expected call of CreateSessionWithContext.
-func (mr *MockClientMockRecorder) CreateSessionWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// GetBucketPolicy indicates an expected call of GetBucketPolicy.
+func (mr *MockClientMockRecorder) GetBucketPolicy(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateSessionWithContext", reflect.TypeOf((*MockClient)(nil).CreateSessionWithContext), varargs...)
-}
-
-// DeleteBucket mocks base method.
-func (m *MockClient) DeleteBucket(arg0 *s3.DeleteBucketInput) (*s3.DeleteBucketOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucket", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucket indicates an expected call of DeleteBucket.
-func (mr *MockClientMockRecorder) DeleteBucket(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucket", reflect.TypeOf((*MockClient)(nil).DeleteBucket), arg0)
-}
-
-// DeleteBucketAnalyticsConfiguration mocks base method.
-func (m *MockClient) DeleteBucketAnalyticsConfiguration(arg0 *s3.DeleteBucketAnalyticsConfigurationInput) (*s3.DeleteBucketAnalyticsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketAnalyticsConfiguration", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketAnalyticsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketAnalyticsConfiguration indicates an expected call of DeleteBucketAnalyticsConfiguration.
-func (mr *MockClientMockRecorder) DeleteBucketAnalyticsConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketAnalyticsConfiguration", reflect.TypeOf((*MockClient)(nil).DeleteBucketAnalyticsConfiguration), arg0)
-}
-
-// DeleteBucketAnalyticsConfigurationRequest mocks base method.
-func (m *MockClient) DeleteBucketAnalyticsConfigurationRequest(arg0 *s3.DeleteBucketAnalyticsConfigurationInput) (*request.Request, *s3.DeleteBucketAnalyticsConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketAnalyticsConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketAnalyticsConfigurationOutput)
- return ret0, ret1
-}
-
-// DeleteBucketAnalyticsConfigurationRequest indicates an expected call of DeleteBucketAnalyticsConfigurationRequest.
-func (mr *MockClientMockRecorder) DeleteBucketAnalyticsConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketAnalyticsConfigurationRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketAnalyticsConfigurationRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketPolicy", reflect.TypeOf((*MockClient)(nil).GetBucketPolicy), varargs...)
}
-// DeleteBucketAnalyticsConfigurationWithContext mocks base method.
-func (m *MockClient) DeleteBucketAnalyticsConfigurationWithContext(arg0 context.Context, arg1 *s3.DeleteBucketAnalyticsConfigurationInput, arg2 ...request.Option) (*s3.DeleteBucketAnalyticsConfigurationOutput, error) {
+// GetObject mocks base method.
+func (m *MockClient) GetObject(arg0 context.Context, arg1 *s3.GetObjectInput, arg2 ...func(*s3.Options)) (*s3.GetObjectOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DeleteBucketAnalyticsConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketAnalyticsConfigurationOutput)
+ ret := m.ctrl.Call(m, "GetObject", varargs...)
+ ret0, _ := ret[0].(*s3.GetObjectOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DeleteBucketAnalyticsConfigurationWithContext indicates an expected call of DeleteBucketAnalyticsConfigurationWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketAnalyticsConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// GetObject indicates an expected call of GetObject.
+func (mr *MockClientMockRecorder) GetObject(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketAnalyticsConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketAnalyticsConfigurationWithContext), varargs...)
-}
-
-// DeleteBucketCors mocks base method.
-func (m *MockClient) DeleteBucketCors(arg0 *s3.DeleteBucketCorsInput) (*s3.DeleteBucketCorsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketCors", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketCorsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketCors indicates an expected call of DeleteBucketCors.
-func (mr *MockClientMockRecorder) DeleteBucketCors(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketCors", reflect.TypeOf((*MockClient)(nil).DeleteBucketCors), arg0)
-}
-
-// DeleteBucketCorsRequest mocks base method.
-func (m *MockClient) DeleteBucketCorsRequest(arg0 *s3.DeleteBucketCorsInput) (*request.Request, *s3.DeleteBucketCorsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketCorsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketCorsOutput)
- return ret0, ret1
-}
-
-// DeleteBucketCorsRequest indicates an expected call of DeleteBucketCorsRequest.
-func (mr *MockClientMockRecorder) DeleteBucketCorsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketCorsRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketCorsRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockClient)(nil).GetObject), varargs...)
}
-// DeleteBucketCorsWithContext mocks base method.
-func (m *MockClient) DeleteBucketCorsWithContext(arg0 context.Context, arg1 *s3.DeleteBucketCorsInput, arg2 ...request.Option) (*s3.DeleteBucketCorsOutput, error) {
+// HeadBucket mocks base method.
+func (m *MockClient) HeadBucket(arg0 context.Context, arg1 *s3.HeadBucketInput, arg2 ...func(*s3.Options)) (*s3.HeadBucketOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DeleteBucketCorsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketCorsOutput)
+ ret := m.ctrl.Call(m, "HeadBucket", varargs...)
+ ret0, _ := ret[0].(*s3.HeadBucketOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DeleteBucketCorsWithContext indicates an expected call of DeleteBucketCorsWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketCorsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// HeadBucket indicates an expected call of HeadBucket.
+func (mr *MockClientMockRecorder) HeadBucket(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketCorsWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketCorsWithContext), varargs...)
-}
-
-// DeleteBucketEncryption mocks base method.
-func (m *MockClient) DeleteBucketEncryption(arg0 *s3.DeleteBucketEncryptionInput) (*s3.DeleteBucketEncryptionOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketEncryption", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketEncryptionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketEncryption indicates an expected call of DeleteBucketEncryption.
-func (mr *MockClientMockRecorder) DeleteBucketEncryption(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketEncryption", reflect.TypeOf((*MockClient)(nil).DeleteBucketEncryption), arg0)
-}
-
-// DeleteBucketEncryptionRequest mocks base method.
-func (m *MockClient) DeleteBucketEncryptionRequest(arg0 *s3.DeleteBucketEncryptionInput) (*request.Request, *s3.DeleteBucketEncryptionOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketEncryptionRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketEncryptionOutput)
- return ret0, ret1
-}
-
-// DeleteBucketEncryptionRequest indicates an expected call of DeleteBucketEncryptionRequest.
-func (mr *MockClientMockRecorder) DeleteBucketEncryptionRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketEncryptionRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketEncryptionRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadBucket", reflect.TypeOf((*MockClient)(nil).HeadBucket), varargs...)
}
-// DeleteBucketEncryptionWithContext mocks base method.
-func (m *MockClient) DeleteBucketEncryptionWithContext(arg0 context.Context, arg1 *s3.DeleteBucketEncryptionInput, arg2 ...request.Option) (*s3.DeleteBucketEncryptionOutput, error) {
+// HeadObject mocks base method.
+func (m *MockClient) HeadObject(arg0 context.Context, arg1 *s3.HeadObjectInput, arg2 ...func(*s3.Options)) (*s3.HeadObjectOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DeleteBucketEncryptionWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketEncryptionOutput)
+ ret := m.ctrl.Call(m, "HeadObject", varargs...)
+ ret0, _ := ret[0].(*s3.HeadObjectOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DeleteBucketEncryptionWithContext indicates an expected call of DeleteBucketEncryptionWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketEncryptionWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// HeadObject indicates an expected call of HeadObject.
+func (mr *MockClientMockRecorder) HeadObject(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketEncryptionWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketEncryptionWithContext), varargs...)
-}
-
-// DeleteBucketIntelligentTieringConfiguration mocks base method.
-func (m *MockClient) DeleteBucketIntelligentTieringConfiguration(arg0 *s3.DeleteBucketIntelligentTieringConfigurationInput) (*s3.DeleteBucketIntelligentTieringConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketIntelligentTieringConfiguration", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketIntelligentTieringConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketIntelligentTieringConfiguration indicates an expected call of DeleteBucketIntelligentTieringConfiguration.
-func (mr *MockClientMockRecorder) DeleteBucketIntelligentTieringConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketIntelligentTieringConfiguration", reflect.TypeOf((*MockClient)(nil).DeleteBucketIntelligentTieringConfiguration), arg0)
-}
-
-// DeleteBucketIntelligentTieringConfigurationRequest mocks base method.
-func (m *MockClient) DeleteBucketIntelligentTieringConfigurationRequest(arg0 *s3.DeleteBucketIntelligentTieringConfigurationInput) (*request.Request, *s3.DeleteBucketIntelligentTieringConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketIntelligentTieringConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketIntelligentTieringConfigurationOutput)
- return ret0, ret1
-}
-
-// DeleteBucketIntelligentTieringConfigurationRequest indicates an expected call of DeleteBucketIntelligentTieringConfigurationRequest.
-func (mr *MockClientMockRecorder) DeleteBucketIntelligentTieringConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketIntelligentTieringConfigurationRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketIntelligentTieringConfigurationRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadObject", reflect.TypeOf((*MockClient)(nil).HeadObject), varargs...)
}
-// DeleteBucketIntelligentTieringConfigurationWithContext mocks base method.
-func (m *MockClient) DeleteBucketIntelligentTieringConfigurationWithContext(arg0 context.Context, arg1 *s3.DeleteBucketIntelligentTieringConfigurationInput, arg2 ...request.Option) (*s3.DeleteBucketIntelligentTieringConfigurationOutput, error) {
+// ListObjectVersions mocks base method.
+func (m *MockClient) ListObjectVersions(arg0 context.Context, arg1 *s3.ListObjectVersionsInput, arg2 ...func(*s3.Options)) (*s3.ListObjectVersionsOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DeleteBucketIntelligentTieringConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketIntelligentTieringConfigurationOutput)
+ ret := m.ctrl.Call(m, "ListObjectVersions", varargs...)
+ ret0, _ := ret[0].(*s3.ListObjectVersionsOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DeleteBucketIntelligentTieringConfigurationWithContext indicates an expected call of DeleteBucketIntelligentTieringConfigurationWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketIntelligentTieringConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// ListObjectVersions indicates an expected call of ListObjectVersions.
+func (mr *MockClientMockRecorder) ListObjectVersions(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketIntelligentTieringConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketIntelligentTieringConfigurationWithContext), varargs...)
-}
-
-// DeleteBucketInventoryConfiguration mocks base method.
-func (m *MockClient) DeleteBucketInventoryConfiguration(arg0 *s3.DeleteBucketInventoryConfigurationInput) (*s3.DeleteBucketInventoryConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketInventoryConfiguration", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketInventoryConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketInventoryConfiguration indicates an expected call of DeleteBucketInventoryConfiguration.
-func (mr *MockClientMockRecorder) DeleteBucketInventoryConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketInventoryConfiguration", reflect.TypeOf((*MockClient)(nil).DeleteBucketInventoryConfiguration), arg0)
-}
-
-// DeleteBucketInventoryConfigurationRequest mocks base method.
-func (m *MockClient) DeleteBucketInventoryConfigurationRequest(arg0 *s3.DeleteBucketInventoryConfigurationInput) (*request.Request, *s3.DeleteBucketInventoryConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketInventoryConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketInventoryConfigurationOutput)
- return ret0, ret1
-}
-
-// DeleteBucketInventoryConfigurationRequest indicates an expected call of DeleteBucketInventoryConfigurationRequest.
-func (mr *MockClientMockRecorder) DeleteBucketInventoryConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketInventoryConfigurationRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketInventoryConfigurationRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectVersions", reflect.TypeOf((*MockClient)(nil).ListObjectVersions), varargs...)
}
-// DeleteBucketInventoryConfigurationWithContext mocks base method.
-func (m *MockClient) DeleteBucketInventoryConfigurationWithContext(arg0 context.Context, arg1 *s3.DeleteBucketInventoryConfigurationInput, arg2 ...request.Option) (*s3.DeleteBucketInventoryConfigurationOutput, error) {
+// ListObjectsV2 mocks base method.
+func (m *MockClient) ListObjectsV2(arg0 context.Context, arg1 *s3.ListObjectsV2Input, arg2 ...func(*s3.Options)) (*s3.ListObjectsV2Output, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DeleteBucketInventoryConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketInventoryConfigurationOutput)
+ ret := m.ctrl.Call(m, "ListObjectsV2", varargs...)
+ ret0, _ := ret[0].(*s3.ListObjectsV2Output)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DeleteBucketInventoryConfigurationWithContext indicates an expected call of DeleteBucketInventoryConfigurationWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketInventoryConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// ListObjectsV2 indicates an expected call of ListObjectsV2.
+func (mr *MockClientMockRecorder) ListObjectsV2(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketInventoryConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketInventoryConfigurationWithContext), varargs...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsV2", reflect.TypeOf((*MockClient)(nil).ListObjectsV2), varargs...)
}
-// DeleteBucketLifecycle mocks base method.
-func (m *MockClient) DeleteBucketLifecycle(arg0 *s3.DeleteBucketLifecycleInput) (*s3.DeleteBucketLifecycleOutput, error) {
+// PutObject mocks base method.
+func (m *MockClient) PutObject(arg0 context.Context, arg1 *s3.PutObjectInput, arg2 ...func(*s3.Options)) (*s3.PutObjectOutput, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketLifecycle", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketLifecycleOutput)
+ varargs := []any{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "PutObject", varargs...)
+ ret0, _ := ret[0].(*s3.PutObjectOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DeleteBucketLifecycle indicates an expected call of DeleteBucketLifecycle.
-func (mr *MockClientMockRecorder) DeleteBucketLifecycle(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketLifecycle", reflect.TypeOf((*MockClient)(nil).DeleteBucketLifecycle), arg0)
-}
-
-// DeleteBucketLifecycleRequest mocks base method.
-func (m *MockClient) DeleteBucketLifecycleRequest(arg0 *s3.DeleteBucketLifecycleInput) (*request.Request, *s3.DeleteBucketLifecycleOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketLifecycleRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketLifecycleOutput)
- return ret0, ret1
-}
-
-// DeleteBucketLifecycleRequest indicates an expected call of DeleteBucketLifecycleRequest.
-func (mr *MockClientMockRecorder) DeleteBucketLifecycleRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketLifecycleRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketLifecycleRequest), arg0)
-}
-
-// DeleteBucketLifecycleWithContext mocks base method.
-func (m *MockClient) DeleteBucketLifecycleWithContext(arg0 context.Context, arg1 *s3.DeleteBucketLifecycleInput, arg2 ...request.Option) (*s3.DeleteBucketLifecycleOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteBucketLifecycleWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketLifecycleOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketLifecycleWithContext indicates an expected call of DeleteBucketLifecycleWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketLifecycleWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketLifecycleWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketLifecycleWithContext), varargs...)
-}
-
-// DeleteBucketMetricsConfiguration mocks base method.
-func (m *MockClient) DeleteBucketMetricsConfiguration(arg0 *s3.DeleteBucketMetricsConfigurationInput) (*s3.DeleteBucketMetricsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketMetricsConfiguration", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketMetricsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketMetricsConfiguration indicates an expected call of DeleteBucketMetricsConfiguration.
-func (mr *MockClientMockRecorder) DeleteBucketMetricsConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketMetricsConfiguration", reflect.TypeOf((*MockClient)(nil).DeleteBucketMetricsConfiguration), arg0)
-}
-
-// DeleteBucketMetricsConfigurationRequest mocks base method.
-func (m *MockClient) DeleteBucketMetricsConfigurationRequest(arg0 *s3.DeleteBucketMetricsConfigurationInput) (*request.Request, *s3.DeleteBucketMetricsConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketMetricsConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketMetricsConfigurationOutput)
- return ret0, ret1
-}
-
-// DeleteBucketMetricsConfigurationRequest indicates an expected call of DeleteBucketMetricsConfigurationRequest.
-func (mr *MockClientMockRecorder) DeleteBucketMetricsConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketMetricsConfigurationRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketMetricsConfigurationRequest), arg0)
-}
-
-// DeleteBucketMetricsConfigurationWithContext mocks base method.
-func (m *MockClient) DeleteBucketMetricsConfigurationWithContext(arg0 context.Context, arg1 *s3.DeleteBucketMetricsConfigurationInput, arg2 ...request.Option) (*s3.DeleteBucketMetricsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteBucketMetricsConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketMetricsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketMetricsConfigurationWithContext indicates an expected call of DeleteBucketMetricsConfigurationWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketMetricsConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketMetricsConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketMetricsConfigurationWithContext), varargs...)
-}
-
-// DeleteBucketOwnershipControls mocks base method.
-func (m *MockClient) DeleteBucketOwnershipControls(arg0 *s3.DeleteBucketOwnershipControlsInput) (*s3.DeleteBucketOwnershipControlsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketOwnershipControls", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketOwnershipControlsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketOwnershipControls indicates an expected call of DeleteBucketOwnershipControls.
-func (mr *MockClientMockRecorder) DeleteBucketOwnershipControls(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketOwnershipControls", reflect.TypeOf((*MockClient)(nil).DeleteBucketOwnershipControls), arg0)
-}
-
-// DeleteBucketOwnershipControlsRequest mocks base method.
-func (m *MockClient) DeleteBucketOwnershipControlsRequest(arg0 *s3.DeleteBucketOwnershipControlsInput) (*request.Request, *s3.DeleteBucketOwnershipControlsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketOwnershipControlsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketOwnershipControlsOutput)
- return ret0, ret1
-}
-
-// DeleteBucketOwnershipControlsRequest indicates an expected call of DeleteBucketOwnershipControlsRequest.
-func (mr *MockClientMockRecorder) DeleteBucketOwnershipControlsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketOwnershipControlsRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketOwnershipControlsRequest), arg0)
-}
-
-// DeleteBucketOwnershipControlsWithContext mocks base method.
-func (m *MockClient) DeleteBucketOwnershipControlsWithContext(arg0 context.Context, arg1 *s3.DeleteBucketOwnershipControlsInput, arg2 ...request.Option) (*s3.DeleteBucketOwnershipControlsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteBucketOwnershipControlsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketOwnershipControlsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketOwnershipControlsWithContext indicates an expected call of DeleteBucketOwnershipControlsWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketOwnershipControlsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketOwnershipControlsWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketOwnershipControlsWithContext), varargs...)
-}
-
-// DeleteBucketPolicy mocks base method.
-func (m *MockClient) DeleteBucketPolicy(arg0 *s3.DeleteBucketPolicyInput) (*s3.DeleteBucketPolicyOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketPolicy", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketPolicyOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketPolicy indicates an expected call of DeleteBucketPolicy.
-func (mr *MockClientMockRecorder) DeleteBucketPolicy(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketPolicy", reflect.TypeOf((*MockClient)(nil).DeleteBucketPolicy), arg0)
-}
-
-// DeleteBucketPolicyRequest mocks base method.
-func (m *MockClient) DeleteBucketPolicyRequest(arg0 *s3.DeleteBucketPolicyInput) (*request.Request, *s3.DeleteBucketPolicyOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketPolicyRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketPolicyOutput)
- return ret0, ret1
-}
-
-// DeleteBucketPolicyRequest indicates an expected call of DeleteBucketPolicyRequest.
-func (mr *MockClientMockRecorder) DeleteBucketPolicyRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketPolicyRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketPolicyRequest), arg0)
-}
-
-// DeleteBucketPolicyWithContext mocks base method.
-func (m *MockClient) DeleteBucketPolicyWithContext(arg0 context.Context, arg1 *s3.DeleteBucketPolicyInput, arg2 ...request.Option) (*s3.DeleteBucketPolicyOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteBucketPolicyWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketPolicyOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketPolicyWithContext indicates an expected call of DeleteBucketPolicyWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketPolicyWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketPolicyWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketPolicyWithContext), varargs...)
-}
-
-// DeleteBucketReplication mocks base method.
-func (m *MockClient) DeleteBucketReplication(arg0 *s3.DeleteBucketReplicationInput) (*s3.DeleteBucketReplicationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketReplication", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketReplicationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketReplication indicates an expected call of DeleteBucketReplication.
-func (mr *MockClientMockRecorder) DeleteBucketReplication(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketReplication", reflect.TypeOf((*MockClient)(nil).DeleteBucketReplication), arg0)
-}
-
-// DeleteBucketReplicationRequest mocks base method.
-func (m *MockClient) DeleteBucketReplicationRequest(arg0 *s3.DeleteBucketReplicationInput) (*request.Request, *s3.DeleteBucketReplicationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketReplicationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketReplicationOutput)
- return ret0, ret1
-}
-
-// DeleteBucketReplicationRequest indicates an expected call of DeleteBucketReplicationRequest.
-func (mr *MockClientMockRecorder) DeleteBucketReplicationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketReplicationRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketReplicationRequest), arg0)
-}
-
-// DeleteBucketReplicationWithContext mocks base method.
-func (m *MockClient) DeleteBucketReplicationWithContext(arg0 context.Context, arg1 *s3.DeleteBucketReplicationInput, arg2 ...request.Option) (*s3.DeleteBucketReplicationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteBucketReplicationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketReplicationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketReplicationWithContext indicates an expected call of DeleteBucketReplicationWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketReplicationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketReplicationWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketReplicationWithContext), varargs...)
-}
-
-// DeleteBucketRequest mocks base method.
-func (m *MockClient) DeleteBucketRequest(arg0 *s3.DeleteBucketInput) (*request.Request, *s3.DeleteBucketOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketOutput)
- return ret0, ret1
-}
-
-// DeleteBucketRequest indicates an expected call of DeleteBucketRequest.
-func (mr *MockClientMockRecorder) DeleteBucketRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketRequest), arg0)
-}
-
-// DeleteBucketTagging mocks base method.
-func (m *MockClient) DeleteBucketTagging(arg0 *s3.DeleteBucketTaggingInput) (*s3.DeleteBucketTaggingOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketTagging", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketTagging indicates an expected call of DeleteBucketTagging.
-func (mr *MockClientMockRecorder) DeleteBucketTagging(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketTagging", reflect.TypeOf((*MockClient)(nil).DeleteBucketTagging), arg0)
-}
-
-// DeleteBucketTaggingRequest mocks base method.
-func (m *MockClient) DeleteBucketTaggingRequest(arg0 *s3.DeleteBucketTaggingInput) (*request.Request, *s3.DeleteBucketTaggingOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketTaggingRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketTaggingOutput)
- return ret0, ret1
-}
-
-// DeleteBucketTaggingRequest indicates an expected call of DeleteBucketTaggingRequest.
-func (mr *MockClientMockRecorder) DeleteBucketTaggingRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketTaggingRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketTaggingRequest), arg0)
-}
-
-// DeleteBucketTaggingWithContext mocks base method.
-func (m *MockClient) DeleteBucketTaggingWithContext(arg0 context.Context, arg1 *s3.DeleteBucketTaggingInput, arg2 ...request.Option) (*s3.DeleteBucketTaggingOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteBucketTaggingWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketTaggingWithContext indicates an expected call of DeleteBucketTaggingWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketTaggingWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketTaggingWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketTaggingWithContext), varargs...)
-}
-
-// DeleteBucketWebsite mocks base method.
-func (m *MockClient) DeleteBucketWebsite(arg0 *s3.DeleteBucketWebsiteInput) (*s3.DeleteBucketWebsiteOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketWebsite", arg0)
- ret0, _ := ret[0].(*s3.DeleteBucketWebsiteOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketWebsite indicates an expected call of DeleteBucketWebsite.
-func (mr *MockClientMockRecorder) DeleteBucketWebsite(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketWebsite", reflect.TypeOf((*MockClient)(nil).DeleteBucketWebsite), arg0)
-}
-
-// DeleteBucketWebsiteRequest mocks base method.
-func (m *MockClient) DeleteBucketWebsiteRequest(arg0 *s3.DeleteBucketWebsiteInput) (*request.Request, *s3.DeleteBucketWebsiteOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBucketWebsiteRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteBucketWebsiteOutput)
- return ret0, ret1
-}
-
-// DeleteBucketWebsiteRequest indicates an expected call of DeleteBucketWebsiteRequest.
-func (mr *MockClientMockRecorder) DeleteBucketWebsiteRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketWebsiteRequest", reflect.TypeOf((*MockClient)(nil).DeleteBucketWebsiteRequest), arg0)
-}
-
-// DeleteBucketWebsiteWithContext mocks base method.
-func (m *MockClient) DeleteBucketWebsiteWithContext(arg0 context.Context, arg1 *s3.DeleteBucketWebsiteInput, arg2 ...request.Option) (*s3.DeleteBucketWebsiteOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteBucketWebsiteWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketWebsiteOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketWebsiteWithContext indicates an expected call of DeleteBucketWebsiteWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketWebsiteWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketWebsiteWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketWebsiteWithContext), varargs...)
-}
-
-// DeleteBucketWithContext mocks base method.
-func (m *MockClient) DeleteBucketWithContext(arg0 context.Context, arg1 *s3.DeleteBucketInput, arg2 ...request.Option) (*s3.DeleteBucketOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteBucketWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteBucketOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBucketWithContext indicates an expected call of DeleteBucketWithContext.
-func (mr *MockClientMockRecorder) DeleteBucketWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBucketWithContext", reflect.TypeOf((*MockClient)(nil).DeleteBucketWithContext), varargs...)
-}
-
-// DeleteObject mocks base method.
-func (m *MockClient) DeleteObject(arg0 *s3.DeleteObjectInput) (*s3.DeleteObjectOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteObject", arg0)
- ret0, _ := ret[0].(*s3.DeleteObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteObject indicates an expected call of DeleteObject.
-func (mr *MockClientMockRecorder) DeleteObject(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObject", reflect.TypeOf((*MockClient)(nil).DeleteObject), arg0)
-}
-
-// DeleteObjectRequest mocks base method.
-func (m *MockClient) DeleteObjectRequest(arg0 *s3.DeleteObjectInput) (*request.Request, *s3.DeleteObjectOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteObjectRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteObjectOutput)
- return ret0, ret1
-}
-
-// DeleteObjectRequest indicates an expected call of DeleteObjectRequest.
-func (mr *MockClientMockRecorder) DeleteObjectRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjectRequest", reflect.TypeOf((*MockClient)(nil).DeleteObjectRequest), arg0)
-}
-
-// DeleteObjectTagging mocks base method.
-func (m *MockClient) DeleteObjectTagging(arg0 *s3.DeleteObjectTaggingInput) (*s3.DeleteObjectTaggingOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteObjectTagging", arg0)
- ret0, _ := ret[0].(*s3.DeleteObjectTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteObjectTagging indicates an expected call of DeleteObjectTagging.
-func (mr *MockClientMockRecorder) DeleteObjectTagging(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjectTagging", reflect.TypeOf((*MockClient)(nil).DeleteObjectTagging), arg0)
-}
-
-// DeleteObjectTaggingRequest mocks base method.
-func (m *MockClient) DeleteObjectTaggingRequest(arg0 *s3.DeleteObjectTaggingInput) (*request.Request, *s3.DeleteObjectTaggingOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteObjectTaggingRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteObjectTaggingOutput)
- return ret0, ret1
-}
-
-// DeleteObjectTaggingRequest indicates an expected call of DeleteObjectTaggingRequest.
-func (mr *MockClientMockRecorder) DeleteObjectTaggingRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjectTaggingRequest", reflect.TypeOf((*MockClient)(nil).DeleteObjectTaggingRequest), arg0)
-}
-
-// DeleteObjectTaggingWithContext mocks base method.
-func (m *MockClient) DeleteObjectTaggingWithContext(arg0 context.Context, arg1 *s3.DeleteObjectTaggingInput, arg2 ...request.Option) (*s3.DeleteObjectTaggingOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteObjectTaggingWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteObjectTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteObjectTaggingWithContext indicates an expected call of DeleteObjectTaggingWithContext.
-func (mr *MockClientMockRecorder) DeleteObjectTaggingWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjectTaggingWithContext", reflect.TypeOf((*MockClient)(nil).DeleteObjectTaggingWithContext), varargs...)
-}
-
-// DeleteObjectWithContext mocks base method.
-func (m *MockClient) DeleteObjectWithContext(arg0 context.Context, arg1 *s3.DeleteObjectInput, arg2 ...request.Option) (*s3.DeleteObjectOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteObjectWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteObjectWithContext indicates an expected call of DeleteObjectWithContext.
-func (mr *MockClientMockRecorder) DeleteObjectWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjectWithContext", reflect.TypeOf((*MockClient)(nil).DeleteObjectWithContext), varargs...)
-}
-
-// DeleteObjects mocks base method.
-func (m *MockClient) DeleteObjects(arg0 *s3.DeleteObjectsInput) (*s3.DeleteObjectsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteObjects", arg0)
- ret0, _ := ret[0].(*s3.DeleteObjectsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteObjects indicates an expected call of DeleteObjects.
-func (mr *MockClientMockRecorder) DeleteObjects(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjects", reflect.TypeOf((*MockClient)(nil).DeleteObjects), arg0)
-}
-
-// DeleteObjectsRequest mocks base method.
-func (m *MockClient) DeleteObjectsRequest(arg0 *s3.DeleteObjectsInput) (*request.Request, *s3.DeleteObjectsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteObjectsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeleteObjectsOutput)
- return ret0, ret1
-}
-
-// DeleteObjectsRequest indicates an expected call of DeleteObjectsRequest.
-func (mr *MockClientMockRecorder) DeleteObjectsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjectsRequest", reflect.TypeOf((*MockClient)(nil).DeleteObjectsRequest), arg0)
-}
-
-// DeleteObjectsWithContext mocks base method.
-func (m *MockClient) DeleteObjectsWithContext(arg0 context.Context, arg1 *s3.DeleteObjectsInput, arg2 ...request.Option) (*s3.DeleteObjectsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeleteObjectsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeleteObjectsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteObjectsWithContext indicates an expected call of DeleteObjectsWithContext.
-func (mr *MockClientMockRecorder) DeleteObjectsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteObjectsWithContext", reflect.TypeOf((*MockClient)(nil).DeleteObjectsWithContext), varargs...)
-}
-
-// DeletePublicAccessBlock mocks base method.
-func (m *MockClient) DeletePublicAccessBlock(arg0 *s3.DeletePublicAccessBlockInput) (*s3.DeletePublicAccessBlockOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeletePublicAccessBlock", arg0)
- ret0, _ := ret[0].(*s3.DeletePublicAccessBlockOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeletePublicAccessBlock indicates an expected call of DeletePublicAccessBlock.
-func (mr *MockClientMockRecorder) DeletePublicAccessBlock(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePublicAccessBlock", reflect.TypeOf((*MockClient)(nil).DeletePublicAccessBlock), arg0)
-}
-
-// DeletePublicAccessBlockRequest mocks base method.
-func (m *MockClient) DeletePublicAccessBlockRequest(arg0 *s3.DeletePublicAccessBlockInput) (*request.Request, *s3.DeletePublicAccessBlockOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeletePublicAccessBlockRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.DeletePublicAccessBlockOutput)
- return ret0, ret1
-}
-
-// DeletePublicAccessBlockRequest indicates an expected call of DeletePublicAccessBlockRequest.
-func (mr *MockClientMockRecorder) DeletePublicAccessBlockRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePublicAccessBlockRequest", reflect.TypeOf((*MockClient)(nil).DeletePublicAccessBlockRequest), arg0)
-}
-
-// DeletePublicAccessBlockWithContext mocks base method.
-func (m *MockClient) DeletePublicAccessBlockWithContext(arg0 context.Context, arg1 *s3.DeletePublicAccessBlockInput, arg2 ...request.Option) (*s3.DeletePublicAccessBlockOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DeletePublicAccessBlockWithContext", varargs...)
- ret0, _ := ret[0].(*s3.DeletePublicAccessBlockOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeletePublicAccessBlockWithContext indicates an expected call of DeletePublicAccessBlockWithContext.
-func (mr *MockClientMockRecorder) DeletePublicAccessBlockWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeletePublicAccessBlockWithContext", reflect.TypeOf((*MockClient)(nil).DeletePublicAccessBlockWithContext), varargs...)
-}
-
-// GetBucketAccelerateConfiguration mocks base method.
-func (m *MockClient) GetBucketAccelerateConfiguration(arg0 *s3.GetBucketAccelerateConfigurationInput) (*s3.GetBucketAccelerateConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketAccelerateConfiguration", arg0)
- ret0, _ := ret[0].(*s3.GetBucketAccelerateConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketAccelerateConfiguration indicates an expected call of GetBucketAccelerateConfiguration.
-func (mr *MockClientMockRecorder) GetBucketAccelerateConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketAccelerateConfiguration", reflect.TypeOf((*MockClient)(nil).GetBucketAccelerateConfiguration), arg0)
-}
-
-// GetBucketAccelerateConfigurationRequest mocks base method.
-func (m *MockClient) GetBucketAccelerateConfigurationRequest(arg0 *s3.GetBucketAccelerateConfigurationInput) (*request.Request, *s3.GetBucketAccelerateConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketAccelerateConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketAccelerateConfigurationOutput)
- return ret0, ret1
-}
-
-// GetBucketAccelerateConfigurationRequest indicates an expected call of GetBucketAccelerateConfigurationRequest.
-func (mr *MockClientMockRecorder) GetBucketAccelerateConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketAccelerateConfigurationRequest", reflect.TypeOf((*MockClient)(nil).GetBucketAccelerateConfigurationRequest), arg0)
-}
-
-// GetBucketAccelerateConfigurationWithContext mocks base method.
-func (m *MockClient) GetBucketAccelerateConfigurationWithContext(arg0 context.Context, arg1 *s3.GetBucketAccelerateConfigurationInput, arg2 ...request.Option) (*s3.GetBucketAccelerateConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketAccelerateConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketAccelerateConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketAccelerateConfigurationWithContext indicates an expected call of GetBucketAccelerateConfigurationWithContext.
-func (mr *MockClientMockRecorder) GetBucketAccelerateConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketAccelerateConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketAccelerateConfigurationWithContext), varargs...)
-}
-
-// GetBucketAcl mocks base method.
-func (m *MockClient) GetBucketAcl(arg0 *s3.GetBucketAclInput) (*s3.GetBucketAclOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketAcl", arg0)
- ret0, _ := ret[0].(*s3.GetBucketAclOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketAcl indicates an expected call of GetBucketAcl.
-func (mr *MockClientMockRecorder) GetBucketAcl(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketAcl", reflect.TypeOf((*MockClient)(nil).GetBucketAcl), arg0)
-}
-
-// GetBucketAclRequest mocks base method.
-func (m *MockClient) GetBucketAclRequest(arg0 *s3.GetBucketAclInput) (*request.Request, *s3.GetBucketAclOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketAclRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketAclOutput)
- return ret0, ret1
-}
-
-// GetBucketAclRequest indicates an expected call of GetBucketAclRequest.
-func (mr *MockClientMockRecorder) GetBucketAclRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketAclRequest", reflect.TypeOf((*MockClient)(nil).GetBucketAclRequest), arg0)
-}
-
-// GetBucketAclWithContext mocks base method.
-func (m *MockClient) GetBucketAclWithContext(arg0 context.Context, arg1 *s3.GetBucketAclInput, arg2 ...request.Option) (*s3.GetBucketAclOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketAclWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketAclOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketAclWithContext indicates an expected call of GetBucketAclWithContext.
-func (mr *MockClientMockRecorder) GetBucketAclWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketAclWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketAclWithContext), varargs...)
-}
-
-// GetBucketAnalyticsConfiguration mocks base method.
-func (m *MockClient) GetBucketAnalyticsConfiguration(arg0 *s3.GetBucketAnalyticsConfigurationInput) (*s3.GetBucketAnalyticsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketAnalyticsConfiguration", arg0)
- ret0, _ := ret[0].(*s3.GetBucketAnalyticsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketAnalyticsConfiguration indicates an expected call of GetBucketAnalyticsConfiguration.
-func (mr *MockClientMockRecorder) GetBucketAnalyticsConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketAnalyticsConfiguration", reflect.TypeOf((*MockClient)(nil).GetBucketAnalyticsConfiguration), arg0)
-}
-
-// GetBucketAnalyticsConfigurationRequest mocks base method.
-func (m *MockClient) GetBucketAnalyticsConfigurationRequest(arg0 *s3.GetBucketAnalyticsConfigurationInput) (*request.Request, *s3.GetBucketAnalyticsConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketAnalyticsConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketAnalyticsConfigurationOutput)
- return ret0, ret1
-}
-
-// GetBucketAnalyticsConfigurationRequest indicates an expected call of GetBucketAnalyticsConfigurationRequest.
-func (mr *MockClientMockRecorder) GetBucketAnalyticsConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketAnalyticsConfigurationRequest", reflect.TypeOf((*MockClient)(nil).GetBucketAnalyticsConfigurationRequest), arg0)
-}
-
-// GetBucketAnalyticsConfigurationWithContext mocks base method.
-func (m *MockClient) GetBucketAnalyticsConfigurationWithContext(arg0 context.Context, arg1 *s3.GetBucketAnalyticsConfigurationInput, arg2 ...request.Option) (*s3.GetBucketAnalyticsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketAnalyticsConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketAnalyticsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketAnalyticsConfigurationWithContext indicates an expected call of GetBucketAnalyticsConfigurationWithContext.
-func (mr *MockClientMockRecorder) GetBucketAnalyticsConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketAnalyticsConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketAnalyticsConfigurationWithContext), varargs...)
-}
-
-// GetBucketCors mocks base method.
-func (m *MockClient) GetBucketCors(arg0 *s3.GetBucketCorsInput) (*s3.GetBucketCorsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketCors", arg0)
- ret0, _ := ret[0].(*s3.GetBucketCorsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketCors indicates an expected call of GetBucketCors.
-func (mr *MockClientMockRecorder) GetBucketCors(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketCors", reflect.TypeOf((*MockClient)(nil).GetBucketCors), arg0)
-}
-
-// GetBucketCorsRequest mocks base method.
-func (m *MockClient) GetBucketCorsRequest(arg0 *s3.GetBucketCorsInput) (*request.Request, *s3.GetBucketCorsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketCorsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketCorsOutput)
- return ret0, ret1
-}
-
-// GetBucketCorsRequest indicates an expected call of GetBucketCorsRequest.
-func (mr *MockClientMockRecorder) GetBucketCorsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketCorsRequest", reflect.TypeOf((*MockClient)(nil).GetBucketCorsRequest), arg0)
-}
-
-// GetBucketCorsWithContext mocks base method.
-func (m *MockClient) GetBucketCorsWithContext(arg0 context.Context, arg1 *s3.GetBucketCorsInput, arg2 ...request.Option) (*s3.GetBucketCorsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketCorsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketCorsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketCorsWithContext indicates an expected call of GetBucketCorsWithContext.
-func (mr *MockClientMockRecorder) GetBucketCorsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketCorsWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketCorsWithContext), varargs...)
-}
-
-// GetBucketEncryption mocks base method.
-func (m *MockClient) GetBucketEncryption(arg0 *s3.GetBucketEncryptionInput) (*s3.GetBucketEncryptionOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketEncryption", arg0)
- ret0, _ := ret[0].(*s3.GetBucketEncryptionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketEncryption indicates an expected call of GetBucketEncryption.
-func (mr *MockClientMockRecorder) GetBucketEncryption(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketEncryption", reflect.TypeOf((*MockClient)(nil).GetBucketEncryption), arg0)
-}
-
-// GetBucketEncryptionRequest mocks base method.
-func (m *MockClient) GetBucketEncryptionRequest(arg0 *s3.GetBucketEncryptionInput) (*request.Request, *s3.GetBucketEncryptionOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketEncryptionRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketEncryptionOutput)
- return ret0, ret1
-}
-
-// GetBucketEncryptionRequest indicates an expected call of GetBucketEncryptionRequest.
-func (mr *MockClientMockRecorder) GetBucketEncryptionRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketEncryptionRequest", reflect.TypeOf((*MockClient)(nil).GetBucketEncryptionRequest), arg0)
-}
-
-// GetBucketEncryptionWithContext mocks base method.
-func (m *MockClient) GetBucketEncryptionWithContext(arg0 context.Context, arg1 *s3.GetBucketEncryptionInput, arg2 ...request.Option) (*s3.GetBucketEncryptionOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketEncryptionWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketEncryptionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketEncryptionWithContext indicates an expected call of GetBucketEncryptionWithContext.
-func (mr *MockClientMockRecorder) GetBucketEncryptionWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketEncryptionWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketEncryptionWithContext), varargs...)
-}
-
-// GetBucketIntelligentTieringConfiguration mocks base method.
-func (m *MockClient) GetBucketIntelligentTieringConfiguration(arg0 *s3.GetBucketIntelligentTieringConfigurationInput) (*s3.GetBucketIntelligentTieringConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketIntelligentTieringConfiguration", arg0)
- ret0, _ := ret[0].(*s3.GetBucketIntelligentTieringConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketIntelligentTieringConfiguration indicates an expected call of GetBucketIntelligentTieringConfiguration.
-func (mr *MockClientMockRecorder) GetBucketIntelligentTieringConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketIntelligentTieringConfiguration", reflect.TypeOf((*MockClient)(nil).GetBucketIntelligentTieringConfiguration), arg0)
-}
-
-// GetBucketIntelligentTieringConfigurationRequest mocks base method.
-func (m *MockClient) GetBucketIntelligentTieringConfigurationRequest(arg0 *s3.GetBucketIntelligentTieringConfigurationInput) (*request.Request, *s3.GetBucketIntelligentTieringConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketIntelligentTieringConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketIntelligentTieringConfigurationOutput)
- return ret0, ret1
-}
-
-// GetBucketIntelligentTieringConfigurationRequest indicates an expected call of GetBucketIntelligentTieringConfigurationRequest.
-func (mr *MockClientMockRecorder) GetBucketIntelligentTieringConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketIntelligentTieringConfigurationRequest", reflect.TypeOf((*MockClient)(nil).GetBucketIntelligentTieringConfigurationRequest), arg0)
-}
-
-// GetBucketIntelligentTieringConfigurationWithContext mocks base method.
-func (m *MockClient) GetBucketIntelligentTieringConfigurationWithContext(arg0 context.Context, arg1 *s3.GetBucketIntelligentTieringConfigurationInput, arg2 ...request.Option) (*s3.GetBucketIntelligentTieringConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketIntelligentTieringConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketIntelligentTieringConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketIntelligentTieringConfigurationWithContext indicates an expected call of GetBucketIntelligentTieringConfigurationWithContext.
-func (mr *MockClientMockRecorder) GetBucketIntelligentTieringConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketIntelligentTieringConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketIntelligentTieringConfigurationWithContext), varargs...)
-}
-
-// GetBucketInventoryConfiguration mocks base method.
-func (m *MockClient) GetBucketInventoryConfiguration(arg0 *s3.GetBucketInventoryConfigurationInput) (*s3.GetBucketInventoryConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketInventoryConfiguration", arg0)
- ret0, _ := ret[0].(*s3.GetBucketInventoryConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketInventoryConfiguration indicates an expected call of GetBucketInventoryConfiguration.
-func (mr *MockClientMockRecorder) GetBucketInventoryConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketInventoryConfiguration", reflect.TypeOf((*MockClient)(nil).GetBucketInventoryConfiguration), arg0)
-}
-
-// GetBucketInventoryConfigurationRequest mocks base method.
-func (m *MockClient) GetBucketInventoryConfigurationRequest(arg0 *s3.GetBucketInventoryConfigurationInput) (*request.Request, *s3.GetBucketInventoryConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketInventoryConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketInventoryConfigurationOutput)
- return ret0, ret1
-}
-
-// GetBucketInventoryConfigurationRequest indicates an expected call of GetBucketInventoryConfigurationRequest.
-func (mr *MockClientMockRecorder) GetBucketInventoryConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketInventoryConfigurationRequest", reflect.TypeOf((*MockClient)(nil).GetBucketInventoryConfigurationRequest), arg0)
-}
-
-// GetBucketInventoryConfigurationWithContext mocks base method.
-func (m *MockClient) GetBucketInventoryConfigurationWithContext(arg0 context.Context, arg1 *s3.GetBucketInventoryConfigurationInput, arg2 ...request.Option) (*s3.GetBucketInventoryConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketInventoryConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketInventoryConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketInventoryConfigurationWithContext indicates an expected call of GetBucketInventoryConfigurationWithContext.
-func (mr *MockClientMockRecorder) GetBucketInventoryConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketInventoryConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketInventoryConfigurationWithContext), varargs...)
-}
-
-// GetBucketLifecycle mocks base method.
-func (m *MockClient) GetBucketLifecycle(arg0 *s3.GetBucketLifecycleInput) (*s3.GetBucketLifecycleOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketLifecycle", arg0)
- ret0, _ := ret[0].(*s3.GetBucketLifecycleOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketLifecycle indicates an expected call of GetBucketLifecycle.
-func (mr *MockClientMockRecorder) GetBucketLifecycle(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLifecycle", reflect.TypeOf((*MockClient)(nil).GetBucketLifecycle), arg0)
-}
-
-// GetBucketLifecycleConfiguration mocks base method.
-func (m *MockClient) GetBucketLifecycleConfiguration(arg0 *s3.GetBucketLifecycleConfigurationInput) (*s3.GetBucketLifecycleConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketLifecycleConfiguration", arg0)
- ret0, _ := ret[0].(*s3.GetBucketLifecycleConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketLifecycleConfiguration indicates an expected call of GetBucketLifecycleConfiguration.
-func (mr *MockClientMockRecorder) GetBucketLifecycleConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLifecycleConfiguration", reflect.TypeOf((*MockClient)(nil).GetBucketLifecycleConfiguration), arg0)
-}
-
-// GetBucketLifecycleConfigurationRequest mocks base method.
-func (m *MockClient) GetBucketLifecycleConfigurationRequest(arg0 *s3.GetBucketLifecycleConfigurationInput) (*request.Request, *s3.GetBucketLifecycleConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketLifecycleConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketLifecycleConfigurationOutput)
- return ret0, ret1
-}
-
-// GetBucketLifecycleConfigurationRequest indicates an expected call of GetBucketLifecycleConfigurationRequest.
-func (mr *MockClientMockRecorder) GetBucketLifecycleConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLifecycleConfigurationRequest", reflect.TypeOf((*MockClient)(nil).GetBucketLifecycleConfigurationRequest), arg0)
-}
-
-// GetBucketLifecycleConfigurationWithContext mocks base method.
-func (m *MockClient) GetBucketLifecycleConfigurationWithContext(arg0 context.Context, arg1 *s3.GetBucketLifecycleConfigurationInput, arg2 ...request.Option) (*s3.GetBucketLifecycleConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketLifecycleConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketLifecycleConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketLifecycleConfigurationWithContext indicates an expected call of GetBucketLifecycleConfigurationWithContext.
-func (mr *MockClientMockRecorder) GetBucketLifecycleConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLifecycleConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketLifecycleConfigurationWithContext), varargs...)
-}
-
-// GetBucketLifecycleRequest mocks base method.
-func (m *MockClient) GetBucketLifecycleRequest(arg0 *s3.GetBucketLifecycleInput) (*request.Request, *s3.GetBucketLifecycleOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketLifecycleRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketLifecycleOutput)
- return ret0, ret1
-}
-
-// GetBucketLifecycleRequest indicates an expected call of GetBucketLifecycleRequest.
-func (mr *MockClientMockRecorder) GetBucketLifecycleRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLifecycleRequest", reflect.TypeOf((*MockClient)(nil).GetBucketLifecycleRequest), arg0)
-}
-
-// GetBucketLifecycleWithContext mocks base method.
-func (m *MockClient) GetBucketLifecycleWithContext(arg0 context.Context, arg1 *s3.GetBucketLifecycleInput, arg2 ...request.Option) (*s3.GetBucketLifecycleOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketLifecycleWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketLifecycleOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketLifecycleWithContext indicates an expected call of GetBucketLifecycleWithContext.
-func (mr *MockClientMockRecorder) GetBucketLifecycleWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLifecycleWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketLifecycleWithContext), varargs...)
-}
-
-// GetBucketLocation mocks base method.
-func (m *MockClient) GetBucketLocation(arg0 *s3.GetBucketLocationInput) (*s3.GetBucketLocationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketLocation", arg0)
- ret0, _ := ret[0].(*s3.GetBucketLocationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketLocation indicates an expected call of GetBucketLocation.
-func (mr *MockClientMockRecorder) GetBucketLocation(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLocation", reflect.TypeOf((*MockClient)(nil).GetBucketLocation), arg0)
-}
-
-// GetBucketLocationRequest mocks base method.
-func (m *MockClient) GetBucketLocationRequest(arg0 *s3.GetBucketLocationInput) (*request.Request, *s3.GetBucketLocationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketLocationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketLocationOutput)
- return ret0, ret1
-}
-
-// GetBucketLocationRequest indicates an expected call of GetBucketLocationRequest.
-func (mr *MockClientMockRecorder) GetBucketLocationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLocationRequest", reflect.TypeOf((*MockClient)(nil).GetBucketLocationRequest), arg0)
-}
-
-// GetBucketLocationWithContext mocks base method.
-func (m *MockClient) GetBucketLocationWithContext(arg0 context.Context, arg1 *s3.GetBucketLocationInput, arg2 ...request.Option) (*s3.GetBucketLocationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketLocationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketLocationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketLocationWithContext indicates an expected call of GetBucketLocationWithContext.
-func (mr *MockClientMockRecorder) GetBucketLocationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLocationWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketLocationWithContext), varargs...)
-}
-
-// GetBucketLogging mocks base method.
-func (m *MockClient) GetBucketLogging(arg0 *s3.GetBucketLoggingInput) (*s3.GetBucketLoggingOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketLogging", arg0)
- ret0, _ := ret[0].(*s3.GetBucketLoggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketLogging indicates an expected call of GetBucketLogging.
-func (mr *MockClientMockRecorder) GetBucketLogging(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLogging", reflect.TypeOf((*MockClient)(nil).GetBucketLogging), arg0)
-}
-
-// GetBucketLoggingRequest mocks base method.
-func (m *MockClient) GetBucketLoggingRequest(arg0 *s3.GetBucketLoggingInput) (*request.Request, *s3.GetBucketLoggingOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketLoggingRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketLoggingOutput)
- return ret0, ret1
-}
-
-// GetBucketLoggingRequest indicates an expected call of GetBucketLoggingRequest.
-func (mr *MockClientMockRecorder) GetBucketLoggingRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLoggingRequest", reflect.TypeOf((*MockClient)(nil).GetBucketLoggingRequest), arg0)
-}
-
-// GetBucketLoggingWithContext mocks base method.
-func (m *MockClient) GetBucketLoggingWithContext(arg0 context.Context, arg1 *s3.GetBucketLoggingInput, arg2 ...request.Option) (*s3.GetBucketLoggingOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketLoggingWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketLoggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketLoggingWithContext indicates an expected call of GetBucketLoggingWithContext.
-func (mr *MockClientMockRecorder) GetBucketLoggingWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketLoggingWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketLoggingWithContext), varargs...)
-}
-
-// GetBucketMetricsConfiguration mocks base method.
-func (m *MockClient) GetBucketMetricsConfiguration(arg0 *s3.GetBucketMetricsConfigurationInput) (*s3.GetBucketMetricsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketMetricsConfiguration", arg0)
- ret0, _ := ret[0].(*s3.GetBucketMetricsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketMetricsConfiguration indicates an expected call of GetBucketMetricsConfiguration.
-func (mr *MockClientMockRecorder) GetBucketMetricsConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketMetricsConfiguration", reflect.TypeOf((*MockClient)(nil).GetBucketMetricsConfiguration), arg0)
-}
-
-// GetBucketMetricsConfigurationRequest mocks base method.
-func (m *MockClient) GetBucketMetricsConfigurationRequest(arg0 *s3.GetBucketMetricsConfigurationInput) (*request.Request, *s3.GetBucketMetricsConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketMetricsConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketMetricsConfigurationOutput)
- return ret0, ret1
-}
-
-// GetBucketMetricsConfigurationRequest indicates an expected call of GetBucketMetricsConfigurationRequest.
-func (mr *MockClientMockRecorder) GetBucketMetricsConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketMetricsConfigurationRequest", reflect.TypeOf((*MockClient)(nil).GetBucketMetricsConfigurationRequest), arg0)
-}
-
-// GetBucketMetricsConfigurationWithContext mocks base method.
-func (m *MockClient) GetBucketMetricsConfigurationWithContext(arg0 context.Context, arg1 *s3.GetBucketMetricsConfigurationInput, arg2 ...request.Option) (*s3.GetBucketMetricsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketMetricsConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketMetricsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketMetricsConfigurationWithContext indicates an expected call of GetBucketMetricsConfigurationWithContext.
-func (mr *MockClientMockRecorder) GetBucketMetricsConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketMetricsConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketMetricsConfigurationWithContext), varargs...)
-}
-
-// GetBucketNotification mocks base method.
-func (m *MockClient) GetBucketNotification(arg0 *s3.GetBucketNotificationConfigurationRequest) (*s3.NotificationConfigurationDeprecated, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketNotification", arg0)
- ret0, _ := ret[0].(*s3.NotificationConfigurationDeprecated)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketNotification indicates an expected call of GetBucketNotification.
-func (mr *MockClientMockRecorder) GetBucketNotification(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketNotification", reflect.TypeOf((*MockClient)(nil).GetBucketNotification), arg0)
-}
-
-// GetBucketNotificationConfiguration mocks base method.
-func (m *MockClient) GetBucketNotificationConfiguration(arg0 *s3.GetBucketNotificationConfigurationRequest) (*s3.NotificationConfiguration, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketNotificationConfiguration", arg0)
- ret0, _ := ret[0].(*s3.NotificationConfiguration)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketNotificationConfiguration indicates an expected call of GetBucketNotificationConfiguration.
-func (mr *MockClientMockRecorder) GetBucketNotificationConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketNotificationConfiguration", reflect.TypeOf((*MockClient)(nil).GetBucketNotificationConfiguration), arg0)
-}
-
-// GetBucketNotificationConfigurationRequest mocks base method.
-func (m *MockClient) GetBucketNotificationConfigurationRequest(arg0 *s3.GetBucketNotificationConfigurationRequest) (*request.Request, *s3.NotificationConfiguration) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketNotificationConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.NotificationConfiguration)
- return ret0, ret1
-}
-
-// GetBucketNotificationConfigurationRequest indicates an expected call of GetBucketNotificationConfigurationRequest.
-func (mr *MockClientMockRecorder) GetBucketNotificationConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketNotificationConfigurationRequest", reflect.TypeOf((*MockClient)(nil).GetBucketNotificationConfigurationRequest), arg0)
-}
-
-// GetBucketNotificationConfigurationWithContext mocks base method.
-func (m *MockClient) GetBucketNotificationConfigurationWithContext(arg0 context.Context, arg1 *s3.GetBucketNotificationConfigurationRequest, arg2 ...request.Option) (*s3.NotificationConfiguration, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketNotificationConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.NotificationConfiguration)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketNotificationConfigurationWithContext indicates an expected call of GetBucketNotificationConfigurationWithContext.
-func (mr *MockClientMockRecorder) GetBucketNotificationConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketNotificationConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketNotificationConfigurationWithContext), varargs...)
-}
-
-// GetBucketNotificationRequest mocks base method.
-func (m *MockClient) GetBucketNotificationRequest(arg0 *s3.GetBucketNotificationConfigurationRequest) (*request.Request, *s3.NotificationConfigurationDeprecated) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketNotificationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.NotificationConfigurationDeprecated)
- return ret0, ret1
-}
-
-// GetBucketNotificationRequest indicates an expected call of GetBucketNotificationRequest.
-func (mr *MockClientMockRecorder) GetBucketNotificationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketNotificationRequest", reflect.TypeOf((*MockClient)(nil).GetBucketNotificationRequest), arg0)
-}
-
-// GetBucketNotificationWithContext mocks base method.
-func (m *MockClient) GetBucketNotificationWithContext(arg0 context.Context, arg1 *s3.GetBucketNotificationConfigurationRequest, arg2 ...request.Option) (*s3.NotificationConfigurationDeprecated, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketNotificationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.NotificationConfigurationDeprecated)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketNotificationWithContext indicates an expected call of GetBucketNotificationWithContext.
-func (mr *MockClientMockRecorder) GetBucketNotificationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketNotificationWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketNotificationWithContext), varargs...)
-}
-
-// GetBucketOwnershipControls mocks base method.
-func (m *MockClient) GetBucketOwnershipControls(arg0 *s3.GetBucketOwnershipControlsInput) (*s3.GetBucketOwnershipControlsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketOwnershipControls", arg0)
- ret0, _ := ret[0].(*s3.GetBucketOwnershipControlsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketOwnershipControls indicates an expected call of GetBucketOwnershipControls.
-func (mr *MockClientMockRecorder) GetBucketOwnershipControls(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketOwnershipControls", reflect.TypeOf((*MockClient)(nil).GetBucketOwnershipControls), arg0)
-}
-
-// GetBucketOwnershipControlsRequest mocks base method.
-func (m *MockClient) GetBucketOwnershipControlsRequest(arg0 *s3.GetBucketOwnershipControlsInput) (*request.Request, *s3.GetBucketOwnershipControlsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketOwnershipControlsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketOwnershipControlsOutput)
- return ret0, ret1
-}
-
-// GetBucketOwnershipControlsRequest indicates an expected call of GetBucketOwnershipControlsRequest.
-func (mr *MockClientMockRecorder) GetBucketOwnershipControlsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketOwnershipControlsRequest", reflect.TypeOf((*MockClient)(nil).GetBucketOwnershipControlsRequest), arg0)
-}
-
-// GetBucketOwnershipControlsWithContext mocks base method.
-func (m *MockClient) GetBucketOwnershipControlsWithContext(arg0 context.Context, arg1 *s3.GetBucketOwnershipControlsInput, arg2 ...request.Option) (*s3.GetBucketOwnershipControlsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketOwnershipControlsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketOwnershipControlsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketOwnershipControlsWithContext indicates an expected call of GetBucketOwnershipControlsWithContext.
-func (mr *MockClientMockRecorder) GetBucketOwnershipControlsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketOwnershipControlsWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketOwnershipControlsWithContext), varargs...)
-}
-
-// GetBucketPolicy mocks base method.
-func (m *MockClient) GetBucketPolicy(arg0 *s3.GetBucketPolicyInput) (*s3.GetBucketPolicyOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketPolicy", arg0)
- ret0, _ := ret[0].(*s3.GetBucketPolicyOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketPolicy indicates an expected call of GetBucketPolicy.
-func (mr *MockClientMockRecorder) GetBucketPolicy(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketPolicy", reflect.TypeOf((*MockClient)(nil).GetBucketPolicy), arg0)
-}
-
-// GetBucketPolicyRequest mocks base method.
-func (m *MockClient) GetBucketPolicyRequest(arg0 *s3.GetBucketPolicyInput) (*request.Request, *s3.GetBucketPolicyOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketPolicyRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketPolicyOutput)
- return ret0, ret1
-}
-
-// GetBucketPolicyRequest indicates an expected call of GetBucketPolicyRequest.
-func (mr *MockClientMockRecorder) GetBucketPolicyRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketPolicyRequest", reflect.TypeOf((*MockClient)(nil).GetBucketPolicyRequest), arg0)
-}
-
-// GetBucketPolicyStatus mocks base method.
-func (m *MockClient) GetBucketPolicyStatus(arg0 *s3.GetBucketPolicyStatusInput) (*s3.GetBucketPolicyStatusOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketPolicyStatus", arg0)
- ret0, _ := ret[0].(*s3.GetBucketPolicyStatusOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketPolicyStatus indicates an expected call of GetBucketPolicyStatus.
-func (mr *MockClientMockRecorder) GetBucketPolicyStatus(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketPolicyStatus", reflect.TypeOf((*MockClient)(nil).GetBucketPolicyStatus), arg0)
-}
-
-// GetBucketPolicyStatusRequest mocks base method.
-func (m *MockClient) GetBucketPolicyStatusRequest(arg0 *s3.GetBucketPolicyStatusInput) (*request.Request, *s3.GetBucketPolicyStatusOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketPolicyStatusRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketPolicyStatusOutput)
- return ret0, ret1
-}
-
-// GetBucketPolicyStatusRequest indicates an expected call of GetBucketPolicyStatusRequest.
-func (mr *MockClientMockRecorder) GetBucketPolicyStatusRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketPolicyStatusRequest", reflect.TypeOf((*MockClient)(nil).GetBucketPolicyStatusRequest), arg0)
-}
-
-// GetBucketPolicyStatusWithContext mocks base method.
-func (m *MockClient) GetBucketPolicyStatusWithContext(arg0 context.Context, arg1 *s3.GetBucketPolicyStatusInput, arg2 ...request.Option) (*s3.GetBucketPolicyStatusOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketPolicyStatusWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketPolicyStatusOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketPolicyStatusWithContext indicates an expected call of GetBucketPolicyStatusWithContext.
-func (mr *MockClientMockRecorder) GetBucketPolicyStatusWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketPolicyStatusWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketPolicyStatusWithContext), varargs...)
-}
-
-// GetBucketPolicyWithContext mocks base method.
-func (m *MockClient) GetBucketPolicyWithContext(arg0 context.Context, arg1 *s3.GetBucketPolicyInput, arg2 ...request.Option) (*s3.GetBucketPolicyOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketPolicyWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketPolicyOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketPolicyWithContext indicates an expected call of GetBucketPolicyWithContext.
-func (mr *MockClientMockRecorder) GetBucketPolicyWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketPolicyWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketPolicyWithContext), varargs...)
-}
-
-// GetBucketReplication mocks base method.
-func (m *MockClient) GetBucketReplication(arg0 *s3.GetBucketReplicationInput) (*s3.GetBucketReplicationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketReplication", arg0)
- ret0, _ := ret[0].(*s3.GetBucketReplicationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketReplication indicates an expected call of GetBucketReplication.
-func (mr *MockClientMockRecorder) GetBucketReplication(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketReplication", reflect.TypeOf((*MockClient)(nil).GetBucketReplication), arg0)
-}
-
-// GetBucketReplicationRequest mocks base method.
-func (m *MockClient) GetBucketReplicationRequest(arg0 *s3.GetBucketReplicationInput) (*request.Request, *s3.GetBucketReplicationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketReplicationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketReplicationOutput)
- return ret0, ret1
-}
-
-// GetBucketReplicationRequest indicates an expected call of GetBucketReplicationRequest.
-func (mr *MockClientMockRecorder) GetBucketReplicationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketReplicationRequest", reflect.TypeOf((*MockClient)(nil).GetBucketReplicationRequest), arg0)
-}
-
-// GetBucketReplicationWithContext mocks base method.
-func (m *MockClient) GetBucketReplicationWithContext(arg0 context.Context, arg1 *s3.GetBucketReplicationInput, arg2 ...request.Option) (*s3.GetBucketReplicationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketReplicationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketReplicationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketReplicationWithContext indicates an expected call of GetBucketReplicationWithContext.
-func (mr *MockClientMockRecorder) GetBucketReplicationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketReplicationWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketReplicationWithContext), varargs...)
-}
-
-// GetBucketRequestPayment mocks base method.
-func (m *MockClient) GetBucketRequestPayment(arg0 *s3.GetBucketRequestPaymentInput) (*s3.GetBucketRequestPaymentOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketRequestPayment", arg0)
- ret0, _ := ret[0].(*s3.GetBucketRequestPaymentOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketRequestPayment indicates an expected call of GetBucketRequestPayment.
-func (mr *MockClientMockRecorder) GetBucketRequestPayment(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketRequestPayment", reflect.TypeOf((*MockClient)(nil).GetBucketRequestPayment), arg0)
-}
-
-// GetBucketRequestPaymentRequest mocks base method.
-func (m *MockClient) GetBucketRequestPaymentRequest(arg0 *s3.GetBucketRequestPaymentInput) (*request.Request, *s3.GetBucketRequestPaymentOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketRequestPaymentRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketRequestPaymentOutput)
- return ret0, ret1
-}
-
-// GetBucketRequestPaymentRequest indicates an expected call of GetBucketRequestPaymentRequest.
-func (mr *MockClientMockRecorder) GetBucketRequestPaymentRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketRequestPaymentRequest", reflect.TypeOf((*MockClient)(nil).GetBucketRequestPaymentRequest), arg0)
-}
-
-// GetBucketRequestPaymentWithContext mocks base method.
-func (m *MockClient) GetBucketRequestPaymentWithContext(arg0 context.Context, arg1 *s3.GetBucketRequestPaymentInput, arg2 ...request.Option) (*s3.GetBucketRequestPaymentOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketRequestPaymentWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketRequestPaymentOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketRequestPaymentWithContext indicates an expected call of GetBucketRequestPaymentWithContext.
-func (mr *MockClientMockRecorder) GetBucketRequestPaymentWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketRequestPaymentWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketRequestPaymentWithContext), varargs...)
-}
-
-// GetBucketTagging mocks base method.
-func (m *MockClient) GetBucketTagging(arg0 *s3.GetBucketTaggingInput) (*s3.GetBucketTaggingOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketTagging", arg0)
- ret0, _ := ret[0].(*s3.GetBucketTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketTagging indicates an expected call of GetBucketTagging.
-func (mr *MockClientMockRecorder) GetBucketTagging(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketTagging", reflect.TypeOf((*MockClient)(nil).GetBucketTagging), arg0)
-}
-
-// GetBucketTaggingRequest mocks base method.
-func (m *MockClient) GetBucketTaggingRequest(arg0 *s3.GetBucketTaggingInput) (*request.Request, *s3.GetBucketTaggingOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketTaggingRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketTaggingOutput)
- return ret0, ret1
-}
-
-// GetBucketTaggingRequest indicates an expected call of GetBucketTaggingRequest.
-func (mr *MockClientMockRecorder) GetBucketTaggingRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketTaggingRequest", reflect.TypeOf((*MockClient)(nil).GetBucketTaggingRequest), arg0)
-}
-
-// GetBucketTaggingWithContext mocks base method.
-func (m *MockClient) GetBucketTaggingWithContext(arg0 context.Context, arg1 *s3.GetBucketTaggingInput, arg2 ...request.Option) (*s3.GetBucketTaggingOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketTaggingWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketTaggingWithContext indicates an expected call of GetBucketTaggingWithContext.
-func (mr *MockClientMockRecorder) GetBucketTaggingWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketTaggingWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketTaggingWithContext), varargs...)
-}
-
-// GetBucketVersioning mocks base method.
-func (m *MockClient) GetBucketVersioning(arg0 *s3.GetBucketVersioningInput) (*s3.GetBucketVersioningOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketVersioning", arg0)
- ret0, _ := ret[0].(*s3.GetBucketVersioningOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketVersioning indicates an expected call of GetBucketVersioning.
-func (mr *MockClientMockRecorder) GetBucketVersioning(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketVersioning", reflect.TypeOf((*MockClient)(nil).GetBucketVersioning), arg0)
-}
-
-// GetBucketVersioningRequest mocks base method.
-func (m *MockClient) GetBucketVersioningRequest(arg0 *s3.GetBucketVersioningInput) (*request.Request, *s3.GetBucketVersioningOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketVersioningRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketVersioningOutput)
- return ret0, ret1
-}
-
-// GetBucketVersioningRequest indicates an expected call of GetBucketVersioningRequest.
-func (mr *MockClientMockRecorder) GetBucketVersioningRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketVersioningRequest", reflect.TypeOf((*MockClient)(nil).GetBucketVersioningRequest), arg0)
-}
-
-// GetBucketVersioningWithContext mocks base method.
-func (m *MockClient) GetBucketVersioningWithContext(arg0 context.Context, arg1 *s3.GetBucketVersioningInput, arg2 ...request.Option) (*s3.GetBucketVersioningOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketVersioningWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketVersioningOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketVersioningWithContext indicates an expected call of GetBucketVersioningWithContext.
-func (mr *MockClientMockRecorder) GetBucketVersioningWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketVersioningWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketVersioningWithContext), varargs...)
-}
-
-// GetBucketWebsite mocks base method.
-func (m *MockClient) GetBucketWebsite(arg0 *s3.GetBucketWebsiteInput) (*s3.GetBucketWebsiteOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketWebsite", arg0)
- ret0, _ := ret[0].(*s3.GetBucketWebsiteOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketWebsite indicates an expected call of GetBucketWebsite.
-func (mr *MockClientMockRecorder) GetBucketWebsite(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketWebsite", reflect.TypeOf((*MockClient)(nil).GetBucketWebsite), arg0)
-}
-
-// GetBucketWebsiteRequest mocks base method.
-func (m *MockClient) GetBucketWebsiteRequest(arg0 *s3.GetBucketWebsiteInput) (*request.Request, *s3.GetBucketWebsiteOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetBucketWebsiteRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetBucketWebsiteOutput)
- return ret0, ret1
-}
-
-// GetBucketWebsiteRequest indicates an expected call of GetBucketWebsiteRequest.
-func (mr *MockClientMockRecorder) GetBucketWebsiteRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketWebsiteRequest", reflect.TypeOf((*MockClient)(nil).GetBucketWebsiteRequest), arg0)
-}
-
-// GetBucketWebsiteWithContext mocks base method.
-func (m *MockClient) GetBucketWebsiteWithContext(arg0 context.Context, arg1 *s3.GetBucketWebsiteInput, arg2 ...request.Option) (*s3.GetBucketWebsiteOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetBucketWebsiteWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetBucketWebsiteOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetBucketWebsiteWithContext indicates an expected call of GetBucketWebsiteWithContext.
-func (mr *MockClientMockRecorder) GetBucketWebsiteWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBucketWebsiteWithContext", reflect.TypeOf((*MockClient)(nil).GetBucketWebsiteWithContext), varargs...)
-}
-
-// GetObject mocks base method.
-func (m *MockClient) GetObject(arg0 *s3.GetObjectInput) (*s3.GetObjectOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObject", arg0)
- ret0, _ := ret[0].(*s3.GetObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObject indicates an expected call of GetObject.
-func (mr *MockClientMockRecorder) GetObject(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObject", reflect.TypeOf((*MockClient)(nil).GetObject), arg0)
-}
-
-// GetObjectAcl mocks base method.
-func (m *MockClient) GetObjectAcl(arg0 *s3.GetObjectAclInput) (*s3.GetObjectAclOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectAcl", arg0)
- ret0, _ := ret[0].(*s3.GetObjectAclOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectAcl indicates an expected call of GetObjectAcl.
-func (mr *MockClientMockRecorder) GetObjectAcl(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectAcl", reflect.TypeOf((*MockClient)(nil).GetObjectAcl), arg0)
-}
-
-// GetObjectAclRequest mocks base method.
-func (m *MockClient) GetObjectAclRequest(arg0 *s3.GetObjectAclInput) (*request.Request, *s3.GetObjectAclOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectAclRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetObjectAclOutput)
- return ret0, ret1
-}
-
-// GetObjectAclRequest indicates an expected call of GetObjectAclRequest.
-func (mr *MockClientMockRecorder) GetObjectAclRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectAclRequest", reflect.TypeOf((*MockClient)(nil).GetObjectAclRequest), arg0)
-}
-
-// GetObjectAclWithContext mocks base method.
-func (m *MockClient) GetObjectAclWithContext(arg0 context.Context, arg1 *s3.GetObjectAclInput, arg2 ...request.Option) (*s3.GetObjectAclOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetObjectAclWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetObjectAclOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectAclWithContext indicates an expected call of GetObjectAclWithContext.
-func (mr *MockClientMockRecorder) GetObjectAclWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectAclWithContext", reflect.TypeOf((*MockClient)(nil).GetObjectAclWithContext), varargs...)
-}
-
-// GetObjectAttributes mocks base method.
-func (m *MockClient) GetObjectAttributes(arg0 *s3.GetObjectAttributesInput) (*s3.GetObjectAttributesOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectAttributes", arg0)
- ret0, _ := ret[0].(*s3.GetObjectAttributesOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectAttributes indicates an expected call of GetObjectAttributes.
-func (mr *MockClientMockRecorder) GetObjectAttributes(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectAttributes", reflect.TypeOf((*MockClient)(nil).GetObjectAttributes), arg0)
-}
-
-// GetObjectAttributesRequest mocks base method.
-func (m *MockClient) GetObjectAttributesRequest(arg0 *s3.GetObjectAttributesInput) (*request.Request, *s3.GetObjectAttributesOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectAttributesRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetObjectAttributesOutput)
- return ret0, ret1
-}
-
-// GetObjectAttributesRequest indicates an expected call of GetObjectAttributesRequest.
-func (mr *MockClientMockRecorder) GetObjectAttributesRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectAttributesRequest", reflect.TypeOf((*MockClient)(nil).GetObjectAttributesRequest), arg0)
-}
-
-// GetObjectAttributesWithContext mocks base method.
-func (m *MockClient) GetObjectAttributesWithContext(arg0 context.Context, arg1 *s3.GetObjectAttributesInput, arg2 ...request.Option) (*s3.GetObjectAttributesOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetObjectAttributesWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetObjectAttributesOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectAttributesWithContext indicates an expected call of GetObjectAttributesWithContext.
-func (mr *MockClientMockRecorder) GetObjectAttributesWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectAttributesWithContext", reflect.TypeOf((*MockClient)(nil).GetObjectAttributesWithContext), varargs...)
-}
-
-// GetObjectLegalHold mocks base method.
-func (m *MockClient) GetObjectLegalHold(arg0 *s3.GetObjectLegalHoldInput) (*s3.GetObjectLegalHoldOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectLegalHold", arg0)
- ret0, _ := ret[0].(*s3.GetObjectLegalHoldOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectLegalHold indicates an expected call of GetObjectLegalHold.
-func (mr *MockClientMockRecorder) GetObjectLegalHold(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectLegalHold", reflect.TypeOf((*MockClient)(nil).GetObjectLegalHold), arg0)
-}
-
-// GetObjectLegalHoldRequest mocks base method.
-func (m *MockClient) GetObjectLegalHoldRequest(arg0 *s3.GetObjectLegalHoldInput) (*request.Request, *s3.GetObjectLegalHoldOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectLegalHoldRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetObjectLegalHoldOutput)
- return ret0, ret1
-}
-
-// GetObjectLegalHoldRequest indicates an expected call of GetObjectLegalHoldRequest.
-func (mr *MockClientMockRecorder) GetObjectLegalHoldRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectLegalHoldRequest", reflect.TypeOf((*MockClient)(nil).GetObjectLegalHoldRequest), arg0)
-}
-
-// GetObjectLegalHoldWithContext mocks base method.
-func (m *MockClient) GetObjectLegalHoldWithContext(arg0 context.Context, arg1 *s3.GetObjectLegalHoldInput, arg2 ...request.Option) (*s3.GetObjectLegalHoldOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetObjectLegalHoldWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetObjectLegalHoldOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectLegalHoldWithContext indicates an expected call of GetObjectLegalHoldWithContext.
-func (mr *MockClientMockRecorder) GetObjectLegalHoldWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectLegalHoldWithContext", reflect.TypeOf((*MockClient)(nil).GetObjectLegalHoldWithContext), varargs...)
-}
-
-// GetObjectLockConfiguration mocks base method.
-func (m *MockClient) GetObjectLockConfiguration(arg0 *s3.GetObjectLockConfigurationInput) (*s3.GetObjectLockConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectLockConfiguration", arg0)
- ret0, _ := ret[0].(*s3.GetObjectLockConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectLockConfiguration indicates an expected call of GetObjectLockConfiguration.
-func (mr *MockClientMockRecorder) GetObjectLockConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectLockConfiguration", reflect.TypeOf((*MockClient)(nil).GetObjectLockConfiguration), arg0)
-}
-
-// GetObjectLockConfigurationRequest mocks base method.
-func (m *MockClient) GetObjectLockConfigurationRequest(arg0 *s3.GetObjectLockConfigurationInput) (*request.Request, *s3.GetObjectLockConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectLockConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetObjectLockConfigurationOutput)
- return ret0, ret1
-}
-
-// GetObjectLockConfigurationRequest indicates an expected call of GetObjectLockConfigurationRequest.
-func (mr *MockClientMockRecorder) GetObjectLockConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectLockConfigurationRequest", reflect.TypeOf((*MockClient)(nil).GetObjectLockConfigurationRequest), arg0)
-}
-
-// GetObjectLockConfigurationWithContext mocks base method.
-func (m *MockClient) GetObjectLockConfigurationWithContext(arg0 context.Context, arg1 *s3.GetObjectLockConfigurationInput, arg2 ...request.Option) (*s3.GetObjectLockConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetObjectLockConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetObjectLockConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectLockConfigurationWithContext indicates an expected call of GetObjectLockConfigurationWithContext.
-func (mr *MockClientMockRecorder) GetObjectLockConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectLockConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).GetObjectLockConfigurationWithContext), varargs...)
-}
-
-// GetObjectRequest mocks base method.
-func (m *MockClient) GetObjectRequest(arg0 *s3.GetObjectInput) (*request.Request, *s3.GetObjectOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetObjectOutput)
- return ret0, ret1
-}
-
-// GetObjectRequest indicates an expected call of GetObjectRequest.
-func (mr *MockClientMockRecorder) GetObjectRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectRequest", reflect.TypeOf((*MockClient)(nil).GetObjectRequest), arg0)
-}
-
-// GetObjectRetention mocks base method.
-func (m *MockClient) GetObjectRetention(arg0 *s3.GetObjectRetentionInput) (*s3.GetObjectRetentionOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectRetention", arg0)
- ret0, _ := ret[0].(*s3.GetObjectRetentionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectRetention indicates an expected call of GetObjectRetention.
-func (mr *MockClientMockRecorder) GetObjectRetention(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectRetention", reflect.TypeOf((*MockClient)(nil).GetObjectRetention), arg0)
-}
-
-// GetObjectRetentionRequest mocks base method.
-func (m *MockClient) GetObjectRetentionRequest(arg0 *s3.GetObjectRetentionInput) (*request.Request, *s3.GetObjectRetentionOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectRetentionRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetObjectRetentionOutput)
- return ret0, ret1
-}
-
-// GetObjectRetentionRequest indicates an expected call of GetObjectRetentionRequest.
-func (mr *MockClientMockRecorder) GetObjectRetentionRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectRetentionRequest", reflect.TypeOf((*MockClient)(nil).GetObjectRetentionRequest), arg0)
-}
-
-// GetObjectRetentionWithContext mocks base method.
-func (m *MockClient) GetObjectRetentionWithContext(arg0 context.Context, arg1 *s3.GetObjectRetentionInput, arg2 ...request.Option) (*s3.GetObjectRetentionOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetObjectRetentionWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetObjectRetentionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectRetentionWithContext indicates an expected call of GetObjectRetentionWithContext.
-func (mr *MockClientMockRecorder) GetObjectRetentionWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectRetentionWithContext", reflect.TypeOf((*MockClient)(nil).GetObjectRetentionWithContext), varargs...)
-}
-
-// GetObjectTagging mocks base method.
-func (m *MockClient) GetObjectTagging(arg0 *s3.GetObjectTaggingInput) (*s3.GetObjectTaggingOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectTagging", arg0)
- ret0, _ := ret[0].(*s3.GetObjectTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectTagging indicates an expected call of GetObjectTagging.
-func (mr *MockClientMockRecorder) GetObjectTagging(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectTagging", reflect.TypeOf((*MockClient)(nil).GetObjectTagging), arg0)
-}
-
-// GetObjectTaggingRequest mocks base method.
-func (m *MockClient) GetObjectTaggingRequest(arg0 *s3.GetObjectTaggingInput) (*request.Request, *s3.GetObjectTaggingOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectTaggingRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetObjectTaggingOutput)
- return ret0, ret1
-}
-
-// GetObjectTaggingRequest indicates an expected call of GetObjectTaggingRequest.
-func (mr *MockClientMockRecorder) GetObjectTaggingRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectTaggingRequest", reflect.TypeOf((*MockClient)(nil).GetObjectTaggingRequest), arg0)
-}
-
-// GetObjectTaggingWithContext mocks base method.
-func (m *MockClient) GetObjectTaggingWithContext(arg0 context.Context, arg1 *s3.GetObjectTaggingInput, arg2 ...request.Option) (*s3.GetObjectTaggingOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetObjectTaggingWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetObjectTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectTaggingWithContext indicates an expected call of GetObjectTaggingWithContext.
-func (mr *MockClientMockRecorder) GetObjectTaggingWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectTaggingWithContext", reflect.TypeOf((*MockClient)(nil).GetObjectTaggingWithContext), varargs...)
-}
-
-// GetObjectTorrent mocks base method.
-func (m *MockClient) GetObjectTorrent(arg0 *s3.GetObjectTorrentInput) (*s3.GetObjectTorrentOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectTorrent", arg0)
- ret0, _ := ret[0].(*s3.GetObjectTorrentOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectTorrent indicates an expected call of GetObjectTorrent.
-func (mr *MockClientMockRecorder) GetObjectTorrent(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectTorrent", reflect.TypeOf((*MockClient)(nil).GetObjectTorrent), arg0)
-}
-
-// GetObjectTorrentRequest mocks base method.
-func (m *MockClient) GetObjectTorrentRequest(arg0 *s3.GetObjectTorrentInput) (*request.Request, *s3.GetObjectTorrentOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetObjectTorrentRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetObjectTorrentOutput)
- return ret0, ret1
-}
-
-// GetObjectTorrentRequest indicates an expected call of GetObjectTorrentRequest.
-func (mr *MockClientMockRecorder) GetObjectTorrentRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectTorrentRequest", reflect.TypeOf((*MockClient)(nil).GetObjectTorrentRequest), arg0)
-}
-
-// GetObjectTorrentWithContext mocks base method.
-func (m *MockClient) GetObjectTorrentWithContext(arg0 context.Context, arg1 *s3.GetObjectTorrentInput, arg2 ...request.Option) (*s3.GetObjectTorrentOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetObjectTorrentWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetObjectTorrentOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectTorrentWithContext indicates an expected call of GetObjectTorrentWithContext.
-func (mr *MockClientMockRecorder) GetObjectTorrentWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectTorrentWithContext", reflect.TypeOf((*MockClient)(nil).GetObjectTorrentWithContext), varargs...)
-}
-
-// GetObjectWithContext mocks base method.
-func (m *MockClient) GetObjectWithContext(arg0 context.Context, arg1 *s3.GetObjectInput, arg2 ...request.Option) (*s3.GetObjectOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetObjectWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetObjectWithContext indicates an expected call of GetObjectWithContext.
-func (mr *MockClientMockRecorder) GetObjectWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetObjectWithContext", reflect.TypeOf((*MockClient)(nil).GetObjectWithContext), varargs...)
-}
-
-// GetPublicAccessBlock mocks base method.
-func (m *MockClient) GetPublicAccessBlock(arg0 *s3.GetPublicAccessBlockInput) (*s3.GetPublicAccessBlockOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetPublicAccessBlock", arg0)
- ret0, _ := ret[0].(*s3.GetPublicAccessBlockOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetPublicAccessBlock indicates an expected call of GetPublicAccessBlock.
-func (mr *MockClientMockRecorder) GetPublicAccessBlock(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicAccessBlock", reflect.TypeOf((*MockClient)(nil).GetPublicAccessBlock), arg0)
-}
-
-// GetPublicAccessBlockRequest mocks base method.
-func (m *MockClient) GetPublicAccessBlockRequest(arg0 *s3.GetPublicAccessBlockInput) (*request.Request, *s3.GetPublicAccessBlockOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetPublicAccessBlockRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.GetPublicAccessBlockOutput)
- return ret0, ret1
-}
-
-// GetPublicAccessBlockRequest indicates an expected call of GetPublicAccessBlockRequest.
-func (mr *MockClientMockRecorder) GetPublicAccessBlockRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicAccessBlockRequest", reflect.TypeOf((*MockClient)(nil).GetPublicAccessBlockRequest), arg0)
-}
-
-// GetPublicAccessBlockWithContext mocks base method.
-func (m *MockClient) GetPublicAccessBlockWithContext(arg0 context.Context, arg1 *s3.GetPublicAccessBlockInput, arg2 ...request.Option) (*s3.GetPublicAccessBlockOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetPublicAccessBlockWithContext", varargs...)
- ret0, _ := ret[0].(*s3.GetPublicAccessBlockOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetPublicAccessBlockWithContext indicates an expected call of GetPublicAccessBlockWithContext.
-func (mr *MockClientMockRecorder) GetPublicAccessBlockWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetPublicAccessBlockWithContext", reflect.TypeOf((*MockClient)(nil).GetPublicAccessBlockWithContext), varargs...)
-}
-
-// HeadBucket mocks base method.
-func (m *MockClient) HeadBucket(arg0 *s3.HeadBucketInput) (*s3.HeadBucketOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "HeadBucket", arg0)
- ret0, _ := ret[0].(*s3.HeadBucketOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// HeadBucket indicates an expected call of HeadBucket.
-func (mr *MockClientMockRecorder) HeadBucket(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadBucket", reflect.TypeOf((*MockClient)(nil).HeadBucket), arg0)
-}
-
-// HeadBucketRequest mocks base method.
-func (m *MockClient) HeadBucketRequest(arg0 *s3.HeadBucketInput) (*request.Request, *s3.HeadBucketOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "HeadBucketRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.HeadBucketOutput)
- return ret0, ret1
-}
-
-// HeadBucketRequest indicates an expected call of HeadBucketRequest.
-func (mr *MockClientMockRecorder) HeadBucketRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadBucketRequest", reflect.TypeOf((*MockClient)(nil).HeadBucketRequest), arg0)
-}
-
-// HeadBucketWithContext mocks base method.
-func (m *MockClient) HeadBucketWithContext(arg0 context.Context, arg1 *s3.HeadBucketInput, arg2 ...request.Option) (*s3.HeadBucketOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "HeadBucketWithContext", varargs...)
- ret0, _ := ret[0].(*s3.HeadBucketOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// HeadBucketWithContext indicates an expected call of HeadBucketWithContext.
-func (mr *MockClientMockRecorder) HeadBucketWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadBucketWithContext", reflect.TypeOf((*MockClient)(nil).HeadBucketWithContext), varargs...)
-}
-
-// HeadObject mocks base method.
-func (m *MockClient) HeadObject(arg0 *s3.HeadObjectInput) (*s3.HeadObjectOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "HeadObject", arg0)
- ret0, _ := ret[0].(*s3.HeadObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// HeadObject indicates an expected call of HeadObject.
-func (mr *MockClientMockRecorder) HeadObject(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadObject", reflect.TypeOf((*MockClient)(nil).HeadObject), arg0)
-}
-
-// HeadObjectRequest mocks base method.
-func (m *MockClient) HeadObjectRequest(arg0 *s3.HeadObjectInput) (*request.Request, *s3.HeadObjectOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "HeadObjectRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.HeadObjectOutput)
- return ret0, ret1
-}
-
-// HeadObjectRequest indicates an expected call of HeadObjectRequest.
-func (mr *MockClientMockRecorder) HeadObjectRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadObjectRequest", reflect.TypeOf((*MockClient)(nil).HeadObjectRequest), arg0)
-}
-
-// HeadObjectWithContext mocks base method.
-func (m *MockClient) HeadObjectWithContext(arg0 context.Context, arg1 *s3.HeadObjectInput, arg2 ...request.Option) (*s3.HeadObjectOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "HeadObjectWithContext", varargs...)
- ret0, _ := ret[0].(*s3.HeadObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// HeadObjectWithContext indicates an expected call of HeadObjectWithContext.
-func (mr *MockClientMockRecorder) HeadObjectWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HeadObjectWithContext", reflect.TypeOf((*MockClient)(nil).HeadObjectWithContext), varargs...)
-}
-
-// ListBucketAnalyticsConfigurations mocks base method.
-func (m *MockClient) ListBucketAnalyticsConfigurations(arg0 *s3.ListBucketAnalyticsConfigurationsInput) (*s3.ListBucketAnalyticsConfigurationsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBucketAnalyticsConfigurations", arg0)
- ret0, _ := ret[0].(*s3.ListBucketAnalyticsConfigurationsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBucketAnalyticsConfigurations indicates an expected call of ListBucketAnalyticsConfigurations.
-func (mr *MockClientMockRecorder) ListBucketAnalyticsConfigurations(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketAnalyticsConfigurations", reflect.TypeOf((*MockClient)(nil).ListBucketAnalyticsConfigurations), arg0)
-}
-
-// ListBucketAnalyticsConfigurationsRequest mocks base method.
-func (m *MockClient) ListBucketAnalyticsConfigurationsRequest(arg0 *s3.ListBucketAnalyticsConfigurationsInput) (*request.Request, *s3.ListBucketAnalyticsConfigurationsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBucketAnalyticsConfigurationsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListBucketAnalyticsConfigurationsOutput)
- return ret0, ret1
-}
-
-// ListBucketAnalyticsConfigurationsRequest indicates an expected call of ListBucketAnalyticsConfigurationsRequest.
-func (mr *MockClientMockRecorder) ListBucketAnalyticsConfigurationsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketAnalyticsConfigurationsRequest", reflect.TypeOf((*MockClient)(nil).ListBucketAnalyticsConfigurationsRequest), arg0)
-}
-
-// ListBucketAnalyticsConfigurationsWithContext mocks base method.
-func (m *MockClient) ListBucketAnalyticsConfigurationsWithContext(arg0 context.Context, arg1 *s3.ListBucketAnalyticsConfigurationsInput, arg2 ...request.Option) (*s3.ListBucketAnalyticsConfigurationsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListBucketAnalyticsConfigurationsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListBucketAnalyticsConfigurationsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBucketAnalyticsConfigurationsWithContext indicates an expected call of ListBucketAnalyticsConfigurationsWithContext.
-func (mr *MockClientMockRecorder) ListBucketAnalyticsConfigurationsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketAnalyticsConfigurationsWithContext", reflect.TypeOf((*MockClient)(nil).ListBucketAnalyticsConfigurationsWithContext), varargs...)
-}
-
-// ListBucketIntelligentTieringConfigurations mocks base method.
-func (m *MockClient) ListBucketIntelligentTieringConfigurations(arg0 *s3.ListBucketIntelligentTieringConfigurationsInput) (*s3.ListBucketIntelligentTieringConfigurationsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBucketIntelligentTieringConfigurations", arg0)
- ret0, _ := ret[0].(*s3.ListBucketIntelligentTieringConfigurationsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBucketIntelligentTieringConfigurations indicates an expected call of ListBucketIntelligentTieringConfigurations.
-func (mr *MockClientMockRecorder) ListBucketIntelligentTieringConfigurations(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketIntelligentTieringConfigurations", reflect.TypeOf((*MockClient)(nil).ListBucketIntelligentTieringConfigurations), arg0)
-}
-
-// ListBucketIntelligentTieringConfigurationsRequest mocks base method.
-func (m *MockClient) ListBucketIntelligentTieringConfigurationsRequest(arg0 *s3.ListBucketIntelligentTieringConfigurationsInput) (*request.Request, *s3.ListBucketIntelligentTieringConfigurationsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBucketIntelligentTieringConfigurationsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListBucketIntelligentTieringConfigurationsOutput)
- return ret0, ret1
-}
-
-// ListBucketIntelligentTieringConfigurationsRequest indicates an expected call of ListBucketIntelligentTieringConfigurationsRequest.
-func (mr *MockClientMockRecorder) ListBucketIntelligentTieringConfigurationsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketIntelligentTieringConfigurationsRequest", reflect.TypeOf((*MockClient)(nil).ListBucketIntelligentTieringConfigurationsRequest), arg0)
-}
-
-// ListBucketIntelligentTieringConfigurationsWithContext mocks base method.
-func (m *MockClient) ListBucketIntelligentTieringConfigurationsWithContext(arg0 context.Context, arg1 *s3.ListBucketIntelligentTieringConfigurationsInput, arg2 ...request.Option) (*s3.ListBucketIntelligentTieringConfigurationsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListBucketIntelligentTieringConfigurationsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListBucketIntelligentTieringConfigurationsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBucketIntelligentTieringConfigurationsWithContext indicates an expected call of ListBucketIntelligentTieringConfigurationsWithContext.
-func (mr *MockClientMockRecorder) ListBucketIntelligentTieringConfigurationsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketIntelligentTieringConfigurationsWithContext", reflect.TypeOf((*MockClient)(nil).ListBucketIntelligentTieringConfigurationsWithContext), varargs...)
-}
-
-// ListBucketInventoryConfigurations mocks base method.
-func (m *MockClient) ListBucketInventoryConfigurations(arg0 *s3.ListBucketInventoryConfigurationsInput) (*s3.ListBucketInventoryConfigurationsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBucketInventoryConfigurations", arg0)
- ret0, _ := ret[0].(*s3.ListBucketInventoryConfigurationsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBucketInventoryConfigurations indicates an expected call of ListBucketInventoryConfigurations.
-func (mr *MockClientMockRecorder) ListBucketInventoryConfigurations(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketInventoryConfigurations", reflect.TypeOf((*MockClient)(nil).ListBucketInventoryConfigurations), arg0)
-}
-
-// ListBucketInventoryConfigurationsRequest mocks base method.
-func (m *MockClient) ListBucketInventoryConfigurationsRequest(arg0 *s3.ListBucketInventoryConfigurationsInput) (*request.Request, *s3.ListBucketInventoryConfigurationsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBucketInventoryConfigurationsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListBucketInventoryConfigurationsOutput)
- return ret0, ret1
-}
-
-// ListBucketInventoryConfigurationsRequest indicates an expected call of ListBucketInventoryConfigurationsRequest.
-func (mr *MockClientMockRecorder) ListBucketInventoryConfigurationsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketInventoryConfigurationsRequest", reflect.TypeOf((*MockClient)(nil).ListBucketInventoryConfigurationsRequest), arg0)
-}
-
-// ListBucketInventoryConfigurationsWithContext mocks base method.
-func (m *MockClient) ListBucketInventoryConfigurationsWithContext(arg0 context.Context, arg1 *s3.ListBucketInventoryConfigurationsInput, arg2 ...request.Option) (*s3.ListBucketInventoryConfigurationsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListBucketInventoryConfigurationsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListBucketInventoryConfigurationsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBucketInventoryConfigurationsWithContext indicates an expected call of ListBucketInventoryConfigurationsWithContext.
-func (mr *MockClientMockRecorder) ListBucketInventoryConfigurationsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketInventoryConfigurationsWithContext", reflect.TypeOf((*MockClient)(nil).ListBucketInventoryConfigurationsWithContext), varargs...)
-}
-
-// ListBucketMetricsConfigurations mocks base method.
-func (m *MockClient) ListBucketMetricsConfigurations(arg0 *s3.ListBucketMetricsConfigurationsInput) (*s3.ListBucketMetricsConfigurationsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBucketMetricsConfigurations", arg0)
- ret0, _ := ret[0].(*s3.ListBucketMetricsConfigurationsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBucketMetricsConfigurations indicates an expected call of ListBucketMetricsConfigurations.
-func (mr *MockClientMockRecorder) ListBucketMetricsConfigurations(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketMetricsConfigurations", reflect.TypeOf((*MockClient)(nil).ListBucketMetricsConfigurations), arg0)
-}
-
-// ListBucketMetricsConfigurationsRequest mocks base method.
-func (m *MockClient) ListBucketMetricsConfigurationsRequest(arg0 *s3.ListBucketMetricsConfigurationsInput) (*request.Request, *s3.ListBucketMetricsConfigurationsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBucketMetricsConfigurationsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListBucketMetricsConfigurationsOutput)
- return ret0, ret1
-}
-
-// ListBucketMetricsConfigurationsRequest indicates an expected call of ListBucketMetricsConfigurationsRequest.
-func (mr *MockClientMockRecorder) ListBucketMetricsConfigurationsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketMetricsConfigurationsRequest", reflect.TypeOf((*MockClient)(nil).ListBucketMetricsConfigurationsRequest), arg0)
-}
-
-// ListBucketMetricsConfigurationsWithContext mocks base method.
-func (m *MockClient) ListBucketMetricsConfigurationsWithContext(arg0 context.Context, arg1 *s3.ListBucketMetricsConfigurationsInput, arg2 ...request.Option) (*s3.ListBucketMetricsConfigurationsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListBucketMetricsConfigurationsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListBucketMetricsConfigurationsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBucketMetricsConfigurationsWithContext indicates an expected call of ListBucketMetricsConfigurationsWithContext.
-func (mr *MockClientMockRecorder) ListBucketMetricsConfigurationsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketMetricsConfigurationsWithContext", reflect.TypeOf((*MockClient)(nil).ListBucketMetricsConfigurationsWithContext), varargs...)
-}
-
-// ListBuckets mocks base method.
-func (m *MockClient) ListBuckets(arg0 *s3.ListBucketsInput) (*s3.ListBucketsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBuckets", arg0)
- ret0, _ := ret[0].(*s3.ListBucketsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBuckets indicates an expected call of ListBuckets.
-func (mr *MockClientMockRecorder) ListBuckets(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBuckets", reflect.TypeOf((*MockClient)(nil).ListBuckets), arg0)
-}
-
-// ListBucketsRequest mocks base method.
-func (m *MockClient) ListBucketsRequest(arg0 *s3.ListBucketsInput) (*request.Request, *s3.ListBucketsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBucketsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListBucketsOutput)
- return ret0, ret1
-}
-
-// ListBucketsRequest indicates an expected call of ListBucketsRequest.
-func (mr *MockClientMockRecorder) ListBucketsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketsRequest", reflect.TypeOf((*MockClient)(nil).ListBucketsRequest), arg0)
-}
-
-// ListBucketsWithContext mocks base method.
-func (m *MockClient) ListBucketsWithContext(arg0 context.Context, arg1 *s3.ListBucketsInput, arg2 ...request.Option) (*s3.ListBucketsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListBucketsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListBucketsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBucketsWithContext indicates an expected call of ListBucketsWithContext.
-func (mr *MockClientMockRecorder) ListBucketsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBucketsWithContext", reflect.TypeOf((*MockClient)(nil).ListBucketsWithContext), varargs...)
-}
-
-// ListDirectoryBuckets mocks base method.
-func (m *MockClient) ListDirectoryBuckets(arg0 *s3.ListDirectoryBucketsInput) (*s3.ListDirectoryBucketsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListDirectoryBuckets", arg0)
- ret0, _ := ret[0].(*s3.ListDirectoryBucketsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListDirectoryBuckets indicates an expected call of ListDirectoryBuckets.
-func (mr *MockClientMockRecorder) ListDirectoryBuckets(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDirectoryBuckets", reflect.TypeOf((*MockClient)(nil).ListDirectoryBuckets), arg0)
-}
-
-// ListDirectoryBucketsPages mocks base method.
-func (m *MockClient) ListDirectoryBucketsPages(arg0 *s3.ListDirectoryBucketsInput, arg1 func(*s3.ListDirectoryBucketsOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListDirectoryBucketsPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListDirectoryBucketsPages indicates an expected call of ListDirectoryBucketsPages.
-func (mr *MockClientMockRecorder) ListDirectoryBucketsPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDirectoryBucketsPages", reflect.TypeOf((*MockClient)(nil).ListDirectoryBucketsPages), arg0, arg1)
-}
-
-// ListDirectoryBucketsPagesWithContext mocks base method.
-func (m *MockClient) ListDirectoryBucketsPagesWithContext(arg0 context.Context, arg1 *s3.ListDirectoryBucketsInput, arg2 func(*s3.ListDirectoryBucketsOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListDirectoryBucketsPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListDirectoryBucketsPagesWithContext indicates an expected call of ListDirectoryBucketsPagesWithContext.
-func (mr *MockClientMockRecorder) ListDirectoryBucketsPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDirectoryBucketsPagesWithContext", reflect.TypeOf((*MockClient)(nil).ListDirectoryBucketsPagesWithContext), varargs...)
-}
-
-// ListDirectoryBucketsRequest mocks base method.
-func (m *MockClient) ListDirectoryBucketsRequest(arg0 *s3.ListDirectoryBucketsInput) (*request.Request, *s3.ListDirectoryBucketsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListDirectoryBucketsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListDirectoryBucketsOutput)
- return ret0, ret1
-}
-
-// ListDirectoryBucketsRequest indicates an expected call of ListDirectoryBucketsRequest.
-func (mr *MockClientMockRecorder) ListDirectoryBucketsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDirectoryBucketsRequest", reflect.TypeOf((*MockClient)(nil).ListDirectoryBucketsRequest), arg0)
-}
-
-// ListDirectoryBucketsWithContext mocks base method.
-func (m *MockClient) ListDirectoryBucketsWithContext(arg0 context.Context, arg1 *s3.ListDirectoryBucketsInput, arg2 ...request.Option) (*s3.ListDirectoryBucketsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListDirectoryBucketsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListDirectoryBucketsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListDirectoryBucketsWithContext indicates an expected call of ListDirectoryBucketsWithContext.
-func (mr *MockClientMockRecorder) ListDirectoryBucketsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListDirectoryBucketsWithContext", reflect.TypeOf((*MockClient)(nil).ListDirectoryBucketsWithContext), varargs...)
-}
-
-// ListMultipartUploads mocks base method.
-func (m *MockClient) ListMultipartUploads(arg0 *s3.ListMultipartUploadsInput) (*s3.ListMultipartUploadsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListMultipartUploads", arg0)
- ret0, _ := ret[0].(*s3.ListMultipartUploadsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListMultipartUploads indicates an expected call of ListMultipartUploads.
-func (mr *MockClientMockRecorder) ListMultipartUploads(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMultipartUploads", reflect.TypeOf((*MockClient)(nil).ListMultipartUploads), arg0)
-}
-
-// ListMultipartUploadsPages mocks base method.
-func (m *MockClient) ListMultipartUploadsPages(arg0 *s3.ListMultipartUploadsInput, arg1 func(*s3.ListMultipartUploadsOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListMultipartUploadsPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListMultipartUploadsPages indicates an expected call of ListMultipartUploadsPages.
-func (mr *MockClientMockRecorder) ListMultipartUploadsPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMultipartUploadsPages", reflect.TypeOf((*MockClient)(nil).ListMultipartUploadsPages), arg0, arg1)
-}
-
-// ListMultipartUploadsPagesWithContext mocks base method.
-func (m *MockClient) ListMultipartUploadsPagesWithContext(arg0 context.Context, arg1 *s3.ListMultipartUploadsInput, arg2 func(*s3.ListMultipartUploadsOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListMultipartUploadsPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListMultipartUploadsPagesWithContext indicates an expected call of ListMultipartUploadsPagesWithContext.
-func (mr *MockClientMockRecorder) ListMultipartUploadsPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMultipartUploadsPagesWithContext", reflect.TypeOf((*MockClient)(nil).ListMultipartUploadsPagesWithContext), varargs...)
-}
-
-// ListMultipartUploadsRequest mocks base method.
-func (m *MockClient) ListMultipartUploadsRequest(arg0 *s3.ListMultipartUploadsInput) (*request.Request, *s3.ListMultipartUploadsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListMultipartUploadsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListMultipartUploadsOutput)
- return ret0, ret1
-}
-
-// ListMultipartUploadsRequest indicates an expected call of ListMultipartUploadsRequest.
-func (mr *MockClientMockRecorder) ListMultipartUploadsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMultipartUploadsRequest", reflect.TypeOf((*MockClient)(nil).ListMultipartUploadsRequest), arg0)
-}
-
-// ListMultipartUploadsWithContext mocks base method.
-func (m *MockClient) ListMultipartUploadsWithContext(arg0 context.Context, arg1 *s3.ListMultipartUploadsInput, arg2 ...request.Option) (*s3.ListMultipartUploadsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListMultipartUploadsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListMultipartUploadsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListMultipartUploadsWithContext indicates an expected call of ListMultipartUploadsWithContext.
-func (mr *MockClientMockRecorder) ListMultipartUploadsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListMultipartUploadsWithContext", reflect.TypeOf((*MockClient)(nil).ListMultipartUploadsWithContext), varargs...)
-}
-
-// ListObjectVersions mocks base method.
-func (m *MockClient) ListObjectVersions(arg0 *s3.ListObjectVersionsInput) (*s3.ListObjectVersionsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListObjectVersions", arg0)
- ret0, _ := ret[0].(*s3.ListObjectVersionsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListObjectVersions indicates an expected call of ListObjectVersions.
-func (mr *MockClientMockRecorder) ListObjectVersions(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectVersions", reflect.TypeOf((*MockClient)(nil).ListObjectVersions), arg0)
-}
-
-// ListObjectVersionsPages mocks base method.
-func (m *MockClient) ListObjectVersionsPages(arg0 *s3.ListObjectVersionsInput, arg1 func(*s3.ListObjectVersionsOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListObjectVersionsPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListObjectVersionsPages indicates an expected call of ListObjectVersionsPages.
-func (mr *MockClientMockRecorder) ListObjectVersionsPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectVersionsPages", reflect.TypeOf((*MockClient)(nil).ListObjectVersionsPages), arg0, arg1)
-}
-
-// ListObjectVersionsPagesWithContext mocks base method.
-func (m *MockClient) ListObjectVersionsPagesWithContext(arg0 context.Context, arg1 *s3.ListObjectVersionsInput, arg2 func(*s3.ListObjectVersionsOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListObjectVersionsPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListObjectVersionsPagesWithContext indicates an expected call of ListObjectVersionsPagesWithContext.
-func (mr *MockClientMockRecorder) ListObjectVersionsPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectVersionsPagesWithContext", reflect.TypeOf((*MockClient)(nil).ListObjectVersionsPagesWithContext), varargs...)
-}
-
-// ListObjectVersionsRequest mocks base method.
-func (m *MockClient) ListObjectVersionsRequest(arg0 *s3.ListObjectVersionsInput) (*request.Request, *s3.ListObjectVersionsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListObjectVersionsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListObjectVersionsOutput)
- return ret0, ret1
-}
-
-// ListObjectVersionsRequest indicates an expected call of ListObjectVersionsRequest.
-func (mr *MockClientMockRecorder) ListObjectVersionsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectVersionsRequest", reflect.TypeOf((*MockClient)(nil).ListObjectVersionsRequest), arg0)
-}
-
-// ListObjectVersionsWithContext mocks base method.
-func (m *MockClient) ListObjectVersionsWithContext(arg0 context.Context, arg1 *s3.ListObjectVersionsInput, arg2 ...request.Option) (*s3.ListObjectVersionsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListObjectVersionsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListObjectVersionsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListObjectVersionsWithContext indicates an expected call of ListObjectVersionsWithContext.
-func (mr *MockClientMockRecorder) ListObjectVersionsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectVersionsWithContext", reflect.TypeOf((*MockClient)(nil).ListObjectVersionsWithContext), varargs...)
-}
-
-// ListObjects mocks base method.
-func (m *MockClient) ListObjects(arg0 *s3.ListObjectsInput) (*s3.ListObjectsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListObjects", arg0)
- ret0, _ := ret[0].(*s3.ListObjectsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListObjects indicates an expected call of ListObjects.
-func (mr *MockClientMockRecorder) ListObjects(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjects", reflect.TypeOf((*MockClient)(nil).ListObjects), arg0)
-}
-
-// ListObjectsPages mocks base method.
-func (m *MockClient) ListObjectsPages(arg0 *s3.ListObjectsInput, arg1 func(*s3.ListObjectsOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListObjectsPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListObjectsPages indicates an expected call of ListObjectsPages.
-func (mr *MockClientMockRecorder) ListObjectsPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsPages", reflect.TypeOf((*MockClient)(nil).ListObjectsPages), arg0, arg1)
-}
-
-// ListObjectsPagesWithContext mocks base method.
-func (m *MockClient) ListObjectsPagesWithContext(arg0 context.Context, arg1 *s3.ListObjectsInput, arg2 func(*s3.ListObjectsOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListObjectsPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListObjectsPagesWithContext indicates an expected call of ListObjectsPagesWithContext.
-func (mr *MockClientMockRecorder) ListObjectsPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsPagesWithContext", reflect.TypeOf((*MockClient)(nil).ListObjectsPagesWithContext), varargs...)
-}
-
-// ListObjectsRequest mocks base method.
-func (m *MockClient) ListObjectsRequest(arg0 *s3.ListObjectsInput) (*request.Request, *s3.ListObjectsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListObjectsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListObjectsOutput)
- return ret0, ret1
-}
-
-// ListObjectsRequest indicates an expected call of ListObjectsRequest.
-func (mr *MockClientMockRecorder) ListObjectsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsRequest", reflect.TypeOf((*MockClient)(nil).ListObjectsRequest), arg0)
-}
-
-// ListObjectsV2 mocks base method.
-func (m *MockClient) ListObjectsV2(arg0 *s3.ListObjectsV2Input) (*s3.ListObjectsV2Output, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListObjectsV2", arg0)
- ret0, _ := ret[0].(*s3.ListObjectsV2Output)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListObjectsV2 indicates an expected call of ListObjectsV2.
-func (mr *MockClientMockRecorder) ListObjectsV2(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsV2", reflect.TypeOf((*MockClient)(nil).ListObjectsV2), arg0)
-}
-
-// ListObjectsV2Pages mocks base method.
-func (m *MockClient) ListObjectsV2Pages(arg0 *s3.ListObjectsV2Input, arg1 func(*s3.ListObjectsV2Output, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListObjectsV2Pages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListObjectsV2Pages indicates an expected call of ListObjectsV2Pages.
-func (mr *MockClientMockRecorder) ListObjectsV2Pages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsV2Pages", reflect.TypeOf((*MockClient)(nil).ListObjectsV2Pages), arg0, arg1)
-}
-
-// ListObjectsV2PagesWithContext mocks base method.
-func (m *MockClient) ListObjectsV2PagesWithContext(arg0 context.Context, arg1 *s3.ListObjectsV2Input, arg2 func(*s3.ListObjectsV2Output, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListObjectsV2PagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListObjectsV2PagesWithContext indicates an expected call of ListObjectsV2PagesWithContext.
-func (mr *MockClientMockRecorder) ListObjectsV2PagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsV2PagesWithContext", reflect.TypeOf((*MockClient)(nil).ListObjectsV2PagesWithContext), varargs...)
-}
-
-// ListObjectsV2Request mocks base method.
-func (m *MockClient) ListObjectsV2Request(arg0 *s3.ListObjectsV2Input) (*request.Request, *s3.ListObjectsV2Output) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListObjectsV2Request", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListObjectsV2Output)
- return ret0, ret1
-}
-
-// ListObjectsV2Request indicates an expected call of ListObjectsV2Request.
-func (mr *MockClientMockRecorder) ListObjectsV2Request(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsV2Request", reflect.TypeOf((*MockClient)(nil).ListObjectsV2Request), arg0)
-}
-
-// ListObjectsV2WithContext mocks base method.
-func (m *MockClient) ListObjectsV2WithContext(arg0 context.Context, arg1 *s3.ListObjectsV2Input, arg2 ...request.Option) (*s3.ListObjectsV2Output, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListObjectsV2WithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListObjectsV2Output)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListObjectsV2WithContext indicates an expected call of ListObjectsV2WithContext.
-func (mr *MockClientMockRecorder) ListObjectsV2WithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsV2WithContext", reflect.TypeOf((*MockClient)(nil).ListObjectsV2WithContext), varargs...)
-}
-
-// ListObjectsWithContext mocks base method.
-func (m *MockClient) ListObjectsWithContext(arg0 context.Context, arg1 *s3.ListObjectsInput, arg2 ...request.Option) (*s3.ListObjectsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListObjectsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListObjectsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListObjectsWithContext indicates an expected call of ListObjectsWithContext.
-func (mr *MockClientMockRecorder) ListObjectsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListObjectsWithContext", reflect.TypeOf((*MockClient)(nil).ListObjectsWithContext), varargs...)
-}
-
-// ListParts mocks base method.
-func (m *MockClient) ListParts(arg0 *s3.ListPartsInput) (*s3.ListPartsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListParts", arg0)
- ret0, _ := ret[0].(*s3.ListPartsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListParts indicates an expected call of ListParts.
-func (mr *MockClientMockRecorder) ListParts(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListParts", reflect.TypeOf((*MockClient)(nil).ListParts), arg0)
-}
-
-// ListPartsPages mocks base method.
-func (m *MockClient) ListPartsPages(arg0 *s3.ListPartsInput, arg1 func(*s3.ListPartsOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListPartsPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListPartsPages indicates an expected call of ListPartsPages.
-func (mr *MockClientMockRecorder) ListPartsPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPartsPages", reflect.TypeOf((*MockClient)(nil).ListPartsPages), arg0, arg1)
-}
-
-// ListPartsPagesWithContext mocks base method.
-func (m *MockClient) ListPartsPagesWithContext(arg0 context.Context, arg1 *s3.ListPartsInput, arg2 func(*s3.ListPartsOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListPartsPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListPartsPagesWithContext indicates an expected call of ListPartsPagesWithContext.
-func (mr *MockClientMockRecorder) ListPartsPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPartsPagesWithContext", reflect.TypeOf((*MockClient)(nil).ListPartsPagesWithContext), varargs...)
-}
-
-// ListPartsRequest mocks base method.
-func (m *MockClient) ListPartsRequest(arg0 *s3.ListPartsInput) (*request.Request, *s3.ListPartsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListPartsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.ListPartsOutput)
- return ret0, ret1
-}
-
-// ListPartsRequest indicates an expected call of ListPartsRequest.
-func (mr *MockClientMockRecorder) ListPartsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPartsRequest", reflect.TypeOf((*MockClient)(nil).ListPartsRequest), arg0)
-}
-
-// ListPartsWithContext mocks base method.
-func (m *MockClient) ListPartsWithContext(arg0 context.Context, arg1 *s3.ListPartsInput, arg2 ...request.Option) (*s3.ListPartsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListPartsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.ListPartsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListPartsWithContext indicates an expected call of ListPartsWithContext.
-func (mr *MockClientMockRecorder) ListPartsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListPartsWithContext", reflect.TypeOf((*MockClient)(nil).ListPartsWithContext), varargs...)
-}
-
-// PutBucketAccelerateConfiguration mocks base method.
-func (m *MockClient) PutBucketAccelerateConfiguration(arg0 *s3.PutBucketAccelerateConfigurationInput) (*s3.PutBucketAccelerateConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketAccelerateConfiguration", arg0)
- ret0, _ := ret[0].(*s3.PutBucketAccelerateConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketAccelerateConfiguration indicates an expected call of PutBucketAccelerateConfiguration.
-func (mr *MockClientMockRecorder) PutBucketAccelerateConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketAccelerateConfiguration", reflect.TypeOf((*MockClient)(nil).PutBucketAccelerateConfiguration), arg0)
-}
-
-// PutBucketAccelerateConfigurationRequest mocks base method.
-func (m *MockClient) PutBucketAccelerateConfigurationRequest(arg0 *s3.PutBucketAccelerateConfigurationInput) (*request.Request, *s3.PutBucketAccelerateConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketAccelerateConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketAccelerateConfigurationOutput)
- return ret0, ret1
-}
-
-// PutBucketAccelerateConfigurationRequest indicates an expected call of PutBucketAccelerateConfigurationRequest.
-func (mr *MockClientMockRecorder) PutBucketAccelerateConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketAccelerateConfigurationRequest", reflect.TypeOf((*MockClient)(nil).PutBucketAccelerateConfigurationRequest), arg0)
-}
-
-// PutBucketAccelerateConfigurationWithContext mocks base method.
-func (m *MockClient) PutBucketAccelerateConfigurationWithContext(arg0 context.Context, arg1 *s3.PutBucketAccelerateConfigurationInput, arg2 ...request.Option) (*s3.PutBucketAccelerateConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketAccelerateConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketAccelerateConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketAccelerateConfigurationWithContext indicates an expected call of PutBucketAccelerateConfigurationWithContext.
-func (mr *MockClientMockRecorder) PutBucketAccelerateConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketAccelerateConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketAccelerateConfigurationWithContext), varargs...)
-}
-
-// PutBucketAcl mocks base method.
-func (m *MockClient) PutBucketAcl(arg0 *s3.PutBucketAclInput) (*s3.PutBucketAclOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketAcl", arg0)
- ret0, _ := ret[0].(*s3.PutBucketAclOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketAcl indicates an expected call of PutBucketAcl.
-func (mr *MockClientMockRecorder) PutBucketAcl(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketAcl", reflect.TypeOf((*MockClient)(nil).PutBucketAcl), arg0)
-}
-
-// PutBucketAclRequest mocks base method.
-func (m *MockClient) PutBucketAclRequest(arg0 *s3.PutBucketAclInput) (*request.Request, *s3.PutBucketAclOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketAclRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketAclOutput)
- return ret0, ret1
-}
-
-// PutBucketAclRequest indicates an expected call of PutBucketAclRequest.
-func (mr *MockClientMockRecorder) PutBucketAclRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketAclRequest", reflect.TypeOf((*MockClient)(nil).PutBucketAclRequest), arg0)
-}
-
-// PutBucketAclWithContext mocks base method.
-func (m *MockClient) PutBucketAclWithContext(arg0 context.Context, arg1 *s3.PutBucketAclInput, arg2 ...request.Option) (*s3.PutBucketAclOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketAclWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketAclOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketAclWithContext indicates an expected call of PutBucketAclWithContext.
-func (mr *MockClientMockRecorder) PutBucketAclWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketAclWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketAclWithContext), varargs...)
-}
-
-// PutBucketAnalyticsConfiguration mocks base method.
-func (m *MockClient) PutBucketAnalyticsConfiguration(arg0 *s3.PutBucketAnalyticsConfigurationInput) (*s3.PutBucketAnalyticsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketAnalyticsConfiguration", arg0)
- ret0, _ := ret[0].(*s3.PutBucketAnalyticsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketAnalyticsConfiguration indicates an expected call of PutBucketAnalyticsConfiguration.
-func (mr *MockClientMockRecorder) PutBucketAnalyticsConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketAnalyticsConfiguration", reflect.TypeOf((*MockClient)(nil).PutBucketAnalyticsConfiguration), arg0)
-}
-
-// PutBucketAnalyticsConfigurationRequest mocks base method.
-func (m *MockClient) PutBucketAnalyticsConfigurationRequest(arg0 *s3.PutBucketAnalyticsConfigurationInput) (*request.Request, *s3.PutBucketAnalyticsConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketAnalyticsConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketAnalyticsConfigurationOutput)
- return ret0, ret1
-}
-
-// PutBucketAnalyticsConfigurationRequest indicates an expected call of PutBucketAnalyticsConfigurationRequest.
-func (mr *MockClientMockRecorder) PutBucketAnalyticsConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketAnalyticsConfigurationRequest", reflect.TypeOf((*MockClient)(nil).PutBucketAnalyticsConfigurationRequest), arg0)
-}
-
-// PutBucketAnalyticsConfigurationWithContext mocks base method.
-func (m *MockClient) PutBucketAnalyticsConfigurationWithContext(arg0 context.Context, arg1 *s3.PutBucketAnalyticsConfigurationInput, arg2 ...request.Option) (*s3.PutBucketAnalyticsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketAnalyticsConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketAnalyticsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketAnalyticsConfigurationWithContext indicates an expected call of PutBucketAnalyticsConfigurationWithContext.
-func (mr *MockClientMockRecorder) PutBucketAnalyticsConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketAnalyticsConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketAnalyticsConfigurationWithContext), varargs...)
-}
-
-// PutBucketCors mocks base method.
-func (m *MockClient) PutBucketCors(arg0 *s3.PutBucketCorsInput) (*s3.PutBucketCorsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketCors", arg0)
- ret0, _ := ret[0].(*s3.PutBucketCorsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketCors indicates an expected call of PutBucketCors.
-func (mr *MockClientMockRecorder) PutBucketCors(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketCors", reflect.TypeOf((*MockClient)(nil).PutBucketCors), arg0)
-}
-
-// PutBucketCorsRequest mocks base method.
-func (m *MockClient) PutBucketCorsRequest(arg0 *s3.PutBucketCorsInput) (*request.Request, *s3.PutBucketCorsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketCorsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketCorsOutput)
- return ret0, ret1
-}
-
-// PutBucketCorsRequest indicates an expected call of PutBucketCorsRequest.
-func (mr *MockClientMockRecorder) PutBucketCorsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketCorsRequest", reflect.TypeOf((*MockClient)(nil).PutBucketCorsRequest), arg0)
-}
-
-// PutBucketCorsWithContext mocks base method.
-func (m *MockClient) PutBucketCorsWithContext(arg0 context.Context, arg1 *s3.PutBucketCorsInput, arg2 ...request.Option) (*s3.PutBucketCorsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketCorsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketCorsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketCorsWithContext indicates an expected call of PutBucketCorsWithContext.
-func (mr *MockClientMockRecorder) PutBucketCorsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketCorsWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketCorsWithContext), varargs...)
-}
-
-// PutBucketEncryption mocks base method.
-func (m *MockClient) PutBucketEncryption(arg0 *s3.PutBucketEncryptionInput) (*s3.PutBucketEncryptionOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketEncryption", arg0)
- ret0, _ := ret[0].(*s3.PutBucketEncryptionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketEncryption indicates an expected call of PutBucketEncryption.
-func (mr *MockClientMockRecorder) PutBucketEncryption(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketEncryption", reflect.TypeOf((*MockClient)(nil).PutBucketEncryption), arg0)
-}
-
-// PutBucketEncryptionRequest mocks base method.
-func (m *MockClient) PutBucketEncryptionRequest(arg0 *s3.PutBucketEncryptionInput) (*request.Request, *s3.PutBucketEncryptionOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketEncryptionRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketEncryptionOutput)
- return ret0, ret1
-}
-
-// PutBucketEncryptionRequest indicates an expected call of PutBucketEncryptionRequest.
-func (mr *MockClientMockRecorder) PutBucketEncryptionRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketEncryptionRequest", reflect.TypeOf((*MockClient)(nil).PutBucketEncryptionRequest), arg0)
-}
-
-// PutBucketEncryptionWithContext mocks base method.
-func (m *MockClient) PutBucketEncryptionWithContext(arg0 context.Context, arg1 *s3.PutBucketEncryptionInput, arg2 ...request.Option) (*s3.PutBucketEncryptionOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketEncryptionWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketEncryptionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketEncryptionWithContext indicates an expected call of PutBucketEncryptionWithContext.
-func (mr *MockClientMockRecorder) PutBucketEncryptionWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketEncryptionWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketEncryptionWithContext), varargs...)
-}
-
-// PutBucketIntelligentTieringConfiguration mocks base method.
-func (m *MockClient) PutBucketIntelligentTieringConfiguration(arg0 *s3.PutBucketIntelligentTieringConfigurationInput) (*s3.PutBucketIntelligentTieringConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketIntelligentTieringConfiguration", arg0)
- ret0, _ := ret[0].(*s3.PutBucketIntelligentTieringConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketIntelligentTieringConfiguration indicates an expected call of PutBucketIntelligentTieringConfiguration.
-func (mr *MockClientMockRecorder) PutBucketIntelligentTieringConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketIntelligentTieringConfiguration", reflect.TypeOf((*MockClient)(nil).PutBucketIntelligentTieringConfiguration), arg0)
-}
-
-// PutBucketIntelligentTieringConfigurationRequest mocks base method.
-func (m *MockClient) PutBucketIntelligentTieringConfigurationRequest(arg0 *s3.PutBucketIntelligentTieringConfigurationInput) (*request.Request, *s3.PutBucketIntelligentTieringConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketIntelligentTieringConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketIntelligentTieringConfigurationOutput)
- return ret0, ret1
-}
-
-// PutBucketIntelligentTieringConfigurationRequest indicates an expected call of PutBucketIntelligentTieringConfigurationRequest.
-func (mr *MockClientMockRecorder) PutBucketIntelligentTieringConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketIntelligentTieringConfigurationRequest", reflect.TypeOf((*MockClient)(nil).PutBucketIntelligentTieringConfigurationRequest), arg0)
-}
-
-// PutBucketIntelligentTieringConfigurationWithContext mocks base method.
-func (m *MockClient) PutBucketIntelligentTieringConfigurationWithContext(arg0 context.Context, arg1 *s3.PutBucketIntelligentTieringConfigurationInput, arg2 ...request.Option) (*s3.PutBucketIntelligentTieringConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketIntelligentTieringConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketIntelligentTieringConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketIntelligentTieringConfigurationWithContext indicates an expected call of PutBucketIntelligentTieringConfigurationWithContext.
-func (mr *MockClientMockRecorder) PutBucketIntelligentTieringConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketIntelligentTieringConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketIntelligentTieringConfigurationWithContext), varargs...)
-}
-
-// PutBucketInventoryConfiguration mocks base method.
-func (m *MockClient) PutBucketInventoryConfiguration(arg0 *s3.PutBucketInventoryConfigurationInput) (*s3.PutBucketInventoryConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketInventoryConfiguration", arg0)
- ret0, _ := ret[0].(*s3.PutBucketInventoryConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketInventoryConfiguration indicates an expected call of PutBucketInventoryConfiguration.
-func (mr *MockClientMockRecorder) PutBucketInventoryConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketInventoryConfiguration", reflect.TypeOf((*MockClient)(nil).PutBucketInventoryConfiguration), arg0)
-}
-
-// PutBucketInventoryConfigurationRequest mocks base method.
-func (m *MockClient) PutBucketInventoryConfigurationRequest(arg0 *s3.PutBucketInventoryConfigurationInput) (*request.Request, *s3.PutBucketInventoryConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketInventoryConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketInventoryConfigurationOutput)
- return ret0, ret1
-}
-
-// PutBucketInventoryConfigurationRequest indicates an expected call of PutBucketInventoryConfigurationRequest.
-func (mr *MockClientMockRecorder) PutBucketInventoryConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketInventoryConfigurationRequest", reflect.TypeOf((*MockClient)(nil).PutBucketInventoryConfigurationRequest), arg0)
-}
-
-// PutBucketInventoryConfigurationWithContext mocks base method.
-func (m *MockClient) PutBucketInventoryConfigurationWithContext(arg0 context.Context, arg1 *s3.PutBucketInventoryConfigurationInput, arg2 ...request.Option) (*s3.PutBucketInventoryConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketInventoryConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketInventoryConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketInventoryConfigurationWithContext indicates an expected call of PutBucketInventoryConfigurationWithContext.
-func (mr *MockClientMockRecorder) PutBucketInventoryConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketInventoryConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketInventoryConfigurationWithContext), varargs...)
-}
-
-// PutBucketLifecycle mocks base method.
-func (m *MockClient) PutBucketLifecycle(arg0 *s3.PutBucketLifecycleInput) (*s3.PutBucketLifecycleOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketLifecycle", arg0)
- ret0, _ := ret[0].(*s3.PutBucketLifecycleOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketLifecycle indicates an expected call of PutBucketLifecycle.
-func (mr *MockClientMockRecorder) PutBucketLifecycle(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketLifecycle", reflect.TypeOf((*MockClient)(nil).PutBucketLifecycle), arg0)
-}
-
-// PutBucketLifecycleConfiguration mocks base method.
-func (m *MockClient) PutBucketLifecycleConfiguration(arg0 *s3.PutBucketLifecycleConfigurationInput) (*s3.PutBucketLifecycleConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketLifecycleConfiguration", arg0)
- ret0, _ := ret[0].(*s3.PutBucketLifecycleConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketLifecycleConfiguration indicates an expected call of PutBucketLifecycleConfiguration.
-func (mr *MockClientMockRecorder) PutBucketLifecycleConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketLifecycleConfiguration", reflect.TypeOf((*MockClient)(nil).PutBucketLifecycleConfiguration), arg0)
-}
-
-// PutBucketLifecycleConfigurationRequest mocks base method.
-func (m *MockClient) PutBucketLifecycleConfigurationRequest(arg0 *s3.PutBucketLifecycleConfigurationInput) (*request.Request, *s3.PutBucketLifecycleConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketLifecycleConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketLifecycleConfigurationOutput)
- return ret0, ret1
-}
-
-// PutBucketLifecycleConfigurationRequest indicates an expected call of PutBucketLifecycleConfigurationRequest.
-func (mr *MockClientMockRecorder) PutBucketLifecycleConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketLifecycleConfigurationRequest", reflect.TypeOf((*MockClient)(nil).PutBucketLifecycleConfigurationRequest), arg0)
-}
-
-// PutBucketLifecycleConfigurationWithContext mocks base method.
-func (m *MockClient) PutBucketLifecycleConfigurationWithContext(arg0 context.Context, arg1 *s3.PutBucketLifecycleConfigurationInput, arg2 ...request.Option) (*s3.PutBucketLifecycleConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketLifecycleConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketLifecycleConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketLifecycleConfigurationWithContext indicates an expected call of PutBucketLifecycleConfigurationWithContext.
-func (mr *MockClientMockRecorder) PutBucketLifecycleConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketLifecycleConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketLifecycleConfigurationWithContext), varargs...)
-}
-
-// PutBucketLifecycleRequest mocks base method.
-func (m *MockClient) PutBucketLifecycleRequest(arg0 *s3.PutBucketLifecycleInput) (*request.Request, *s3.PutBucketLifecycleOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketLifecycleRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketLifecycleOutput)
- return ret0, ret1
-}
-
-// PutBucketLifecycleRequest indicates an expected call of PutBucketLifecycleRequest.
-func (mr *MockClientMockRecorder) PutBucketLifecycleRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketLifecycleRequest", reflect.TypeOf((*MockClient)(nil).PutBucketLifecycleRequest), arg0)
-}
-
-// PutBucketLifecycleWithContext mocks base method.
-func (m *MockClient) PutBucketLifecycleWithContext(arg0 context.Context, arg1 *s3.PutBucketLifecycleInput, arg2 ...request.Option) (*s3.PutBucketLifecycleOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketLifecycleWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketLifecycleOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketLifecycleWithContext indicates an expected call of PutBucketLifecycleWithContext.
-func (mr *MockClientMockRecorder) PutBucketLifecycleWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketLifecycleWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketLifecycleWithContext), varargs...)
-}
-
-// PutBucketLogging mocks base method.
-func (m *MockClient) PutBucketLogging(arg0 *s3.PutBucketLoggingInput) (*s3.PutBucketLoggingOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketLogging", arg0)
- ret0, _ := ret[0].(*s3.PutBucketLoggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketLogging indicates an expected call of PutBucketLogging.
-func (mr *MockClientMockRecorder) PutBucketLogging(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketLogging", reflect.TypeOf((*MockClient)(nil).PutBucketLogging), arg0)
-}
-
-// PutBucketLoggingRequest mocks base method.
-func (m *MockClient) PutBucketLoggingRequest(arg0 *s3.PutBucketLoggingInput) (*request.Request, *s3.PutBucketLoggingOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketLoggingRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketLoggingOutput)
- return ret0, ret1
-}
-
-// PutBucketLoggingRequest indicates an expected call of PutBucketLoggingRequest.
-func (mr *MockClientMockRecorder) PutBucketLoggingRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketLoggingRequest", reflect.TypeOf((*MockClient)(nil).PutBucketLoggingRequest), arg0)
-}
-
-// PutBucketLoggingWithContext mocks base method.
-func (m *MockClient) PutBucketLoggingWithContext(arg0 context.Context, arg1 *s3.PutBucketLoggingInput, arg2 ...request.Option) (*s3.PutBucketLoggingOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketLoggingWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketLoggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketLoggingWithContext indicates an expected call of PutBucketLoggingWithContext.
-func (mr *MockClientMockRecorder) PutBucketLoggingWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketLoggingWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketLoggingWithContext), varargs...)
-}
-
-// PutBucketMetricsConfiguration mocks base method.
-func (m *MockClient) PutBucketMetricsConfiguration(arg0 *s3.PutBucketMetricsConfigurationInput) (*s3.PutBucketMetricsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketMetricsConfiguration", arg0)
- ret0, _ := ret[0].(*s3.PutBucketMetricsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketMetricsConfiguration indicates an expected call of PutBucketMetricsConfiguration.
-func (mr *MockClientMockRecorder) PutBucketMetricsConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketMetricsConfiguration", reflect.TypeOf((*MockClient)(nil).PutBucketMetricsConfiguration), arg0)
-}
-
-// PutBucketMetricsConfigurationRequest mocks base method.
-func (m *MockClient) PutBucketMetricsConfigurationRequest(arg0 *s3.PutBucketMetricsConfigurationInput) (*request.Request, *s3.PutBucketMetricsConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketMetricsConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketMetricsConfigurationOutput)
- return ret0, ret1
-}
-
-// PutBucketMetricsConfigurationRequest indicates an expected call of PutBucketMetricsConfigurationRequest.
-func (mr *MockClientMockRecorder) PutBucketMetricsConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketMetricsConfigurationRequest", reflect.TypeOf((*MockClient)(nil).PutBucketMetricsConfigurationRequest), arg0)
-}
-
-// PutBucketMetricsConfigurationWithContext mocks base method.
-func (m *MockClient) PutBucketMetricsConfigurationWithContext(arg0 context.Context, arg1 *s3.PutBucketMetricsConfigurationInput, arg2 ...request.Option) (*s3.PutBucketMetricsConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketMetricsConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketMetricsConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketMetricsConfigurationWithContext indicates an expected call of PutBucketMetricsConfigurationWithContext.
-func (mr *MockClientMockRecorder) PutBucketMetricsConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketMetricsConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketMetricsConfigurationWithContext), varargs...)
-}
-
-// PutBucketNotification mocks base method.
-func (m *MockClient) PutBucketNotification(arg0 *s3.PutBucketNotificationInput) (*s3.PutBucketNotificationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketNotification", arg0)
- ret0, _ := ret[0].(*s3.PutBucketNotificationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketNotification indicates an expected call of PutBucketNotification.
-func (mr *MockClientMockRecorder) PutBucketNotification(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketNotification", reflect.TypeOf((*MockClient)(nil).PutBucketNotification), arg0)
-}
-
-// PutBucketNotificationConfiguration mocks base method.
-func (m *MockClient) PutBucketNotificationConfiguration(arg0 *s3.PutBucketNotificationConfigurationInput) (*s3.PutBucketNotificationConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketNotificationConfiguration", arg0)
- ret0, _ := ret[0].(*s3.PutBucketNotificationConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketNotificationConfiguration indicates an expected call of PutBucketNotificationConfiguration.
-func (mr *MockClientMockRecorder) PutBucketNotificationConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketNotificationConfiguration", reflect.TypeOf((*MockClient)(nil).PutBucketNotificationConfiguration), arg0)
-}
-
-// PutBucketNotificationConfigurationRequest mocks base method.
-func (m *MockClient) PutBucketNotificationConfigurationRequest(arg0 *s3.PutBucketNotificationConfigurationInput) (*request.Request, *s3.PutBucketNotificationConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketNotificationConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketNotificationConfigurationOutput)
- return ret0, ret1
-}
-
-// PutBucketNotificationConfigurationRequest indicates an expected call of PutBucketNotificationConfigurationRequest.
-func (mr *MockClientMockRecorder) PutBucketNotificationConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketNotificationConfigurationRequest", reflect.TypeOf((*MockClient)(nil).PutBucketNotificationConfigurationRequest), arg0)
-}
-
-// PutBucketNotificationConfigurationWithContext mocks base method.
-func (m *MockClient) PutBucketNotificationConfigurationWithContext(arg0 context.Context, arg1 *s3.PutBucketNotificationConfigurationInput, arg2 ...request.Option) (*s3.PutBucketNotificationConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketNotificationConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketNotificationConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketNotificationConfigurationWithContext indicates an expected call of PutBucketNotificationConfigurationWithContext.
-func (mr *MockClientMockRecorder) PutBucketNotificationConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketNotificationConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketNotificationConfigurationWithContext), varargs...)
-}
-
-// PutBucketNotificationRequest mocks base method.
-func (m *MockClient) PutBucketNotificationRequest(arg0 *s3.PutBucketNotificationInput) (*request.Request, *s3.PutBucketNotificationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketNotificationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketNotificationOutput)
- return ret0, ret1
-}
-
-// PutBucketNotificationRequest indicates an expected call of PutBucketNotificationRequest.
-func (mr *MockClientMockRecorder) PutBucketNotificationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketNotificationRequest", reflect.TypeOf((*MockClient)(nil).PutBucketNotificationRequest), arg0)
-}
-
-// PutBucketNotificationWithContext mocks base method.
-func (m *MockClient) PutBucketNotificationWithContext(arg0 context.Context, arg1 *s3.PutBucketNotificationInput, arg2 ...request.Option) (*s3.PutBucketNotificationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketNotificationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketNotificationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketNotificationWithContext indicates an expected call of PutBucketNotificationWithContext.
-func (mr *MockClientMockRecorder) PutBucketNotificationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketNotificationWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketNotificationWithContext), varargs...)
-}
-
-// PutBucketOwnershipControls mocks base method.
-func (m *MockClient) PutBucketOwnershipControls(arg0 *s3.PutBucketOwnershipControlsInput) (*s3.PutBucketOwnershipControlsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketOwnershipControls", arg0)
- ret0, _ := ret[0].(*s3.PutBucketOwnershipControlsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketOwnershipControls indicates an expected call of PutBucketOwnershipControls.
-func (mr *MockClientMockRecorder) PutBucketOwnershipControls(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketOwnershipControls", reflect.TypeOf((*MockClient)(nil).PutBucketOwnershipControls), arg0)
-}
-
-// PutBucketOwnershipControlsRequest mocks base method.
-func (m *MockClient) PutBucketOwnershipControlsRequest(arg0 *s3.PutBucketOwnershipControlsInput) (*request.Request, *s3.PutBucketOwnershipControlsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketOwnershipControlsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketOwnershipControlsOutput)
- return ret0, ret1
-}
-
-// PutBucketOwnershipControlsRequest indicates an expected call of PutBucketOwnershipControlsRequest.
-func (mr *MockClientMockRecorder) PutBucketOwnershipControlsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketOwnershipControlsRequest", reflect.TypeOf((*MockClient)(nil).PutBucketOwnershipControlsRequest), arg0)
-}
-
-// PutBucketOwnershipControlsWithContext mocks base method.
-func (m *MockClient) PutBucketOwnershipControlsWithContext(arg0 context.Context, arg1 *s3.PutBucketOwnershipControlsInput, arg2 ...request.Option) (*s3.PutBucketOwnershipControlsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketOwnershipControlsWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketOwnershipControlsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketOwnershipControlsWithContext indicates an expected call of PutBucketOwnershipControlsWithContext.
-func (mr *MockClientMockRecorder) PutBucketOwnershipControlsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketOwnershipControlsWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketOwnershipControlsWithContext), varargs...)
-}
-
-// PutBucketPolicy mocks base method.
-func (m *MockClient) PutBucketPolicy(arg0 *s3.PutBucketPolicyInput) (*s3.PutBucketPolicyOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketPolicy", arg0)
- ret0, _ := ret[0].(*s3.PutBucketPolicyOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketPolicy indicates an expected call of PutBucketPolicy.
-func (mr *MockClientMockRecorder) PutBucketPolicy(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketPolicy", reflect.TypeOf((*MockClient)(nil).PutBucketPolicy), arg0)
-}
-
-// PutBucketPolicyRequest mocks base method.
-func (m *MockClient) PutBucketPolicyRequest(arg0 *s3.PutBucketPolicyInput) (*request.Request, *s3.PutBucketPolicyOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketPolicyRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketPolicyOutput)
- return ret0, ret1
-}
-
-// PutBucketPolicyRequest indicates an expected call of PutBucketPolicyRequest.
-func (mr *MockClientMockRecorder) PutBucketPolicyRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketPolicyRequest", reflect.TypeOf((*MockClient)(nil).PutBucketPolicyRequest), arg0)
-}
-
-// PutBucketPolicyWithContext mocks base method.
-func (m *MockClient) PutBucketPolicyWithContext(arg0 context.Context, arg1 *s3.PutBucketPolicyInput, arg2 ...request.Option) (*s3.PutBucketPolicyOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketPolicyWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketPolicyOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketPolicyWithContext indicates an expected call of PutBucketPolicyWithContext.
-func (mr *MockClientMockRecorder) PutBucketPolicyWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketPolicyWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketPolicyWithContext), varargs...)
-}
-
-// PutBucketReplication mocks base method.
-func (m *MockClient) PutBucketReplication(arg0 *s3.PutBucketReplicationInput) (*s3.PutBucketReplicationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketReplication", arg0)
- ret0, _ := ret[0].(*s3.PutBucketReplicationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketReplication indicates an expected call of PutBucketReplication.
-func (mr *MockClientMockRecorder) PutBucketReplication(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketReplication", reflect.TypeOf((*MockClient)(nil).PutBucketReplication), arg0)
-}
-
-// PutBucketReplicationRequest mocks base method.
-func (m *MockClient) PutBucketReplicationRequest(arg0 *s3.PutBucketReplicationInput) (*request.Request, *s3.PutBucketReplicationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketReplicationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketReplicationOutput)
- return ret0, ret1
-}
-
-// PutBucketReplicationRequest indicates an expected call of PutBucketReplicationRequest.
-func (mr *MockClientMockRecorder) PutBucketReplicationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketReplicationRequest", reflect.TypeOf((*MockClient)(nil).PutBucketReplicationRequest), arg0)
-}
-
-// PutBucketReplicationWithContext mocks base method.
-func (m *MockClient) PutBucketReplicationWithContext(arg0 context.Context, arg1 *s3.PutBucketReplicationInput, arg2 ...request.Option) (*s3.PutBucketReplicationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketReplicationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketReplicationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketReplicationWithContext indicates an expected call of PutBucketReplicationWithContext.
-func (mr *MockClientMockRecorder) PutBucketReplicationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketReplicationWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketReplicationWithContext), varargs...)
-}
-
-// PutBucketRequestPayment mocks base method.
-func (m *MockClient) PutBucketRequestPayment(arg0 *s3.PutBucketRequestPaymentInput) (*s3.PutBucketRequestPaymentOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketRequestPayment", arg0)
- ret0, _ := ret[0].(*s3.PutBucketRequestPaymentOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketRequestPayment indicates an expected call of PutBucketRequestPayment.
-func (mr *MockClientMockRecorder) PutBucketRequestPayment(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketRequestPayment", reflect.TypeOf((*MockClient)(nil).PutBucketRequestPayment), arg0)
-}
-
-// PutBucketRequestPaymentRequest mocks base method.
-func (m *MockClient) PutBucketRequestPaymentRequest(arg0 *s3.PutBucketRequestPaymentInput) (*request.Request, *s3.PutBucketRequestPaymentOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketRequestPaymentRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketRequestPaymentOutput)
- return ret0, ret1
-}
-
-// PutBucketRequestPaymentRequest indicates an expected call of PutBucketRequestPaymentRequest.
-func (mr *MockClientMockRecorder) PutBucketRequestPaymentRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketRequestPaymentRequest", reflect.TypeOf((*MockClient)(nil).PutBucketRequestPaymentRequest), arg0)
-}
-
-// PutBucketRequestPaymentWithContext mocks base method.
-func (m *MockClient) PutBucketRequestPaymentWithContext(arg0 context.Context, arg1 *s3.PutBucketRequestPaymentInput, arg2 ...request.Option) (*s3.PutBucketRequestPaymentOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketRequestPaymentWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketRequestPaymentOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketRequestPaymentWithContext indicates an expected call of PutBucketRequestPaymentWithContext.
-func (mr *MockClientMockRecorder) PutBucketRequestPaymentWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketRequestPaymentWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketRequestPaymentWithContext), varargs...)
-}
-
-// PutBucketTagging mocks base method.
-func (m *MockClient) PutBucketTagging(arg0 *s3.PutBucketTaggingInput) (*s3.PutBucketTaggingOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketTagging", arg0)
- ret0, _ := ret[0].(*s3.PutBucketTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketTagging indicates an expected call of PutBucketTagging.
-func (mr *MockClientMockRecorder) PutBucketTagging(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketTagging", reflect.TypeOf((*MockClient)(nil).PutBucketTagging), arg0)
-}
-
-// PutBucketTaggingRequest mocks base method.
-func (m *MockClient) PutBucketTaggingRequest(arg0 *s3.PutBucketTaggingInput) (*request.Request, *s3.PutBucketTaggingOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketTaggingRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketTaggingOutput)
- return ret0, ret1
-}
-
-// PutBucketTaggingRequest indicates an expected call of PutBucketTaggingRequest.
-func (mr *MockClientMockRecorder) PutBucketTaggingRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketTaggingRequest", reflect.TypeOf((*MockClient)(nil).PutBucketTaggingRequest), arg0)
-}
-
-// PutBucketTaggingWithContext mocks base method.
-func (m *MockClient) PutBucketTaggingWithContext(arg0 context.Context, arg1 *s3.PutBucketTaggingInput, arg2 ...request.Option) (*s3.PutBucketTaggingOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketTaggingWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketTaggingWithContext indicates an expected call of PutBucketTaggingWithContext.
-func (mr *MockClientMockRecorder) PutBucketTaggingWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketTaggingWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketTaggingWithContext), varargs...)
-}
-
-// PutBucketVersioning mocks base method.
-func (m *MockClient) PutBucketVersioning(arg0 *s3.PutBucketVersioningInput) (*s3.PutBucketVersioningOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketVersioning", arg0)
- ret0, _ := ret[0].(*s3.PutBucketVersioningOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketVersioning indicates an expected call of PutBucketVersioning.
-func (mr *MockClientMockRecorder) PutBucketVersioning(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketVersioning", reflect.TypeOf((*MockClient)(nil).PutBucketVersioning), arg0)
-}
-
-// PutBucketVersioningRequest mocks base method.
-func (m *MockClient) PutBucketVersioningRequest(arg0 *s3.PutBucketVersioningInput) (*request.Request, *s3.PutBucketVersioningOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketVersioningRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketVersioningOutput)
- return ret0, ret1
-}
-
-// PutBucketVersioningRequest indicates an expected call of PutBucketVersioningRequest.
-func (mr *MockClientMockRecorder) PutBucketVersioningRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketVersioningRequest", reflect.TypeOf((*MockClient)(nil).PutBucketVersioningRequest), arg0)
-}
-
-// PutBucketVersioningWithContext mocks base method.
-func (m *MockClient) PutBucketVersioningWithContext(arg0 context.Context, arg1 *s3.PutBucketVersioningInput, arg2 ...request.Option) (*s3.PutBucketVersioningOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketVersioningWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketVersioningOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketVersioningWithContext indicates an expected call of PutBucketVersioningWithContext.
-func (mr *MockClientMockRecorder) PutBucketVersioningWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketVersioningWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketVersioningWithContext), varargs...)
-}
-
-// PutBucketWebsite mocks base method.
-func (m *MockClient) PutBucketWebsite(arg0 *s3.PutBucketWebsiteInput) (*s3.PutBucketWebsiteOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketWebsite", arg0)
- ret0, _ := ret[0].(*s3.PutBucketWebsiteOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketWebsite indicates an expected call of PutBucketWebsite.
-func (mr *MockClientMockRecorder) PutBucketWebsite(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketWebsite", reflect.TypeOf((*MockClient)(nil).PutBucketWebsite), arg0)
-}
-
-// PutBucketWebsiteRequest mocks base method.
-func (m *MockClient) PutBucketWebsiteRequest(arg0 *s3.PutBucketWebsiteInput) (*request.Request, *s3.PutBucketWebsiteOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutBucketWebsiteRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutBucketWebsiteOutput)
- return ret0, ret1
-}
-
-// PutBucketWebsiteRequest indicates an expected call of PutBucketWebsiteRequest.
-func (mr *MockClientMockRecorder) PutBucketWebsiteRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketWebsiteRequest", reflect.TypeOf((*MockClient)(nil).PutBucketWebsiteRequest), arg0)
-}
-
-// PutBucketWebsiteWithContext mocks base method.
-func (m *MockClient) PutBucketWebsiteWithContext(arg0 context.Context, arg1 *s3.PutBucketWebsiteInput, arg2 ...request.Option) (*s3.PutBucketWebsiteOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutBucketWebsiteWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutBucketWebsiteOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutBucketWebsiteWithContext indicates an expected call of PutBucketWebsiteWithContext.
-func (mr *MockClientMockRecorder) PutBucketWebsiteWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBucketWebsiteWithContext", reflect.TypeOf((*MockClient)(nil).PutBucketWebsiteWithContext), varargs...)
-}
-
-// PutObject mocks base method.
-func (m *MockClient) PutObject(arg0 *s3.PutObjectInput) (*s3.PutObjectOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObject", arg0)
- ret0, _ := ret[0].(*s3.PutObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObject indicates an expected call of PutObject.
-func (mr *MockClientMockRecorder) PutObject(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockClient)(nil).PutObject), arg0)
-}
-
-// PutObjectAcl mocks base method.
-func (m *MockClient) PutObjectAcl(arg0 *s3.PutObjectAclInput) (*s3.PutObjectAclOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectAcl", arg0)
- ret0, _ := ret[0].(*s3.PutObjectAclOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectAcl indicates an expected call of PutObjectAcl.
-func (mr *MockClientMockRecorder) PutObjectAcl(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectAcl", reflect.TypeOf((*MockClient)(nil).PutObjectAcl), arg0)
-}
-
-// PutObjectAclRequest mocks base method.
-func (m *MockClient) PutObjectAclRequest(arg0 *s3.PutObjectAclInput) (*request.Request, *s3.PutObjectAclOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectAclRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutObjectAclOutput)
- return ret0, ret1
-}
-
-// PutObjectAclRequest indicates an expected call of PutObjectAclRequest.
-func (mr *MockClientMockRecorder) PutObjectAclRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectAclRequest", reflect.TypeOf((*MockClient)(nil).PutObjectAclRequest), arg0)
-}
-
-// PutObjectAclWithContext mocks base method.
-func (m *MockClient) PutObjectAclWithContext(arg0 context.Context, arg1 *s3.PutObjectAclInput, arg2 ...request.Option) (*s3.PutObjectAclOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutObjectAclWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutObjectAclOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectAclWithContext indicates an expected call of PutObjectAclWithContext.
-func (mr *MockClientMockRecorder) PutObjectAclWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectAclWithContext", reflect.TypeOf((*MockClient)(nil).PutObjectAclWithContext), varargs...)
-}
-
-// PutObjectLegalHold mocks base method.
-func (m *MockClient) PutObjectLegalHold(arg0 *s3.PutObjectLegalHoldInput) (*s3.PutObjectLegalHoldOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectLegalHold", arg0)
- ret0, _ := ret[0].(*s3.PutObjectLegalHoldOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectLegalHold indicates an expected call of PutObjectLegalHold.
-func (mr *MockClientMockRecorder) PutObjectLegalHold(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectLegalHold", reflect.TypeOf((*MockClient)(nil).PutObjectLegalHold), arg0)
-}
-
-// PutObjectLegalHoldRequest mocks base method.
-func (m *MockClient) PutObjectLegalHoldRequest(arg0 *s3.PutObjectLegalHoldInput) (*request.Request, *s3.PutObjectLegalHoldOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectLegalHoldRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutObjectLegalHoldOutput)
- return ret0, ret1
-}
-
-// PutObjectLegalHoldRequest indicates an expected call of PutObjectLegalHoldRequest.
-func (mr *MockClientMockRecorder) PutObjectLegalHoldRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectLegalHoldRequest", reflect.TypeOf((*MockClient)(nil).PutObjectLegalHoldRequest), arg0)
-}
-
-// PutObjectLegalHoldWithContext mocks base method.
-func (m *MockClient) PutObjectLegalHoldWithContext(arg0 context.Context, arg1 *s3.PutObjectLegalHoldInput, arg2 ...request.Option) (*s3.PutObjectLegalHoldOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutObjectLegalHoldWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutObjectLegalHoldOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectLegalHoldWithContext indicates an expected call of PutObjectLegalHoldWithContext.
-func (mr *MockClientMockRecorder) PutObjectLegalHoldWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectLegalHoldWithContext", reflect.TypeOf((*MockClient)(nil).PutObjectLegalHoldWithContext), varargs...)
-}
-
-// PutObjectLockConfiguration mocks base method.
-func (m *MockClient) PutObjectLockConfiguration(arg0 *s3.PutObjectLockConfigurationInput) (*s3.PutObjectLockConfigurationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectLockConfiguration", arg0)
- ret0, _ := ret[0].(*s3.PutObjectLockConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectLockConfiguration indicates an expected call of PutObjectLockConfiguration.
-func (mr *MockClientMockRecorder) PutObjectLockConfiguration(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectLockConfiguration", reflect.TypeOf((*MockClient)(nil).PutObjectLockConfiguration), arg0)
-}
-
-// PutObjectLockConfigurationRequest mocks base method.
-func (m *MockClient) PutObjectLockConfigurationRequest(arg0 *s3.PutObjectLockConfigurationInput) (*request.Request, *s3.PutObjectLockConfigurationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectLockConfigurationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutObjectLockConfigurationOutput)
- return ret0, ret1
-}
-
-// PutObjectLockConfigurationRequest indicates an expected call of PutObjectLockConfigurationRequest.
-func (mr *MockClientMockRecorder) PutObjectLockConfigurationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectLockConfigurationRequest", reflect.TypeOf((*MockClient)(nil).PutObjectLockConfigurationRequest), arg0)
-}
-
-// PutObjectLockConfigurationWithContext mocks base method.
-func (m *MockClient) PutObjectLockConfigurationWithContext(arg0 context.Context, arg1 *s3.PutObjectLockConfigurationInput, arg2 ...request.Option) (*s3.PutObjectLockConfigurationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutObjectLockConfigurationWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutObjectLockConfigurationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectLockConfigurationWithContext indicates an expected call of PutObjectLockConfigurationWithContext.
-func (mr *MockClientMockRecorder) PutObjectLockConfigurationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectLockConfigurationWithContext", reflect.TypeOf((*MockClient)(nil).PutObjectLockConfigurationWithContext), varargs...)
-}
-
-// PutObjectRequest mocks base method.
-func (m *MockClient) PutObjectRequest(arg0 *s3.PutObjectInput) (*request.Request, *s3.PutObjectOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutObjectOutput)
- return ret0, ret1
-}
-
-// PutObjectRequest indicates an expected call of PutObjectRequest.
-func (mr *MockClientMockRecorder) PutObjectRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectRequest", reflect.TypeOf((*MockClient)(nil).PutObjectRequest), arg0)
-}
-
-// PutObjectRetention mocks base method.
-func (m *MockClient) PutObjectRetention(arg0 *s3.PutObjectRetentionInput) (*s3.PutObjectRetentionOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectRetention", arg0)
- ret0, _ := ret[0].(*s3.PutObjectRetentionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectRetention indicates an expected call of PutObjectRetention.
-func (mr *MockClientMockRecorder) PutObjectRetention(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectRetention", reflect.TypeOf((*MockClient)(nil).PutObjectRetention), arg0)
-}
-
-// PutObjectRetentionRequest mocks base method.
-func (m *MockClient) PutObjectRetentionRequest(arg0 *s3.PutObjectRetentionInput) (*request.Request, *s3.PutObjectRetentionOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectRetentionRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutObjectRetentionOutput)
- return ret0, ret1
-}
-
-// PutObjectRetentionRequest indicates an expected call of PutObjectRetentionRequest.
-func (mr *MockClientMockRecorder) PutObjectRetentionRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectRetentionRequest", reflect.TypeOf((*MockClient)(nil).PutObjectRetentionRequest), arg0)
-}
-
-// PutObjectRetentionWithContext mocks base method.
-func (m *MockClient) PutObjectRetentionWithContext(arg0 context.Context, arg1 *s3.PutObjectRetentionInput, arg2 ...request.Option) (*s3.PutObjectRetentionOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutObjectRetentionWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutObjectRetentionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectRetentionWithContext indicates an expected call of PutObjectRetentionWithContext.
-func (mr *MockClientMockRecorder) PutObjectRetentionWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectRetentionWithContext", reflect.TypeOf((*MockClient)(nil).PutObjectRetentionWithContext), varargs...)
-}
-
-// PutObjectTagging mocks base method.
-func (m *MockClient) PutObjectTagging(arg0 *s3.PutObjectTaggingInput) (*s3.PutObjectTaggingOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectTagging", arg0)
- ret0, _ := ret[0].(*s3.PutObjectTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectTagging indicates an expected call of PutObjectTagging.
-func (mr *MockClientMockRecorder) PutObjectTagging(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectTagging", reflect.TypeOf((*MockClient)(nil).PutObjectTagging), arg0)
-}
-
-// PutObjectTaggingRequest mocks base method.
-func (m *MockClient) PutObjectTaggingRequest(arg0 *s3.PutObjectTaggingInput) (*request.Request, *s3.PutObjectTaggingOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutObjectTaggingRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutObjectTaggingOutput)
- return ret0, ret1
-}
-
-// PutObjectTaggingRequest indicates an expected call of PutObjectTaggingRequest.
-func (mr *MockClientMockRecorder) PutObjectTaggingRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectTaggingRequest", reflect.TypeOf((*MockClient)(nil).PutObjectTaggingRequest), arg0)
-}
-
-// PutObjectTaggingWithContext mocks base method.
-func (m *MockClient) PutObjectTaggingWithContext(arg0 context.Context, arg1 *s3.PutObjectTaggingInput, arg2 ...request.Option) (*s3.PutObjectTaggingOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutObjectTaggingWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutObjectTaggingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectTaggingWithContext indicates an expected call of PutObjectTaggingWithContext.
-func (mr *MockClientMockRecorder) PutObjectTaggingWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectTaggingWithContext", reflect.TypeOf((*MockClient)(nil).PutObjectTaggingWithContext), varargs...)
-}
-
-// PutObjectWithContext mocks base method.
-func (m *MockClient) PutObjectWithContext(arg0 context.Context, arg1 *s3.PutObjectInput, arg2 ...request.Option) (*s3.PutObjectOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutObjectWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutObjectWithContext indicates an expected call of PutObjectWithContext.
-func (mr *MockClientMockRecorder) PutObjectWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObjectWithContext", reflect.TypeOf((*MockClient)(nil).PutObjectWithContext), varargs...)
-}
-
-// PutPublicAccessBlock mocks base method.
-func (m *MockClient) PutPublicAccessBlock(arg0 *s3.PutPublicAccessBlockInput) (*s3.PutPublicAccessBlockOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutPublicAccessBlock", arg0)
- ret0, _ := ret[0].(*s3.PutPublicAccessBlockOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutPublicAccessBlock indicates an expected call of PutPublicAccessBlock.
-func (mr *MockClientMockRecorder) PutPublicAccessBlock(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutPublicAccessBlock", reflect.TypeOf((*MockClient)(nil).PutPublicAccessBlock), arg0)
-}
-
-// PutPublicAccessBlockRequest mocks base method.
-func (m *MockClient) PutPublicAccessBlockRequest(arg0 *s3.PutPublicAccessBlockInput) (*request.Request, *s3.PutPublicAccessBlockOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutPublicAccessBlockRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.PutPublicAccessBlockOutput)
- return ret0, ret1
-}
-
-// PutPublicAccessBlockRequest indicates an expected call of PutPublicAccessBlockRequest.
-func (mr *MockClientMockRecorder) PutPublicAccessBlockRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutPublicAccessBlockRequest", reflect.TypeOf((*MockClient)(nil).PutPublicAccessBlockRequest), arg0)
-}
-
-// PutPublicAccessBlockWithContext mocks base method.
-func (m *MockClient) PutPublicAccessBlockWithContext(arg0 context.Context, arg1 *s3.PutPublicAccessBlockInput, arg2 ...request.Option) (*s3.PutPublicAccessBlockOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutPublicAccessBlockWithContext", varargs...)
- ret0, _ := ret[0].(*s3.PutPublicAccessBlockOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutPublicAccessBlockWithContext indicates an expected call of PutPublicAccessBlockWithContext.
-func (mr *MockClientMockRecorder) PutPublicAccessBlockWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutPublicAccessBlockWithContext", reflect.TypeOf((*MockClient)(nil).PutPublicAccessBlockWithContext), varargs...)
-}
-
-// RestoreObject mocks base method.
-func (m *MockClient) RestoreObject(arg0 *s3.RestoreObjectInput) (*s3.RestoreObjectOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "RestoreObject", arg0)
- ret0, _ := ret[0].(*s3.RestoreObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// RestoreObject indicates an expected call of RestoreObject.
-func (mr *MockClientMockRecorder) RestoreObject(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreObject", reflect.TypeOf((*MockClient)(nil).RestoreObject), arg0)
-}
-
-// RestoreObjectRequest mocks base method.
-func (m *MockClient) RestoreObjectRequest(arg0 *s3.RestoreObjectInput) (*request.Request, *s3.RestoreObjectOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "RestoreObjectRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.RestoreObjectOutput)
- return ret0, ret1
-}
-
-// RestoreObjectRequest indicates an expected call of RestoreObjectRequest.
-func (mr *MockClientMockRecorder) RestoreObjectRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreObjectRequest", reflect.TypeOf((*MockClient)(nil).RestoreObjectRequest), arg0)
-}
-
-// RestoreObjectWithContext mocks base method.
-func (m *MockClient) RestoreObjectWithContext(arg0 context.Context, arg1 *s3.RestoreObjectInput, arg2 ...request.Option) (*s3.RestoreObjectOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "RestoreObjectWithContext", varargs...)
- ret0, _ := ret[0].(*s3.RestoreObjectOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// RestoreObjectWithContext indicates an expected call of RestoreObjectWithContext.
-func (mr *MockClientMockRecorder) RestoreObjectWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreObjectWithContext", reflect.TypeOf((*MockClient)(nil).RestoreObjectWithContext), varargs...)
-}
-
-// SelectObjectContent mocks base method.
-func (m *MockClient) SelectObjectContent(arg0 *s3.SelectObjectContentInput) (*s3.SelectObjectContentOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "SelectObjectContent", arg0)
- ret0, _ := ret[0].(*s3.SelectObjectContentOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// SelectObjectContent indicates an expected call of SelectObjectContent.
-func (mr *MockClientMockRecorder) SelectObjectContent(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectObjectContent", reflect.TypeOf((*MockClient)(nil).SelectObjectContent), arg0)
-}
-
-// SelectObjectContentRequest mocks base method.
-func (m *MockClient) SelectObjectContentRequest(arg0 *s3.SelectObjectContentInput) (*request.Request, *s3.SelectObjectContentOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "SelectObjectContentRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.SelectObjectContentOutput)
- return ret0, ret1
-}
-
-// SelectObjectContentRequest indicates an expected call of SelectObjectContentRequest.
-func (mr *MockClientMockRecorder) SelectObjectContentRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectObjectContentRequest", reflect.TypeOf((*MockClient)(nil).SelectObjectContentRequest), arg0)
-}
-
-// SelectObjectContentWithContext mocks base method.
-func (m *MockClient) SelectObjectContentWithContext(arg0 context.Context, arg1 *s3.SelectObjectContentInput, arg2 ...request.Option) (*s3.SelectObjectContentOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "SelectObjectContentWithContext", varargs...)
- ret0, _ := ret[0].(*s3.SelectObjectContentOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// SelectObjectContentWithContext indicates an expected call of SelectObjectContentWithContext.
-func (mr *MockClientMockRecorder) SelectObjectContentWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SelectObjectContentWithContext", reflect.TypeOf((*MockClient)(nil).SelectObjectContentWithContext), varargs...)
-}
-
-// UploadPart mocks base method.
-func (m *MockClient) UploadPart(arg0 *s3.UploadPartInput) (*s3.UploadPartOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UploadPart", arg0)
- ret0, _ := ret[0].(*s3.UploadPartOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UploadPart indicates an expected call of UploadPart.
-func (mr *MockClientMockRecorder) UploadPart(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadPart", reflect.TypeOf((*MockClient)(nil).UploadPart), arg0)
-}
-
-// UploadPartCopy mocks base method.
-func (m *MockClient) UploadPartCopy(arg0 *s3.UploadPartCopyInput) (*s3.UploadPartCopyOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UploadPartCopy", arg0)
- ret0, _ := ret[0].(*s3.UploadPartCopyOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UploadPartCopy indicates an expected call of UploadPartCopy.
-func (mr *MockClientMockRecorder) UploadPartCopy(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadPartCopy", reflect.TypeOf((*MockClient)(nil).UploadPartCopy), arg0)
-}
-
-// UploadPartCopyRequest mocks base method.
-func (m *MockClient) UploadPartCopyRequest(arg0 *s3.UploadPartCopyInput) (*request.Request, *s3.UploadPartCopyOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UploadPartCopyRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.UploadPartCopyOutput)
- return ret0, ret1
-}
-
-// UploadPartCopyRequest indicates an expected call of UploadPartCopyRequest.
-func (mr *MockClientMockRecorder) UploadPartCopyRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadPartCopyRequest", reflect.TypeOf((*MockClient)(nil).UploadPartCopyRequest), arg0)
-}
-
-// UploadPartCopyWithContext mocks base method.
-func (m *MockClient) UploadPartCopyWithContext(arg0 context.Context, arg1 *s3.UploadPartCopyInput, arg2 ...request.Option) (*s3.UploadPartCopyOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UploadPartCopyWithContext", varargs...)
- ret0, _ := ret[0].(*s3.UploadPartCopyOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UploadPartCopyWithContext indicates an expected call of UploadPartCopyWithContext.
-func (mr *MockClientMockRecorder) UploadPartCopyWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadPartCopyWithContext", reflect.TypeOf((*MockClient)(nil).UploadPartCopyWithContext), varargs...)
-}
-
-// UploadPartRequest mocks base method.
-func (m *MockClient) UploadPartRequest(arg0 *s3.UploadPartInput) (*request.Request, *s3.UploadPartOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UploadPartRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.UploadPartOutput)
- return ret0, ret1
-}
-
-// UploadPartRequest indicates an expected call of UploadPartRequest.
-func (mr *MockClientMockRecorder) UploadPartRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadPartRequest", reflect.TypeOf((*MockClient)(nil).UploadPartRequest), arg0)
-}
-
-// UploadPartWithContext mocks base method.
-func (m *MockClient) UploadPartWithContext(arg0 context.Context, arg1 *s3.UploadPartInput, arg2 ...request.Option) (*s3.UploadPartOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UploadPartWithContext", varargs...)
- ret0, _ := ret[0].(*s3.UploadPartOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UploadPartWithContext indicates an expected call of UploadPartWithContext.
-func (mr *MockClientMockRecorder) UploadPartWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadPartWithContext", reflect.TypeOf((*MockClient)(nil).UploadPartWithContext), varargs...)
-}
-
-// WaitUntilBucketExists mocks base method.
-func (m *MockClient) WaitUntilBucketExists(arg0 *s3.HeadBucketInput) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "WaitUntilBucketExists", arg0)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilBucketExists indicates an expected call of WaitUntilBucketExists.
-func (mr *MockClientMockRecorder) WaitUntilBucketExists(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilBucketExists", reflect.TypeOf((*MockClient)(nil).WaitUntilBucketExists), arg0)
-}
-
-// WaitUntilBucketExistsWithContext mocks base method.
-func (m *MockClient) WaitUntilBucketExistsWithContext(arg0 context.Context, arg1 *s3.HeadBucketInput, arg2 ...request.WaiterOption) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "WaitUntilBucketExistsWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilBucketExistsWithContext indicates an expected call of WaitUntilBucketExistsWithContext.
-func (mr *MockClientMockRecorder) WaitUntilBucketExistsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilBucketExistsWithContext", reflect.TypeOf((*MockClient)(nil).WaitUntilBucketExistsWithContext), varargs...)
-}
-
-// WaitUntilBucketNotExists mocks base method.
-func (m *MockClient) WaitUntilBucketNotExists(arg0 *s3.HeadBucketInput) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "WaitUntilBucketNotExists", arg0)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilBucketNotExists indicates an expected call of WaitUntilBucketNotExists.
-func (mr *MockClientMockRecorder) WaitUntilBucketNotExists(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilBucketNotExists", reflect.TypeOf((*MockClient)(nil).WaitUntilBucketNotExists), arg0)
-}
-
-// WaitUntilBucketNotExistsWithContext mocks base method.
-func (m *MockClient) WaitUntilBucketNotExistsWithContext(arg0 context.Context, arg1 *s3.HeadBucketInput, arg2 ...request.WaiterOption) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "WaitUntilBucketNotExistsWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilBucketNotExistsWithContext indicates an expected call of WaitUntilBucketNotExistsWithContext.
-func (mr *MockClientMockRecorder) WaitUntilBucketNotExistsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilBucketNotExistsWithContext", reflect.TypeOf((*MockClient)(nil).WaitUntilBucketNotExistsWithContext), varargs...)
-}
-
-// WaitUntilObjectExists mocks base method.
-func (m *MockClient) WaitUntilObjectExists(arg0 *s3.HeadObjectInput) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "WaitUntilObjectExists", arg0)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilObjectExists indicates an expected call of WaitUntilObjectExists.
-func (mr *MockClientMockRecorder) WaitUntilObjectExists(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilObjectExists", reflect.TypeOf((*MockClient)(nil).WaitUntilObjectExists), arg0)
-}
-
-// WaitUntilObjectExistsWithContext mocks base method.
-func (m *MockClient) WaitUntilObjectExistsWithContext(arg0 context.Context, arg1 *s3.HeadObjectInput, arg2 ...request.WaiterOption) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "WaitUntilObjectExistsWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilObjectExistsWithContext indicates an expected call of WaitUntilObjectExistsWithContext.
-func (mr *MockClientMockRecorder) WaitUntilObjectExistsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilObjectExistsWithContext", reflect.TypeOf((*MockClient)(nil).WaitUntilObjectExistsWithContext), varargs...)
-}
-
-// WaitUntilObjectNotExists mocks base method.
-func (m *MockClient) WaitUntilObjectNotExists(arg0 *s3.HeadObjectInput) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "WaitUntilObjectNotExists", arg0)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilObjectNotExists indicates an expected call of WaitUntilObjectNotExists.
-func (mr *MockClientMockRecorder) WaitUntilObjectNotExists(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilObjectNotExists", reflect.TypeOf((*MockClient)(nil).WaitUntilObjectNotExists), arg0)
-}
-
-// WaitUntilObjectNotExistsWithContext mocks base method.
-func (m *MockClient) WaitUntilObjectNotExistsWithContext(arg0 context.Context, arg1 *s3.HeadObjectInput, arg2 ...request.WaiterOption) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "WaitUntilObjectNotExistsWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilObjectNotExistsWithContext indicates an expected call of WaitUntilObjectNotExistsWithContext.
-func (mr *MockClientMockRecorder) WaitUntilObjectNotExistsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilObjectNotExistsWithContext", reflect.TypeOf((*MockClient)(nil).WaitUntilObjectNotExistsWithContext), varargs...)
-}
-
-// WriteGetObjectResponse mocks base method.
-func (m *MockClient) WriteGetObjectResponse(arg0 *s3.WriteGetObjectResponseInput) (*s3.WriteGetObjectResponseOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "WriteGetObjectResponse", arg0)
- ret0, _ := ret[0].(*s3.WriteGetObjectResponseOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// WriteGetObjectResponse indicates an expected call of WriteGetObjectResponse.
-func (mr *MockClientMockRecorder) WriteGetObjectResponse(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteGetObjectResponse", reflect.TypeOf((*MockClient)(nil).WriteGetObjectResponse), arg0)
-}
-
-// WriteGetObjectResponseRequest mocks base method.
-func (m *MockClient) WriteGetObjectResponseRequest(arg0 *s3.WriteGetObjectResponseInput) (*request.Request, *s3.WriteGetObjectResponseOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "WriteGetObjectResponseRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*s3.WriteGetObjectResponseOutput)
- return ret0, ret1
-}
-
-// WriteGetObjectResponseRequest indicates an expected call of WriteGetObjectResponseRequest.
-func (mr *MockClientMockRecorder) WriteGetObjectResponseRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteGetObjectResponseRequest", reflect.TypeOf((*MockClient)(nil).WriteGetObjectResponseRequest), arg0)
-}
-
-// WriteGetObjectResponseWithContext mocks base method.
-func (m *MockClient) WriteGetObjectResponseWithContext(arg0 context.Context, arg1 *s3.WriteGetObjectResponseInput, arg2 ...request.Option) (*s3.WriteGetObjectResponseOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "WriteGetObjectResponseWithContext", varargs...)
- ret0, _ := ret[0].(*s3.WriteGetObjectResponseOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// WriteGetObjectResponseWithContext indicates an expected call of WriteGetObjectResponseWithContext.
-func (mr *MockClientMockRecorder) WriteGetObjectResponseWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// PutObject indicates an expected call of PutObject.
+func (mr *MockClientMockRecorder) PutObject(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WriteGetObjectResponseWithContext", reflect.TypeOf((*MockClient)(nil).WriteGetObjectResponseWithContext), varargs...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutObject", reflect.TypeOf((*MockClient)(nil).PutObject), varargs...)
}
diff --git a/internal/server/handler.go b/internal/server/handler.go
index d1142fab..345ab575 100644
--- a/internal/server/handler.go
+++ b/internal/server/handler.go
@@ -2,6 +2,7 @@ package server
import (
"context"
+ "errors"
"fmt"
"net"
"regexp"
@@ -26,6 +27,7 @@ import (
"google.golang.org/grpc/keepalive"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
+ "google.golang.org/protobuf/proto"
"github.com/coinbase/chainstorage/internal/blockchain/client"
"github.com/coinbase/chainstorage/internal/blockchain/parser"
@@ -40,7 +42,6 @@ import (
"github.com/coinbase/chainstorage/internal/utils/consts"
"github.com/coinbase/chainstorage/internal/utils/fxparams"
"github.com/coinbase/chainstorage/internal/utils/log"
- "github.com/coinbase/chainstorage/internal/utils/syncgroup"
"github.com/coinbase/chainstorage/internal/utils/utils"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
"github.com/coinbase/chainstorage/sdk/services"
@@ -107,6 +108,38 @@ type (
contextKey string
)
+// GetBlockByTimestamp implements chainstorage.ChainStorageServer.
+func (s *Server) GetBlockByTimestamp(ctx context.Context, req *api.GetBlockByTimestampRequest) (*api.GetBlockByTimestampResponse, error) {
+ clientID := getClientID(ctx)
+
+ tag := s.config.GetEffectiveBlockTag(req.GetTag())
+ if err := s.validateTag(tag); err != nil {
+ return nil, xerrors.Errorf("failed to validate tag: %w", err)
+ }
+
+ timestamp := req.GetTimestamp()
+ if timestamp == 0 {
+ return nil, status.Error(codes.InvalidArgument, "timestamp is required")
+ }
+
+ // Get block from meta storage using timestamp
+ block, err := s.metaStorage.GetBlockByTimestamp(ctx, tag, timestamp)
+ if err != nil {
+ // Don't wrap the error to allow the interceptor to properly map it
+ return nil, err
+ }
+
+ s.emitBlocksMetric("timestamp", clientID, 1)
+
+ return &api.GetBlockByTimestampResponse{
+ Tag: block.Tag,
+ Hash: block.Hash,
+ ParentHash: block.ParentHash,
+ Height: block.Height,
+ Timestamp: uint64(block.Timestamp.GetSeconds()),
+ }, nil
+}
+
const (
// Custom interceptors
errorInterceptorID = "xerror"
@@ -138,6 +171,20 @@ const (
accountStateServedCounter = "account_state_served"
+ shadowReadCounter = "shadow_read"
+ shadowReadLogicalBytesCounter = "shadow_read_logical_bytes"
+ shadowReadLatencyTimer = "shadow_read_latency"
+ shadowPathTag = "path"
+ shadowOutcomeTag = "outcome"
+ shadowPathConsolidated = "consolidated"
+ shadowPathSingleBlock = "single-block"
+ shadowOutcomeMiss = "miss"
+ shadowOutcomeError = "error"
+ shadowOutcomeValidation = "validation_mismatch"
+ shadowOutcomeSuccess = "success"
+ shadowOutcomeFallbackSuccess = "fallback_success"
+ shadowOutcomeFallbackFailure = "fallback_failure"
+
errorCounter = "error"
serviceTag = "service"
methodTag = "method"
@@ -190,6 +237,7 @@ var rcuByMethod = map[string]int{
"GetRosettaBlocksByRange": 50,
"GetNativeTransaction": 10,
"GetVerifiedAccountState": 10,
+ "GetBlockByTimestamp": 5,
}
func NewServer(params ServerParams) *Server {
@@ -326,6 +374,24 @@ func (s *Server) emitAccountStateMetric(clientID string, count int64) {
s.metrics.scope.Tagged(map[string]string{clientIDTag: clientID}).Counter(accountStateServedCounter).Inc(count)
}
+func (s *Server) emitShadowReadMetric(outcome string, count int64) {
+ if count <= 0 {
+ return
+ }
+ s.metrics.scope.Tagged(map[string]string{shadowOutcomeTag: outcome}).Counter(shadowReadCounter).Inc(count)
+}
+
+func (s *Server) emitShadowReadLogicalBytes(path string, count int64) {
+ if count <= 0 {
+ return
+ }
+ s.metrics.scope.Tagged(map[string]string{shadowPathTag: path}).Counter(shadowReadLogicalBytesCounter).Inc(count)
+}
+
+func (s *Server) emitShadowReadLatency(path string, duration time.Duration) {
+ s.metrics.scope.Tagged(map[string]string{shadowPathTag: path}).Timer(shadowReadLatencyTimer).Record(duration)
+}
+
func (s *Server) GetLatestBlock(ctx context.Context, req *api.GetLatestBlockRequest) (*api.GetLatestBlockResponse, error) {
tag := s.config.GetEffectiveBlockTag(req.GetTag())
if err := s.validateTag(tag); err != nil {
@@ -348,12 +414,37 @@ func (s *Server) GetLatestBlock(ctx context.Context, req *api.GetLatestBlockRequ
func (s *Server) GetBlockFile(ctx context.Context, req *api.GetBlockFileRequest) (*api.GetBlockFileResponse, error) {
clientID := getClientID(ctx)
+ if err := validateBlockReadSource(req.GetReadSource()); err != nil {
+ return nil, err
+ }
block, err := s.getBlockFromMetaStorage(ctx, req)
if err != nil {
return nil, xerrors.Errorf("failed to get block from meta storage: %w", err)
}
+ if shouldReadFromConsolidated(req.GetReadSource(), s.config.AWS.Storage.Consolidation.ReadShadowFirst) {
+ if consolidated, ok := s.getConsolidatedBlockMetadata(ctx, block); ok {
+ blockFile, err := s.newBlockFile(consolidated)
+ if err == nil {
+ s.emitBlocksMetric(formatFile, clientID, 1)
+ return &api.GetBlockFileResponse{File: blockFile}, nil
+ }
+ if isConsolidatedBlockMetadata(block) {
+ return nil, xerrors.Errorf("failed to prepare consolidated block file: %w", err)
+ }
+ s.emitShadowReadMetric(shadowOutcomeError, 1)
+ s.logger.Warn(
+ "failed to prepare consolidated block file; falling back to single-block",
+ zap.Uint32("tag", block.GetTag()),
+ zap.Uint64("height", block.GetHeight()),
+ zap.String("hash", block.GetHash()),
+ zap.String("consolidated_object_key", consolidated.GetObjectKeyMain()),
+ zap.Error(err),
+ )
+ }
+ }
+
blockFile, err := s.newBlockFile(block)
if err != nil {
return nil, xerrors.Errorf("failed to prepare block file: %w", err)
@@ -368,20 +459,37 @@ func (s *Server) GetBlockFile(ctx context.Context, req *api.GetBlockFileRequest)
func (s *Server) GetBlockFilesByRange(ctx context.Context, req *api.GetBlockFilesByRangeRequest) (*api.GetBlockFilesByRangeResponse, error) {
clientID := getClientID(ctx)
+ if err := validateBlockReadSource(req.GetReadSource()); err != nil {
+ return nil, err
+ }
blocks, err := s.getBlocksFromMetaStorage(ctx, req, s.config.Api.MaxNumBlockFiles)
if err != nil {
return nil, xerrors.Errorf("failed to get blocks from meta storage: %w", err)
}
- blockFiles := make([]*api.BlockFile, len(blocks))
- for i := 0; i < len(blocks); i++ {
- blockFile, err := s.newBlockFile(blocks[i])
- if err != nil {
+ selectedBlocks := blocks
+ shadowCount := 0
+ if shouldReadFromConsolidated(req.GetReadSource(), s.config.AWS.Storage.Consolidation.ReadShadowFirst) {
+ selectedBlocks, shadowCount, _, _ = s.selectShadowBlockMetadata(ctx, blocks)
+ }
+
+ blockFiles, err := s.newBlockFiles(selectedBlocks)
+ if err != nil && shadowCount > 0 {
+ if allConsolidatedBlockMetadata(blocks) {
return nil, xerrors.Errorf("newBlockFile error: %w", err)
}
-
- blockFiles[i] = blockFile
+ s.emitShadowReadMetric(shadowOutcomeError, int64(shadowCount))
+ s.logger.Warn(
+ "failed to prepare consolidated block files; falling back to single-block",
+ zap.Int("num_blocks", len(blocks)),
+ zap.Int("shadow_blocks", shadowCount),
+ zap.Error(err),
+ )
+ blockFiles, err = s.newBlockFiles(blocks)
+ }
+ if err != nil {
+ return nil, xerrors.Errorf("newBlockFile error: %w", err)
}
s.emitBlocksMetric(formatFile, clientID, int64(len(blockFiles)))
@@ -391,13 +499,16 @@ func (s *Server) GetBlockFilesByRange(ctx context.Context, req *api.GetBlockFile
func (s *Server) GetRawBlock(ctx context.Context, req *api.GetRawBlockRequest) (*api.GetRawBlockResponse, error) {
clientID := getClientID(ctx)
+ if err := validateBlockReadSource(req.GetReadSource()); err != nil {
+ return nil, err
+ }
block, err := s.getBlockFromMetaStorage(ctx, req)
if err != nil {
return nil, xerrors.Errorf("failed to get block from meta storage: %w", err)
}
- rawBlock, err := s.getBlockFromBlobStorage(ctx, block)
+ rawBlock, err := s.getBlockFromBlobStorage(ctx, block, req.GetReadSource())
if err != nil {
return nil, xerrors.Errorf("failed to get raw blocks: %w", err)
}
@@ -411,13 +522,16 @@ func (s *Server) GetRawBlock(ctx context.Context, req *api.GetRawBlockRequest) (
func (s *Server) GetRawBlocksByRange(ctx context.Context, req *api.GetRawBlocksByRangeRequest) (*api.GetRawBlocksByRangeResponse, error) {
clientID := getClientID(ctx)
+ if err := validateBlockReadSource(req.GetReadSource()); err != nil {
+ return nil, err
+ }
blocks, err := s.getBlocksFromMetaStorage(ctx, req, s.config.Api.MaxNumBlocks)
if err != nil {
return nil, xerrors.Errorf("failed to get blocks from meta storage: %w", err)
}
- rawBlocks, err := s.getBlocksFromBlobStorage(ctx, blocks)
+ rawBlocks, err := s.getBlocksFromBlobStorage(ctx, blocks, req.GetReadSource())
if err != nil {
return nil, xerrors.Errorf("failed to get raw blocks: %w", err)
}
@@ -431,13 +545,16 @@ func (s *Server) GetRawBlocksByRange(ctx context.Context, req *api.GetRawBlocksB
func (s *Server) GetNativeBlock(ctx context.Context, req *api.GetNativeBlockRequest) (*api.GetNativeBlockResponse, error) {
clientID := getClientID(ctx)
+ if err := validateBlockReadSource(req.GetReadSource()); err != nil {
+ return nil, err
+ }
block, err := s.getBlockFromMetaStorage(ctx, req)
if err != nil {
return nil, xerrors.Errorf("failed to get block from meta storage: %w", err)
}
- rawBlock, err := s.getBlockFromBlobStorage(ctx, block)
+ rawBlock, err := s.getBlockFromBlobStorage(ctx, block, req.GetReadSource())
if err != nil {
return nil, xerrors.Errorf("failed to get raw blocks: %w", err)
}
@@ -456,13 +573,16 @@ func (s *Server) GetNativeBlock(ctx context.Context, req *api.GetNativeBlockRequ
func (s *Server) GetNativeBlocksByRange(ctx context.Context, req *api.GetNativeBlocksByRangeRequest) (*api.GetNativeBlocksByRangeResponse, error) {
clientID := getClientID(ctx)
+ if err := validateBlockReadSource(req.GetReadSource()); err != nil {
+ return nil, err
+ }
blocks, err := s.getBlocksFromMetaStorage(ctx, req, s.config.Api.MaxNumBlocks)
if err != nil {
return nil, xerrors.Errorf("failed to get blocks from meta storage: %w", err)
}
- rawBlocks, err := s.getBlocksFromBlobStorage(ctx, blocks)
+ rawBlocks, err := s.getBlocksFromBlobStorage(ctx, blocks, req.GetReadSource())
if err != nil {
return nil, xerrors.Errorf("failed to get raw blocks: %w", err)
}
@@ -487,13 +607,16 @@ func (s *Server) GetNativeBlocksByRange(ctx context.Context, req *api.GetNativeB
func (s *Server) GetRosettaBlock(ctx context.Context, req *api.GetRosettaBlockRequest) (*api.GetRosettaBlockResponse, error) {
// TODO: short-circuit fetching block from blob-storage if RosettaParser is not implemented for chain
clientID := getClientID(ctx)
+ if err := validateBlockReadSource(req.GetReadSource()); err != nil {
+ return nil, err
+ }
block, err := s.getBlockFromMetaStorage(ctx, req)
if err != nil {
return nil, xerrors.Errorf("failed to get block from meta storage: %w", err)
}
- rawBlock, err := s.getBlockFromBlobStorage(ctx, block)
+ rawBlock, err := s.getBlockFromBlobStorage(ctx, block, req.GetReadSource())
if err != nil {
return nil, xerrors.Errorf("failed to get raw blocks: %w", err)
}
@@ -512,13 +635,16 @@ func (s *Server) GetRosettaBlock(ctx context.Context, req *api.GetRosettaBlockRe
func (s *Server) GetRosettaBlocksByRange(ctx context.Context, req *api.GetRosettaBlocksByRangeRequest) (*api.GetRosettaBlocksByRangeResponse, error) {
clientID := getClientID(ctx)
+ if err := validateBlockReadSource(req.GetReadSource()); err != nil {
+ return nil, err
+ }
blocks, err := s.getBlocksFromMetaStorage(ctx, req, s.config.Api.MaxNumBlocks)
if err != nil {
return nil, xerrors.Errorf("failed to get blocks from meta storage: %w", err)
}
- rawBlocks, err := s.getBlocksFromBlobStorage(ctx, blocks)
+ rawBlocks, err := s.getBlocksFromBlobStorage(ctx, blocks, req.GetReadSource())
if err != nil {
return nil, xerrors.Errorf("failed to get raw blocks: %w", err)
}
@@ -579,7 +705,7 @@ func (s *Server) GetNativeTransaction(ctx context.Context, req *api.GetNativeTra
return nil, xerrors.Errorf("failed to get blocks from transaction storage: %w", err)
}
- rawBlocks, err := s.getBlocksFromBlobStorage(ctx, blocks)
+ rawBlocks, err := s.getBlocksFromBlobStorage(ctx, blocks, api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT)
if err != nil {
return nil, xerrors.Errorf("failed to get raw blocks: %w", err)
}
@@ -618,7 +744,7 @@ func (s *Server) GetVerifiedAccountState(ctx context.Context, req *api.GetVerifi
return nil, xerrors.Errorf("failed to get block from meta storage: %w", err)
}
- rawBlock, err := s.getBlockFromBlobStorage(ctx, block)
+ rawBlock, err := s.getBlockFromBlobStorage(ctx, block, api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT)
if err != nil {
return nil, xerrors.Errorf("failed to get raw blocks: %w", err)
}
@@ -703,6 +829,23 @@ func (s *Server) getBlocksFromTransactionStorage(ctx context.Context, tag uint32
}
func (s *Server) newBlockFile(block *api.BlockMetadata) (*api.BlockFile, error) {
+ return s.newBlockFileWithPresignCache(block, nil)
+}
+
+func (s *Server) newBlockFiles(blocks []*api.BlockMetadata) ([]*api.BlockFile, error) {
+ blockFiles := make([]*api.BlockFile, len(blocks))
+ presignedURLs := make(map[string]string)
+ for i := 0; i < len(blocks); i++ {
+ blockFile, err := s.newBlockFileWithPresignCache(blocks[i], presignedURLs)
+ if err != nil {
+ return nil, err
+ }
+ blockFiles[i] = blockFile
+ }
+ return blockFiles, nil
+}
+
+func (s *Server) newBlockFileWithPresignCache(block *api.BlockMetadata, presignedURLs map[string]string) (*api.BlockFile, error) {
if block.Skipped {
return &api.BlockFile{
Tag: block.Tag,
@@ -713,20 +856,31 @@ func (s *Server) newBlockFile(block *api.BlockMetadata) (*api.BlockFile, error)
key := block.GetObjectKeyMain()
compression := storage_utils.GetCompressionType(key)
- fileUrl, err := s.blobStorage.PreSign(context.Background(), key)
- if err != nil {
- return nil, xerrors.Errorf("failed to generate presigned url: %w", err)
+ fileUrl, ok := presignedURLs[key]
+ if !ok {
+ var err error
+ fileUrl, err = s.blobStorage.PreSign(context.Background(), key)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to generate presigned url: %w", err)
+ }
+ if presignedURLs != nil {
+ presignedURLs[key] = fileUrl
+ }
}
return &api.BlockFile{
- Tag: block.Tag,
- Hash: block.Hash,
- ParentHash: block.ParentHash,
- Height: block.Height,
- ParentHeight: block.ParentHeight,
- FileUrl: fileUrl,
- Compression: compression,
- BlockTimestamp: block.Timestamp,
+ Tag: block.Tag,
+ Hash: block.Hash,
+ ParentHash: block.ParentHash,
+ Height: block.Height,
+ ParentHeight: block.ParentHeight,
+ FileUrl: fileUrl,
+ Compression: compression,
+ BlockTimestamp: block.Timestamp,
+ ObjectFormat: block.GetObjectFormat(),
+ ByteOffset: block.GetByteOffset(),
+ ByteLength: block.GetByteLength(),
+ UncompressedLength: block.GetUncompressedLength(),
}, nil
}
@@ -806,37 +960,338 @@ func (s *Server) getBlocksFromMetaStorage(ctx context.Context, req requestByRang
return blocks, nil
}
-func (s *Server) getBlockFromBlobStorage(ctx context.Context, block *api.BlockMetadata) (*api.Block, error) {
+func (s *Server) getBlockFromBlobStorage(ctx context.Context, block *api.BlockMetadata, readSource api.BlockReadSource) (*api.Block, error) {
+ if !shouldReadFromConsolidated(readSource, s.config.AWS.Storage.Consolidation.ReadShadowFirst) {
+ output, err := s.blobStorage.Download(ctx, block)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to download from blob storage (input={%+v}): %w", block, err)
+ }
+ return output, nil
+ }
+
+ consolidated, ok := s.getConsolidatedBlockMetadata(ctx, block)
+ if !ok {
+ output, err := s.downloadBlockWithShadowMetrics(ctx, block, shadowPathSingleBlock)
+ if err != nil {
+ s.emitShadowReadMetric(shadowOutcomeFallbackFailure, 1)
+ return nil, xerrors.Errorf("failed to download fallback single-block block from blob storage (input={%+v}): %w", block, err)
+ }
+ s.emitShadowReadMetric(shadowOutcomeFallbackSuccess, 1)
+ return output, nil
+ }
+
+ output, err := s.downloadBlockWithShadowMetrics(ctx, consolidated, shadowPathConsolidated)
+ if err == nil {
+ s.emitShadowReadMetric(shadowOutcomeSuccess, 1)
+ return blockWithPrimaryMetadata(output, block), nil
+ }
+ if isConsolidatedBlockMetadata(block) {
+ s.emitShadowReadMetric(shadowReadErrorOutcome(err), 1)
+ return nil, err
+ }
+
+ s.emitShadowReadMetric(shadowReadErrorOutcome(err), 1)
+ s.logger.Warn(
+ "shadow consolidated block read failed; falling back to single-block",
+ zap.Uint32("tag", block.GetTag()),
+ zap.Uint64("height", block.GetHeight()),
+ zap.String("hash", block.GetHash()),
+ zap.String("consolidated_object_key", consolidated.GetObjectKeyMain()),
+ zap.Error(err),
+ )
+ fallback, fallbackErr := s.downloadBlockWithShadowMetrics(ctx, block, shadowPathSingleBlock)
+ if fallbackErr != nil {
+ s.emitShadowReadMetric(shadowOutcomeFallbackFailure, 1)
+ return nil, xerrors.Errorf("failed to download fallback single-block block from blob storage (input={%+v}): %w", block, fallbackErr)
+ }
+ s.emitShadowReadMetric(shadowOutcomeFallbackSuccess, 1)
+ return fallback, nil
+}
+
+func (s *Server) getBlocksFromBlobStorage(ctx context.Context, blocks []*api.BlockMetadata, readSource api.BlockReadSource) ([]*api.Block, error) {
+ if !shouldReadFromConsolidated(readSource, s.config.AWS.Storage.Consolidation.ReadShadowFirst) {
+ output, err := s.blobStorage.DownloadMany(ctx, blocks)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to download blocks from blob storage: %w", err)
+ }
+ return output, nil
+ }
+
+ selectedBlocks, shadowCount, fallbackCount, _ := s.selectShadowBlockMetadata(ctx, blocks)
+ output, err := s.downloadBlocksWithShadowMetrics(ctx, selectedBlocks)
+ if err == nil {
+ s.emitShadowReadMetric(shadowOutcomeSuccess, int64(shadowCount))
+ s.emitShadowReadMetric(shadowOutcomeFallbackSuccess, int64(fallbackCount))
+ return blocksWithPrimaryMetadata(output, blocks), nil
+ }
+
+ if shadowCount == 0 {
+ s.emitShadowReadMetric(shadowOutcomeFallbackFailure, int64(fallbackCount))
+ return nil, xerrors.Errorf("failed to download blocks from blob storage: %w", err)
+ }
+ if allConsolidatedBlockMetadata(blocks) {
+ s.emitShadowReadMetric(shadowReadErrorOutcome(err), int64(shadowCount))
+ return nil, err
+ }
+
+ s.emitShadowReadMetric(shadowReadErrorOutcome(err), int64(shadowCount))
+ s.logger.Warn(
+ "shadow consolidated range read failed; falling back to single-block",
+ zap.Int("num_blocks", len(blocks)),
+ zap.Int("shadow_blocks", shadowCount),
+ zap.Error(err),
+ )
+ fallback, fallbackErr := s.downloadBlocksWithShadowMetrics(ctx, blocks)
+ if fallbackErr != nil {
+ s.emitShadowReadMetric(shadowOutcomeFallbackFailure, int64(len(blocks)))
+ return nil, xerrors.Errorf("failed to download fallback single-block blocks from blob storage: %w", fallbackErr)
+ }
+ s.emitShadowReadMetric(shadowOutcomeFallbackSuccess, int64(len(blocks)))
+ return blocksWithPrimaryMetadata(fallback, blocks), nil
+}
+
+func validateBlockReadSource(readSource api.BlockReadSource) error {
+ switch readSource {
+ case api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT,
+ api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED:
+ return nil
+ default:
+ return status.Errorf(codes.InvalidArgument, "invalid read_source: %v", readSource)
+ }
+}
+
+func shouldReadFromConsolidated(readSource api.BlockReadSource, defaultReadShadowFirst bool) bool {
+ switch readSource {
+ case api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED:
+ return true
+ case api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK:
+ return false
+ default:
+ return defaultReadShadowFirst
+ }
+}
+
+func (s *Server) getConsolidatedBlockMetadata(ctx context.Context, block *api.BlockMetadata) (*api.BlockMetadata, bool) {
+ if isConsolidatedBlockMetadata(block) {
+ return block, true
+ }
+ return s.getShadowBlockMetadata(ctx, block)
+}
+
+func (s *Server) getShadowBlockMetadata(ctx context.Context, block *api.BlockMetadata) (*api.BlockMetadata, bool) {
+ if block.GetSkipped() {
+ s.emitShadowReadMetric(shadowOutcomeMiss, 1)
+ return nil, false
+ }
+
+ shadow, err := s.metaStorage.GetBlockConsolidationShadow(ctx, block)
+ if err != nil {
+ if xerrors.Is(err, storage.ErrItemNotFound) {
+ s.emitShadowReadMetric(shadowOutcomeMiss, 1)
+ return nil, false
+ }
+ s.emitShadowReadMetric(shadowOutcomeError, 1)
+ s.logger.Warn(
+ "shadow consolidated metadata lookup failed; falling back to single-block",
+ zap.Uint32("tag", block.GetTag()),
+ zap.Uint64("height", block.GetHeight()),
+ zap.String("hash", block.GetHash()),
+ zap.Error(err),
+ )
+ return nil, false
+ }
+ if !isConsolidatedBlockMetadata(shadow) {
+ s.emitShadowReadMetric(shadowOutcomeValidation, 1)
+ s.logger.Warn(
+ "shadow consolidated metadata is invalid; falling back to single-block",
+ zap.Uint32("tag", block.GetTag()),
+ zap.Uint64("height", block.GetHeight()),
+ zap.String("hash", block.GetHash()),
+ zap.String("shadow_object_key", shadow.GetObjectKeyMain()),
+ zap.Int32("object_format", int32(shadow.GetObjectFormat())),
+ zap.Uint64("byte_length", shadow.GetByteLength()),
+ )
+ return nil, false
+ }
+ return shadow, true
+}
+
+func (s *Server) selectShadowBlockMetadata(ctx context.Context, blocks []*api.BlockMetadata) ([]*api.BlockMetadata, int, int, error) {
+ selected := make([]*api.BlockMetadata, len(blocks))
+ var shadowCount int
+ var fallbackCount int
+ shadows, err := s.metaStorage.GetBlocksConsolidationShadow(ctx, blocks)
+ if err != nil {
+ s.emitShadowReadMetric(shadowOutcomeError, int64(len(blocks)))
+ s.logger.Warn(
+ "shadow consolidated metadata batch lookup failed; falling back to single-block",
+ zap.Int("num_blocks", len(blocks)),
+ zap.Error(err),
+ )
+ copy(selected, blocks)
+ return selected, 0, len(blocks), xerrors.Errorf("failed to get consolidated block metadata: %w", err)
+ }
+ if len(shadows) != len(blocks) {
+ s.emitShadowReadMetric(shadowOutcomeError, int64(len(blocks)))
+ s.logger.Warn(
+ "shadow consolidated metadata batch lookup returned invalid result count; falling back to single-block",
+ zap.Int("num_blocks", len(blocks)),
+ zap.Int("num_shadows", len(shadows)),
+ )
+ copy(selected, blocks)
+ return selected, 0, len(blocks), xerrors.Errorf("consolidated metadata batch lookup returned %d results for %d blocks", len(shadows), len(blocks))
+ }
+
+ var selectionErr error
+ for i, block := range blocks {
+ shadow := shadows[i]
+ if shadow == nil {
+ if isConsolidatedBlockMetadata(block) {
+ selected[i] = block
+ shadowCount++
+ continue
+ }
+ s.emitShadowReadMetric(shadowOutcomeMiss, 1)
+ selected[i] = block
+ fallbackCount++
+ continue
+ }
+ if isConsolidatedBlockMetadata(shadow) {
+ selected[i] = shadow
+ shadowCount++
+ continue
+ }
+ s.emitShadowReadMetric(shadowOutcomeValidation, 1)
+ s.logger.Warn(
+ "shadow consolidated metadata is invalid; falling back to single-block",
+ zap.Uint32("tag", block.GetTag()),
+ zap.Uint64("height", block.GetHeight()),
+ zap.String("hash", block.GetHash()),
+ zap.String("shadow_object_key", shadow.GetObjectKeyMain()),
+ zap.Int32("object_format", int32(shadow.GetObjectFormat())),
+ zap.Uint64("byte_length", shadow.GetByteLength()),
+ )
+ selected[i] = block
+ fallbackCount++
+ if selectionErr == nil {
+ selectionErr = status.Errorf(codes.FailedPrecondition, "invalid consolidated block metadata for height %d", block.GetHeight())
+ }
+ }
+ return selected, shadowCount, fallbackCount, selectionErr
+}
+
+func (s *Server) downloadBlockWithShadowMetrics(ctx context.Context, block *api.BlockMetadata, path string) (*api.Block, error) {
+ start := time.Now()
output, err := s.blobStorage.Download(ctx, block)
+ s.emitShadowReadLatency(path, time.Since(start))
if err != nil {
return nil, xerrors.Errorf("failed to download from blob storage (input={%+v}): %w", block, err)
}
+ s.emitShadowReadLogicalBytes(path, blockReadLogicalBytes(block, output))
+ return output, nil
+}
+func (s *Server) downloadBlocksWithShadowMetrics(ctx context.Context, blocks []*api.BlockMetadata) ([]*api.Block, error) {
+ start := time.Now()
+ output, err := s.blobStorage.DownloadMany(ctx, blocks)
+ for _, block := range blocks {
+ path := shadowPathSingleBlock
+ if isConsolidatedBlockMetadata(block) {
+ path = shadowPathConsolidated
+ }
+ s.emitShadowReadLatency(path, time.Since(start))
+ }
+ if err != nil {
+ return nil, xerrors.Errorf("failed to download blocks from blob storage: %w", err)
+ }
+ for i, block := range blocks {
+ path := shadowPathSingleBlock
+ if isConsolidatedBlockMetadata(block) {
+ path = shadowPathConsolidated
+ }
+ var downloaded *api.Block
+ if i < len(output) {
+ downloaded = output[i]
+ }
+ s.emitShadowReadLogicalBytes(path, blockReadLogicalBytes(block, downloaded))
+ }
return output, nil
}
-func (s *Server) getBlocksFromBlobStorage(ctx context.Context, blocks []*api.BlockMetadata) ([]*api.Block, error) {
- result := make([]*api.Block, len(blocks))
- group, ctx := syncgroup.New(ctx, syncgroup.WithThrottling(int(s.config.Api.NumWorkers)))
- for i := range blocks {
- i := i
- group.Go(func() error {
- input := blocks[i]
- output, err := s.blobStorage.Download(ctx, input)
- if err != nil {
- return xerrors.Errorf("failed to download from blob storage (input={%+v}): %w", input, err)
- }
+func blockReadLogicalBytes(metadata *api.BlockMetadata, block *api.Block) int64 {
+ if isConsolidatedBlockMetadata(metadata) {
+ return int64(metadata.GetByteLength())
+ }
+ if block != nil {
+ return int64(proto.Size(block))
+ }
+ return 0
+}
- result[i] = output
- return nil
- })
+func blockWithPrimaryMetadata(block *api.Block, metadata *api.BlockMetadata) *api.Block {
+ if block != nil {
+ block.Metadata = metadata
}
+ return block
+}
- if err := group.Wait(); err != nil {
- return nil, xerrors.Errorf("failed to download blocks from blob storage: %w", err)
+func blocksWithPrimaryMetadata(blocks []*api.Block, metadatas []*api.BlockMetadata) []*api.Block {
+ for i, block := range blocks {
+ if i >= len(metadatas) {
+ break
+ }
+ blockWithPrimaryMetadata(block, metadatas[i])
}
+ return blocks
+}
+
+func isConsolidatedBlockMetadata(metadata *api.BlockMetadata) bool {
+ return metadata.GetObjectFormat() == api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH && metadata.GetByteLength() > 0
+}
+
+func allConsolidatedBlockMetadata(metadatas []*api.BlockMetadata) bool {
+ if len(metadatas) == 0 {
+ return false
+ }
+ for _, metadata := range metadatas {
+ if !isConsolidatedBlockMetadata(metadata) {
+ return false
+ }
+ }
+ return true
+}
+
+func shadowReadErrorOutcome(err error) string {
+ if isShadowValidationError(err) {
+ return shadowOutcomeValidation
+ }
+ return shadowOutcomeError
+}
+
+func isShadowValidationError(err error) bool {
+ if err == nil {
+ return false
+ }
+ for current := err; current != nil; current = errors.Unwrap(current) {
+ if isCSCBValidationMessage(current.Error()) {
+ return true
+ }
+ }
+ return false
+}
- return result, nil
+func isCSCBValidationMessage(errText string) bool {
+ return strings.Contains(errText, "invalid CSCB") ||
+ strings.Contains(errText, "unsupported CSCB") ||
+ strings.Contains(errText, "unexpected CSCB") ||
+ strings.Contains(errText, "duplicate CSCB") ||
+ strings.Contains(errText, "failed to decompress CSCB chunk") ||
+ strings.Contains(errText, "CSCB header") ||
+ strings.Contains(errText, "CSCB index") ||
+ strings.Contains(errText, "CSCB envelope") ||
+ strings.Contains(errText, "CSCB block") ||
+ strings.Contains(errText, "CSCB chunk")
}
func (s *Server) newAuthContext(ctx context.Context) context.Context {
diff --git a/internal/server/handler_test.go b/internal/server/handler_test.go
index 4b0c0b0e..aa47011d 100644
--- a/internal/server/handler_test.go
+++ b/internal/server/handler_test.go
@@ -10,18 +10,17 @@ import (
"testing"
"time"
- "github.com/aws/aws-sdk-go/aws"
- awsClient "github.com/aws/aws-sdk-go/aws/client"
- "github.com/aws/aws-sdk-go/awstesting"
- "github.com/aws/aws-sdk-go/awstesting/unit"
geth "github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/stretchr/testify/suite"
+ "github.com/uber-go/tally/v4"
"go.uber.org/fx"
"go.uber.org/mock/gomock"
"golang.org/x/xerrors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/timestamppb"
"github.com/coinbase/chainstorage/internal/blockchain/client"
clientmocks "github.com/coinbase/chainstorage/internal/blockchain/client/mocks"
@@ -80,7 +79,6 @@ type handlerTestSuite struct {
suite.Suite
ctrl *gomock.Controller
app testapp.TestApp
- awsClient *awsClient.Client
metaStorage *metastoragemocks.MockMetaStorage
blobStorage *blobstoragemocks.MockBlobStorage
transactionStorage *metastoragemocks.MockTransactionStorage
@@ -99,9 +97,6 @@ func TestHandlerSuite(t *testing.T) {
func (s *handlerTestSuite) SetupTest() {
s.ctrl = gomock.NewController(s.T())
- s.awsClient = awstesting.NewClient(&aws.Config{
- Region: unit.Session.Config.Region,
- })
s.metaStorage = metastoragemocks.NewMockMetaStorage(s.ctrl)
s.blobStorage = blobstoragemocks.NewMockBlobStorage(s.ctrl)
s.transactionStorage = metastoragemocks.NewMockTransactionStorage(s.ctrl)
@@ -131,6 +126,84 @@ func (s *handlerTestSuite) SetupTest() {
s.config.Chain.Feature.VerifiedAccountStateEnabled = true
}
+func makeShadowBlockMetadata(block *api.BlockMetadata) *api.BlockMetadata {
+ shadow := proto.Clone(block).(*api.BlockMetadata)
+ shadow.ObjectKeyMain = fmt.Sprintf("consolidated/v=1/shard=000000000000-000000010000/%012d-%012d-deadbeef.cscb.zstd", block.Height, block.Height)
+ shadow.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH
+ shadow.ByteOffset = 4096
+ shadow.ByteLength = 8192
+ shadow.UncompressedLength = 16384
+ return shadow
+}
+
+func TestIsShadowValidationError(t *testing.T) {
+ tests := []struct {
+ name string
+ err error
+ want bool
+ }{
+ {
+ name: "block offset mismatch",
+ err: xerrors.New("CSCB block offset mismatch at height 100: index=1 metadata=2"),
+ want: true,
+ },
+ {
+ name: "unsupported header",
+ err: xerrors.New("unsupported CSCB format version: 99"),
+ want: true,
+ },
+ {
+ name: "chunk length mismatch",
+ err: xerrors.New("CSCB chunk length mismatch: got 1 want 2"),
+ want: true,
+ },
+ {
+ name: "block payload bounds",
+ err: xerrors.New("CSCB block payload exceeds chunk bounds: end=10 chunk_length=5"),
+ want: true,
+ },
+ {
+ name: "block index envelope bounds",
+ err: xerrors.New("CSCB block index record 3 exceeds envelope length"),
+ want: true,
+ },
+ {
+ name: "wrapped cscb object timeout",
+ err: xerrors.Errorf("failed to download CSCB object (key=consolidated): %w", xerrors.New("s3 timeout")),
+ want: false,
+ },
+ {
+ name: "wrapped metadata context timeout",
+ err: xerrors.Errorf("failed to download from blob storage (input={ObjectFormat:BLOCK_OBJECT_FORMAT_CSCB_BATCH}): %w", xerrors.New("s3 timeout")),
+ want: false,
+ },
+ {
+ name: "hash mismatch",
+ err: xerrors.New("CSCB block hash mismatch at height 100"),
+ want: true,
+ },
+ {
+ name: "generic storage error",
+ err: xerrors.New("s3 timeout"),
+ want: false,
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ require := testutil.Require(t)
+ require.Equal(test.want, isShadowValidationError(test.err))
+ })
+ }
+}
+
+func TestValidateBlockReadSource(t *testing.T) {
+ require := testutil.Require(t)
+ require.NoError(validateBlockReadSource(api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT))
+ require.NoError(validateBlockReadSource(api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK))
+ require.NoError(validateBlockReadSource(api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED))
+ require.Error(validateBlockReadSource(api.BlockReadSource(99)))
+}
+
func (s *handlerTestSuite) TearDownTest() {
s.app.Close()
s.ctrl.Finish()
@@ -219,11 +292,15 @@ func (s *handlerTestSuite) TestGetBlockFile() {
require := testutil.Require(s.T())
tag := s.app.Config().Chain.BlockTag.Stable
expected := &api.BlockFile{
- Tag: tag,
- Hash: hash,
- ParentHash: parentHash,
- Height: height,
- FileUrl: "http://endpoint/foo/bar",
+ Tag: tag,
+ Hash: hash,
+ ParentHash: parentHash,
+ Height: height,
+ FileUrl: "http://endpoint/foo/bar",
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 4096,
+ ByteLength: 8192,
+ UncompressedLength: 8192,
}
gomock.InOrder(
s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
@@ -232,11 +309,15 @@ func (s *handlerTestSuite) TestGetBlockFile() {
require.Equal(height, height_)
require.Equal(hash, hash_)
return &api.BlockMetadata{
- Tag: tag,
- Hash: hash,
- ParentHash: parentHash,
- Height: height,
- ObjectKeyMain: objectKeyMain,
+ Tag: tag,
+ Hash: hash,
+ ParentHash: parentHash,
+ Height: height,
+ ObjectKeyMain: objectKeyMain,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 4096,
+ ByteLength: 8192,
+ UncompressedLength: 8192,
}, nil
},
),
@@ -257,6 +338,208 @@ func (s *handlerTestSuite) TestGetBlockFile() {
require.Equal(expected, resp.File)
}
+func (s *handlerTestSuite) TestGetBlockFile_DefaultUsesReadShadowFirst() {
+ const (
+ height uint64 = 13193825
+ hash = "0xda5a0439434adf072394e0b94f78e56032c5409a2c58668995f306b171ff4ace"
+ parentHash = "0xba6a6c85739b50384625e10718524fb2c1fcf88858eabf6db9bd851902b53546"
+ objectKeyMain = "foo/bar"
+ )
+
+ require := testutil.Require(s.T())
+ s.server.config.AWS.Storage.Consolidation.ReadShadowFirst = true
+ tag := s.app.Config().Chain.BlockTag.Stable
+ blockMetadata := &api.BlockMetadata{
+ Tag: tag,
+ Hash: hash,
+ ParentHash: parentHash,
+ Height: height,
+ ObjectKeyMain: objectKeyMain,
+ }
+ shadowMetadata := makeShadowBlockMetadata(blockMetadata)
+ expected := &api.BlockFile{
+ Tag: tag,
+ Hash: hash,
+ ParentHash: parentHash,
+ Height: height,
+ FileUrl: "http://endpoint/consolidated",
+ Compression: api.Compression_ZSTD,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: shadowMetadata.GetByteOffset(),
+ ByteLength: shadowMetadata.GetByteLength(),
+ UncompressedLength: shadowMetadata.GetUncompressedLength(),
+ }
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), tag, height, hash).Times(1).Return(blockMetadata, nil),
+ s.metaStorage.EXPECT().GetBlockConsolidationShadow(gomock.Any(), blockMetadata).Times(1).Return(shadowMetadata, nil),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), shadowMetadata.GetObjectKeyMain()).Times(1).Return("http://endpoint/consolidated", nil),
+ )
+
+ resp, err := s.server.GetBlockFile(context.Background(), &api.GetBlockFileRequest{
+ Height: height,
+ Hash: hash,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(expected, resp.File)
+}
+
+func (s *handlerTestSuite) TestGetBlockFile_ReadSourceSingleBlockIgnoresReadShadowFirst() {
+ const (
+ height uint64 = 13193825
+ hash = "0xda5a0439434adf072394e0b94f78e56032c5409a2c58668995f306b171ff4ace"
+ parentHash = "0xba6a6c85739b50384625e10718524fb2c1fcf88858eabf6db9bd851902b53546"
+ objectKeyMain = "foo/bar"
+ )
+
+ require := testutil.Require(s.T())
+ s.server.config.AWS.Storage.Consolidation.ReadShadowFirst = true
+ tag := s.app.Config().Chain.BlockTag.Stable
+ expected := &api.BlockFile{
+ Tag: tag,
+ Hash: hash,
+ ParentHash: parentHash,
+ Height: height,
+ FileUrl: "http://endpoint/foo/bar",
+ }
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), tag, height, hash).Times(1).Return(&api.BlockMetadata{
+ Tag: tag,
+ Hash: hash,
+ ParentHash: parentHash,
+ Height: height,
+ ObjectKeyMain: objectKeyMain,
+ }, nil),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), objectKeyMain).Times(1).Return("http://endpoint/foo/bar", nil),
+ )
+
+ resp, err := s.server.GetBlockFile(context.Background(), &api.GetBlockFileRequest{
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(expected, resp.File)
+}
+
+func (s *handlerTestSuite) TestGetBlockFile_ReadSourceConsolidatedUsesShadowMetadata() {
+ const (
+ height uint64 = 13193825
+ hash = "0xda5a0439434adf072394e0b94f78e56032c5409a2c58668995f306b171ff4ace"
+ parentHash = "0xba6a6c85739b50384625e10718524fb2c1fcf88858eabf6db9bd851902b53546"
+ objectKeyMain = "foo/bar"
+ )
+
+ require := testutil.Require(s.T())
+ tag := s.app.Config().Chain.BlockTag.Stable
+ blockMetadata := &api.BlockMetadata{
+ Tag: tag,
+ Hash: hash,
+ ParentHash: parentHash,
+ Height: height,
+ ObjectKeyMain: objectKeyMain,
+ }
+ shadowMetadata := makeShadowBlockMetadata(blockMetadata)
+ expected := &api.BlockFile{
+ Tag: tag,
+ Hash: hash,
+ ParentHash: parentHash,
+ Height: height,
+ FileUrl: "http://endpoint/consolidated",
+ Compression: api.Compression_ZSTD,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: shadowMetadata.GetByteOffset(),
+ ByteLength: shadowMetadata.GetByteLength(),
+ UncompressedLength: shadowMetadata.GetUncompressedLength(),
+ }
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), tag, height, hash).Times(1).Return(blockMetadata, nil),
+ s.metaStorage.EXPECT().GetBlockConsolidationShadow(gomock.Any(), blockMetadata).Times(1).Return(shadowMetadata, nil),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), shadowMetadata.GetObjectKeyMain()).Times(1).Return("http://endpoint/consolidated", nil),
+ )
+
+ resp, err := s.server.GetBlockFile(context.Background(), &api.GetBlockFileRequest{
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(expected, resp.File)
+}
+
+func (s *handlerTestSuite) TestGetBlockFile_ReadSourceConsolidatedFallsBackOnPresignError() {
+ const (
+ height uint64 = 13193825
+ hash = "0xda5a0439434adf072394e0b94f78e56032c5409a2c58668995f306b171ff4ace"
+ parentHash = "0xba6a6c85739b50384625e10718524fb2c1fcf88858eabf6db9bd851902b53546"
+ objectKeyMain = "foo/bar"
+ )
+
+ require := testutil.Require(s.T())
+ tag := s.app.Config().Chain.BlockTag.Stable
+ blockMetadata := &api.BlockMetadata{
+ Tag: tag,
+ Hash: hash,
+ ParentHash: parentHash,
+ Height: height,
+ ObjectKeyMain: objectKeyMain,
+ }
+ shadowMetadata := makeShadowBlockMetadata(blockMetadata)
+ expected := &api.BlockFile{
+ Tag: tag,
+ Hash: hash,
+ ParentHash: parentHash,
+ Height: height,
+ FileUrl: "http://endpoint/single-block",
+ }
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), tag, height, hash).Times(1).Return(blockMetadata, nil),
+ s.metaStorage.EXPECT().GetBlockConsolidationShadow(gomock.Any(), blockMetadata).Times(1).Return(shadowMetadata, nil),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), shadowMetadata.GetObjectKeyMain()).Times(1).Return("", xerrors.New("presign failed")),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), objectKeyMain).Times(1).Return("http://endpoint/single-block", nil),
+ )
+
+ resp, err := s.server.GetBlockFile(context.Background(), &api.GetBlockFileRequest{
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(expected, resp.File)
+}
+
+func (s *handlerTestSuite) TestGetBlockFile_PromotedConsolidatedPresignErrorDoesNotFallbackToSingleBlock() {
+ const (
+ height uint64 = 13193825
+ )
+
+ require := testutil.Require(s.T())
+ tag := s.app.Config().Chain.BlockTag.Stable
+ singleBlockMetadata := testutil.MakeBlockMetadatasFromStartHeight(height, 1, tag)[0]
+ promotedMetadata := makeShadowBlockMetadata(singleBlockMetadata)
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), tag, promotedMetadata.GetHeight(), promotedMetadata.GetHash()).Times(1).Return(promotedMetadata, nil),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), promotedMetadata.GetObjectKeyMain()).Times(1).Return("", xerrors.New("presign failed")),
+ )
+
+ resp, err := s.server.GetBlockFile(context.Background(), &api.GetBlockFileRequest{
+ Height: promotedMetadata.GetHeight(),
+ Hash: promotedMetadata.GetHash(),
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.Error(err)
+ require.Nil(resp)
+ require.Contains(err.Error(), "failed to prepare consolidated block file")
+}
+
func (s *handlerTestSuite) TestGetBlockFile_Gzip() {
const (
height uint64 = 13193825
@@ -504,6 +787,197 @@ func (s *handlerTestSuite) TestGetBlockFilesByRange() {
require.NoError(err)
}
+func (s *handlerTestSuite) TestGetBlockFilesByRange_ReusesPresignedURLForSameObject() {
+ require := testutil.Require(s.T())
+ stableTag := s.app.Config().Chain.BlockTag.Stable
+ const objectKey = "BLOCKCHAIN_ETHEREUM/NETWORK_ETHEREUM_MAINNET/consolidated/v=0/shard=000000000000-000000010000/000000009000-000000009002-sha.cscb.zstd"
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+ func(ctx context.Context, tag uint32, startHeight, endHeight uint64) ([]*api.BlockMetadata, error) {
+ require.Equal(stableTag, tag)
+ require.Equal(uint64(9000), startHeight)
+ require.Equal(uint64(9002), endHeight)
+ return []*api.BlockMetadata{
+ {
+ Hash: "hash1",
+ ParentHash: "hash0",
+ Height: 9000,
+ ParentHeight: 8999,
+ ObjectKeyMain: objectKey,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 0,
+ ByteLength: 100,
+ UncompressedLength: 100,
+ },
+ {
+ Hash: "hash2",
+ ParentHash: "hash1",
+ Height: 9001,
+ ParentHeight: 9000,
+ ObjectKeyMain: objectKey,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 100,
+ ByteLength: 120,
+ UncompressedLength: 120,
+ },
+ }, nil
+ },
+ ),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+ func(ctx context.Context, tag uint32) (*api.BlockMetadata, error) {
+ require.Equal(stableTag, tag)
+ return &api.BlockMetadata{
+ Tag: 0,
+ Hash: "hash3",
+ ParentHash: "hash2",
+ Height: 100000,
+ ObjectKeyMain: "key",
+ }, nil
+ },
+ ),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), objectKey).Times(1).Return("http://endpoint/consolidated", nil),
+ )
+
+ resp, err := s.server.GetBlockFilesByRange(context.Background(), &api.GetBlockFilesByRangeRequest{
+ StartHeight: 9000,
+ EndHeight: 9002,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Len(resp.GetFiles(), 2)
+ require.Equal("http://endpoint/consolidated", resp.GetFiles()[0].GetFileUrl())
+ require.Equal(resp.GetFiles()[0].GetFileUrl(), resp.GetFiles()[1].GetFileUrl())
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, resp.GetFiles()[0].GetObjectFormat())
+ require.Equal(uint64(100), resp.GetFiles()[1].GetByteOffset())
+ require.Equal(uint64(120), resp.GetFiles()[1].GetByteLength())
+}
+
+func (s *handlerTestSuite) TestGetBlockFilesByRange_ReadSourceConsolidatedUsesShadowMetadata() {
+ require := testutil.Require(s.T())
+ stableTag := s.app.Config().Chain.BlockTag.Stable
+ const (
+ startHeight uint64 = 9000
+ endHeight uint64 = 9002
+ objectKey = "consolidated/v=1/shard=000000000000-000000010000/000000009000-000000009002-deadbeef.cscb.zstd"
+ )
+
+ blockMetadatas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, int(endHeight-startHeight), stableTag)
+ shadowMetadatas := make([]*api.BlockMetadata, len(blockMetadatas))
+ for i, blockMetadata := range blockMetadatas {
+ shadowMetadatas[i] = makeShadowBlockMetadata(blockMetadata)
+ shadowMetadatas[i].ObjectKeyMain = objectKey
+ shadowMetadatas[i].ByteOffset = uint64(i * 100)
+ shadowMetadatas[i].ByteLength = 100
+ shadowMetadatas[i].UncompressedLength = 200
+ }
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), stableTag, startHeight, endHeight).Times(1).Return(blockMetadatas, nil),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), stableTag).Times(1).Return(testutil.MakeBlockMetadata(10000, stableTag), nil),
+ s.metaStorage.EXPECT().GetBlocksConsolidationShadow(gomock.Any(), blockMetadatas).Times(1).Return(shadowMetadatas, nil),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), objectKey).Times(1).Return("http://endpoint/consolidated", nil),
+ )
+
+ resp, err := s.server.GetBlockFilesByRange(context.Background(), &api.GetBlockFilesByRangeRequest{
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Len(resp.GetFiles(), len(blockMetadatas))
+ require.Equal("http://endpoint/consolidated", resp.GetFiles()[0].GetFileUrl())
+ require.Equal(resp.GetFiles()[0].GetFileUrl(), resp.GetFiles()[1].GetFileUrl())
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, resp.GetFiles()[0].GetObjectFormat())
+ require.Equal(uint64(100), resp.GetFiles()[1].GetByteOffset())
+ require.Equal(uint64(100), resp.GetFiles()[1].GetByteLength())
+ require.Equal(uint64(200), resp.GetFiles()[1].GetUncompressedLength())
+}
+
+func (s *handlerTestSuite) TestGetBlockFilesByRange_DefaultUsesReadShadowFirst() {
+ require := testutil.Require(s.T())
+ s.server.config.AWS.Storage.Consolidation.ReadShadowFirst = true
+ stableTag := s.app.Config().Chain.BlockTag.Stable
+ const (
+ startHeight uint64 = 9000
+ endHeight uint64 = 9002
+ objectKey = "consolidated/v=1/shard=000000000000-000000010000/000000009000-000000009002-deadbeef.cscb.zstd"
+ )
+
+ blockMetadatas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, int(endHeight-startHeight), stableTag)
+ shadowMetadatas := make([]*api.BlockMetadata, len(blockMetadatas))
+ for i, blockMetadata := range blockMetadatas {
+ shadowMetadatas[i] = makeShadowBlockMetadata(blockMetadata)
+ shadowMetadatas[i].ObjectKeyMain = objectKey
+ shadowMetadatas[i].ByteOffset = uint64(i * 100)
+ shadowMetadatas[i].ByteLength = 100
+ shadowMetadatas[i].UncompressedLength = 200
+ }
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), stableTag, startHeight, endHeight).Times(1).Return(blockMetadatas, nil),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), stableTag).Times(1).Return(testutil.MakeBlockMetadata(10000, stableTag), nil),
+ s.metaStorage.EXPECT().GetBlocksConsolidationShadow(gomock.Any(), blockMetadatas).Times(1).Return(shadowMetadatas, nil),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), objectKey).Times(1).Return("http://endpoint/consolidated", nil),
+ )
+
+ resp, err := s.server.GetBlockFilesByRange(context.Background(), &api.GetBlockFilesByRangeRequest{
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Len(resp.GetFiles(), len(blockMetadatas))
+ require.Equal("http://endpoint/consolidated", resp.GetFiles()[0].GetFileUrl())
+ require.Equal(resp.GetFiles()[0].GetFileUrl(), resp.GetFiles()[1].GetFileUrl())
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, resp.GetFiles()[0].GetObjectFormat())
+ require.Equal(uint64(100), resp.GetFiles()[1].GetByteOffset())
+ require.Equal(uint64(100), resp.GetFiles()[1].GetByteLength())
+ require.Equal(uint64(200), resp.GetFiles()[1].GetUncompressedLength())
+}
+
+func (s *handlerTestSuite) TestGetBlockFilesByRange_ReadSourceConsolidatedFallsBackOnPresignError() {
+ require := testutil.Require(s.T())
+ stableTag := s.app.Config().Chain.BlockTag.Stable
+ const (
+ startHeight uint64 = 9000
+ endHeight uint64 = 9002
+ objectKey = "consolidated/v=1/shard=000000000000-000000010000/000000009000-000000009002-deadbeef.cscb.zstd"
+ )
+
+ blockMetadatas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, int(endHeight-startHeight), stableTag)
+ shadowMetadatas := make([]*api.BlockMetadata, len(blockMetadatas))
+ for i, blockMetadata := range blockMetadatas {
+ shadowMetadatas[i] = makeShadowBlockMetadata(blockMetadata)
+ shadowMetadatas[i].ObjectKeyMain = objectKey
+ }
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), stableTag, startHeight, endHeight).Times(1).Return(blockMetadatas, nil),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), stableTag).Times(1).Return(testutil.MakeBlockMetadata(10000, stableTag), nil),
+ s.metaStorage.EXPECT().GetBlocksConsolidationShadow(gomock.Any(), blockMetadatas).Times(1).Return(shadowMetadatas, nil),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), objectKey).Times(1).Return("", xerrors.New("presign failed")),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), blockMetadatas[0].GetObjectKeyMain()).Times(1).Return("http://endpoint/single-block/0", nil),
+ s.blobStorage.EXPECT().PreSign(gomock.Any(), blockMetadatas[1].GetObjectKeyMain()).Times(1).Return("http://endpoint/single-block/1", nil),
+ )
+
+ resp, err := s.server.GetBlockFilesByRange(context.Background(), &api.GetBlockFilesByRangeRequest{
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Len(resp.GetFiles(), len(blockMetadatas))
+ require.Equal("http://endpoint/single-block/0", resp.GetFiles()[0].GetFileUrl())
+ require.Equal("http://endpoint/single-block/1", resp.GetFiles()[1].GetFileUrl())
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK, resp.GetFiles()[0].GetObjectFormat())
+ require.Zero(resp.GetFiles()[0].GetByteOffset())
+ require.Zero(resp.GetFiles()[0].GetByteLength())
+ require.Zero(resp.GetFiles()[0].GetUncompressedLength())
+}
+
func (s *handlerTestSuite) TestGetBlockFilesByRange_ExceededLatest() {
require := testutil.Require(s.T())
latest := uint64(99)
@@ -602,12 +1076,262 @@ func (s *handlerTestSuite) TestGetRawBlock_Gzip() {
)
resp, err := s.server.GetRawBlock(context.Background(), &api.GetRawBlockRequest{
- Height: blockMetadata.Height,
- Hash: blockMetadata.Hash,
+ Height: blockMetadata.Height,
+ Hash: blockMetadata.Hash,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(block, resp.Block)
+}
+
+func (s *handlerTestSuite) TestGetRawBlock_InvalidReadSource() {
+ require := testutil.Require(s.T())
+ resp, err := s.server.GetRawBlock(context.Background(), &api.GetRawBlockRequest{
+ Height: 1,
+ ReadSource: api.BlockReadSource(99),
+ })
+ require.Nil(resp)
+ s.verifyStatusCode(codes.InvalidArgument, err)
+}
+
+func (s *handlerTestSuite) TestGetRawBlock_DefaultUsesReadShadowFirst() {
+ const (
+ height uint64 = 13193825
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+ s.server.config.AWS.Storage.Consolidation.ReadShadowFirst = true
+
+ tag := s.app.Config().Chain.BlockTag.Stable
+ blockMetadata := testutil.MakeBlockMetadatasFromStartHeight(height, 1, tag)[0]
+ shadowMetadata := makeShadowBlockMetadata(blockMetadata)
+ shadowBlock := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+ shadowBlock.Metadata = shadowMetadata
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+ func(ctx context.Context, tag uint32, height uint64, hash string) (*api.BlockMetadata, error) {
+ require.Equal(blockMetadata.Tag, tag)
+ require.Equal(blockMetadata.Height, height)
+ require.Equal(blockMetadata.Hash, hash)
+ return blockMetadata, nil
+ },
+ ),
+ s.metaStorage.EXPECT().GetBlockConsolidationShadow(gomock.Any(), blockMetadata).Times(1).Return(shadowMetadata, nil),
+ s.blobStorage.EXPECT().Download(gomock.Any(), shadowMetadata).Times(1).Return(shadowBlock, nil),
+ )
+
+ resp, err := s.server.GetRawBlock(context.Background(), &api.GetRawBlockRequest{
+ Height: blockMetadata.Height,
+ Hash: blockMetadata.Hash,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(blockMetadata, resp.Block.GetMetadata())
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=success"].Value())
+ require.Equal(int64(shadowMetadata.ByteLength), snapshot.Counters()["server.shadow_read_logical_bytes+path=consolidated"].Value())
+}
+
+func (s *handlerTestSuite) TestGetRawBlock_ReadSourceConsolidatedUsesShadowWhenConfigDisabled() {
+ const (
+ height uint64 = 13193825
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().Chain.BlockTag.Stable
+ blockMetadata := testutil.MakeBlockMetadatasFromStartHeight(height, 1, tag)[0]
+ shadowMetadata := makeShadowBlockMetadata(blockMetadata)
+ shadowBlock := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+ shadowBlock.Metadata = shadowMetadata
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(blockMetadata, nil),
+ s.metaStorage.EXPECT().GetBlockConsolidationShadow(gomock.Any(), blockMetadata).Times(1).Return(shadowMetadata, nil),
+ s.blobStorage.EXPECT().Download(gomock.Any(), shadowMetadata).Times(1).Return(shadowBlock, nil),
+ )
+
+ resp, err := s.server.GetRawBlock(context.Background(), &api.GetRawBlockRequest{
+ Height: blockMetadata.Height,
+ Hash: blockMetadata.Hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(blockMetadata, resp.Block.GetMetadata())
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=success"].Value())
+ require.Equal(int64(shadowMetadata.ByteLength), snapshot.Counters()["server.shadow_read_logical_bytes+path=consolidated"].Value())
+}
+
+func (s *handlerTestSuite) TestGetRawBlock_ReadSourceSingleBlockOverridesConfig() {
+ const (
+ height uint64 = 13193825
+ )
+
+ require := testutil.Require(s.T())
+ s.server.config.AWS.Storage.Consolidation.ReadShadowFirst = true
+
+ tag := s.app.Config().Chain.BlockTag.Stable
+ blockMetadata := testutil.MakeBlockMetadatasFromStartHeight(height, 1, tag)[0]
+ block := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(blockMetadata, nil),
+ s.blobStorage.EXPECT().Download(gomock.Any(), blockMetadata).Times(1).Return(block, nil),
+ )
+
+ resp, err := s.server.GetRawBlock(context.Background(), &api.GetRawBlockRequest{
+ Height: blockMetadata.Height,
+ Hash: blockMetadata.Hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(block, resp.Block)
+}
+
+func (s *handlerTestSuite) TestGetRawBlock_ReadSourceConsolidatedFallsBackOnShadowDownloadError() {
+ const (
+ height uint64 = 13193825
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().Chain.BlockTag.Stable
+ blockMetadata := testutil.MakeBlockMetadatasFromStartHeight(height, 1, tag)[0]
+ shadowMetadata := makeShadowBlockMetadata(blockMetadata)
+ block := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(blockMetadata, nil),
+ s.metaStorage.EXPECT().GetBlockConsolidationShadow(gomock.Any(), blockMetadata).Times(1).Return(shadowMetadata, nil),
+ s.blobStorage.EXPECT().Download(gomock.Any(), shadowMetadata).Times(1).Return(nil, xerrors.New("s3 timeout")),
+ s.blobStorage.EXPECT().Download(gomock.Any(), blockMetadata).Times(1).Return(block, nil),
+ )
+
+ resp, err := s.server.GetRawBlock(context.Background(), &api.GetRawBlockRequest{
+ Height: blockMetadata.Height,
+ Hash: blockMetadata.Hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(block, resp.Block)
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=error"].Value())
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=fallback_success"].Value())
+}
+
+func (s *handlerTestSuite) TestGetRawBlock_PromotedConsolidatedDownloadErrorDoesNotFallbackToSingleBlock() {
+ const (
+ height uint64 = 13193825
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().Chain.BlockTag.Stable
+ singleBlockMetadata := testutil.MakeBlockMetadatasFromStartHeight(height, 1, tag)[0]
+ promotedMetadata := makeShadowBlockMetadata(singleBlockMetadata)
+ downloadErr := xerrors.New("consolidated object missing")
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), tag, promotedMetadata.GetHeight(), promotedMetadata.GetHash()).Times(1).Return(promotedMetadata, nil),
+ s.blobStorage.EXPECT().Download(gomock.Any(), promotedMetadata).Times(1).Return(nil, downloadErr),
+ )
+
+ resp, err := s.server.GetRawBlock(context.Background(), &api.GetRawBlockRequest{
+ Height: promotedMetadata.GetHeight(),
+ Hash: promotedMetadata.GetHash(),
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.Error(err)
+ require.Nil(resp)
+ require.Contains(err.Error(), downloadErr.Error())
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=error"].Value())
+ require.NotContains(snapshot.Counters(), "server.shadow_read+outcome=fallback_success")
+}
+
+func (s *handlerTestSuite) TestGetRawBlock_ReadSourceConsolidatedFallsBackOnMiss() {
+ const (
+ height uint64 = 13193825
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().Chain.BlockTag.Stable
+ blockMetadata := testutil.MakeBlockMetadatasFromStartHeight(height, 1, tag)[0]
+ block := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(blockMetadata, nil),
+ s.metaStorage.EXPECT().GetBlockConsolidationShadow(gomock.Any(), blockMetadata).Times(1).Return(nil, storage.ErrItemNotFound),
+ s.blobStorage.EXPECT().Download(gomock.Any(), blockMetadata).Times(1).Return(block, nil),
+ )
+
+ resp, err := s.server.GetRawBlock(context.Background(), &api.GetRawBlockRequest{
+ Height: blockMetadata.Height,
+ Hash: blockMetadata.Hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(block, resp.Block)
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=miss"].Value())
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=fallback_success"].Value())
+}
+
+func (s *handlerTestSuite) TestGetRawBlock_ReadSourceConsolidatedFallsBackOnValidationError() {
+ const (
+ height uint64 = 13193825
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().Chain.BlockTag.Stable
+ blockMetadata := testutil.MakeBlockMetadatasFromStartHeight(height, 1, tag)[0]
+ shadowMetadata := makeShadowBlockMetadata(blockMetadata)
+ block := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1).Return(blockMetadata, nil),
+ s.metaStorage.EXPECT().GetBlockConsolidationShadow(gomock.Any(), blockMetadata).Times(1).Return(shadowMetadata, nil),
+ s.blobStorage.EXPECT().Download(gomock.Any(), shadowMetadata).Times(1).Return(nil, xerrors.New("CSCB block hash mismatch at height 13193825")),
+ s.blobStorage.EXPECT().Download(gomock.Any(), blockMetadata).Times(1).Return(block, nil),
+ )
+
+ resp, err := s.server.GetRawBlock(context.Background(), &api.GetRawBlockRequest{
+ Height: blockMetadata.Height,
+ Hash: blockMetadata.Hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
})
require.NoError(err)
require.NotNil(resp)
require.Equal(block, resp.Block)
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=validation_mismatch"].Value())
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=fallback_success"].Value())
}
func (s *handlerTestSuite) TestGetRawBlocksByRange_StableTag() {
@@ -641,11 +1365,16 @@ func (s *handlerTestSuite) TestGetRawBlocksByRange_StableTag() {
return testutil.MakeBlockMetadata(10000, tag), nil
},
),
- s.blobStorage.EXPECT().Download(gomock.Any(), gomock.Any()).Times(numBlocks).DoAndReturn(
- func(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
- block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
- require.True(ok)
- return block, nil
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+ func(ctx context.Context, metadatas []*api.BlockMetadata) ([]*api.Block, error) {
+ require.Len(metadatas, numBlocks)
+ output := make([]*api.Block, len(metadatas))
+ for i, metadata := range metadatas {
+ block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
+ require.True(ok)
+ output[i] = block
+ }
+ return output, nil
},
),
)
@@ -693,11 +1422,16 @@ func (s *handlerTestSuite) TestGetRawBlocksByRange_LatestTag() {
return testutil.MakeBlockMetadata(10000, tag), nil
},
),
- s.blobStorage.EXPECT().Download(gomock.Any(), gomock.Any()).Times(numBlocks).DoAndReturn(
- func(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
- block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
- require.True(ok)
- return block, nil
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+ func(ctx context.Context, metadatas []*api.BlockMetadata) ([]*api.Block, error) {
+ require.Len(metadatas, numBlocks)
+ output := make([]*api.Block, len(metadatas))
+ for i, metadata := range metadatas {
+ block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
+ require.True(ok)
+ output[i] = block
+ }
+ return output, nil
},
),
)
@@ -747,14 +1481,18 @@ func (s *handlerTestSuite) TestGetRawBlocksByRange_DownloadError() {
return testutil.MakeBlockMetadata(10000, tag), nil
},
),
- s.blobStorage.EXPECT().Download(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(
- func(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
- block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
- require.True(ok)
- if block.Metadata.Height == failedHeight {
- return nil, fmt.Errorf("mock download error")
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+ func(ctx context.Context, metadatas []*api.BlockMetadata) ([]*api.Block, error) {
+ output := make([]*api.Block, len(metadatas))
+ for i, metadata := range metadatas {
+ block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
+ require.True(ok)
+ if block.Metadata.Height == failedHeight {
+ return nil, fmt.Errorf("mock download error")
+ }
+ output[i] = block
}
- return block, nil
+ return output, nil
},
),
)
@@ -792,8 +1530,8 @@ func (s *handlerTestSuite) TestGetRawBlocksByRange_DownloadCancelError() {
return testutil.MakeBlockMetadata(10000, tag), nil
},
),
- s.blobStorage.EXPECT().Download(gomock.Any(), gomock.Any()).AnyTimes().DoAndReturn(
- func(_ context.Context, _ *api.BlockMetadata) (*api.Block, error) {
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+ func(_ context.Context, _ []*api.BlockMetadata) ([]*api.Block, error) {
return nil, storage.ErrRequestCanceled
},
),
@@ -808,6 +1546,304 @@ func (s *handlerTestSuite) TestGetRawBlocksByRange_DownloadCancelError() {
s.verifyStatusCode(codes.Canceled, err)
}
+func (s *handlerTestSuite) TestGetRawBlocksByRange_ReadSourceConsolidatedFallsBackOnShadowDownloadError() {
+ const (
+ startHeight uint64 = 9000
+ endHeight uint64 = 9003
+ numBlocks = int(endHeight - startHeight)
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().GetLatestBlockTag()
+ blockMetadatas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, numBlocks, tag)
+ shadowMetadatas := make([]*api.BlockMetadata, len(blockMetadatas))
+ for i, blockMetadata := range blockMetadatas {
+ shadowMetadatas[i] = makeShadowBlockMetadata(blockMetadata)
+ }
+ blocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), tag, startHeight, endHeight).Times(1).Return(blockMetadatas, nil),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), tag).Times(1).Return(testutil.MakeBlockMetadata(10000, tag), nil),
+ s.metaStorage.EXPECT().GetBlocksConsolidationShadow(gomock.Any(), blockMetadatas).Times(1).Return(shadowMetadatas, nil),
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), shadowMetadatas).Times(1).Return(nil, xerrors.Errorf("failed to download CSCB object (key=consolidated): %w", xerrors.New("s3 timeout"))),
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), blockMetadatas).Times(1).Return(blocks, nil),
+ )
+
+ resp, err := s.server.GetRawBlocksByRange(context.Background(), &api.GetRawBlocksByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(blocks, resp.Blocks)
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(numBlocks), snapshot.Counters()["server.shadow_read+outcome=error"].Value())
+ require.Equal(int64(numBlocks), snapshot.Counters()["server.shadow_read+outcome=fallback_success"].Value())
+}
+
+func (s *handlerTestSuite) TestGetRawBlocksByRange_PromotedConsolidatedDownloadErrorDoesNotFallbackToSingleBlock() {
+ const (
+ startHeight uint64 = 9000
+ endHeight uint64 = 9003
+ numBlocks = int(endHeight - startHeight)
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().GetLatestBlockTag()
+ singleBlockMetadatas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, numBlocks, tag)
+ promotedMetadatas := make([]*api.BlockMetadata, len(singleBlockMetadatas))
+ for i, singleBlockMetadata := range singleBlockMetadatas {
+ promotedMetadatas[i] = makeShadowBlockMetadata(singleBlockMetadata)
+ }
+ downloadErr := xerrors.New("consolidated object missing")
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), tag, startHeight, endHeight).Times(1).Return(promotedMetadatas, nil),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), tag).Times(1).Return(testutil.MakeBlockMetadata(10000, tag), nil),
+ s.metaStorage.EXPECT().GetBlocksConsolidationShadow(gomock.Any(), promotedMetadatas).Times(1).Return(make([]*api.BlockMetadata, len(promotedMetadatas)), nil),
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), promotedMetadatas).Times(1).Return(nil, downloadErr),
+ )
+
+ resp, err := s.server.GetRawBlocksByRange(context.Background(), &api.GetRawBlocksByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.Error(err)
+ require.Nil(resp)
+ require.Contains(err.Error(), downloadErr.Error())
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(numBlocks), snapshot.Counters()["server.shadow_read+outcome=error"].Value())
+ require.NotContains(snapshot.Counters(), "server.shadow_read+outcome=fallback_success")
+}
+
+func (s *handlerTestSuite) TestGetRawBlocksByRange_ReadSourceConsolidatedUsesBatchLookupWithMiss() {
+ const (
+ startHeight uint64 = 9000
+ endHeight uint64 = 9003
+ numBlocks = int(endHeight - startHeight)
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().GetLatestBlockTag()
+ blockMetadatas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, numBlocks, tag)
+ shadowMetadatas := make([]*api.BlockMetadata, len(blockMetadatas))
+ shadowMetadatas[0] = makeShadowBlockMetadata(blockMetadatas[0])
+ shadowMetadatas[2] = makeShadowBlockMetadata(blockMetadatas[2])
+ selectedMetadatas := []*api.BlockMetadata{shadowMetadatas[0], blockMetadatas[1], shadowMetadatas[2]}
+ blocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ downloadedBlocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ downloadedBlocks[0].Metadata = shadowMetadatas[0]
+ downloadedBlocks[2].Metadata = shadowMetadatas[2]
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), tag, startHeight, endHeight).Times(1).Return(blockMetadatas, nil),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), tag).Times(1).Return(testutil.MakeBlockMetadata(10000, tag), nil),
+ s.metaStorage.EXPECT().GetBlocksConsolidationShadow(gomock.Any(), blockMetadatas).Times(1).Return(shadowMetadatas, nil),
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), selectedMetadatas).Times(1).Return(downloadedBlocks, nil),
+ )
+
+ resp, err := s.server.GetRawBlocksByRange(context.Background(), &api.GetRawBlocksByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(blocks, resp.Blocks)
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(2), snapshot.Counters()["server.shadow_read+outcome=success"].Value())
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=miss"].Value())
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=fallback_success"].Value())
+}
+
+func (s *handlerTestSuite) TestGetRawBlocksByRange_DefaultUsesReadShadowFirstWithMiss() {
+ const (
+ startHeight uint64 = 9000
+ endHeight uint64 = 9003
+ numBlocks = int(endHeight - startHeight)
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+ s.server.config.AWS.Storage.Consolidation.ReadShadowFirst = true
+
+ tag := s.app.Config().GetLatestBlockTag()
+ blockMetadatas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, numBlocks, tag)
+ shadowMetadatas := make([]*api.BlockMetadata, len(blockMetadatas))
+ shadowMetadatas[0] = makeShadowBlockMetadata(blockMetadatas[0])
+ shadowMetadatas[2] = makeShadowBlockMetadata(blockMetadatas[2])
+ selectedMetadatas := []*api.BlockMetadata{shadowMetadatas[0], blockMetadatas[1], shadowMetadatas[2]}
+ blocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ downloadedBlocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ downloadedBlocks[0].Metadata = shadowMetadatas[0]
+ downloadedBlocks[2].Metadata = shadowMetadatas[2]
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), tag, startHeight, endHeight).Times(1).Return(blockMetadatas, nil),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), tag).Times(1).Return(testutil.MakeBlockMetadata(10000, tag), nil),
+ s.metaStorage.EXPECT().GetBlocksConsolidationShadow(gomock.Any(), blockMetadatas).Times(1).Return(shadowMetadatas, nil),
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), selectedMetadatas).Times(1).Return(downloadedBlocks, nil),
+ )
+
+ resp, err := s.server.GetRawBlocksByRange(context.Background(), &api.GetRawBlocksByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(blocks, resp.Blocks)
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(2), snapshot.Counters()["server.shadow_read+outcome=success"].Value())
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=miss"].Value())
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=fallback_success"].Value())
+}
+
+func (s *handlerTestSuite) TestGetRawBlocksByRange_ReadSourceConsolidatedUsesBatchLookupWhenConfigDisabled() {
+ const (
+ startHeight uint64 = 9000
+ endHeight uint64 = 9003
+ numBlocks = int(endHeight - startHeight)
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().GetLatestBlockTag()
+ blockMetadatas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, numBlocks, tag)
+ shadowMetadatas := make([]*api.BlockMetadata, len(blockMetadatas))
+ for i, blockMetadata := range blockMetadatas {
+ shadowMetadatas[i] = makeShadowBlockMetadata(blockMetadata)
+ }
+ blocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ downloadedBlocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ for i, shadowMetadata := range shadowMetadatas {
+ downloadedBlocks[i].Metadata = shadowMetadata
+ }
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), tag, startHeight, endHeight).Times(1).Return(blockMetadatas, nil),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), tag).Times(1).Return(testutil.MakeBlockMetadata(10000, tag), nil),
+ s.metaStorage.EXPECT().GetBlocksConsolidationShadow(gomock.Any(), blockMetadatas).Times(1).Return(shadowMetadatas, nil),
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), shadowMetadatas).Times(1).Return(downloadedBlocks, nil),
+ )
+
+ resp, err := s.server.GetRawBlocksByRange(context.Background(), &api.GetRawBlocksByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(blocks, resp.Blocks)
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(numBlocks), snapshot.Counters()["server.shadow_read+outcome=success"].Value())
+}
+
+func (s *handlerTestSuite) TestGetRawBlocksByRange_ReadSourceConsolidatedFallsBackOnMiss() {
+ const (
+ startHeight uint64 = 9000
+ endHeight uint64 = 9003
+ numBlocks = int(endHeight - startHeight)
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().GetLatestBlockTag()
+ blockMetadatas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, numBlocks, tag)
+ shadowMetadatas := make([]*api.BlockMetadata, len(blockMetadatas))
+ shadowMetadatas[0] = makeShadowBlockMetadata(blockMetadatas[0])
+ shadowMetadatas[2] = makeShadowBlockMetadata(blockMetadatas[2])
+ selectedMetadatas := []*api.BlockMetadata{shadowMetadatas[0], blockMetadatas[1], shadowMetadatas[2]}
+ blocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ downloadedBlocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ downloadedBlocks[0].Metadata = shadowMetadatas[0]
+ downloadedBlocks[2].Metadata = shadowMetadatas[2]
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), tag, startHeight, endHeight).Times(1).Return(blockMetadatas, nil),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), tag).Times(1).Return(testutil.MakeBlockMetadata(10000, tag), nil),
+ s.metaStorage.EXPECT().GetBlocksConsolidationShadow(gomock.Any(), blockMetadatas).Times(1).Return(shadowMetadatas, nil),
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), selectedMetadatas).Times(1).Return(downloadedBlocks, nil),
+ )
+
+ resp, err := s.server.GetRawBlocksByRange(context.Background(), &api.GetRawBlocksByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(blocks, resp.Blocks)
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(2), snapshot.Counters()["server.shadow_read+outcome=success"].Value())
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=miss"].Value())
+ require.Equal(int64(1), snapshot.Counters()["server.shadow_read+outcome=fallback_success"].Value())
+}
+
+func (s *handlerTestSuite) TestGetRawBlocksByRange_ReadSourceConsolidatedAllMissFallbackFailure() {
+ const (
+ startHeight uint64 = 9000
+ endHeight uint64 = 9003
+ numBlocks = int(endHeight - startHeight)
+ )
+
+ require := testutil.Require(s.T())
+ scope := tally.NewTestScope("", nil)
+ s.server.metrics = newServerMetrics(scope)
+
+ tag := s.app.Config().GetLatestBlockTag()
+ blockMetadatas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, numBlocks, tag)
+ shadowMetadatas := make([]*api.BlockMetadata, len(blockMetadatas))
+
+ gomock.InOrder(
+ s.metaStorage.EXPECT().GetBlocksByHeightRange(gomock.Any(), tag, startHeight, endHeight).Times(1).Return(blockMetadatas, nil),
+ s.metaStorage.EXPECT().GetLatestBlock(gomock.Any(), tag).Times(1).Return(testutil.MakeBlockMetadata(10000, tag), nil),
+ s.metaStorage.EXPECT().GetBlocksConsolidationShadow(gomock.Any(), blockMetadatas).Times(1).Return(shadowMetadatas, nil),
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), blockMetadatas).Times(1).Return(nil, xerrors.New("mock single-block download error")),
+ )
+
+ resp, err := s.server.GetRawBlocksByRange(context.Background(), &api.GetRawBlocksByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ })
+ require.Error(err)
+ require.Nil(resp)
+
+ snapshot := scope.Snapshot()
+ require.Equal(int64(numBlocks), snapshot.Counters()["server.shadow_read+outcome=miss"].Value())
+ require.Equal(int64(numBlocks), snapshot.Counters()["server.shadow_read+outcome=fallback_failure"].Value())
+}
+
func (s *handlerTestSuite) TestGetRawBlocksByRange_MaxRangeExceeded() {
require := testutil.Require(s.T())
resp, err := s.server.GetRawBlocksByRange(context.Background(), &api.GetRawBlocksByRangeRequest{
@@ -844,7 +1880,7 @@ func (s *handlerTestSuite) TestGetNativeBlockByRange() {
},
),
s.parser.EXPECT().ParseNativeBlock(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
- func(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+ func(ctx context.Context, rawBlock *api.Block, _ ...parser.ParseOption) (*api.NativeBlock, error) {
return testutil.MakeNativeBlock(rawBlock.Metadata.Height, tag), nil
},
),
@@ -895,15 +1931,20 @@ func (s *handlerTestSuite) TestGetNativeBlocksByRange() {
return testutil.MakeBlockMetadata(10000, tag), nil
},
),
- s.blobStorage.EXPECT().Download(gomock.Any(), gomock.Any()).Times(numBlocks).DoAndReturn(
- func(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
- block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
- require.True(ok)
- return block, nil
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+ func(ctx context.Context, metadatas []*api.BlockMetadata) ([]*api.Block, error) {
+ require.Len(metadatas, numBlocks)
+ output := make([]*api.Block, len(metadatas))
+ for i, metadata := range metadatas {
+ block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
+ require.True(ok)
+ output[i] = block
+ }
+ return output, nil
},
),
s.parser.EXPECT().ParseNativeBlock(gomock.Any(), gomock.Any()).Times(numBlocks).DoAndReturn(
- func(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+ func(ctx context.Context, rawBlock *api.Block, _ ...parser.ParseOption) (*api.NativeBlock, error) {
return testutil.MakeNativeBlock(rawBlock.Metadata.Height, tag), nil
},
),
@@ -1052,11 +2093,16 @@ func (s *handlerTestSuite) TestGetRosettaBlocksByRange() {
return testutil.MakeBlockMetadata(10000, tag), nil
},
),
- s.blobStorage.EXPECT().Download(gomock.Any(), gomock.Any()).Times(numBlocks).DoAndReturn(
- func(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
- block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
- require.True(ok)
- return block, nil
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+ func(ctx context.Context, metadatas []*api.BlockMetadata) ([]*api.Block, error) {
+ require.Len(metadatas, numBlocks)
+ output := make([]*api.Block, len(metadatas))
+ for i, metadata := range metadatas {
+ block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
+ require.True(ok)
+ output[i] = block
+ }
+ return output, nil
},
),
s.parser.EXPECT().ParseRosettaBlock(gomock.Any(), gomock.Any()).Times(numBlocks).DoAndReturn(
@@ -1131,11 +2177,16 @@ func (s *handlerTestSuite) TestGetRosettaBlocksByRange_NotImplemented() {
return testutil.MakeBlockMetadata(10000, tag), nil
},
),
- s.blobStorage.EXPECT().Download(gomock.Any(), gomock.Any()).Times(numBlocks).DoAndReturn(
- func(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
- block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
- require.True(ok)
- return block, nil
+ s.blobStorage.EXPECT().DownloadMany(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
+ func(ctx context.Context, metadatas []*api.BlockMetadata) ([]*api.Block, error) {
+ require.Len(metadatas, numBlocks)
+ output := make([]*api.Block, len(metadatas))
+ for i, metadata := range metadatas {
+ block, ok := blocksByObjectKey[metadata.ObjectKeyMain]
+ require.True(ok)
+ output[i] = block
+ }
+ return output, nil
},
),
)
@@ -2642,8 +3693,8 @@ func (s *handlerTestSuite) TestGetNativeTransaction() {
GetBlocksByHeights(gomock.Any(), stableTag, gomock.Any()).
Return([]*api.BlockMetadata{blockMetadata}, nil),
s.blobStorage.EXPECT().
- Download(gomock.Any(), gomock.Any()).
- Return(rawBlock, nil),
+ DownloadMany(gomock.Any(), []*api.BlockMetadata{blockMetadata}).
+ Return([]*api.Block{rawBlock}, nil),
s.parser.EXPECT().
ParseNativeBlock(gomock.Any(), rawBlock).
Return(nativeBlock, nil),
@@ -2729,7 +3780,7 @@ func (s *handlerTestSuite) TestGetVerifiedAccountState() {
},
),
s.parser.EXPECT().ParseNativeBlock(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
- func(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+ func(ctx context.Context, rawBlock *api.Block, _ ...parser.ParseOption) (*api.NativeBlock, error) {
return nativeBlock, nil
},
),
@@ -2849,7 +3900,7 @@ func (s *handlerTestSuite) TestGetVerifiedAccountState_GetAccountProof_Failure()
},
),
s.parser.EXPECT().ParseNativeBlock(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
- func(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+ func(ctx context.Context, rawBlock *api.Block, _ ...parser.ParseOption) (*api.NativeBlock, error) {
return nativeBlock, nil
},
),
@@ -2924,7 +3975,7 @@ func (s *handlerTestSuite) TestGetVerifiedAccountState_ValidateAccountState_Fail
},
),
s.parser.EXPECT().ParseNativeBlock(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
- func(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+ func(ctx context.Context, rawBlock *api.Block, _ ...parser.ParseOption) (*api.NativeBlock, error) {
return nativeBlock, nil
},
),
@@ -3036,7 +4087,7 @@ func (s *handlerTestSuite) TestGetVerifiedAccountState_Erc20() {
},
),
s.parser.EXPECT().ParseNativeBlock(gomock.Any(), gomock.Any()).Times(1).DoAndReturn(
- func(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error) {
+ func(ctx context.Context, rawBlock *api.Block, _ ...parser.ParseOption) (*api.NativeBlock, error) {
return nativeBlock, nil
},
),
@@ -3088,3 +4139,113 @@ func (s *handlerTestSuite) TestGetVerifiedAccountState_Erc20() {
require.Equal(result, resp.GetResponse())
}
+
+func (s *handlerTestSuite) TestGetBlockByTimestamp_Success() {
+ require := testutil.Require(s.T())
+
+ // Create a test block with a specific timestamp
+ testTimestamp := uint64(1640995200) // 2022-01-01 00:00:00 UTC
+ testTime := time.Unix(int64(testTimestamp), 0)
+ stableTag := s.app.Config().GetStableBlockTag()
+
+ expectedBlock := &api.BlockMetadata{
+ Tag: stableTag,
+ Hash: "test_hash_123",
+ ParentHash: "test_parent_hash_122",
+ Height: 123,
+ ParentHeight: 122,
+ ObjectKeyMain: "test_object_key",
+ Timestamp: timestamppb.New(testTime),
+ Skipped: false,
+ }
+
+ // Set up mock expectation
+ s.metaStorage.EXPECT().GetBlockByTimestamp(gomock.Any(), stableTag, testTimestamp).Times(1).DoAndReturn(
+ func(ctx context.Context, tag uint32, timestamp uint64) (*api.BlockMetadata, error) {
+ require.Equal(stableTag, tag)
+ return expectedBlock, nil
+ },
+ )
+
+ // Make the request
+ req := &api.GetBlockByTimestampRequest{
+ Tag: 0, // This will be converted to stable tag
+ Timestamp: testTimestamp,
+ }
+
+ resp, err := s.server.GetBlockByTimestamp(context.Background(), req)
+
+ // Verify the response
+ require.NoError(err)
+ require.NotNil(resp)
+ require.Equal(expectedBlock.Tag, resp.Tag)
+ require.Equal(expectedBlock.Hash, resp.Hash)
+ require.Equal(expectedBlock.ParentHash, resp.ParentHash)
+ require.Equal(expectedBlock.Height, resp.Height)
+ require.Equal(uint64(expectedBlock.Timestamp.GetSeconds()), resp.Timestamp)
+}
+
+func (s *handlerTestSuite) TestGetBlockByTimestamp_MissingTimestamp() {
+ require := testutil.Require(s.T())
+
+ // Test with missing timestamp
+ req := &api.GetBlockByTimestampRequest{
+ Tag: 0,
+ // Timestamp: 0 (missing)
+ }
+
+ resp, err := s.server.GetBlockByTimestamp(context.Background(), req)
+
+ require.Nil(resp)
+ s.verifyStatusCode(codes.InvalidArgument, err)
+}
+
+func (s *handlerTestSuite) TestGetBlockByTimestamp_NotFound() {
+ require := testutil.Require(s.T())
+
+ testTimestamp := uint64(1640995200) // 2022-01-01 00:00:00 UTC
+ stableTag := s.app.Config().GetStableBlockTag()
+
+ // Set up mock expectation to return not found
+ s.metaStorage.EXPECT().GetBlockByTimestamp(gomock.Any(), stableTag, testTimestamp).Times(1).DoAndReturn(
+ func(ctx context.Context, tag uint32, timestamp uint64) (*api.BlockMetadata, error) {
+ require.Equal(stableTag, tag)
+ return nil, storage.ErrItemNotFound
+ },
+ )
+
+ req := &api.GetBlockByTimestampRequest{
+ Tag: 0, // This will be converted to stable tag
+ Timestamp: testTimestamp,
+ }
+
+ resp, err := s.server.GetBlockByTimestamp(context.Background(), req)
+
+ require.Nil(resp)
+ s.verifyStatusCode(codes.NotFound, err)
+}
+
+func (s *handlerTestSuite) TestGetBlockByTimestamp_StorageError() {
+ require := testutil.Require(s.T())
+
+ testTimestamp := uint64(1640995200) // 2022-01-01 00:00:00 UTC
+ stableTag := s.app.Config().GetStableBlockTag()
+
+ // Set up mock expectation to return an error
+ s.metaStorage.EXPECT().GetBlockByTimestamp(gomock.Any(), stableTag, testTimestamp).Times(1).DoAndReturn(
+ func(ctx context.Context, tag uint32, timestamp uint64) (*api.BlockMetadata, error) {
+ require.Equal(stableTag, tag)
+ return nil, fmt.Errorf("database connection failed")
+ },
+ )
+
+ req := &api.GetBlockByTimestampRequest{
+ Tag: 0, // This will be converted to stable tag
+ Timestamp: testTimestamp,
+ }
+
+ resp, err := s.server.GetBlockByTimestamp(context.Background(), req)
+
+ require.Nil(resp)
+ s.verifyStatusCode(codes.Internal, err)
+}
diff --git a/internal/storage/blobstorage/cscb/encoder.go b/internal/storage/blobstorage/cscb/encoder.go
new file mode 100644
index 00000000..7a125b01
--- /dev/null
+++ b/internal/storage/blobstorage/cscb/encoder.go
@@ -0,0 +1,819 @@
+package cscb
+
+import (
+ "bytes"
+ "compress/gzip"
+ "context"
+ "crypto/sha256"
+ "encoding/binary"
+ "encoding/hex"
+ "fmt"
+ "hash"
+ "hash/crc32"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/klauspost/compress/zstd"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/internal"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+const (
+ HeaderSize = 96
+ EnvelopeHeaderSize = 80
+ BlockIndexRecordSize = 96
+ ChunkIndexRecordSize = 72
+
+ formatVersion = 1
+ codecGzip = 1
+ codecZstd = 2
+ compressionScopeChunked = 2
+
+ fileSuffixGzip = ".cscb.gzip"
+ fileSuffixZstd = ".cscb.zstd"
+
+ MaxZstdLongDistanceWindowLog = 29
+
+ defaultMemoryBudgetBytes = 256 * 1024 * 1024
+ maxZstdDecoderMemoryBytes = 1 << MaxZstdLongDistanceWindowLog
+)
+
+var (
+ magic = [4]byte{'C', 'S', 'C', 'B'}
+ envelopeMagic = [4]byte{'E', 'N', 'V', '1'}
+)
+
+type (
+ EncodeConfig struct {
+ Blockchain common.Blockchain
+ Network common.Network
+ SideChain api.SideChain
+ Codec api.Compression
+ CodecLevel int
+ ZstdLongDistanceWindowLog *int
+ MaxBlocks uint64
+ CompressionChunkBlocks uint64
+ MaxChunkUncompressedBytes *uint64
+ MaxChunkCompressedBytes *uint64
+ MaxCompressedBytes uint64
+ MaxUncompressedBytes uint64
+ ShardSize uint64
+ MemoryBudgetBytes *uint64
+ LocalSpillDir string
+ LocalSpillMaxBytes *uint64
+ }
+
+ Object struct {
+ Key string
+ SHA256 string
+ Length uint64
+ PayloadUncompressedLength uint64
+ PayloadCompressedLength uint64
+ Placements []internal.BlockPlacement
+
+ data []byte
+ tempFile string
+ limiter *spillLimiter
+ diskSize uint64
+ }
+
+ readSeekCloser struct {
+ *bytes.Reader
+ }
+
+ blockRecord struct {
+ height uint64
+ logicalPayloadOffset uint64
+ payloadLength uint64
+ payloadCRC32 uint32
+ chunkIndex uint32
+ chunkRelativeOffset uint64
+ hashSHA256 [32]byte
+ metadataID uint64
+ }
+
+ chunkRecord struct {
+ index uint32
+ startHeight uint64
+ endHeight uint64
+ compressedPayloadOffset uint64
+ compressedLength uint64
+ uncompressedOffset uint64
+ uncompressedLength uint64
+ chunkCRC32 uint32
+ blockCount uint32
+ }
+
+ chunkBuilder struct {
+ index uint32
+ startHeight uint64
+ endHeight uint64
+ compressedPayloadOffset uint64
+ uncompressedOffset uint64
+ uncompressedLength uint64
+ blockCount uint32
+ chunkCRC hash.Hash32
+ writer io.WriteCloser
+ }
+)
+
+func Encode(ctx context.Context, cfg EncodeConfig, blocks []internal.ConsolidatedBlockPayload) (*Object, error) {
+ if err := validateConfig(cfg); err != nil {
+ return nil, err
+ }
+ if len(blocks) == 0 {
+ return nil, xerrors.New("cannot encode empty CSCB object")
+ }
+ if cfg.MaxBlocks > 0 && uint64(len(blocks)) > cfg.MaxBlocks {
+ return nil, xerrors.Errorf("CSCB block count %d exceeds max_blocks %d", len(blocks), cfg.MaxBlocks)
+ }
+ internal.RecordConsolidatedUploadProgress(ctx, "cscb_encode_started", len(blocks))
+
+ first := blocks[0].Metadata
+ if first == nil {
+ return nil, xerrors.New("block metadata is required")
+ }
+ tag := first.Tag
+ startHeight := first.Height
+ shardStart, shardEnd := shardBounds(startHeight, cfg.ShardSize)
+
+ limiter := newSpillLimiter(cfg.LocalSpillMaxBytes)
+ payloadSink, err := newSpillBuffer(cfg.LocalSpillDir, cfg.MemoryBudgetBytes, limiter)
+ if err != nil {
+ return nil, err
+ }
+ defer payloadSink.cleanup()
+
+ var (
+ blockRecords []blockRecord
+ chunkRecords []chunkRecord
+ placements []internal.BlockPlacement
+ currentChunk *chunkBuilder
+ payloadCRC = crc32.NewIEEE()
+ logicalOffset uint64
+ prevHeight uint64
+ totalUncompressed uint64
+ )
+ defer func() {
+ if currentChunk != nil && currentChunk.writer != nil {
+ _ = currentChunk.writer.Close()
+ }
+ }()
+
+ flushChunk := func() error {
+ if currentChunk == nil || currentChunk.blockCount == 0 {
+ return nil
+ }
+ if err := currentChunk.writer.Close(); err != nil {
+ return xerrors.Errorf("failed to close CSCB chunk compressor: %w", err)
+ }
+ currentChunk.writer = nil
+ compressedLength := payloadSink.size - currentChunk.compressedPayloadOffset
+ if cfg.MaxChunkCompressedBytes != nil && compressedLength > *cfg.MaxChunkCompressedBytes && currentChunk.blockCount > 1 {
+ return xerrors.Errorf("CSCB compressed chunk length %d exceeds max_chunk_compressed_bytes %d", compressedLength, *cfg.MaxChunkCompressedBytes)
+ }
+ record := chunkRecord{
+ index: currentChunk.index,
+ startHeight: currentChunk.startHeight,
+ endHeight: currentChunk.endHeight,
+ compressedPayloadOffset: currentChunk.compressedPayloadOffset,
+ compressedLength: compressedLength,
+ uncompressedOffset: currentChunk.uncompressedOffset,
+ uncompressedLength: currentChunk.uncompressedLength,
+ chunkCRC32: currentChunk.chunkCRC.Sum32(),
+ blockCount: currentChunk.blockCount,
+ }
+ chunkRecords = append(chunkRecords, record)
+ internal.RecordConsolidatedUploadProgress(ctx, "cscb_chunk_flushed", record.index, record.startHeight, record.endHeight, record.compressedLength)
+ currentChunk = nil
+ return nil
+ }
+
+ for i, payload := range blocks {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ metadata := payload.Metadata
+ if metadata == nil {
+ return nil, xerrors.Errorf("block %d metadata is required", i)
+ }
+ if metadata.Skipped {
+ return nil, xerrors.Errorf("skipped block cannot be stored in CSCB payload (height=%d)", metadata.Height)
+ }
+ if metadata.Tag != tag {
+ return nil, xerrors.Errorf("CSCB object cannot mix tags: got %d want %d", metadata.Tag, tag)
+ }
+ if i > 0 && metadata.Height <= prevHeight {
+ return nil, xerrors.Errorf("CSCB blocks must be strictly increasing by height: got %d after %d", metadata.Height, prevHeight)
+ }
+ if metadata.Height >= shardEnd {
+ return nil, xerrors.Errorf("CSCB object crosses shard boundary: height=%d shard_end=%d", metadata.Height, shardEnd)
+ }
+ if payload.MetadataID < 0 {
+ return nil, xerrors.Errorf("CSCB metadata id hint cannot be negative (height=%d metadata_id=%d)", metadata.Height, payload.MetadataID)
+ }
+ prevHeight = metadata.Height
+
+ payloadLength, err := validatePayloadSource(payload)
+ if err != nil {
+ return nil, xerrors.Errorf("invalid CSCB payload at height %d: %w", metadata.Height, err)
+ }
+ if err := validateAdd(totalUncompressed, payloadLength, uint64(len(blocks)), cfg.MaxUncompressedBytes); err != nil {
+ return nil, err
+ }
+
+ if currentChunk != nil && shouldFlushChunk(currentChunk, payloadLength, cfg) {
+ if err := flushChunk(); err != nil {
+ return nil, err
+ }
+ }
+ if currentChunk == nil {
+ var err error
+ currentChunk, err = newChunkBuilder(
+ uint32(len(chunkRecords)),
+ metadata.Height,
+ logicalOffset,
+ payloadSink.size,
+ payloadSink,
+ cfg,
+ )
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ chunkRelativeOffset := currentChunk.uncompressedLength
+ blockCRC := crc32.NewIEEE()
+ if err := writePayload(ctx, payload, payloadLength, currentChunk.writer, payloadCRC, currentChunk.chunkCRC, blockCRC); err != nil {
+ return nil, xerrors.Errorf("failed to write CSCB payload at height %d: %w", metadata.Height, err)
+ }
+ hashDigest := sha256.Sum256([]byte(metadata.Hash))
+ blockRecords = append(blockRecords, blockRecord{
+ height: metadata.Height,
+ logicalPayloadOffset: logicalOffset,
+ payloadLength: payloadLength,
+ payloadCRC32: blockCRC.Sum32(),
+ chunkIndex: currentChunk.index,
+ chunkRelativeOffset: chunkRelativeOffset,
+ hashSHA256: hashDigest,
+ metadataID: uint64(payload.MetadataID),
+ })
+ placements = append(placements, internal.BlockPlacement{
+ MetadataID: payload.MetadataID,
+ Height: metadata.Height,
+ Hash: metadata.Hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: logicalOffset,
+ ByteLength: payloadLength,
+ UncompressedLength: payloadLength,
+ })
+ logicalOffset += payloadLength
+ totalUncompressed += payloadLength
+ currentChunk.uncompressedLength += payloadLength
+ currentChunk.blockCount++
+ currentChunk.endHeight = metadata.Height + 1
+ internal.RecordConsolidatedUploadProgress(ctx, "cscb_block_encoded", metadata.Height, i+1, len(blocks))
+ }
+ if err := flushChunk(); err != nil {
+ return nil, err
+ }
+
+ payloadCompressedLength := payloadSink.size
+ if cfg.MaxCompressedBytes > 0 && payloadCompressedLength > cfg.MaxCompressedBytes && len(blocks) > 1 {
+ return nil, xerrors.Errorf("CSCB compressed payload length %d exceeds max_compressed_bytes %d", payloadCompressedLength, cfg.MaxCompressedBytes)
+ }
+
+ endHeight := blocks[len(blocks)-1].Metadata.Height + 1
+ envelope, err := buildEnvelope(blockRecords, chunkRecords, startHeight, endHeight)
+ if err != nil {
+ return nil, err
+ }
+ payloadOffset := uint64(HeaderSize + len(envelope))
+ applyAbsoluteChunkOffsets(envelope, len(blockRecords), payloadOffset)
+ header := buildHeader(cfg.Codec, uint64(len(blockRecords)), uint64(len(chunkRecords)), startHeight, endHeight, envelope, payloadCompressedLength, totalUncompressed, payloadCRC)
+ object, err := materializeObject(ctx, cfg.LocalSpillDir, cfg.MemoryBudgetBytes, header, envelope, payloadSink, limiter)
+ if err != nil {
+ return nil, err
+ }
+
+ key := buildObjectKey(cfg, tag, shardStart, shardEnd, startHeight, endHeight, object.SHA256)
+ object.Key = key
+ object.PayloadUncompressedLength = totalUncompressed
+ object.PayloadCompressedLength = payloadCompressedLength
+ object.Placements = placements
+ for i := range object.Placements {
+ object.Placements[i].ObjectKey = key
+ }
+ return object, nil
+}
+
+func (o *Object) Open() (io.ReadCloser, error) {
+ if o.tempFile != "" {
+ return os.Open(o.tempFile)
+ }
+ return readSeekCloser{Reader: bytes.NewReader(o.data)}, nil
+}
+
+func (readSeekCloser) Close() error {
+ return nil
+}
+
+func (o *Object) Bytes() ([]byte, bool) {
+ if o.tempFile != "" {
+ return nil, false
+ }
+ return o.data, true
+}
+
+func (o *Object) Close() error {
+ if o.tempFile == "" {
+ return nil
+ }
+ err := os.Remove(o.tempFile)
+ o.tempFile = ""
+ if o.limiter != nil {
+ o.limiter.release(o.diskSize)
+ o.diskSize = 0
+ }
+ return err
+}
+
+func validateConfig(cfg EncodeConfig) error {
+ switch cfg.Codec {
+ case api.Compression_GZIP, api.Compression_ZSTD:
+ default:
+ return xerrors.Errorf("unsupported CSCB codec: %v", cfg.Codec)
+ }
+ if cfg.CodecLevel <= 0 {
+ return xerrors.New("CSCB codec_level must be positive")
+ }
+ if cfg.Codec == api.Compression_GZIP && cfg.CodecLevel > gzip.BestCompression {
+ return xerrors.Errorf("gzip CSCB codec_level must be between 1 and %d", gzip.BestCompression)
+ }
+ if cfg.Codec == api.Compression_ZSTD && cfg.ZstdLongDistanceWindowLog != nil {
+ windowLog := *cfg.ZstdLongDistanceWindowLog
+ if windowLog < 10 || windowLog > MaxZstdLongDistanceWindowLog {
+ return xerrors.Errorf("zstd_long_distance_window_log must be between 10 and %d, got %d", MaxZstdLongDistanceWindowLog, windowLog)
+ }
+ }
+ if cfg.CompressionChunkBlocks == 0 {
+ return xerrors.New("CSCB compression_chunk_blocks must be positive")
+ }
+ if cfg.ShardSize == 0 {
+ return xerrors.New("CSCB shard_size must be positive")
+ }
+ if cfg.MemoryBudgetBytes != nil && *cfg.MemoryBudgetBytes == 0 {
+ return xerrors.New("CSCB memory_budget_bytes must be positive when set")
+ }
+ if cfg.LocalSpillMaxBytes != nil && *cfg.LocalSpillMaxBytes == 0 {
+ return xerrors.New("CSCB local_spill_max_bytes must be positive when set")
+ }
+ return nil
+}
+
+func validatePayloadSource(payload internal.ConsolidatedBlockPayload) (uint64, error) {
+ if payload.RawBlockPayload == nil {
+ return 0, xerrors.New("raw block payload source is required")
+ }
+ expected := payload.RawBlockPayload.Length()
+ if expected == 0 {
+ return 0, xerrors.New("raw block payload source cannot be empty")
+ }
+ if payload.UncompressedLength != 0 && payload.UncompressedLength != expected {
+ return 0, xerrors.Errorf("uncompressed length mismatch: payload=%d source=%d", payload.UncompressedLength, expected)
+ }
+ return expected, nil
+}
+
+func writePayload(ctx context.Context, payload internal.ConsolidatedBlockPayload, expected uint64, writers ...io.Writer) error {
+ reader, err := payload.RawBlockPayload.Open(ctx)
+ if err != nil {
+ return err
+ }
+ defer func() { _ = reader.Close() }()
+ copied, err := io.Copy(io.MultiWriter(writers...), reader)
+ if err != nil {
+ return err
+ }
+ if uint64(copied) != expected {
+ return xerrors.Errorf("payload length mismatch: read %d want %d", copied, expected)
+ }
+ return nil
+}
+
+func validateAdd(current uint64, next uint64, blockCount uint64, limit uint64) error {
+ if limit == 0 {
+ return nil
+ }
+ if current+next <= limit {
+ return nil
+ }
+ if blockCount == 1 {
+ return nil
+ }
+ return xerrors.Errorf("CSCB uncompressed payload length %d exceeds max_uncompressed_bytes %d", current+next, limit)
+}
+
+func shouldFlushChunk(chunk *chunkBuilder, nextPayloadLength uint64, cfg EncodeConfig) bool {
+ if uint64(chunk.blockCount) >= cfg.CompressionChunkBlocks {
+ return true
+ }
+ if chunk.blockCount > 0 && chunk.uncompressedLength+nextPayloadLength > effectiveMemoryBudget(cfg.MemoryBudgetBytes) {
+ return true
+ }
+ if cfg.MaxChunkUncompressedBytes == nil {
+ return false
+ }
+ if chunk.blockCount == 0 {
+ return false
+ }
+ return chunk.uncompressedLength+nextPayloadLength > *cfg.MaxChunkUncompressedBytes
+}
+
+func newChunkBuilder(index uint32, startHeight uint64, uncompressedOffset uint64, compressedPayloadOffset uint64, dst io.Writer, cfg EncodeConfig) (*chunkBuilder, error) {
+ writer, err := newChunkCompressor(dst, cfg.Codec, cfg.CodecLevel, cfg.ZstdLongDistanceWindowLog)
+ if err != nil {
+ return nil, err
+ }
+ return &chunkBuilder{
+ index: index,
+ startHeight: startHeight,
+ compressedPayloadOffset: compressedPayloadOffset,
+ uncompressedOffset: uncompressedOffset,
+ chunkCRC: crc32.NewIEEE(),
+ writer: writer,
+ }, nil
+}
+
+func newChunkCompressor(dst io.Writer, codec api.Compression, level int, windowLog *int) (io.WriteCloser, error) {
+ switch codec {
+ case api.Compression_GZIP:
+ writer, err := gzip.NewWriterLevel(dst, level)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create gzip writer: %w", err)
+ }
+ return writer, nil
+ case api.Compression_ZSTD:
+ opts := []zstd.EOption{
+ zstd.WithEncoderLevel(zstd.EncoderLevelFromZstd(level)),
+ zstd.WithEncoderConcurrency(1),
+ zstd.WithEncoderCRC(false),
+ }
+ if windowLog != nil {
+ opts = append(opts, zstd.WithWindowSize(1< b.limit {
+ if err := b.reserveDisk(uint64(b.buf.Len())); err != nil {
+ return 0, err
+ }
+ file, err := createTemp(b.dir, "chainstorage-cscb-payload-*.tmp")
+ if err != nil {
+ b.releaseDisk()
+ return 0, xerrors.Errorf("failed to create CSCB payload temp file: %w", err)
+ }
+ if _, err := file.Write(b.buf.Bytes()); err != nil {
+ _ = file.Close()
+ _ = os.Remove(file.Name())
+ b.releaseDisk()
+ return 0, xerrors.Errorf("failed to spill CSCB payload buffer: %w", err)
+ }
+ b.buf.Reset()
+ b.file = file
+ }
+ var n int
+ var err error
+ if b.file != nil {
+ if err := b.reserveDisk(uint64(len(p))); err != nil {
+ return 0, err
+ }
+ n, err = b.file.Write(p)
+ if n < len(p) {
+ b.limiter.release(uint64(len(p) - n))
+ b.diskSize -= uint64(len(p) - n)
+ }
+ } else {
+ n, err = b.buf.Write(p)
+ }
+ b.size += uint64(n)
+ return n, err
+}
+
+func (b *spillBuffer) Open() (io.ReadCloser, error) {
+ if b.file == nil {
+ return io.NopCloser(bytes.NewReader(b.buf.Bytes())), nil
+ }
+ if err := b.file.Sync(); err != nil {
+ return nil, xerrors.Errorf("failed to sync CSCB payload temp file: %w", err)
+ }
+ return os.Open(b.file.Name())
+}
+
+func (b *spillBuffer) inMemory() bool {
+ return b.file == nil
+}
+
+func (b *spillBuffer) cleanup() {
+ if b.file == nil {
+ return
+ }
+ name := b.file.Name()
+ _ = b.file.Close()
+ _ = os.Remove(name)
+ b.releaseDisk()
+}
+
+func effectiveMemoryBudget(limit *uint64) uint64 {
+ if limit == nil {
+ return defaultMemoryBudgetBytes
+ }
+ return *limit
+}
+
+type spillLimiter struct {
+ limit *uint64
+ used uint64
+}
+
+func newSpillLimiter(limit *uint64) *spillLimiter {
+ return &spillLimiter{limit: limit}
+}
+
+func (l *spillLimiter) reserve(n uint64) error {
+ if l == nil || l.limit == nil || n == 0 {
+ return nil
+ }
+ if n > *l.limit || l.used > *l.limit-n {
+ return xerrors.Errorf("CSCB local spill bytes would exceed local_spill_max_bytes %d", *l.limit)
+ }
+ l.used += n
+ return nil
+}
+
+func (l *spillLimiter) release(n uint64) {
+ if l == nil || l.limit == nil || n == 0 {
+ return
+ }
+ if n >= l.used {
+ l.used = 0
+ return
+ }
+ l.used -= n
+}
+
+func (b *spillBuffer) reserveDisk(n uint64) error {
+ if err := b.limiter.reserve(n); err != nil {
+ return err
+ }
+ b.diskSize += n
+ return nil
+}
+
+func (b *spillBuffer) releaseDisk() {
+ b.limiter.release(b.diskSize)
+ b.diskSize = 0
+}
+
+func spillDir(dir string) string {
+ if dir == "" {
+ return os.TempDir()
+ }
+ return filepath.Clean(dir)
+}
+
+func createTemp(dir string, pattern string) (*os.File, error) {
+ spillPath := spillDir(dir)
+ if err := os.MkdirAll(spillPath, 0o700); err != nil {
+ return nil, err
+ }
+ return os.CreateTemp(spillPath, pattern)
+}
diff --git a/internal/storage/blobstorage/cscb/encoder_test.go b/internal/storage/blobstorage/cscb/encoder_test.go
new file mode 100644
index 00000000..14673330
--- /dev/null
+++ b/internal/storage/blobstorage/cscb/encoder_test.go
@@ -0,0 +1,252 @@
+package cscb
+
+import (
+ "bytes"
+ "compress/gzip"
+ "context"
+ "crypto/sha256"
+ "encoding/binary"
+ "hash/crc32"
+ "io"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+
+ "github.com/klauspost/compress/zstd"
+
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/internal"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestEncodeCSCB_Zstd(t *testing.T) {
+ require := testutil.Require(t)
+
+ object, err := Encode(context.Background(), testEncodeConfig(api.Compression_ZSTD), testPayloads())
+ require.NoError(err)
+ defer object.Close()
+
+ data, ok := object.Bytes()
+ require.True(ok)
+ require.Equal(uint64(len(data)), object.Length)
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, object.Placements[0].ObjectFormat)
+ require.Equal("BLOCKCHAIN_SOLANA/NETWORK_SOLANA_MAINNET/consolidated/v=7/shard=00000000000000000000-00000000000000010000/00000000000000000100-00000000000000000103-"+object.SHA256+".cscb.zstd", object.Key)
+
+ header := data[:HeaderSize]
+ envelope := data[HeaderSize : HeaderSize+int(binary.LittleEndian.Uint64(header[40:48]))]
+ require.Equal([]byte("CSCB"), header[0:4])
+ require.Equal(byte(formatVersion), header[4])
+ require.Equal(byte(codecZstd), header[5])
+ require.Equal(byte(compressionScopeChunked), header[6])
+ require.Equal(uint32(3), binary.LittleEndian.Uint32(header[8:12]))
+ require.Equal(uint32(2), binary.LittleEndian.Uint32(header[12:16]))
+ require.Equal(uint64(100), binary.LittleEndian.Uint64(header[16:24]))
+ require.Equal(uint64(103), binary.LittleEndian.Uint64(header[24:32]))
+ require.Equal(uint64(HeaderSize), binary.LittleEndian.Uint64(header[32:40]))
+ require.Equal(uint64(80+3*BlockIndexRecordSize+2*ChunkIndexRecordSize), binary.LittleEndian.Uint64(header[40:48]))
+ require.Equal(crc32.ChecksumIEEE(envelope), binary.LittleEndian.Uint32(header[48:52]))
+ require.Equal(uint32(BlockIndexRecordSize), binary.LittleEndian.Uint32(header[52:56]))
+ require.Equal(uint32(ChunkIndexRecordSize), binary.LittleEndian.Uint32(header[56:60]))
+ require.Equal(uint64(HeaderSize+len(envelope)), binary.LittleEndian.Uint64(header[60:68]))
+
+ require.Equal([]byte("ENV1"), envelope[0:4])
+ require.Equal(uint64(3), binary.LittleEndian.Uint64(envelope[8:16]))
+ require.Equal(uint64(2), binary.LittleEndian.Uint64(envelope[16:24]))
+ require.Equal(uint64(100), binary.LittleEndian.Uint64(envelope[24:32]))
+ require.Equal(uint64(103), binary.LittleEndian.Uint64(envelope[32:40]))
+ require.Equal(uint64(EnvelopeHeaderSize), binary.LittleEndian.Uint64(envelope[40:48]))
+
+ payloads := [][]byte{[]byte("alpha"), []byte("bravo-bravo"), []byte("charlie")}
+ firstBlock := envelope[EnvelopeHeaderSize : EnvelopeHeaderSize+BlockIndexRecordSize]
+ require.Equal(uint64(100), binary.LittleEndian.Uint64(firstBlock[0:8]))
+ require.Equal(uint64(0), binary.LittleEndian.Uint64(firstBlock[8:16]))
+ require.Equal(uint64(len(payloads[0])), binary.LittleEndian.Uint64(firstBlock[16:24]))
+ require.Equal(crc32.ChecksumIEEE(payloads[0]), binary.LittleEndian.Uint32(firstBlock[24:28]))
+ require.Equal(uint32(0), binary.LittleEndian.Uint32(firstBlock[28:32]))
+ require.Equal(uint64(0), binary.LittleEndian.Uint64(firstBlock[32:40]))
+ hash := sha256.Sum256([]byte("hash-100"))
+ require.Equal(hash[:], firstBlock[48:80])
+ require.Equal(uint64(9001), binary.LittleEndian.Uint64(firstBlock[80:88]))
+
+ secondBlock := envelope[EnvelopeHeaderSize+BlockIndexRecordSize : EnvelopeHeaderSize+2*BlockIndexRecordSize]
+ require.Equal(uint64(len(payloads[0])), binary.LittleEndian.Uint64(secondBlock[8:16]))
+ require.Equal(uint32(0), binary.LittleEndian.Uint32(secondBlock[28:32]))
+ require.Equal(uint64(len(payloads[0])), binary.LittleEndian.Uint64(secondBlock[32:40]))
+
+ chunkIndexOffset := EnvelopeHeaderSize + 3*BlockIndexRecordSize
+ chunk0 := envelope[chunkIndexOffset : chunkIndexOffset+ChunkIndexRecordSize]
+ chunk1 := envelope[chunkIndexOffset+ChunkIndexRecordSize : chunkIndexOffset+2*ChunkIndexRecordSize]
+ require.Equal(uint32(0), binary.LittleEndian.Uint32(chunk0[0:4]))
+ require.Equal(uint64(100), binary.LittleEndian.Uint64(chunk0[8:16]))
+ require.Equal(uint64(102), binary.LittleEndian.Uint64(chunk0[16:24]))
+ require.Equal(uint64(HeaderSize+len(envelope)), binary.LittleEndian.Uint64(chunk0[24:32]))
+ require.Equal(uint64(0), binary.LittleEndian.Uint64(chunk0[40:48]))
+ require.Equal(uint64(len(payloads[0])+len(payloads[1])), binary.LittleEndian.Uint64(chunk0[48:56]))
+ require.Equal(uint32(2), binary.LittleEndian.Uint32(chunk0[60:64]))
+
+ require.Equal(uint32(1), binary.LittleEndian.Uint32(chunk1[0:4]))
+ require.Equal(uint64(102), binary.LittleEndian.Uint64(chunk1[8:16]))
+ require.Equal(uint64(103), binary.LittleEndian.Uint64(chunk1[16:24]))
+ require.Equal(uint64(len(payloads[0])+len(payloads[1])), binary.LittleEndian.Uint64(chunk1[40:48]))
+ require.Equal(uint64(len(payloads[2])), binary.LittleEndian.Uint64(chunk1[48:56]))
+ require.Equal(uint32(1), binary.LittleEndian.Uint32(chunk1[60:64]))
+
+ require.Equal(uint64(0), object.Placements[0].ByteOffset)
+ require.Equal(uint64(len(payloads[0])), object.Placements[0].ByteLength)
+ require.Equal(uint64(len(payloads[0])+len(payloads[1])), object.Placements[2].ByteOffset)
+ require.Equal(object.Key, object.Placements[2].ObjectKey)
+
+ require.Equal(append(append(payloads[0], payloads[1]...), payloads[2]...), readAllChunks(t, data, header, envelope, api.Compression_ZSTD))
+}
+
+func TestEncodeCSCB_Gzip(t *testing.T) {
+ require := testutil.Require(t)
+
+ object, err := Encode(context.Background(), testEncodeConfig(api.Compression_GZIP), testPayloads())
+ require.NoError(err)
+ defer object.Close()
+
+ data, ok := object.Bytes()
+ require.True(ok)
+ require.Equal(byte(codecGzip), data[5])
+ require.Equal(".cscb.gzip", object.Key[len(object.Key)-len(".cscb.gzip"):])
+ header := data[:HeaderSize]
+ envelope := data[HeaderSize : HeaderSize+int(binary.LittleEndian.Uint64(header[40:48]))]
+ require.Equal(append(append([]byte("alpha"), []byte("bravo-bravo")...), []byte("charlie")...), readAllChunks(t, data, header, envelope, api.Compression_GZIP))
+}
+
+func TestObjectOpenInMemoryReaderIsSeekable(t *testing.T) {
+ require := testutil.Require(t)
+
+ object, err := Encode(context.Background(), testEncodeConfig(api.Compression_ZSTD), testPayloads())
+ require.NoError(err)
+ defer object.Close()
+
+ reader, err := object.Open()
+ require.NoError(err)
+ defer reader.Close()
+
+ seeker, ok := reader.(io.Seeker)
+ require.True(ok)
+ offset, err := seeker.Seek(4, io.SeekStart)
+ require.NoError(err)
+ require.Equal(int64(4), offset)
+
+ buf := make([]byte, 4)
+ n, err := reader.Read(buf)
+ require.NoError(err)
+ require.Equal(4, n)
+ require.Equal([]byte{byte(formatVersion), byte(codecZstd), byte(compressionScopeChunked), 0}, buf)
+}
+
+func TestEncodeCSCB_SpillsWhenMemoryBudgetExceeded(t *testing.T) {
+ require := testutil.Require(t)
+
+ memoryBudget := uint64(1)
+ cfg := testEncodeConfig(api.Compression_ZSTD)
+ cfg.MemoryBudgetBytes = &memoryBudget
+ cfg.LocalSpillDir = filepath.Join(t.TempDir(), "missing-spill-dir")
+ object, err := Encode(context.Background(), cfg, testPayloads())
+ require.NoError(err)
+
+ _, ok := object.Bytes()
+ require.False(ok)
+ reader, err := object.Open()
+ require.NoError(err)
+ data, err := io.ReadAll(reader)
+ require.NoError(err)
+ require.NoError(reader.Close())
+ require.Len(data, int(object.Length))
+ tempFile := object.tempFile
+ require.NotEmpty(tempFile)
+ require.True(strings.HasPrefix(tempFile, cfg.LocalSpillDir))
+ require.NoError(object.Close())
+ _, err = os.Stat(tempFile)
+ require.True(os.IsNotExist(err))
+}
+
+func TestEncodeCSCB_EnforcesLocalSpillMaxBytes(t *testing.T) {
+ require := testutil.Require(t)
+
+ memoryBudget := uint64(1)
+ localSpillMaxBytes := uint64(1)
+ cfg := testEncodeConfig(api.Compression_GZIP)
+ cfg.MemoryBudgetBytes = &memoryBudget
+ cfg.LocalSpillMaxBytes = &localSpillMaxBytes
+ cfg.LocalSpillDir = t.TempDir()
+
+ _, err := Encode(context.Background(), cfg, testPayloads())
+ require.Error(err)
+ require.Contains(err.Error(), "local_spill_max_bytes")
+}
+
+func testEncodeConfig(codec api.Compression) EncodeConfig {
+ return EncodeConfig{
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Codec: codec,
+ CodecLevel: 6,
+ MaxBlocks: 1000,
+ CompressionChunkBlocks: 2,
+ MaxCompressedBytes: 1024 * 1024,
+ MaxUncompressedBytes: 1024 * 1024,
+ ShardSize: 10000,
+ }
+}
+
+func testPayloads() []internal.ConsolidatedBlockPayload {
+ return []internal.ConsolidatedBlockPayload{
+ makePayload(7, 100, "hash-100", 9001, []byte("alpha")),
+ makePayload(7, 101, "hash-101", 9002, []byte("bravo-bravo")),
+ makePayload(7, 102, "hash-102", 9003, []byte("charlie")),
+ }
+}
+
+func makePayload(tag uint32, height uint64, hash string, metadataID int64, raw []byte) internal.ConsolidatedBlockPayload {
+ return internal.ConsolidatedBlockPayload{
+ Metadata: &api.BlockMetadata{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ },
+ MetadataID: metadataID,
+ RawBlockPayload: internal.BytesPayloadSource(raw),
+ UncompressedLength: uint64(len(raw)),
+ }
+}
+
+func readAllChunks(t *testing.T, data []byte, header []byte, envelope []byte, codec api.Compression) []byte {
+ t.Helper()
+ require := testutil.Require(t)
+ chunkCount := int(binary.LittleEndian.Uint32(header[12:16]))
+ chunkIndexOffset := EnvelopeHeaderSize + int(binary.LittleEndian.Uint64(envelope[8:16]))*BlockIndexRecordSize
+ var out []byte
+ for i := 0; i < chunkCount; i++ {
+ record := envelope[chunkIndexOffset+i*ChunkIndexRecordSize : chunkIndexOffset+(i+1)*ChunkIndexRecordSize]
+ offset := binary.LittleEndian.Uint64(record[24:32])
+ length := binary.LittleEndian.Uint64(record[32:40])
+ frame := data[offset : offset+length]
+ var reader io.ReadCloser
+ var err error
+ switch codec {
+ case api.Compression_GZIP:
+ reader, err = gzip.NewReader(bytes.NewReader(frame))
+ case api.Compression_ZSTD:
+ zr, zerr := zstd.NewReader(bytes.NewReader(frame))
+ require.NoError(zerr)
+ defer zr.Close()
+ decoded, zerr := io.ReadAll(zr)
+ require.NoError(zerr)
+ out = append(out, decoded...)
+ continue
+ }
+ require.NoError(err)
+ decoded, err := io.ReadAll(reader)
+ require.NoError(err)
+ require.NoError(reader.Close())
+ out = append(out, decoded...)
+ }
+ return out
+}
diff --git a/internal/storage/blobstorage/cscb/reader.go b/internal/storage/blobstorage/cscb/reader.go
new file mode 100644
index 00000000..37d8891e
--- /dev/null
+++ b/internal/storage/blobstorage/cscb/reader.go
@@ -0,0 +1,705 @@
+package cscb
+
+import (
+ "bytes"
+ "compress/gzip"
+ "crypto/sha256"
+ "encoding/binary"
+ "hash"
+ "hash/crc32"
+ "io"
+ "sort"
+
+ "github.com/klauspost/compress/zstd"
+ "golang.org/x/xerrors"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ Header struct {
+ Codec api.Compression
+ BlockCount uint64
+ ChunkCount uint64
+ StartHeight uint64
+ EndHeight uint64
+ EnvelopeOffset uint64
+ EnvelopeLength uint64
+ EnvelopeCRC32 uint32
+ BlockIndexRecordSize uint32
+ ChunkIndexRecordSize uint32
+ PayloadOffset uint64
+ PayloadCompressedLength uint64
+ PayloadUncompressedLength uint64
+ PayloadCRC32 uint32
+ }
+
+ Index struct {
+ Header Header
+ Blocks []BlockDescriptor
+ Chunks []ChunkDescriptor
+
+ blocksByHeight map[uint64]int
+ }
+
+ BlockDescriptor struct {
+ Height uint64
+ LogicalPayloadOffset uint64
+ PayloadLength uint64
+ PayloadCRC32 uint32
+ ChunkIndex uint32
+ ChunkRelativeOffset uint64
+ HashSHA256 [32]byte
+ MetadataID uint64
+ }
+
+ ChunkDescriptor struct {
+ Index uint32
+ StartHeight uint64
+ EndHeight uint64
+ CompressedPayloadOffset uint64
+ CompressedLength uint64
+ UncompressedOffset uint64
+ UncompressedLength uint64
+ ChunkCRC32 uint32
+ BlockCount uint32
+ }
+)
+
+func HeaderEnvelopeLength(data []byte) (uint64, error) {
+ header, err := ParseHeader(data)
+ if err != nil {
+ return 0, err
+ }
+ length, err := checkedAdd(header.EnvelopeOffset, header.EnvelopeLength)
+ if err != nil {
+ return 0, err
+ }
+ return length, nil
+}
+
+func ParseHeader(data []byte) (*Header, error) {
+ if len(data) < HeaderSize {
+ return nil, xerrors.Errorf("CSCB header requires %d bytes, got %d", HeaderSize, len(data))
+ }
+ if !bytes.Equal(data[0:4], magic[:]) {
+ return nil, xerrors.New("invalid CSCB magic")
+ }
+ if version := data[4]; version != formatVersion {
+ return nil, xerrors.Errorf("unsupported CSCB format version: %d", version)
+ }
+ codec, err := parseCodec(data[5])
+ if err != nil {
+ return nil, err
+ }
+ if scope := data[6]; scope != compressionScopeChunked {
+ return nil, xerrors.Errorf("unsupported CSCB compression scope: %d", scope)
+ }
+
+ header := &Header{
+ Codec: codec,
+ BlockCount: uint64(binary.LittleEndian.Uint32(data[8:12])),
+ ChunkCount: uint64(binary.LittleEndian.Uint32(data[12:16])),
+ StartHeight: binary.LittleEndian.Uint64(data[16:24]),
+ EndHeight: binary.LittleEndian.Uint64(data[24:32]),
+ EnvelopeOffset: binary.LittleEndian.Uint64(data[32:40]),
+ EnvelopeLength: binary.LittleEndian.Uint64(data[40:48]),
+ EnvelopeCRC32: binary.LittleEndian.Uint32(data[48:52]),
+ BlockIndexRecordSize: binary.LittleEndian.Uint32(data[52:56]),
+ ChunkIndexRecordSize: binary.LittleEndian.Uint32(data[56:60]),
+ PayloadOffset: binary.LittleEndian.Uint64(data[60:68]),
+ PayloadCompressedLength: binary.LittleEndian.Uint64(data[68:76]),
+ PayloadUncompressedLength: binary.LittleEndian.Uint64(data[76:84]),
+ PayloadCRC32: binary.LittleEndian.Uint32(data[84:88]),
+ }
+ if header.BlockCount == 0 {
+ return nil, xerrors.New("CSCB header has zero blocks")
+ }
+ if header.ChunkCount == 0 {
+ return nil, xerrors.New("CSCB header has zero chunks")
+ }
+ if header.EnvelopeOffset != HeaderSize {
+ return nil, xerrors.Errorf("unexpected CSCB envelope offset: got %d want %d", header.EnvelopeOffset, HeaderSize)
+ }
+ if header.BlockIndexRecordSize != BlockIndexRecordSize {
+ return nil, xerrors.Errorf("unexpected CSCB block record size: got %d want %d", header.BlockIndexRecordSize, BlockIndexRecordSize)
+ }
+ if header.ChunkIndexRecordSize != ChunkIndexRecordSize {
+ return nil, xerrors.Errorf("unexpected CSCB chunk record size: got %d want %d", header.ChunkIndexRecordSize, ChunkIndexRecordSize)
+ }
+ expectedPayloadOffset, err := checkedAdd(header.EnvelopeOffset, header.EnvelopeLength)
+ if err != nil {
+ return nil, err
+ }
+ if header.PayloadOffset != expectedPayloadOffset {
+ return nil, xerrors.Errorf("unexpected CSCB payload offset: got %d want %d", header.PayloadOffset, expectedPayloadOffset)
+ }
+ return header, nil
+}
+
+func ParseIndex(data []byte) (*Index, error) {
+ header, err := ParseHeader(data)
+ if err != nil {
+ return nil, err
+ }
+ required, err := HeaderEnvelopeLength(data)
+ if err != nil {
+ return nil, err
+ }
+ if required > uint64(len(data)) {
+ return nil, xerrors.Errorf("CSCB index requires %d bytes, got %d", required, len(data))
+ }
+
+ envelopeStart, err := toInt(header.EnvelopeOffset)
+ if err != nil {
+ return nil, err
+ }
+ envelopeLength, err := toInt(header.EnvelopeLength)
+ if err != nil {
+ return nil, err
+ }
+ envelope := data[envelopeStart : envelopeStart+envelopeLength]
+ if crc := crc32.ChecksumIEEE(envelope); crc != header.EnvelopeCRC32 {
+ return nil, xerrors.Errorf("CSCB envelope CRC mismatch: got %08x want %08x", crc, header.EnvelopeCRC32)
+ }
+ if len(envelope) < EnvelopeHeaderSize {
+ return nil, xerrors.Errorf("CSCB envelope requires %d bytes, got %d", EnvelopeHeaderSize, len(envelope))
+ }
+ if !bytes.Equal(envelope[0:4], envelopeMagic[:]) {
+ return nil, xerrors.New("invalid CSCB envelope magic")
+ }
+
+ blockCount := binary.LittleEndian.Uint64(envelope[8:16])
+ chunkCount := binary.LittleEndian.Uint64(envelope[16:24])
+ if blockCount != header.BlockCount {
+ return nil, xerrors.Errorf("CSCB block count mismatch: header=%d envelope=%d", header.BlockCount, blockCount)
+ }
+ if chunkCount != header.ChunkCount {
+ return nil, xerrors.Errorf("CSCB chunk count mismatch: header=%d envelope=%d", header.ChunkCount, chunkCount)
+ }
+ if startHeight := binary.LittleEndian.Uint64(envelope[24:32]); startHeight != header.StartHeight {
+ return nil, xerrors.Errorf("CSCB start height mismatch: header=%d envelope=%d", header.StartHeight, startHeight)
+ }
+ if endHeight := binary.LittleEndian.Uint64(envelope[32:40]); endHeight != header.EndHeight {
+ return nil, xerrors.Errorf("CSCB end height mismatch: header=%d envelope=%d", header.EndHeight, endHeight)
+ }
+
+ blockIndexOffset := binary.LittleEndian.Uint64(envelope[40:48])
+ blockIndexLength := binary.LittleEndian.Uint64(envelope[48:56])
+ chunkIndexOffset := binary.LittleEndian.Uint64(envelope[56:64])
+ chunkIndexLength := binary.LittleEndian.Uint64(envelope[64:72])
+ if blockIndexOffset != EnvelopeHeaderSize {
+ return nil, xerrors.Errorf("unexpected CSCB block index offset: got %d want %d", blockIndexOffset, EnvelopeHeaderSize)
+ }
+ expectedBlockIndexLength, err := checkedMul(blockCount, BlockIndexRecordSize)
+ if err != nil {
+ return nil, err
+ }
+ if blockIndexLength != expectedBlockIndexLength {
+ return nil, xerrors.Errorf("unexpected CSCB block index length: got %d want %d", blockIndexLength, expectedBlockIndexLength)
+ }
+ expectedChunkIndexOffset, err := checkedAdd(blockIndexOffset, blockIndexLength)
+ if err != nil {
+ return nil, err
+ }
+ if chunkIndexOffset != expectedChunkIndexOffset {
+ return nil, xerrors.Errorf("unexpected CSCB chunk index offset: got %d want %d", chunkIndexOffset, expectedChunkIndexOffset)
+ }
+ expectedChunkIndexLength, err := checkedMul(chunkCount, ChunkIndexRecordSize)
+ if err != nil {
+ return nil, err
+ }
+ if chunkIndexLength != expectedChunkIndexLength {
+ return nil, xerrors.Errorf("unexpected CSCB chunk index length: got %d want %d", chunkIndexLength, expectedChunkIndexLength)
+ }
+ indexEnd, err := checkedAdd(chunkIndexOffset, chunkIndexLength)
+ if err != nil {
+ return nil, err
+ }
+ if indexEnd != header.EnvelopeLength {
+ return nil, xerrors.Errorf("unexpected CSCB envelope length: index_end=%d envelope_length=%d", indexEnd, header.EnvelopeLength)
+ }
+
+ blocks, err := parseBlockDescriptors(envelope, blockIndexOffset, blockCount)
+ if err != nil {
+ return nil, err
+ }
+ chunks, err := parseChunkDescriptors(envelope, chunkIndexOffset, chunkCount)
+ if err != nil {
+ return nil, err
+ }
+ index := &Index{
+ Header: *header,
+ Blocks: blocks,
+ Chunks: chunks,
+ blocksByHeight: make(map[uint64]int, len(blocks)),
+ }
+ for i := range blocks {
+ if _, ok := index.blocksByHeight[blocks[i].Height]; ok {
+ return nil, xerrors.Errorf("duplicate CSCB block height: %d", blocks[i].Height)
+ }
+ index.blocksByHeight[blocks[i].Height] = i
+ }
+ return index, nil
+}
+
+func (i *Index) LookupBlock(metadata *api.BlockMetadata) (*BlockDescriptor, *ChunkDescriptor, error) {
+ if metadata == nil {
+ return nil, nil, xerrors.New("block metadata is required")
+ }
+ blockIndex, ok := i.blocksByHeight[metadata.GetHeight()]
+ if !ok {
+ return nil, nil, xerrors.Errorf("CSCB block height %d not found", metadata.GetHeight())
+ }
+ block := &i.Blocks[blockIndex]
+ if block.LogicalPayloadOffset != metadata.GetByteOffset() {
+ return nil, nil, xerrors.Errorf("CSCB block offset mismatch at height %d: index=%d metadata=%d", metadata.GetHeight(), block.LogicalPayloadOffset, metadata.GetByteOffset())
+ }
+ if block.PayloadLength != metadata.GetByteLength() {
+ return nil, nil, xerrors.Errorf("CSCB block length mismatch at height %d: index=%d metadata=%d", metadata.GetHeight(), block.PayloadLength, metadata.GetByteLength())
+ }
+ if metadata.GetUncompressedLength() != 0 && metadata.GetUncompressedLength() != block.PayloadLength {
+ return nil, nil, xerrors.Errorf("CSCB block uncompressed length mismatch at height %d: index=%d metadata=%d", metadata.GetHeight(), block.PayloadLength, metadata.GetUncompressedLength())
+ }
+ hashDigest := sha256.Sum256([]byte(metadata.GetHash()))
+ if hashDigest != block.HashSHA256 {
+ return nil, nil, xerrors.Errorf("CSCB block hash mismatch at height %d", metadata.GetHeight())
+ }
+ if int(block.ChunkIndex) >= len(i.Chunks) {
+ return nil, nil, xerrors.Errorf("CSCB chunk index %d out of range for height %d", block.ChunkIndex, metadata.GetHeight())
+ }
+ chunk := &i.Chunks[block.ChunkIndex]
+ if metadata.GetHeight() < chunk.StartHeight || metadata.GetHeight() >= chunk.EndHeight {
+ return nil, nil, xerrors.Errorf("CSCB block height %d outside chunk range [%d, %d)", metadata.GetHeight(), chunk.StartHeight, chunk.EndHeight)
+ }
+ return block, chunk, nil
+}
+
+func ValidateChunkPayload(chunkPayload []byte, chunk *ChunkDescriptor) error {
+ if chunk == nil {
+ return xerrors.New("CSCB chunk descriptor is required")
+ }
+ if uint64(len(chunkPayload)) != chunk.UncompressedLength {
+ return xerrors.Errorf("CSCB chunk length mismatch: got %d want %d", len(chunkPayload), chunk.UncompressedLength)
+ }
+ if crc := crc32.ChecksumIEEE(chunkPayload); crc != chunk.ChunkCRC32 {
+ return xerrors.Errorf("CSCB chunk CRC mismatch: got %08x want %08x", crc, chunk.ChunkCRC32)
+ }
+ return nil
+}
+
+func DecodeChunkFrame(frame io.Reader, codec api.Compression) ([]byte, error) {
+ reader, err := NewChunkDecompressor(frame, codec)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = reader.Close() }()
+ decoded, err := io.ReadAll(reader)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to decompress CSCB chunk: %w", err)
+ }
+ return decoded, nil
+}
+
+type blockPayloadRead struct {
+ originalIndex int
+ block *BlockDescriptor
+ start uint64
+ end uint64
+}
+
+type blockPayloadStream struct {
+ frame io.Closer
+ reader io.ReadCloser
+ block *BlockDescriptor
+ remaining uint64
+ crc hash.Hash32
+ validated bool
+ closed bool
+}
+
+// ExtractBlockPayloadsFromChunkFrame streams a compressed chunk and returns
+// only the requested block payloads in the same order as blocks. It validates
+// each requested block CRC without materializing the full decompressed chunk.
+func ExtractBlockPayloadsFromChunkFrame(frame io.Reader, codec api.Compression, chunk *ChunkDescriptor, blocks []*BlockDescriptor) ([][]byte, error) {
+ if chunk == nil {
+ return nil, xerrors.New("CSCB chunk descriptor is required")
+ }
+ if len(blocks) == 0 {
+ return nil, nil
+ }
+
+ reads := make([]blockPayloadRead, len(blocks))
+ for i, block := range blocks {
+ if block == nil {
+ return nil, xerrors.New("CSCB block descriptor is required")
+ }
+ if block.ChunkIndex != chunk.Index {
+ return nil, xerrors.Errorf("CSCB block chunk mismatch at height %d: block=%d chunk=%d", block.Height, block.ChunkIndex, chunk.Index)
+ }
+ end, err := checkedAdd(block.ChunkRelativeOffset, block.PayloadLength)
+ if err != nil {
+ return nil, err
+ }
+ if end > chunk.UncompressedLength {
+ return nil, xerrors.Errorf("CSCB block payload exceeds chunk bounds: end=%d chunk_length=%d", end, chunk.UncompressedLength)
+ }
+ reads[i] = blockPayloadRead{
+ originalIndex: i,
+ block: block,
+ start: block.ChunkRelativeOffset,
+ end: end,
+ }
+ }
+ sort.SliceStable(reads, func(i, j int) bool {
+ return reads[i].start < reads[j].start
+ })
+
+ reader, err := NewChunkDecompressor(frame, codec)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = reader.Close() }()
+
+ payloads := make([][]byte, len(blocks))
+ var offset uint64
+ var hasPrevious bool
+ var previousStart uint64
+ var previousEnd uint64
+ var previousPayload []byte
+ var previousCRC uint32
+ for _, read := range reads {
+ if read.start < offset {
+ if hasPrevious && read.start == previousStart && read.end == previousEnd {
+ if previousCRC != read.block.PayloadCRC32 {
+ return nil, xerrors.Errorf("CSCB duplicate block CRC mismatch at height %d: got %08x want %08x", read.block.Height, previousCRC, read.block.PayloadCRC32)
+ }
+ payloads[read.originalIndex] = append([]byte(nil), previousPayload...)
+ continue
+ }
+ return nil, xerrors.Errorf("CSCB block payload overlaps previous read at height %d: start=%d offset=%d", read.block.Height, read.start, offset)
+ }
+ if err := discardExactly(reader, read.start-offset); err != nil {
+ return nil, err
+ }
+
+ length, err := toInt(read.block.PayloadLength)
+ if err != nil {
+ return nil, err
+ }
+ payload := make([]byte, length)
+ if _, err := io.ReadFull(reader, payload); err != nil {
+ return nil, xerrors.Errorf("failed to read CSCB block payload at height %d: %w", read.block.Height, err)
+ }
+ offset = read.end
+ if crc := crc32.ChecksumIEEE(payload); crc != read.block.PayloadCRC32 {
+ return nil, xerrors.Errorf("CSCB block CRC mismatch at height %d: got %08x want %08x", read.block.Height, crc, read.block.PayloadCRC32)
+ } else {
+ previousCRC = crc
+ }
+ payloads[read.originalIndex] = payload
+ hasPrevious = true
+ previousStart = read.start
+ previousEnd = read.end
+ previousPayload = payload
+ }
+ return payloads, nil
+}
+
+// OpenBlockPayloadFromChunkFrame streams one block payload out of a compressed
+// CSCB chunk frame. The returned reader validates the block payload length and
+// CRC when read to EOF. Close drains unread payload bytes before closing so
+// callers that stop early can still observe validation/short-read errors.
+func OpenBlockPayloadFromChunkFrame(frame io.ReadCloser, codec api.Compression, chunk *ChunkDescriptor, block *BlockDescriptor) (io.ReadCloser, error) {
+ if frame == nil {
+ return nil, xerrors.New("CSCB chunk frame is required")
+ }
+ if chunk == nil {
+ _ = frame.Close()
+ return nil, xerrors.New("CSCB chunk descriptor is required")
+ }
+ if err := validateBlockPayloadBounds(chunk, block); err != nil {
+ _ = frame.Close()
+ return nil, err
+ }
+
+ reader, err := NewChunkDecompressor(frame, codec)
+ if err != nil {
+ _ = frame.Close()
+ return nil, err
+ }
+ if err := discardExactly(reader, block.ChunkRelativeOffset); err != nil {
+ _ = reader.Close()
+ _ = frame.Close()
+ return nil, err
+ }
+ return &blockPayloadStream{
+ frame: frame,
+ reader: reader,
+ block: block,
+ remaining: block.PayloadLength,
+ crc: crc32.NewIEEE(),
+ }, nil
+}
+
+func NewChunkDecompressor(frame io.Reader, codec api.Compression) (io.ReadCloser, error) {
+ switch codec {
+ case api.Compression_GZIP:
+ reader, err := gzip.NewReader(frame)
+ if err != nil {
+ return nil, xerrors.Errorf("gzip reader: %w", err)
+ }
+ return reader, nil
+ case api.Compression_ZSTD:
+ reader, err := zstd.NewReader(
+ frame,
+ zstd.WithDecoderConcurrency(1),
+ zstd.WithDecoderLowmem(true),
+ zstd.WithDecoderMaxMemory(maxZstdDecoderMemoryBytes),
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("zstd reader: %w", err)
+ }
+ return &zstdReadCloser{reader}, nil
+ default:
+ return nil, xerrors.Errorf("unsupported CSCB codec: %v", codec)
+ }
+}
+
+func validateBlockPayloadBounds(chunk *ChunkDescriptor, block *BlockDescriptor) error {
+ if block == nil {
+ return xerrors.New("CSCB block descriptor is required")
+ }
+ if block.ChunkIndex != chunk.Index {
+ return xerrors.Errorf("CSCB block chunk mismatch at height %d: block=%d chunk=%d", block.Height, block.ChunkIndex, chunk.Index)
+ }
+ end, err := checkedAdd(block.ChunkRelativeOffset, block.PayloadLength)
+ if err != nil {
+ return err
+ }
+ if end > chunk.UncompressedLength {
+ return xerrors.Errorf("CSCB block payload exceeds chunk bounds: end=%d chunk_length=%d", end, chunk.UncompressedLength)
+ }
+ return nil
+}
+
+func (r *blockPayloadStream) Read(p []byte) (int, error) {
+ if r.closed {
+ return 0, xerrors.New("CSCB block payload reader is closed")
+ }
+ if r.remaining == 0 {
+ if err := r.validate(); err != nil {
+ return 0, err
+ }
+ return 0, io.EOF
+ }
+ if len(p) == 0 {
+ return 0, nil
+ }
+ if uint64(len(p)) > r.remaining {
+ p = p[:int(r.remaining)]
+ }
+ n, err := r.reader.Read(p)
+ if n > 0 {
+ if _, writeErr := r.crc.Write(p[:n]); writeErr != nil {
+ return n, writeErr
+ }
+ r.remaining -= uint64(n)
+ }
+ if err != nil && r.remaining > 0 {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ return n, err
+ }
+ if r.remaining == 0 {
+ if validateErr := r.validate(); validateErr != nil {
+ return n, validateErr
+ }
+ }
+ return n, err
+}
+
+func (r *blockPayloadStream) Close() error {
+ if r.closed {
+ return nil
+ }
+ var closeErr error
+ if !r.validated {
+ if _, err := io.Copy(io.Discard, r); err != nil {
+ closeErr = err
+ }
+ }
+ r.closed = true
+ if err := r.reader.Close(); closeErr == nil && err != nil {
+ closeErr = err
+ }
+ if err := r.frame.Close(); closeErr == nil && err != nil {
+ closeErr = err
+ }
+ return closeErr
+}
+
+func (r *blockPayloadStream) validate() error {
+ if r.validated {
+ return nil
+ }
+ r.validated = true
+ if r.remaining != 0 {
+ return xerrors.Errorf("CSCB block payload length mismatch at height %d: missing %d bytes", r.block.Height, r.remaining)
+ }
+ if crc := r.crc.Sum32(); crc != r.block.PayloadCRC32 {
+ return xerrors.Errorf("CSCB block CRC mismatch at height %d: got %08x want %08x", r.block.Height, crc, r.block.PayloadCRC32)
+ }
+ return nil
+}
+
+func ExtractBlockPayload(chunkPayload []byte, block *BlockDescriptor) ([]byte, error) {
+ if block == nil {
+ return nil, xerrors.New("CSCB block descriptor is required")
+ }
+ end, err := checkedAdd(block.ChunkRelativeOffset, block.PayloadLength)
+ if err != nil {
+ return nil, err
+ }
+ if end > uint64(len(chunkPayload)) {
+ return nil, xerrors.Errorf("CSCB block payload exceeds chunk bounds: end=%d chunk_length=%d", end, len(chunkPayload))
+ }
+ startIndex, err := toInt(block.ChunkRelativeOffset)
+ if err != nil {
+ return nil, err
+ }
+ endIndex, err := toInt(end)
+ if err != nil {
+ return nil, err
+ }
+ payload := chunkPayload[startIndex:endIndex]
+ if crc := crc32.ChecksumIEEE(payload); crc != block.PayloadCRC32 {
+ return nil, xerrors.Errorf("CSCB block CRC mismatch: got %08x want %08x", crc, block.PayloadCRC32)
+ }
+ return payload, nil
+}
+
+func discardExactly(reader io.Reader, n uint64) error {
+ if n == 0 {
+ return nil
+ }
+ const maxInt64 = uint64(1<<63 - 1)
+ if n >= maxInt64 {
+ return xerrors.Errorf("discard length %d is too large", n)
+ }
+ if _, err := io.CopyN(io.Discard, reader, int64(n)); err != nil {
+ return xerrors.Errorf("failed to skip CSCB chunk bytes: %w", err)
+ }
+ return nil
+}
+
+type zstdReadCloser struct {
+ *zstd.Decoder
+}
+
+func (z *zstdReadCloser) Close() error {
+ z.Decoder.Close()
+ return nil
+}
+
+func parseBlockDescriptors(envelope []byte, offset uint64, count uint64) ([]BlockDescriptor, error) {
+ start, err := toInt(offset)
+ if err != nil {
+ return nil, err
+ }
+ countInt, err := toInt(count)
+ if err != nil {
+ return nil, err
+ }
+ blocks := make([]BlockDescriptor, countInt)
+ for i := 0; i < countInt; i++ {
+ recordStart := start + i*BlockIndexRecordSize
+ recordEnd := recordStart + BlockIndexRecordSize
+ if recordEnd > len(envelope) {
+ return nil, xerrors.Errorf("CSCB block index record %d exceeds envelope length", i)
+ }
+ record := envelope[recordStart:recordEnd]
+ var hash [32]byte
+ copy(hash[:], record[48:80])
+ blocks[i] = BlockDescriptor{
+ Height: binary.LittleEndian.Uint64(record[0:8]),
+ LogicalPayloadOffset: binary.LittleEndian.Uint64(record[8:16]),
+ PayloadLength: binary.LittleEndian.Uint64(record[16:24]),
+ PayloadCRC32: binary.LittleEndian.Uint32(record[24:28]),
+ ChunkIndex: binary.LittleEndian.Uint32(record[28:32]),
+ ChunkRelativeOffset: binary.LittleEndian.Uint64(record[32:40]),
+ HashSHA256: hash,
+ MetadataID: binary.LittleEndian.Uint64(record[80:88]),
+ }
+ }
+ return blocks, nil
+}
+
+func parseChunkDescriptors(envelope []byte, offset uint64, count uint64) ([]ChunkDescriptor, error) {
+ start, err := toInt(offset)
+ if err != nil {
+ return nil, err
+ }
+ countInt, err := toInt(count)
+ if err != nil {
+ return nil, err
+ }
+ chunks := make([]ChunkDescriptor, countInt)
+ for i := 0; i < countInt; i++ {
+ recordStart := start + i*ChunkIndexRecordSize
+ recordEnd := recordStart + ChunkIndexRecordSize
+ if recordEnd > len(envelope) {
+ return nil, xerrors.Errorf("CSCB chunk index record %d exceeds envelope length", i)
+ }
+ record := envelope[recordStart:recordEnd]
+ chunks[i] = ChunkDescriptor{
+ Index: binary.LittleEndian.Uint32(record[0:4]),
+ StartHeight: binary.LittleEndian.Uint64(record[8:16]),
+ EndHeight: binary.LittleEndian.Uint64(record[16:24]),
+ CompressedPayloadOffset: binary.LittleEndian.Uint64(record[24:32]),
+ CompressedLength: binary.LittleEndian.Uint64(record[32:40]),
+ UncompressedOffset: binary.LittleEndian.Uint64(record[40:48]),
+ UncompressedLength: binary.LittleEndian.Uint64(record[48:56]),
+ ChunkCRC32: binary.LittleEndian.Uint32(record[56:60]),
+ BlockCount: binary.LittleEndian.Uint32(record[60:64]),
+ }
+ if chunks[i].Index != uint32(i) {
+ return nil, xerrors.Errorf("unexpected CSCB chunk index: got %d want %d", chunks[i].Index, i)
+ }
+ }
+ return chunks, nil
+}
+
+func parseCodec(id byte) (api.Compression, error) {
+ switch id {
+ case codecGzip:
+ return api.Compression_GZIP, nil
+ case codecZstd:
+ return api.Compression_ZSTD, nil
+ default:
+ return api.Compression_NONE, xerrors.Errorf("unsupported CSCB codec id: %d", id)
+ }
+}
+
+func checkedAdd(a uint64, b uint64) (uint64, error) {
+ if a > ^uint64(0)-b {
+ return 0, xerrors.Errorf("uint64 overflow: %d + %d", a, b)
+ }
+ return a + b, nil
+}
+
+func checkedMul(a uint64, b uint64) (uint64, error) {
+ if a != 0 && b > ^uint64(0)/a {
+ return 0, xerrors.Errorf("uint64 overflow: %d * %d", a, b)
+ }
+ return a * b, nil
+}
+
+func toInt(v uint64) (int, error) {
+ maxInt := uint64(^uint(0) >> 1)
+ if v > maxInt {
+ return 0, xerrors.Errorf("value %d exceeds max int %d", v, maxInt)
+ }
+ return int(v), nil
+}
diff --git a/internal/storage/blobstorage/cscb/reader_test.go b/internal/storage/blobstorage/cscb/reader_test.go
new file mode 100644
index 00000000..84bcc763
--- /dev/null
+++ b/internal/storage/blobstorage/cscb/reader_test.go
@@ -0,0 +1,209 @@
+package cscb
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestParseIndexAndExtractBlockPayload(t *testing.T) {
+ require := testutil.Require(t)
+
+ object, err := Encode(context.Background(), testEncodeConfig(api.Compression_ZSTD), testPayloads())
+ require.NoError(err)
+ defer object.Close()
+
+ data, ok := object.Bytes()
+ require.True(ok)
+ index, err := ParseIndex(data)
+ require.NoError(err)
+ require.Equal(api.Compression_ZSTD, index.Header.Codec)
+ require.Len(index.Blocks, 3)
+ require.Len(index.Chunks, 2)
+
+ metadata := &api.BlockMetadata{
+ Tag: 7,
+ Height: 101,
+ Hash: "hash-101",
+ ObjectKeyMain: object.Key,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: object.Placements[1].ByteOffset,
+ ByteLength: object.Placements[1].ByteLength,
+ UncompressedLength: object.Placements[1].UncompressedLength,
+ }
+ block, chunk, err := index.LookupBlock(metadata)
+ require.NoError(err)
+ require.Equal(uint32(0), chunk.Index)
+
+ frame := data[chunk.CompressedPayloadOffset : chunk.CompressedPayloadOffset+chunk.CompressedLength]
+ chunkPayload, err := DecodeChunkFrame(bytes.NewReader(frame), index.Header.Codec)
+ require.NoError(err)
+
+ require.NoError(ValidateChunkPayload(chunkPayload, chunk))
+ payload, err := ExtractBlockPayload(chunkPayload, block)
+ require.NoError(err)
+ require.Equal([]byte("bravo-bravo"), payload)
+
+ streamedPayloads, err := ExtractBlockPayloadsFromChunkFrame(bytes.NewReader(frame), index.Header.Codec, chunk, []*BlockDescriptor{block})
+ require.NoError(err)
+ require.Len(streamedPayloads, 1)
+ require.Equal([]byte("bravo-bravo"), streamedPayloads[0])
+
+ stream, err := OpenBlockPayloadFromChunkFrame(io.NopCloser(bytes.NewReader(frame)), index.Header.Codec, chunk, block)
+ require.NoError(err)
+ buf := make([]byte, 5)
+ n, err := stream.Read(buf)
+ require.NoError(err)
+ require.Equal(5, n)
+ require.Equal([]byte("bravo"), buf)
+ require.NoError(stream.Close())
+}
+
+func TestOpenBlockPayloadFromChunkFrameReportsCRCMismatch(t *testing.T) {
+ require := testutil.Require(t)
+
+ object, err := Encode(context.Background(), testEncodeConfig(api.Compression_ZSTD), testPayloads())
+ require.NoError(err)
+ defer object.Close()
+
+ data, ok := object.Bytes()
+ require.True(ok)
+ index, err := ParseIndex(data)
+ require.NoError(err)
+
+ metadata := &api.BlockMetadata{
+ Tag: 7,
+ Height: 101,
+ Hash: "hash-101",
+ ObjectKeyMain: object.Key,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: object.Placements[1].ByteOffset,
+ ByteLength: object.Placements[1].ByteLength,
+ UncompressedLength: object.Placements[1].UncompressedLength,
+ }
+ block, chunk, err := index.LookupBlock(metadata)
+ require.NoError(err)
+ corruptBlock := *block
+ corruptBlock.PayloadCRC32 ^= 0x1
+
+ frame := data[chunk.CompressedPayloadOffset : chunk.CompressedPayloadOffset+chunk.CompressedLength]
+ stream, err := OpenBlockPayloadFromChunkFrame(io.NopCloser(bytes.NewReader(frame)), index.Header.Codec, chunk, &corruptBlock)
+ require.NoError(err)
+ _, err = io.ReadAll(stream)
+ require.Error(err)
+ require.Contains(err.Error(), "CRC mismatch")
+}
+
+func TestExtractBlockPayloadsFromChunkFramePreservesInputOrder(t *testing.T) {
+ require := testutil.Require(t)
+
+ object, err := Encode(context.Background(), testEncodeConfig(api.Compression_ZSTD), testPayloads())
+ require.NoError(err)
+ defer object.Close()
+
+ data, ok := object.Bytes()
+ require.True(ok)
+ index, err := ParseIndex(data)
+ require.NoError(err)
+ require.Len(index.Chunks, 2)
+
+ metadata0 := &api.BlockMetadata{
+ Tag: 7,
+ Height: 100,
+ Hash: "hash-100",
+ ObjectKeyMain: object.Key,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: object.Placements[0].ByteOffset,
+ ByteLength: object.Placements[0].ByteLength,
+ UncompressedLength: object.Placements[0].UncompressedLength,
+ }
+ metadata1 := &api.BlockMetadata{
+ Tag: 7,
+ Height: 101,
+ Hash: "hash-101",
+ ObjectKeyMain: object.Key,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: object.Placements[1].ByteOffset,
+ ByteLength: object.Placements[1].ByteLength,
+ UncompressedLength: object.Placements[1].UncompressedLength,
+ }
+ block0, chunk0, err := index.LookupBlock(metadata0)
+ require.NoError(err)
+ block1, chunk1, err := index.LookupBlock(metadata1)
+ require.NoError(err)
+ require.Equal(chunk0.Index, chunk1.Index)
+
+ frame := data[chunk0.CompressedPayloadOffset : chunk0.CompressedPayloadOffset+chunk0.CompressedLength]
+ payloads, err := ExtractBlockPayloadsFromChunkFrame(bytes.NewReader(frame), index.Header.Codec, chunk0, []*BlockDescriptor{block1, block0})
+ require.NoError(err)
+ require.Len(payloads, 2)
+ require.Equal([]byte("bravo-bravo"), payloads[0])
+ require.Equal([]byte("alpha"), payloads[1])
+
+ duplicatePayloads, err := ExtractBlockPayloadsFromChunkFrame(bytes.NewReader(frame), index.Header.Codec, chunk0, []*BlockDescriptor{block1, block0, block1})
+ require.NoError(err)
+ require.Len(duplicatePayloads, 3)
+ require.Equal([]byte("bravo-bravo"), duplicatePayloads[0])
+ require.Equal([]byte("alpha"), duplicatePayloads[1])
+ require.Equal([]byte("bravo-bravo"), duplicatePayloads[2])
+ duplicatePayloads[0][0] = 'B'
+ require.Equal([]byte("bravo-bravo"), duplicatePayloads[2])
+}
+
+func TestLookupBlockRejectsMismatchedMetadata(t *testing.T) {
+ require := testutil.Require(t)
+
+ object, err := Encode(context.Background(), testEncodeConfig(api.Compression_ZSTD), testPayloads())
+ require.NoError(err)
+ defer object.Close()
+
+ data, ok := object.Bytes()
+ require.True(ok)
+ index, err := ParseIndex(data)
+ require.NoError(err)
+
+ _, _, err = index.LookupBlock(&api.BlockMetadata{
+ Tag: 7,
+ Height: 101,
+ Hash: "wrong-hash",
+ ObjectKeyMain: object.Key,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: object.Placements[1].ByteOffset,
+ ByteLength: object.Placements[1].ByteLength,
+ UncompressedLength: object.Placements[1].UncompressedLength,
+ })
+ require.Error(err)
+ require.Contains(err.Error(), "hash mismatch")
+}
+
+func TestDecodeChunkFrameSupportsMaxAllowedZstdWindow(t *testing.T) {
+ require := testutil.Require(t)
+
+ windowLog := MaxZstdLongDistanceWindowLog
+ cfg := testEncodeConfig(api.Compression_ZSTD)
+ cfg.ZstdLongDistanceWindowLog = &windowLog
+ payload := []byte(strings.Repeat("solana-block-payload-", 1024))
+ blocks := testPayloads()[:1]
+ blocks[0] = makePayload(7, 100, "hash-100", 9001, payload)
+ object, err := Encode(context.Background(), cfg, blocks)
+ require.NoError(err)
+ defer object.Close()
+
+ data, ok := object.Bytes()
+ require.True(ok)
+ index, err := ParseIndex(data)
+ require.NoError(err)
+ require.Len(index.Chunks, 1)
+ chunk := &index.Chunks[0]
+ frame := data[chunk.CompressedPayloadOffset : chunk.CompressedPayloadOffset+chunk.CompressedLength]
+
+ chunkPayload, err := DecodeChunkFrame(bytes.NewReader(frame), index.Header.Codec)
+ require.NoError(err)
+ require.NoError(ValidateChunkPayload(chunkPayload, chunk))
+ require.Equal(payload, chunkPayload)
+}
diff --git a/internal/storage/blobstorage/downloader/block.go b/internal/storage/blobstorage/downloader/block.go
index ebdc7522..19e808fb 100644
--- a/internal/storage/blobstorage/downloader/block.go
+++ b/internal/storage/blobstorage/downloader/block.go
@@ -2,8 +2,14 @@ package downloader
import (
"context"
+ "fmt"
+ "io"
"io/ioutil"
"net/http"
+ "os"
+ "runtime"
+ "sort"
+ "sync"
"time"
"go.uber.org/fx"
@@ -13,18 +19,76 @@ import (
tracehttp "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http"
"github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/cscb"
"github.com/coinbase/chainstorage/internal/storage/internal/errors"
storage_utils "github.com/coinbase/chainstorage/internal/storage/utils"
"github.com/coinbase/chainstorage/internal/utils/finalizer"
"github.com/coinbase/chainstorage/internal/utils/fxparams"
"github.com/coinbase/chainstorage/internal/utils/log"
"github.com/coinbase/chainstorage/internal/utils/retry"
+ "github.com/coinbase/chainstorage/internal/utils/syncgroup"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
)
type (
BlockDownloader interface {
Download(ctx context.Context, blockFile *api.BlockFile) (*api.Block, error)
+ DownloadMany(ctx context.Context, blockFiles []*api.BlockFile) ([]*api.Block, error)
+
+ // OpenRawBlockPayload opens the validated, decompressed protobuf
+ // payload bytes for one raw block without unmarshaling api.Block.
+ // Callers MUST close the returned payload.
+ OpenRawBlockPayload(ctx context.Context, blockFile *api.BlockFile) (*RawBlockPayload, error)
+
+ // OpenRawBlockPayloads opens an iterator over validated,
+ // decompressed protobuf payload bytes in blockFiles order without
+ // unmarshaling api.Block. The iterator returns io.EOF when
+ // exhausted. Callers MUST close each returned payload and the
+ // iterator.
+ OpenRawBlockPayloads(ctx context.Context, blockFiles []*api.BlockFile) (RawBlockPayloadIterator, error)
+
+ // DownloadStream spools the compressed block to a local temp
+ // file, decompresses to a second temp file, and returns a
+ // chain-agnostic SpooledBlock handle. The decompressed spool
+ // is held open until SpooledBlock.Close() is called — callers
+ // MUST close it to release disk.
+ //
+ // This is plumbing only: no proto parsing happens at the
+ // downloader layer. Chain-specific walkers live in the
+ // parser package, which consumes the SpooledBlock to produce
+ // a StreamedBlock with chain-aware lazy accessors.
+ //
+ // A runtime cleanup is wired as a safety net for leaks but
+ // runs non-deterministically; do not rely on it.
+ DownloadStream(ctx context.Context, blockFile *api.BlockFile) (*SpooledBlock, error)
+ }
+
+ // SpooledBlock is a chain-agnostic handle over the decompressed
+ // bytes of a block, backed by a local temp file. The parser
+ // package consumes it and produces a chain-specific
+ // StreamedBlock with lazy accessors.
+ //
+ // Open may be called multiple times; each call returns an
+ // independent io.ReadCloser positioned at byte 0 of the
+ // decompressed stream. The caller is responsible for closing
+ // every returned reader.
+ //
+ // Close removes the backing temp file and is safe to call
+ // multiple times.
+ SpooledBlock struct {
+ // BlockFile is the source descriptor (chain, tag, height,
+ // hash, compression). Parser implementations branch on
+ // BlockFile.Skipped, among other fields.
+ BlockFile *api.BlockFile
+
+ // Open returns a fresh read-only handle on the decompressed
+ // spool file. Never nil for non-skipped blocks; for skipped
+ // blocks Open returns an empty reader so callers can treat
+ // the API uniformly.
+ Open func() (io.ReadCloser, error)
+
+ closeOnce sync.Once
+ closeFn func() error
}
BlockDownloaderParams struct {
@@ -38,24 +102,59 @@ type (
}
blockDownloaderImpl struct {
- config *config.Config
- logger *zap.Logger
- httpClient HTTPClient
- retry retry.RetryWithResult[*api.Block]
+ config *config.Config
+ logger *zap.Logger
+ httpClient HTTPClient
+ retry retry.RetryWithResult[*api.Block]
+ retryBytes retry.RetryWithResult[[]byte]
+ retryPayloads retry.RetryWithResult[[][]byte]
+ retryRaw retry.RetryWithResult[*RawBlockPayload]
}
+
+ downloadRef struct {
+ index int
+ blockFile *api.BlockFile
+ }
+
+ cscbBlockDownload struct {
+ ref downloadRef
+ block *cscb.BlockDescriptor
+ chunk *cscb.ChunkDescriptor
+ }
+
+ downloadLimiter chan struct{}
)
+// Close releases the locally-spooled decompressed file. Safe to call
+// multiple times; after Close, Open returns a file-not-found error.
+func (s *SpooledBlock) Close() error {
+ if s == nil {
+ return nil
+ }
+ var err error
+ s.closeOnce.Do(func() {
+ if s.closeFn != nil {
+ err = s.closeFn()
+ }
+ })
+ return err
+}
+
const (
- timeout = time.Second * 30
+ timeout = time.Second * 30
+ cscbInitialIndexReadSize = 64 * 1024
)
func NewBlockDownloader(params BlockDownloaderParams) BlockDownloader {
logger := log.WithPackage(params.Logger)
return &blockDownloaderImpl{
- config: params.Config,
- logger: logger,
- httpClient: params.HttpClient,
- retry: retry.NewWithResult[*api.Block](retry.WithLogger(logger)),
+ config: params.Config,
+ logger: logger,
+ httpClient: params.HttpClient,
+ retry: retry.NewWithResult[*api.Block](retry.WithLogger(logger)),
+ retryBytes: retry.NewWithResult[[]byte](retry.WithLogger(logger)),
+ retryPayloads: retry.NewWithResult[[][]byte](retry.WithLogger(logger)),
+ retryRaw: retry.NewWithResult[*RawBlockPayload](retry.WithLogger(logger)),
}
}
@@ -72,16 +171,16 @@ func NewHTTPClient() HTTPClient {
func (d *blockDownloaderImpl) Download(ctx context.Context, blockFile *api.BlockFile) (*api.Block, error) {
if blockFile.Skipped {
// No blob data is available when the block is skipped.
- return &api.Block{
- Blockchain: d.config.Chain.Blockchain,
- Network: d.config.Chain.Network,
- Metadata: &api.BlockMetadata{
- Tag: blockFile.Tag,
- Height: blockFile.Height,
- Skipped: true,
- },
- Blobdata: nil,
- }, nil
+ return d.skippedBlock(blockFile), nil
+ }
+
+ if isCSCBBlockFile(blockFile) {
+ defer d.logDuration(time.Now())
+ payload, err := d.downloadCSCBBlockPayload(ctx, blockFile)
+ if err != nil {
+ return nil, err
+ }
+ return unmarshalCSCBBlock(blockFile, payload)
}
defer d.logDuration(time.Now())
@@ -128,9 +227,554 @@ func (d *blockDownloaderImpl) Download(ctx context.Context, blockFile *api.Block
})
}
+func (d *blockDownloaderImpl) DownloadMany(ctx context.Context, blockFiles []*api.BlockFile) ([]*api.Block, error) {
+ defer d.logDuration(time.Now())
+
+ result := make([]*api.Block, len(blockFiles))
+ singleBlockRefs := make([]downloadRef, 0, len(blockFiles))
+ cscbRefsByURL := make(map[string][]downloadRef)
+ for i, blockFile := range blockFiles {
+ if blockFile.GetSkipped() {
+ result[i] = d.skippedBlock(blockFile)
+ continue
+ }
+ ref := downloadRef{
+ index: i,
+ blockFile: blockFile,
+ }
+ if !isCSCBBlockFile(blockFile) {
+ singleBlockRefs = append(singleBlockRefs, ref)
+ continue
+ }
+ fileURL := blockFile.GetFileUrl()
+ if fileURL == "" {
+ return nil, xerrors.Errorf("missing CSCB file url for height %d", blockFile.GetHeight())
+ }
+ cscbRefsByURL[fileURL] = append(cscbRefsByURL[fileURL], ref)
+ }
+
+ limiter := newDownloadLimiter(d.downloadWorkerLimit())
+ group, ctx := syncgroup.New(ctx)
+ for _, ref := range singleBlockRefs {
+ ref := ref
+ group.Go(func() error {
+ var block *api.Block
+ err := limiter.Do(ctx, func() error {
+ var err error
+ block, err = d.Download(ctx, ref.blockFile)
+ return err
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to download single-block file (height=%d): %w", ref.blockFile.GetHeight(), err)
+ }
+ result[ref.index] = block
+ return nil
+ })
+ }
+ for fileURL, refs := range cscbRefsByURL {
+ fileURL, refs := fileURL, refs
+ group.Go(func() error {
+ if err := d.downloadCSCBFile(ctx, limiter, fileURL, refs, result); err != nil {
+ return xerrors.Errorf("failed to download CSCB block file %s: %w", fileURL, err)
+ }
+ return nil
+ })
+ }
+ if err := group.Wait(); err != nil {
+ return nil, err
+ }
+ return result, nil
+}
+
func (c *blockDownloaderImpl) logDuration(start time.Time) {
c.logger.Debug(
"downloader.request",
zap.Duration("duration", time.Since(start)),
)
}
+
+// DownloadStream spools decompressed block bytes to a local file and
+// returns a chain-agnostic SpooledBlock handle over that file. Single-block
+// objects stream the compressed HTTP body through a decompressor
+// directly into the spool. CSCB objects range-read the target
+// compressed chunk, decode and validate that one chunk in memory, and
+// spool only the requested block payload. The spool persists until
+// SpooledBlock.Close() is called — callers MUST close it.
+//
+// There is only ONE tempfile per call (the decompressed spool). HTTP
+// retries truncate + rewind the spool and reissue the GET. This keeps
+// per-block I/O cost and tempfile churn minimal under concurrent
+// workloads.
+//
+// For skipped blocks, the returned SpooledBlock has an Open() that
+// returns an empty reader and a no-op Close; the parser detects
+// skipped blocks via BlockFile.Skipped.
+func (d *blockDownloaderImpl) DownloadStream(ctx context.Context, blockFile *api.BlockFile) (*SpooledBlock, error) {
+ if blockFile.Skipped {
+ return &SpooledBlock{
+ BlockFile: blockFile,
+ Open: func() (io.ReadCloser, error) {
+ return io.NopCloser(io.LimitReader(nil, 0)), nil
+ },
+ closeFn: func() error { return nil },
+ }, nil
+ }
+
+ defer d.logDuration(time.Now())
+
+ // Single tempfile: the decompressed spool. Ownership transfers
+ // to the returned SpooledBlock on success; removed here on any
+ // error path.
+ tmpDecompressed, err := os.CreateTemp("", "chainstorage-block-decompressed-*.bin")
+ if err != nil {
+ return nil, xerrors.Errorf("create decompressed spool: %w", err)
+ }
+ decompressedPath := tmpDecompressed.Name()
+
+ transferred := false
+ defer func() {
+ if !transferred {
+ _ = tmpDecompressed.Close()
+ _ = os.Remove(decompressedPath)
+ }
+ }()
+
+ if err := d.spoolDecompressedToFile(ctx, blockFile, tmpDecompressed); err != nil {
+ return nil, err
+ }
+ // Intentionally skip Sync: the spool is read back through the
+ // OS page cache by the same process, so flushing to disk only
+ // adds fdatasync latency under concurrent load.
+ if err := tmpDecompressed.Close(); err != nil {
+ return nil, xerrors.Errorf("close decompressed spool: %w", err)
+ }
+
+ stream := &SpooledBlock{
+ BlockFile: blockFile,
+ Open: func() (io.ReadCloser, error) {
+ f, err := os.Open(decompressedPath)
+ if err != nil {
+ return nil, xerrors.Errorf("open decompressed spool: %w", err)
+ }
+ return f, nil
+ },
+ closeFn: func() error {
+ if err := os.Remove(decompressedPath); err != nil && !os.IsNotExist(err) {
+ return xerrors.Errorf("remove decompressed spool: %w", err)
+ }
+ return nil
+ },
+ }
+
+ // Safety net: if the caller leaks the stream, GC will eventually
+ // clean up the spool file. Non-deterministic — callers must still
+ // explicitly Close().
+ runtime.AddCleanup(stream, func(path string) {
+ _ = os.Remove(path)
+ }, decompressedPath)
+
+ transferred = true
+ return stream, nil
+}
+
+// spoolDecompressedToFile performs a (retryable) HTTP GET, wraps the
+// response body in the appropriate decompressor, and writes the
+// decompressed block payload to dst. Single-block objects are streamed into
+// dst. CSCB objects buffer one decompressed chunk in memory for chunk
+// CRC validation, then write only the requested block payload to dst.
+// The file is truncated + rewound on each retry attempt. This combines
+// download + decompression into a single pass with only one tempfile,
+// reducing per-block I/O cost under concurrent workloads compared to
+// separate compressed + decompressed spools.
+func (d *blockDownloaderImpl) spoolDecompressedToFile(ctx context.Context, blockFile *api.BlockFile, dst *os.File) error {
+ if isCSCBBlockFile(blockFile) {
+ payload, err := d.downloadCSCBBlockPayload(ctx, blockFile)
+ if err != nil {
+ return err
+ }
+ if _, err := dst.Write(payload); err != nil {
+ return xerrors.Errorf("write CSCB block payload to spool: %w", err)
+ }
+ return nil
+ }
+
+ _, err := d.retry.Retry(ctx, func(ctx context.Context) (*api.Block, error) {
+ if _, err := dst.Seek(0, io.SeekStart); err != nil {
+ return nil, err
+ }
+ if err := dst.Truncate(0); err != nil {
+ return nil, err
+ }
+
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, blockFile.FileUrl, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("create request: %w", err)
+ }
+
+ resp, err := d.httpClient.Do(req)
+ if err != nil {
+ return nil, retry.Retryable(xerrors.Errorf("http do: %w", err))
+ }
+ closer := finalizer.WithCloser(resp.Body)
+ defer closer.Finalize()
+
+ if sc := resp.StatusCode; sc != http.StatusOK {
+ if sc == http.StatusRequestTimeout || sc == http.StatusTooManyRequests || sc >= http.StatusInternalServerError {
+ return nil, retry.Retryable(xerrors.Errorf("status %d: %w", sc, errors.ErrDownloadFailure))
+ }
+ return nil, xerrors.Errorf("status %d: %w", sc, errors.ErrDownloadFailure)
+ }
+
+ dec, err := storage_utils.DecompressReader(resp.Body, blockFile.Compression)
+ if err != nil {
+ return nil, xerrors.Errorf("wrap decompressor: %w", err)
+ }
+ defer func() { _ = dec.Close() }()
+
+ if _, err := io.Copy(dst, dec); err != nil {
+ return nil, retry.Retryable(xerrors.Errorf("decompress to spool: %w", err))
+ }
+ return nil, closer.Close()
+ })
+ return err
+}
+
+func (d *blockDownloaderImpl) downloadCSCBBlockPayload(ctx context.Context, blockFile *api.BlockFile) ([]byte, error) {
+ index, err := d.readCSCBIndex(ctx, nil, blockFile.GetFileUrl())
+ if err != nil {
+ return nil, err
+ }
+ block, chunk, err := index.LookupBlock(blockFileToMetadata(blockFile))
+ if err != nil {
+ return nil, err
+ }
+ blockPayloads, err := d.downloadCSCBChunkPayloads(ctx, blockFile.GetFileUrl(), index.Header.Codec, chunk, []*cscb.BlockDescriptor{block})
+ if err != nil {
+ return nil, err
+ }
+ return blockPayloads[0], nil
+}
+
+func (d *blockDownloaderImpl) downloadCSCBFile(ctx context.Context, limiter downloadLimiter, fileURL string, refs []downloadRef, result []*api.Block) error {
+ index, err := d.readCSCBIndex(ctx, limiter, fileURL)
+ if err != nil {
+ return err
+ }
+
+ downloadsByChunk := make(map[uint32][]cscbBlockDownload)
+ for _, ref := range refs {
+ metadata := blockFileToMetadata(ref.blockFile)
+ block, chunk, err := index.LookupBlock(metadata)
+ if err != nil {
+ return err
+ }
+ downloadsByChunk[chunk.Index] = append(downloadsByChunk[chunk.Index], cscbBlockDownload{
+ ref: ref,
+ block: block,
+ chunk: chunk,
+ })
+ }
+
+ chunkIndexes := make([]int, 0, len(downloadsByChunk))
+ for chunkIndex := range downloadsByChunk {
+ chunkIndexes = append(chunkIndexes, int(chunkIndex))
+ }
+ sort.Ints(chunkIndexes)
+
+ group, ctx := syncgroup.New(ctx)
+ for _, chunkIndex := range chunkIndexes {
+ downloads := downloadsByChunk[uint32(chunkIndex)]
+ group.Go(func() error {
+ return limiter.Do(ctx, func() error {
+ chunk := downloads[0].chunk
+ blocks := make([]*cscb.BlockDescriptor, len(downloads))
+ for i, download := range downloads {
+ blocks[i] = download.block
+ }
+ blockPayloads, err := d.downloadCSCBChunkPayloads(ctx, fileURL, index.Header.Codec, chunk, blocks)
+ if err != nil {
+ return err
+ }
+ for i, download := range downloads {
+ blockPayload := blockPayloads[i]
+ block, err := unmarshalCSCBBlock(download.ref.blockFile, blockPayload)
+ if err != nil {
+ return err
+ }
+ result[download.ref.index] = block
+ }
+ return nil
+ })
+ })
+ }
+ if err := group.Wait(); err != nil {
+ return err
+ }
+ return nil
+}
+
+func (d *blockDownloaderImpl) readCSCBIndex(ctx context.Context, limiter downloadLimiter, fileURL string) (*cscb.Index, error) {
+ first, err := d.readHTTPRange(ctx, limiter, fileURL, 0, cscbInitialIndexReadSize-1)
+ if err != nil {
+ return nil, err
+ }
+ required, err := cscb.HeaderEnvelopeLength(first)
+ if err != nil {
+ return nil, err
+ }
+ if required <= uint64(len(first)) {
+ return cscb.ParseIndex(first)
+ }
+ remaining, err := d.readHTTPRange(ctx, limiter, fileURL, uint64(len(first)), required-1)
+ if err != nil {
+ return nil, err
+ }
+ indexData := make([]byte, 0, required)
+ indexData = append(indexData, first...)
+ indexData = append(indexData, remaining...)
+ return cscb.ParseIndex(indexData)
+}
+
+func (d *blockDownloaderImpl) downloadCSCBChunkPayloads(ctx context.Context, fileURL string, codec api.Compression, chunk *cscb.ChunkDescriptor, blocks []*cscb.BlockDescriptor) ([][]byte, error) {
+ if len(blocks) == 0 {
+ return nil, xerrors.New("CSCB block descriptors are required")
+ }
+ return d.retryPayloads.Retry(ctx, func(ctx context.Context) ([][]byte, error) {
+ payloads, err := d.downloadCSCBChunkPayloadsUnthrottled(ctx, fileURL, codec, chunk, blocks)
+ if err != nil && (xerrors.Is(err, io.EOF) || xerrors.Is(err, io.ErrUnexpectedEOF)) {
+ return nil, retry.Retryable(err)
+ }
+ return payloads, err
+ })
+}
+
+func (d *blockDownloaderImpl) downloadCSCBChunkPayloadsUnthrottled(ctx context.Context, fileURL string, codec api.Compression, chunk *cscb.ChunkDescriptor, blocks []*cscb.BlockDescriptor) ([][]byte, error) {
+ body, err := d.openHTTPRangeByLengthUnthrottled(ctx, fileURL, chunk.CompressedPayloadOffset, chunk.CompressedLength)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = body.Close() }()
+
+ limitedBody, err := limitReaderByLength(body, chunk.CompressedLength)
+ if err != nil {
+ return nil, err
+ }
+ return cscb.ExtractBlockPayloadsFromChunkFrame(limitedBody, codec, chunk, blocks)
+}
+
+func (d *blockDownloaderImpl) readHTTPRange(ctx context.Context, limiter downloadLimiter, fileURL string, start uint64, end uint64) ([]byte, error) {
+ if end < start {
+ return nil, xerrors.Errorf("invalid range for %s: start=%d end=%d", fileURL, start, end)
+ }
+ var body []byte
+ err := limiter.Do(ctx, func() error {
+ var err error
+ body, err = d.readHTTPRangeUnthrottled(ctx, fileURL, start, end)
+ return err
+ })
+ return body, err
+}
+
+func (d *blockDownloaderImpl) readHTTPRangeUnthrottled(ctx context.Context, fileURL string, start uint64, end uint64) ([]byte, error) {
+ return d.retryBytes.Retry(ctx, func(ctx context.Context) ([]byte, error) {
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create range download request: %w", err)
+ }
+ req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
+
+ httpResp, err := d.httpClient.Do(req)
+ if err != nil {
+ return nil, retry.Retryable(xerrors.Errorf("failed to range download block file: %w", err))
+ }
+
+ finalizer := finalizer.WithCloser(httpResp.Body)
+ defer finalizer.Finalize()
+
+ ignoredInitialRange := false
+ if statusCode := httpResp.StatusCode; statusCode != http.StatusPartialContent {
+ if statusCode == http.StatusOK && start == 0 {
+ // Some test transports or non-S3-compatible stores may
+ // ignore Range for the initial index read. Treat that as
+ // usable because the object starts at byte 0 and contains
+ // at least the header/envelope we need.
+ ignoredInitialRange = true
+ } else if statusCode == http.StatusRequestTimeout ||
+ statusCode == http.StatusTooManyRequests ||
+ statusCode >= http.StatusInternalServerError {
+ return nil, retry.Retryable(xerrors.Errorf("received %d status code: %w", statusCode, errors.ErrDownloadFailure))
+ } else {
+ return nil, xerrors.Errorf("received non-retryable %d status code: %w", statusCode, errors.ErrDownloadFailure)
+ }
+ }
+
+ expectedLength := end - start + 1
+ var bodyBytes []byte
+ var readErr error
+ if ignoredInitialRange {
+ bodyBytes, readErr = readRangePrefixBody(httpResp.Body, expectedLength)
+ } else {
+ bodyBytes, readErr = readExpectedRangeBody(httpResp.Body, expectedLength, start == 0)
+ }
+ if readErr != nil {
+ return nil, retry.Retryable(xerrors.Errorf("failed to read range body: %w", readErr))
+ }
+ return bodyBytes, finalizer.Close()
+ })
+}
+
+func (d *blockDownloaderImpl) openHTTPRangeByLengthUnthrottled(ctx context.Context, fileURL string, offset uint64, length uint64) (io.ReadCloser, error) {
+ if length == 0 {
+ return nil, xerrors.Errorf("empty range read for %s at offset %d", fileURL, offset)
+ }
+ end, err := inclusiveRangeEnd(offset, length)
+ if err != nil {
+ return nil, err
+ }
+ return d.openHTTPRangeUnthrottled(ctx, fileURL, offset, end)
+}
+
+func (d *blockDownloaderImpl) openHTTPRangeUnthrottled(ctx context.Context, fileURL string, start uint64, end uint64) (io.ReadCloser, error) {
+ if end < start {
+ return nil, xerrors.Errorf("invalid range for %s: start=%d end=%d", fileURL, start, end)
+ }
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileURL, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create range download request: %w", err)
+ }
+ req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", start, end))
+
+ httpResp, err := d.httpClient.Do(req)
+ if err != nil {
+ return nil, retry.Retryable(xerrors.Errorf("failed to range download block file: %w", err))
+ }
+ if statusCode := httpResp.StatusCode; statusCode != http.StatusPartialContent {
+ if httpResp.Body != nil {
+ _ = httpResp.Body.Close()
+ }
+ if statusCode == http.StatusRequestTimeout ||
+ statusCode == http.StatusTooManyRequests ||
+ statusCode >= http.StatusInternalServerError {
+ return nil, retry.Retryable(xerrors.Errorf("received %d status code: %w", statusCode, errors.ErrDownloadFailure))
+ }
+ return nil, xerrors.Errorf("received non-retryable %d status code: %w", statusCode, errors.ErrDownloadFailure)
+ }
+ if httpResp.Body == nil {
+ return nil, xerrors.Errorf("empty range body for %s", fileURL)
+ }
+ return httpResp.Body, nil
+}
+
+func limitReaderByLength(reader io.Reader, length uint64) (io.Reader, error) {
+ const maxInt64 = uint64(1<<63 - 1)
+ if length >= maxInt64 {
+ return nil, xerrors.Errorf("range body length %d is too large", length)
+ }
+ return io.LimitReader(reader, int64(length)), nil
+}
+
+func readRangePrefixBody(body io.Reader, maxLength uint64) ([]byte, error) {
+ const maxInt64 = uint64(1<<63 - 1)
+ if maxLength >= maxInt64 {
+ return nil, xerrors.Errorf("range body length %d is too large", maxLength)
+ }
+ return io.ReadAll(io.LimitReader(body, int64(maxLength)))
+}
+
+func readExpectedRangeBody(body io.Reader, expectedLength uint64, allowShort bool) ([]byte, error) {
+ const maxInt64 = uint64(1<<63 - 1)
+ if expectedLength >= maxInt64 {
+ return nil, xerrors.Errorf("range body length %d is too large", expectedLength)
+ }
+ bodyBytes, err := io.ReadAll(io.LimitReader(body, int64(expectedLength)+1))
+ if err != nil {
+ return nil, err
+ }
+ if uint64(len(bodyBytes)) > expectedLength || (!allowShort && uint64(len(bodyBytes)) != expectedLength) {
+ return nil, xerrors.Errorf("range body length mismatch: got %d want %d", len(bodyBytes), expectedLength)
+ }
+ return bodyBytes, nil
+}
+
+func newDownloadLimiter(limit int) downloadLimiter {
+ if limit <= 0 {
+ return nil
+ }
+ return make(downloadLimiter, limit)
+}
+
+func (l downloadLimiter) Do(ctx context.Context, fn func() error) error {
+ if l == nil {
+ return fn()
+ }
+ select {
+ case l <- struct{}{}:
+ defer func() { <-l }()
+ return fn()
+ case <-ctx.Done():
+ return ctx.Err()
+ }
+}
+
+func isCSCBBlockFile(blockFile *api.BlockFile) bool {
+ return blockFile.GetObjectFormat() == api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH && blockFile.GetByteLength() != 0
+}
+
+func (d *blockDownloaderImpl) skippedBlock(blockFile *api.BlockFile) *api.Block {
+ return &api.Block{
+ Blockchain: d.config.Chain.Blockchain,
+ Network: d.config.Chain.Network,
+ SideChain: d.config.Chain.Sidechain,
+ Metadata: &api.BlockMetadata{
+ Tag: blockFile.GetTag(),
+ Height: blockFile.GetHeight(),
+ Skipped: true,
+ },
+ Blobdata: nil,
+ }
+}
+
+func blockFileToMetadata(blockFile *api.BlockFile) *api.BlockMetadata {
+ return &api.BlockMetadata{
+ Tag: blockFile.GetTag(),
+ Hash: blockFile.GetHash(),
+ ParentHash: blockFile.GetParentHash(),
+ Height: blockFile.GetHeight(),
+ ParentHeight: blockFile.GetParentHeight(),
+ Skipped: blockFile.GetSkipped(),
+ Timestamp: blockFile.GetBlockTimestamp(),
+ ObjectFormat: blockFile.GetObjectFormat(),
+ ByteOffset: blockFile.GetByteOffset(),
+ ByteLength: blockFile.GetByteLength(),
+ UncompressedLength: blockFile.GetUncompressedLength(),
+ }
+}
+
+func unmarshalCSCBBlock(blockFile *api.BlockFile, blockData []byte) (*api.Block, error) {
+ var block api.Block
+ if err := proto.Unmarshal(blockData, &block); err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal CSCB block payload: %w", err)
+ }
+ block.Metadata = blockFileToMetadata(blockFile)
+ return &block, nil
+}
+
+func (d *blockDownloaderImpl) downloadWorkerLimit() int {
+ if limit := int(d.config.SDK.NumWorkers); limit > 0 {
+ return limit
+ }
+ if limit := int(d.config.Api.NumWorkers); limit > 0 {
+ return limit
+ }
+ return 1
+}
+
+func inclusiveRangeEnd(offset uint64, length uint64) (uint64, error) {
+ if length == 0 {
+ return 0, xerrors.New("range length must be positive")
+ }
+ if offset > ^uint64(0)-(length-1) {
+ return 0, xerrors.Errorf("range overflow: offset=%d length=%d", offset, length)
+ }
+ return offset + length - 1, nil
+}
diff --git a/internal/storage/blobstorage/downloader/block_test.go b/internal/storage/blobstorage/downloader/block_test.go
index 6d2c225a..37f296f8 100644
--- a/internal/storage/blobstorage/downloader/block_test.go
+++ b/internal/storage/blobstorage/downloader/block_test.go
@@ -1,10 +1,15 @@
package downloader
import (
+ "bytes"
"context"
+ "fmt"
+ "io"
"net/http"
"net/http/httptest"
+ "sync"
"testing"
+ "time"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/suite"
@@ -13,6 +18,9 @@ import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/cscb"
+ blobstorageinternal "github.com/coinbase/chainstorage/internal/storage/blobstorage/internal"
"github.com/coinbase/chainstorage/internal/storage/internal/errors"
storage_utils "github.com/coinbase/chainstorage/internal/storage/utils"
"github.com/coinbase/chainstorage/internal/utils/testapp"
@@ -59,6 +67,38 @@ func TestBlockDownloaderSuite(t *testing.T) {
suite.Run(t, new(blockDownloaderTestSuite))
}
+func TestReadExpectedRangeBodyAllowsShortInitialRead(t *testing.T) {
+ require := testutil.Require(t)
+
+ actual, err := readExpectedRangeBody(bytes.NewReader([]byte("short")), 64*1024, true)
+ require.NoError(err)
+ require.Equal([]byte("short"), actual)
+}
+
+func TestReadExpectedRangeBodyRejectsShortNonInitialRead(t *testing.T) {
+ require := testutil.Require(t)
+
+ _, err := readExpectedRangeBody(bytes.NewReader([]byte("short")), 10, false)
+ require.Error(err)
+ require.Contains(err.Error(), "range body length mismatch")
+}
+
+func TestReadExpectedRangeBodyRejectsOversizedRead(t *testing.T) {
+ require := testutil.Require(t)
+
+ _, err := readExpectedRangeBody(bytes.NewReader([]byte("too-long")), 3, true)
+ require.Error(err)
+ require.Contains(err.Error(), "range body length mismatch")
+}
+
+func TestReadRangePrefixBodyTruncatesOversizedRead(t *testing.T) {
+ require := testutil.Require(t)
+
+ actual, err := readRangePrefixBody(bytes.NewReader([]byte("too-long")), 3)
+ require.NoError(err)
+ require.Equal([]byte("too"), actual)
+}
+
func (s *blockDownloaderTestSuite) SetupTest() {
s.app = testapp.New(
s.T(),
@@ -79,7 +119,9 @@ func (s *blockDownloaderTestSuite) SetupTest() {
}
func (s *blockDownloaderTestSuite) TearDownTest() {
- s.httpServer.Close()
+ if s.httpServer != nil {
+ s.httpServer.Close()
+ }
s.app.Close()
}
@@ -168,6 +210,609 @@ func (s *blockDownloaderTestSuite) TestSkipped() {
}
}
+func (s *blockDownloaderTestSuite) TestDownloadStream_Success() {
+ require := testutil.Require(s.T())
+
+ bitcoinBlock := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_BITCOIN,
+ Network: common.Network_NETWORK_BITCOIN_MAINNET,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 123, Hash: "0xabc"},
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: []byte(`{"hash":"0xabc","height":123,"tx":[]}`),
+ },
+ },
+ }
+ blockBytes, err := proto.Marshal(bitcoinBlock)
+ require.NoError(err)
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(s.newHttpServerFunc(http.MethodGet, http.StatusOK, blockBytes)),
+ fx.Populate(&s.httpServer),
+ fx.Provide(s.newHttpClientFunc()),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ spooled, err := s.downloader.DownloadStream(context.Background(), s.blockFile)
+ require.NoError(err)
+ defer spooled.Close()
+
+ require.Equal(s.blockFile, spooled.BlockFile)
+
+ // Open should yield the full decompressed proto bytes (no
+ // compression in this test since the HTTP body is already raw).
+ rc, err := spooled.Open()
+ require.NoError(err)
+ defer rc.Close()
+ gotBytes, err := io.ReadAll(rc)
+ require.NoError(err)
+ require.Equal(blockBytes, gotBytes)
+}
+
+func (s *blockDownloaderTestSuite) TestDownloadStream_Skipped() {
+ require := testutil.Require(s.T())
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ fx.Provide(s.newHttpClientFunc()),
+ )
+
+ spooled, err := s.downloader.DownloadStream(context.Background(), s.skippedBlockFile)
+ require.NoError(err)
+ defer spooled.Close()
+
+ require.True(spooled.BlockFile.GetSkipped())
+ rc, err := spooled.Open()
+ require.NoError(err)
+ defer rc.Close()
+ got, err := io.ReadAll(rc)
+ require.NoError(err)
+ require.Empty(got)
+}
+
+func (s *blockDownloaderTestSuite) TestDownloadStream_CloseRemovesSpool() {
+ require := testutil.Require(s.T())
+
+ bitcoinBlock := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_BITCOIN,
+ Network: common.Network_NETWORK_BITCOIN_MAINNET,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 42, Hash: "0xabc"},
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{Header: []byte(`{"hash":"0xabc"}`)},
+ },
+ }
+ blockBytes, err := proto.Marshal(bitcoinBlock)
+ require.NoError(err)
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(s.newHttpServerFunc(http.MethodGet, http.StatusOK, blockBytes)),
+ fx.Populate(&s.httpServer),
+ fx.Provide(s.newHttpClientFunc()),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ spooled, err := s.downloader.DownloadStream(context.Background(), s.blockFile)
+ require.NoError(err)
+
+ // Open works before Close.
+ rc, err := spooled.Open()
+ require.NoError(err)
+ require.NoError(rc.Close())
+
+ require.NoError(spooled.Close())
+ // Second Close must be a no-op.
+ require.NoError(spooled.Close())
+
+ // After Close, Open fails (spool removed).
+ _, err = spooled.Open()
+ require.Error(err)
+}
+
+func (s *blockDownloaderTestSuite) TestDownload_CSCB() {
+ require := testutil.Require(s.T())
+ fixture := newCSCBDownloaderFixture(s.T(), 3, 2)
+ defer fixture.close()
+
+ server, recorder := newRangeHTTPServer(fixture.data)
+ defer server.Close()
+ for _, file := range fixture.blockFiles {
+ file.FileUrl = server.URL
+ }
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(func() HTTPClient { return server.Client() }),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ rawBlock, err := s.downloader.Download(context.Background(), fixture.blockFiles[1])
+ require.NoError(err)
+ expected := proto.Clone(fixture.blocks[1]).(*api.Block)
+ expected.Metadata = blockFileToMetadata(fixture.blockFiles[1])
+ if diff := cmp.Diff(expected, rawBlock, protocmp.Transform()); diff != "" {
+ require.FailNow(diff)
+ }
+
+ ranges := recorder.rangesSnapshot()
+ require.Equal([]string{
+ "bytes=0-65535",
+ rangeHeader(fixture.index.Chunks[0].CompressedPayloadOffset, fixture.index.Chunks[0].CompressedLength),
+ }, ranges)
+}
+
+func (s *blockDownloaderTestSuite) TestDownload_CSCBInitialIndexIgnoresRangeWithBoundedPrefix() {
+ require := testutil.Require(s.T())
+ fixture := newCSCBDownloaderFixture(s.T(), 3, 2)
+ defer fixture.close()
+
+ data := append([]byte{}, fixture.data...)
+ if len(data) <= 64*1024 {
+ data = append(data, bytes.Repeat([]byte{0}, 64*1024-len(data)+1)...)
+ }
+ server, recorder := newInitialRangeIgnoringHTTPServer(data)
+ defer server.Close()
+ for _, file := range fixture.blockFiles {
+ file.FileUrl = server.URL
+ }
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(func() HTTPClient { return server.Client() }),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ rawBlock, err := s.downloader.Download(context.Background(), fixture.blockFiles[1])
+ require.NoError(err)
+ expected := proto.Clone(fixture.blocks[1]).(*api.Block)
+ expected.Metadata = blockFileToMetadata(fixture.blockFiles[1])
+ if diff := cmp.Diff(expected, rawBlock, protocmp.Transform()); diff != "" {
+ require.FailNow(diff)
+ }
+
+ ranges := recorder.rangesSnapshot()
+ require.Equal([]string{
+ "bytes=0-65535",
+ rangeHeader(fixture.index.Chunks[0].CompressedPayloadOffset, fixture.index.Chunks[0].CompressedLength),
+ }, ranges)
+}
+
+func (s *blockDownloaderTestSuite) TestDownloadMany_CSCBGroupsByChunk() {
+ require := testutil.Require(s.T())
+ fixture := newCSCBDownloaderFixture(s.T(), 4, 2)
+ defer fixture.close()
+
+ server, recorder := newRangeHTTPServer(fixture.data)
+ defer server.Close()
+ for _, file := range fixture.blockFiles {
+ file.FileUrl = server.URL
+ }
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(func() HTTPClient { return server.Client() }),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ blockFiles := []*api.BlockFile{
+ fixture.blockFiles[1],
+ fixture.blockFiles[0],
+ fixture.blockFiles[3],
+ }
+ rawBlocks, err := s.downloader.DownloadMany(context.Background(), blockFiles)
+ require.NoError(err)
+ require.Len(rawBlocks, len(blockFiles))
+
+ for i, blockFile := range blockFiles {
+ sourceIndex := int(blockFile.GetHeight() - 100)
+ expected := proto.Clone(fixture.blocks[sourceIndex]).(*api.Block)
+ expected.Metadata = blockFileToMetadata(blockFile)
+ if diff := cmp.Diff(expected, rawBlocks[i], protocmp.Transform()); diff != "" {
+ require.FailNow(diff)
+ }
+ }
+
+ ranges := recorder.rangesSnapshot()
+ require.ElementsMatch([]string{
+ "bytes=0-65535",
+ rangeHeader(fixture.index.Chunks[0].CompressedPayloadOffset, fixture.index.Chunks[0].CompressedLength),
+ rangeHeader(fixture.index.Chunks[1].CompressedPayloadOffset, fixture.index.Chunks[1].CompressedLength),
+ }, ranges)
+}
+
+func (s *blockDownloaderTestSuite) TestDownloadMany_CSCBAllowsDuplicateInputs() {
+ require := testutil.Require(s.T())
+ fixture := newCSCBDownloaderFixture(s.T(), 4, 2)
+ defer fixture.close()
+
+ server, recorder := newRangeHTTPServer(fixture.data)
+ defer server.Close()
+ for _, file := range fixture.blockFiles {
+ file.FileUrl = server.URL
+ }
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(func() HTTPClient { return server.Client() }),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ blockFiles := []*api.BlockFile{
+ fixture.blockFiles[1],
+ fixture.blockFiles[1],
+ }
+ rawBlocks, err := s.downloader.DownloadMany(context.Background(), blockFiles)
+ require.NoError(err)
+ require.Len(rawBlocks, len(blockFiles))
+
+ for i, blockFile := range blockFiles {
+ expected := proto.Clone(fixture.blocks[1]).(*api.Block)
+ expected.Metadata = blockFileToMetadata(blockFile)
+ if diff := cmp.Diff(expected, rawBlocks[i], protocmp.Transform()); diff != "" {
+ require.FailNow(diff)
+ }
+ }
+
+ ranges := recorder.rangesSnapshot()
+ require.Equal([]string{
+ "bytes=0-65535",
+ rangeHeader(fixture.index.Chunks[0].CompressedPayloadOffset, fixture.index.Chunks[0].CompressedLength),
+ }, ranges)
+}
+
+func (s *blockDownloaderTestSuite) TestDownloadMany_CSCBLimitsConcurrencyAcrossFiles() {
+ require := testutil.Require(s.T())
+ const workerLimit = 2
+
+ fixtureA := newCSCBDownloaderFixture(s.T(), 4, 1)
+ defer fixtureA.close()
+ fixtureB := newCSCBDownloaderFixture(s.T(), 4, 1)
+ defer fixtureB.close()
+ tracker := &rangeConcurrencyTracker{}
+
+ serverA, _ := newRangeHTTPServerWithTracker(fixtureA.data, tracker, 20*time.Millisecond)
+ defer serverA.Close()
+ serverB, _ := newRangeHTTPServerWithTracker(fixtureB.data, tracker, 20*time.Millisecond)
+ defer serverB.Close()
+ for _, file := range fixtureA.blockFiles {
+ file.FileUrl = serverA.URL
+ }
+ for _, file := range fixtureB.blockFiles {
+ file.FileUrl = serverB.URL
+ }
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Invoke(func(cfg *config.Config) {
+ cfg.SDK.NumWorkers = workerLimit
+ cfg.Api.NumWorkers = workerLimit
+ }),
+ fx.Provide(func() HTTPClient { return serverA.Client() }),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ blockFiles := append([]*api.BlockFile{}, fixtureA.blockFiles...)
+ blockFiles = append(blockFiles, fixtureB.blockFiles...)
+ rawBlocks, err := s.downloader.DownloadMany(context.Background(), blockFiles)
+ require.NoError(err)
+ require.Len(rawBlocks, len(blockFiles))
+ require.LessOrEqual(tracker.peakActive(), workerLimit)
+}
+
+func (s *blockDownloaderTestSuite) TestDownloadMany_MixedSingleBlockAndCSCBPreservesOrder() {
+ require := testutil.Require(s.T())
+ fixture := newCSCBDownloaderFixture(s.T(), 2, 2)
+ defer fixture.close()
+
+ cscbServer, _ := newRangeHTTPServer(fixture.data)
+ defer cscbServer.Close()
+ for _, file := range fixture.blockFiles {
+ file.FileUrl = cscbServer.URL
+ }
+
+ singleBlock := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ETHEREUM,
+ Network: common.Network_NETWORK_ETHEREUM_MAINNET,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 900, Hash: "single-block-hash"},
+ }
+ singleBlockPayload, err := proto.Marshal(singleBlock)
+ require.NoError(err)
+ singleBlockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ require.Equal(http.MethodGet, r.Method)
+ _, _ = w.Write(singleBlockPayload)
+ }))
+ defer singleBlockServer.Close()
+
+ singleBlockFile := &api.BlockFile{
+ Tag: 1,
+ Height: 900,
+ Hash: "single-block-hash",
+ FileUrl: singleBlockServer.URL,
+ }
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(func() HTTPClient { return cscbServer.Client() }),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ rawBlocks, err := s.downloader.DownloadMany(context.Background(), []*api.BlockFile{
+ fixture.blockFiles[1],
+ singleBlockFile,
+ fixture.blockFiles[0],
+ })
+ require.NoError(err)
+ require.Len(rawBlocks, 3)
+
+ expectedFirst := proto.Clone(fixture.blocks[1]).(*api.Block)
+ expectedFirst.Metadata = blockFileToMetadata(fixture.blockFiles[1])
+ expectedThird := proto.Clone(fixture.blocks[0]).(*api.Block)
+ expectedThird.Metadata = blockFileToMetadata(fixture.blockFiles[0])
+ if diff := cmp.Diff(expectedFirst, rawBlocks[0], protocmp.Transform()); diff != "" {
+ require.FailNow(diff)
+ }
+ if diff := cmp.Diff(singleBlock, rawBlocks[1], protocmp.Transform()); diff != "" {
+ require.FailNow(diff)
+ }
+ if diff := cmp.Diff(expectedThird, rawBlocks[2], protocmp.Transform()); diff != "" {
+ require.FailNow(diff)
+ }
+}
+
+func (s *blockDownloaderTestSuite) TestDownloadStream_CSCB() {
+ require := testutil.Require(s.T())
+ fixture := newCSCBDownloaderFixture(s.T(), 2, 2)
+ defer fixture.close()
+
+ server, _ := newRangeHTTPServer(fixture.data)
+ defer server.Close()
+ for _, file := range fixture.blockFiles {
+ file.FileUrl = server.URL
+ }
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(func() HTTPClient { return server.Client() }),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ spooled, err := s.downloader.DownloadStream(context.Background(), fixture.blockFiles[1])
+ require.NoError(err)
+ defer spooled.Close()
+
+ rc, err := spooled.Open()
+ require.NoError(err)
+ defer rc.Close()
+ gotBytes, err := io.ReadAll(rc)
+ require.NoError(err)
+
+ var rawBlock api.Block
+ require.NoError(proto.Unmarshal(gotBytes, &rawBlock))
+ expected := proto.Clone(fixture.blocks[1]).(*api.Block)
+ if diff := cmp.Diff(expected, &rawBlock, protocmp.Transform()); diff != "" {
+ require.FailNow(diff)
+ }
+}
+
+func (s *blockDownloaderTestSuite) TestOpenRawBlockPayload_SingleBlockGzip() {
+ require := testutil.Require(s.T())
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(s.newHttpServerFunc(http.MethodGet, http.StatusOK, expectedBlockCompressedBytes)),
+ fx.Populate(&s.httpServer),
+ fx.Provide(s.newHttpClientFunc()),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+ s.blockFile.Compression = api.Compression_GZIP
+ s.blockFile.UncompressedLength = uint64(len(expectedBlockBytes))
+
+ payload, err := s.downloader.OpenRawBlockPayload(context.Background(), s.blockFile)
+ require.NoError(err)
+ defer payload.Close()
+ require.Equal(s.blockFile, payload.BlockFile)
+ require.Equal(uint64(len(expectedBlockBytes)), payload.Length)
+
+ got, err := io.ReadAll(payload)
+ require.NoError(err)
+ require.Equal(expectedBlockBytes, got)
+}
+
+func (s *blockDownloaderTestSuite) TestOpenRawBlockPayload_SingleBlockDetectsShortPayload() {
+ require := testutil.Require(s.T())
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(s.newHttpServerFunc(http.MethodGet, http.StatusOK, expectedBlockBytes[:len(expectedBlockBytes)-1])),
+ fx.Populate(&s.httpServer),
+ fx.Provide(s.newHttpClientFunc()),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+ s.blockFile.UncompressedLength = uint64(len(expectedBlockBytes))
+
+ payload, err := s.downloader.OpenRawBlockPayload(context.Background(), s.blockFile)
+ require.NoError(err)
+ defer payload.Close()
+
+ _, err = io.ReadAll(payload)
+ require.Error(err)
+ require.Contains(err.Error(), "length mismatch")
+}
+
+func (s *blockDownloaderTestSuite) TestOpenRawBlockPayload_SingleBlockDetectsOversizedPayload() {
+ require := testutil.Require(s.T())
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(s.newHttpServerFunc(http.MethodGet, http.StatusOK, append(append([]byte(nil), expectedBlockBytes...), []byte("extra")...))),
+ fx.Populate(&s.httpServer),
+ fx.Provide(s.newHttpClientFunc()),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+ s.blockFile.UncompressedLength = uint64(len(expectedBlockBytes))
+
+ payload, err := s.downloader.OpenRawBlockPayload(context.Background(), s.blockFile)
+ require.NoError(err)
+ defer payload.Close()
+
+ _, err = io.ReadAll(payload)
+ require.Error(err)
+ require.Contains(err.Error(), "exceeds expected length")
+}
+
+func (s *blockDownloaderTestSuite) TestOpenRawBlockPayload_CSCBStreamsSinglePayload() {
+ require := testutil.Require(s.T())
+ fixture := newCSCBDownloaderFixture(s.T(), 2, 2)
+ defer fixture.close()
+
+ server, recorder := newRangeHTTPServer(fixture.data)
+ defer server.Close()
+ for _, file := range fixture.blockFiles {
+ file.FileUrl = server.URL
+ }
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(func() HTTPClient { return server.Client() }),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ payload, err := s.downloader.OpenRawBlockPayload(context.Background(), fixture.blockFiles[1])
+ require.NoError(err)
+ defer payload.Close()
+ require.Equal(fixture.blockFiles[1], payload.BlockFile)
+ require.Equal(fixture.blockFiles[1].GetByteLength(), payload.Length)
+
+ gotBytes, err := io.ReadAll(payload)
+ require.NoError(err)
+ var rawBlock api.Block
+ require.NoError(proto.Unmarshal(gotBytes, &rawBlock))
+ expected := proto.Clone(fixture.blocks[1]).(*api.Block)
+ if diff := cmp.Diff(expected, &rawBlock, protocmp.Transform()); diff != "" {
+ require.FailNow(diff)
+ }
+
+ require.Equal([]string{
+ "bytes=0-65535",
+ rangeHeader(fixture.index.Chunks[0].CompressedPayloadOffset, fixture.index.Chunks[0].CompressedLength),
+ }, recorder.rangesSnapshot())
+}
+
+func (s *blockDownloaderTestSuite) TestOpenRawBlockPayloads_CSCBGroupsByChunk() {
+ require := testutil.Require(s.T())
+ fixture := newCSCBDownloaderFixture(s.T(), 4, 2)
+ defer fixture.close()
+
+ server, recorder := newRangeHTTPServer(fixture.data)
+ defer server.Close()
+ for _, file := range fixture.blockFiles {
+ file.FileUrl = server.URL
+ }
+
+ s.app = testapp.New(
+ s.T(),
+ fx.Provide(func() HTTPClient { return server.Client() }),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&s.downloader),
+ )
+
+ blockFiles := []*api.BlockFile{
+ fixture.blockFiles[1],
+ fixture.blockFiles[0],
+ fixture.blockFiles[3],
+ }
+ iter, err := s.downloader.OpenRawBlockPayloads(context.Background(), blockFiles)
+ require.NoError(err)
+ defer iter.Close()
+
+ for i, blockFile := range blockFiles {
+ payload, err := iter.Next(context.Background())
+ require.NoError(err)
+ require.Equal(blockFile, payload.BlockFile)
+ gotBytes, err := io.ReadAll(payload)
+ require.NoError(err)
+ require.NoError(payload.Close())
+
+ var rawBlock api.Block
+ require.NoError(proto.Unmarshal(gotBytes, &rawBlock))
+ sourceIndex := int(blockFile.GetHeight() - 100)
+ expected := proto.Clone(fixture.blocks[sourceIndex]).(*api.Block)
+ if diff := cmp.Diff(expected, &rawBlock, protocmp.Transform()); diff != "" {
+ require.FailNow(fmt.Sprintf("payload %d mismatch: %s", i, diff))
+ }
+ }
+ _, err = iter.Next(context.Background())
+ require.ErrorIs(err, io.EOF)
+
+ require.ElementsMatch([]string{
+ "bytes=0-65535",
+ rangeHeader(fixture.index.Chunks[0].CompressedPayloadOffset, fixture.index.Chunks[0].CompressedLength),
+ rangeHeader(fixture.index.Chunks[1].CompressedPayloadOffset, fixture.index.Chunks[1].CompressedLength),
+ }, recorder.rangesSnapshot())
+}
+
+func BenchmarkOpenRawBlockPayload_CSCB_vs_Download(b *testing.B) {
+ fixture := newCSCBDownloaderFixtureWithPayloadSize(b, 2, 2, 4*1024*1024)
+ defer fixture.close()
+
+ server, _ := newRangeHTTPServer(fixture.data)
+ defer server.Close()
+ for _, file := range fixture.blockFiles {
+ file.FileUrl = server.URL
+ }
+
+ var dl BlockDownloader
+ app := testapp.New(
+ b,
+ fx.Provide(func() HTTPClient { return server.Client() }),
+ fx.Provide(NewBlockDownloader),
+ fx.Populate(&dl),
+ )
+ defer app.Close()
+
+ blockFile := fixture.blockFiles[1]
+ b.Run("download_unmarshal", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ block, err := dl.Download(context.Background(), blockFile)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if got := len(block.GetSolana().GetHeader()); got == 0 {
+ b.Fatalf("expected solana payload, got %d bytes", got)
+ }
+ }
+ })
+ b.Run("open_raw_payload", func(b *testing.B) {
+ b.ReportAllocs()
+ for i := 0; i < b.N; i++ {
+ payload, err := dl.OpenRawBlockPayload(context.Background(), blockFile)
+ if err != nil {
+ b.Fatal(err)
+ }
+ if _, err := io.Copy(io.Discard, payload); err != nil {
+ _ = payload.Close()
+ b.Fatal(err)
+ }
+ if err := payload.Close(); err != nil {
+ b.Fatal(err)
+ }
+ }
+ })
+}
+
func (s *blockDownloaderTestSuite) newHttpClientFunc() httpClientFunc {
return func() HTTPClient {
return s.httpServer.Client()
@@ -189,3 +834,224 @@ func (s *blockDownloaderTestSuite) newHttpServerFunc(httpMethod string, respStat
return server
}
}
+
+type cscbDownloaderFixture struct {
+ object *cscb.Object
+ data []byte
+ index *cscb.Index
+ blocks []*api.Block
+ blockFiles []*api.BlockFile
+}
+
+func newCSCBDownloaderFixture(t *testing.T, blockCount int, chunkBlocks uint64) *cscbDownloaderFixture {
+ return newCSCBDownloaderFixtureWithPayloadSize(t, blockCount, chunkBlocks, 0)
+}
+
+func newCSCBDownloaderFixtureWithPayloadSize(t testing.TB, blockCount int, chunkBlocks uint64, payloadSize int) *cscbDownloaderFixture {
+ t.Helper()
+ require := testutil.Require(t)
+
+ blocks := make([]*api.Block, blockCount)
+ payloads := make([]blobstorageinternal.ConsolidatedBlockPayload, blockCount)
+ for i := 0; i < blockCount; i++ {
+ height := uint64(100 + i)
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ETHEREUM,
+ Network: common.Network_NETWORK_ETHEREUM_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Hash: fmt.Sprintf("hash-%d", height),
+ ParentHash: fmt.Sprintf("hash-%d", height-1),
+ Height: height,
+ ParentHeight: height - 1,
+ },
+ }
+ if payloadSize > 0 {
+ block.Blockchain = common.Blockchain_BLOCKCHAIN_SOLANA
+ block.Network = common.Network_NETWORK_SOLANA_MAINNET
+ block.Blobdata = &api.Block_Solana{
+ Solana: &api.SolanaBlobdata{
+ Header: bytes.Repeat([]byte{byte('a' + i)}, payloadSize),
+ },
+ }
+ }
+ blockBytes, err := proto.Marshal(block)
+ require.NoError(err)
+ blocks[i] = block
+ payloads[i] = blobstorageinternal.ConsolidatedBlockPayload{
+ Metadata: block.Metadata,
+ MetadataID: int64(i + 1),
+ RawBlockPayload: blobstorageinternal.BytesPayloadSource(blockBytes),
+ UncompressedLength: uint64(len(blockBytes)),
+ }
+ }
+
+ object, err := cscb.Encode(context.Background(), cscb.EncodeConfig{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ETHEREUM,
+ Network: common.Network_NETWORK_ETHEREUM_MAINNET,
+ Codec: api.Compression_ZSTD,
+ CodecLevel: 1,
+ MaxBlocks: uint64(blockCount),
+ CompressionChunkBlocks: chunkBlocks,
+ ShardSize: 10_000,
+ }, payloads)
+ require.NoError(err)
+ data, ok := object.Bytes()
+ require.True(ok)
+ index, err := cscb.ParseIndex(data)
+ require.NoError(err)
+
+ blockFiles := make([]*api.BlockFile, blockCount)
+ for i, placement := range object.Placements {
+ source := blocks[i].GetMetadata()
+ blockFiles[i] = &api.BlockFile{
+ Tag: source.GetTag(),
+ Hash: source.GetHash(),
+ ParentHash: source.GetParentHash(),
+ Height: source.GetHeight(),
+ ParentHeight: source.GetParentHeight(),
+ Compression: api.Compression_ZSTD,
+ ObjectFormat: placement.ObjectFormat,
+ ByteOffset: placement.ByteOffset,
+ ByteLength: placement.ByteLength,
+ UncompressedLength: placement.UncompressedLength,
+ }
+ }
+
+ return &cscbDownloaderFixture{
+ object: object,
+ data: data,
+ index: index,
+ blocks: blocks,
+ blockFiles: blockFiles,
+ }
+}
+
+func (f *cscbDownloaderFixture) close() {
+ if f != nil && f.object != nil {
+ _ = f.object.Close()
+ }
+}
+
+type rangeRequestRecorder struct {
+ mu sync.Mutex
+ ranges []string
+ tracker *rangeConcurrencyTracker
+ delay time.Duration
+}
+
+type rangeConcurrencyTracker struct {
+ mu sync.Mutex
+ active int
+ peak int
+}
+
+func newRangeHTTPServer(data []byte) (*httptest.Server, *rangeRequestRecorder) {
+ return newRangeHTTPServerWithTracker(data, nil, 0)
+}
+
+func newInitialRangeIgnoringHTTPServer(data []byte) (*httptest.Server, *rangeRequestRecorder) {
+ recorder := &rangeRequestRecorder{}
+ server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ if request.Method != http.MethodGet {
+ writer.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ rangeValue := request.Header.Get("Range")
+ done := recorder.record(rangeValue)
+ defer done()
+ if rangeValue == "bytes=0-65535" {
+ _, _ = writer.Write(data)
+ return
+ }
+
+ writeRangeResponse(writer, rangeValue, data)
+ }))
+ return server, recorder
+}
+
+func newRangeHTTPServerWithTracker(data []byte, tracker *rangeConcurrencyTracker, delay time.Duration) (*httptest.Server, *rangeRequestRecorder) {
+ recorder := &rangeRequestRecorder{
+ tracker: tracker,
+ delay: delay,
+ }
+ server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ if request.Method != http.MethodGet {
+ writer.WriteHeader(http.StatusMethodNotAllowed)
+ return
+ }
+
+ rangeValue := request.Header.Get("Range")
+ done := recorder.record(rangeValue)
+ defer done()
+ if recorder.delay > 0 {
+ time.Sleep(recorder.delay)
+ }
+ if rangeValue == "" {
+ _, _ = writer.Write(data)
+ return
+ }
+
+ writeRangeResponse(writer, rangeValue, data)
+ }))
+ return server, recorder
+}
+
+func writeRangeResponse(writer http.ResponseWriter, rangeValue string, data []byte) {
+ var start, end uint64
+ if _, err := fmt.Sscanf(rangeValue, "bytes=%d-%d", &start, &end); err != nil {
+ writer.WriteHeader(http.StatusBadRequest)
+ return
+ }
+ if start >= uint64(len(data)) || end < start {
+ writer.WriteHeader(http.StatusRequestedRangeNotSatisfiable)
+ return
+ }
+ if end >= uint64(len(data)) {
+ end = uint64(len(data)) - 1
+ }
+ writer.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(data)))
+ writer.WriteHeader(http.StatusPartialContent)
+ _, _ = writer.Write(data[start : end+1])
+}
+
+func (r *rangeRequestRecorder) record(rangeValue string) func() {
+ r.mu.Lock()
+ r.ranges = append(r.ranges, rangeValue)
+ r.mu.Unlock()
+ if r.tracker == nil {
+ return func() {}
+ }
+ return r.tracker.begin()
+}
+
+func (r *rangeRequestRecorder) rangesSnapshot() []string {
+ r.mu.Lock()
+ defer r.mu.Unlock()
+ return append([]string(nil), r.ranges...)
+}
+
+func (t *rangeConcurrencyTracker) begin() func() {
+ t.mu.Lock()
+ t.active++
+ if t.active > t.peak {
+ t.peak = t.active
+ }
+ t.mu.Unlock()
+ return func() {
+ t.mu.Lock()
+ t.active--
+ t.mu.Unlock()
+ }
+}
+
+func (t *rangeConcurrencyTracker) peakActive() int {
+ t.mu.Lock()
+ defer t.mu.Unlock()
+ return t.peak
+}
+
+func rangeHeader(offset uint64, length uint64) string {
+ return fmt.Sprintf("bytes=%d-%d", offset, offset+length-1)
+}
diff --git a/internal/storage/blobstorage/downloader/mocks/mocks.go b/internal/storage/blobstorage/downloader/mocks/mocks.go
index 90f087c9..3976727a 100644
--- a/internal/storage/blobstorage/downloader/mocks/mocks.go
+++ b/internal/storage/blobstorage/downloader/mocks/mocks.go
@@ -13,6 +13,7 @@ import (
context "context"
reflect "reflect"
+ downloader "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
chainstorage "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
gomock "go.uber.org/mock/gomock"
)
@@ -54,3 +55,63 @@ func (mr *MockBlockDownloaderMockRecorder) Download(arg0, arg1 any) *gomock.Call
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockBlockDownloader)(nil).Download), arg0, arg1)
}
+
+// DownloadMany mocks base method.
+func (m *MockBlockDownloader) DownloadMany(arg0 context.Context, arg1 []*chainstorage.BlockFile) ([]*chainstorage.Block, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DownloadMany", arg0, arg1)
+ ret0, _ := ret[0].([]*chainstorage.Block)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// DownloadMany indicates an expected call of DownloadMany.
+func (mr *MockBlockDownloaderMockRecorder) DownloadMany(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadMany", reflect.TypeOf((*MockBlockDownloader)(nil).DownloadMany), arg0, arg1)
+}
+
+// DownloadStream mocks base method.
+func (m *MockBlockDownloader) DownloadStream(arg0 context.Context, arg1 *chainstorage.BlockFile) (*downloader.SpooledBlock, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DownloadStream", arg0, arg1)
+ ret0, _ := ret[0].(*downloader.SpooledBlock)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// DownloadStream indicates an expected call of DownloadStream.
+func (mr *MockBlockDownloaderMockRecorder) DownloadStream(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadStream", reflect.TypeOf((*MockBlockDownloader)(nil).DownloadStream), arg0, arg1)
+}
+
+// OpenRawBlockPayload mocks base method.
+func (m *MockBlockDownloader) OpenRawBlockPayload(arg0 context.Context, arg1 *chainstorage.BlockFile) (*downloader.RawBlockPayload, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "OpenRawBlockPayload", arg0, arg1)
+ ret0, _ := ret[0].(*downloader.RawBlockPayload)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// OpenRawBlockPayload indicates an expected call of OpenRawBlockPayload.
+func (mr *MockBlockDownloaderMockRecorder) OpenRawBlockPayload(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenRawBlockPayload", reflect.TypeOf((*MockBlockDownloader)(nil).OpenRawBlockPayload), arg0, arg1)
+}
+
+// OpenRawBlockPayloads mocks base method.
+func (m *MockBlockDownloader) OpenRawBlockPayloads(arg0 context.Context, arg1 []*chainstorage.BlockFile) (downloader.RawBlockPayloadIterator, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "OpenRawBlockPayloads", arg0, arg1)
+ ret0, _ := ret[0].(downloader.RawBlockPayloadIterator)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// OpenRawBlockPayloads indicates an expected call of OpenRawBlockPayloads.
+func (mr *MockBlockDownloaderMockRecorder) OpenRawBlockPayloads(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenRawBlockPayloads", reflect.TypeOf((*MockBlockDownloader)(nil).OpenRawBlockPayloads), arg0, arg1)
+}
diff --git a/internal/storage/blobstorage/downloader/raw_payload.go b/internal/storage/blobstorage/downloader/raw_payload.go
new file mode 100644
index 00000000..f013db22
--- /dev/null
+++ b/internal/storage/blobstorage/downloader/raw_payload.go
@@ -0,0 +1,417 @@
+package downloader
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "net/http"
+ "sync"
+
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/cscb"
+ "github.com/coinbase/chainstorage/internal/storage/internal/errors"
+ storage_utils "github.com/coinbase/chainstorage/internal/storage/utils"
+ "github.com/coinbase/chainstorage/internal/utils/retry"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ // RawBlockPayload is an open reader for one validated, decompressed
+ // protobuf-serialized api.Block payload. It deliberately does not unmarshal
+ // the payload.
+ RawBlockPayload struct {
+ BlockFile *api.BlockFile
+ Length uint64
+
+ reader io.ReadCloser
+ closeOnce sync.Once
+ }
+
+ // RawBlockPayloadIterator yields raw block payloads in input order and
+ // returns io.EOF when exhausted. The caller owns each returned payload and
+ // must close it before advancing if it wants bounded resources.
+ RawBlockPayloadIterator interface {
+ Next(ctx context.Context) (*RawBlockPayload, error)
+ Close() error
+ }
+
+ rawBlockPayloadIteratorImpl struct {
+ downloader *blockDownloaderImpl
+ blockFiles []*api.BlockFile
+ current int
+ pending []*RawBlockPayload
+ indexByURL map[string]*cscb.Index
+ closed bool
+ }
+
+ multiCloserReadCloser struct {
+ io.Reader
+ closers []io.Closer
+ closeOnce sync.Once
+ }
+
+ exactLengthReadCloser struct {
+ reader io.ReadCloser
+ height uint64
+ remaining uint64
+ validated bool
+ closed bool
+ }
+
+ limitedReadCloser struct {
+ io.Reader
+ closer io.Closer
+ }
+)
+
+func (d *blockDownloaderImpl) OpenRawBlockPayload(ctx context.Context, blockFile *api.BlockFile) (*RawBlockPayload, error) {
+ if blockFile == nil {
+ return nil, xerrors.New("block file is required")
+ }
+ if blockFile.GetSkipped() {
+ return newBytesRawBlockPayload(blockFile, nil), nil
+ }
+ if isCSCBBlockFile(blockFile) {
+ return d.openRawCSCBBlockPayload(ctx, blockFile)
+ }
+ return d.retryRaw.Retry(ctx, func(ctx context.Context) (*RawBlockPayload, error) {
+ payload, err := d.openRawSingleBlockPayload(ctx, blockFile)
+ if err != nil && (xerrors.Is(err, io.EOF) || xerrors.Is(err, io.ErrUnexpectedEOF)) {
+ return nil, retry.Retryable(err)
+ }
+ return payload, err
+ })
+}
+
+func (d *blockDownloaderImpl) OpenRawBlockPayloads(ctx context.Context, blockFiles []*api.BlockFile) (RawBlockPayloadIterator, error) {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ return &rawBlockPayloadIteratorImpl{
+ downloader: d,
+ blockFiles: blockFiles,
+ indexByURL: make(map[string]*cscb.Index),
+ }, nil
+}
+
+func (p *RawBlockPayload) Read(buf []byte) (int, error) {
+ if p == nil || p.reader == nil {
+ return 0, xerrors.New("raw block payload is closed")
+ }
+ return p.reader.Read(buf)
+}
+
+func (p *RawBlockPayload) Close() error {
+ if p == nil {
+ return nil
+ }
+ var err error
+ p.closeOnce.Do(func() {
+ if p.reader != nil {
+ err = p.reader.Close()
+ }
+ p.reader = nil
+ })
+ return err
+}
+
+func (i *rawBlockPayloadIteratorImpl) Next(ctx context.Context) (*RawBlockPayload, error) {
+ if i.closed {
+ return nil, xerrors.New("raw block payload iterator is closed")
+ }
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ if len(i.pending) > 0 {
+ next := i.pending[0]
+ i.pending = i.pending[1:]
+ return next, nil
+ }
+ if i.current >= len(i.blockFiles) {
+ return nil, io.EOF
+ }
+
+ blockFile := i.blockFiles[i.current]
+ if blockFile.GetSkipped() || !isCSCBBlockFile(blockFile) {
+ i.current++
+ return i.downloader.OpenRawBlockPayload(ctx, blockFile)
+ }
+ if err := i.loadNextCSCBGroup(ctx); err != nil {
+ return nil, err
+ }
+ return i.Next(ctx)
+}
+
+func (i *rawBlockPayloadIteratorImpl) Close() error {
+ if i.closed {
+ return nil
+ }
+ i.closed = true
+ var closeErr error
+ for _, payload := range i.pending {
+ if err := payload.Close(); closeErr == nil && err != nil {
+ closeErr = err
+ }
+ }
+ i.pending = nil
+ return closeErr
+}
+
+func (i *rawBlockPayloadIteratorImpl) loadNextCSCBGroup(ctx context.Context) error {
+ firstFile := i.blockFiles[i.current]
+ fileURL := firstFile.GetFileUrl()
+ if fileURL == "" {
+ return xerrors.Errorf("missing CSCB file url for height %d", firstFile.GetHeight())
+ }
+ index, err := i.getCSCBIndex(ctx, fileURL)
+ if err != nil {
+ return err
+ }
+ firstBlock, firstChunk, err := index.LookupBlock(blockFileToMetadata(firstFile))
+ if err != nil {
+ return err
+ }
+
+ downloads := []cscbBlockDownload{{
+ ref: downloadRef{
+ index: i.current,
+ blockFile: firstFile,
+ },
+ block: firstBlock,
+ chunk: firstChunk,
+ }}
+ nextIndex := i.current + 1
+ for nextIndex < len(i.blockFiles) {
+ nextFile := i.blockFiles[nextIndex]
+ if nextFile.GetSkipped() || !isCSCBBlockFile(nextFile) || nextFile.GetFileUrl() != fileURL {
+ break
+ }
+ nextBlock, nextChunk, err := index.LookupBlock(blockFileToMetadata(nextFile))
+ if err != nil {
+ return err
+ }
+ if nextChunk.Index != firstChunk.Index {
+ break
+ }
+ downloads = append(downloads, cscbBlockDownload{
+ ref: downloadRef{
+ index: nextIndex,
+ blockFile: nextFile,
+ },
+ block: nextBlock,
+ chunk: nextChunk,
+ })
+ nextIndex++
+ }
+
+ blocks := make([]*cscb.BlockDescriptor, len(downloads))
+ for j, download := range downloads {
+ blocks[j] = download.block
+ }
+ payloads, err := i.downloader.downloadCSCBChunkPayloads(ctx, fileURL, index.Header.Codec, firstChunk, blocks)
+ if err != nil {
+ return err
+ }
+ if len(payloads) != len(downloads) {
+ return xerrors.Errorf("CSCB raw payload count mismatch: got %d want %d", len(payloads), len(downloads))
+ }
+
+ i.pending = make([]*RawBlockPayload, len(payloads))
+ for j, payload := range payloads {
+ i.pending[j] = newBytesRawBlockPayload(downloads[j].ref.blockFile, payload)
+ }
+ i.current = nextIndex
+ return nil
+}
+
+func (i *rawBlockPayloadIteratorImpl) getCSCBIndex(ctx context.Context, fileURL string) (*cscb.Index, error) {
+ if index, ok := i.indexByURL[fileURL]; ok {
+ return index, nil
+ }
+ index, err := i.downloader.readCSCBIndex(ctx, nil, fileURL)
+ if err != nil {
+ return nil, err
+ }
+ i.indexByURL[fileURL] = index
+ return index, nil
+}
+
+func (d *blockDownloaderImpl) openRawSingleBlockPayload(ctx context.Context, blockFile *api.BlockFile) (*RawBlockPayload, error) {
+ req, err := http.NewRequestWithContext(ctx, http.MethodGet, blockFile.GetFileUrl(), nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create download request: %w", err)
+ }
+
+ httpResp, err := d.httpClient.Do(req)
+ if err != nil {
+ return nil, retry.Retryable(xerrors.Errorf("failed to download block file: %w", err))
+ }
+ if statusCode := httpResp.StatusCode; statusCode != http.StatusOK {
+ if httpResp.Body != nil {
+ _ = httpResp.Body.Close()
+ }
+ if statusCode == http.StatusRequestTimeout ||
+ statusCode == http.StatusTooManyRequests ||
+ statusCode >= http.StatusInternalServerError {
+ return nil, retry.Retryable(xerrors.Errorf("received %d status code: %w", statusCode, errors.ErrDownloadFailure))
+ }
+ return nil, xerrors.Errorf("received non-retryable %d status code: %w", statusCode, errors.ErrDownloadFailure)
+ }
+ if httpResp.Body == nil {
+ return nil, xerrors.Errorf("empty body for %s", blockFile.GetFileUrl())
+ }
+ decoder, err := storage_utils.DecompressReader(httpResp.Body, blockFile.GetCompression())
+ if err != nil {
+ _ = httpResp.Body.Close()
+ return nil, xerrors.Errorf("wrap decompressor: %w", err)
+ }
+ reader := io.ReadCloser(&multiCloserReadCloser{
+ Reader: decoder,
+ closers: []io.Closer{decoder, httpResp.Body},
+ })
+ length := blockFile.GetUncompressedLength()
+ if length != 0 {
+ reader = &exactLengthReadCloser{
+ reader: reader,
+ height: blockFile.GetHeight(),
+ remaining: length,
+ }
+ }
+ return newRawBlockPayload(blockFile, length, reader), nil
+}
+
+func (d *blockDownloaderImpl) openRawCSCBBlockPayload(ctx context.Context, blockFile *api.BlockFile) (*RawBlockPayload, error) {
+ fileURL := blockFile.GetFileUrl()
+ if fileURL == "" {
+ return nil, xerrors.Errorf("missing CSCB file url for height %d", blockFile.GetHeight())
+ }
+ index, err := d.readCSCBIndex(ctx, nil, fileURL)
+ if err != nil {
+ return nil, err
+ }
+ block, chunk, err := index.LookupBlock(blockFileToMetadata(blockFile))
+ if err != nil {
+ return nil, err
+ }
+ return d.retryRaw.Retry(ctx, func(ctx context.Context) (*RawBlockPayload, error) {
+ body, err := d.openHTTPRangeByLengthUnthrottled(ctx, fileURL, chunk.CompressedPayloadOffset, chunk.CompressedLength)
+ if err != nil {
+ return nil, err
+ }
+ limitedBody, err := limitReaderByLength(body, chunk.CompressedLength)
+ if err != nil {
+ _ = body.Close()
+ return nil, err
+ }
+ reader, err := cscb.OpenBlockPayloadFromChunkFrame(&limitedReadCloser{
+ Reader: limitedBody,
+ closer: body,
+ }, index.Header.Codec, chunk, block)
+ if err != nil {
+ return nil, err
+ }
+ return newRawBlockPayload(blockFile, block.PayloadLength, reader), nil
+ })
+}
+
+func newBytesRawBlockPayload(blockFile *api.BlockFile, payload []byte) *RawBlockPayload {
+ return newRawBlockPayload(blockFile, uint64(len(payload)), io.NopCloser(bytes.NewReader(payload)))
+}
+
+func newRawBlockPayload(blockFile *api.BlockFile, length uint64, reader io.ReadCloser) *RawBlockPayload {
+ return &RawBlockPayload{
+ BlockFile: blockFile,
+ Length: length,
+ reader: reader,
+ }
+}
+
+func (r *multiCloserReadCloser) Close() error {
+ var closeErr error
+ r.closeOnce.Do(func() {
+ for _, closer := range r.closers {
+ if closer == nil {
+ continue
+ }
+ if err := closer.Close(); closeErr == nil && err != nil {
+ closeErr = err
+ }
+ }
+ })
+ return closeErr
+}
+
+func (r *exactLengthReadCloser) Read(buf []byte) (int, error) {
+ if r.closed {
+ return 0, xerrors.New("single-block raw payload reader is closed")
+ }
+ if r.remaining == 0 {
+ return 0, r.validateEOF()
+ }
+ if len(buf) == 0 {
+ return 0, nil
+ }
+ if uint64(len(buf)) > r.remaining {
+ buf = buf[:int(r.remaining)]
+ }
+
+ n, err := r.reader.Read(buf)
+ if n > 0 {
+ r.remaining -= uint64(n)
+ }
+ if err != nil {
+ if r.remaining > 0 {
+ if err == io.EOF {
+ err = io.ErrUnexpectedEOF
+ }
+ return n, xerrors.Errorf("single-block raw payload length mismatch at height %d: missing %d bytes: %w", r.height, r.remaining, err)
+ }
+ if err == io.EOF {
+ r.validated = true
+ }
+ return n, err
+ }
+ return n, nil
+}
+
+func (r *exactLengthReadCloser) Close() error {
+ if r.closed {
+ return nil
+ }
+ var closeErr error
+ if !r.validated {
+ if _, err := io.Copy(io.Discard, r); err != nil {
+ closeErr = err
+ }
+ }
+ r.closed = true
+ if err := r.reader.Close(); closeErr == nil && err != nil {
+ closeErr = err
+ }
+ return closeErr
+}
+
+func (r *exactLengthReadCloser) validateEOF() error {
+ if r.validated {
+ return io.EOF
+ }
+ var extra [1]byte
+ n, err := r.reader.Read(extra[:])
+ if n > 0 {
+ r.validated = true
+ return xerrors.Errorf("single-block raw payload length mismatch at height %d: payload exceeds expected length", r.height)
+ }
+ if err == nil {
+ return nil
+ }
+ if err == io.EOF {
+ r.validated = true
+ return io.EOF
+ }
+ return err
+}
+
+func (r *limitedReadCloser) Close() error {
+ return r.closer.Close()
+}
diff --git a/internal/storage/blobstorage/gcs/blob_storage.go b/internal/storage/blobstorage/gcs/blob_storage.go
index 5b1528cd..79e4ca0f 100644
--- a/internal/storage/blobstorage/gcs/blob_storage.go
+++ b/internal/storage/blobstorage/gcs/blob_storage.go
@@ -22,6 +22,7 @@ import (
"github.com/coinbase/chainstorage/internal/utils/fxparams"
"github.com/coinbase/chainstorage/internal/utils/instrument"
"github.com/coinbase/chainstorage/internal/utils/log"
+ "github.com/coinbase/chainstorage/internal/utils/syncgroup"
"github.com/coinbase/chainstorage/protos/coinbase/c3/common"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
)
@@ -61,18 +62,18 @@ const (
blobSizeMetricName = "blob_size"
)
-var _ internal.BlobStorage = (*blobStorageImpl)(nil)
+var _ internal.BlobStorageCore = (*blobStorageImpl)(nil)
-func NewFactory(params BlobStorageParams) internal.BlobStorageFactory {
+func newFactory(params BlobStorageParams) internal.BlobStorageFactory {
return &blobStorageFactory{params}
}
// Create implements BlobStorageFactory.
-func (f *blobStorageFactory) Create() (internal.BlobStorage, error) {
- return New(f.params)
+func (f *blobStorageFactory) Create() (internal.BlobStorageCore, error) {
+ return newBlobStorage(f.params)
}
-func New(params BlobStorageParams) (internal.BlobStorage, error) {
+func newBlobStorage(params BlobStorageParams) (internal.BlobStorageCore, error) {
metrics := params.Metrics.SubScope("blob_storage").Tagged(map[string]string{
"storage_type": "gcs",
})
@@ -211,6 +212,10 @@ func (s *blobStorageImpl) Upload(ctx context.Context, block *api.Block, compress
})
}
+func (s *blobStorageImpl) UploadConsolidated(ctx context.Context, blocks []internal.ConsolidatedBlockPayload) (string, []internal.BlockPlacement, error) {
+ return "", nil, xerrors.New("consolidated block upload is not supported by GCS blob storage")
+}
+
func (s *blobStorageImpl) Download(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
return s.instrumentDownload.Instrument(ctx, func(ctx context.Context) (*api.Block, error) {
defer s.logDuration("download", time.Now())
@@ -263,6 +268,35 @@ func (s *blobStorageImpl) Download(ctx context.Context, metadata *api.BlockMetad
})
}
+func (s *blobStorageImpl) DownloadMany(ctx context.Context, metadatas []*api.BlockMetadata) ([]*api.Block, error) {
+ blocks := make([]*api.Block, len(metadatas))
+ group, ctx := syncgroup.New(ctx, syncgroup.WithThrottling(s.downloadWorkerLimit()))
+ for i := range metadatas {
+ i := i
+ group.Go(func() error {
+ metadata := metadatas[i]
+ block, err := s.Download(ctx, metadata)
+ if err != nil {
+ return xerrors.Errorf("failed to download from GCS blob storage (input={%+v}): %w", metadata, err)
+ }
+ blocks[i] = block
+ return nil
+ })
+ }
+ if err := group.Wait(); err != nil {
+ return nil, err
+ }
+ return blocks, nil
+}
+
+func (s *blobStorageImpl) downloadWorkerLimit() int {
+ limit := int(s.config.Api.NumWorkers)
+ if limit <= 0 {
+ return 1
+ }
+ return limit
+}
+
// PreSign implements internal.BlobStorage.
func (s *blobStorageImpl) PreSign(ctx context.Context, objectKey string) (string, error) {
fileUrl, err := s.client.Bucket(s.bucket).SignedURL(objectKey, &storage.SignedURLOptions{
diff --git a/internal/storage/blobstorage/gcs/blob_storage_integration_test.go b/internal/storage/blobstorage/gcs/blob_storage_integration_test.go
index 3c7dae2d..49bb1bdd 100644
--- a/internal/storage/blobstorage/gcs/blob_storage_integration_test.go
+++ b/internal/storage/blobstorage/gcs/blob_storage_integration_test.go
@@ -19,13 +19,13 @@ import (
type gcpBlobStorageTestSuite struct {
suite.Suite
config *config.Config
- storage internal.BlobStorage
+ storage internal.BlobStorageCore
}
func (s *gcpBlobStorageTestSuite) SetupTest() {
require := testutil.Require(s.T())
- var storage internal.BlobStorage
+ var storage internal.BlobStorageCore
cfg, err := config.New()
require.NoError(err)
cfg.StorageType.BlobStorageType = config.BlobStorageType_GCS
@@ -33,7 +33,7 @@ func (s *gcpBlobStorageTestSuite) SetupTest() {
s.config = cfg
app := testapp.New(
s.T(),
- fx.Provide(New),
+ fx.Provide(newBlobStorage),
testapp.WithIntegration(),
testapp.WithConfig(s.config),
fx.Populate(&storage),
@@ -106,6 +106,7 @@ func (s *gcpBlobStorageTestSuite) TestIntegrationGcsBlobStorageIntegration_GzipF
}
func TestIntegrationGcsBlobStorageTestSuite(t *testing.T) {
+ t.Skip()
require := testutil.Require(t)
cfg, err := config.New()
require.NoError(err)
diff --git a/internal/storage/blobstorage/gcs/module.go b/internal/storage/blobstorage/gcs/module.go
index b41d1fd2..5fd1aede 100644
--- a/internal/storage/blobstorage/gcs/module.go
+++ b/internal/storage/blobstorage/gcs/module.go
@@ -7,6 +7,6 @@ import (
var Module = fx.Options(
fx.Provide(fx.Annotated{
Name: "blobstorage/gcs",
- Target: NewFactory,
- }),
+ Target: newFactory,
+ }, fx.Private),
)
diff --git a/internal/storage/blobstorage/internal/blobstorage.go b/internal/storage/blobstorage/internal/blobstorage.go
index c85ec7de..323c9479 100644
--- a/internal/storage/blobstorage/internal/blobstorage.go
+++ b/internal/storage/blobstorage/internal/blobstorage.go
@@ -1,7 +1,10 @@
package internal
import (
+ "bytes"
"context"
+ "io"
+ "os"
"go.uber.org/fx"
"golang.org/x/xerrors"
@@ -22,15 +25,57 @@ type (
BlockDataCompression api.Compression
}
+ PayloadSource interface {
+ Open(ctx context.Context) (io.ReadCloser, error)
+ Length() uint64
+ }
+
+ BytesPayloadSource []byte
+
+ FilePayloadSource struct {
+ path string
+ length uint64
+ }
+
+ ConsolidatedBlockPayload struct {
+ Metadata *api.BlockMetadata
+ MetadataID int64
+ RawBlockPayload PayloadSource
+ UncompressedLength uint64
+ }
+
+ BlockPlacement struct {
+ MetadataID int64
+ Height uint64
+ Hash string
+ ObjectKey string
+ ObjectFormat api.BlockObjectFormat
+ ByteOffset uint64
+ ByteLength uint64
+ UncompressedLength uint64
+ }
+
BlobStorage interface {
- Upload(ctx context.Context, block *api.Block, compression api.Compression) (string, error)
- UploadRaw(ctx context.Context, rawBlockData *RawBlockData) (string, error)
+ UploadConsolidated(ctx context.Context, blocks []ConsolidatedBlockPayload) (string, []BlockPlacement, error)
Download(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error)
+ DownloadMany(ctx context.Context, metadata []*api.BlockMetadata) ([]*api.Block, error)
PreSign(ctx context.Context, objectKey string) (string, error)
}
+ // SingleBlockUploader is deliberately confined to the blobstorage package
+ // tree. Production consumers receive only the guarded public uploader.
+ SingleBlockUploader interface {
+ Upload(ctx context.Context, block *api.Block, compression api.Compression) (string, error)
+ UploadRaw(ctx context.Context, rawBlockData *RawBlockData) (string, error)
+ }
+
+ BlobStorageCore interface {
+ BlobStorage
+ SingleBlockUploader
+ }
+
BlobStorageFactory interface {
- Create() (BlobStorage, error)
+ Create() (BlobStorageCore, error)
}
BlobStorageFactoryParams struct {
@@ -41,7 +86,37 @@ type (
}
)
-func WithBlobStorageFactory(params BlobStorageFactoryParams) (BlobStorage, error) {
+func (s BytesPayloadSource) Open(ctx context.Context) (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(s)), nil
+}
+
+func (s BytesPayloadSource) Length() uint64 {
+ return uint64(len(s))
+}
+
+func NewFilePayloadSource(path string, length uint64) FilePayloadSource {
+ return FilePayloadSource{
+ path: path,
+ length: length,
+ }
+}
+
+func (s FilePayloadSource) Open(ctx context.Context) (io.ReadCloser, error) {
+ if err := ctx.Err(); err != nil {
+ return nil, err
+ }
+ return os.Open(s.path)
+}
+
+func (s FilePayloadSource) Length() uint64 {
+ return s.length
+}
+
+func (s FilePayloadSource) Path() string {
+ return s.path
+}
+
+func WithBlobStorageFactory(params BlobStorageFactoryParams) (BlobStorageCore, error) {
var factory BlobStorageFactory
storageType := params.Config.StorageType.BlobStorageType
switch storageType {
diff --git a/internal/storage/blobstorage/internal/progress.go b/internal/storage/blobstorage/internal/progress.go
new file mode 100644
index 00000000..6fc131cb
--- /dev/null
+++ b/internal/storage/blobstorage/internal/progress.go
@@ -0,0 +1,81 @@
+package internal
+
+import (
+ "context"
+ "io"
+)
+
+const consolidatedUploadProgressReadInterval = 64 * 1024 * 1024
+
+type (
+ ConsolidatedUploadProgress func(stage string, details ...any)
+
+ consolidatedUploadProgressKey struct{}
+
+ progressReadCloser struct {
+ ctx context.Context
+ stage string
+ reader io.ReadCloser
+ totalRead uint64
+ nextRead uint64
+ }
+
+ seekableProgressReadCloser struct {
+ *progressReadCloser
+ seeker io.Seeker
+ }
+)
+
+func WithConsolidatedUploadProgress(ctx context.Context, fn ConsolidatedUploadProgress) context.Context {
+ if fn == nil {
+ return ctx
+ }
+ return context.WithValue(ctx, consolidatedUploadProgressKey{}, fn)
+}
+
+func RecordConsolidatedUploadProgress(ctx context.Context, stage string, details ...any) {
+ fn, ok := ctx.Value(consolidatedUploadProgressKey{}).(ConsolidatedUploadProgress)
+ if !ok {
+ return
+ }
+ fn(stage, details...)
+}
+
+func NewConsolidatedUploadProgressReadCloser(ctx context.Context, stage string, reader io.ReadCloser) io.ReadCloser {
+ if _, ok := ctx.Value(consolidatedUploadProgressKey{}).(ConsolidatedUploadProgress); !ok {
+ return reader
+ }
+ progressReader := &progressReadCloser{
+ ctx: ctx,
+ stage: stage,
+ reader: reader,
+ nextRead: consolidatedUploadProgressReadInterval,
+ }
+ if seeker, ok := reader.(io.Seeker); ok {
+ return &seekableProgressReadCloser{
+ progressReadCloser: progressReader,
+ seeker: seeker,
+ }
+ }
+ return progressReader
+}
+
+func (r *progressReadCloser) Read(p []byte) (int, error) {
+ n, err := r.reader.Read(p)
+ if n > 0 {
+ r.totalRead += uint64(n)
+ if r.totalRead >= r.nextRead {
+ RecordConsolidatedUploadProgress(r.ctx, r.stage, r.totalRead)
+ r.nextRead = r.totalRead + consolidatedUploadProgressReadInterval
+ }
+ }
+ return n, err
+}
+
+func (r *progressReadCloser) Close() error {
+ return r.reader.Close()
+}
+
+func (r *seekableProgressReadCloser) Seek(offset int64, whence int) (int64, error) {
+ return r.seeker.Seek(offset, whence)
+}
diff --git a/internal/storage/blobstorage/internal/progress_test.go b/internal/storage/blobstorage/internal/progress_test.go
new file mode 100644
index 00000000..7b2f4b41
--- /dev/null
+++ b/internal/storage/blobstorage/internal/progress_test.go
@@ -0,0 +1,79 @@
+package internal
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+type infiniteReader struct{}
+
+func (infiniteReader) Read(p []byte) (int, error) {
+ return len(p), nil
+}
+
+type readSeekCloser struct {
+ *bytes.Reader
+}
+
+func (readSeekCloser) Close() error {
+ return nil
+}
+
+func TestConsolidatedUploadProgressReadCloserReportsThreshold(t *testing.T) {
+ var progressBytes []uint64
+ ctx := WithConsolidatedUploadProgress(context.Background(), func(stage string, details ...any) {
+ require.Equal(t, "s3_put_read", stage)
+ require.Len(t, details, 1)
+ progressBytes = append(progressBytes, details[0].(uint64))
+ })
+
+ reader := NewConsolidatedUploadProgressReadCloser(
+ ctx,
+ "s3_put_read",
+ io.NopCloser(io.LimitReader(infiniteReader{}, int64(consolidatedUploadProgressReadInterval+1))),
+ )
+ copied, err := io.Copy(io.Discard, reader)
+ require.NoError(t, err)
+ require.NoError(t, reader.Close())
+ require.Equal(t, int64(consolidatedUploadProgressReadInterval+1), copied)
+ require.Len(t, progressBytes, 1)
+ require.GreaterOrEqual(t, progressBytes[0], uint64(consolidatedUploadProgressReadInterval))
+}
+
+func TestConsolidatedUploadProgressReadCloserPreservesSeekableReader(t *testing.T) {
+ ctx := WithConsolidatedUploadProgress(context.Background(), func(stage string, details ...any) {})
+ reader := NewConsolidatedUploadProgressReadCloser(
+ ctx,
+ "s3_put_read",
+ readSeekCloser{Reader: bytes.NewReader([]byte("abcdef"))},
+ )
+
+ seeker, ok := reader.(io.Seeker)
+ require.True(t, ok)
+ offset, err := seeker.Seek(2, io.SeekStart)
+ require.NoError(t, err)
+ require.Equal(t, int64(2), offset)
+
+ buf := make([]byte, 2)
+ n, err := reader.Read(buf)
+ require.NoError(t, err)
+ require.Equal(t, 2, n)
+ require.Equal(t, []byte("cd"), buf)
+ require.NoError(t, reader.Close())
+}
+
+func TestConsolidatedUploadProgressReadCloserDoesNotAdvertiseSeekForNonSeekableReader(t *testing.T) {
+ ctx := WithConsolidatedUploadProgress(context.Background(), func(stage string, details ...any) {})
+ reader := NewConsolidatedUploadProgressReadCloser(
+ ctx,
+ "s3_put_read",
+ io.NopCloser(io.LimitReader(infiniteReader{}, 1)),
+ )
+ _, ok := reader.(io.Seeker)
+ require.False(t, ok)
+ require.NoError(t, reader.Close())
+}
diff --git a/internal/storage/blobstorage/mocks/mocks.go b/internal/storage/blobstorage/mocks/mocks.go
index 04561adc..f8dbfa1f 100644
--- a/internal/storage/blobstorage/mocks/mocks.go
+++ b/internal/storage/blobstorage/mocks/mocks.go
@@ -1,9 +1,9 @@
// Code generated by MockGen. DO NOT EDIT.
-// Source: github.com/coinbase/chainstorage/internal/storage/blobstorage (interfaces: BlobStorage)
+// Source: github.com/coinbase/chainstorage/internal/storage/blobstorage (interfaces: BlobStorage,SingleBlockUploader)
//
// Generated by this command:
//
-// mockgen -destination internal/storage/blobstorage/mocks/mocks.go -package blobstoragemocks github.com/coinbase/chainstorage/internal/storage/blobstorage BlobStorage
+// mockgen -destination internal/storage/blobstorage/mocks/mocks.go -package blobstoragemocks github.com/coinbase/chainstorage/internal/storage/blobstorage BlobStorage,SingleBlockUploader
//
// Package blobstoragemocks is a generated GoMock package.
@@ -56,6 +56,21 @@ func (mr *MockBlobStorageMockRecorder) Download(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Download", reflect.TypeOf((*MockBlobStorage)(nil).Download), arg0, arg1)
}
+// DownloadMany mocks base method.
+func (m *MockBlobStorage) DownloadMany(arg0 context.Context, arg1 []*chainstorage.BlockMetadata) ([]*chainstorage.Block, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "DownloadMany", arg0, arg1)
+ ret0, _ := ret[0].([]*chainstorage.Block)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// DownloadMany indicates an expected call of DownloadMany.
+func (mr *MockBlobStorageMockRecorder) DownloadMany(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DownloadMany", reflect.TypeOf((*MockBlobStorage)(nil).DownloadMany), arg0, arg1)
+}
+
// PreSign mocks base method.
func (m *MockBlobStorage) PreSign(arg0 context.Context, arg1 string) (string, error) {
m.ctrl.T.Helper()
@@ -71,8 +86,47 @@ func (mr *MockBlobStorageMockRecorder) PreSign(arg0, arg1 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PreSign", reflect.TypeOf((*MockBlobStorage)(nil).PreSign), arg0, arg1)
}
+// UploadConsolidated mocks base method.
+func (m *MockBlobStorage) UploadConsolidated(arg0 context.Context, arg1 []internal.ConsolidatedBlockPayload) (string, []internal.BlockPlacement, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "UploadConsolidated", arg0, arg1)
+ ret0, _ := ret[0].(string)
+ ret1, _ := ret[1].([]internal.BlockPlacement)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// UploadConsolidated indicates an expected call of UploadConsolidated.
+func (mr *MockBlobStorageMockRecorder) UploadConsolidated(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadConsolidated", reflect.TypeOf((*MockBlobStorage)(nil).UploadConsolidated), arg0, arg1)
+}
+
+// MockSingleBlockUploader is a mock of SingleBlockUploader interface.
+type MockSingleBlockUploader struct {
+ ctrl *gomock.Controller
+ recorder *MockSingleBlockUploaderMockRecorder
+}
+
+// MockSingleBlockUploaderMockRecorder is the mock recorder for MockSingleBlockUploader.
+type MockSingleBlockUploaderMockRecorder struct {
+ mock *MockSingleBlockUploader
+}
+
+// NewMockSingleBlockUploader creates a new mock instance.
+func NewMockSingleBlockUploader(ctrl *gomock.Controller) *MockSingleBlockUploader {
+ mock := &MockSingleBlockUploader{ctrl: ctrl}
+ mock.recorder = &MockSingleBlockUploaderMockRecorder{mock}
+ return mock
+}
+
+// EXPECT returns an object that allows the caller to indicate expected use.
+func (m *MockSingleBlockUploader) EXPECT() *MockSingleBlockUploaderMockRecorder {
+ return m.recorder
+}
+
// Upload mocks base method.
-func (m *MockBlobStorage) Upload(arg0 context.Context, arg1 *chainstorage.Block, arg2 chainstorage.Compression) (string, error) {
+func (m *MockSingleBlockUploader) Upload(arg0 context.Context, arg1 *chainstorage.Block, arg2 chainstorage.Compression) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Upload", arg0, arg1, arg2)
ret0, _ := ret[0].(string)
@@ -81,13 +135,13 @@ func (m *MockBlobStorage) Upload(arg0 context.Context, arg1 *chainstorage.Block,
}
// Upload indicates an expected call of Upload.
-func (mr *MockBlobStorageMockRecorder) Upload(arg0, arg1, arg2 any) *gomock.Call {
+func (mr *MockSingleBlockUploaderMockRecorder) Upload(arg0, arg1, arg2 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*MockBlobStorage)(nil).Upload), arg0, arg1, arg2)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Upload", reflect.TypeOf((*MockSingleBlockUploader)(nil).Upload), arg0, arg1, arg2)
}
// UploadRaw mocks base method.
-func (m *MockBlobStorage) UploadRaw(arg0 context.Context, arg1 *internal.RawBlockData) (string, error) {
+func (m *MockSingleBlockUploader) UploadRaw(arg0 context.Context, arg1 *internal.RawBlockData) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "UploadRaw", arg0, arg1)
ret0, _ := ret[0].(string)
@@ -96,7 +150,7 @@ func (m *MockBlobStorage) UploadRaw(arg0 context.Context, arg1 *internal.RawBloc
}
// UploadRaw indicates an expected call of UploadRaw.
-func (mr *MockBlobStorageMockRecorder) UploadRaw(arg0, arg1 any) *gomock.Call {
+func (mr *MockSingleBlockUploaderMockRecorder) UploadRaw(arg0, arg1 any) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadRaw", reflect.TypeOf((*MockBlobStorage)(nil).UploadRaw), arg0, arg1)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UploadRaw", reflect.TypeOf((*MockSingleBlockUploader)(nil).UploadRaw), arg0, arg1)
}
diff --git a/internal/storage/blobstorage/module.go b/internal/storage/blobstorage/module.go
index 4b7d1ce9..2c306de3 100644
--- a/internal/storage/blobstorage/module.go
+++ b/internal/storage/blobstorage/module.go
@@ -9,14 +9,26 @@ import (
)
type (
- BlobStorage = internal.BlobStorage
- BlobStorageFactory = internal.BlobStorageFactory
- BlobStorageFactoryParams = internal.BlobStorageFactoryParams
- RawBlockData = internal.RawBlockData
+ BlobStorage = internal.BlobStorage
+ BlobStorageFactory = internal.BlobStorageFactory
+ BlobStorageFactoryParams = internal.BlobStorageFactoryParams
+ RawBlockData = internal.RawBlockData
+ PayloadSource = internal.PayloadSource
+ BytesPayloadSource = internal.BytesPayloadSource
+ FilePayloadSource = internal.FilePayloadSource
+ ConsolidatedBlockPayload = internal.ConsolidatedBlockPayload
+ BlockPlacement = internal.BlockPlacement
+ ConsolidatedUploadProgress = internal.ConsolidatedUploadProgress
)
-var Module = fx.Options(
- fx.Provide(internal.WithBlobStorageFactory),
- s3.Module,
- gcs.Module,
+var (
+ NewFilePayloadSource = internal.NewFilePayloadSource
+ WithConsolidatedUploadProgress = internal.WithConsolidatedUploadProgress
+ RecordConsolidatedUploadProgress = internal.RecordConsolidatedUploadProgress
+
+ Module = fx.Module("blobstorage",
+ fx.Provide(withBlobStorageFactory),
+ s3.Module,
+ gcs.Module,
+ )
)
diff --git a/internal/storage/blobstorage/s3/blob_storage.go b/internal/storage/blobstorage/s3/blob_storage.go
index c9cea0d0..651b59e8 100644
--- a/internal/storage/blobstorage/s3/blob_storage.go
+++ b/internal/storage/blobstorage/s3/blob_storage.go
@@ -5,14 +5,17 @@ import (
"context"
"crypto/md5" // #nosec G501
"encoding/base64"
+ "errors"
"fmt"
+ "io"
+ "sort"
"time"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/awserr"
- "github.com/aws/aws-sdk-go/aws/request"
- awss3 "github.com/aws/aws-sdk-go/service/s3"
- "github.com/aws/aws-sdk-go/service/s3/s3manager"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ awss3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/aws/smithy-go"
"github.com/uber-go/tally/v4"
"go.uber.org/fx"
"go.uber.org/zap"
@@ -21,12 +24,14 @@ import (
"github.com/coinbase/chainstorage/internal/config"
"github.com/coinbase/chainstorage/internal/s3"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/cscb"
"github.com/coinbase/chainstorage/internal/storage/blobstorage/internal"
- "github.com/coinbase/chainstorage/internal/storage/internal/errors"
+ storageerrors "github.com/coinbase/chainstorage/internal/storage/internal/errors"
storage_utils "github.com/coinbase/chainstorage/internal/storage/utils"
"github.com/coinbase/chainstorage/internal/utils/fxparams"
"github.com/coinbase/chainstorage/internal/utils/instrument"
"github.com/coinbase/chainstorage/internal/utils/log"
+ "github.com/coinbase/chainstorage/internal/utils/syncgroup"
"github.com/coinbase/chainstorage/protos/coinbase/c3/common"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
)
@@ -45,22 +50,34 @@ type (
}
blobStorageImpl struct {
- logger *zap.Logger
- config *config.Config
- bucket string
- client s3.Client
- downloader s3.Downloader
- uploader s3.Uploader
- blobStorageMetrics *blobStorageMetrics
- instrumentUpload instrument.InstrumentWithResult[string]
- instrumentUploadRaw instrument.InstrumentWithResult[string]
- instrumentDownload instrument.InstrumentWithResult[*api.Block]
+ logger *zap.Logger
+ config *config.Config
+ bucket string
+ client s3.Client
+ downloader s3.Downloader
+ uploader s3.Uploader
+ blobStorageMetrics *blobStorageMetrics
+ instrumentUpload instrument.InstrumentWithResult[string]
+ instrumentUploadRaw instrument.InstrumentWithResult[string]
+ instrumentDownload instrument.InstrumentWithResult[*api.Block]
+ instrumentDownloadMany instrument.InstrumentWithResult[[]*api.Block]
}
blobStorageMetrics struct {
blobDownloadedSize tally.Timer
blobUploadedSize tally.Timer
}
+
+ downloadRef struct {
+ index int
+ metadata *api.BlockMetadata
+ }
+
+ cscbBlockDownload struct {
+ ref downloadRef
+ block *cscb.BlockDescriptor
+ chunk *cscb.ChunkDescriptor
+ }
)
const (
@@ -68,36 +85,44 @@ const (
blobDownloaderScopeName = "downloader"
blobSizeMetricName = "blob_size"
- // https://docs.aws.amazon.com/AmazonS3/latest/userguide/acl-overview.html#CannedACL
- bucketOwnerFullControl = "bucket-owner-full-control"
+ cscbMetadataFormat = "chainstorage-format"
+ cscbMetadataCompressionScope = "chainstorage-compression-scope"
+ cscbMetadataSHA256 = "chainstorage-sha256"
+ cscbMetadataCodec = "chainstorage-codec"
+ cscbMetadataUncompressedLength = "chainstorage-uncompressed-length"
+
+ maxSinglePutObjectSize = 5 * 1024 * 1024 * 1024
+
+ cscbInitialIndexReadSize = 64 * 1024
)
-var _ internal.BlobStorage = (*blobStorageImpl)(nil)
+var _ internal.BlobStorageCore = (*blobStorageImpl)(nil)
-func NewFactory(params BlobStorageParams) internal.BlobStorageFactory {
+func newFactory(params BlobStorageParams) internal.BlobStorageFactory {
return &blobStorageFactory{params}
}
// Create implements BlobStorageFactory.
-func (f *blobStorageFactory) Create() (internal.BlobStorage, error) {
- return New(f.params)
+func (f *blobStorageFactory) Create() (internal.BlobStorageCore, error) {
+ return newBlobStorage(f.params)
}
-func New(params BlobStorageParams) (internal.BlobStorage, error) {
+func newBlobStorage(params BlobStorageParams) (internal.BlobStorageCore, error) {
metrics := params.Metrics.SubScope("blob_storage").Tagged(map[string]string{
"storage_type": "s3",
})
return &blobStorageImpl{
- logger: log.WithPackage(params.Logger),
- config: params.Config,
- bucket: params.Config.AWS.Bucket,
- client: params.Client,
- downloader: params.Downloader,
- uploader: params.Uploader,
- blobStorageMetrics: newBlobStorageMetrics(metrics),
- instrumentUpload: instrument.NewWithResult[string](metrics, "upload"),
- instrumentUploadRaw: instrument.NewWithResult[string](metrics, "upload_raw"),
- instrumentDownload: instrument.NewWithResult[*api.Block](metrics, "download"),
+ logger: log.WithPackage(params.Logger),
+ config: params.Config,
+ bucket: params.Config.AWS.Bucket,
+ client: params.Client,
+ downloader: params.Downloader,
+ uploader: params.Uploader,
+ blobStorageMetrics: newBlobStorageMetrics(metrics),
+ instrumentUpload: instrument.NewWithResult[string](metrics, "upload"),
+ instrumentUploadRaw: instrument.NewWithResult[string](metrics, "upload_raw"),
+ instrumentDownload: instrument.NewWithResult[*api.Block](metrics, "download"),
+ instrumentDownloadMany: instrument.NewWithResult[[]*api.Block](metrics, "download_many"),
}, nil
}
@@ -144,12 +169,12 @@ func (s *blobStorageImpl) uploadRaw(ctx context.Context, rawBlockData *internal.
checksum := base64.StdEncoding.EncodeToString(h.Sum(nil))
- if _, err := s.uploader.UploadWithContext(ctx, &s3manager.UploadInput{
+ if _, err := s.uploader.Upload(ctx, &awss3.PutObjectInput{
Bucket: aws.String(s.bucket),
Key: aws.String(key),
Body: bytes.NewReader(rawBlockData.BlockData),
ContentMD5: aws.String(checksum),
- ACL: aws.String(bucketOwnerFullControl),
+ ACL: awss3types.ObjectCannedACLBucketOwnerFullControl,
}); err != nil {
return "", xerrors.Errorf("failed to upload to s3: %w", err)
}
@@ -201,68 +226,627 @@ func (s *blobStorageImpl) Upload(ctx context.Context, block *api.Block, compress
})
}
+func (s *blobStorageImpl) UploadConsolidated(ctx context.Context, blocks []internal.ConsolidatedBlockPayload) (string, []internal.BlockPlacement, error) {
+ defer s.logDuration("upload_consolidated", time.Now())
+
+ totalStart := time.Now()
+ consolidation := s.config.AWS.Storage.Consolidation
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_encode_config_loaded", len(blocks))
+ encodeStart := time.Now()
+ object, err := cscb.Encode(ctx, cscb.EncodeConfig{
+ Blockchain: s.config.Chain.Blockchain,
+ Network: s.config.Chain.Network,
+ SideChain: s.config.Chain.Sidechain,
+ Codec: consolidation.Codec,
+ CodecLevel: consolidation.CodecLevel,
+ ZstdLongDistanceWindowLog: consolidation.ZstdLongDistanceWindowLog,
+ MaxBlocks: consolidation.MaxBlocks,
+ CompressionChunkBlocks: consolidation.CompressionChunkBlocks,
+ MaxChunkCompressedBytes: consolidation.MaxChunkCompressedBytes,
+ MaxChunkUncompressedBytes: consolidation.MaxChunkUncompressedBytes,
+ MaxCompressedBytes: consolidation.MaxCompressedBytes,
+ MaxUncompressedBytes: consolidation.MaxUncompressedBytes,
+ ShardSize: consolidation.ShardSize,
+ MemoryBudgetBytes: consolidation.MemoryBudgetBytes,
+ LocalSpillDir: consolidation.LocalSpillDir,
+ LocalSpillMaxBytes: consolidation.LocalSpillMaxBytes,
+ }, blocks)
+ if err != nil {
+ return "", nil, err
+ }
+ defer func() { _ = object.Close() }()
+ encodeDuration := time.Since(encodeStart)
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_object_encoded", object.Key, object.Length)
+ s.logger.Info(
+ "encoded CSCB object",
+ zap.String("object_key", object.Key),
+ zap.Int("blocks", len(blocks)),
+ zap.Uint64("compressed_bytes", object.Length),
+ zap.Uint64("payload_compressed_bytes", object.PayloadCompressedLength),
+ zap.Uint64("payload_uncompressed_bytes", object.PayloadUncompressedLength),
+ zap.Duration("duration", encodeDuration),
+ )
+
+ headExistingStart := time.Now()
+ found, err := s.validateExistingConsolidatedObject(ctx, object)
+ if err != nil {
+ return "", nil, err
+ }
+ headExistingDuration := time.Since(headExistingStart)
+ s.logger.Info(
+ "checked existing CSCB object",
+ zap.String("object_key", object.Key),
+ zap.Bool("found", found),
+ zap.Duration("duration", headExistingDuration),
+ )
+ var uploadDuration time.Duration
+ var uploadMD5Duration time.Duration
+ var uploadPutDuration time.Duration
+ var headUploadedDuration time.Duration
+ if !found {
+ uploadStart := time.Now()
+ uploaded, md5Duration, putDuration, err := s.uploadConsolidatedObject(ctx, object, consolidation.Codec)
+ if err != nil {
+ return "", nil, err
+ }
+ uploadDuration = time.Since(uploadStart)
+ uploadMD5Duration = md5Duration
+ uploadPutDuration = putDuration
+ s.logger.Info(
+ "put CSCB object",
+ zap.String("object_key", object.Key),
+ zap.Bool("uploaded", uploaded),
+ zap.Uint64("bytes", object.Length),
+ zap.Duration("md5_duration", uploadMD5Duration),
+ zap.Duration("s3_put_duration", uploadPutDuration),
+ zap.Duration("duration", uploadDuration),
+ )
+ if uploaded {
+ headUploadedStart := time.Now()
+ if err := s.validateUploadedConsolidatedObject(ctx, object); err != nil {
+ return "", nil, err
+ }
+ headUploadedDuration = time.Since(headUploadedStart)
+ s.logger.Info(
+ "validated uploaded CSCB object",
+ zap.String("object_key", object.Key),
+ zap.Uint64("bytes", object.Length),
+ zap.Duration("duration", headUploadedDuration),
+ )
+ s.blobStorageMetrics.blobUploadedSize.Record(time.Duration(object.Length) * time.Millisecond)
+ }
+ }
+ s.logger.Info(
+ "finished consolidated upload",
+ zap.String("object_key", object.Key),
+ zap.Bool("existing_object", found),
+ zap.Int("blocks", len(blocks)),
+ zap.Uint64("compressed_bytes", object.Length),
+ zap.Uint64("payload_uncompressed_bytes", object.PayloadUncompressedLength),
+ zap.Duration("encode_duration", encodeDuration),
+ zap.Duration("head_existing_duration", headExistingDuration),
+ zap.Duration("md5_duration", uploadMD5Duration),
+ zap.Duration("s3_put_duration", uploadPutDuration),
+ zap.Duration("upload_duration", uploadDuration),
+ zap.Duration("head_uploaded_duration", headUploadedDuration),
+ zap.Duration("total_duration", time.Since(totalStart)),
+ )
+ return object.Key, object.Placements, nil
+}
+
+func (s *blobStorageImpl) uploadConsolidatedObject(ctx context.Context, object *cscb.Object, compression api.Compression) (bool, time.Duration, time.Duration, error) {
+ if object.Length > maxSinglePutObjectSize {
+ return false, 0, 0, xerrors.Errorf("CSCB object length %d exceeds v1 single-put limit %d", object.Length, maxSinglePutObjectSize)
+ }
+ reader, err := object.Open()
+ if err != nil {
+ return false, 0, 0, err
+ }
+ defer func() { _ = reader.Close() }()
+
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_md5_started", object.Key, object.Length)
+ md5Start := time.Now()
+ contentMD5, err := computeContentMD5(ctx, object)
+ md5Duration := time.Since(md5Start)
+ if err != nil {
+ return false, md5Duration, 0, err
+ }
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_md5_finished", object.Key, object.Length)
+ metadata := map[string]string{
+ cscbMetadataFormat: "cscb",
+ cscbMetadataCompressionScope: "batch-chunked",
+ cscbMetadataSHA256: object.SHA256,
+ cscbMetadataCodec: codecName(compression),
+ cscbMetadataUncompressedLength: fmt.Sprintf("%d", object.PayloadUncompressedLength),
+ }
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_put_started", object.Key, object.Length)
+ putStart := time.Now()
+ if _, err := s.client.PutObject(ctx, &awss3.PutObjectInput{
+ Bucket: aws.String(s.bucket),
+ Key: aws.String(object.Key),
+ Body: internal.NewConsolidatedUploadProgressReadCloser(ctx, "s3_put_read", reader),
+ ContentLength: aws.Int64(int64(object.Length)),
+ ContentMD5: aws.String(contentMD5),
+ ACL: awss3types.ObjectCannedACLBucketOwnerFullControl,
+ IfNoneMatch: aws.String("*"),
+ Metadata: metadata,
+ }, func(options *awss3.Options) {
+ // Content-MD5 is the integrity check here. Avoid SDK-added CRC32 trailers
+ // that switch large uploads to aws-chunked request bodies.
+ options.RequestChecksumCalculation = aws.RequestChecksumCalculationWhenRequired
+ }); err != nil {
+ putDuration := time.Since(putStart)
+ if isObjectPreconditionFailed(err) {
+ found, validateErr := s.validateExistingConsolidatedObject(ctx, object)
+ if validateErr != nil {
+ return false, md5Duration, putDuration, validateErr
+ }
+ if found {
+ return false, md5Duration, putDuration, nil
+ }
+ }
+ return false, md5Duration, putDuration, xerrors.Errorf("failed to upload CSCB object to s3: %w", err)
+ }
+ putDuration := time.Since(putStart)
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_put_finished", object.Key, object.Length)
+ return true, md5Duration, putDuration, nil
+}
+
+func computeContentMD5(ctx context.Context, object *cscb.Object) (string, error) {
+ reader, err := object.Open()
+ if err != nil {
+ return "", err
+ }
+ defer func() { _ = reader.Close() }()
+
+ // #nosec G401 -- S3 Content-MD5 is a transport integrity header, not a cryptographic trust boundary.
+ h := md5.New()
+ if _, err := io.Copy(h, internal.NewConsolidatedUploadProgressReadCloser(ctx, "s3_md5_read", reader)); err != nil {
+ return "", xerrors.Errorf("failed to compute CSCB content md5: %w", err)
+ }
+ return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
+}
+
+func (s *blobStorageImpl) validateExistingConsolidatedObject(ctx context.Context, object *cscb.Object) (bool, error) {
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_head_existing_started", object.Key)
+ out, err := s.client.HeadObject(ctx, &awss3.HeadObjectInput{
+ Bucket: aws.String(s.bucket),
+ Key: aws.String(object.Key),
+ })
+ if err != nil {
+ if isObjectNotFound(err) {
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_head_existing_not_found", object.Key)
+ return false, nil
+ }
+ return false, xerrors.Errorf("failed to head CSCB object (bucket=%s, key=%s): %w", s.bucket, object.Key, err)
+ }
+ if err := validateConsolidatedHead(out, object); err != nil {
+ return false, err
+ }
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_head_existing_finished", object.Key, object.Length)
+ return true, nil
+}
+
+func (s *blobStorageImpl) validateUploadedConsolidatedObject(ctx context.Context, object *cscb.Object) error {
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_head_uploaded_started", object.Key)
+ out, err := s.client.HeadObject(ctx, &awss3.HeadObjectInput{
+ Bucket: aws.String(s.bucket),
+ Key: aws.String(object.Key),
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to validate uploaded CSCB object (bucket=%s, key=%s): %w", s.bucket, object.Key, err)
+ }
+ if err := validateConsolidatedHead(out, object); err != nil {
+ return err
+ }
+ internal.RecordConsolidatedUploadProgress(ctx, "s3_head_uploaded_finished", object.Key, object.Length)
+ return nil
+}
+
+func validateConsolidatedHead(out *awss3.HeadObjectOutput, object *cscb.Object) error {
+ if out == nil {
+ return xerrors.New("empty CSCB HeadObject response")
+ }
+ if out.ContentLength == nil {
+ return xerrors.Errorf("CSCB object %s has no content length", object.Key)
+ }
+ if uint64(*out.ContentLength) != object.Length {
+ return xerrors.Errorf("CSCB object %s length mismatch: got %d want %d", object.Key, *out.ContentLength, object.Length)
+ }
+ if metadataSHA := out.Metadata[cscbMetadataSHA256]; metadataSHA != object.SHA256 {
+ return xerrors.Errorf("CSCB object %s sha256 metadata mismatch: got %q want %q", object.Key, metadataSHA, object.SHA256)
+ }
+ return nil
+}
+
+func isObjectNotFound(err error) bool {
+ var notFound *awss3types.NotFound
+ if errors.As(err, ¬Found) {
+ return true
+ }
+ var noSuchKey *awss3types.NoSuchKey
+ if errors.As(err, &noSuchKey) {
+ return true
+ }
+ var apiErr smithy.APIError
+ if errors.As(err, &apiErr) {
+ switch apiErr.ErrorCode() {
+ case "NotFound", "NoSuchKey", "404":
+ return true
+ }
+ }
+ return false
+}
+
+func isObjectPreconditionFailed(err error) bool {
+ var apiErr smithy.APIError
+ if errors.As(err, &apiErr) {
+ switch apiErr.ErrorCode() {
+ case "PreconditionFailed", "412":
+ return true
+ }
+ }
+ return false
+}
+
+func codecName(compression api.Compression) string {
+ switch compression {
+ case api.Compression_GZIP:
+ return "gzip"
+ case api.Compression_ZSTD:
+ return "zstd"
+ default:
+ return "unknown"
+ }
+}
+
func (s *blobStorageImpl) Download(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
return s.instrumentDownload.Instrument(ctx, func(ctx context.Context) (*api.Block, error) {
defer s.logDuration("download", time.Now())
if metadata.Skipped {
- // No blob data is available when the block is skipped.
- return &api.Block{
- Blockchain: s.config.Chain.Blockchain,
- Network: s.config.Chain.Network,
- SideChain: s.config.Chain.Sidechain,
- Metadata: metadata,
- Blobdata: nil,
- }, nil
+ return s.skippedBlock(metadata), nil
}
+ if isSingleBlockObject(metadata) {
+ return s.downloadSingleBlock(ctx, metadata)
+ }
+ return s.downloadCSCBBlock(ctx, metadata)
+ })
+}
- key := metadata.ObjectKeyMain
- buf := aws.NewWriteAtBuffer([]byte{})
+func (s *blobStorageImpl) DownloadMany(ctx context.Context, metadatas []*api.BlockMetadata) ([]*api.Block, error) {
+ return s.instrumentDownloadMany.Instrument(ctx, func(ctx context.Context) ([]*api.Block, error) {
+ defer s.logDuration("download_many", time.Now())
+
+ result := make([]*api.Block, len(metadatas))
+ singleBlockRefs := make([]downloadRef, 0, len(metadatas))
+ cscbRefsByObject := make(map[string][]downloadRef)
+ for i, metadata := range metadatas {
+ if metadata.GetSkipped() {
+ result[i] = s.skippedBlock(metadata)
+ continue
+ }
+ ref := downloadRef{
+ index: i,
+ metadata: metadata,
+ }
+ if isSingleBlockObject(metadata) {
+ singleBlockRefs = append(singleBlockRefs, ref)
+ continue
+ }
+ key := metadata.GetObjectKeyMain()
+ if key == "" {
+ return nil, xerrors.Errorf("missing CSCB object key for height %d", metadata.GetHeight())
+ }
+ cscbRefsByObject[key] = append(cscbRefsByObject[key], ref)
+ }
+
+ group, ctx := syncgroup.New(ctx, syncgroup.WithThrottling(s.downloadWorkerLimit()))
+ for _, ref := range singleBlockRefs {
+ ref := ref
+ group.Go(func() error {
+ block, err := s.Download(ctx, ref.metadata)
+ if err != nil {
+ return xerrors.Errorf("failed to download single-block object (input={%+v}): %w", ref.metadata, err)
+ }
+ result[ref.index] = block
+ return nil
+ })
+ }
+ for key, refs := range cscbRefsByObject {
+ key, refs := key, refs
+ group.Go(func() error {
+ if err := s.downloadCSCBObject(ctx, key, refs, result); err != nil {
+ return xerrors.Errorf("failed to download CSCB object %s: %w", key, err)
+ }
+ return nil
+ })
+ }
+ if err := group.Wait(); err != nil {
+ return nil, err
+ }
+ return result, nil
+ })
+}
+
+func (s *blobStorageImpl) skippedBlock(metadata *api.BlockMetadata) *api.Block {
+ return &api.Block{
+ Blockchain: s.config.Chain.Blockchain,
+ Network: s.config.Chain.Network,
+ SideChain: s.config.Chain.Sidechain,
+ Metadata: metadata,
+ Blobdata: nil,
+ }
+}
- size, err := s.downloader.DownloadWithContext(ctx, buf, &awss3.GetObjectInput{
- Bucket: aws.String(s.bucket),
- Key: aws.String(key),
+func isSingleBlockObject(metadata *api.BlockMetadata) bool {
+ return metadata.GetObjectFormat() != api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH || metadata.GetByteLength() == 0
+}
+
+func (s *blobStorageImpl) downloadWorkerLimit() int {
+ limit := int(s.config.Api.NumWorkers)
+ if limit <= 0 {
+ return 1
+ }
+ return limit
+}
+
+func (s *blobStorageImpl) downloadSingleBlock(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
+ key := metadata.ObjectKeyMain
+ buf := manager.NewWriteAtBuffer([]byte{})
+
+ size, err := s.downloader.Download(ctx, buf, &awss3.GetObjectInput{
+ Bucket: aws.String(s.bucket),
+ Key: aws.String(key),
+ })
+ if err != nil {
+ if errors.Is(err, context.Canceled) {
+ return nil, storageerrors.ErrRequestCanceled
+ }
+ return nil, xerrors.Errorf("failed to download from s3 (bucket=%s, key=%s): %w", s.bucket, key, err)
+ }
+
+ // a workaround to use timer
+ s.blobStorageMetrics.blobDownloadedSize.Record(time.Duration(size) * time.Millisecond)
+
+ compression := storage_utils.GetCompressionType(key)
+ blockData, err := storage_utils.Decompress(buf.Bytes(), compression)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to decompress block data with type %v: %w", compression.String(), err)
+ }
+
+ return unmarshalBlockData(s.bucket, key, metadata, blockData)
+}
+
+func (s *blobStorageImpl) downloadCSCBBlock(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
+ result := make([]*api.Block, 1)
+ ref := downloadRef{
+ index: 0,
+ metadata: metadata,
+ }
+ if err := s.downloadCSCBObject(ctx, metadata.GetObjectKeyMain(), []downloadRef{ref}, result); err != nil {
+ return nil, err
+ }
+ return result[0], nil
+}
+
+func (s *blobStorageImpl) downloadCSCBObject(ctx context.Context, key string, refs []downloadRef, result []*api.Block) error {
+ index, err := s.readCSCBIndex(ctx, key)
+ if err != nil {
+ return err
+ }
+
+ downloadsByChunk := make(map[uint32][]cscbBlockDownload)
+ for _, ref := range refs {
+ block, chunk, err := index.LookupBlock(ref.metadata)
+ if err != nil {
+ return err
+ }
+ downloadsByChunk[chunk.Index] = append(downloadsByChunk[chunk.Index], cscbBlockDownload{
+ ref: ref,
+ block: block,
+ chunk: chunk,
})
+ }
+
+ chunkIndexes := make([]int, 0, len(downloadsByChunk))
+ for chunkIndex := range downloadsByChunk {
+ chunkIndexes = append(chunkIndexes, int(chunkIndex))
+ }
+ sort.Ints(chunkIndexes)
+
+ for _, chunkIndex := range chunkIndexes {
+ downloads := downloadsByChunk[uint32(chunkIndex)]
+ chunk := downloads[0].chunk
+ blocks := make([]*cscb.BlockDescriptor, len(downloads))
+ for i, download := range downloads {
+ blocks[i] = download.block
+ }
+ blockPayloads, err := s.downloadCSCBChunkPayloads(ctx, key, index.Header.Codec, chunk, blocks)
if err != nil {
- if aerr, ok := err.(awserr.Error); ok && aerr.Code() == request.CanceledErrorCode {
- return nil, errors.ErrRequestCanceled
+ return err
+ }
+ for i, download := range downloads {
+ blockPayload := blockPayloads[i]
+ block, err := unmarshalBlockData(s.bucket, key, download.ref.metadata, blockPayload)
+ if err != nil {
+ return err
}
- return nil, xerrors.Errorf("failed to download from s3 (bucket=%s, key=%s): %w", s.bucket, key, err)
+ result[download.ref.index] = block
}
+ }
+ return nil
+}
- // a workaround to use timer
- s.blobStorageMetrics.blobDownloadedSize.Record(time.Duration(size) * time.Millisecond)
+func (s *blobStorageImpl) readCSCBIndex(ctx context.Context, key string) (*cscb.Index, error) {
+ first, err := s.readObjectRange(ctx, key, 0, cscbInitialIndexReadSize-1)
+ if err != nil {
+ return nil, err
+ }
+ required, err := cscb.HeaderEnvelopeLength(first)
+ if err != nil {
+ return nil, err
+ }
+ if required <= uint64(len(first)) {
+ return cscb.ParseIndex(first)
+ }
+ remaining, err := s.readObjectRange(ctx, key, uint64(len(first)), required-1)
+ if err != nil {
+ return nil, err
+ }
+ indexData := make([]byte, 0, required)
+ indexData = append(indexData, first...)
+ indexData = append(indexData, remaining...)
+ return cscb.ParseIndex(indexData)
+}
- compression := storage_utils.GetCompressionType(key)
- blockData, err := storage_utils.Decompress(buf.Bytes(), compression)
- if err != nil {
- return nil, xerrors.Errorf("failed to decompress block data with type %v: %w", compression.String(), err)
+func (s *blobStorageImpl) downloadCSCBChunkPayloads(ctx context.Context, key string, codec api.Compression, chunk *cscb.ChunkDescriptor, blocks []*cscb.BlockDescriptor) ([][]byte, error) {
+ if len(blocks) == 0 {
+ return nil, xerrors.New("CSCB block descriptors are required")
+ }
+ body, err := s.openObjectRangeByLength(ctx, key, chunk.CompressedPayloadOffset, chunk.CompressedLength)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = body.Close() }()
+
+ countingBody := &countingReadCloser{ReadCloser: body}
+ limitedBody, err := limitReaderByLength(countingBody, chunk.CompressedLength)
+ if err != nil {
+ return nil, err
+ }
+ payloads, err := cscb.ExtractBlockPayloadsFromChunkFrame(limitedBody, codec, chunk, blocks)
+ s.blobStorageMetrics.blobDownloadedSize.Record(time.Duration(countingBody.bytesRead) * time.Millisecond)
+ if err != nil {
+ if errors.Is(err, context.Canceled) {
+ return nil, storageerrors.ErrRequestCanceled
}
+ return nil, err
+ }
+ return payloads, nil
+}
- var block api.Block
- err = proto.Unmarshal(blockData, &block)
- if err != nil {
- return nil, xerrors.Errorf("failed to unmarshal data downloaded from s3 bucket %s key %s: %w", s.bucket, key, err)
+func (s *blobStorageImpl) openObjectRangeByLength(ctx context.Context, key string, offset uint64, length uint64) (io.ReadCloser, error) {
+ if length == 0 {
+ return nil, xerrors.Errorf("empty range read for key %s at offset %d", key, offset)
+ }
+ end, err := inclusiveRangeEnd(offset, length)
+ if err != nil {
+ return nil, err
+ }
+ return s.openObjectRange(ctx, key, offset, end)
+}
+
+func (s *blobStorageImpl) readObjectRange(ctx context.Context, key string, start uint64, end uint64) ([]byte, error) {
+ if end < start {
+ return nil, xerrors.Errorf("invalid range for key %s: start=%d end=%d", key, start, end)
+ }
+ body, err := s.openObjectRange(ctx, key, start, end)
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = body.Close() }()
+ expectedLength := end - start + 1
+ data, err := readExpectedRangeBody(body, expectedLength, start == 0)
+ if err != nil {
+ if errors.Is(err, context.Canceled) {
+ return nil, storageerrors.ErrRequestCanceled
}
+ return nil, xerrors.Errorf("failed to read s3 range body (bucket=%s, key=%s, range=bytes=%d-%d): %w", s.bucket, key, start, end, err)
+ }
+ s.blobStorageMetrics.blobDownloadedSize.Record(time.Duration(len(data)) * time.Millisecond)
+ return data, nil
+}
- // When metadata is loaded from meta storage,
- // the new fields, e.g. ParentHeight, may be populated with default values.
- // Overwrite metadata using the one loaded from meta storage.
- block.Metadata = metadata
- return &block, nil
+func (s *blobStorageImpl) openObjectRange(ctx context.Context, key string, start uint64, end uint64) (io.ReadCloser, error) {
+ if end < start {
+ return nil, xerrors.Errorf("invalid range for key %s: start=%d end=%d", key, start, end)
+ }
+ output, err := s.client.GetObject(ctx, &awss3.GetObjectInput{
+ Bucket: aws.String(s.bucket),
+ Key: aws.String(key),
+ Range: aws.String(fmt.Sprintf("bytes=%d-%d", start, end)),
})
+ if err != nil {
+ if errors.Is(err, context.Canceled) {
+ return nil, storageerrors.ErrRequestCanceled
+ }
+ return nil, xerrors.Errorf("failed to range download from s3 (bucket=%s, key=%s, range=bytes=%d-%d): %w", s.bucket, key, start, end, err)
+ }
+ if output.Body == nil {
+ return nil, xerrors.Errorf("empty s3 body (bucket=%s, key=%s, range=bytes=%d-%d)", s.bucket, key, start, end)
+ }
+ return output.Body, nil
+}
+
+func limitReaderByLength(reader io.Reader, length uint64) (io.Reader, error) {
+ const maxInt64 = uint64(1<<63 - 1)
+ if length >= maxInt64 {
+ return nil, xerrors.Errorf("range body length %d is too large", length)
+ }
+ return io.LimitReader(reader, int64(length)), nil
+}
+
+type countingReadCloser struct {
+ io.ReadCloser
+ bytesRead uint64
+}
+
+func (r *countingReadCloser) Read(p []byte) (int, error) {
+ n, err := r.ReadCloser.Read(p)
+ r.bytesRead += uint64(n)
+ return n, err
+}
+
+func readExpectedRangeBody(body io.Reader, expectedLength uint64, allowShort bool) ([]byte, error) {
+ const maxInt64 = uint64(1<<63 - 1)
+ if expectedLength >= maxInt64 {
+ return nil, xerrors.Errorf("range body length %d is too large", expectedLength)
+ }
+ data, err := io.ReadAll(io.LimitReader(body, int64(expectedLength)+1))
+ if err != nil {
+ return nil, err
+ }
+ if uint64(len(data)) > expectedLength || (!allowShort && uint64(len(data)) != expectedLength) {
+ return nil, xerrors.Errorf("range body length mismatch: got %d want %d", len(data), expectedLength)
+ }
+ return data, nil
+}
+
+func inclusiveRangeEnd(offset uint64, length uint64) (uint64, error) {
+ if length == 0 {
+ return 0, xerrors.New("range length must be positive")
+ }
+ if offset > ^uint64(0)-(length-1) {
+ return 0, xerrors.Errorf("range overflow: offset=%d length=%d", offset, length)
+ }
+ return offset + length - 1, nil
+}
+
+func unmarshalBlockData(bucket string, key string, metadata *api.BlockMetadata, blockData []byte) (*api.Block, error) {
+ var block api.Block
+ err := proto.Unmarshal(blockData, &block)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to unmarshal data downloaded from s3 bucket %s key %s: %w", bucket, key, err)
+ }
+
+ // When metadata is loaded from meta storage,
+ // the new fields, e.g. ParentHeight, may be populated with default values.
+ // Overwrite metadata using the one loaded from meta storage.
+ block.Metadata = metadata
+ return &block, nil
}
func (s *blobStorageImpl) PreSign(ctx context.Context, objectKey string) (string, error) {
- getObjectReq, _ := s.client.GetObjectRequest(&awss3.GetObjectInput{
+ presignClient := awss3.NewPresignClient(s.client.(*awss3.Client))
+ presignResult, err := presignClient.PresignGetObject(ctx, &awss3.GetObjectInput{
Bucket: aws.String(s.config.AWS.Bucket),
Key: aws.String(objectKey),
- })
- fileUrl, err := getObjectReq.Presign(s.config.AWS.PresignedUrlExpiration)
+ }, awss3.WithPresignExpires(s.config.AWS.PresignedUrlExpiration))
if err != nil {
return "", xerrors.Errorf("failed to generate presigned url: %w", err)
}
- return fileUrl, nil
+ return presignResult.URL, nil
}
func (s *blobStorageImpl) logDuration(method string, start time.Time) {
diff --git a/internal/storage/blobstorage/s3/blob_storage_consolidated_test.go b/internal/storage/blobstorage/s3/blob_storage_consolidated_test.go
new file mode 100644
index 00000000..7e574b7e
--- /dev/null
+++ b/internal/storage/blobstorage/s3/blob_storage_consolidated_test.go
@@ -0,0 +1,634 @@
+package s3
+
+import (
+ "bytes"
+ "context"
+ "crypto/md5" // #nosec G501
+ "crypto/sha256"
+ "encoding/base64"
+ "fmt"
+ "io"
+ "strings"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ awss3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/aws/smithy-go"
+ "go.uber.org/fx"
+ "go.uber.org/mock/gomock"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/s3"
+ s3mocks "github.com/coinbase/chainstorage/internal/s3/mocks"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/cscb"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/internal"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestBlobStorage_UploadConsolidated_NewObject(t *testing.T) {
+ require := testutil.Require(t)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ downloader := s3mocks.NewMockDownloader(ctrl)
+ uploader := s3mocks.NewMockUploader(ctrl)
+ client := s3mocks.NewMockClient(ctrl)
+
+ var uploadedKey string
+ var uploadedLength int64
+ var uploadedSHA string
+ gomock.InOrder(
+ client.EXPECT().HeadObject(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, input *awss3.HeadObjectInput, opts ...func(*awss3.Options)) (*awss3.HeadObjectOutput, error) {
+ require.NotNil(input.Bucket)
+ require.NotEmpty(*input.Bucket)
+ require.NotNil(input.Key)
+ require.True(strings.HasPrefix(*input.Key, "BLOCKCHAIN_SOLANA/NETWORK_SOLANA_MAINNET/consolidated/v=7/shard=00000000000000000000-00000000000000010000/00000000000000000100-00000000000000000102-"))
+ return nil, &awss3types.NotFound{}
+ }),
+ client.EXPECT().PutObject(gomock.Any(), gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, input *awss3.PutObjectInput, opts ...func(*awss3.Options)) (*awss3.PutObjectOutput, error) {
+ require.NotNil(input.Bucket)
+ require.NotEmpty(*input.Bucket)
+ require.NotNil(input.Key)
+ require.NotNil(input.Body)
+ require.NotNil(input.ContentLength)
+ require.NotNil(input.ContentMD5)
+ seeker, ok := input.Body.(io.Seeker)
+ require.True(ok)
+ _, err := seeker.Seek(0, io.SeekStart)
+ require.NoError(err)
+ require.Empty(input.ChecksumAlgorithm)
+ require.Nil(input.ChecksumCRC32)
+ require.Nil(input.ChecksumCRC32C)
+ require.Nil(input.ChecksumCRC64NVME)
+ require.Nil(input.ChecksumSHA1)
+ require.Nil(input.ChecksumSHA256)
+ require.NotNil(input.IfNoneMatch)
+ require.Equal("*", *input.IfNoneMatch)
+ require.Equal(awss3types.ObjectCannedACLBucketOwnerFullControl, input.ACL)
+ require.Equal("cscb", input.Metadata[cscbMetadataFormat])
+ require.Equal("batch-chunked", input.Metadata[cscbMetadataCompressionScope])
+ require.Equal("zstd", input.Metadata[cscbMetadataCodec])
+ require.Equal("11", input.Metadata[cscbMetadataUncompressedLength])
+
+ body, err := io.ReadAll(input.Body)
+ require.NoError(err)
+ require.Equal(int64(len(body)), *input.ContentLength)
+ var options awss3.Options
+ for _, opt := range opts {
+ opt(&options)
+ }
+ require.Equal(aws.RequestChecksumCalculationWhenRequired, options.RequestChecksumCalculation)
+ sum := sha256.Sum256(body)
+ md5Sum := md5.Sum(body)
+ uploadedKey = *input.Key
+ uploadedLength = int64(len(body))
+ uploadedSHA = fmt.Sprintf("%x", sum[:])
+ require.Equal(uploadedSHA, input.Metadata[cscbMetadataSHA256])
+ require.Contains(uploadedKey, uploadedSHA)
+ require.Equal(base64.StdEncoding.EncodeToString(md5Sum[:]), *input.ContentMD5)
+ return &awss3.PutObjectOutput{}, nil
+ }),
+ client.EXPECT().HeadObject(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, input *awss3.HeadObjectInput, opts ...func(*awss3.Options)) (*awss3.HeadObjectOutput, error) {
+ require.Equal(uploadedKey, *input.Key)
+ return &awss3.HeadObjectOutput{
+ ContentLength: aws.Int64(uploadedLength),
+ Metadata: map[string]string{
+ cscbMetadataSHA256: uploadedSHA,
+ },
+ }, nil
+ }),
+ )
+
+ var storage internal.BlobStorageCore
+ app := testapp.New(
+ t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_SOLANA, common.Network_NETWORK_SOLANA_MAINNET),
+ fx.Provide(newBlobStorage),
+ fx.Provide(func() s3.Downloader { return downloader }),
+ fx.Provide(func() s3.Uploader { return uploader }),
+ fx.Provide(func() s3.Client { return client }),
+ fx.Populate(&storage),
+ )
+ defer app.Close()
+
+ var stages []string
+ ctx := internal.WithConsolidatedUploadProgress(context.Background(), func(stage string, details ...any) {
+ stages = append(stages, stage)
+ })
+ objectKey, placements, err := storage.UploadConsolidated(ctx, testS3Payloads())
+ require.NoError(err)
+ require.Equal(uploadedKey, objectKey)
+ require.Len(placements, 2)
+ require.Equal(uploadedKey, placements[0].ObjectKey)
+ require.Equal(uint64(0), placements[0].ByteOffset)
+ require.Equal(uint64(5), placements[0].ByteLength)
+ require.Equal(uint64(5), placements[1].ByteOffset)
+ require.Equal(uint64(6), placements[1].ByteLength)
+ require.Contains(stages, "s3_encode_config_loaded")
+ require.Contains(stages, "cscb_encode_started")
+ require.Contains(stages, "cscb_block_encoded")
+ require.Contains(stages, "cscb_chunk_flushed")
+ require.Contains(stages, "cscb_materialize_started")
+ require.Contains(stages, "s3_md5_started")
+ require.Contains(stages, "s3_md5_finished")
+ require.Contains(stages, "s3_put_started")
+ require.Contains(stages, "s3_put_finished")
+ require.Contains(stages, "s3_head_uploaded_started")
+ require.Contains(stages, "s3_head_uploaded_finished")
+}
+
+func TestBlobStorage_UploadConsolidated_AcceptsExistingExactObject(t *testing.T) {
+ require := testutil.Require(t)
+
+ expected, err := cscb.Encode(context.Background(), testS3EncodeConfig(), testS3Payloads())
+ require.NoError(err)
+ defer expected.Close()
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ downloader := s3mocks.NewMockDownloader(ctrl)
+ uploader := s3mocks.NewMockUploader(ctrl)
+ client := s3mocks.NewMockClient(ctrl)
+ uploader.EXPECT().Upload(gomock.Any(), gomock.Any()).Times(0)
+ client.EXPECT().PutObject(gomock.Any(), gomock.Any()).Times(0)
+ client.EXPECT().HeadObject(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, input *awss3.HeadObjectInput, opts ...func(*awss3.Options)) (*awss3.HeadObjectOutput, error) {
+ require.Equal(expected.Key, *input.Key)
+ return &awss3.HeadObjectOutput{
+ ContentLength: aws.Int64(int64(expected.Length)),
+ Metadata: map[string]string{
+ cscbMetadataSHA256: expected.SHA256,
+ },
+ }, nil
+ })
+
+ var storage internal.BlobStorageCore
+ app := testapp.New(
+ t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_SOLANA, common.Network_NETWORK_SOLANA_MAINNET),
+ fx.Provide(newBlobStorage),
+ fx.Provide(func() s3.Downloader { return downloader }),
+ fx.Provide(func() s3.Uploader { return uploader }),
+ fx.Provide(func() s3.Client { return client }),
+ fx.Populate(&storage),
+ )
+ defer app.Close()
+
+ objectKey, placements, err := storage.UploadConsolidated(context.Background(), testS3Payloads())
+ require.NoError(err)
+ require.Equal(expected.Key, objectKey)
+ require.Len(placements, 2)
+ require.Equal(expected.Key, placements[0].ObjectKey)
+}
+
+func TestBlobStorage_UploadConsolidated_RejectsExistingDifferentObject(t *testing.T) {
+ require := testutil.Require(t)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ downloader := s3mocks.NewMockDownloader(ctrl)
+ uploader := s3mocks.NewMockUploader(ctrl)
+ client := s3mocks.NewMockClient(ctrl)
+ uploader.EXPECT().Upload(gomock.Any(), gomock.Any()).Times(0)
+ client.EXPECT().PutObject(gomock.Any(), gomock.Any()).Times(0)
+ client.EXPECT().HeadObject(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, input *awss3.HeadObjectInput, opts ...func(*awss3.Options)) (*awss3.HeadObjectOutput, error) {
+ return &awss3.HeadObjectOutput{
+ ContentLength: aws.Int64(1),
+ Metadata: map[string]string{
+ cscbMetadataSHA256: "different",
+ },
+ }, nil
+ })
+
+ var storage internal.BlobStorageCore
+ app := testapp.New(
+ t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_SOLANA, common.Network_NETWORK_SOLANA_MAINNET),
+ fx.Provide(newBlobStorage),
+ fx.Provide(func() s3.Downloader { return downloader }),
+ fx.Provide(func() s3.Uploader { return uploader }),
+ fx.Provide(func() s3.Client { return client }),
+ fx.Populate(&storage),
+ )
+ defer app.Close()
+
+ _, _, err := storage.UploadConsolidated(context.Background(), testS3Payloads())
+ require.Error(err)
+ require.Contains(err.Error(), "length mismatch")
+}
+
+func TestBlobStorage_UploadConsolidated_AcceptsConcurrentExactObject(t *testing.T) {
+ require := testutil.Require(t)
+
+ expected, err := cscb.Encode(context.Background(), testS3EncodeConfig(), testS3Payloads())
+ require.NoError(err)
+ defer expected.Close()
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ downloader := s3mocks.NewMockDownloader(ctrl)
+ uploader := s3mocks.NewMockUploader(ctrl)
+ client := s3mocks.NewMockClient(ctrl)
+ gomock.InOrder(
+ client.EXPECT().HeadObject(gomock.Any(), gomock.Any()).
+ Return(nil, &awss3types.NotFound{}),
+ client.EXPECT().PutObject(gomock.Any(), gomock.Any(), gomock.Any()).
+ Return(nil, &smithy.GenericAPIError{Code: "PreconditionFailed", Message: "object already exists"}),
+ client.EXPECT().HeadObject(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, input *awss3.HeadObjectInput, opts ...func(*awss3.Options)) (*awss3.HeadObjectOutput, error) {
+ require.Equal(expected.Key, *input.Key)
+ return &awss3.HeadObjectOutput{
+ ContentLength: aws.Int64(int64(expected.Length)),
+ Metadata: map[string]string{
+ cscbMetadataSHA256: expected.SHA256,
+ },
+ }, nil
+ }),
+ )
+
+ var storage internal.BlobStorageCore
+ app := testapp.New(
+ t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_SOLANA, common.Network_NETWORK_SOLANA_MAINNET),
+ fx.Provide(newBlobStorage),
+ fx.Provide(func() s3.Downloader { return downloader }),
+ fx.Provide(func() s3.Uploader { return uploader }),
+ fx.Provide(func() s3.Client { return client }),
+ fx.Populate(&storage),
+ )
+ defer app.Close()
+
+ objectKey, placements, err := storage.UploadConsolidated(context.Background(), testS3Payloads())
+ require.NoError(err)
+ require.Equal(expected.Key, objectKey)
+ require.Len(placements, 2)
+}
+
+func TestBlobStorage_DownloadConsolidatedBlock_RangeReadsChunk(t *testing.T) {
+ require := testutil.Require(t)
+ object, metadatas, expectedBlocks, objectData, index := testS3CSCBProtoObject(t)
+ defer object.Close()
+
+ targetMetadata := metadatas[1]
+ _, targetChunk, err := index.LookupBlock(targetMetadata)
+ require.NoError(err)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ downloader := s3mocks.NewMockDownloader(ctrl)
+ uploader := s3mocks.NewMockUploader(ctrl)
+ client := s3mocks.NewMockClient(ctrl)
+ expectedRanges := map[string]int{
+ "bytes=0-65535": 1,
+ rangeHeader(targetChunk): 1,
+ }
+ expectRangeReads(t, client, objectData, expectedRanges)
+
+ var storage internal.BlobStorageCore
+ app := testapp.New(
+ t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_SOLANA, common.Network_NETWORK_SOLANA_MAINNET),
+ fx.Provide(newBlobStorage),
+ fx.Provide(func() s3.Downloader { return downloader }),
+ fx.Provide(func() s3.Uploader { return uploader }),
+ fx.Provide(func() s3.Client { return client }),
+ fx.Populate(&storage),
+ )
+ defer app.Close()
+
+ actual, err := storage.Download(context.Background(), targetMetadata)
+ require.NoError(err)
+ require.True(proto.Equal(expectedBlocks[1], actual))
+ assertRangeReadsConsumed(t, expectedRanges)
+}
+
+func TestBlobStorage_DownloadManyConsolidatedBlocks_GroupsByObjectAndChunk(t *testing.T) {
+ require := testutil.Require(t)
+ object, metadatas, expectedBlocks, objectData, index := testS3CSCBProtoObject(t)
+ defer object.Close()
+
+ _, chunk0, err := index.LookupBlock(metadatas[0])
+ require.NoError(err)
+ _, chunk1, err := index.LookupBlock(metadatas[2])
+ require.NoError(err)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ downloader := s3mocks.NewMockDownloader(ctrl)
+ uploader := s3mocks.NewMockUploader(ctrl)
+ client := s3mocks.NewMockClient(ctrl)
+ expectedRanges := map[string]int{
+ "bytes=0-65535": 1,
+ rangeHeader(chunk0): 1,
+ rangeHeader(chunk1): 1,
+ }
+ expectRangeReads(t, client, objectData, expectedRanges)
+
+ var storage internal.BlobStorageCore
+ app := testapp.New(
+ t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_SOLANA, common.Network_NETWORK_SOLANA_MAINNET),
+ fx.Provide(newBlobStorage),
+ fx.Provide(func() s3.Downloader { return downloader }),
+ fx.Provide(func() s3.Uploader { return uploader }),
+ fx.Provide(func() s3.Client { return client }),
+ fx.Populate(&storage),
+ )
+ defer app.Close()
+
+ actual, err := storage.DownloadMany(context.Background(), []*api.BlockMetadata{
+ metadatas[2],
+ metadatas[0],
+ metadatas[1],
+ })
+ require.NoError(err)
+ require.Len(actual, 3)
+ require.True(proto.Equal(expectedBlocks[2], actual[0]))
+ require.True(proto.Equal(expectedBlocks[0], actual[1]))
+ require.True(proto.Equal(expectedBlocks[1], actual[2]))
+ assertRangeReadsConsumed(t, expectedRanges)
+}
+
+func TestBlobStorage_DownloadManyConsolidatedBlocks_AllowsDuplicateInputs(t *testing.T) {
+ require := testutil.Require(t)
+ object, metadatas, expectedBlocks, objectData, index := testS3CSCBProtoObject(t)
+ defer object.Close()
+
+ _, chunk, err := index.LookupBlock(metadatas[1])
+ require.NoError(err)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ downloader := s3mocks.NewMockDownloader(ctrl)
+ uploader := s3mocks.NewMockUploader(ctrl)
+ client := s3mocks.NewMockClient(ctrl)
+ expectedRanges := map[string]int{
+ "bytes=0-65535": 1,
+ rangeHeader(chunk): 1,
+ }
+ expectRangeReads(t, client, objectData, expectedRanges)
+
+ var storage internal.BlobStorageCore
+ app := testapp.New(
+ t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_SOLANA, common.Network_NETWORK_SOLANA_MAINNET),
+ fx.Provide(newBlobStorage),
+ fx.Provide(func() s3.Downloader { return downloader }),
+ fx.Provide(func() s3.Uploader { return uploader }),
+ fx.Provide(func() s3.Client { return client }),
+ fx.Populate(&storage),
+ )
+ defer app.Close()
+
+ actual, err := storage.DownloadMany(context.Background(), []*api.BlockMetadata{
+ metadatas[1],
+ metadatas[1],
+ })
+ require.NoError(err)
+ require.Len(actual, 2)
+ require.True(proto.Equal(expectedBlocks[1], actual[0]))
+ require.True(proto.Equal(expectedBlocks[1], actual[1]))
+ assertRangeReadsConsumed(t, expectedRanges)
+}
+
+func TestReadExpectedRangeBodyAllowsShortInitialRead(t *testing.T) {
+ require := testutil.Require(t)
+
+ actual, err := readExpectedRangeBody(bytes.NewReader([]byte("short")), 64*1024, true)
+ require.NoError(err)
+ require.Equal([]byte("short"), actual)
+}
+
+func TestReadExpectedRangeBodyRejectsShortNonInitialRead(t *testing.T) {
+ require := testutil.Require(t)
+
+ _, err := readExpectedRangeBody(bytes.NewReader([]byte("short")), 10, false)
+ require.Error(err)
+ require.Contains(err.Error(), "range body length mismatch")
+}
+
+func TestReadExpectedRangeBodyRejectsOversizedRead(t *testing.T) {
+ require := testutil.Require(t)
+
+ _, err := readExpectedRangeBody(bytes.NewReader([]byte("too-long")), 3, true)
+ require.Error(err)
+ require.Contains(err.Error(), "range body length mismatch")
+}
+
+func TestBlobStorage_DownloadConsolidatedMetadataWithZeroLengthUsesSingleBlockPath(t *testing.T) {
+ require := testutil.Require(t)
+
+ metadata := &api.BlockMetadata{
+ Tag: 7,
+ Height: 100,
+ Hash: "hash-100",
+ ObjectKeyMain: "single-block/object",
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteLength: 0,
+ }
+ expectedBlock := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: metadata,
+ }
+ payload, err := proto.Marshal(expectedBlock)
+ require.NoError(err)
+
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ downloader := s3mocks.NewMockDownloader(ctrl)
+ downloader.EXPECT().Download(gomock.Any(), gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, writer io.WriterAt, input *awss3.GetObjectInput, opts ...func(*manager.Downloader)) (int64, error) {
+ require.Equal(metadata.ObjectKeyMain, aws.ToString(input.Key))
+ written, err := writer.WriteAt(payload, 0)
+ return int64(written), err
+ })
+ uploader := s3mocks.NewMockUploader(ctrl)
+ client := s3mocks.NewMockClient(ctrl)
+ client.EXPECT().GetObject(gomock.Any(), gomock.Any()).Times(0)
+
+ var storage internal.BlobStorageCore
+ app := testapp.New(
+ t,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_SOLANA, common.Network_NETWORK_SOLANA_MAINNET),
+ fx.Provide(newBlobStorage),
+ fx.Provide(func() s3.Downloader { return downloader }),
+ fx.Provide(func() s3.Uploader { return uploader }),
+ fx.Provide(func() s3.Client { return client }),
+ fx.Populate(&storage),
+ )
+ defer app.Close()
+
+ actual, err := storage.Download(context.Background(), metadata)
+ require.NoError(err)
+ require.True(proto.Equal(expectedBlock, actual))
+}
+
+func testS3EncodeConfig() cscb.EncodeConfig {
+ return cscb.EncodeConfig{
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Codec: api.Compression_ZSTD,
+ CodecLevel: 6,
+ MaxBlocks: 1000,
+ CompressionChunkBlocks: 10,
+ MaxCompressedBytes: 2147483648,
+ MaxUncompressedBytes: 137438953472,
+ ShardSize: 10000,
+ }
+}
+
+func testS3Payloads() []internal.ConsolidatedBlockPayload {
+ return []internal.ConsolidatedBlockPayload{
+ {
+ Metadata: &api.BlockMetadata{
+ Tag: 7,
+ Height: 100,
+ Hash: "hash-100",
+ },
+ MetadataID: 1001,
+ RawBlockPayload: internal.BytesPayloadSource([]byte("alpha")),
+ UncompressedLength: 5,
+ },
+ {
+ Metadata: &api.BlockMetadata{
+ Tag: 7,
+ Height: 101,
+ Hash: "hash-101",
+ },
+ MetadataID: 1002,
+ RawBlockPayload: internal.BytesPayloadSource([]byte("bravo!")),
+ UncompressedLength: 6,
+ },
+ }
+}
+
+func testS3CSCBProtoObject(t *testing.T) (*cscb.Object, []*api.BlockMetadata, []*api.Block, []byte, *cscb.Index) {
+ t.Helper()
+ require := testutil.Require(t)
+
+ blocks := []*api.Block{
+ {
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 7,
+ Height: 100,
+ Hash: "hash-100",
+ },
+ },
+ {
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 7,
+ Height: 101,
+ Hash: "hash-101",
+ },
+ },
+ {
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 7,
+ Height: 102,
+ Hash: "hash-102",
+ },
+ },
+ }
+ payloads := make([]internal.ConsolidatedBlockPayload, len(blocks))
+ for i, block := range blocks {
+ raw, err := proto.Marshal(block)
+ require.NoError(err)
+ payloads[i] = internal.ConsolidatedBlockPayload{
+ Metadata: block.Metadata,
+ MetadataID: int64(1000 + i),
+ RawBlockPayload: internal.BytesPayloadSource(raw),
+ UncompressedLength: uint64(len(raw)),
+ }
+ }
+
+ cfg := testS3EncodeConfig()
+ cfg.CompressionChunkBlocks = 2
+ object, err := cscb.Encode(context.Background(), cfg, payloads)
+ require.NoError(err)
+
+ data, ok := object.Bytes()
+ require.True(ok)
+ index, err := cscb.ParseIndex(data)
+ require.NoError(err)
+
+ metadatas := make([]*api.BlockMetadata, len(blocks))
+ expectedBlocks := make([]*api.Block, len(blocks))
+ for i, placement := range object.Placements {
+ metadata := proto.Clone(blocks[i].Metadata).(*api.BlockMetadata)
+ metadata.ObjectKeyMain = object.Key
+ metadata.ObjectFormat = placement.ObjectFormat
+ metadata.ByteOffset = placement.ByteOffset
+ metadata.ByteLength = placement.ByteLength
+ metadata.UncompressedLength = placement.UncompressedLength
+ metadatas[i] = metadata
+
+ expected := proto.Clone(blocks[i]).(*api.Block)
+ expected.Metadata = metadata
+ expectedBlocks[i] = expected
+ }
+ return object, metadatas, expectedBlocks, data, index
+}
+
+func expectRangeReads(t *testing.T, client *s3mocks.MockClient, objectData []byte, expectedRanges map[string]int) {
+ t.Helper()
+ require := testutil.Require(t)
+
+ totalReads := 0
+ for _, count := range expectedRanges {
+ totalReads += count
+ }
+ client.EXPECT().GetObject(gomock.Any(), gomock.Any()).Times(totalReads).
+ DoAndReturn(func(ctx context.Context, input *awss3.GetObjectInput, opts ...func(*awss3.Options)) (*awss3.GetObjectOutput, error) {
+ rangeValue := aws.ToString(input.Range)
+ require.Positive(expectedRanges[rangeValue], "unexpected range read %s", rangeValue)
+ expectedRanges[rangeValue]--
+
+ var start uint64
+ var end uint64
+ _, err := fmt.Sscanf(rangeValue, "bytes=%d-%d", &start, &end)
+ require.NoError(err)
+ require.Less(start, uint64(len(objectData)))
+ if end >= uint64(len(objectData)) {
+ end = uint64(len(objectData)) - 1
+ }
+ return &awss3.GetObjectOutput{
+ Body: io.NopCloser(bytes.NewReader(objectData[start : end+1])),
+ }, nil
+ })
+}
+
+func assertRangeReadsConsumed(t *testing.T, expectedRanges map[string]int) {
+ t.Helper()
+ require := testutil.Require(t)
+ for rangeValue, count := range expectedRanges {
+ require.Zero(count, "range %s was not read expected number of times", rangeValue)
+ }
+}
+
+func rangeHeader(chunk *cscb.ChunkDescriptor) string {
+ return fmt.Sprintf("bytes=%d-%d", chunk.CompressedPayloadOffset, chunk.CompressedPayloadOffset+chunk.CompressedLength-1)
+}
diff --git a/internal/storage/blobstorage/s3/blob_storage_integration_test.go b/internal/storage/blobstorage/s3/blob_storage_integration_test.go
index 5d5900c8..91b73b68 100644
--- a/internal/storage/blobstorage/s3/blob_storage_integration_test.go
+++ b/internal/storage/blobstorage/s3/blob_storage_integration_test.go
@@ -20,11 +20,11 @@ func TestIntegrationBlobStorage(t *testing.T) {
require := testutil.Require(t)
- var storage internal.BlobStorage
+ var storage internal.BlobStorageCore
app := testapp.New(
t,
testapp.WithIntegration(),
- fx.Provide(New),
+ fx.Provide(newBlobStorage),
s3.Module,
fx.Populate(&storage),
)
@@ -63,12 +63,12 @@ func TestIntegrationBlobStorageIntegration_GzipFormat(t *testing.T) {
require := testutil.Require(t)
- var storage internal.BlobStorage
+ var storage internal.BlobStorageCore
app := testapp.New(
t,
testapp.WithIntegration(),
testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_SOLANA, common.Network_NETWORK_SOLANA_MAINNET),
- fx.Provide(New),
+ fx.Provide(newBlobStorage),
s3.Module,
fx.Populate(&storage),
)
diff --git a/internal/storage/blobstorage/s3/blob_storage_test.go b/internal/storage/blobstorage/s3/blob_storage_test.go
index 87f4696b..cdd8b068 100644
--- a/internal/storage/blobstorage/s3/blob_storage_test.go
+++ b/internal/storage/blobstorage/s3/blob_storage_test.go
@@ -2,18 +2,16 @@ package s3
import (
"context"
- "fmt"
"io"
"testing"
- "github.com/aws/aws-sdk-go/awstesting"
- "github.com/aws/aws-sdk-go/awstesting/unit"
- awss3 "github.com/aws/aws-sdk-go/service/s3"
- "github.com/aws/aws-sdk-go/service/s3/s3manager"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ awss3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
"go.uber.org/fx"
"go.uber.org/mock/gomock"
- "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
"github.com/coinbase/chainstorage/internal/s3"
s3mocks "github.com/coinbase/chainstorage/internal/s3/mocks"
"github.com/coinbase/chainstorage/internal/storage/blobstorage/internal"
@@ -34,8 +32,8 @@ func TestBlobStorage_NoCompression(t *testing.T) {
defer ctrl.Finish()
downloader := s3mocks.NewMockDownloader(ctrl)
- downloader.EXPECT().DownloadWithContext(gomock.Any(), gomock.Any(), gomock.Any()).
- DoAndReturn(func(ctx context.Context, writer io.WriterAt, input *awss3.GetObjectInput, opts ...jsonrpc.Option) (int64, error) {
+ downloader.EXPECT().Download(gomock.Any(), gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, writer io.WriterAt, input *awss3.GetObjectInput, opts ...func(*manager.Downloader)) (int64, error) {
require.NotNil(input.Bucket)
require.NotEmpty(*input.Bucket)
require.NotNil(input.Key)
@@ -45,8 +43,8 @@ func TestBlobStorage_NoCompression(t *testing.T) {
})
uploader := s3mocks.NewMockUploader(ctrl)
- uploader.EXPECT().UploadWithContext(gomock.Any(), gomock.Any()).
- DoAndReturn(func(ctx context.Context, input *s3manager.UploadInput, opts ...jsonrpc.Option) (*s3manager.UploadOutput, error) {
+ uploader.EXPECT().Upload(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, input *awss3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) {
require.NotNil(input.Bucket)
require.NotEmpty(*input.Bucket)
require.NotNil(input.Key)
@@ -54,14 +52,14 @@ func TestBlobStorage_NoCompression(t *testing.T) {
require.NotNil(input.ContentMD5)
require.NotEmpty(*input.ContentMD5)
- return &s3manager.UploadOutput{}, nil
+ return &manager.UploadOutput{}, nil
})
client := s3mocks.NewMockClient(ctrl)
- var storage internal.BlobStorage
+ var storage internal.BlobStorageCore
app := testapp.New(
t,
- fx.Provide(New),
+ fx.Provide(newBlobStorage),
fx.Provide(func() s3.Downloader { return downloader }),
fx.Provide(func() s3.Uploader { return uploader }),
fx.Provide(func() s3.Client { return client }),
@@ -103,8 +101,8 @@ func TestBlobStorage_NoCompression_WithSidechain(t *testing.T) {
defer ctrl.Finish()
downloader := s3mocks.NewMockDownloader(ctrl)
- downloader.EXPECT().DownloadWithContext(gomock.Any(), gomock.Any(), gomock.Any()).
- DoAndReturn(func(ctx context.Context, writer io.WriterAt, input *awss3.GetObjectInput, opts ...jsonrpc.Option) (int64, error) {
+ downloader.EXPECT().Download(gomock.Any(), gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, writer io.WriterAt, input *awss3.GetObjectInput, opts ...func(*manager.Downloader)) (int64, error) {
require.NotNil(input.Bucket)
require.NotEmpty(*input.Bucket)
require.NotNil(input.Key)
@@ -114,25 +112,25 @@ func TestBlobStorage_NoCompression_WithSidechain(t *testing.T) {
})
uploader := s3mocks.NewMockUploader(ctrl)
- uploader.EXPECT().UploadWithContext(gomock.Any(), gomock.Any()).
- DoAndReturn(func(ctx context.Context, input *s3manager.UploadInput, opts ...jsonrpc.Option) (*s3manager.UploadOutput, error) {
+ uploader.EXPECT().Upload(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, input *awss3.PutObjectInput, opts ...func(*manager.Uploader)) (*manager.UploadOutput, error) {
require.NotNil(input.Bucket)
require.NotEmpty(*input.Bucket)
require.NotNil(input.Key)
require.Equal(expectedObjectKey, *input.Key)
require.NotNil(input.ContentMD5)
require.NotEmpty(*input.ContentMD5)
- require.Equal(*input.ACL, bucketOwnerFullControl)
+ require.Equal(awss3types.ObjectCannedACLBucketOwnerFullControl, input.ACL)
- return &s3manager.UploadOutput{}, nil
+ return &manager.UploadOutput{}, nil
})
client := s3mocks.NewMockClient(ctrl)
- var storage internal.BlobStorage
+ var storage internal.BlobStorageCore
app := testapp.New(
t,
testapp.WithBlockchainNetworkSidechain(common.Blockchain_BLOCKCHAIN_ETHEREUM, common.Network_NETWORK_ETHEREUM_MAINNET, api.SideChain_SIDECHAIN_ETHEREUM_MAINNET_BEACON),
- fx.Provide(New),
+ fx.Provide(newBlobStorage),
fx.Provide(func() s3.Downloader { return downloader }),
fx.Provide(func() s3.Uploader { return uploader }),
fx.Provide(func() s3.Client { return client }),
@@ -168,10 +166,10 @@ func TestBlobStorage_NoCompression_WithSidechain(t *testing.T) {
func TestBlobStorage_NoCompression_SkippedBlock(t *testing.T) {
require := testutil.Require(t)
- var storage internal.BlobStorage
+ var storage internal.BlobStorageCore
app := testapp.New(
t,
- fx.Provide(New),
+ fx.Provide(newBlobStorage),
fx.Provide(func() s3.Downloader { return nil }),
fx.Provide(func() s3.Uploader { return nil }),
fx.Provide(func() s3.Client { return nil }),
@@ -211,13 +209,19 @@ func TestBlobStorage_DownloadErrRequestCanceled(t *testing.T) {
defer ctrl.Finish()
uploader := s3mocks.NewMockUploader(ctrl)
- downloader := s3manager.NewDownloader(unit.Session)
+ downloader := s3mocks.NewMockDownloader(ctrl)
client := s3mocks.NewMockClient(ctrl)
- var blobStorage internal.BlobStorage
+ // Setup mock to return context canceled error
+ downloader.EXPECT().Download(gomock.Any(), gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, writer io.WriterAt, input *awss3.GetObjectInput, opts ...func(*manager.Downloader)) (int64, error) {
+ return 0, context.Canceled
+ })
+
+ var blobStorage internal.BlobStorageCore
app := testapp.New(
t,
- fx.Provide(New),
+ fx.Provide(newBlobStorage),
fx.Provide(func() s3.Downloader { return downloader }),
fx.Provide(func() s3.Uploader { return uploader }),
fx.Provide(func() s3.Client { return client }),
@@ -226,17 +230,16 @@ func TestBlobStorage_DownloadErrRequestCanceled(t *testing.T) {
defer app.Close()
require.NotNil(blobStorage)
- ctx := &awstesting.FakeContext{DoneCh: make(chan struct{})}
- ctx.Error = fmt.Errorf("context canceled")
- close(ctx.DoneCh)
-
metadata := &api.BlockMetadata{
Tag: 1,
Height: 12345,
Hash: "0xabcde",
ObjectKeyMain: "some download key",
}
- _, err := blobStorage.Download(ctx, metadata)
+ _, err := blobStorage.Download(context.Background(), metadata)
require.Error(err)
require.Equal(errors.ErrRequestCanceled, err)
}
+
+// Silence unused import warning
+var _ = aws.String
diff --git a/internal/storage/blobstorage/s3/module.go b/internal/storage/blobstorage/s3/module.go
index 714c6cf9..1868bf93 100644
--- a/internal/storage/blobstorage/s3/module.go
+++ b/internal/storage/blobstorage/s3/module.go
@@ -7,6 +7,6 @@ import (
var Module = fx.Options(
fx.Provide(fx.Annotated{
Name: "blobstorage/s3",
- Target: NewFactory,
- }),
+ Target: newFactory,
+ }, fx.Private),
)
diff --git a/internal/storage/blobstorage/single_block_uploader.go b/internal/storage/blobstorage/single_block_uploader.go
new file mode 100644
index 00000000..681b140c
--- /dev/null
+++ b/internal/storage/blobstorage/single_block_uploader.go
@@ -0,0 +1,106 @@
+package blobstorage
+
+import (
+ "context"
+
+ "go.uber.org/fx"
+
+ storageinternal "github.com/coinbase/chainstorage/internal/storage/blobstorage/internal"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ // SingleBlockUploader is the only production interface that can create
+ // single-block objects. Every call is serialized with the metadata retirement
+ // fence before it reaches the raw blob store.
+ SingleBlockUploader interface {
+ Upload(ctx context.Context, block *api.Block, compression api.Compression) (string, error)
+ UploadRaw(ctx context.Context, rawBlockData *RawBlockData) (string, error)
+ }
+
+ blobStorageProviderParams struct {
+ fx.In
+ fxparams.Params
+ S3 storageinternal.BlobStorageFactory `name:"blobstorage/s3"`
+ GCS storageinternal.BlobStorageFactory `name:"blobstorage/gcs"`
+ MetaStorage metastorage.MetaStorage
+ }
+
+ blobStorageResult struct {
+ fx.Out
+ BlobStorage BlobStorage
+ SingleBlockUploader SingleBlockUploader
+ }
+
+ safeBlobStorage struct {
+ storage storageinternal.BlobStorage
+ }
+
+ guardedSingleBlockUploader struct {
+ raw storageinternal.SingleBlockUploader
+ metaStorage metastorage.MetaStorage
+ }
+)
+
+func withBlobStorageFactory(params blobStorageProviderParams) (blobStorageResult, error) {
+ core, err := storageinternal.WithBlobStorageFactory(storageinternal.BlobStorageFactoryParams{
+ Params: params.Params,
+ S3: params.S3,
+ GCS: params.GCS,
+ })
+ if err != nil {
+ return blobStorageResult{}, err
+ }
+ return blobStorageResult{
+ BlobStorage: &safeBlobStorage{storage: core},
+ SingleBlockUploader: &guardedSingleBlockUploader{
+ raw: core,
+ metaStorage: params.MetaStorage,
+ },
+ }, nil
+}
+
+func (s *safeBlobStorage) UploadConsolidated(ctx context.Context, blocks []ConsolidatedBlockPayload) (string, []BlockPlacement, error) {
+ return s.storage.UploadConsolidated(ctx, blocks)
+}
+
+func (s *safeBlobStorage) Download(ctx context.Context, metadata *api.BlockMetadata) (*api.Block, error) {
+ return s.storage.Download(ctx, metadata)
+}
+
+func (s *safeBlobStorage) DownloadMany(ctx context.Context, metadata []*api.BlockMetadata) ([]*api.Block, error) {
+ return s.storage.DownloadMany(ctx, metadata)
+}
+
+func (s *safeBlobStorage) PreSign(ctx context.Context, objectKey string) (string, error) {
+ return s.storage.PreSign(ctx, objectKey)
+}
+
+func (u *guardedSingleBlockUploader) Upload(
+ ctx context.Context,
+ block *api.Block,
+ compression api.Compression,
+) (string, error) {
+ var metadata *api.BlockMetadata
+ if block != nil {
+ metadata = block.Metadata
+ }
+ return metastorage.UploadSingleBlockObject(ctx, u.metaStorage, metadata, func() (string, error) {
+ return u.raw.Upload(ctx, block, compression)
+ })
+}
+
+func (u *guardedSingleBlockUploader) UploadRaw(
+ ctx context.Context,
+ rawBlockData *RawBlockData,
+) (string, error) {
+ var metadata *api.BlockMetadata
+ if rawBlockData != nil {
+ metadata = rawBlockData.BlockMetadata
+ }
+ return metastorage.UploadSingleBlockObject(ctx, u.metaStorage, metadata, func() (string, error) {
+ return u.raw.UploadRaw(ctx, rawBlockData)
+ })
+}
diff --git a/internal/storage/blobstorage/single_block_uploader_test.go b/internal/storage/blobstorage/single_block_uploader_test.go
new file mode 100644
index 00000000..443376b5
--- /dev/null
+++ b/internal/storage/blobstorage/single_block_uploader_test.go
@@ -0,0 +1,112 @@
+package blobstorage
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "go.uber.org/fx"
+ "go.uber.org/mock/gomock"
+
+ storageinternal "github.com/coinbase/chainstorage/internal/storage/blobstorage/internal"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ metastoragemocks "github.com/coinbase/chainstorage/internal/storage/metastorage/mocks"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type singleBlockUploadTestRaw struct {
+ uploadCalls int
+ uploadRawCalls int
+}
+
+func (u *singleBlockUploadTestRaw) Upload(context.Context, *api.Block, api.Compression) (string, error) {
+ u.uploadCalls++
+ return "single-block-key", nil
+}
+
+func (u *singleBlockUploadTestRaw) UploadRaw(context.Context, *storageinternal.RawBlockData) (string, error) {
+ u.uploadRawCalls++
+ return "single-block-raw-key", nil
+}
+
+type fencedSingleBlockUploadTestGuard struct{}
+
+func (fencedSingleBlockUploadTestGuard) RetirementFenced() bool {
+ return true
+}
+
+func (fencedSingleBlockUploadTestGuard) Release() error {
+ return nil
+}
+
+func TestSingleBlockUploaderRejectsFencedUploadBeforeRawStorage(t *testing.T) {
+ controller := gomock.NewController(t)
+ metaStorage := metastoragemocks.NewMockMetaStorage(controller)
+ raw := &singleBlockUploadTestRaw{}
+ uploader := &guardedSingleBlockUploader{raw: raw, metaStorage: metaStorage}
+ block := &api.Block{Metadata: &api.BlockMetadata{Tag: 2, Height: 123, Hash: "hash"}}
+ metaStorage.EXPECT().AcquireSingleBlockUploadGuard(gomock.Any(), uint32(2), uint64(123), "hash").
+ Return(fencedSingleBlockUploadTestGuard{}, nil)
+
+ _, err := uploader.Upload(context.Background(), block, api.Compression_GZIP)
+ require.ErrorIs(t, err, metastorage.ErrSingleBlockRetirementFenced)
+ require.Zero(t, raw.uploadCalls)
+}
+
+func TestSingleBlockUploaderGuardsRawUpload(t *testing.T) {
+ controller := gomock.NewController(t)
+ metaStorage := metastoragemocks.NewMockMetaStorage(controller)
+ raw := &singleBlockUploadTestRaw{}
+ uploader := &guardedSingleBlockUploader{raw: raw, metaStorage: metaStorage}
+ rawBlock := &RawBlockData{BlockMetadata: &api.BlockMetadata{Tag: 2, Height: 124, Hash: "hash-raw"}}
+ metaStorage.EXPECT().AcquireSingleBlockUploadGuard(gomock.Any(), uint32(2), uint64(124), "hash-raw").
+ Return(metastorage.NewUnfencedSingleBlockUploadGuard(), nil)
+
+ key, err := uploader.UploadRaw(context.Background(), rawBlock)
+ require.NoError(t, err)
+ require.Equal(t, "single-block-raw-key", key)
+ require.Equal(t, 1, raw.uploadRawCalls)
+}
+
+func TestSafeBlobStorageDoesNotExposeSingleBlockUploads(t *testing.T) {
+ core := &singleBlockUploadTestCore{}
+ var storage BlobStorage = &safeBlobStorage{storage: core}
+
+ _, exposesRawUpload := storage.(SingleBlockUploader)
+ require.False(t, exposesRawUpload)
+}
+
+func TestModuleDoesNotExposeRawBlobStorageFactory(t *testing.T) {
+ type rawFactoryParams struct {
+ fx.In
+ S3 storageinternal.BlobStorageFactory `name:"blobstorage/s3"`
+ }
+
+ app := fx.New(
+ Module,
+ fx.NopLogger,
+ fx.Invoke(func(rawFactoryParams) {}),
+ )
+ require.Error(t, app.Err())
+ require.Contains(t, app.Err().Error(), "blobstorage/s3")
+}
+
+type singleBlockUploadTestCore struct {
+ singleBlockUploadTestRaw
+}
+
+func (*singleBlockUploadTestCore) UploadConsolidated(context.Context, []storageinternal.ConsolidatedBlockPayload) (string, []storageinternal.BlockPlacement, error) {
+ return "", nil, nil
+}
+
+func (*singleBlockUploadTestCore) Download(context.Context, *api.BlockMetadata) (*api.Block, error) {
+ return nil, nil
+}
+
+func (*singleBlockUploadTestCore) DownloadMany(context.Context, []*api.BlockMetadata) ([]*api.Block, error) {
+ return nil, nil
+}
+
+func (*singleBlockUploadTestCore) PreSign(context.Context, string) (string, error) {
+ return "", nil
+}
diff --git a/internal/storage/cscbrepair/full_lifecycle_integration_test.go b/internal/storage/cscbrepair/full_lifecycle_integration_test.go
new file mode 100644
index 00000000..52d19041
--- /dev/null
+++ b/internal/storage/cscbrepair/full_lifecycle_integration_test.go
@@ -0,0 +1,1177 @@
+package cscbrepair_test
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "database/sql"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ awss3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/pressly/goose/v3"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/fx"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ chains3 "github.com/coinbase/chainstorage/internal/s3"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage"
+ "github.com/coinbase/chainstorage/internal/storage/cscbrepair"
+ "github.com/coinbase/chainstorage/internal/storage/cscbrepairlock"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ metapostgres "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+ storageutils "github.com/coinbase/chainstorage/internal/storage/utils"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestIntegrationCSCBRepairPendingRangeIsolation(t *testing.T) {
+ if os.Getenv("TEST_TYPE") != "integration" {
+ t.Skip("integration test")
+ }
+ require := require.New(t)
+ ctx := context.Background()
+ unique := time.Now().UTC().UnixNano()
+ tag := uint32(1_600_000_000 + unique%100_000_000)
+ startHeight := uint64(8_500_000_000 + unique%100_000_000)
+ endHeight := startHeight + 1_000
+
+ cfg, err := config.New(
+ config.WithEnvironment(config.EnvLocal),
+ config.WithBlockchain(common.Blockchain_BLOCKCHAIN_SOLANA),
+ config.WithNetwork(common.Network_NETWORK_SOLANA_MAINNET),
+ )
+ require.NoError(err)
+ if cfg.AWS.Postgres == nil {
+ t.Skip("Postgres is not configured")
+ }
+ configureRepairTestEnvironment(t, cfg.AWS.Postgres)
+ db, err := openRepairDB(ctx, cfg.AWS.Postgres)
+ require.NoError(err)
+ defer func() { _ = db.Close() }()
+ goose.SetBaseFS(metapostgres.GetEmbeddedMigrations())
+ require.NoError(goose.SetDialect("postgres"))
+ require.NoError(goose.UpContext(ctx, db, "db/migrations"))
+
+ type pendingRange struct {
+ key string
+ start uint64
+ end uint64
+ }
+ insertPending := func(tag uint32, pending pendingRange) {
+ _, err := db.ExecContext(ctx, `
+ INSERT INTO cscb_repair_manifest (
+ tag, state, bucket, old_consolidated_object_key_main,
+ start_height, end_height, canonical_block_count, total_block_count, row_set_sha256
+ ) VALUES ($1, $2, $3, $4, $5, $6, 0, 0, $7)`,
+ tag,
+ cscbrepair.StatePreparing,
+ "range-isolation-bucket",
+ pending.key,
+ pending.start,
+ pending.end,
+ repairSHA256(pending.key),
+ )
+ require.NoError(err)
+ }
+ inRangeKey := fmt.Sprintf("consolidated/in-range-%d.cscb.zstd", unique)
+ pendingRanges := []pendingRange{
+ {key: fmt.Sprintf("consolidated/below-%d.cscb.zstd", unique), start: startHeight - 1_000, end: startHeight},
+ {key: fmt.Sprintf("consolidated/above-%d.cscb.zstd", unique), start: endHeight, end: endHeight + 1_000},
+ {key: inRangeKey, start: startHeight, end: endHeight},
+ }
+ for _, pending := range pendingRanges {
+ insertPending(tag, pending)
+ }
+
+ repository := cscbrepair.NewPostgresRepository(db)
+ pending, err := repository.FindPending(ctx, tag, startHeight, endHeight)
+ require.NoError(err)
+ require.NotNil(pending)
+ require.Equal(inRangeKey, pending.OldConsolidatedObjectKey)
+
+ objectKeys, err := repository.ListCandidateObjectKeys(ctx, tag, startHeight, endHeight, 10)
+ require.NoError(err)
+ require.Equal([]string{inRangeKey}, objectKeys)
+
+ emptyStart := endHeight + 2_000
+ pending, err = repository.FindPending(ctx, tag, emptyStart, emptyStart+1_000)
+ require.NoError(err)
+ require.Nil(pending)
+ objectKeys, err = repository.ListCandidateObjectKeys(ctx, tag, emptyStart, emptyStart+1_000, 10)
+ require.NoError(err)
+ require.Empty(objectKeys)
+
+ crossingTag := tag + 1
+ crossingKey := fmt.Sprintf("consolidated/cross-right-%d.cscb.zstd", unique)
+ insertPending(crossingTag, pendingRange{
+ key: crossingKey,
+ start: startHeight + 500,
+ end: endHeight + 1,
+ })
+ pending, err = repository.FindPending(ctx, crossingTag, startHeight, endHeight)
+ require.NoError(err)
+ require.NotNil(pending)
+ require.Equal(crossingKey, pending.OldConsolidatedObjectKey)
+ _, err = repository.ListCandidateObjectKeys(ctx, crossingTag, startHeight, endHeight, 10)
+ require.ErrorContains(err, "exceeds approved range")
+}
+
+func TestIntegrationCSCBRepairFullLifecycle(t *testing.T) {
+ if os.Getenv("TEST_TYPE") != "integration" {
+ t.Skip("integration test")
+ }
+ require := require.New(t)
+ ctx := context.Background()
+ unique := time.Now().UTC().UnixNano()
+ tag := uint32(1_700_000_000 + unique%100_000_000)
+ height := uint64(9_500_000_000 + unique%100_000_000)
+ bucket := fmt.Sprintf("chainstorage-cscb-repair-%d", unique)
+
+ cfg, err := config.New(
+ config.WithEnvironment(config.EnvLocal),
+ config.WithBlockchain(common.Blockchain_BLOCKCHAIN_SOLANA),
+ config.WithNetwork(common.Network_NETWORK_SOLANA_MAINNET),
+ )
+ require.NoError(err)
+ if cfg.AWS.Postgres == nil {
+ t.Skip("Postgres is not configured")
+ }
+ cfg.StorageType.BlobStorageType = config.BlobStorageType_S3
+ cfg.StorageType.MetaStorageType = config.MetaStorageType_POSTGRES
+ cfg.AWS.Bucket = bucket
+ cfg.AWS.IsLocalStack = true
+ cfg.AWS.IsResetLocal = true
+ localStackEndpoint := configureRepairTestEnvironment(t, cfg.AWS.Postgres)
+ cfg.Chain.BlockStartHeight = height
+ cfg.AWS.Storage.Consolidation.Enabled = true
+ cfg.AWS.Storage.Consolidation.Mode = config.ConsolidationModeHistoricalBackfill
+
+ db, err := openRepairDB(ctx, cfg.AWS.Postgres)
+ require.NoError(err)
+ defer func() { _ = db.Close() }()
+ goose.SetBaseFS(metapostgres.GetEmbeddedMigrations())
+ require.NoError(goose.SetDialect("postgres"))
+ require.NoError(goose.UpContext(ctx, db, "db/migrations"))
+
+ var (
+ meta metastorage.MetaStorage
+ blob blobstorage.BlobStorage
+ singleBlockUploader blobstorage.SingleBlockUploader
+ s3Session *chains3.S3
+ )
+ app := testapp.New(
+ t,
+ testapp.WithIntegration(),
+ testapp.WithConfig(cfg),
+ chains3.Module,
+ metastorage.Module,
+ blobstorage.Module,
+ fx.Replace(newRepairLocalStackConfig(ctx, localStackEndpoint)),
+ fx.Populate(&meta, &blob, &singleBlockUploader, &s3Session),
+ )
+ defer app.Close()
+ require.NotNil(meta)
+ require.NotNil(blob)
+ require.NotNil(singleBlockUploader)
+ require.NotNil(s3Session)
+
+ rawS3 := awss3.NewFromConfig(s3Session.Config)
+ _, err = rawS3.PutBucketVersioning(ctx, &awss3.PutBucketVersioningInput{
+ Bucket: aws.String(bucket),
+ VersioningConfiguration: &awss3types.VersioningConfiguration{
+ Status: awss3types.BucketVersioningStatusEnabled,
+ },
+ })
+ require.NoError(err)
+ defer cleanupRepairBucket(t, rawS3, bucket)
+ store := retirement.NewS3ObjectStore(rawS3)
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: tag,
+ Height: height,
+ Hash: fmt.Sprintf("repair-hash-%d", unique),
+ ParentHash: fmt.Sprintf("repair-parent-%d", unique),
+ ParentHeight: height - 1,
+ Timestamp: timestamppb.New(time.Now().UTC().Truncate(time.Second)),
+ },
+ Blobdata: &api.Block_Solana{
+ Solana: &api.SolanaBlobdata{Header: []byte(`{"slot":9500000000,"transactions":["repair"]}`)},
+ },
+ }
+ historicalBlock := proto.Clone(block).(*api.Block)
+ historicalBlock.GetSolana().Header = []byte(`{"slot":9500000000,"transactions":["stale"]}`)
+ singleBlockKey, err := singleBlockUploader.Upload(ctx, historicalBlock, api.Compression_GZIP)
+ require.NoError(err)
+ semanticDuplicate := proto.Clone(block).(*api.Block)
+ semanticDuplicate.GetSolana().Header = []byte("{\n \"transactions\": [\"repair\"],\n \"slot\": 9500000000\n}")
+ require.JSONEq(string(block.GetSolana().Header), string(semanticDuplicate.GetSolana().Header))
+ require.NotEqual(string(historicalBlock.GetSolana().Header), string(semanticDuplicate.GetSolana().Header))
+ duplicateSingleBlockKey, err := singleBlockUploader.Upload(ctx, semanticDuplicate, api.Compression_GZIP)
+ require.NoError(err)
+ require.Equal(singleBlockKey, duplicateSingleBlockKey)
+ singleBlockTopology, err := store.ListObjectVersions(ctx, bucket, singleBlockKey)
+ require.NoError(err)
+ require.Len(singleBlockTopology.Versions, 2)
+ require.Empty(singleBlockTopology.DeleteMarkers)
+ var currentSingleBlockVersion, historicalSingleBlockVersion retirement.ObjectVersion
+ for _, version := range singleBlockTopology.Versions {
+ if version.IsLatest {
+ currentSingleBlockVersion = version
+ } else {
+ historicalSingleBlockVersion = version
+ }
+ }
+ require.NotEmpty(currentSingleBlockVersion.VersionID)
+ require.NotEmpty(historicalSingleBlockVersion.VersionID)
+ require.NotEqual(currentSingleBlockVersion.ETag, historicalSingleBlockVersion.ETag)
+ require.NotEqual(currentSingleBlockVersion.Bytes, historicalSingleBlockVersion.Bytes)
+ block.Metadata.ObjectKeyMain = singleBlockKey
+ require.NoError(meta.PersistBlockMetas(ctx, true, []*api.BlockMetadata{block.Metadata}, nil))
+
+ records, err := meta.GetBlocksMissingConsolidationShadow(ctx, tag, height, height+1, 1)
+ require.NoError(err)
+ require.Len(records, 1)
+ blockMetadataID := records[0].ID
+ var repairID int64
+ defer cleanupRepairMetadata(t, db, blockMetadataID, &repairID)
+
+ downloaded, err := blob.Download(ctx, records[0].Metadata)
+ require.NoError(err)
+ require.True(proto.Equal(
+ storageutils.CloneBlockWithoutStoragePlacement(semanticDuplicate),
+ storageutils.CloneBlockWithoutStoragePlacement(downloaded),
+ ))
+ dirtyBlock := proto.Clone(historicalBlock).(*api.Block)
+ dirtyBlock.Metadata.ObjectKeyMain = singleBlockKey
+ dirtyBlock.Metadata.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK
+ require.True(storageutils.HasBlockStoragePlacement(dirtyBlock))
+ require.False(proto.Equal(
+ storageutils.CloneBlockWithoutStoragePlacement(semanticDuplicate),
+ storageutils.CloneBlockWithoutStoragePlacement(dirtyBlock),
+ ))
+ dirtyPayload, err := proto.Marshal(dirtyBlock)
+ require.NoError(err)
+ dirtyKey, dirtyPlacements, err := blob.UploadConsolidated(ctx, []blobstorage.ConsolidatedBlockPayload{{
+ Metadata: records[0].Metadata,
+ MetadataID: blockMetadataID,
+ RawBlockPayload: blobstorage.BytesPayloadSource(dirtyPayload),
+ UncompressedLength: uint64(len(dirtyPayload)),
+ }})
+ require.NoError(err)
+ require.Len(dirtyPlacements, 1)
+ dirtyPlacement := dirtyPlacements[0]
+ require.NoError(meta.PersistBlockConsolidationShadows(ctx, []*metastorage.ConsolidationShadowPlacement{{
+ BlockMetadataID: blockMetadataID,
+ Tag: tag,
+ Height: height,
+ Hash: block.Metadata.Hash,
+ SingleBlockObjectKeyMain: singleBlockKey,
+ ConsolidatedObjectKeyMain: dirtyKey,
+ ObjectFormat: dirtyPlacement.ObjectFormat,
+ ByteOffset: dirtyPlacement.ByteOffset,
+ ByteLength: dirtyPlacement.ByteLength,
+ UncompressedLength: dirtyPlacement.UncompressedLength,
+ }}))
+ promotion, err := meta.PromoteBlockConsolidationShadows(ctx, tag, height, height+1, 1, 72*time.Hour)
+ require.NoError(err)
+ require.Equal(uint64(1), promotion.Blocks)
+
+ repository := cscbrepair.NewPostgresRepository(db)
+ candidateLockTx, err := db.BeginTx(ctx, nil)
+ require.NoError(err)
+ require.NoError(cscbrepairlock.AcquireTag(ctx, candidateLockTx, tag))
+ listCtx, cancelList := context.WithTimeout(ctx, 2*time.Second)
+ candidateKeys, err := repository.ListCandidateObjectKeys(listCtx, tag, height, height+1, 10)
+ cancelList()
+ require.NoError(err)
+ require.Equal([]string{dirtyKey}, candidateKeys)
+ require.NoError(candidateLockTx.Rollback(), "read-only discovery must not wait for the placement-writer lock")
+ exactCandidate, err := repository.FindCandidateByObjectKey(ctx, tag, dirtyKey)
+ require.NoError(err)
+ require.Equal(dirtyKey, exactCandidate.OldConsolidatedObjectKey)
+
+ repairer := cscbrepair.NewRepairer(repository, store, bucket)
+ executionKey := repairSHA256(fmt.Sprintf("repair-main-%d", unique))
+
+ // A writer that acquired the upload guard before repair preparation must
+ // finish its S3 PUT before the repair can fence and pin the current version.
+ preFenceGuard, err := meta.AcquireSingleBlockUploadGuard(ctx, tag, height, block.Metadata.Hash)
+ require.NoError(err)
+ require.False(preFenceGuard.RetirementFenced())
+ oldCurrentSingleBlockVersion := currentSingleBlockVersion
+ guardedPayload, err := store.ReadObjectVersion(ctx, bucket, singleBlockKey, oldCurrentSingleBlockVersion.VersionID)
+ require.NoError(err)
+ type prepareResult struct {
+ manifest *cscbrepair.Manifest
+ err error
+ }
+ prepared := make(chan prepareResult, 1)
+ go func() {
+ preparedManifest, prepareErr := repairer.PrepareObject(
+ ctx,
+ executionKey,
+ tag,
+ height,
+ height+1,
+ 1,
+ dirtyKey,
+ nil,
+ )
+ prepared <- prepareResult{manifest: preparedManifest, err: prepareErr}
+ }()
+ select {
+ case result := <-prepared:
+ require.Failf("repair preparation bypassed in-flight upload guard", "result=%+v", result)
+ case <-time.After(100 * time.Millisecond):
+ }
+ guardedPut, err := rawS3.PutObject(ctx, &awss3.PutObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(singleBlockKey),
+ Body: bytes.NewReader(guardedPayload),
+ })
+ require.NoError(err)
+ require.NotNil(guardedPut.VersionId)
+ require.NoError(preFenceGuard.Release())
+ preparedResult := <-prepared
+ require.NoError(preparedResult.err)
+ manifest := preparedResult.manifest
+ require.NotNil(manifest)
+ repairID = manifest.ID
+ postWriterTopology, err := store.ListObjectVersions(ctx, bucket, singleBlockKey)
+ require.NoError(err)
+ for _, version := range postWriterTopology.Versions {
+ if version.VersionID == aws.ToString(guardedPut.VersionId) {
+ currentSingleBlockVersion = version
+ }
+ }
+ require.Equal(aws.ToString(guardedPut.VersionId), currentSingleBlockVersion.VersionID)
+ _, err = rawS3.DeleteObject(ctx, &awss3.DeleteObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(singleBlockKey),
+ VersionId: aws.String(oldCurrentSingleBlockVersion.VersionID),
+ })
+ require.NoError(err)
+ require.Equal(cscbrepair.StatePrepared, manifest.State)
+ require.Equal(dirtyKey, manifest.OldConsolidatedObjectKey)
+ require.Len(manifest.Blocks, 1)
+ require.Equal(currentSingleBlockVersion.VersionID, manifest.Blocks[0].SingleBlockObjectVersion.VersionID)
+ require.Len(manifest.Blocks[0].PayloadSHA256, 64)
+ pendingKeys, err := repository.ListCandidateObjectKeys(ctx, tag, height, height+1, 10)
+ require.NoError(err)
+ require.Equal([]string{dirtyKey}, pendingKeys)
+
+ // The trigger must serialize any reference to the pinned key, even when a
+ // writer changes only object_format to a non-CSCB value. This closes the
+ // final-reference-check race independently of the writer's claimed format.
+ objectLockTx, err := db.BeginTx(ctx, nil)
+ require.NoError(err)
+ _, err = objectLockTx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 1))`, dirtyKey)
+ require.NoError(err)
+ blockedWrite := make(chan error, 1)
+ writeCtx, cancelWrite := context.WithTimeout(ctx, 5*time.Second)
+ defer cancelWrite()
+ go func() {
+ _, writeErr := db.ExecContext(writeCtx, `
+ UPDATE block_metadata
+ SET object_format = $2
+ WHERE id = $1`, blockMetadataID, api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK)
+ blockedWrite <- writeErr
+ }()
+ select {
+ case writeErr := <-blockedWrite:
+ require.Failf("format-only old-key writer bypassed advisory lock", "error=%v", writeErr)
+ case <-time.After(100 * time.Millisecond):
+ }
+ require.NoError(objectLockTx.Commit())
+ require.ErrorContains(<-blockedWrite, "cannot reference a pinned old CSCB object")
+
+ uploadGuard, err := meta.AcquireSingleBlockUploadGuard(ctx, tag, height, block.Metadata.Hash)
+ require.NoError(err)
+ require.True(uploadGuard.RetirementFenced(), "active repair must fence single-block uploads")
+ require.NoError(uploadGuard.Release())
+ replayedSingleBlock := proto.Clone(block.Metadata).(*api.BlockMetadata)
+ replayedSingleBlock.ObjectKeyMain = singleBlockKey
+ replayedSingleBlock.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK
+ replayedSingleBlock.ByteOffset = 0
+ replayedSingleBlock.ByteLength = 0
+ replayedSingleBlock.UncompressedLength = 0
+ require.NoError(meta.PersistBlockMetas(ctx, false, []*api.BlockMetadata{replayedSingleBlock}, nil))
+ pinnedActive, err := meta.GetBlockByHeight(ctx, tag, height)
+ require.NoError(err)
+ require.Equal(dirtyKey, pinnedActive.ObjectKeyMain, "late metadata persistence must not strand a prepared repair")
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, pinnedActive.ObjectFormat)
+ require.Equal(manifest.Blocks[0].OldByteOffset, pinnedActive.ByteOffset)
+ require.Equal(manifest.Blocks[0].OldByteLength, pinnedActive.ByteLength)
+ require.Equal(manifest.Blocks[0].OldUncompressedLength, pinnedActive.UncompressedLength)
+ _, err = db.ExecContext(ctx, `
+ UPDATE block_consolidation_shadow
+ SET single_block_delete_after = clock_timestamp() - INTERVAL '1 second'
+ WHERE block_metadata_id = $1`, blockMetadataID)
+ require.NoError(err)
+ err = retirement.NewPostgresRepository(db).PrepareRetirement(ctx, retirement.RetirementManifest{
+ BlockMetadataID: blockMetadataID,
+ Tag: tag,
+ Height: height,
+ Hash: block.Metadata.Hash,
+ State: retirement.RetirementStateEligible,
+ Bucket: bucket,
+ SingleBlockObjectKey: singleBlockKey,
+ SingleBlockObjectKeySHA256: repairSHA256(singleBlockKey),
+ SingleBlockObjectVersionIDs: []string{manifest.Blocks[0].SingleBlockObjectVersion.VersionID},
+ SingleBlockObjectETag: manifest.Blocks[0].SingleBlockObjectVersion.ETag,
+ SingleBlockObjectBytes: manifest.Blocks[0].SingleBlockObjectVersion.Bytes,
+ ConsolidatedObjectKey: dirtyKey,
+ ConsolidatedObjectVersionID: manifest.OldConsolidatedObjectVersion.VersionID,
+ ConsolidatedObjectETag: manifest.OldConsolidatedObjectVersion.ETag,
+ ConsolidatedByteOffset: manifest.Blocks[0].OldByteOffset,
+ ConsolidatedByteLength: manifest.Blocks[0].OldByteLength,
+ ConsolidatedUncompressedLength: manifest.Blocks[0].OldUncompressedLength,
+ PayloadSHA256: manifest.Blocks[0].PayloadSHA256,
+ PreparedAt: time.Now().UTC(),
+ })
+ require.ErrorContains(err, "failed to lock canonical retirement metadata")
+
+ activeBeforeRebuild, err := meta.GetBlockByHeight(ctx, tag, height)
+ require.NoError(err)
+ require.Equal(dirtyKey, activeBeforeRebuild.ObjectKeyMain)
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, activeBeforeRebuild.ObjectFormat)
+
+ var pinnedPayloads []cscbrepair.PinnedPayload
+ require.NoError(repairer.VisitPinnedPayloads(ctx, manifest.ID, nil, func(payload cscbrepair.PinnedPayload) error {
+ pinnedPayloads = append(pinnedPayloads, payload)
+ return nil
+ }))
+ require.Len(pinnedPayloads, 1)
+ var pinnedBlock api.Block
+ require.NoError(proto.Unmarshal(pinnedPayloads[0].RawBlockPayload, &pinnedBlock))
+ require.False(storageutils.HasBlockStoragePlacement(&pinnedBlock))
+ require.True(proto.Equal(
+ storageutils.CloneBlockWithoutStoragePlacement(semanticDuplicate),
+ storageutils.CloneBlockWithoutStoragePlacement(&pinnedBlock),
+ ))
+ cleanPayload := pinnedPayloads[0].RawBlockPayload
+ cleanKey, cleanPlacements, err := blob.UploadConsolidated(ctx, []blobstorage.ConsolidatedBlockPayload{{
+ Metadata: pinnedPayloads[0].Metadata,
+ MetadataID: blockMetadataID,
+ RawBlockPayload: blobstorage.BytesPayloadSource(cleanPayload),
+ UncompressedLength: uint64(len(cleanPayload)),
+ }})
+ require.NoError(err)
+ require.NotEqual(dirtyKey, cleanKey)
+ require.Len(cleanPlacements, 1)
+ cleanPlacement := cleanPlacements[0]
+ activeBeforePromotion, err := meta.GetBlockByHeight(ctx, tag, height)
+ require.NoError(err)
+ require.Equal(dirtyKey, activeBeforePromotion.ObjectKeyMain, "source CSCB must remain active until atomic promotion")
+
+ manifest, err = repairer.VerifyAndPromote(ctx, manifest.ID, cleanKey, []cscbrepair.RebuiltPlacement{{
+ BlockMetadataID: cleanPlacement.MetadataID,
+ Height: cleanPlacement.Height,
+ Hash: cleanPlacement.Hash,
+ ObjectFormat: cleanPlacement.ObjectFormat,
+ ByteOffset: cleanPlacement.ByteOffset,
+ ByteLength: cleanPlacement.ByteLength,
+ UncompressedLength: cleanPlacement.UncompressedLength,
+ }}, 72*time.Hour, nil)
+ require.NoError(err)
+ require.Equal(cscbrepair.StateCompleted, manifest.State)
+ require.Equal(cleanKey, manifest.NewConsolidatedObjectKey)
+ require.NotZero(manifest.NewConsolidatedObjectVersion.Bytes)
+ require.NotNil(manifest.RestoredAt)
+ require.NotNil(manifest.VerifiedAt)
+ require.NotNil(manifest.CompletedAt)
+ require.Equal("old_consolidated_object_retained_unreferenced", manifest.Outcome)
+ var retentionStartedAt, singleBlockDeleteAfter time.Time
+ require.NoError(db.QueryRowContext(ctx, `
+ SELECT single_block_retention_started_at, single_block_delete_after
+ FROM block_consolidation_shadow
+ WHERE block_metadata_id = $1`, blockMetadataID).Scan(&retentionStartedAt, &singleBlockDeleteAfter))
+ require.False(retentionStartedAt.Before(*manifest.RestoredAt))
+ require.WithinDuration(retentionStartedAt.Add(73*time.Hour), singleBlockDeleteAfter, time.Microsecond)
+ activeAfterPromotion, err := meta.GetBlockByHeight(ctx, tag, height)
+ require.NoError(err)
+ require.Equal(cleanKey, activeAfterPromotion.ObjectKeyMain)
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, activeAfterPromotion.ObjectFormat)
+ pendingAfterPromotion, err := repository.FindPending(ctx, tag, height, height+1)
+ require.NoError(err)
+ require.Nil(pendingAfterPromotion, "rollback must not resume a mixed placement after atomic promotion")
+ candidatesAfterPromotion, err := repository.ListCandidateObjectKeys(ctx, tag, height, height+1, 10)
+ require.NoError(err)
+ require.Empty(candidatesAfterPromotion, "rerunning a completed repair range must terminate without new work")
+
+ manifest, err = repairer.Complete(ctx, manifest.ID, nil)
+ require.NoError(err)
+ require.Equal(cscbrepair.StateCompleted, manifest.State)
+ require.NotNil(manifest.CompletedAt)
+ require.Equal("old_consolidated_object_retained_unreferenced", manifest.Outcome)
+ requireOldCSCBUnreferenced(t, db, dirtyKey)
+ dirtyTopology, err := store.ListObjectVersions(ctx, bucket, dirtyKey)
+ require.NoError(err)
+ require.Len(dirtyTopology.Versions, 1)
+ require.Empty(dirtyTopology.DeleteMarkers)
+ require.Equal(manifest.OldConsolidatedObjectVersion.VersionID, dirtyTopology.Versions[0].VersionID)
+ cleanTopology, err := store.ListObjectVersions(ctx, bucket, cleanKey)
+ require.NoError(err)
+ require.Len(cleanTopology.Versions, 1)
+ rawClean, err := retirement.NewPinnedPayloadVerifier(store).InspectConsolidated(ctx, retirement.Candidate{
+ Bucket: bucket,
+ ConsolidatedKey: cleanKey,
+ CSCBVersionID: cleanTopology.Versions[0].VersionID,
+ Tag: tag,
+ Height: height,
+ Hash: block.Metadata.Hash,
+ ByteOffset: cleanPlacement.ByteOffset,
+ ByteLength: cleanPlacement.ByteLength,
+ UncompressedLength: cleanPlacement.UncompressedLength,
+ })
+ require.NoError(err)
+ require.False(rawClean.HasStoragePlacement)
+ singleBlockTopology, err = store.ListObjectVersions(ctx, bucket, singleBlockKey)
+ require.NoError(err)
+ require.Len(singleBlockTopology.Versions, 2)
+ _, err = rawS3.DeleteObject(ctx, &awss3.DeleteObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(singleBlockKey),
+ VersionId: aws.String(historicalSingleBlockVersion.VersionID),
+ })
+ require.NoError(err)
+ singleBlockTopology, err = store.ListObjectVersions(ctx, bucket, singleBlockKey)
+ require.NoError(err)
+ require.Len(singleBlockTopology.Versions, 1)
+ require.Equal(currentSingleBlockVersion.VersionID, singleBlockTopology.Versions[0].VersionID)
+
+ activeClean, err := meta.GetBlockByHeight(ctx, tag, height)
+ require.NoError(err)
+ require.Equal(cleanKey, activeClean.ObjectKeyMain)
+ readClean, err := blob.Download(ctx, activeClean)
+ require.NoError(err)
+ // Normal downloads overlay the active database placement after parsing; raw
+ // CSCB neutrality is asserted independently above.
+ require.True(storageutils.HasBlockStoragePlacement(readClean))
+ require.True(proto.Equal(
+ storageutils.CloneBlockWithoutStoragePlacement(semanticDuplicate),
+ storageutils.CloneBlockWithoutStoragePlacement(readClean),
+ ))
+
+ resumed, err := repairer.Complete(ctx, manifest.ID, nil)
+ require.NoError(err)
+ require.Equal(cscbrepair.StateCompleted, resumed.State)
+ retried, err := repairer.PrepareNext(ctx, executionKey, tag, height, height+1, 1, nil)
+ require.NoError(err)
+ require.Equal(manifest.ID, retried.ID)
+ require.Equal(cscbrepair.StateCompleted, retried.State)
+ concurrentExecutionKey := repairSHA256(fmt.Sprintf("repair-concurrent-%d", unique))
+ completedByObject, err := repairer.PrepareObject(
+ ctx,
+ concurrentExecutionKey,
+ tag,
+ height,
+ height+1,
+ 1,
+ dirtyKey,
+ nil,
+ )
+ require.NoError(err)
+ require.Equal(manifest.ID, completedByObject.ID)
+ require.Equal(cscbrepair.StateCompleted, completedByObject.State)
+
+ // Exercise the real retirement executor after repair completion. The S3
+ // object deletion, shadow cleanup, retirement manifest, and repair audit
+ // path scrub must commit as one recoverable lifecycle.
+ _, err = db.ExecContext(ctx, `
+ UPDATE block_consolidation_shadow
+ SET single_block_retention_started_at = clock_timestamp() - INTERVAL '96 hours',
+ single_block_delete_after = clock_timestamp() - INTERVAL '24 hours'
+ WHERE block_metadata_id = $1`, blockMetadataID)
+ require.NoError(err)
+ policy := repairRetentionSafeBucketPolicy(bucket)
+ _, err = rawS3.PutBucketPolicy(ctx, &awss3.PutBucketPolicyInput{Bucket: aws.String(bucket), Policy: aws.String(policy)})
+ require.NoError(err)
+ retirementPlanner := retirement.NewPlanner(retirement.NewPostgresRepository(db), store)
+ retirementRequest := retirement.PlanRequest{
+ Environment: "local",
+ Blockchain: "solana",
+ Network: "mainnet",
+ Bucket: bucket,
+ Tag: tag,
+ StartHeight: height,
+ EndHeight: height + 1,
+ Limit: 1,
+ Now: time.Now().UTC(),
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ Approval: retirement.Approval{
+ Chain: "solana-mainnet",
+ StartHeight: height,
+ EndHeight: height + 1,
+ },
+ }
+ retirementReport, err := retirementPlanner.Plan(ctx, retirementRequest)
+ require.NoError(err)
+ require.Len(retirementReport.Items, 1)
+ require.Equal(retirement.ActionDeleteObjectVersion, retirementReport.Items[0].Action)
+ require.Empty(retirementReport.Items[0].SkipReason)
+ safety, err := store.InspectObjectRetentionSafety(ctx, bucket, cleanKey)
+ require.NoError(err)
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO cscb_retirement_safety_observation (
+ bucket, consolidated_object_key_main, configuration_sha256,
+ first_observed_at, last_observed_at
+ ) VALUES ($1, $2, $3, clock_timestamp() - INTERVAL '16 minutes', clock_timestamp() - INTERVAL '16 minutes')
+ ON CONFLICT (bucket, consolidated_object_key_main) DO UPDATE SET
+ configuration_sha256 = EXCLUDED.configuration_sha256,
+ first_observed_at = EXCLUDED.first_observed_at,
+ last_observed_at = EXCLUDED.last_observed_at`, bucket, cleanKey, safety.ConfigurationSHA256)
+ require.NoError(err)
+ require.NoError(retirementPlanner.Apply(ctx, retirementRequest, retirementReport))
+ require.Equal(retirement.ActionDeletedVerified, retirementReport.Items[0].Action)
+
+ retiredSingleTopology, err := store.ListObjectVersions(ctx, bucket, singleBlockKey)
+ require.NoError(err)
+ require.Empty(retiredSingleTopology.Versions)
+ require.Empty(retiredSingleTopology.DeleteMarkers)
+ var repairSinglePath sql.NullString
+ var repairSinglePathHash string
+ require.NoError(db.QueryRowContext(ctx, `
+ SELECT single_block_object_key_main, single_block_object_key_sha256
+ FROM cscb_repair_block
+ WHERE repair_id = $1 AND block_metadata_id = $2`, manifest.ID, blockMetadataID).Scan(
+ &repairSinglePath,
+ &repairSinglePathHash,
+ ))
+ require.False(repairSinglePath.Valid)
+ require.Equal(repairSHA256(singleBlockKey), repairSinglePathHash)
+ activeAfterRetirement, err := meta.GetBlockByHeight(ctx, tag, height)
+ require.NoError(err)
+ require.Equal(cleanKey, activeAfterRetirement.ObjectKeyMain)
+ readAfterRetirement, err := blob.Download(ctx, activeAfterRetirement)
+ require.NoError(err)
+ require.True(proto.Equal(
+ storageutils.CloneBlockWithoutStoragePlacement(semanticDuplicate),
+ storageutils.CloneBlockWithoutStoragePlacement(readAfterRetirement),
+ ))
+
+ // A CSCB whose rows all became non-canonical must still be restored and
+ // audited. It does not produce a replacement CSCB because normal
+ // consolidation intentionally processes canonical rows only.
+ nonCanonicalHeight := height + 1
+ nonCanonicalBlock := proto.Clone(block).(*api.Block)
+ nonCanonicalBlock.Metadata = proto.Clone(block.Metadata).(*api.BlockMetadata)
+ nonCanonicalBlock.Metadata.Height = nonCanonicalHeight
+ nonCanonicalBlock.Metadata.ParentHeight = height
+ nonCanonicalBlock.Metadata.Hash = fmt.Sprintf("repair-noncanonical-hash-%d", unique)
+ nonCanonicalBlock.Metadata.ParentHash = block.Metadata.Hash
+ nonCanonicalSingleKey, err := singleBlockUploader.Upload(ctx, nonCanonicalBlock, api.Compression_GZIP)
+ require.NoError(err)
+ nonCanonicalBlock.Metadata.ObjectKeyMain = nonCanonicalSingleKey
+ require.NoError(meta.PersistBlockMetas(ctx, true, []*api.BlockMetadata{nonCanonicalBlock.Metadata}, nil))
+ nonCanonicalRecords, err := meta.GetBlocksMissingConsolidationShadow(ctx, tag, nonCanonicalHeight, nonCanonicalHeight+1, 1)
+ require.NoError(err)
+ require.Len(nonCanonicalRecords, 1)
+ nonCanonicalMetadataID := nonCanonicalRecords[0].ID
+ var nonCanonicalRepairID int64
+ defer cleanupRepairMetadata(t, db, nonCanonicalMetadataID, &nonCanonicalRepairID)
+
+ nonCanonicalDownloaded, err := blob.Download(ctx, nonCanonicalRecords[0].Metadata)
+ require.NoError(err)
+ nonCanonicalDirty := proto.Clone(nonCanonicalDownloaded).(*api.Block)
+ nonCanonicalDirty.Metadata.ObjectKeyMain = nonCanonicalSingleKey
+ nonCanonicalDirty.Metadata.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK
+ nonCanonicalPayload, err := proto.Marshal(nonCanonicalDirty)
+ require.NoError(err)
+ nonCanonicalDirtyKey, nonCanonicalPlacements, err := blob.UploadConsolidated(ctx, []blobstorage.ConsolidatedBlockPayload{{
+ Metadata: nonCanonicalRecords[0].Metadata,
+ MetadataID: nonCanonicalMetadataID,
+ RawBlockPayload: blobstorage.BytesPayloadSource(nonCanonicalPayload),
+ UncompressedLength: uint64(len(nonCanonicalPayload)),
+ }})
+ require.NoError(err)
+ require.Len(nonCanonicalPlacements, 1)
+ nonCanonicalPlacement := nonCanonicalPlacements[0]
+ require.NoError(meta.PersistBlockConsolidationShadows(ctx, []*metastorage.ConsolidationShadowPlacement{{
+ BlockMetadataID: nonCanonicalMetadataID,
+ Tag: tag,
+ Height: nonCanonicalHeight,
+ Hash: nonCanonicalBlock.Metadata.Hash,
+ SingleBlockObjectKeyMain: nonCanonicalSingleKey,
+ ConsolidatedObjectKeyMain: nonCanonicalDirtyKey,
+ ObjectFormat: nonCanonicalPlacement.ObjectFormat,
+ ByteOffset: nonCanonicalPlacement.ByteOffset,
+ ByteLength: nonCanonicalPlacement.ByteLength,
+ UncompressedLength: nonCanonicalPlacement.UncompressedLength,
+ }}))
+ promotion, err = meta.PromoteBlockConsolidationShadows(ctx, tag, nonCanonicalHeight, nonCanonicalHeight+1, 1, 72*time.Hour)
+ require.NoError(err)
+ require.Equal(uint64(1), promotion.Blocks)
+ replacementBlock := proto.Clone(nonCanonicalBlock).(*api.Block)
+ replacementBlock.Metadata = proto.Clone(nonCanonicalBlock.Metadata).(*api.BlockMetadata)
+ replacementBlock.Metadata.Hash = fmt.Sprintf("repair-replacement-hash-%d", unique)
+ replacementBlock.Metadata.ParentHash = block.Metadata.Hash
+ replacementSingleKey, err := singleBlockUploader.Upload(ctx, replacementBlock, api.Compression_GZIP)
+ require.NoError(err)
+ replacementBlock.Metadata.ObjectKeyMain = replacementSingleKey
+ require.NoError(meta.PersistBlockMetas(ctx, true, []*api.BlockMetadata{replacementBlock.Metadata}, nil))
+ var replacementMetadataID int64
+ err = db.QueryRowContext(ctx, `
+ SELECT id FROM block_metadata
+ WHERE tag = $1 AND height = $2 AND hash = $3 AND skipped = FALSE`,
+ tag,
+ nonCanonicalHeight,
+ replacementBlock.Metadata.Hash,
+ ).Scan(&replacementMetadataID)
+ require.NoError(err)
+ defer cleanupRepairMetadata(t, db, replacementMetadataID, nil)
+
+ nonCanonicalExecutionKey := repairSHA256(fmt.Sprintf("repair-noncanonical-%d", unique))
+ nonCanonicalManifest, err := repairer.PrepareNext(
+ ctx,
+ nonCanonicalExecutionKey,
+ tag,
+ nonCanonicalHeight,
+ nonCanonicalHeight+1,
+ 1,
+ nil,
+ )
+ require.NoError(err)
+ require.NotNil(nonCanonicalManifest)
+ nonCanonicalRepairID = nonCanonicalManifest.ID
+ require.Zero(nonCanonicalManifest.CanonicalBlockCount)
+ require.Equal(uint64(1), nonCanonicalManifest.TotalBlockCount)
+ nonCanonicalManifest, err = repairer.Restore(ctx, nonCanonicalManifest.ID, nil)
+ require.NoError(err)
+ require.Equal(cscbrepair.StateRestored, nonCanonicalManifest.State)
+ activeNonCanonical, err := meta.GetBlockByHash(ctx, tag, nonCanonicalHeight, nonCanonicalBlock.Metadata.Hash)
+ require.NoError(err)
+ require.Equal(nonCanonicalSingleKey, activeNonCanonical.ObjectKeyMain)
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK, activeNonCanonical.ObjectFormat)
+ readNonCanonical, err := blob.Download(ctx, activeNonCanonical)
+ require.NoError(err)
+ require.True(proto.Equal(
+ storageutils.CloneBlockWithoutStoragePlacement(nonCanonicalBlock),
+ storageutils.CloneBlockWithoutStoragePlacement(readNonCanonical),
+ ))
+ nonCanonicalManifest, err = repairer.Complete(ctx, nonCanonicalManifest.ID, nil)
+ require.NoError(err)
+ require.Equal(cscbrepair.StateCompleted, nonCanonicalManifest.State)
+ require.Empty(nonCanonicalManifest.NewConsolidatedObjectKey)
+ requireOldCSCBUnreferenced(t, db, nonCanonicalDirtyKey)
+ nonCanonicalDirtyTopology, err := store.ListObjectVersions(ctx, bucket, nonCanonicalDirtyKey)
+ require.NoError(err)
+ require.Len(nonCanonicalDirtyTopology.Versions, 1)
+ require.Empty(nonCanonicalDirtyTopology.DeleteMarkers)
+
+ shadowOnlyKey := fmt.Sprintf("BLOCKCHAIN_SOLANA/NETWORK_SOLANA_MAINNET/consolidated/v=2/shadow-only-%d.cscb.zstd", unique)
+ tagLockTx, err := db.BeginTx(ctx, nil)
+ require.NoError(err)
+ _, err = tagLockTx.ExecContext(
+ ctx,
+ `SELECT pg_advisory_xact_lock(hashtextextended($1, 2))`,
+ fmt.Sprintf("cscb_repair_tag/%d", tag),
+ )
+ require.NoError(err)
+ shadowWrite := make(chan error, 1)
+ go func() {
+ shadowWrite <- meta.PersistBlockConsolidationShadows(ctx, []*metastorage.ConsolidationShadowPlacement{{
+ BlockMetadataID: nonCanonicalMetadataID,
+ Tag: tag,
+ Height: nonCanonicalHeight,
+ Hash: nonCanonicalBlock.Metadata.Hash,
+ SingleBlockObjectKeyMain: nonCanonicalSingleKey,
+ ConsolidatedObjectKeyMain: shadowOnlyKey,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: nonCanonicalPlacement.ByteOffset,
+ ByteLength: nonCanonicalPlacement.ByteLength,
+ UncompressedLength: nonCanonicalPlacement.UncompressedLength,
+ }})
+ }()
+ select {
+ case writeErr := <-shadowWrite:
+ require.Failf("consolidated shadow writer bypassed repair tag lock", "error=%v", writeErr)
+ case <-time.After(100 * time.Millisecond):
+ }
+ require.NoError(tagLockTx.Commit())
+ require.NoError(<-shadowWrite)
+ shadowOnlyExecutionKey := repairSHA256(fmt.Sprintf("repair-shadow-only-%d", unique))
+ _, err = repairer.PrepareNext(
+ ctx,
+ shadowOnlyExecutionKey,
+ tag,
+ nonCanonicalHeight,
+ nonCanonicalHeight+1,
+ 1,
+ nil,
+ )
+ require.ErrorContains(err, "shadow-only consolidated object")
+ _, err = db.ExecContext(ctx, `DELETE FROM block_consolidation_shadow WHERE block_metadata_id = $1`, nonCanonicalMetadataID)
+ require.NoError(err)
+
+ // Application writers acquire the tag lock before touching metadata rows.
+ // Holding the tag lock must block persistence while leaving the row lock free.
+ tagLockTx, err = db.BeginTx(ctx, nil)
+ require.NoError(err)
+ _, err = tagLockTx.ExecContext(
+ ctx,
+ `SELECT pg_advisory_xact_lock(hashtextextended($1, 2))`,
+ fmt.Sprintf("cscb_repair_tag/%d", tag),
+ )
+ require.NoError(err)
+ metadataWrite := make(chan error, 1)
+ go func() {
+ metadataWrite <- meta.PersistBlockMetas(ctx, false, []*api.BlockMetadata{proto.Clone(activeClean).(*api.BlockMetadata)}, nil)
+ }()
+ select {
+ case writeErr := <-metadataWrite:
+ require.Failf("metadata writer bypassed repair tag lock", "error=%v", writeErr)
+ case <-time.After(100 * time.Millisecond):
+ }
+ var lockedMetadataID int64
+ require.NoError(tagLockTx.QueryRowContext(
+ ctx,
+ `SELECT id FROM block_metadata WHERE id = $1 FOR UPDATE NOWAIT`,
+ blockMetadataID,
+ ).Scan(&lockedMetadataID))
+ require.Equal(blockMetadataID, lockedMetadataID)
+ require.NoError(tagLockTx.Commit())
+ require.NoError(<-metadataWrite)
+
+ noCandidateExecutionKey := repairSHA256(fmt.Sprintf("repair-no-candidate-%d", unique))
+ defer cleanupRepairExecution(t, db, noCandidateExecutionKey)
+ tagLockTx, err = db.BeginTx(ctx, nil)
+ require.NoError(err)
+ _, err = tagLockTx.ExecContext(
+ ctx,
+ `SELECT pg_advisory_xact_lock(hashtextextended($1, 2))`,
+ fmt.Sprintf("cscb_repair_tag/%d", tag),
+ )
+ require.NoError(err)
+ type candidateResult struct {
+ manifest *cscbrepair.Manifest
+ err error
+ }
+ noCandidateResult := make(chan candidateResult, 1)
+ go func() {
+ next, prepareErr := repairer.PrepareNext(
+ ctx,
+ noCandidateExecutionKey,
+ tag,
+ height,
+ height+1,
+ 1,
+ nil,
+ )
+ noCandidateResult <- candidateResult{manifest: next, err: prepareErr}
+ }()
+ select {
+ case result := <-noCandidateResult:
+ require.Failf("terminal no-candidate binding bypassed repair tag lock", "manifest=%v error=%v", result.manifest, result.err)
+ case <-time.After(100 * time.Millisecond):
+ }
+ require.NoError(tagLockTx.Commit())
+ result := <-noCandidateResult
+ require.NoError(result.err)
+ require.Nil(result.manifest)
+ next, err := repairer.PrepareNext(ctx, noCandidateExecutionKey, tag, height, height+1, 1, nil)
+ require.NoError(err)
+ require.Nil(next)
+
+ // Skipped canonical slots inside a CSCB's height span have no payload row.
+ // They must not be mistaken for unrelated canonical block data during restore.
+ gapStartHeight := height + 100
+ gapHeights := []uint64{gapStartHeight, gapStartHeight + 2}
+ gapBlocks := make([]*api.Block, 0, len(gapHeights))
+ gapSingleBlockKeys := make([]string, 0, len(gapHeights))
+ for i, gapHeight := range gapHeights {
+ gapBlock := storageutils.CloneBlockWithoutStoragePlacement(block)
+ gapBlock.Metadata.Height = gapHeight
+ gapBlock.Metadata.Hash = fmt.Sprintf("repair-gap-hash-%d-%d", unique, gapHeight)
+ if i == 0 {
+ gapBlock.Metadata.ParentHeight = gapHeight - 1
+ gapBlock.Metadata.ParentHash = fmt.Sprintf("repair-gap-parent-%d", unique)
+ } else {
+ gapBlock.Metadata.ParentHeight = gapHeights[i-1]
+ gapBlock.Metadata.ParentHash = gapBlocks[i-1].Metadata.Hash
+ }
+ gapSingleBlockKey, uploadErr := singleBlockUploader.Upload(ctx, gapBlock, api.Compression_GZIP)
+ require.NoError(uploadErr)
+ gapBlock.Metadata.ObjectKeyMain = gapSingleBlockKey
+ gapBlocks = append(gapBlocks, gapBlock)
+ gapSingleBlockKeys = append(gapSingleBlockKeys, gapSingleBlockKey)
+ }
+ skippedMetadata := &api.BlockMetadata{
+ Tag: tag,
+ Height: gapStartHeight + 1,
+ Skipped: true,
+ }
+ require.NoError(meta.PersistBlockMetas(ctx, true, []*api.BlockMetadata{
+ gapBlocks[0].Metadata,
+ skippedMetadata,
+ gapBlocks[1].Metadata,
+ }, nil))
+ gapRecords, err := meta.GetBlocksMissingConsolidationShadow(ctx, tag, gapStartHeight, gapStartHeight+3, 3)
+ require.NoError(err)
+ require.Len(gapRecords, 2)
+ var skippedMetadataID int64
+ require.NoError(db.QueryRowContext(ctx, `
+ SELECT id FROM block_metadata
+ WHERE tag = $1 AND height = $2 AND skipped = TRUE`,
+ tag,
+ skippedMetadata.Height,
+ ).Scan(&skippedMetadataID))
+ var gapRepairID int64
+ defer cleanupRepairMetadata(t, db, gapRecords[0].ID, nil)
+ defer cleanupRepairMetadata(t, db, gapRecords[1].ID, nil)
+ defer cleanupRepairMetadata(t, db, skippedMetadataID, nil)
+ defer cleanupRepairMetadata(t, db, 0, &gapRepairID)
+
+ gapPayloads := make([]blobstorage.ConsolidatedBlockPayload, 0, len(gapRecords))
+ for i, record := range gapRecords {
+ dirtyGapBlock := proto.Clone(gapBlocks[i]).(*api.Block)
+ dirtyGapBlock.Metadata.ObjectKeyMain = gapSingleBlockKeys[i]
+ dirtyGapBlock.Metadata.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK
+ require.True(storageutils.HasBlockStoragePlacement(dirtyGapBlock))
+ dirtyGapPayload, marshalErr := proto.Marshal(dirtyGapBlock)
+ require.NoError(marshalErr)
+ gapPayloads = append(gapPayloads, blobstorage.ConsolidatedBlockPayload{
+ Metadata: record.Metadata,
+ MetadataID: record.ID,
+ RawBlockPayload: blobstorage.BytesPayloadSource(dirtyGapPayload),
+ UncompressedLength: uint64(len(dirtyGapPayload)),
+ })
+ }
+ gapDirtyKey, gapPlacements, err := blob.UploadConsolidated(ctx, gapPayloads)
+ require.NoError(err)
+ require.Len(gapPlacements, 2)
+ gapShadows := make([]*metastorage.ConsolidationShadowPlacement, 0, len(gapRecords))
+ for i, record := range gapRecords {
+ gapShadows = append(gapShadows, &metastorage.ConsolidationShadowPlacement{
+ BlockMetadataID: record.ID,
+ Tag: tag,
+ Height: record.Metadata.Height,
+ Hash: record.Metadata.Hash,
+ SingleBlockObjectKeyMain: gapSingleBlockKeys[i],
+ ConsolidatedObjectKeyMain: gapDirtyKey,
+ ObjectFormat: gapPlacements[i].ObjectFormat,
+ ByteOffset: gapPlacements[i].ByteOffset,
+ ByteLength: gapPlacements[i].ByteLength,
+ UncompressedLength: gapPlacements[i].UncompressedLength,
+ })
+ }
+ require.NoError(meta.PersistBlockConsolidationShadows(ctx, gapShadows))
+ promotion, err = meta.PromoteBlockConsolidationShadows(ctx, tag, gapStartHeight, gapStartHeight+3, 3, 72*time.Hour)
+ require.NoError(err)
+ require.Equal(uint64(2), promotion.Blocks)
+
+ gapExecutionKey := repairSHA256(fmt.Sprintf("repair-skipped-gap-%d", unique))
+ gapManifest, err := repairer.PrepareObject(
+ ctx,
+ gapExecutionKey,
+ tag,
+ gapStartHeight,
+ gapStartHeight+3,
+ 3,
+ gapDirtyKey,
+ nil,
+ )
+ require.NoError(err)
+ gapRepairID = gapManifest.ID
+ require.Equal(gapStartHeight, gapManifest.StartHeight)
+ require.Equal(gapStartHeight+3, gapManifest.EndHeight)
+ require.Equal(uint64(2), gapManifest.CanonicalBlockCount)
+
+ _, err = db.ExecContext(ctx, `UPDATE block_metadata SET skipped = FALSE WHERE id = $1`, skippedMetadataID)
+ require.NoError(err)
+ _, err = repairer.Restore(ctx, gapManifest.ID, nil)
+ require.ErrorContains(err, "CSCB repair range contains unrelated canonical metadata_id=")
+ _, err = db.ExecContext(ctx, `UPDATE block_metadata SET skipped = TRUE WHERE id = $1`, skippedMetadataID)
+ require.NoError(err)
+ gapManifest, err = repairer.Restore(ctx, gapManifest.ID, nil)
+ require.NoError(err)
+ require.Equal(cscbrepair.StateRestored, gapManifest.State)
+ for i, gapHeight := range gapHeights {
+ activeGapBlock, getErr := meta.GetBlockByHeight(ctx, tag, gapHeight)
+ require.NoError(getErr)
+ require.Equal(gapSingleBlockKeys[i], activeGapBlock.ObjectKeyMain)
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK, activeGapBlock.ObjectFormat)
+ }
+
+ // Pinned-key fencing is based on exact manifest identity, not a path naming
+ // convention or a caller-supplied CSCB format value.
+ nonstandardOldKey := fmt.Sprintf("custom-layout/dirty-object-%d", unique)
+ var nonstandardRepairID int64
+ err = db.QueryRowContext(ctx, `
+ INSERT INTO cscb_repair_manifest (
+ tag, state, bucket, old_consolidated_object_key_main,
+ old_consolidated_object_version_id, old_consolidated_object_etag,
+ old_consolidated_object_bytes, start_height, end_height,
+ canonical_block_count, total_block_count, row_set_sha256
+ ) VALUES ($1, 'prepared', $2, $3, 'version', 'etag', 1, $4, $5, 0, 0, $6)
+ RETURNING id`,
+ tag,
+ bucket,
+ nonstandardOldKey,
+ height,
+ height+1,
+ strings.Repeat("0", 64),
+ ).Scan(&nonstandardRepairID)
+ require.NoError(err)
+ defer cleanupRepairMetadata(t, db, 0, &nonstandardRepairID)
+ _, err = db.ExecContext(ctx, `
+ UPDATE block_metadata
+ SET object_key_main = $2, object_format = $3
+ WHERE id = $1`,
+ replacementMetadataID,
+ nonstandardOldKey,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK,
+ )
+ require.ErrorContains(err, "cannot reference a pinned old CSCB object")
+}
+
+func configureRepairTestEnvironment(t *testing.T, postgres *config.PostgresConfig) string {
+ t.Helper()
+ switch postgres.Host {
+ case "localhost", "127.0.0.1", "::1":
+ postgres.Port = 5433
+ postgres.Database = "chainstorage_solana_mainnet"
+ postgres.User = "cs_solana_mainnet_worker"
+ postgres.Password = "worker_password"
+ postgres.SSLMode = "require"
+ return "http://localhost:4566"
+ case "postgres":
+ return "http://localstack:4566"
+ default:
+ t.Fatalf("refusing to run CSCB repair lifecycle test against PostgreSQL host %q", postgres.Host)
+ return ""
+ }
+}
+
+func newRepairLocalStackConfig(ctx context.Context, endpoint string) aws.Config {
+ cfg, err := awsconfig.LoadDefaultConfig(
+ ctx,
+ awsconfig.WithRegion("us-east-1"),
+ awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("test", "test", "")),
+ awsconfig.WithEndpointResolverWithOptions(
+ aws.EndpointResolverWithOptionsFunc(func(string, string, ...interface{}) (aws.Endpoint, error) {
+ return aws.Endpoint{URL: endpoint, HostnameImmutable: true}, nil
+ }),
+ ),
+ )
+ if err != nil {
+ panic(fmt.Sprintf("failed to configure LocalStack: %v", err))
+ }
+ return cfg
+}
+
+func repairSHA256(value string) string {
+ digest := sha256.Sum256([]byte(value))
+ return hex.EncodeToString(digest[:])
+}
+
+func repairRetentionSafeBucketPolicy(bucket string) string {
+ return fmt.Sprintf(`{
+ "Version":"2012-10-17",
+ "Statement":[
+ {"Effect":"Deny","Principal":"*","Action":"s3:PutObject","Resource":"arn:aws:s3:::%[1]s/*/consolidated/*","Condition":{"Null":{"s3:if-none-match":"true"}}},
+ {"Effect":"Deny","Principal":"*","Action":["s3:DeleteObject","s3:DeleteObjectVersion","s3:ReplicateObject","s3:ReplicateDelete"],"Resource":"arn:aws:s3:::%[1]s/*/consolidated/*"},
+ {"Effect":"Deny","Principal":"*","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::%[1]s"}
+ ]
+ }`, bucket)
+}
+
+func requireOldCSCBUnreferenced(t *testing.T, db *sql.DB, objectKey string) {
+ t.Helper()
+ var references uint64
+ err := db.QueryRowContext(context.Background(), `
+ SELECT
+ (SELECT COUNT(*) FROM block_metadata WHERE object_key_main = $1)
+ +
+ (SELECT COUNT(*) FROM block_consolidation_shadow WHERE consolidated_object_key_main = $1)`,
+ objectKey,
+ ).Scan(&references)
+ require.NoError(t, err)
+ require.Zero(t, references)
+}
+
+func openRepairDB(ctx context.Context, cfg *config.PostgresConfig) (*sql.DB, error) {
+ dsn := fmt.Sprintf(
+ "host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
+ cfg.Host,
+ cfg.Port,
+ cfg.Database,
+ cfg.User,
+ cfg.Password,
+ cfg.SSLMode,
+ )
+ db, err := sql.Open("postgres", dsn)
+ if err != nil {
+ return nil, err
+ }
+ if err := db.PingContext(ctx); err != nil {
+ _ = db.Close()
+ return nil, err
+ }
+ return db, nil
+}
+
+func cleanupRepairMetadata(t *testing.T, db *sql.DB, blockMetadataID int64, repairID *int64) {
+ t.Helper()
+ ctx := context.Background()
+ if repairID != nil && *repairID != 0 {
+ _, _ = db.ExecContext(ctx, `DELETE FROM cscb_retirement_safety_observation WHERE bucket = (SELECT bucket FROM cscb_repair_manifest WHERE id = $1)`, *repairID)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE cscb_repair_execution DISABLE TRIGGER cscb_repair_execution_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE cscb_repair_block DISABLE TRIGGER cscb_repair_block_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE cscb_repair_manifest DISABLE TRIGGER cscb_repair_manifest_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM cscb_repair_execution WHERE repair_id = $1`, *repairID)
+ _, _ = db.ExecContext(ctx, `DELETE FROM cscb_repair_block WHERE repair_id = $1`, *repairID)
+ _, _ = db.ExecContext(ctx, `DELETE FROM cscb_repair_manifest WHERE id = $1`, *repairID)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE cscb_repair_manifest ENABLE TRIGGER cscb_repair_manifest_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE cscb_repair_block ENABLE TRIGGER cscb_repair_block_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE cscb_repair_execution ENABLE TRIGGER cscb_repair_execution_delete_trigger`)
+ }
+ _, _ = db.ExecContext(ctx, `ALTER TABLE block_single_block_retention DISABLE TRIGGER block_single_block_retention_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_single_block_retention WHERE block_metadata_id = $1`, blockMetadataID)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE block_single_block_retention ENABLE TRIGGER block_single_block_retention_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_consolidation_shadow WHERE block_metadata_id = $1`, blockMetadataID)
+ _, _ = db.ExecContext(ctx, `DELETE FROM canonical_blocks WHERE block_metadata_id = $1`, blockMetadataID)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_metadata WHERE id = $1`, blockMetadataID)
+}
+
+func cleanupRepairExecution(t *testing.T, db *sql.DB, executionKey string) {
+ t.Helper()
+ ctx := context.Background()
+ _, _ = db.ExecContext(ctx, `ALTER TABLE cscb_repair_execution DISABLE TRIGGER cscb_repair_execution_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM cscb_repair_execution WHERE execution_key = $1`, executionKey)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE cscb_repair_execution ENABLE TRIGGER cscb_repair_execution_delete_trigger`)
+}
+
+func cleanupRepairBucket(t *testing.T, client *awss3.Client, bucket string) {
+ t.Helper()
+ ctx := context.Background()
+ _, _ = client.DeleteBucketPolicy(ctx, &awss3.DeleteBucketPolicyInput{Bucket: aws.String(bucket)})
+ versions, err := client.ListObjectVersions(ctx, &awss3.ListObjectVersionsInput{Bucket: aws.String(bucket)})
+ if err == nil {
+ for _, version := range versions.Versions {
+ _, _ = client.DeleteObject(ctx, &awss3.DeleteObjectInput{Bucket: aws.String(bucket), Key: version.Key, VersionId: version.VersionId})
+ }
+ for _, marker := range versions.DeleteMarkers {
+ _, _ = client.DeleteObject(ctx, &awss3.DeleteObjectInput{Bucket: aws.String(bucket), Key: marker.Key, VersionId: marker.VersionId})
+ }
+ }
+ _, _ = client.DeleteBucket(ctx, &awss3.DeleteBucketInput{Bucket: aws.String(bucket)})
+}
diff --git a/internal/storage/cscbrepair/migration_integration_test.go b/internal/storage/cscbrepair/migration_integration_test.go
new file mode 100644
index 00000000..b82a7bc2
--- /dev/null
+++ b/internal/storage/cscbrepair/migration_integration_test.go
@@ -0,0 +1,272 @@
+package cscbrepair_test
+
+import (
+ "context"
+ "database/sql"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/lib/pq"
+ "github.com/pressly/goose/v3"
+ "github.com/stretchr/testify/require"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ metapostgres "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+)
+
+func TestIntegrationCSCBRepairReferenceIndexMigration(t *testing.T) {
+ if os.Getenv("TEST_TYPE") != "integration" {
+ t.Skip("integration test")
+ }
+ require := require.New(t)
+ ctx := context.Background()
+
+ cfg, err := config.New(
+ config.WithEnvironment(config.EnvLocal),
+ config.WithBlockchain(common.Blockchain_BLOCKCHAIN_SOLANA),
+ config.WithNetwork(common.Network_NETWORK_SOLANA_MAINNET),
+ )
+ require.NoError(err)
+ require.NotNil(cfg.AWS.Postgres)
+ configureRepairTestEnvironment(t, cfg.AWS.Postgres)
+
+ db, err := openRepairDB(ctx, cfg.AWS.Postgres)
+ require.NoError(err)
+ defer func() { _ = db.Close() }()
+
+ goose.SetBaseFS(metapostgres.GetEmbeddedMigrations())
+ require.NoError(goose.SetDialect("postgres"))
+ require.NoError(goose.UpContext(ctx, db, "db/migrations"))
+ requireCandidateIndex(t, db)
+ requireReferenceIndexes(t, db, "hash")
+ requireNoTemporaryIndexes(t, db)
+ requireIndexPlans(t, db)
+
+ // Production rolls this migration down before applying the corrected index
+ // definitions. Exercise that exact clean down/up path.
+ require.NoError(goose.DownToContext(ctx, db, "db/migrations", 20260714000001))
+ requireNoRepairIndexes(t, db)
+
+ require.NoError(goose.UpContext(ctx, db, "db/migrations"))
+ requireCandidateIndex(t, db)
+ requireReferenceIndexes(t, db, "hash")
+ requireNoTemporaryIndexes(t, db)
+ requireIndexPlans(t, db)
+}
+
+func requireCandidateIndex(t *testing.T, db *sql.DB) {
+ t.Helper()
+ var method, definition string
+ var valid, ready bool
+ err := db.QueryRow(`
+ SELECT access_method.amname, index_state.indisvalid, index_state.indisready,
+ pg_get_indexdef(index_state.indexrelid)
+ FROM pg_index index_state
+ JOIN pg_class index_class ON index_class.oid = index_state.indexrelid
+ JOIN pg_namespace index_namespace ON index_namespace.oid = index_class.relnamespace
+ JOIN pg_am access_method ON access_method.oid = index_class.relam
+ WHERE index_namespace.nspname = current_schema()
+ AND index_class.relname = 'idx_block_metadata_cscb_repair_candidate'`).Scan(
+ &method,
+ &valid,
+ &ready,
+ &definition,
+ )
+ require.NoError(t, err)
+ require.Equal(t, "btree", method)
+ require.True(t, valid)
+ require.True(t, ready)
+ require.Contains(t, definition, "(tag, height DESC, object_key_main, id)")
+ require.Contains(t, definition, "object_format = 1")
+}
+
+func requireNoRepairIndexes(t *testing.T, db *sql.DB) {
+ t.Helper()
+ var count int
+ err := db.QueryRow(`
+ SELECT COUNT(*)
+ FROM pg_class index_class
+ JOIN pg_namespace index_namespace ON index_namespace.oid = index_class.relnamespace
+ WHERE index_namespace.nspname = current_schema()
+ AND index_class.relname IN (
+ 'idx_block_metadata_cscb_repair_candidate',
+ 'idx_block_metadata_object_key_reference',
+ 'idx_block_consolidation_shadow_object_key_reference',
+ 'idx_block_metadata_cscb_repair_candidate_new',
+ 'idx_block_metadata_object_key_reference_hash_new',
+ 'idx_block_consolidation_shadow_object_key_reference_hash_new'
+ )`).Scan(&count)
+ require.NoError(t, err)
+ require.Zero(t, count)
+}
+
+func requireNoTemporaryIndexes(t *testing.T, db *sql.DB) {
+ t.Helper()
+ var count int
+ err := db.QueryRow(`
+ SELECT COUNT(*)
+ FROM pg_class index_class
+ JOIN pg_namespace index_namespace ON index_namespace.oid = index_class.relnamespace
+ WHERE index_namespace.nspname = current_schema()
+ AND index_class.relname IN (
+ 'idx_block_metadata_cscb_repair_candidate_new',
+ 'idx_block_metadata_object_key_reference_hash_new',
+ 'idx_block_consolidation_shadow_object_key_reference_hash_new'
+ )`).Scan(&count)
+ require.NoError(t, err)
+ require.Zero(t, count)
+}
+
+func requireReferenceIndexes(t *testing.T, db *sql.DB, expectedMethod string) {
+ t.Helper()
+ indexes := []struct {
+ name string
+ column string
+ }{
+ {name: "idx_block_metadata_object_key_reference", column: "object_key_main"},
+ {name: "idx_block_consolidation_shadow_object_key_reference", column: "consolidated_object_key_main"},
+ }
+ for _, index := range indexes {
+ requireReferenceIndex(t, db, index.name, index.column, expectedMethod)
+ }
+}
+
+func requireReferenceIndex(t *testing.T, db *sql.DB, indexName string, column string, expectedMethod string) {
+ t.Helper()
+ var method, definition string
+ var valid, ready bool
+ err := db.QueryRow(`
+ SELECT access_method.amname, index_state.indisvalid, index_state.indisready,
+ pg_get_indexdef(index_state.indexrelid)
+ FROM pg_index index_state
+ JOIN pg_class index_class ON index_class.oid = index_state.indexrelid
+ JOIN pg_namespace index_namespace ON index_namespace.oid = index_class.relnamespace
+ JOIN pg_am access_method ON access_method.oid = index_class.relam
+ WHERE index_namespace.nspname = current_schema()
+ AND index_class.relname = $1`, indexName).Scan(
+ &method,
+ &valid,
+ &ready,
+ &definition,
+ )
+ require.NoError(t, err)
+ require.Equal(t, expectedMethod, method)
+ require.True(t, valid)
+ require.True(t, ready)
+ if expectedMethod == "hash" {
+ require.Contains(t, definition, "USING hash ("+column+")")
+ }
+}
+
+func requireIndexPlans(t *testing.T, db *sql.DB) {
+ t.Helper()
+ const candidateListQuery = `
+ WITH overlapping_keys AS (
+ SELECT DISTINCT bm.object_key_main
+ FROM block_metadata bm
+ WHERE bm.tag = $1
+ AND bm.height >= $2
+ AND bm.height < $3
+ AND bm.object_format = $4
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ AND NOT EXISTS (
+ SELECT 1
+ FROM cscb_repair_manifest repair
+ WHERE repair.tag = bm.tag
+ AND (
+ repair.old_consolidated_object_key_main = bm.object_key_main
+ OR repair.new_consolidated_object_key_main = bm.object_key_main
+ )
+ )
+ )
+ SELECT bm.object_key_main, MIN(bm.height), MAX(bm.height)
+ FROM block_metadata bm
+ JOIN overlapping_keys candidate ON candidate.object_key_main = bm.object_key_main
+ WHERE bm.tag = $1
+ AND bm.object_format = $4
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ GROUP BY bm.object_key_main
+ ORDER BY MAX(bm.height) DESC, bm.object_key_main DESC
+ LIMIT $5`
+ const shadowOnlyCandidateQuery = `
+ SELECT shadow.consolidated_object_key_main, MIN(bm.height), MAX(bm.height)
+ FROM block_consolidation_shadow shadow
+ JOIN block_metadata bm ON bm.id = shadow.block_metadata_id
+ AND bm.tag = shadow.tag
+ AND bm.height = shadow.height
+ AND bm.hash IS NOT DISTINCT FROM shadow.hash
+ WHERE shadow.tag = $1
+ AND shadow.height >= $2
+ AND shadow.height < $3
+ AND shadow.object_format = $4
+ AND shadow.consolidated_object_key_main IS NOT NULL
+ AND shadow.consolidated_object_key_main <> ''
+ AND (
+ bm.object_key_main IS DISTINCT FROM shadow.consolidated_object_key_main
+ OR bm.object_format <> $4
+ )
+ AND NOT EXISTS (
+ SELECT 1
+ FROM cscb_repair_manifest repair
+ WHERE repair.tag = shadow.tag
+ AND (
+ repair.old_consolidated_object_key_main = shadow.consolidated_object_key_main
+ OR repair.new_consolidated_object_key_main = shadow.consolidated_object_key_main
+ )
+ )
+ GROUP BY shadow.consolidated_object_key_main
+ ORDER BY MAX(bm.height) DESC, shadow.consolidated_object_key_main DESC
+ LIMIT 1`
+ requireReferenceIndexPlan(t, db, `
+ SELECT COUNT(*)
+ FROM block_metadata
+ WHERE object_key_main = $1
+ AND object_key_main <> ''
+ AND NOT (id = ANY($2))`, "idx_block_metadata_object_key_reference", "consolidated/test.cscb", pq.Array([]int64{1}))
+ requireReferenceIndexPlan(t, db, `
+ SELECT COUNT(*)
+ FROM block_consolidation_shadow
+ WHERE consolidated_object_key_main = $1
+ AND consolidated_object_key_main <> ''
+ AND NOT (block_metadata_id = ANY($2))`, "idx_block_consolidation_shadow_object_key_reference", "consolidated/test.cscb", pq.Array([]int64{1}))
+ requireReferenceIndexPlan(t, db, `
+ SELECT object_key_main
+ FROM block_metadata
+ WHERE tag = $1
+ AND height >= $2
+ AND height < $3
+ AND object_format = $4
+ AND object_key_main IS NOT NULL
+ AND object_key_main <> ''`, "idx_block_metadata_cscb_repair_candidate", uint32(2), uint64(1000), uint64(2000), 1)
+ requireReferenceIndexPlan(t, db, candidateListQuery, "idx_block_metadata_cscb_repair_candidate", uint32(2), uint64(1000), uint64(2000), 1, 10)
+ requireReferenceIndexPlan(t, db, candidateListQuery, "idx_block_metadata_object_key_reference", uint32(2), uint64(1000), uint64(2000), 1, 10)
+ requireReferenceIndexPlan(t, db, shadowOnlyCandidateQuery, "idx_block_consolidation_shadow_tag_height", uint32(2), uint64(1000), uint64(2000), 1)
+ requireReferenceIndexPlan(t, db, shadowOnlyCandidateQuery, "block_metadata_pkey", uint32(2), uint64(1000), uint64(2000), 1)
+}
+
+func requireReferenceIndexPlan(t *testing.T, db *sql.DB, query string, indexName string, args ...any) {
+ t.Helper()
+ tx, err := db.Begin()
+ require.NoError(t, err)
+ defer func() { _ = tx.Rollback() }()
+ _, err = tx.Exec(`SET LOCAL enable_seqscan = off`)
+ require.NoError(t, err)
+
+ rows, err := tx.Query("EXPLAIN (COSTS OFF) "+query, args...)
+ require.NoError(t, err)
+ defer rows.Close()
+
+ var plan strings.Builder
+ for rows.Next() {
+ var line string
+ require.NoError(t, rows.Scan(&line))
+ plan.WriteString(line)
+ plan.WriteByte('\n')
+ }
+ require.NoError(t, rows.Err())
+ require.Contains(t, plan.String(), indexName)
+}
diff --git a/internal/storage/cscbrepair/postgres_repository.go b/internal/storage/cscbrepair/postgres_repository.go
new file mode 100644
index 00000000..bb6d08a4
--- /dev/null
+++ b/internal/storage/cscbrepair/postgres_repository.go
@@ -0,0 +1,2367 @@
+package cscbrepair
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "sort"
+ "time"
+
+ "github.com/lib/pq"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/storage/cscbrepairlock"
+ "github.com/coinbase/chainstorage/internal/storage/retirementlock"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+const manifestColumns = `
+ id, tag, state, bucket,
+ old_consolidated_object_key_main, old_consolidated_object_version_id,
+ old_consolidated_object_etag, old_consolidated_object_bytes,
+ start_height, end_height, canonical_block_count, total_block_count, row_set_sha256,
+ new_consolidated_object_key_main, new_consolidated_object_version_id,
+ new_consolidated_object_etag, new_consolidated_object_bytes,
+ outcome, prepared_at, restored_at, verified_at,
+ completed_at`
+
+const repairPromotionRetentionCommitGrace = time.Hour
+
+type (
+ PostgresRepository struct {
+ db *sql.DB
+ }
+
+ rowScanner interface {
+ Scan(dest ...any) error
+ }
+
+ queryer interface {
+ QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
+ QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
+ }
+)
+
+func NewPostgresRepository(db *sql.DB) *PostgresRepository {
+ return &PostgresRepository{db: db}
+}
+
+func (r *PostgresRepository) FindByExecutionKey(
+ ctx context.Context,
+ executionKey string,
+) (*Manifest, bool, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, false, err
+ }
+ if !validExecutionKey(executionKey) {
+ return nil, false, xerrors.New("valid CSCB repair execution key is required")
+ }
+ var repairID sql.NullInt64
+ if err := r.db.QueryRowContext(ctx, `
+ SELECT repair_id
+ FROM cscb_repair_execution
+ WHERE execution_key = $1`, executionKey).Scan(&repairID); err != nil {
+ if err == sql.ErrNoRows {
+ return nil, false, nil
+ }
+ return nil, false, xerrors.Errorf("failed to find CSCB repair execution binding: %w", err)
+ }
+ if !repairID.Valid {
+ return nil, true, nil
+ }
+ manifest, err := loadManifest(ctx, r.db, repairID.Int64, false)
+ if err != nil {
+ return nil, false, err
+ }
+ return manifest, true, nil
+}
+
+func (r *PostgresRepository) FindPending(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if endHeight <= startHeight {
+ return nil, xerrors.New("valid CSCB repair range is required")
+ }
+ query := fmt.Sprintf(`
+ SELECT %s
+ FROM cscb_repair_manifest
+ WHERE tag = $1
+ AND state <> $2
+ AND end_height > $3
+ AND start_height < $4
+ ORDER BY id ASC
+ LIMIT 1`, manifestColumns)
+ manifest, err := scanManifest(r.db.QueryRowContext(
+ ctx,
+ query,
+ tag,
+ StateCompleted,
+ startHeight,
+ endHeight,
+ ))
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ return nil, xerrors.Errorf("failed to find pending CSCB repair: %w", err)
+ }
+ if err := loadManifestBlocks(ctx, r.db, manifest); err != nil {
+ return nil, err
+ }
+ return manifest, nil
+}
+
+func (r *PostgresRepository) FindByObjectKey(
+ ctx context.Context,
+ tag uint32,
+ objectKey string,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if objectKey == "" {
+ return nil, xerrors.New("CSCB repair object key is required")
+ }
+ query := fmt.Sprintf(`
+ SELECT %s
+ FROM cscb_repair_manifest
+ WHERE tag = $1
+ AND old_consolidated_object_key_main = $2
+ LIMIT 1`, manifestColumns)
+ manifest, err := scanManifest(r.db.QueryRowContext(ctx, query, tag, objectKey))
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, nil
+ }
+ return nil, xerrors.Errorf("failed to find CSCB repair manifest for object %q: %w", objectKey, err)
+ }
+ if err := loadManifestBlocks(ctx, r.db, manifest); err != nil {
+ return nil, err
+ }
+ return manifest, nil
+}
+
+func (r *PostgresRepository) FindNextCandidate(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if endHeight <= startHeight {
+ return nil, nil
+ }
+ objectKey, err := findNextCandidateObject(ctx, r.db, tag, startHeight, endHeight)
+ if err != nil {
+ return nil, err
+ }
+ if objectKey == "" {
+ if err := requireNoShadowOnlyCandidates(ctx, r.db, tag, startHeight, endHeight); err != nil {
+ return nil, err
+ }
+ return nil, nil
+ }
+ manifest, err := loadCandidateByKey(ctx, r.db, tag, objectKey)
+ if err != nil {
+ return nil, err
+ }
+ return manifest, nil
+}
+
+func (r *PostgresRepository) FindCandidateByObjectKey(
+ ctx context.Context,
+ tag uint32,
+ objectKey string,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if objectKey == "" {
+ return nil, xerrors.New("CSCB repair object key is required")
+ }
+ return loadCandidateByKey(ctx, r.db, tag, objectKey)
+}
+
+func (r *PostgresRepository) ListCandidateObjectKeys(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ limit int,
+) ([]string, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if endHeight <= startHeight {
+ return nil, xerrors.New("valid CSCB repair range is required")
+ }
+ if limit <= 0 {
+ return nil, xerrors.New("CSCB repair candidate limit must be positive")
+ }
+
+ objectKeys := make([]string, 0, limit)
+ pendingRows, err := r.db.QueryContext(ctx, `
+ SELECT old_consolidated_object_key_main, start_height, end_height
+ FROM cscb_repair_manifest
+ WHERE tag = $1
+ AND state <> $2
+ AND end_height > $3
+ AND start_height < $4
+ ORDER BY end_height DESC, old_consolidated_object_key_main DESC
+ LIMIT $5`, tag, StateCompleted, startHeight, endHeight, limit)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to list pending CSCB repair candidates: %w", err)
+ }
+ for pendingRows.Next() {
+ var objectKey string
+ var candidateStart, candidateEnd uint64
+ if err := pendingRows.Scan(&objectKey, &candidateStart, &candidateEnd); err != nil {
+ _ = pendingRows.Close()
+ return nil, xerrors.Errorf("failed to scan pending CSCB repair candidate: %w", err)
+ }
+ if candidateStart < startHeight || candidateEnd > endHeight {
+ _ = pendingRows.Close()
+ return nil, xerrors.Errorf(
+ "pending CSCB repair object %q exceeds approved range: object=[%d, %d) approved=[%d, %d)",
+ objectKey,
+ candidateStart,
+ candidateEnd,
+ startHeight,
+ endHeight,
+ )
+ }
+ objectKeys = append(objectKeys, objectKey)
+ }
+ if err := pendingRows.Err(); err != nil {
+ _ = pendingRows.Close()
+ return nil, xerrors.Errorf("failed to iterate pending CSCB repair candidates: %w", err)
+ }
+ if err := pendingRows.Close(); err != nil {
+ return nil, xerrors.Errorf("failed to close pending CSCB repair candidates: %w", err)
+ }
+
+ remaining := limit - len(objectKeys)
+ if remaining > 0 {
+ activeRows, err := r.db.QueryContext(ctx, `
+ WITH overlapping_keys AS (
+ SELECT DISTINCT bm.object_key_main
+ FROM block_metadata bm
+ WHERE bm.tag = $1
+ AND bm.height >= $2
+ AND bm.height < $3
+ AND bm.object_format = $4
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ AND NOT EXISTS (
+ SELECT 1
+ FROM cscb_repair_manifest repair
+ WHERE repair.tag = bm.tag
+ AND (
+ repair.old_consolidated_object_key_main = bm.object_key_main
+ OR repair.new_consolidated_object_key_main = bm.object_key_main
+ )
+ )
+ )
+ SELECT bm.object_key_main, MIN(bm.height), MAX(bm.height)
+ FROM block_metadata bm
+ JOIN overlapping_keys candidate ON candidate.object_key_main = bm.object_key_main
+ WHERE bm.tag = $1
+ AND bm.object_format = $4
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ GROUP BY bm.object_key_main
+ ORDER BY MAX(bm.height) DESC, bm.object_key_main DESC
+ LIMIT $5`,
+ tag,
+ startHeight,
+ endHeight,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ remaining,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to list active CSCB repair candidates: %w", err)
+ }
+ for activeRows.Next() {
+ var objectKey string
+ var minHeight, maxHeight uint64
+ if err := activeRows.Scan(&objectKey, &minHeight, &maxHeight); err != nil {
+ _ = activeRows.Close()
+ return nil, xerrors.Errorf("failed to scan active CSCB repair candidate: %w", err)
+ }
+ if minHeight < startHeight || maxHeight >= endHeight {
+ _ = activeRows.Close()
+ return nil, xerrors.Errorf(
+ "CSCB repair object %q crosses approved range: object=[%d, %d] approved=[%d, %d)",
+ objectKey,
+ minHeight,
+ maxHeight,
+ startHeight,
+ endHeight,
+ )
+ }
+ objectKeys = append(objectKeys, objectKey)
+ }
+ if err := activeRows.Err(); err != nil {
+ _ = activeRows.Close()
+ return nil, xerrors.Errorf("failed to iterate active CSCB repair candidates: %w", err)
+ }
+ if err := activeRows.Close(); err != nil {
+ return nil, xerrors.Errorf("failed to close active CSCB repair candidates: %w", err)
+ }
+ }
+
+ if len(objectKeys) == 0 {
+ if err := requireNoShadowOnlyCandidates(ctx, r.db, tag, startHeight, endHeight); err != nil {
+ return nil, err
+ }
+ }
+ return objectKeys, nil
+}
+
+func findNextCandidateObject(
+ ctx context.Context,
+ q queryer,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+) (string, error) {
+ if endHeight <= startHeight {
+ return "", nil
+ }
+ const query = `
+ WITH overlapping_keys AS (
+ SELECT DISTINCT bm.object_key_main
+ FROM block_metadata bm
+ WHERE bm.tag = $1
+ AND bm.height >= $2
+ AND bm.height < $3
+ AND bm.object_format = $4
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ AND NOT EXISTS (
+ SELECT 1
+ FROM cscb_repair_manifest repair
+ WHERE repair.tag = bm.tag
+ AND (
+ repair.old_consolidated_object_key_main = bm.object_key_main
+ OR repair.new_consolidated_object_key_main = bm.object_key_main
+ )
+ )
+ )
+ SELECT bm.object_key_main, MIN(bm.height), MAX(bm.height)
+ FROM block_metadata bm
+ JOIN overlapping_keys candidate ON candidate.object_key_main = bm.object_key_main
+ WHERE bm.tag = $1
+ AND bm.object_format = $4
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ GROUP BY bm.object_key_main
+ ORDER BY MAX(bm.height) DESC, bm.object_key_main DESC
+ LIMIT 1`
+ var objectKey string
+ var minHeight, maxHeight uint64
+ if err := q.QueryRowContext(
+ ctx,
+ query,
+ tag,
+ startHeight,
+ endHeight,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ).Scan(&objectKey, &minHeight, &maxHeight); err != nil {
+ if err == sql.ErrNoRows {
+ return "", nil
+ }
+ return "", xerrors.Errorf("failed to find next active CSCB repair candidate: %w", err)
+ }
+ if minHeight < startHeight || maxHeight >= endHeight {
+ return "", xerrors.Errorf(
+ "CSCB repair object %q crosses approved range: object=[%d, %d] approved=[%d, %d)",
+ objectKey,
+ minHeight,
+ maxHeight,
+ startHeight,
+ endHeight,
+ )
+ }
+ return objectKey, nil
+}
+
+func requireNoShadowOnlyCandidates(
+ ctx context.Context,
+ q queryer,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+) error {
+ const query = `
+ SELECT shadow.consolidated_object_key_main, MIN(bm.height), MAX(bm.height)
+ FROM block_consolidation_shadow shadow
+ JOIN block_metadata bm ON bm.id = shadow.block_metadata_id
+ AND bm.tag = shadow.tag
+ AND bm.height = shadow.height
+ AND bm.hash IS NOT DISTINCT FROM shadow.hash
+ WHERE shadow.tag = $1
+ AND shadow.height >= $2
+ AND shadow.height < $3
+ AND shadow.object_format = $4
+ AND shadow.consolidated_object_key_main IS NOT NULL
+ AND shadow.consolidated_object_key_main <> ''
+ AND (
+ bm.object_key_main IS DISTINCT FROM shadow.consolidated_object_key_main
+ OR bm.object_format <> $4
+ )
+ AND NOT EXISTS (
+ SELECT 1
+ FROM cscb_repair_manifest repair
+ WHERE repair.tag = shadow.tag
+ AND (
+ repair.old_consolidated_object_key_main = shadow.consolidated_object_key_main
+ OR repair.new_consolidated_object_key_main = shadow.consolidated_object_key_main
+ )
+ )
+ GROUP BY shadow.consolidated_object_key_main
+ ORDER BY MAX(bm.height) DESC, shadow.consolidated_object_key_main DESC
+ LIMIT 1`
+ var objectKey string
+ var minHeight, maxHeight uint64
+ if err := q.QueryRowContext(
+ ctx,
+ query,
+ tag,
+ startHeight,
+ endHeight,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ).Scan(&objectKey, &minHeight, &maxHeight); err != nil {
+ if err == sql.ErrNoRows {
+ return nil
+ }
+ return xerrors.Errorf("failed to inspect shadow-only CSCB repair candidates: %w", err)
+ }
+ return xerrors.Errorf(
+ "CSCB repair range contains shadow-only consolidated object %q at heights [%d, %d]",
+ objectKey,
+ minHeight,
+ maxHeight,
+ )
+}
+
+func (r *PostgresRepository) FenceCandidate(ctx context.Context, manifest *Manifest) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if err := validateFencedInput(manifest); err != nil {
+ return nil, err
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to begin CSCB repair fencing: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ if err := lockRepairTag(ctx, tx, manifest.Tag); err != nil {
+ return nil, err
+ }
+ if err := lockCandidateRows(ctx, tx, manifest.Blocks); err != nil {
+ return nil, err
+ }
+ if err := lockConsolidatedObjectKey(ctx, tx, manifest.OldConsolidatedObjectKey); err != nil {
+ return nil, err
+ }
+ current, err := loadCandidateByKey(ctx, tx, manifest.Tag, manifest.OldConsolidatedObjectKey)
+ if err != nil {
+ return nil, err
+ }
+ if err := compareCandidateRows(current, manifest); err != nil {
+ return nil, err
+ }
+ if err := requireNoUnexpectedOldReferences(ctx, tx, manifest.OldConsolidatedObjectKey, manifest.Blocks); err != nil {
+ return nil, err
+ }
+
+ const insertManifest = `
+ INSERT INTO cscb_repair_manifest (
+ tag, state, bucket, old_consolidated_object_key_main,
+ start_height, end_height, canonical_block_count, total_block_count, row_set_sha256
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
+ ON CONFLICT (tag, old_consolidated_object_key_main) DO NOTHING
+ RETURNING id`
+ var repairID int64
+ err = tx.QueryRowContext(
+ ctx,
+ insertManifest,
+ manifest.Tag,
+ StatePreparing,
+ manifest.Bucket,
+ manifest.OldConsolidatedObjectKey,
+ manifest.StartHeight,
+ manifest.EndHeight,
+ manifest.CanonicalBlockCount,
+ manifest.TotalBlockCount,
+ manifest.RowSetSHA256,
+ ).Scan(&repairID)
+ inserted := err == nil
+ if err != nil && err != sql.ErrNoRows {
+ return nil, xerrors.Errorf("failed to insert CSCB repair manifest: %w", err)
+ }
+
+ if inserted {
+ const insertBlock = `
+ INSERT INTO cscb_repair_block (
+ repair_id, block_metadata_id, canonical, tag, height, hash,
+ single_block_object_key_main, single_block_object_key_sha256,
+ old_byte_offset, old_byte_length, old_uncompressed_length
+ ) VALUES ($1, $2, $3, $4, $5, NULLIF($6, ''), $7, $8, $9, $10, $11)`
+ for _, block := range manifest.Blocks {
+ if _, err := tx.ExecContext(
+ ctx,
+ insertBlock,
+ repairID,
+ block.BlockMetadataID,
+ block.Canonical,
+ block.Tag,
+ block.Height,
+ block.Hash,
+ block.SingleBlockObjectKey,
+ block.SingleBlockObjectKeySHA256,
+ block.OldByteOffset,
+ block.OldByteLength,
+ block.OldUncompressedLength,
+ ); err != nil {
+ return nil, xerrors.Errorf("failed to insert CSCB repair block metadata_id=%d: %w", block.BlockMetadataID, err)
+ }
+ }
+ } else {
+ const existingID = `
+ SELECT id
+ FROM cscb_repair_manifest
+ WHERE tag = $1 AND old_consolidated_object_key_main = $2`
+ if err := tx.QueryRowContext(ctx, existingID, manifest.Tag, manifest.OldConsolidatedObjectKey).Scan(&repairID); err != nil {
+ return nil, xerrors.Errorf("failed to load concurrent CSCB repair manifest: %w", err)
+ }
+ }
+
+ fenced, err := loadManifest(ctx, tx, repairID, false)
+ if err != nil {
+ return nil, err
+ }
+ if !sameFencedManifest(fenced, manifest) {
+ return nil, xerrors.Errorf("CSCB repair manifest conflict for old object %q", manifest.OldConsolidatedObjectKey)
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit CSCB repair fence: %w", err)
+ }
+ return fenced, nil
+}
+
+func (r *PostgresRepository) RecordInspection(
+ ctx context.Context,
+ repairID int64,
+ oldObject ObjectVersion,
+ blocks []Block,
+ alreadyClean bool,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if err := validateInspectionInput(oldObject, blocks); err != nil {
+ return nil, err
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to begin recording CSCB repair inspection: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ manifest, err := lockManifestForRepair(ctx, tx, repairID)
+ if err != nil {
+ return nil, err
+ }
+ if manifest.State != StatePreparing {
+ if !sameInspection(manifest, oldObject, blocks, alreadyClean) {
+ return nil, xerrors.Errorf("CSCB repair inspection changed for repair id %d", repairID)
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit idempotent CSCB repair inspection: %w", err)
+ }
+ return manifest, nil
+ }
+ if len(blocks) != len(manifest.Blocks) {
+ return nil, xerrors.Errorf("CSCB repair inspection block count changed: expected=%d actual=%d", len(manifest.Blocks), len(blocks))
+ }
+ if err := lockCandidateRows(ctx, tx, manifest.Blocks); err != nil {
+ return nil, err
+ }
+ if err := lockConsolidatedObjectKey(ctx, tx, manifest.OldConsolidatedObjectKey); err != nil {
+ return nil, err
+ }
+ current, err := loadCandidateByKey(ctx, tx, manifest.Tag, manifest.OldConsolidatedObjectKey)
+ if err != nil {
+ return nil, err
+ }
+ if err := compareCandidateRows(current, manifest); err != nil {
+ return nil, err
+ }
+ if err := requireNoUnexpectedOldReferences(ctx, tx, manifest.OldConsolidatedObjectKey, manifest.Blocks); err != nil {
+ return nil, err
+ }
+
+ const updateBlock = `
+ UPDATE cscb_repair_block
+ SET single_block_object_version_id = $3,
+ single_block_object_etag = $4,
+ single_block_object_bytes = $5,
+ payload_sha256 = $6
+ WHERE repair_id = $1
+ AND block_metadata_id = $2
+ AND single_block_object_key_main = $7
+ AND single_block_object_key_sha256 = $8
+ AND single_block_object_version_id IS NULL
+ AND single_block_object_etag IS NULL
+ AND single_block_object_bytes IS NULL
+ AND payload_sha256 IS NULL`
+ for i := range blocks {
+ block := &blocks[i]
+ expected := manifest.Blocks[i]
+ if block.BlockMetadataID != expected.BlockMetadataID ||
+ block.SingleBlockObjectKey != expected.SingleBlockObjectKey ||
+ block.SingleBlockObjectKeySHA256 != expected.SingleBlockObjectKeySHA256 {
+ return nil, xerrors.Errorf("CSCB repair inspection row changed for metadata_id=%d", expected.BlockMetadataID)
+ }
+ result, err := tx.ExecContext(
+ ctx,
+ updateBlock,
+ manifest.ID,
+ block.BlockMetadataID,
+ block.SingleBlockObjectVersion.VersionID,
+ block.SingleBlockObjectVersion.ETag,
+ block.SingleBlockObjectVersion.Bytes,
+ block.PayloadSHA256,
+ block.SingleBlockObjectKey,
+ block.SingleBlockObjectKeySHA256,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to record CSCB repair block inspection metadata_id=%d: %w", block.BlockMetadataID, err)
+ }
+ if err := requireRowsAffected(result, 1, "record CSCB repair block inspection"); err != nil {
+ return nil, err
+ }
+ }
+
+ state := StatePrepared
+ outcome := ""
+ terminal := false
+ if alreadyClean {
+ state = StateCompleted
+ outcome = alreadyCleanOutcome
+ terminal = true
+ }
+ const updateManifest = `
+ UPDATE cscb_repair_manifest
+ SET state = $2,
+ old_consolidated_object_version_id = $3,
+ old_consolidated_object_etag = $4,
+ old_consolidated_object_bytes = $5,
+ outcome = $6,
+ verified_at = CASE WHEN $7 THEN clock_timestamp() ELSE NULL END,
+ completed_at = CASE WHEN $7 THEN clock_timestamp() ELSE NULL END,
+ updated_at = clock_timestamp()
+ WHERE id = $1 AND state = $8`
+ result, err := tx.ExecContext(
+ ctx,
+ updateManifest,
+ manifest.ID,
+ state,
+ oldObject.VersionID,
+ oldObject.ETag,
+ oldObject.Bytes,
+ outcome,
+ terminal,
+ StatePreparing,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to record CSCB repair inspection: %w", err)
+ }
+ if err := requireRowsAffected(result, 1, "record CSCB repair inspection"); err != nil {
+ return nil, err
+ }
+ inspected, err := loadManifest(ctx, tx, manifest.ID, false)
+ if err != nil {
+ return nil, err
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit CSCB repair inspection: %w", err)
+ }
+ return inspected, nil
+}
+
+func (r *PostgresRepository) BindExecutionKey(
+ ctx context.Context,
+ executionKey string,
+ repairID int64,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if !validExecutionKey(executionKey) || repairID <= 0 {
+ return nil, xerrors.New("valid CSCB repair execution binding is required")
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to begin CSCB repair execution binding: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ const insert = `
+ INSERT INTO cscb_repair_execution (execution_key, repair_id)
+ VALUES ($1, $2)
+ ON CONFLICT (execution_key) DO NOTHING`
+ if _, err := tx.ExecContext(ctx, insert, executionKey, repairID); err != nil {
+ return nil, xerrors.Errorf("failed to bind CSCB repair execution: %w", err)
+ }
+ var boundRepairID sql.NullInt64
+ if err := tx.QueryRowContext(ctx, `
+ SELECT repair_id
+ FROM cscb_repair_execution
+ WHERE execution_key = $1
+ FOR UPDATE`, executionKey).Scan(&boundRepairID); err != nil {
+ return nil, xerrors.Errorf("failed to load CSCB repair execution binding: %w", err)
+ }
+ if !boundRepairID.Valid {
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit CSCB repair no-candidate execution binding: %w", err)
+ }
+ return nil, nil
+ }
+ if boundRepairID.Int64 != repairID {
+ return nil, xerrors.Errorf(
+ "CSCB repair execution key is already bound to repair id %d, requested %d",
+ boundRepairID.Int64,
+ repairID,
+ )
+ }
+ manifest, err := loadManifest(ctx, tx, boundRepairID.Int64, false)
+ if err != nil {
+ return nil, err
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit CSCB repair execution binding: %w", err)
+ }
+ return manifest, nil
+}
+
+func (r *PostgresRepository) BindNoCandidateExecution(
+ ctx context.Context,
+ executionKey string,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if !validExecutionKey(executionKey) {
+ return nil, xerrors.New("valid CSCB repair execution key is required")
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to begin CSCB repair no-candidate execution binding: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ if err := lockRepairTag(ctx, tx, tag); err != nil {
+ return nil, err
+ }
+ objectKey, err := findNextCandidateObject(ctx, tx, tag, startHeight, endHeight)
+ if err != nil {
+ return nil, xerrors.Errorf("failed terminal CSCB repair candidate check: %w", err)
+ }
+ if objectKey != "" {
+ return nil, xerrors.Errorf("CSCB repair candidate appeared before range completion: object=%q", objectKey)
+ }
+ if err := requireNoShadowOnlyCandidates(ctx, tx, tag, startHeight, endHeight); err != nil {
+ return nil, xerrors.Errorf("failed terminal CSCB repair shadow check: %w", err)
+ }
+
+ if _, err := tx.ExecContext(ctx, `
+ INSERT INTO cscb_repair_execution (execution_key, repair_id)
+ VALUES ($1, NULL)
+ ON CONFLICT (execution_key) DO NOTHING`, executionKey); err != nil {
+ return nil, xerrors.Errorf("failed to bind CSCB repair no-candidate execution: %w", err)
+ }
+ var boundRepairID sql.NullInt64
+ if err := tx.QueryRowContext(ctx, `
+ SELECT repair_id
+ FROM cscb_repair_execution
+ WHERE execution_key = $1
+ FOR UPDATE`, executionKey).Scan(&boundRepairID); err != nil {
+ return nil, xerrors.Errorf("failed to load CSCB repair no-candidate execution binding: %w", err)
+ }
+ var manifest *Manifest
+ if boundRepairID.Valid {
+ manifest, err = loadManifest(ctx, tx, boundRepairID.Int64, false)
+ if err != nil {
+ return nil, err
+ }
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit CSCB repair no-candidate execution binding: %w", err)
+ }
+ return manifest, nil
+}
+
+func (r *PostgresRepository) Get(ctx context.Context, repairID int64) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ return loadManifest(ctx, r.db, repairID, false)
+}
+
+func (r *PostgresRepository) RestoreToSingleBlock(
+ ctx context.Context,
+ repairID int64,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to begin CSCB repair restore: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ manifest, err := lockManifestForRepair(ctx, tx, repairID)
+ if err != nil {
+ return nil, err
+ }
+ if manifest.State != StatePrepared {
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit idempotent CSCB repair restore: %w", err)
+ }
+ return manifest, nil
+ }
+ if err := lockCandidateRows(ctx, tx, manifest.Blocks); err != nil {
+ return nil, err
+ }
+ current, err := loadCandidateByKey(ctx, tx, manifest.Tag, manifest.OldConsolidatedObjectKey)
+ if err != nil {
+ return nil, err
+ }
+ if err := compareCandidateRows(current, manifest); err != nil {
+ return nil, err
+ }
+
+ if manifest.CanonicalBlockCount > 0 {
+ const unrelatedCanonical = `
+ SELECT bm.id
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON bm.id = cb.block_metadata_id
+ AND bm.tag = cb.tag
+ AND bm.height = cb.height
+ WHERE cb.tag = $1
+ AND cb.height >= $2
+ AND cb.height < $3
+ AND bm.skipped = FALSE
+ AND NOT EXISTS (
+ SELECT 1 FROM cscb_repair_block block
+ WHERE block.repair_id = $4 AND block.block_metadata_id = bm.id
+ )
+ LIMIT 1`
+ var unrelatedID int64
+ err = tx.QueryRowContext(
+ ctx,
+ unrelatedCanonical,
+ manifest.Tag,
+ manifest.StartHeight,
+ manifest.EndHeight,
+ manifest.ID,
+ ).Scan(&unrelatedID)
+ if err != nil && err != sql.ErrNoRows {
+ return nil, xerrors.Errorf("failed to check CSCB repair range isolation: %w", err)
+ }
+ if err == nil {
+ return nil, xerrors.Errorf("CSCB repair range contains unrelated canonical metadata_id=%d", unrelatedID)
+ }
+ }
+
+ const restoreMetadata = `
+ UPDATE block_metadata bm
+ SET object_key_main = block.single_block_object_key_main,
+ object_format = $2,
+ byte_offset = NULL,
+ byte_length = NULL,
+ uncompressed_length = NULL
+ FROM cscb_repair_block block
+ WHERE block.repair_id = $1
+ AND bm.id = block.block_metadata_id
+ AND bm.tag = block.tag
+ AND bm.height = block.height
+ AND bm.hash IS NOT DISTINCT FROM block.hash
+ AND bm.skipped = FALSE
+ AND bm.single_block_retention_fenced_at IS NULL
+ AND bm.object_key_main = $3
+ AND bm.object_format = $4
+ AND bm.byte_offset = block.old_byte_offset
+ AND bm.byte_length = block.old_byte_length
+ AND bm.uncompressed_length = block.old_uncompressed_length`
+ result, err := tx.ExecContext(
+ ctx,
+ restoreMetadata,
+ manifest.ID,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK,
+ manifest.OldConsolidatedObjectKey,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to restore active single-block metadata: %w", err)
+ }
+ if err := requireRowsAffected(result, manifest.TotalBlockCount, "restore active single-block metadata"); err != nil {
+ return nil, err
+ }
+
+ const deleteOldShadows = `
+ DELETE FROM block_consolidation_shadow shadow
+ USING cscb_repair_block block
+ WHERE block.repair_id = $1
+ AND shadow.block_metadata_id = block.block_metadata_id
+ AND shadow.tag = block.tag
+ AND shadow.height = block.height
+ AND shadow.hash IS NOT DISTINCT FROM block.hash
+ AND shadow.single_block_object_key_main = block.single_block_object_key_main
+ AND shadow.single_block_object_deleted_at IS NULL
+ AND shadow.consolidated_object_key_main = $2
+ AND shadow.object_format = $3
+ AND shadow.byte_offset = block.old_byte_offset
+ AND shadow.byte_length = block.old_byte_length
+ AND shadow.uncompressed_length = block.old_uncompressed_length`
+ result, err = tx.ExecContext(
+ ctx,
+ deleteOldShadows,
+ manifest.ID,
+ manifest.OldConsolidatedObjectKey,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to remove invalidated CSCB shadows: %w", err)
+ }
+ if err := requireRowsAffected(result, manifest.TotalBlockCount, "remove invalidated CSCB shadows"); err != nil {
+ return nil, err
+ }
+
+ const markRestored = `
+ UPDATE cscb_repair_manifest
+ SET state = $2,
+ restored_at = clock_timestamp(),
+ updated_at = clock_timestamp()
+ WHERE id = $1 AND state = $3`
+ result, err = tx.ExecContext(ctx, markRestored, manifest.ID, StateRestored, StatePrepared)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to mark CSCB repair restored: %w", err)
+ }
+ if err := requireRowsAffected(result, 1, "mark CSCB repair restored"); err != nil {
+ return nil, err
+ }
+ restored, err := loadManifest(ctx, tx, manifest.ID, false)
+ if err != nil {
+ return nil, err
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit CSCB repair restore: %w", err)
+ }
+ return restored, nil
+}
+
+func (r *PostgresRepository) GetRebuilt(ctx context.Context, repairID int64) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ manifest, err := loadManifest(ctx, r.db, repairID, false)
+ if err != nil {
+ return nil, err
+ }
+ if err := loadRebuiltBlocks(ctx, r.db, manifest); err != nil {
+ return nil, err
+ }
+ return manifest, nil
+}
+
+func (r *PostgresRepository) RecordVerified(
+ ctx context.Context,
+ repairID int64,
+ objectKey string,
+ object ObjectVersion,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if objectKey == "" || object.VersionID == "" || object.ETag == "" || object.Bytes == 0 {
+ return nil, xerrors.New("verified CSCB object identity is required")
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to begin recording verified CSCB repair: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ manifest, err := lockManifestForRepair(ctx, tx, repairID)
+ if err != nil {
+ return nil, err
+ }
+ if manifest.State == StateVerified || manifest.State == StateCompleted {
+ if manifest.NewConsolidatedObjectKey != objectKey ||
+ manifest.NewConsolidatedObjectVersion.VersionID != object.VersionID ||
+ manifest.NewConsolidatedObjectVersion.ETag != object.ETag ||
+ manifest.NewConsolidatedObjectVersion.Bytes != object.Bytes {
+ return nil, xerrors.Errorf("verified CSCB repair identity changed for repair id %d", manifest.ID)
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit idempotent verified CSCB repair: %w", err)
+ }
+ return manifest, nil
+ }
+ if manifest.State != StateRestored {
+ return nil, xerrors.Errorf("CSCB repair must be restored before verification: id=%d state=%s", manifest.ID, manifest.State)
+ }
+ if err := lockRepairRows(ctx, tx, manifest.Blocks); err != nil {
+ return nil, err
+ }
+ if err := lockConsolidatedObjectKeys(ctx, tx, manifest.OldConsolidatedObjectKey, objectKey); err != nil {
+ return nil, err
+ }
+ if err := loadRebuiltBlocks(ctx, tx, manifest); err != nil {
+ return nil, err
+ }
+ if err := validateRebuiltMetadata(manifest, objectKey); err != nil {
+ return nil, err
+ }
+ if err := requireExactNewReferences(ctx, tx, manifest, objectKey); err != nil {
+ return nil, err
+ }
+ if err := requireNoOldReferences(ctx, tx, manifest.OldConsolidatedObjectKey); err != nil {
+ return nil, err
+ }
+
+ const update = `
+ UPDATE cscb_repair_manifest
+ SET state = $2,
+ new_consolidated_object_key_main = $3,
+ new_consolidated_object_version_id = $4,
+ new_consolidated_object_etag = $5,
+ new_consolidated_object_bytes = $6,
+ verified_at = clock_timestamp(),
+ updated_at = clock_timestamp()
+ WHERE id = $1 AND state = $7`
+ result, err := tx.ExecContext(
+ ctx,
+ update,
+ manifest.ID,
+ StateVerified,
+ objectKey,
+ object.VersionID,
+ object.ETag,
+ object.Bytes,
+ StateRestored,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to record verified CSCB repair: %w", err)
+ }
+ if err := requireRowsAffected(result, 1, "record verified CSCB repair"); err != nil {
+ return nil, err
+ }
+ verified, err := loadManifest(ctx, tx, manifest.ID, false)
+ if err != nil {
+ return nil, err
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit verified CSCB repair: %w", err)
+ }
+ return verified, nil
+}
+
+func (r *PostgresRepository) PromoteCompleted(
+ ctx context.Context,
+ repairID int64,
+ objectKey string,
+ object ObjectVersion,
+ placements []RebuiltPlacement,
+ singleBlockObjectRetention time.Duration,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if objectKey == "" || object.VersionID == "" || object.ETag == "" || object.Bytes == 0 {
+ return nil, xerrors.New("verified CSCB object identity is required")
+ }
+ if singleBlockObjectRetention <= 0 {
+ return nil, xerrors.New("positive single-block object retention is required")
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to begin atomic CSCB repair promotion: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ manifest, err := lockManifestForRepair(ctx, tx, repairID)
+ if err != nil {
+ return nil, err
+ }
+ if manifest.State == StateCompleted {
+ if manifest.NewConsolidatedObjectKey != objectKey ||
+ manifest.NewConsolidatedObjectVersion != object {
+ return nil, xerrors.Errorf("completed CSCB repair identity changed for repair id %d", manifest.ID)
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit idempotent atomic CSCB repair promotion: %w", err)
+ }
+ return manifest, nil
+ }
+ if manifest.State != StatePrepared && manifest.State != StateRestored {
+ return nil, xerrors.Errorf(
+ "CSCB repair must be prepared or restored before atomic promotion: id=%d state=%s",
+ manifest.ID,
+ manifest.State,
+ )
+ }
+ placementsByID, err := validateRebuiltPlacements(manifest, objectKey, placements)
+ if err != nil {
+ return nil, err
+ }
+ if err := lockRepairRows(ctx, tx, manifest.Blocks); err != nil {
+ return nil, err
+ }
+ if err := lockConsolidatedObjectKeys(ctx, tx, manifest.OldConsolidatedObjectKey, objectKey); err != nil {
+ return nil, err
+ }
+ if err := requireNoUnexpectedOldReferences(ctx, tx, manifest.OldConsolidatedObjectKey, manifest.Blocks); err != nil {
+ return nil, err
+ }
+ var promotionTime time.Time
+ if err := tx.QueryRowContext(ctx, `SELECT clock_timestamp()`).Scan(&promotionTime); err != nil {
+ return nil, xerrors.Errorf("failed to capture CSCB repair promotion time: %w", err)
+ }
+ if manifest.State == StatePrepared {
+ const markRestored = `
+ UPDATE cscb_repair_manifest
+ SET state = $2,
+ restored_at = $3,
+ updated_at = $3
+ WHERE id = $1 AND state = $4`
+ result, err := tx.ExecContext(ctx, markRestored, manifest.ID, StateRestored, promotionTime, StatePrepared)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to mark CSCB repair restored during atomic promotion: %w", err)
+ }
+ if err := requireRowsAffected(result, 1, "mark CSCB repair restored during atomic promotion"); err != nil {
+ return nil, err
+ }
+ manifest.State = StateRestored
+ manifest.RestoredAt = &promotionTime
+ }
+ deleteAfter := promotionTime.Add(singleBlockObjectRetention + repairPromotionRetentionCommitGrace)
+
+ for _, block := range manifest.Blocks {
+ placement := placementsByID[block.BlockMetadataID]
+ if err := validateRepairPromotionRow(ctx, tx, manifest, block, objectKey, placement); err != nil {
+ return nil, err
+ }
+
+ const upsertShadow = `
+ INSERT INTO block_consolidation_shadow (
+ block_metadata_id, tag, height, hash, single_block_object_key_main,
+ consolidated_object_key_main, object_format, byte_offset, byte_length,
+ uncompressed_length, validated_at, single_block_retention_started_at,
+ single_block_delete_after
+ ) VALUES ($1, $2, $3, NULLIF($4, ''), $5, $6, $7, $8, $9, $10, $11, $11, $12)
+ ON CONFLICT (block_metadata_id) DO UPDATE SET
+ tag = EXCLUDED.tag,
+ height = EXCLUDED.height,
+ hash = EXCLUDED.hash,
+ single_block_object_key_main = EXCLUDED.single_block_object_key_main,
+ consolidated_object_key_main = EXCLUDED.consolidated_object_key_main,
+ object_format = EXCLUDED.object_format,
+ byte_offset = EXCLUDED.byte_offset,
+ byte_length = EXCLUDED.byte_length,
+ uncompressed_length = EXCLUDED.uncompressed_length,
+ validated_at = EXCLUDED.validated_at,
+ single_block_retention_started_at = EXCLUDED.single_block_retention_started_at,
+ single_block_delete_after = EXCLUDED.single_block_delete_after
+ WHERE block_consolidation_shadow.tag = EXCLUDED.tag
+ AND block_consolidation_shadow.height = EXCLUDED.height
+ AND block_consolidation_shadow.hash IS NOT DISTINCT FROM EXCLUDED.hash
+ AND block_consolidation_shadow.single_block_object_key_main = EXCLUDED.single_block_object_key_main
+ AND block_consolidation_shadow.single_block_object_deleted_at IS NULL
+ RETURNING block_metadata_id`
+ var shadowID int64
+ if err := tx.QueryRowContext(
+ ctx,
+ upsertShadow,
+ block.BlockMetadataID,
+ block.Tag,
+ block.Height,
+ block.Hash,
+ block.SingleBlockObjectKey,
+ objectKey,
+ int32(placement.ObjectFormat),
+ placement.ByteOffset,
+ placement.ByteLength,
+ placement.UncompressedLength,
+ promotionTime,
+ deleteAfter,
+ ).Scan(&shadowID); err != nil {
+ return nil, xerrors.Errorf("failed to upsert repaired CSCB shadow for metadata_id=%d: %w", block.BlockMetadataID, err)
+ }
+
+ const promoteMetadata = `
+ UPDATE block_metadata
+ SET object_key_main = $5,
+ object_format = $6,
+ byte_offset = $7,
+ byte_length = $8,
+ uncompressed_length = $9
+ WHERE id = $1
+ AND tag = $2
+ AND height = $3
+ AND hash IS NOT DISTINCT FROM NULLIF($4, '')
+ AND skipped = FALSE
+ AND single_block_retention_fenced_at IS NULL`
+ result, err := tx.ExecContext(
+ ctx,
+ promoteMetadata,
+ block.BlockMetadataID,
+ block.Tag,
+ block.Height,
+ block.Hash,
+ objectKey,
+ int32(placement.ObjectFormat),
+ placement.ByteOffset,
+ placement.ByteLength,
+ placement.UncompressedLength,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to promote repaired CSCB metadata_id=%d: %w", block.BlockMetadataID, err)
+ }
+ if err := requireRowsAffected(result, 1, "promote repaired CSCB metadata"); err != nil {
+ return nil, err
+ }
+ }
+
+ if err := requireNoOldReferences(ctx, tx, manifest.OldConsolidatedObjectKey); err != nil {
+ return nil, err
+ }
+ if err := requireExactRepairedReferences(ctx, tx, manifest, objectKey); err != nil {
+ return nil, err
+ }
+ const markVerified = `
+ UPDATE cscb_repair_manifest
+ SET state = $2,
+ new_consolidated_object_key_main = $3,
+ new_consolidated_object_version_id = $4,
+ new_consolidated_object_etag = $5,
+ new_consolidated_object_bytes = $6,
+ restored_at = COALESCE(restored_at, $7),
+ verified_at = $7,
+ updated_at = $7
+ WHERE id = $1 AND state = $8`
+ result, err := tx.ExecContext(
+ ctx,
+ markVerified,
+ manifest.ID,
+ StateVerified,
+ objectKey,
+ object.VersionID,
+ object.ETag,
+ object.Bytes,
+ promotionTime,
+ StateRestored,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to mark atomically promoted CSCB repair verified: %w", err)
+ }
+ if err := requireRowsAffected(result, 1, "mark atomically promoted CSCB repair verified"); err != nil {
+ return nil, err
+ }
+ const markCompleted = `
+ UPDATE cscb_repair_manifest
+ SET state = $2,
+ outcome = $3,
+ completed_at = $4,
+ updated_at = $4
+ WHERE id = $1 AND state = $5`
+ result, err = tx.ExecContext(
+ ctx,
+ markCompleted,
+ manifest.ID,
+ StateCompleted,
+ completedOutcome,
+ promotionTime,
+ StateVerified,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to complete atomically promoted CSCB repair: %w", err)
+ }
+ if err := requireRowsAffected(result, 1, "complete atomically promoted CSCB repair"); err != nil {
+ return nil, err
+ }
+ completed, err := loadManifest(ctx, tx, manifest.ID, false)
+ if err != nil {
+ return nil, err
+ }
+ if err := loadRebuiltBlocks(ctx, tx, completed); err != nil {
+ return nil, err
+ }
+ if err := validateRebuiltMetadata(completed, objectKey); err != nil {
+ return nil, err
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit atomic CSCB repair promotion: %w", err)
+ }
+ return completed, nil
+}
+
+func (r *PostgresRepository) CompleteRetainingOldObject(
+ ctx context.Context,
+ repairID int64,
+ outcome string,
+) (*Manifest, error) {
+ if err := r.validateDB(); err != nil {
+ return nil, err
+ }
+ if outcome == "" {
+ return nil, xerrors.New("CSCB repair completion outcome is required")
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to begin CSCB repair completion: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+ manifest, err := lockManifestForRepair(ctx, tx, repairID)
+ if err != nil {
+ return nil, err
+ }
+ if manifest.State == StateCompleted {
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit idempotent CSCB repair completion: %w", err)
+ }
+ return manifest, nil
+ }
+ if manifest.CanonicalBlockCount == 0 {
+ if manifest.State != StateRestored {
+ return nil, xerrors.Errorf("zero-canonical CSCB repair must be restored before completion: id=%d state=%s", manifest.ID, manifest.State)
+ }
+ } else if manifest.State != StateVerified {
+ return nil, xerrors.Errorf("CSCB repair must be verified before completion: id=%d state=%s", manifest.ID, manifest.State)
+ }
+ if err := lockRepairRows(ctx, tx, manifest.Blocks); err != nil {
+ return nil, err
+ }
+ if err := lockConsolidatedObjectKeys(
+ ctx,
+ tx,
+ manifest.OldConsolidatedObjectKey,
+ manifest.NewConsolidatedObjectKey,
+ ); err != nil {
+ return nil, err
+ }
+ if err := loadRebuiltBlocks(ctx, tx, manifest); err != nil {
+ return nil, err
+ }
+ if err := validateRebuiltMetadata(manifest, manifest.NewConsolidatedObjectKey); err != nil {
+ return nil, err
+ }
+ if manifest.CanonicalBlockCount > 0 {
+ var err error
+ if allRepairRowsOnObject(manifest, manifest.NewConsolidatedObjectKey) {
+ err = requireExactRepairedReferences(ctx, tx, manifest, manifest.NewConsolidatedObjectKey)
+ } else {
+ err = requireExactNewReferences(ctx, tx, manifest, manifest.NewConsolidatedObjectKey)
+ }
+ if err != nil {
+ return nil, err
+ }
+ }
+ if err := requireNoOldReferences(ctx, tx, manifest.OldConsolidatedObjectKey); err != nil {
+ return nil, err
+ }
+ const update = `
+ UPDATE cscb_repair_manifest
+ SET state = $2,
+ outcome = $3,
+ completed_at = clock_timestamp(),
+ updated_at = clock_timestamp()
+ WHERE id = $1 AND state = $4`
+ result, err := tx.ExecContext(ctx, update, manifest.ID, StateCompleted, outcome, manifest.State)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to complete CSCB repair: %w", err)
+ }
+ if err := requireRowsAffected(result, 1, "complete CSCB repair"); err != nil {
+ return nil, err
+ }
+ completed, err := loadManifest(ctx, tx, manifest.ID, false)
+ if err != nil {
+ return nil, err
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit CSCB repair completion: %w", err)
+ }
+ return completed, nil
+}
+
+func (r *PostgresRepository) validateDB() error {
+ if r == nil || r.db == nil {
+ return xerrors.New("postgres db is required for CSCB repair")
+ }
+ return nil
+}
+
+func loadCandidateByKey(ctx context.Context, q queryer, tag uint32, objectKey string) (*Manifest, error) {
+ const query = `
+ SELECT
+ bm.id,
+ EXISTS (
+ SELECT 1 FROM canonical_blocks cb
+ WHERE cb.block_metadata_id = bm.id AND cb.tag = bm.tag AND cb.height = bm.height
+ ) AS canonical,
+ bm.tag,
+ bm.height,
+ COALESCE(bm.hash, ''),
+ bm.skipped,
+ (bm.single_block_retention_fenced_at IS NOT NULL),
+ (retirement.block_metadata_id IS NOT NULL),
+ COALESCE(shadow.single_block_object_key_main, ''),
+ (shadow.single_block_object_deleted_at IS NOT NULL),
+ COALESCE(shadow.consolidated_object_key_main, ''),
+ bm.byte_offset,
+ bm.byte_length,
+ bm.uncompressed_length
+ FROM block_metadata bm
+ LEFT JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ LEFT JOIN block_single_block_retention retirement ON retirement.block_metadata_id = bm.id
+ WHERE bm.tag = $1
+ AND bm.object_key_main = $2
+ AND bm.object_key_main <> ''
+ AND bm.object_format = $3
+ ORDER BY bm.height ASC, bm.id ASC`
+ rows, err := q.QueryContext(ctx, query, tag, objectKey, api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to query active CSCB repair rows: %w", err)
+ }
+ defer func() { _ = rows.Close() }()
+ manifest := &Manifest{
+ Tag: tag,
+ OldConsolidatedObjectKey: objectKey,
+ StartHeight: ^uint64(0),
+ }
+ for rows.Next() {
+ var block Block
+ var oldOffset, oldLength, oldUncompressed sql.NullInt64
+ if err := rows.Scan(
+ &block.BlockMetadataID,
+ &block.Canonical,
+ &block.Tag,
+ &block.Height,
+ &block.Hash,
+ &block.Skipped,
+ &block.RetirementFenced,
+ &block.RetirementManifestExists,
+ &block.SingleBlockObjectKey,
+ &block.SingleBlockObjectDeleted,
+ &block.OldConsolidatedObjectKey,
+ &oldOffset,
+ &oldLength,
+ &oldUncompressed,
+ ); err != nil {
+ return nil, xerrors.Errorf("failed to scan active CSCB repair row: %w", err)
+ }
+ if oldOffset.Valid && oldOffset.Int64 >= 0 {
+ block.OldByteOffset = uint64(oldOffset.Int64)
+ }
+ if oldLength.Valid && oldLength.Int64 >= 0 {
+ block.OldByteLength = uint64(oldLength.Int64)
+ }
+ if oldUncompressed.Valid && oldUncompressed.Int64 >= 0 {
+ block.OldUncompressedLength = uint64(oldUncompressed.Int64)
+ }
+ manifest.Blocks = append(manifest.Blocks, block)
+ manifest.TotalBlockCount++
+ if block.Canonical {
+ manifest.CanonicalBlockCount++
+ }
+ if block.Height < manifest.StartHeight {
+ manifest.StartHeight = block.Height
+ }
+ if block.Height == ^uint64(0) {
+ return nil, xerrors.New("CSCB repair row height overflows exclusive end height")
+ }
+ if block.Height+1 > manifest.EndHeight {
+ manifest.EndHeight = block.Height + 1
+ }
+ }
+ if err := rows.Err(); err != nil {
+ return nil, xerrors.Errorf("failed to iterate active CSCB repair rows: %w", err)
+ }
+ if len(manifest.Blocks) == 0 {
+ return nil, xerrors.Errorf("active CSCB repair object %q has no metadata references", objectKey)
+ }
+ return manifest, nil
+}
+
+func loadManifest(ctx context.Context, q queryer, repairID int64, forUpdate bool) (*Manifest, error) {
+ suffix := ""
+ if forUpdate {
+ suffix = " FOR UPDATE"
+ }
+ query := fmt.Sprintf(`SELECT %s FROM cscb_repair_manifest WHERE id = $1%s`, manifestColumns, suffix)
+ manifest, err := scanManifest(q.QueryRowContext(ctx, query, repairID))
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, xerrors.Errorf("CSCB repair manifest not found: id=%d", repairID)
+ }
+ return nil, xerrors.Errorf("failed to load CSCB repair manifest: %w", err)
+ }
+ if err := loadManifestBlocks(ctx, q, manifest); err != nil {
+ return nil, err
+ }
+ return manifest, nil
+}
+
+func scanManifest(row rowScanner) (*Manifest, error) {
+ var manifest Manifest
+ var state string
+ var oldVersionID, oldETag sql.NullString
+ var oldBytes sql.NullInt64
+ var newKey, newVersionID, newETag sql.NullString
+ var newBytes sql.NullInt64
+ var restoredAt, verifiedAt, completedAt sql.NullTime
+ if err := row.Scan(
+ &manifest.ID,
+ &manifest.Tag,
+ &state,
+ &manifest.Bucket,
+ &manifest.OldConsolidatedObjectKey,
+ &oldVersionID,
+ &oldETag,
+ &oldBytes,
+ &manifest.StartHeight,
+ &manifest.EndHeight,
+ &manifest.CanonicalBlockCount,
+ &manifest.TotalBlockCount,
+ &manifest.RowSetSHA256,
+ &newKey,
+ &newVersionID,
+ &newETag,
+ &newBytes,
+ &manifest.Outcome,
+ &manifest.PreparedAt,
+ &restoredAt,
+ &verifiedAt,
+ &completedAt,
+ ); err != nil {
+ return nil, err
+ }
+ manifest.State = State(state)
+ manifest.OldConsolidatedObjectVersion.VersionID = oldVersionID.String
+ manifest.OldConsolidatedObjectVersion.ETag = oldETag.String
+ if oldBytes.Valid && oldBytes.Int64 > 0 {
+ manifest.OldConsolidatedObjectVersion.Bytes = uint64(oldBytes.Int64)
+ }
+ manifest.NewConsolidatedObjectKey = newKey.String
+ manifest.NewConsolidatedObjectVersion.VersionID = newVersionID.String
+ manifest.NewConsolidatedObjectVersion.ETag = newETag.String
+ if newBytes.Valid && newBytes.Int64 > 0 {
+ manifest.NewConsolidatedObjectVersion.Bytes = uint64(newBytes.Int64)
+ }
+ manifest.RestoredAt = nullableTime(restoredAt)
+ manifest.VerifiedAt = nullableTime(verifiedAt)
+ manifest.CompletedAt = nullableTime(completedAt)
+ return &manifest, nil
+}
+
+func loadManifestBlocks(ctx context.Context, q queryer, manifest *Manifest) error {
+ const query = `
+ SELECT block_metadata_id, canonical, tag, height, COALESCE(hash, ''),
+ COALESCE(single_block_object_key_main, ''), single_block_object_key_sha256,
+ COALESCE(single_block_object_version_id, ''),
+ COALESCE(single_block_object_etag, ''), COALESCE(single_block_object_bytes, 0),
+ COALESCE(payload_sha256, ''),
+ old_byte_offset, old_byte_length, old_uncompressed_length
+ FROM cscb_repair_block
+ WHERE repair_id = $1
+ ORDER BY height ASC, block_metadata_id ASC`
+ rows, err := q.QueryContext(ctx, query, manifest.ID)
+ if err != nil {
+ return xerrors.Errorf("failed to query CSCB repair blocks: %w", err)
+ }
+ defer func() { _ = rows.Close() }()
+ manifest.Blocks = nil
+ for rows.Next() {
+ var block Block
+ block.OldConsolidatedObjectKey = manifest.OldConsolidatedObjectKey
+ if err := rows.Scan(
+ &block.BlockMetadataID,
+ &block.Canonical,
+ &block.Tag,
+ &block.Height,
+ &block.Hash,
+ &block.SingleBlockObjectKey,
+ &block.SingleBlockObjectKeySHA256,
+ &block.SingleBlockObjectVersion.VersionID,
+ &block.SingleBlockObjectVersion.ETag,
+ &block.SingleBlockObjectVersion.Bytes,
+ &block.PayloadSHA256,
+ &block.OldByteOffset,
+ &block.OldByteLength,
+ &block.OldUncompressedLength,
+ ); err != nil {
+ return xerrors.Errorf("failed to scan CSCB repair block: %w", err)
+ }
+ manifest.Blocks = append(manifest.Blocks, block)
+ }
+ if err := rows.Err(); err != nil {
+ return xerrors.Errorf("failed to iterate CSCB repair blocks: %w", err)
+ }
+ if manifest.ID != 0 && uint64(len(manifest.Blocks)) != manifest.TotalBlockCount {
+ return xerrors.Errorf(
+ "CSCB repair manifest block count mismatch: id=%d expected=%d actual=%d",
+ manifest.ID,
+ manifest.TotalBlockCount,
+ len(manifest.Blocks),
+ )
+ }
+ return nil
+}
+
+func loadRebuiltBlocks(ctx context.Context, q queryer, manifest *Manifest) error {
+ const query = `
+ SELECT
+ block.block_metadata_id,
+ EXISTS (
+ SELECT 1 FROM canonical_blocks cb
+ WHERE cb.block_metadata_id = bm.id AND cb.tag = bm.tag AND cb.height = bm.height
+ ) AS canonical,
+ COALESCE(bm.object_key_main, ''),
+ bm.object_format,
+ bm.byte_offset,
+ bm.byte_length,
+ bm.uncompressed_length,
+ COALESCE(shadow.consolidated_object_key_main, ''),
+ shadow.validated_at,
+ shadow.single_block_retention_started_at,
+ shadow.single_block_delete_after
+ FROM cscb_repair_block block
+ JOIN block_metadata bm ON bm.id = block.block_metadata_id
+ LEFT JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ WHERE block.repair_id = $1
+ ORDER BY block.height ASC, block.block_metadata_id ASC`
+ rows, err := q.QueryContext(ctx, query, manifest.ID)
+ if err != nil {
+ return xerrors.Errorf("failed to query rebuilt CSCB repair rows: %w", err)
+ }
+ defer func() { _ = rows.Close() }()
+ blocksByID := make(map[int64]*Block, len(manifest.Blocks))
+ for i := range manifest.Blocks {
+ blocksByID[manifest.Blocks[i].BlockMetadataID] = &manifest.Blocks[i]
+ }
+ seen := 0
+ for rows.Next() {
+ var id int64
+ var canonical bool
+ var activeOffset, activeLength, activeUncompressed sql.NullInt64
+ var validatedAt, retentionStartedAt, deleteAfter sql.NullTime
+ var activeKey, newKey string
+ var objectFormat int32
+ if err := rows.Scan(
+ &id,
+ &canonical,
+ &activeKey,
+ &objectFormat,
+ &activeOffset,
+ &activeLength,
+ &activeUncompressed,
+ &newKey,
+ &validatedAt,
+ &retentionStartedAt,
+ &deleteAfter,
+ ); err != nil {
+ return xerrors.Errorf("failed to scan rebuilt CSCB repair row: %w", err)
+ }
+ block, ok := blocksByID[id]
+ if !ok {
+ return xerrors.Errorf("rebuilt CSCB repair returned unexpected metadata_id=%d", id)
+ }
+ if canonical != block.Canonical {
+ return xerrors.Errorf("canonical identity changed during CSCB repair for metadata_id=%d", id)
+ }
+ block.ActiveObjectKey = activeKey
+ block.ActiveObjectFormat = objectFormat
+ block.NewConsolidatedObjectKey = newKey
+ block.NewByteOffset = nullableUint64(activeOffset)
+ block.NewByteLength = nullableUint64(activeLength)
+ block.NewUncompressedLength = nullableUint64(activeUncompressed)
+ block.NewValidatedAt = nullableTime(validatedAt)
+ block.NewRetentionStartedAt = nullableTime(retentionStartedAt)
+ block.NewSingleBlockDeleteAfter = nullableTime(deleteAfter)
+ seen++
+ }
+ if err := rows.Err(); err != nil {
+ return xerrors.Errorf("failed to iterate rebuilt CSCB repair rows: %w", err)
+ }
+ if uint64(seen) != manifest.TotalBlockCount {
+ return xerrors.Errorf("rebuilt CSCB repair row count mismatch: expected=%d actual=%d", manifest.TotalBlockCount, seen)
+ }
+ return nil
+}
+
+func lockManifestForRepair(ctx context.Context, tx *sql.Tx, repairID int64) (*Manifest, error) {
+ var tag uint32
+ if err := tx.QueryRowContext(ctx, `SELECT tag FROM cscb_repair_manifest WHERE id = $1`, repairID).Scan(&tag); err != nil {
+ if err == sql.ErrNoRows {
+ return nil, xerrors.Errorf("CSCB repair manifest not found: id=%d", repairID)
+ }
+ return nil, xerrors.Errorf("failed to load CSCB repair tag: %w", err)
+ }
+ if err := lockRepairTag(ctx, tx, tag); err != nil {
+ return nil, err
+ }
+ return loadManifest(ctx, tx, repairID, true)
+}
+
+func lockCandidateRows(ctx context.Context, tx *sql.Tx, blocks []Block) error {
+ ordered := append([]Block(nil), blocks...)
+ sort.Slice(ordered, func(i, j int) bool {
+ if ordered[i].Tag != ordered[j].Tag {
+ return ordered[i].Tag < ordered[j].Tag
+ }
+ if ordered[i].Height != ordered[j].Height {
+ return ordered[i].Height < ordered[j].Height
+ }
+ if ordered[i].Hash != ordered[j].Hash {
+ return ordered[i].Hash < ordered[j].Hash
+ }
+ return ordered[i].BlockMetadataID < ordered[j].BlockMetadataID
+ })
+ ids := make([]int64, 0, len(ordered))
+ for _, block := range ordered {
+ if err := retirementlock.Acquire(ctx, tx, block.Tag, block.Height, block.Hash); err != nil {
+ return err
+ }
+ ids = append(ids, block.BlockMetadataID)
+ }
+ rows, err := tx.QueryContext(ctx, `
+ SELECT bm.id
+ FROM block_metadata bm
+ JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ WHERE bm.id = ANY($1)
+ ORDER BY bm.id
+ FOR UPDATE OF bm, shadow`, pq.Array(ids))
+ if err != nil {
+ return xerrors.Errorf("failed to lock CSCB repair metadata rows: %w", err)
+ }
+ defer func() { _ = rows.Close() }()
+ count := 0
+ for rows.Next() {
+ count++
+ }
+ if err := rows.Err(); err != nil {
+ return xerrors.Errorf("failed to iterate locked CSCB repair rows: %w", err)
+ }
+ if count != len(ids) {
+ return xerrors.Errorf("CSCB repair lock count mismatch: expected=%d actual=%d", len(ids), count)
+ }
+ return nil
+}
+
+func lockConsolidatedObjectKey(ctx context.Context, tx *sql.Tx, objectKey string) error {
+ if objectKey == "" {
+ return xerrors.New("consolidated object key is required for CSCB repair lock")
+ }
+ if _, err := tx.ExecContext(
+ ctx,
+ `SELECT pg_advisory_xact_lock(hashtextextended($1, 1))`,
+ objectKey,
+ ); err != nil {
+ return xerrors.Errorf("failed to acquire CSCB repair object lock: %w", err)
+ }
+ return nil
+}
+
+func lockConsolidatedObjectKeys(ctx context.Context, tx *sql.Tx, objectKeys ...string) error {
+ unique := make(map[string]struct{}, len(objectKeys))
+ ordered := make([]string, 0, len(objectKeys))
+ for _, objectKey := range objectKeys {
+ if objectKey == "" {
+ continue
+ }
+ if _, ok := unique[objectKey]; ok {
+ continue
+ }
+ unique[objectKey] = struct{}{}
+ ordered = append(ordered, objectKey)
+ }
+ if len(ordered) == 0 {
+ return xerrors.New("at least one consolidated object key is required for CSCB repair locks")
+ }
+ sort.Strings(ordered)
+ for _, objectKey := range ordered {
+ if err := lockConsolidatedObjectKey(ctx, tx, objectKey); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func lockRepairTag(ctx context.Context, tx *sql.Tx, tag uint32) error {
+ return cscbrepairlock.AcquireTag(ctx, tx, tag)
+}
+
+func lockRepairRows(ctx context.Context, tx *sql.Tx, blocks []Block) error {
+ ordered := append([]Block(nil), blocks...)
+ sort.Slice(ordered, func(i, j int) bool {
+ if ordered[i].Tag != ordered[j].Tag {
+ return ordered[i].Tag < ordered[j].Tag
+ }
+ if ordered[i].Height != ordered[j].Height {
+ return ordered[i].Height < ordered[j].Height
+ }
+ if ordered[i].Hash != ordered[j].Hash {
+ return ordered[i].Hash < ordered[j].Hash
+ }
+ return ordered[i].BlockMetadataID < ordered[j].BlockMetadataID
+ })
+ ids := make([]int64, 0, len(ordered))
+ for _, block := range ordered {
+ if err := retirementlock.Acquire(ctx, tx, block.Tag, block.Height, block.Hash); err != nil {
+ return err
+ }
+ ids = append(ids, block.BlockMetadataID)
+ }
+ rows, err := tx.QueryContext(ctx, `
+ SELECT id FROM block_metadata
+ WHERE id = ANY($1)
+ ORDER BY id
+ FOR UPDATE`, pq.Array(ids))
+ if err != nil {
+ return xerrors.Errorf("failed to lock rebuilt block metadata: %w", err)
+ }
+ count := 0
+ for rows.Next() {
+ count++
+ }
+ if err := rows.Err(); err != nil {
+ _ = rows.Close()
+ return xerrors.Errorf("failed to iterate locked rebuilt block metadata: %w", err)
+ }
+ if err := rows.Close(); err != nil {
+ return xerrors.Errorf("failed to close rebuilt block metadata rows: %w", err)
+ }
+ if count != len(ids) {
+ return xerrors.Errorf("rebuilt metadata lock count mismatch: expected=%d actual=%d", len(ids), count)
+ }
+ shadowRows, err := tx.QueryContext(ctx, `
+ SELECT block_metadata_id FROM block_consolidation_shadow
+ WHERE block_metadata_id = ANY($1)
+ ORDER BY block_metadata_id
+ FOR UPDATE`, pq.Array(ids))
+ if err != nil {
+ return xerrors.Errorf("failed to lock rebuilt consolidation shadows: %w", err)
+ }
+ defer func() { _ = shadowRows.Close() }()
+ for shadowRows.Next() {
+ }
+ return shadowRows.Err()
+}
+
+func validateRepairPromotionRow(
+ ctx context.Context,
+ tx *sql.Tx,
+ manifest *Manifest,
+ block Block,
+ newObjectKey string,
+ placement RebuiltPlacement,
+) error {
+ const query = `
+ SELECT
+ EXISTS (
+ SELECT 1 FROM canonical_blocks cb
+ WHERE cb.block_metadata_id = bm.id AND cb.tag = bm.tag AND cb.height = bm.height
+ ),
+ COALESCE(bm.object_key_main, ''),
+ bm.object_format,
+ bm.byte_offset,
+ bm.byte_length,
+ bm.uncompressed_length,
+ bm.skipped,
+ (bm.single_block_retention_fenced_at IS NOT NULL),
+ EXISTS (
+ SELECT 1 FROM block_single_block_retention retirement
+ WHERE retirement.block_metadata_id = bm.id
+ ),
+ (shadow.block_metadata_id IS NOT NULL),
+ COALESCE(shadow.single_block_object_key_main, ''),
+ COALESCE(shadow.consolidated_object_key_main, ''),
+ shadow.object_format,
+ shadow.byte_offset,
+ shadow.byte_length,
+ shadow.uncompressed_length,
+ (shadow.single_block_object_deleted_at IS NOT NULL)
+ FROM block_metadata bm
+ LEFT JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ WHERE bm.id = $1
+ AND bm.tag = $2
+ AND bm.height = $3
+ AND bm.hash IS NOT DISTINCT FROM NULLIF($4, '')`
+ var canonical, skipped, fenced, retirementManifest, shadowExists, singleBlockDeleted bool
+ var activeKey, shadowSingleKey, shadowConsolidatedKey string
+ var activeFormat int32
+ var activeOffset, activeLength, activeUncompressed sql.NullInt64
+ var shadowFormat, shadowOffset, shadowLength, shadowUncompressed sql.NullInt64
+ if err := tx.QueryRowContext(
+ ctx,
+ query,
+ block.BlockMetadataID,
+ block.Tag,
+ block.Height,
+ block.Hash,
+ ).Scan(
+ &canonical,
+ &activeKey,
+ &activeFormat,
+ &activeOffset,
+ &activeLength,
+ &activeUncompressed,
+ &skipped,
+ &fenced,
+ &retirementManifest,
+ &shadowExists,
+ &shadowSingleKey,
+ &shadowConsolidatedKey,
+ &shadowFormat,
+ &shadowOffset,
+ &shadowLength,
+ &shadowUncompressed,
+ &singleBlockDeleted,
+ ); err != nil {
+ if err == sql.ErrNoRows {
+ return xerrors.Errorf("CSCB repair metadata identity changed for metadata_id=%d", block.BlockMetadataID)
+ }
+ return xerrors.Errorf("failed to validate CSCB repair promotion row metadata_id=%d: %w", block.BlockMetadataID, err)
+ }
+ if canonical != block.Canonical || skipped || fenced || retirementManifest || singleBlockDeleted {
+ return xerrors.Errorf("CSCB repair promotion row is blocked or changed for metadata_id=%d", block.BlockMetadataID)
+ }
+ oldLayout := activeKey == manifest.OldConsolidatedObjectKey &&
+ activeFormat == int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH) &&
+ nullableUint64(activeOffset) == block.OldByteOffset &&
+ nullableUint64(activeLength) == block.OldByteLength &&
+ nullableUint64(activeUncompressed) == block.OldUncompressedLength
+ singleLayout := activeKey == block.SingleBlockObjectKey &&
+ activeFormat == int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK) &&
+ !activeOffset.Valid && !activeLength.Valid && !activeUncompressed.Valid
+ newLayout := activeKey == newObjectKey &&
+ activeFormat == int32(placement.ObjectFormat) &&
+ nullableUint64(activeOffset) == placement.ByteOffset &&
+ nullableUint64(activeLength) == placement.ByteLength &&
+ nullableUint64(activeUncompressed) == placement.UncompressedLength
+ if !oldLayout && !singleLayout && !newLayout {
+ return xerrors.Errorf("CSCB repair active placement changed for metadata_id=%d", block.BlockMetadataID)
+ }
+ if !shadowExists {
+ if !singleLayout {
+ return xerrors.Errorf("CSCB repair shadow is missing for metadata_id=%d", block.BlockMetadataID)
+ }
+ return nil
+ }
+ if shadowSingleKey != block.SingleBlockObjectKey || !shadowFormat.Valid ||
+ shadowFormat.Int64 != int64(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH) {
+ return xerrors.Errorf("CSCB repair shadow identity changed for metadata_id=%d", block.BlockMetadataID)
+ }
+ oldShadow := shadowConsolidatedKey == manifest.OldConsolidatedObjectKey &&
+ nullableUint64(shadowOffset) == block.OldByteOffset &&
+ nullableUint64(shadowLength) == block.OldByteLength &&
+ nullableUint64(shadowUncompressed) == block.OldUncompressedLength
+ newShadow := shadowConsolidatedKey == newObjectKey &&
+ nullableUint64(shadowOffset) == placement.ByteOffset &&
+ nullableUint64(shadowLength) == placement.ByteLength &&
+ nullableUint64(shadowUncompressed) == placement.UncompressedLength
+ if !oldShadow && !newShadow {
+ return xerrors.Errorf("CSCB repair shadow placement changed for metadata_id=%d", block.BlockMetadataID)
+ }
+ return nil
+}
+
+func compareCandidateRows(current *Manifest, pinned *Manifest) error {
+ if current == nil || pinned == nil {
+ return xerrors.New("CSCB repair candidate is required")
+ }
+ if current.Tag != pinned.Tag ||
+ current.OldConsolidatedObjectKey != pinned.OldConsolidatedObjectKey ||
+ current.StartHeight != pinned.StartHeight ||
+ current.EndHeight != pinned.EndHeight ||
+ current.CanonicalBlockCount != pinned.CanonicalBlockCount ||
+ current.TotalBlockCount != pinned.TotalBlockCount ||
+ len(current.Blocks) != len(pinned.Blocks) {
+ return xerrors.Errorf("active CSCB repair candidate changed for old object %q", pinned.OldConsolidatedObjectKey)
+ }
+ for i := range pinned.Blocks {
+ a := current.Blocks[i]
+ b := pinned.Blocks[i]
+ if a.BlockMetadataID != b.BlockMetadataID ||
+ a.Canonical != b.Canonical ||
+ a.Tag != b.Tag ||
+ a.Height != b.Height ||
+ a.Hash != b.Hash ||
+ a.Skipped ||
+ a.RetirementFenced ||
+ a.RetirementManifestExists ||
+ a.SingleBlockObjectKey != b.SingleBlockObjectKey ||
+ a.SingleBlockObjectDeleted ||
+ a.OldConsolidatedObjectKey != b.OldConsolidatedObjectKey ||
+ a.OldByteOffset != b.OldByteOffset ||
+ a.OldByteLength != b.OldByteLength ||
+ a.OldUncompressedLength != b.OldUncompressedLength {
+ return xerrors.Errorf("active CSCB repair row changed for metadata_id=%d", b.BlockMetadataID)
+ }
+ }
+ return nil
+}
+
+func validateRebuiltMetadata(manifest *Manifest, objectKey string) error {
+ if manifest.RestoredAt == nil {
+ return xerrors.Errorf("CSCB repair restore timestamp is missing: id=%d", manifest.ID)
+ }
+ canonicalCount := uint64(0)
+ for _, block := range manifest.Blocks {
+ if block.Canonical {
+ canonicalCount++
+ }
+ if block.ActiveObjectKey != objectKey {
+ if block.Canonical {
+ return xerrors.Errorf("canonical row is not on the rebuilt CSCB: metadata_id=%d", block.BlockMetadataID)
+ }
+ if block.ActiveObjectKey != block.SingleBlockObjectKey ||
+ block.ActiveObjectFormat != int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK) ||
+ block.NewConsolidatedObjectKey != "" ||
+ block.NewByteOffset != 0 ||
+ block.NewByteLength != 0 ||
+ block.NewUncompressedLength != 0 ||
+ block.NewValidatedAt != nil ||
+ block.NewRetentionStartedAt != nil ||
+ block.NewSingleBlockDeleteAfter != nil {
+ return xerrors.Errorf("non-canonical row did not remain on single-block storage: metadata_id=%d", block.BlockMetadataID)
+ }
+ continue
+ }
+ if block.NewConsolidatedObjectKey != objectKey ||
+ block.ActiveObjectFormat != int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH) ||
+ block.NewByteLength == 0 ||
+ block.NewUncompressedLength == 0 ||
+ block.NewValidatedAt == nil ||
+ block.NewRetentionStartedAt == nil ||
+ block.NewSingleBlockDeleteAfter == nil ||
+ block.NewRetentionStartedAt.Before(*manifest.RestoredAt) ||
+ !block.NewSingleBlockDeleteAfter.After(*block.NewRetentionStartedAt) {
+ return xerrors.Errorf("canonical row is not on a freshly promoted CSCB: metadata_id=%d", block.BlockMetadataID)
+ }
+ if block.NewSingleBlockDeleteAfter.Before(block.NewRetentionStartedAt.Add(72 * time.Hour)) {
+ return xerrors.Errorf(
+ "canonical row does not have the required 72-hour single-block retention: metadata_id=%d",
+ block.BlockMetadataID,
+ )
+ }
+ }
+ if canonicalCount != manifest.CanonicalBlockCount {
+ return xerrors.Errorf("rebuilt canonical block count mismatch: expected=%d actual=%d", manifest.CanonicalBlockCount, canonicalCount)
+ }
+ if canonicalCount == 0 {
+ if objectKey != "" {
+ return xerrors.Errorf("zero-canonical CSCB repair unexpectedly produced object %q", objectKey)
+ }
+ return nil
+ }
+ if objectKey == "" {
+ return xerrors.New("rebuilt CSCB object key is missing")
+ }
+ if objectKey == manifest.OldConsolidatedObjectKey {
+ return xerrors.Errorf("rebuilt CSCB reused dirty object key %q", objectKey)
+ }
+ return nil
+}
+
+func requireNoUnexpectedOldReferences(ctx context.Context, q queryer, objectKey string, blocks []Block) error {
+ ids := make([]int64, 0, len(blocks))
+ for _, block := range blocks {
+ ids = append(ids, block.BlockMetadataID)
+ }
+ const query = `
+ SELECT
+ (SELECT COUNT(*)
+ FROM block_metadata
+ WHERE object_key_main = $1
+ AND object_key_main <> ''
+ AND NOT (id = ANY($2)))
+ +
+ (SELECT COUNT(*)
+ FROM block_consolidation_shadow
+ WHERE consolidated_object_key_main = $1
+ AND consolidated_object_key_main <> ''
+ AND NOT (block_metadata_id = ANY($2)))`
+ var references uint64
+ if err := q.QueryRowContext(ctx, query, objectKey, pq.Array(ids)).Scan(&references); err != nil {
+ return xerrors.Errorf("failed to count unexpected dirty CSCB references: %w", err)
+ }
+ if references != 0 {
+ return xerrors.Errorf("dirty CSCB %q has %d references outside the pinned row set", objectKey, references)
+ }
+ return nil
+}
+
+func requireExactNewReferences(ctx context.Context, q queryer, manifest *Manifest, objectKey string) error {
+ ids := make([]int64, 0, manifest.CanonicalBlockCount)
+ for _, block := range manifest.Blocks {
+ if block.Canonical {
+ ids = append(ids, block.BlockMetadataID)
+ }
+ }
+ if len(ids) == 0 || uint64(len(ids)) != manifest.CanonicalBlockCount {
+ return xerrors.Errorf(
+ "rebuilt CSCB expected canonical reference count mismatch: expected=%d actual=%d",
+ manifest.CanonicalBlockCount,
+ len(ids),
+ )
+ }
+ const query = `
+ SELECT
+ (SELECT COUNT(*)
+ FROM block_metadata
+ WHERE object_key_main = $1
+ AND object_key_main <> ''
+ AND NOT (id = ANY($2)))
+ +
+ (SELECT COUNT(*)
+ FROM block_consolidation_shadow
+ WHERE consolidated_object_key_main = $1
+ AND consolidated_object_key_main <> ''
+ AND NOT (block_metadata_id = ANY($2)))`
+ var references uint64
+ if err := q.QueryRowContext(ctx, query, objectKey, pq.Array(ids)).Scan(&references); err != nil {
+ return xerrors.Errorf("failed to count unexpected rebuilt CSCB references: %w", err)
+ }
+ if references != 0 {
+ return xerrors.Errorf("rebuilt CSCB %q has %d references outside the pinned canonical row set", objectKey, references)
+ }
+ return nil
+}
+
+func requireExactRepairedReferences(ctx context.Context, q queryer, manifest *Manifest, objectKey string) error {
+ ids := make([]int64, 0, len(manifest.Blocks))
+ for _, block := range manifest.Blocks {
+ ids = append(ids, block.BlockMetadataID)
+ }
+ if len(ids) == 0 || len(ids) != len(manifest.Blocks) {
+ return xerrors.Errorf(
+ "rebuilt CSCB expected reference count mismatch: expected=%d actual=%d",
+ len(manifest.Blocks),
+ len(ids),
+ )
+ }
+ const query = `
+ SELECT
+ (SELECT COUNT(*)
+ FROM block_metadata
+ WHERE object_key_main = $1
+ AND object_key_main <> ''
+ AND id = ANY($2))
+ +
+ (SELECT COUNT(*)
+ FROM block_consolidation_shadow
+ WHERE consolidated_object_key_main = $1
+ AND consolidated_object_key_main <> ''
+ AND block_metadata_id = ANY($2)),
+ (SELECT COUNT(*)
+ FROM block_metadata
+ WHERE object_key_main = $1
+ AND object_key_main <> ''
+ AND NOT (id = ANY($2)))
+ +
+ (SELECT COUNT(*)
+ FROM block_consolidation_shadow
+ WHERE consolidated_object_key_main = $1
+ AND consolidated_object_key_main <> ''
+ AND NOT (block_metadata_id = ANY($2)))`
+ var expectedReferences, unexpectedReferences uint64
+ if err := q.QueryRowContext(ctx, query, objectKey, pq.Array(ids)).Scan(&expectedReferences, &unexpectedReferences); err != nil {
+ return xerrors.Errorf("failed to count rebuilt CSCB references: %w", err)
+ }
+ want := uint64(len(ids) * 2)
+ if expectedReferences != want || unexpectedReferences != 0 {
+ return xerrors.Errorf(
+ "rebuilt CSCB %q reference mismatch: expected=%d actual=%d unexpected=%d",
+ objectKey,
+ want,
+ expectedReferences,
+ unexpectedReferences,
+ )
+ }
+ return nil
+}
+
+func allRepairRowsOnObject(manifest *Manifest, objectKey string) bool {
+ if manifest == nil || objectKey == "" || len(manifest.Blocks) == 0 {
+ return false
+ }
+ for _, block := range manifest.Blocks {
+ if block.ActiveObjectKey != objectKey || block.NewConsolidatedObjectKey != objectKey {
+ return false
+ }
+ }
+ return true
+}
+
+func requireNoOldReferences(ctx context.Context, q queryer, objectKey string) error {
+ const query = `
+ SELECT
+ (SELECT COUNT(*)
+ FROM block_metadata
+ WHERE object_key_main = $1 AND object_key_main <> '')
+ +
+ (SELECT COUNT(*)
+ FROM block_consolidation_shadow
+ WHERE consolidated_object_key_main = $1 AND consolidated_object_key_main <> '')`
+ var references uint64
+ if err := q.QueryRowContext(ctx, query, objectKey).Scan(&references); err != nil {
+ return xerrors.Errorf("failed to count old CSCB references: %w", err)
+ }
+ if references != 0 {
+ return xerrors.Errorf("old CSCB %q still has %d database references", objectKey, references)
+ }
+ return nil
+}
+
+func validateFencedInput(manifest *Manifest) error {
+ if manifest == nil || manifest.Tag == 0 || manifest.Bucket == "" ||
+ manifest.State != StatePreparing ||
+ manifest.OldConsolidatedObjectKey == "" ||
+ manifest.EndHeight <= manifest.StartHeight ||
+ manifest.TotalBlockCount != uint64(len(manifest.Blocks)) ||
+ len(manifest.RowSetSHA256) != 64 || manifest.RowSetSHA256 != rowSetSHA256(manifest) {
+ return xerrors.New("complete CSCB repair fence is required")
+ }
+ canonicalCount := uint64(0)
+ for _, block := range manifest.Blocks {
+ if block.BlockMetadataID == 0 || block.Tag != manifest.Tag || block.SingleBlockObjectKey == "" ||
+ len(block.SingleBlockObjectKeySHA256) != 64 ||
+ block.SingleBlockObjectKeySHA256 != objectKeySHA256(block.SingleBlockObjectKey) ||
+ block.OldConsolidatedObjectKey != manifest.OldConsolidatedObjectKey ||
+ block.OldByteLength == 0 || block.OldUncompressedLength == 0 {
+ return xerrors.Errorf("incomplete CSCB repair fence block metadata_id=%d", block.BlockMetadataID)
+ }
+ if block.Canonical {
+ canonicalCount++
+ }
+ }
+ if canonicalCount != manifest.CanonicalBlockCount {
+ return xerrors.Errorf("CSCB repair fence canonical count mismatch: expected=%d actual=%d", manifest.CanonicalBlockCount, canonicalCount)
+ }
+ return nil
+}
+
+func validateInspectionInput(oldObject ObjectVersion, blocks []Block) error {
+ if !immutableVersionID(oldObject.VersionID) || oldObject.ETag == "" || oldObject.Bytes == 0 || len(blocks) == 0 {
+ return xerrors.New("complete CSCB repair object inspection is required")
+ }
+ for _, block := range blocks {
+ if block.BlockMetadataID == 0 || block.SingleBlockObjectKey == "" ||
+ block.SingleBlockObjectKeySHA256 != objectKeySHA256(block.SingleBlockObjectKey) ||
+ !immutableVersionID(block.SingleBlockObjectVersion.VersionID) ||
+ block.SingleBlockObjectVersion.ETag == "" || block.SingleBlockObjectVersion.Bytes == 0 ||
+ len(block.PayloadSHA256) != 64 {
+ return xerrors.Errorf("incomplete CSCB repair inspection block metadata_id=%d", block.BlockMetadataID)
+ }
+ }
+ return nil
+}
+
+func sameFencedManifest(actual *Manifest, expected *Manifest) bool {
+ if actual == nil || expected == nil ||
+ actual.Tag != expected.Tag ||
+ actual.State != StatePreparing ||
+ actual.Bucket != expected.Bucket ||
+ actual.OldConsolidatedObjectKey != expected.OldConsolidatedObjectKey ||
+ actual.StartHeight != expected.StartHeight ||
+ actual.EndHeight != expected.EndHeight ||
+ actual.CanonicalBlockCount != expected.CanonicalBlockCount ||
+ actual.TotalBlockCount != expected.TotalBlockCount ||
+ actual.RowSetSHA256 != expected.RowSetSHA256 ||
+ len(actual.Blocks) != len(expected.Blocks) {
+ return false
+ }
+ for i := range actual.Blocks {
+ a := actual.Blocks[i]
+ b := expected.Blocks[i]
+ if a.BlockMetadataID != b.BlockMetadataID || a.Canonical != b.Canonical ||
+ a.Tag != b.Tag || a.Height != b.Height || a.Hash != b.Hash ||
+ a.SingleBlockObjectKey != b.SingleBlockObjectKey ||
+ a.SingleBlockObjectKeySHA256 != b.SingleBlockObjectKeySHA256 ||
+ a.OldByteOffset != b.OldByteOffset || a.OldByteLength != b.OldByteLength ||
+ a.OldUncompressedLength != b.OldUncompressedLength {
+ return false
+ }
+ }
+ return true
+}
+
+func sameInspection(actual *Manifest, oldObject ObjectVersion, blocks []Block, alreadyClean bool) bool {
+ expectedState := StatePrepared
+ expectedOutcome := ""
+ if alreadyClean {
+ expectedState = StateCompleted
+ expectedOutcome = alreadyCleanOutcome
+ }
+ if actual == nil || actual.State != expectedState || actual.Outcome != expectedOutcome ||
+ actual.OldConsolidatedObjectVersion != oldObject || len(actual.Blocks) != len(blocks) {
+ return false
+ }
+ for i := range blocks {
+ a := actual.Blocks[i]
+ b := blocks[i]
+ if a.BlockMetadataID != b.BlockMetadataID ||
+ a.SingleBlockObjectKey != b.SingleBlockObjectKey ||
+ a.SingleBlockObjectKeySHA256 != b.SingleBlockObjectKeySHA256 ||
+ a.SingleBlockObjectVersion != b.SingleBlockObjectVersion ||
+ a.PayloadSHA256 != b.PayloadSHA256 {
+ return false
+ }
+ }
+ return true
+}
+
+func requireRowsAffected(result sql.Result, expected uint64, action string) error {
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return xerrors.Errorf("failed to inspect %s result: %w", action, err)
+ }
+ if uint64(rows) != expected {
+ return xerrors.Errorf("%s guard failed: expected=%d actual=%d", action, expected, rows)
+ }
+ return nil
+}
+
+func nullableUint64(value sql.NullInt64) uint64 {
+ if !value.Valid || value.Int64 < 0 {
+ return 0
+ }
+ return uint64(value.Int64)
+}
+
+func nullableTime(value sql.NullTime) *time.Time {
+ if !value.Valid {
+ return nil
+ }
+ result := value.Time
+ return &result
+}
diff --git a/internal/storage/cscbrepair/repairer.go b/internal/storage/cscbrepair/repairer.go
new file mode 100644
index 00000000..756d64a2
--- /dev/null
+++ b/internal/storage/cscbrepair/repairer.go
@@ -0,0 +1,924 @@
+package cscbrepair
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "fmt"
+ "strings"
+ "time"
+
+ "golang.org/x/xerrors"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+ storageutils "github.com/coinbase/chainstorage/internal/storage/utils"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+const (
+ completedOutcome = "old_consolidated_object_retained_unreferenced"
+ alreadyCleanOutcome = OutcomeAlreadyCleanStorageNeutral
+)
+
+type repairerImpl struct {
+ repository Repository
+ store retirement.ObjectStore
+ bucket string
+}
+
+func NewRepairer(repository Repository, store retirement.ObjectStore, bucket string) Repairer {
+ return &repairerImpl{
+ repository: repository,
+ store: store,
+ bucket: bucket,
+ }
+}
+
+func (r *repairerImpl) ListCandidates(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ limit int,
+) ([]string, error) {
+ if err := r.validate(); err != nil {
+ return nil, err
+ }
+ if limit <= 0 {
+ return nil, xerrors.New("CSCB repair candidate limit must be positive")
+ }
+ return r.repository.ListCandidateObjectKeys(ctx, tag, startHeight, endHeight, limit)
+}
+
+func (r *repairerImpl) PrepareNext(
+ ctx context.Context,
+ executionKey string,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ maxBlocks uint64,
+ progress Progress,
+) (*Manifest, error) {
+ return r.prepare(ctx, executionKey, tag, startHeight, endHeight, maxBlocks, "", progress)
+}
+
+func (r *repairerImpl) PrepareObject(
+ ctx context.Context,
+ executionKey string,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ maxBlocks uint64,
+ objectKey string,
+ progress Progress,
+) (*Manifest, error) {
+ if objectKey == "" {
+ return nil, xerrors.New("CSCB repair object key is required")
+ }
+ return r.prepare(ctx, executionKey, tag, startHeight, endHeight, maxBlocks, objectKey, progress)
+}
+
+func (r *repairerImpl) prepare(
+ ctx context.Context,
+ executionKey string,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ maxBlocks uint64,
+ objectKey string,
+ progress Progress,
+) (*Manifest, error) {
+ if err := r.validate(); err != nil {
+ return nil, err
+ }
+ if !validExecutionKey(executionKey) {
+ return nil, xerrors.New("valid CSCB repair execution key is required")
+ }
+ bound, found, err := r.repository.FindByExecutionKey(ctx, executionKey)
+ if err != nil {
+ return nil, err
+ }
+ if found {
+ if bound == nil {
+ return nil, nil
+ }
+ if bound.Tag != tag {
+ return nil, xerrors.Errorf("bound CSCB repair tag changed: expected=%d actual=%d", tag, bound.Tag)
+ }
+ if objectKey != "" && bound.OldConsolidatedObjectKey != objectKey {
+ return nil, xerrors.Errorf(
+ "bound CSCB repair object changed: expected=%q actual=%q",
+ objectKey,
+ bound.OldConsolidatedObjectKey,
+ )
+ }
+ if err := validateApprovedManifest(bound, startHeight, endHeight, maxBlocks); err != nil {
+ return nil, xerrors.Errorf("bound CSCB repair does not fit the approved request: %w", err)
+ }
+ return r.inspectFencedCandidate(ctx, bound, progress)
+ }
+ var pending *Manifest
+ if objectKey == "" {
+ pending, err = r.repository.FindPending(ctx, tag, startHeight, endHeight)
+ } else {
+ pending, err = r.repository.FindByObjectKey(ctx, tag, objectKey)
+ }
+ if err != nil {
+ return nil, err
+ }
+ if pending != nil {
+ if err := validateApprovedManifest(pending, startHeight, endHeight, maxBlocks); err != nil {
+ return nil, xerrors.Errorf("pending CSCB repair does not fit the approved request: %w", err)
+ }
+ bound, err := r.repository.BindExecutionKey(ctx, executionKey, pending.ID)
+ if err != nil {
+ return nil, err
+ }
+ return r.inspectFencedCandidate(ctx, bound, progress)
+ }
+ var candidate *Manifest
+ if objectKey == "" {
+ candidate, err = r.repository.FindNextCandidate(ctx, tag, startHeight, endHeight)
+ } else {
+ candidate, err = r.repository.FindCandidateByObjectKey(ctx, tag, objectKey)
+ }
+ if err != nil {
+ return nil, err
+ }
+ if candidate == nil {
+ if objectKey != "" {
+ return nil, xerrors.Errorf("CSCB repair candidate is no longer available: object=%q", objectKey)
+ }
+ return r.repository.BindNoCandidateExecution(ctx, executionKey, tag, startHeight, endHeight)
+ }
+ if err := validateCandidate(candidate, startHeight, endHeight, maxBlocks); err != nil {
+ return nil, err
+ }
+ candidate.State = StatePreparing
+ candidate.Bucket = r.bucket
+ for i := range candidate.Blocks {
+ candidate.Blocks[i].SingleBlockObjectKeySHA256 = objectKeySHA256(candidate.Blocks[i].SingleBlockObjectKey)
+ }
+ candidate.RowSetSHA256 = rowSetSHA256(candidate)
+ fenced, err := r.repository.FenceCandidate(ctx, candidate)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to persist CSCB repair fence: %w", err)
+ }
+ bound, err = r.repository.BindExecutionKey(ctx, executionKey, fenced.ID)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to bind CSCB repair execution: %w", err)
+ }
+ return r.inspectFencedCandidate(ctx, bound, progress)
+}
+
+func (r *repairerImpl) inspectFencedCandidate(
+ ctx context.Context,
+ manifest *Manifest,
+ progress Progress,
+) (*Manifest, error) {
+ if manifest == nil || manifest.State != StatePreparing {
+ return manifest, nil
+ }
+ oldVersion, err := r.inspectExactObjectVersion(ctx, manifest.OldConsolidatedObjectKey)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to pin CSCB repair source object: %w", err)
+ }
+ blocks := append([]Block(nil), manifest.Blocks...)
+ verifier := retirement.NewPinnedPayloadVerifier(r.store)
+ hasStoragePlacement := false
+ for i := range blocks {
+ block := &blocks[i]
+ if objectKeySHA256(block.SingleBlockObjectKey) != block.SingleBlockObjectKeySHA256 {
+ return nil, xerrors.Errorf("single-block object key audit digest changed at height %d", block.Height)
+ }
+ singleBlockCandidate := retirement.Candidate{
+ Bucket: manifest.Bucket,
+ Key: block.SingleBlockObjectKey,
+ Height: block.Height,
+ Hash: block.Hash,
+ Tag: block.Tag,
+ }
+ var singleVersion ObjectVersion
+ if manifest.CanonicalBlockCount == 0 {
+ // A zero-canonical repair leaves active metadata on an unversioned
+ // single-block key. It is only safe when no historical version can
+ // become current later.
+ singleVersion, err = r.inspectExactObjectVersion(ctx, block.SingleBlockObjectKey)
+ } else {
+ singleVersion, err = r.inspectSingleBlockObjectVersion(ctx, singleBlockCandidate)
+ }
+ if err != nil {
+ return nil, xerrors.Errorf("failed to pin single-block object at height %d: %w", block.Height, err)
+ }
+ block.SingleBlockObjectVersion = singleVersion
+ payload := makePayloadCandidate(manifest, block, manifest.OldConsolidatedObjectKey, oldVersion)
+ singleInspection, err := verifier.InspectSingleBlock(ctx, payload)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to verify pinned single-block payload at height %d: %w", block.Height, err)
+ }
+ oldInspection, err := verifier.InspectConsolidated(ctx, payload)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to verify source CSCB payload at height %d: %w", block.Height, err)
+ }
+ hasStoragePlacement = hasStoragePlacement || oldInspection.HasStoragePlacement
+ // The pinned current single-block version is the authoritative repair
+ // input. The old CSCB is decoded to verify its identity and classify its
+ // placement state, but its provider-specific payload rendering is not
+ // copied into the rebuilt object.
+ block.PayloadSHA256 = singleInspection.CanonicalSHA256
+ reportProgress(progress, "prepare_payload_verified", i+1, len(blocks), block.Height)
+ }
+ inspected, err := r.repository.RecordInspection(ctx, manifest.ID, oldVersion, blocks, !hasStoragePlacement)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to persist CSCB repair inspection: %w", err)
+ }
+ stage := "prepared"
+ if inspected.State == StateCompleted && inspected.Outcome == alreadyCleanOutcome {
+ stage = "already_clean_storage_neutral"
+ }
+ reportProgress(progress, stage, len(inspected.Blocks), len(inspected.Blocks), inspected.EndHeight-1)
+ return inspected, nil
+}
+
+func (r *repairerImpl) Restore(ctx context.Context, repairID int64, progress Progress) (*Manifest, error) {
+ if err := r.validate(); err != nil {
+ return nil, err
+ }
+ manifest, err := r.repository.Get(ctx, repairID)
+ if err != nil {
+ return nil, err
+ }
+ if manifest.State != StatePrepared {
+ return manifest, nil
+ }
+ if err := r.requirePinnedObjectVersion(ctx, manifest.OldConsolidatedObjectKey, manifest.OldConsolidatedObjectVersion); err != nil {
+ return nil, xerrors.Errorf("CSCB repair source changed before restore: %w", err)
+ }
+ for i := range manifest.Blocks {
+ block := &manifest.Blocks[i]
+ if err := r.requirePinnedSingleBlockObjectVersionForManifest(ctx, manifest, block); err != nil {
+ return nil, xerrors.Errorf("single-block object changed before restore at height %d: %w", block.Height, err)
+ }
+ reportProgress(progress, "restore_object_revalidated", i+1, len(manifest.Blocks), block.Height)
+ }
+ restored, err := r.repository.RestoreToSingleBlock(ctx, repairID)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to restore single-block metadata: %w", err)
+ }
+ reportProgress(progress, "restored", len(restored.Blocks), len(restored.Blocks), restored.EndHeight-1)
+ return restored, nil
+}
+
+func (r *repairerImpl) Get(ctx context.Context, repairID int64) (*Manifest, error) {
+ if err := r.validate(); err != nil {
+ return nil, err
+ }
+ return r.repository.Get(ctx, repairID)
+}
+
+func (r *repairerImpl) VisitPinnedPayloads(
+ ctx context.Context,
+ repairID int64,
+ progress Progress,
+ visit func(PinnedPayload) error,
+) error {
+ if err := r.validate(); err != nil {
+ return err
+ }
+ if visit == nil {
+ return xerrors.New("pinned payload visitor is required")
+ }
+ manifest, err := r.repository.Get(ctx, repairID)
+ if err != nil {
+ return err
+ }
+ if manifest.State != StatePrepared && manifest.State != StateRestored {
+ return xerrors.Errorf(
+ "CSCB repair must be prepared or restored before loading pinned payloads: id=%d state=%s",
+ manifest.ID,
+ manifest.State,
+ )
+ }
+ if manifest.CanonicalBlockCount == 0 {
+ return xerrors.Errorf("zero-canonical CSCB repair does not produce a rebuilt object: id=%d", manifest.ID)
+ }
+ if err := r.requirePinnedObjectVersion(
+ ctx,
+ manifest.OldConsolidatedObjectKey,
+ manifest.OldConsolidatedObjectVersion,
+ ); err != nil {
+ return xerrors.Errorf("CSCB repair source changed before rebuild: %w", err)
+ }
+
+ verifier := retirement.NewPinnedPayloadVerifier(r.store)
+ for i := range manifest.Blocks {
+ block := &manifest.Blocks[i]
+ if err := r.requirePinnedSingleBlockObjectVersion(
+ ctx,
+ block.SingleBlockObjectKey,
+ block.SingleBlockObjectVersion,
+ ); err != nil {
+ return xerrors.Errorf("single-block object changed before rebuild at height %d: %w", block.Height, err)
+ }
+ candidate := makePayloadCandidate(
+ manifest,
+ block,
+ manifest.OldConsolidatedObjectKey,
+ manifest.OldConsolidatedObjectVersion,
+ )
+ pinnedBlock, inspection, err := verifier.ReadSingleBlock(ctx, candidate)
+ if err != nil {
+ return xerrors.Errorf("failed to read pinned single-block payload at height %d: %w", block.Height, err)
+ }
+ if inspection.CanonicalSHA256 != block.PayloadSHA256 {
+ return xerrors.Errorf("single-block payload digest changed before rebuild at height %d", block.Height)
+ }
+ normalized := storageutils.CloneBlockWithoutStoragePlacement(pinnedBlock)
+ raw, err := (proto.MarshalOptions{Deterministic: true}).Marshal(normalized)
+ if err != nil {
+ return xerrors.Errorf("failed to marshal pinned block at height %d: %w", block.Height, err)
+ }
+ metadata, ok := proto.Clone(normalized.GetMetadata()).(*api.BlockMetadata)
+ if !ok || metadata == nil {
+ return xerrors.Errorf("pinned block metadata is missing at height %d", block.Height)
+ }
+ if err := visit(PinnedPayload{
+ BlockMetadataID: block.BlockMetadataID,
+ Metadata: metadata,
+ RawBlockPayload: raw,
+ }); err != nil {
+ return xerrors.Errorf("failed to stage pinned payload at height %d: %w", block.Height, err)
+ }
+ reportProgress(progress, "pinned_payload_loaded", i+1, len(manifest.Blocks), block.Height)
+ }
+ return nil
+}
+
+func (r *repairerImpl) VerifyAndPromote(
+ ctx context.Context,
+ repairID int64,
+ objectKey string,
+ placements []RebuiltPlacement,
+ singleBlockObjectRetention time.Duration,
+ progress Progress,
+) (*Manifest, error) {
+ if err := r.validate(); err != nil {
+ return nil, err
+ }
+ if singleBlockObjectRetention <= 0 {
+ return nil, xerrors.New("positive single-block object retention is required")
+ }
+ manifest, err := r.repository.Get(ctx, repairID)
+ if err != nil {
+ return nil, err
+ }
+ if manifest.State == StateVerified || manifest.State == StateCompleted {
+ return manifest, nil
+ }
+ if manifest.State != StatePrepared && manifest.State != StateRestored {
+ return nil, xerrors.Errorf(
+ "CSCB repair must be prepared or restored before promotion: id=%d state=%s",
+ manifest.ID,
+ manifest.State,
+ )
+ }
+ if manifest.CanonicalBlockCount == 0 {
+ return nil, xerrors.Errorf("zero-canonical CSCB repair cannot promote a rebuilt object: id=%d", manifest.ID)
+ }
+ if objectKey == "" || objectKey == manifest.OldConsolidatedObjectKey {
+ return nil, xerrors.Errorf("rebuilt CSCB object key is invalid: %q", objectKey)
+ }
+ newVersion, err := r.inspectExactObjectVersion(ctx, objectKey)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to pin rebuilt CSCB object: %w", err)
+ }
+ placementsByID, err := validateRebuiltPlacements(manifest, objectKey, placements)
+ if err != nil {
+ return nil, err
+ }
+
+ verifier := retirement.NewPinnedPayloadVerifier(r.store)
+ for i := range manifest.Blocks {
+ block := &manifest.Blocks[i]
+ if err := r.requirePinnedSingleBlockObjectVersion(
+ ctx,
+ block.SingleBlockObjectKey,
+ block.SingleBlockObjectVersion,
+ ); err != nil {
+ return nil, xerrors.Errorf("single-block object changed during rebuild at height %d: %w", block.Height, err)
+ }
+ placement := placementsByID[block.BlockMetadataID]
+ candidate := retirement.Candidate{
+ Bucket: manifest.Bucket,
+ Key: block.SingleBlockObjectKey,
+ VersionID: block.SingleBlockObjectVersion.VersionID,
+ Height: block.Height,
+ Hash: block.Hash,
+ ConsolidatedKey: objectKey,
+ Tag: block.Tag,
+ CSCBVersionID: newVersion.VersionID,
+ ByteOffset: placement.ByteOffset,
+ ByteLength: placement.ByteLength,
+ UncompressedLength: placement.UncompressedLength,
+ }
+ singleInspection, err := verifier.InspectSingleBlock(ctx, candidate)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to re-read pinned single-block payload at height %d: %w", block.Height, err)
+ }
+ if singleInspection.CanonicalSHA256 != block.PayloadSHA256 {
+ return nil, xerrors.Errorf("single-block payload digest changed during rebuild at height %d", block.Height)
+ }
+ cleanInspection, err := verifier.InspectConsolidated(ctx, candidate)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to verify rebuilt CSCB payload at height %d: %w", block.Height, err)
+ }
+ if cleanInspection.HasStoragePlacement || cleanInspection.CanonicalSHA256 != block.PayloadSHA256 {
+ return nil, xerrors.Errorf("rebuilt CSCB payload is not storage-neutral and identical at height %d", block.Height)
+ }
+ reportProgress(progress, "rebuilt_payload_verified", i+1, len(manifest.Blocks), block.Height)
+ }
+ if err := r.requirePinnedObjectVersion(
+ ctx,
+ manifest.OldConsolidatedObjectKey,
+ manifest.OldConsolidatedObjectVersion,
+ ); err != nil {
+ return nil, xerrors.Errorf("CSCB repair source changed before atomic promotion: %w", err)
+ }
+ if err := r.requirePinnedObjectVersion(ctx, objectKey, newVersion); err != nil {
+ return nil, xerrors.Errorf("rebuilt CSCB changed before atomic promotion: %w", err)
+ }
+ completed, err := r.repository.PromoteCompleted(
+ ctx,
+ manifest.ID,
+ objectKey,
+ newVersion,
+ placements,
+ singleBlockObjectRetention,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to atomically promote and complete CSCB repair: %w", err)
+ }
+ reportProgress(progress, "promoted_completed", len(completed.Blocks), len(completed.Blocks), completed.EndHeight-1)
+ return completed, nil
+}
+
+func (r *repairerImpl) VerifyRebuilt(ctx context.Context, repairID int64, progress Progress) (*Manifest, error) {
+ if err := r.validate(); err != nil {
+ return nil, err
+ }
+ manifest, err := r.repository.GetRebuilt(ctx, repairID)
+ if err != nil {
+ return nil, err
+ }
+ if manifest.State == StateVerified || manifest.State == StateCompleted {
+ return manifest, nil
+ }
+ if manifest.State != StateRestored {
+ return nil, xerrors.Errorf("CSCB repair must be restored before payload verification: id=%d state=%s", manifest.ID, manifest.State)
+ }
+ newObjectKey, err := rebuiltObjectKey(manifest)
+ if err != nil {
+ return nil, err
+ }
+ newVersion, err := r.inspectExactObjectVersion(ctx, newObjectKey)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to pin rebuilt CSCB object: %w", err)
+ }
+ verifier := retirement.NewPinnedPayloadVerifier(r.store)
+ verifiedCanonical := 0
+ for i := range manifest.Blocks {
+ block := &manifest.Blocks[i]
+ if err := r.requirePinnedSingleBlockObjectVersionForManifest(ctx, manifest, block); err != nil {
+ return nil, xerrors.Errorf("single-block object changed during repair at height %d: %w", block.Height, err)
+ }
+ payload := makePayloadCandidate(manifest, block, newObjectKey, newVersion)
+ singleInspection, err := verifier.InspectSingleBlock(ctx, payload)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to re-read pinned single-block payload at height %d: %w", block.Height, err)
+ }
+ if singleInspection.CanonicalSHA256 != block.PayloadSHA256 {
+ return nil, xerrors.Errorf("single-block payload digest changed during repair at height %d", block.Height)
+ }
+ if block.Canonical {
+ cleanInspection, err := verifier.InspectConsolidated(ctx, payload)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to verify rebuilt CSCB payload at height %d: %w", block.Height, err)
+ }
+ if cleanInspection.HasStoragePlacement {
+ return nil, xerrors.Errorf("rebuilt CSCB retains storage placement at height %d", block.Height)
+ }
+ if cleanInspection.CanonicalSHA256 != block.PayloadSHA256 {
+ return nil, xerrors.Errorf("rebuilt CSCB payload differs from pinned single-block payload at height %d", block.Height)
+ }
+ verifiedCanonical++
+ }
+ reportProgress(progress, "rebuilt_payload_verified", i+1, len(manifest.Blocks), block.Height)
+ }
+ if uint64(verifiedCanonical) != manifest.CanonicalBlockCount {
+ return nil, xerrors.Errorf(
+ "rebuilt CSCB canonical verification count mismatch: expected=%d actual=%d",
+ manifest.CanonicalBlockCount,
+ verifiedCanonical,
+ )
+ }
+ verified, err := r.repository.RecordVerified(ctx, manifest.ID, newObjectKey, newVersion)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to record verified CSCB repair: %w", err)
+ }
+ reportProgress(progress, "verified", len(manifest.Blocks), len(manifest.Blocks), manifest.EndHeight-1)
+ return verified, nil
+}
+
+func (r *repairerImpl) Complete(ctx context.Context, repairID int64, progress Progress) (*Manifest, error) {
+ if err := r.validate(); err != nil {
+ return nil, err
+ }
+ manifest, err := r.repository.GetRebuilt(ctx, repairID)
+ if err != nil {
+ return nil, err
+ }
+ if manifest.State == StateCompleted {
+ return manifest, nil
+ }
+ if manifest.CanonicalBlockCount == 0 {
+ if manifest.State != StateRestored {
+ return nil, xerrors.Errorf("zero-canonical CSCB repair must be restored before completion: id=%d state=%s", manifest.ID, manifest.State)
+ }
+ } else if manifest.State != StateVerified {
+ return nil, xerrors.Errorf("CSCB repair must be verified before completion: id=%d state=%s", manifest.ID, manifest.State)
+ }
+ if err := r.requirePinnedObjectVersion(ctx, manifest.OldConsolidatedObjectKey, manifest.OldConsolidatedObjectVersion); err != nil {
+ return nil, xerrors.Errorf("retained CSCB repair source changed before completion: %w", err)
+ }
+ if manifest.CanonicalBlockCount > 0 {
+ if err := r.requirePinnedObjectVersion(
+ ctx,
+ manifest.NewConsolidatedObjectKey,
+ manifest.NewConsolidatedObjectVersion,
+ ); err != nil {
+ return nil, xerrors.Errorf("rebuilt CSCB changed before repair completion: %w", err)
+ }
+ }
+
+ verifier := retirement.NewPinnedPayloadVerifier(r.store)
+ for i := range manifest.Blocks {
+ block := &manifest.Blocks[i]
+ if err := r.requirePinnedSingleBlockObjectVersionForManifest(ctx, manifest, block); err != nil {
+ return nil, xerrors.Errorf("single-block object changed before completion at height %d: %w", block.Height, err)
+ }
+ payload := makePayloadCandidate(manifest, block, manifest.NewConsolidatedObjectKey, manifest.NewConsolidatedObjectVersion)
+ singleInspection, err := verifier.InspectSingleBlock(ctx, payload)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to verify retained single-block payload at height %d: %w", block.Height, err)
+ }
+ if singleInspection.CanonicalSHA256 != block.PayloadSHA256 {
+ return nil, xerrors.Errorf("single-block payload digest changed before completion at height %d", block.Height)
+ }
+ if manifest.NewConsolidatedObjectKey != "" && block.ActiveObjectKey == manifest.NewConsolidatedObjectKey {
+ cleanInspection, err := verifier.InspectConsolidated(ctx, payload)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to verify rebuilt CSCB payload before completion at height %d: %w", block.Height, err)
+ }
+ if cleanInspection.HasStoragePlacement || cleanInspection.CanonicalSHA256 != block.PayloadSHA256 {
+ return nil, xerrors.Errorf("rebuilt CSCB payload is not storage-neutral and identical at height %d", block.Height)
+ }
+ }
+ reportProgress(progress, "completion_payload_verified", i+1, len(manifest.Blocks), block.Height)
+ }
+ completed, err := r.repository.CompleteRetainingOldObject(ctx, manifest.ID, completedOutcome)
+ if err != nil {
+ return nil, err
+ }
+ reportProgress(progress, "completed", len(completed.Blocks), len(completed.Blocks), completed.EndHeight-1)
+ return completed, nil
+}
+
+func (r *repairerImpl) validate() error {
+ if r == nil || r.repository == nil || r.store == nil || r.bucket == "" {
+ return xerrors.New("CSCB repair requires a repository, object store, and bucket")
+ }
+ return nil
+}
+
+func validateCandidate(manifest *Manifest, startHeight uint64, endHeight uint64, maxBlocks uint64) error {
+ if err := validateApprovedManifest(manifest, startHeight, endHeight, maxBlocks); err != nil {
+ return err
+ }
+ for _, block := range manifest.Blocks {
+ if block.Skipped || block.RetirementFenced || block.RetirementManifestExists ||
+ block.SingleBlockObjectKey == "" || block.SingleBlockObjectDeleted ||
+ block.OldConsolidatedObjectKey != manifest.OldConsolidatedObjectKey ||
+ block.OldByteLength == 0 || block.OldUncompressedLength == 0 {
+ return xerrors.Errorf("CSCB repair candidate is blocked or incomplete at metadata_id=%d height=%d", block.BlockMetadataID, block.Height)
+ }
+ }
+ return nil
+}
+
+func validateApprovedManifest(manifest *Manifest, startHeight uint64, endHeight uint64, maxBlocks uint64) error {
+ if manifest == nil || manifest.OldConsolidatedObjectKey == "" || len(manifest.Blocks) == 0 {
+ return xerrors.New("active CSCB repair manifest is required")
+ }
+ if maxBlocks == 0 || manifest.TotalBlockCount > maxBlocks || manifest.CanonicalBlockCount > maxBlocks {
+ return xerrors.Errorf(
+ "CSCB repair manifest exceeds one consolidation object: total=%d canonical=%d max_blocks=%d",
+ manifest.TotalBlockCount,
+ manifest.CanonicalBlockCount,
+ maxBlocks,
+ )
+ }
+ if manifest.TotalBlockCount == 0 || manifest.TotalBlockCount != uint64(len(manifest.Blocks)) {
+ return xerrors.New("CSCB repair manifest must contain a complete non-empty row set")
+ }
+ if manifest.StartHeight < startHeight || manifest.EndHeight > endHeight {
+ return xerrors.Errorf(
+ "CSCB repair object range [%d, %d) exceeds approved range [%d, %d)",
+ manifest.StartHeight,
+ manifest.EndHeight,
+ startHeight,
+ endHeight,
+ )
+ }
+ return nil
+}
+
+func validExecutionKey(value string) bool {
+ if len(value) != 64 {
+ return false
+ }
+ for _, ch := range value {
+ if (ch < '0' || ch > '9') && (ch < 'a' || ch > 'f') {
+ return false
+ }
+ }
+ return true
+}
+
+func (r *repairerImpl) inspectExactObjectVersion(ctx context.Context, key string) (ObjectVersion, error) {
+ topology, err := r.store.ListObjectVersions(ctx, r.bucket, key)
+ if err != nil {
+ return ObjectVersion{}, err
+ }
+ if len(topology.Versions) != 1 || len(topology.DeleteMarkers) != 0 {
+ return ObjectVersion{}, xerrors.Errorf(
+ "object must have exactly one data version and zero delete markers: key=%q versions=%d delete_markers=%d",
+ key,
+ len(topology.Versions),
+ len(topology.DeleteMarkers),
+ )
+ }
+ version := topology.Versions[0]
+ result := ObjectVersion{VersionID: version.VersionID, ETag: version.ETag, Bytes: version.Bytes}
+ if !version.IsLatest || !immutableVersionID(result.VersionID) || result.ETag == "" || result.Bytes == 0 {
+ return ObjectVersion{}, xerrors.Errorf("object current version is not immutable and complete: key=%q", key)
+ }
+ return result, nil
+}
+
+func (r *repairerImpl) inspectSingleBlockObjectVersion(
+ ctx context.Context,
+ candidate retirement.Candidate,
+) (ObjectVersion, error) {
+ topology, err := r.store.ListObjectVersions(ctx, r.bucket, candidate.Key)
+ if err != nil {
+ return ObjectVersion{}, err
+ }
+ current, err := currentSingleBlockObjectVersion(topology)
+ if err != nil {
+ return ObjectVersion{}, xerrors.Errorf("unsafe single-block object topology: key=%q: %w", candidate.Key, err)
+ }
+ // Historical versions are not read by Chainstorage and are not repair
+ // inputs. The caller pins and verifies the current immutable version against
+ // the source CSCB before changing any metadata.
+ return current, nil
+}
+
+func (r *repairerImpl) requirePinnedObjectVersion(ctx context.Context, key string, expected ObjectVersion) error {
+ topology, err := r.store.ListObjectVersions(ctx, r.bucket, key)
+ if err != nil {
+ return err
+ }
+ return requireExactTopology(topology, expected)
+}
+
+func (r *repairerImpl) requirePinnedSingleBlockObjectVersion(ctx context.Context, key string, expected ObjectVersion) error {
+ topology, err := r.store.ListObjectVersions(ctx, r.bucket, key)
+ if err != nil {
+ return err
+ }
+ current, err := currentSingleBlockObjectVersion(topology)
+ if err != nil {
+ return err
+ }
+ if current != expected {
+ return xerrors.Errorf(
+ "pinned single-block object current version changed: version=%q etag=%q bytes=%d",
+ current.VersionID,
+ current.ETag,
+ current.Bytes,
+ )
+ }
+ return nil
+}
+
+func (r *repairerImpl) requirePinnedSingleBlockObjectVersionForManifest(
+ ctx context.Context,
+ _ *Manifest,
+ block *Block,
+) error {
+ return r.requirePinnedSingleBlockObjectVersion(ctx, block.SingleBlockObjectKey, block.SingleBlockObjectVersion)
+}
+
+func currentSingleBlockObjectVersion(topology retirement.ObjectVersionTopology) (ObjectVersion, error) {
+ if len(topology.Versions) == 0 || len(topology.DeleteMarkers) != 0 {
+ return ObjectVersion{}, xerrors.Errorf(
+ "expected at least one data version and zero delete markers, got versions=%d delete_markers=%d",
+ len(topology.Versions),
+ len(topology.DeleteMarkers),
+ )
+ }
+
+ var current ObjectVersion
+ currentFound := false
+ for _, version := range topology.Versions {
+ candidate := ObjectVersion{VersionID: version.VersionID, ETag: version.ETag, Bytes: version.Bytes}
+ if !immutableVersionID(candidate.VersionID) || candidate.ETag == "" || candidate.Bytes == 0 {
+ return ObjectVersion{}, xerrors.Errorf("single-block object version is not immutable and complete: version=%q", candidate.VersionID)
+ }
+ if version.IsLatest {
+ if currentFound {
+ return ObjectVersion{}, xerrors.New("single-block object has multiple latest data versions")
+ }
+ current = candidate
+ currentFound = true
+ }
+ }
+ if !currentFound {
+ return ObjectVersion{}, xerrors.New("single-block object has no latest data version")
+ }
+ return current, nil
+}
+
+func requireExactTopology(topology retirement.ObjectVersionTopology, expected ObjectVersion) error {
+ if len(topology.Versions) != 1 || len(topology.DeleteMarkers) != 0 {
+ return xerrors.Errorf(
+ "expected one data version and zero delete markers, got versions=%d delete_markers=%d",
+ len(topology.Versions),
+ len(topology.DeleteMarkers),
+ )
+ }
+ version := topology.Versions[0]
+ if !version.IsLatest || version.VersionID != expected.VersionID || version.ETag != expected.ETag || version.Bytes != expected.Bytes {
+ return xerrors.Errorf(
+ "pinned object version changed: version=%q etag=%q bytes=%d latest=%t",
+ version.VersionID,
+ version.ETag,
+ version.Bytes,
+ version.IsLatest,
+ )
+ }
+ return nil
+}
+
+func rebuiltObjectKey(manifest *Manifest) (string, error) {
+ objectKey := ""
+ for _, block := range manifest.Blocks {
+ if !block.Canonical {
+ continue
+ }
+ if block.NewConsolidatedObjectKey == "" || block.NewConsolidatedObjectKey != block.ActiveObjectKey {
+ return "", xerrors.Errorf("rebuilt CSCB placement is incomplete at height %d", block.Height)
+ }
+ if objectKey == "" {
+ objectKey = block.NewConsolidatedObjectKey
+ continue
+ }
+ if objectKey != block.NewConsolidatedObjectKey {
+ return "", xerrors.Errorf("one dirty CSCB repair produced multiple new objects: %q and %q", objectKey, block.NewConsolidatedObjectKey)
+ }
+ }
+ if objectKey == "" {
+ return "", xerrors.New("rebuilt CSCB object key is missing")
+ }
+ if objectKey == manifest.OldConsolidatedObjectKey {
+ return "", xerrors.Errorf("rebuilt CSCB reused dirty object key %q", objectKey)
+ }
+ return objectKey, nil
+}
+
+func validateRebuiltPlacements(
+ manifest *Manifest,
+ objectKey string,
+ placements []RebuiltPlacement,
+) (map[int64]RebuiltPlacement, error) {
+ if manifest == nil || len(placements) != len(manifest.Blocks) {
+ return nil, xerrors.Errorf(
+ "rebuilt CSCB placement count mismatch: expected=%d actual=%d",
+ len(manifest.Blocks),
+ len(placements),
+ )
+ }
+ blocksByID := make(map[int64]Block, len(manifest.Blocks))
+ for _, block := range manifest.Blocks {
+ blocksByID[block.BlockMetadataID] = block
+ }
+ placementsByID := make(map[int64]RebuiltPlacement, len(placements))
+ for _, placement := range placements {
+ block, ok := blocksByID[placement.BlockMetadataID]
+ if !ok {
+ return nil, xerrors.Errorf("rebuilt CSCB returned unexpected metadata_id=%d", placement.BlockMetadataID)
+ }
+ if _, ok := placementsByID[placement.BlockMetadataID]; ok {
+ return nil, xerrors.Errorf("rebuilt CSCB returned duplicate metadata_id=%d", placement.BlockMetadataID)
+ }
+ if placement.Height != block.Height || placement.Hash != block.Hash ||
+ placement.ObjectFormat != api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH ||
+ placement.ByteLength == 0 || placement.UncompressedLength == 0 {
+ return nil, xerrors.Errorf(
+ "rebuilt CSCB placement is invalid for metadata_id=%d object=%q",
+ placement.BlockMetadataID,
+ objectKey,
+ )
+ }
+ placementsByID[placement.BlockMetadataID] = placement
+ }
+ return placementsByID, nil
+}
+
+func makePayloadCandidate(
+ manifest *Manifest,
+ block *Block,
+ consolidatedKey string,
+ consolidatedVersion ObjectVersion,
+) retirement.Candidate {
+ return retirement.Candidate{
+ Bucket: manifest.Bucket,
+ Key: block.SingleBlockObjectKey,
+ VersionID: block.SingleBlockObjectVersion.VersionID,
+ Height: block.Height,
+ Hash: block.Hash,
+ ConsolidatedKey: consolidatedKey,
+ Tag: block.Tag,
+ CSCBVersionID: consolidatedVersion.VersionID,
+ ByteOffset: blockPlacementOffset(block, consolidatedKey, manifest.OldConsolidatedObjectKey),
+ ByteLength: blockPlacementLength(block, consolidatedKey, manifest.OldConsolidatedObjectKey),
+ UncompressedLength: blockPlacementUncompressedLength(block, consolidatedKey, manifest.OldConsolidatedObjectKey),
+ }
+}
+
+func blockPlacementOffset(block *Block, objectKey string, oldObjectKey string) uint64 {
+ if objectKey == oldObjectKey {
+ return block.OldByteOffset
+ }
+ return block.NewByteOffset
+}
+
+func blockPlacementLength(block *Block, objectKey string, oldObjectKey string) uint64 {
+ if objectKey == oldObjectKey {
+ return block.OldByteLength
+ }
+ return block.NewByteLength
+}
+
+func blockPlacementUncompressedLength(block *Block, objectKey string, oldObjectKey string) uint64 {
+ if objectKey == oldObjectKey {
+ return block.OldUncompressedLength
+ }
+ return block.NewUncompressedLength
+}
+
+func rowSetSHA256(manifest *Manifest) string {
+ digest := sha256.New()
+ for _, block := range manifest.Blocks {
+ _, _ = fmt.Fprintf(
+ digest,
+ "%d\x1f%t\x1f%d\x1f%d\x1f%d:%s\x1f%d:%s\x1f%s\x1f%d\x1f%d\x1f%d\n",
+ block.BlockMetadataID,
+ block.Canonical,
+ block.Tag,
+ block.Height,
+ len(block.Hash),
+ block.Hash,
+ len(block.SingleBlockObjectKey),
+ block.SingleBlockObjectKey,
+ block.SingleBlockObjectKeySHA256,
+ block.OldByteOffset,
+ block.OldByteLength,
+ block.OldUncompressedLength,
+ )
+ }
+ return hex.EncodeToString(digest.Sum(nil))
+}
+
+func objectKeySHA256(objectKey string) string {
+ digest := sha256.Sum256([]byte(objectKey))
+ return hex.EncodeToString(digest[:])
+}
+
+func immutableVersionID(versionID string) bool {
+ value := strings.TrimSpace(versionID)
+ return value != "" && !strings.EqualFold(value, "null")
+}
+
+var _ Repairer = (*repairerImpl)(nil)
diff --git a/internal/storage/cscbrepair/repairer_test.go b/internal/storage/cscbrepair/repairer_test.go
new file mode 100644
index 00000000..1f32acc8
--- /dev/null
+++ b/internal/storage/cscbrepair/repairer_test.go
@@ -0,0 +1,1313 @@
+package cscbrepair
+
+import (
+ "bytes"
+ "compress/gzip"
+ "context"
+ "crypto/sha256"
+ "encoding/binary"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "hash/crc32"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/cscb"
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+ storageutils "github.com/coinbase/chainstorage/internal/storage/utils"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestValidateCandidateAllowsAllNonCanonicalRows(t *testing.T) {
+ candidate := validRepairCandidate()
+ candidate.Blocks[0].Canonical = false
+ candidate.CanonicalBlockCount = 0
+ require.NoError(t, validateCandidate(candidate, 1000, 1001, 1))
+}
+
+func TestValidateCandidateRejectsUnsafeSingleBlockState(t *testing.T) {
+ valid := validRepairCandidate()
+ require.NoError(t, validateCandidate(valid, 1000, 1001, 1))
+
+ tests := map[string]func(*Manifest){
+ "retirement fence": func(candidate *Manifest) { candidate.Blocks[0].RetirementFenced = true },
+ "retirement manifest": func(candidate *Manifest) { candidate.Blocks[0].RetirementManifestExists = true },
+ "deleted single block": func(candidate *Manifest) { candidate.Blocks[0].SingleBlockObjectDeleted = true },
+ "missing single block key": func(candidate *Manifest) { candidate.Blocks[0].SingleBlockObjectKey = "" },
+ }
+ for name, mutate := range tests {
+ t.Run(name, func(t *testing.T) {
+ candidate := validRepairCandidate()
+ mutate(candidate)
+ require.ErrorContains(t, validateCandidate(candidate, 1000, 1001, 1), "blocked or incomplete")
+ })
+ }
+}
+
+func TestPrepareNextRejectsPendingRepairOutsideApprovedRange(t *testing.T) {
+ pending := validRepairCandidate()
+ pending.State = StatePrepared
+ repository := &pendingRepairRepository{pending: pending}
+ repairer := NewRepairer(repository, &pendingObjectStore{}, "repair-bucket")
+
+ _, err := repairer.PrepareNext(context.Background(), executionKey("outside-range"), 2, 2000, 2001, 1, nil)
+ require.ErrorContains(t, err, "pending CSCB repair does not fit the approved request")
+ require.ErrorContains(t, err, "exceeds approved range")
+}
+
+func TestPrepareNextPersistsFenceBeforeS3InspectionAndRetriesSameManifest(t *testing.T) {
+ inspectionErr := errors.New("S3 inspection stopped")
+ repository := &preparationRepository{candidate: validRepairCandidate()}
+ store := &preparationObjectStore{repository: repository, listErr: inspectionErr}
+ repairer := NewRepairer(repository, store, "repair-bucket")
+ key := executionKey("durable-fence")
+
+ _, err := repairer.PrepareNext(context.Background(), key, 2, 1000, 1001, 1, nil)
+ require.ErrorIs(t, err, inspectionErr)
+ require.True(t, repository.fenced)
+ require.True(t, repository.bound)
+ require.False(t, repository.inspectionRecorded)
+ require.Equal(t, StatePreparing, repository.manifest.State)
+ require.Empty(t, repository.manifest.OldConsolidatedObjectVersion.VersionID)
+ require.Len(t, repository.manifest.RowSetSHA256, 64)
+ require.Equal(t, objectKeySHA256("single/1000.gzip"), repository.manifest.Blocks[0].SingleBlockObjectKeySHA256)
+ require.Equal(t, 1, repository.fenceCalls)
+
+ _, err = repairer.PrepareNext(context.Background(), key, 2, 1000, 1001, 1, nil)
+ require.ErrorIs(t, err, inspectionErr)
+ require.Equal(t, 1, repository.fenceCalls, "retry must reuse the durable preparing manifest")
+ require.Equal(t, 2, store.listCalls)
+}
+
+func TestPrepareObjectSelectsOnlyAssignedObject(t *testing.T) {
+ inspectionErr := errors.New("S3 inspection stopped")
+ candidate := validRepairCandidate()
+ repository := &preparationRepository{candidate: candidate}
+ store := &preparationObjectStore{repository: repository, listErr: inspectionErr}
+ repairer := NewRepairer(repository, store, "repair-bucket")
+
+ _, err := repairer.PrepareObject(
+ context.Background(),
+ executionKey("assigned-object"),
+ candidate.Tag,
+ candidate.StartHeight,
+ candidate.EndHeight,
+ 1,
+ candidate.OldConsolidatedObjectKey,
+ nil,
+ )
+ require.ErrorIs(t, err, inspectionErr)
+ require.Equal(t, candidate.OldConsolidatedObjectKey, repository.requestedObjectKey)
+ require.True(t, repository.fenced)
+ require.True(t, repository.bound)
+}
+
+func TestPrepareObjectRejectsExecutionBoundToDifferentObject(t *testing.T) {
+ bound := validRepairCandidate()
+ bound.ID = 41
+ bound.State = StatePrepared
+ repository := &boundRepairRepository{manifest: bound}
+ repairer := NewRepairer(repository, &pendingObjectStore{}, "repair-bucket")
+
+ _, err := repairer.PrepareObject(
+ context.Background(),
+ executionKey("bound-object"),
+ bound.Tag,
+ bound.StartHeight,
+ bound.EndHeight,
+ 1,
+ "consolidated/different.cscb.zstd",
+ nil,
+ )
+ require.ErrorContains(t, err, "bound CSCB repair object changed")
+}
+
+func TestPrepareObjectReturnsCompletedManifestFromConcurrentWorkflow(t *testing.T) {
+ completed := validRepairCandidate()
+ completed.ID = 42
+ completed.State = StateCompleted
+ repository := &completedObjectRepository{manifest: completed}
+ repairer := NewRepairer(repository, &pendingObjectStore{}, "repair-bucket")
+ key := executionKey("completed-object")
+
+ manifest, err := repairer.PrepareObject(
+ context.Background(),
+ key,
+ completed.Tag,
+ completed.StartHeight,
+ completed.EndHeight,
+ 1,
+ completed.OldConsolidatedObjectKey,
+ nil,
+ )
+ require.NoError(t, err)
+ require.Same(t, completed, manifest)
+ require.Equal(t, key, repository.executionKey)
+}
+
+func TestListCandidatesDelegatesExactLimit(t *testing.T) {
+ repository := &candidateListRepository{
+ objectKeys: []string{"consolidated/dirty-b.cscb.zstd", "consolidated/dirty-a.cscb.zstd"},
+ }
+ repairer := NewRepairer(repository, &pendingObjectStore{}, "repair-bucket")
+
+ objectKeys, err := repairer.ListCandidates(context.Background(), 2, 1000, 1200, 2)
+ require.NoError(t, err)
+ require.Equal(t, repository.objectKeys, objectKeys)
+ require.Equal(t, 2, repository.limit)
+}
+
+func TestPrepareNextCompletesAlreadyCleanCSCBWithoutRestore(t *testing.T) {
+ candidate := validRepairCandidate()
+ candidate.Blocks[0].Hash = "clean-hash"
+ candidate.Blocks[0].OldByteOffset = 0
+ block := &api.Block{Metadata: &api.BlockMetadata{
+ Tag: candidate.Tag,
+ Height: candidate.Blocks[0].Height,
+ Hash: candidate.Blocks[0].Hash,
+ }}
+ payload, err := proto.Marshal(block)
+ require.NoError(t, err)
+ singleObject, err := storageutils.Compress(payload, api.Compression_GZIP)
+ require.NoError(t, err)
+ candidate.Blocks[0].OldByteLength = uint64(len(payload))
+ candidate.Blocks[0].OldUncompressedLength = uint64(len(payload))
+ candidate.OldConsolidatedObjectKey = "consolidated/clean.cscb.gzip"
+ candidate.Blocks[0].OldConsolidatedObjectKey = candidate.OldConsolidatedObjectKey
+ cscbObject := buildRepairSingleBlockCSCB(t, retirement.Candidate{
+ BlockMetadataID: candidate.Blocks[0].BlockMetadataID,
+ Tag: candidate.Tag,
+ Height: candidate.Blocks[0].Height,
+ Hash: candidate.Blocks[0].Hash,
+ ByteOffset: 0,
+ ByteLength: uint64(len(payload)),
+ UncompressedLength: uint64(len(payload)),
+ }, payload)
+
+ repository := &preparationRepository{candidate: candidate}
+ store := &preparationObjectStore{
+ repository: repository,
+ topologies: map[string]retirement.ObjectVersionTopology{
+ candidate.OldConsolidatedObjectKey: {
+ Versions: []retirement.ObjectVersion{{VersionID: "clean-cscb-version", ETag: "clean-cscb-etag", Bytes: uint64(len(cscbObject)), IsLatest: true}},
+ },
+ candidate.Blocks[0].SingleBlockObjectKey: {
+ Versions: []retirement.ObjectVersion{
+ {VersionID: "single-current-version", ETag: "single-etag", Bytes: uint64(len(singleObject)), IsLatest: true},
+ {VersionID: "single-historical-version", ETag: "single-etag", Bytes: uint64(len(singleObject))},
+ },
+ },
+ },
+ objects: map[string][]byte{
+ candidate.OldConsolidatedObjectKey: cscbObject,
+ candidate.Blocks[0].SingleBlockObjectKey: singleObject,
+ },
+ }
+
+ manifest, err := NewRepairer(repository, store, "repair-bucket").PrepareNext(
+ context.Background(),
+ executionKey("already-clean"),
+ candidate.Tag,
+ candidate.StartHeight,
+ candidate.EndHeight,
+ 1,
+ nil,
+ )
+ require.NoError(t, err)
+ require.Equal(t, StateCompleted, manifest.State)
+ require.Equal(t, alreadyCleanOutcome, manifest.Outcome)
+ require.NotNil(t, manifest.VerifiedAt)
+ require.NotNil(t, manifest.CompletedAt)
+ require.Nil(t, manifest.RestoredAt)
+ require.True(t, repository.inspectionRecorded)
+ require.True(t, repository.alreadyClean)
+}
+
+func TestPrepareNextUsesPinnedSingleBlockAsAuthoritativeRepairPayload(t *testing.T) {
+ candidate := validRepairCandidate()
+ candidate.Blocks[0].Hash = "repair-hash"
+ candidate.Blocks[0].OldByteOffset = 0
+
+ currentBlock := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: candidate.Tag,
+ Height: candidate.Blocks[0].Height,
+ Hash: candidate.Blocks[0].Hash,
+ ObjectKeyMain: candidate.Blocks[0].SingleBlockObjectKey,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK,
+ },
+ Blobdata: &api.Block_Solana{
+ Solana: &api.SolanaBlobdata{Header: []byte(`{"transactions":["current"]}`)},
+ },
+ }
+ oldBlock := proto.Clone(currentBlock).(*api.Block)
+ oldBlock.GetSolana().Header = []byte(`{"transactions":["stale"]}`)
+
+ currentPayload, err := proto.Marshal(currentBlock)
+ require.NoError(t, err)
+ currentObject, err := storageutils.Compress(currentPayload, api.Compression_GZIP)
+ require.NoError(t, err)
+ oldPayload, err := proto.Marshal(oldBlock)
+ require.NoError(t, err)
+ candidate.Blocks[0].OldByteLength = uint64(len(oldPayload))
+ candidate.Blocks[0].OldUncompressedLength = uint64(len(oldPayload))
+ oldObject := buildRepairSingleBlockCSCB(t, retirement.Candidate{
+ BlockMetadataID: candidate.Blocks[0].BlockMetadataID,
+ Tag: candidate.Tag,
+ Height: candidate.Blocks[0].Height,
+ Hash: candidate.Blocks[0].Hash,
+ ByteOffset: 0,
+ ByteLength: uint64(len(oldPayload)),
+ UncompressedLength: uint64(len(oldPayload)),
+ }, oldPayload)
+
+ repository := &preparationRepository{candidate: candidate}
+ store := &preparationObjectStore{
+ repository: repository,
+ topologies: map[string]retirement.ObjectVersionTopology{
+ candidate.OldConsolidatedObjectKey: {
+ Versions: []retirement.ObjectVersion{{
+ VersionID: "old-cscb-version",
+ ETag: "old-cscb-etag",
+ Bytes: uint64(len(oldObject)),
+ IsLatest: true,
+ }},
+ },
+ candidate.Blocks[0].SingleBlockObjectKey: {
+ Versions: []retirement.ObjectVersion{{
+ VersionID: "current-single-version",
+ ETag: "current-single-etag",
+ Bytes: uint64(len(currentObject)),
+ IsLatest: true,
+ }},
+ },
+ },
+ objects: map[string][]byte{
+ candidate.OldConsolidatedObjectKey: oldObject,
+ candidate.Blocks[0].SingleBlockObjectKey: currentObject,
+ },
+ }
+ repairer := NewRepairer(repository, store, "repair-bucket")
+
+ manifest, err := repairer.PrepareNext(
+ context.Background(),
+ executionKey("authoritative-single-block"),
+ candidate.Tag,
+ candidate.StartHeight,
+ candidate.EndHeight,
+ 1,
+ nil,
+ )
+ require.NoError(t, err)
+ require.Equal(t, StatePrepared, manifest.State)
+ require.False(t, repository.alreadyClean)
+
+ currentNormalized := storageutils.CloneBlockWithoutStoragePlacement(currentBlock)
+ expectedPayload, err := (proto.MarshalOptions{Deterministic: true}).Marshal(currentNormalized)
+ require.NoError(t, err)
+ expectedDigest := sha256.Sum256(expectedPayload)
+ require.Equal(t, hex.EncodeToString(expectedDigest[:]), manifest.Blocks[0].PayloadSHA256)
+
+ var pinned PinnedPayload
+ require.NoError(t, repairer.VisitPinnedPayloads(
+ context.Background(),
+ manifest.ID,
+ nil,
+ func(payload PinnedPayload) error {
+ pinned = payload
+ return nil
+ },
+ ))
+ var rebuilt api.Block
+ require.NoError(t, proto.Unmarshal(pinned.RawBlockPayload, &rebuilt))
+ require.False(t, storageutils.HasBlockStoragePlacement(&rebuilt))
+ require.True(t, proto.Equal(currentNormalized, &rebuilt))
+ require.False(t, proto.Equal(storageutils.CloneBlockWithoutStoragePlacement(oldBlock), &rebuilt))
+}
+
+func TestInspectSingleBlockObjectVersionPinsCurrentWithoutReadingHistoricalPayloads(t *testing.T) {
+ candidate := singleBlockVersionCandidate()
+ currentPayload := singleBlockVersionPayload(t, candidate, `{"blockhash":"block-hash","transactions":[]}`)
+ historicalPayload := singleBlockVersionPayload(t, candidate, `{"blockhash":"block-hash","transactions":["stale"]}`)
+ require.NotEqual(t, currentPayload, historicalPayload)
+ store := &versionedObjectStore{
+ topology: retirement.ObjectVersionTopology{Versions: []retirement.ObjectVersion{
+ {VersionID: "current-version", ETag: "current-etag", Bytes: uint64(len(currentPayload)), IsLatest: true},
+ {VersionID: "historical-version", ETag: "historical-etag", Bytes: uint64(len(historicalPayload))},
+ }},
+ objects: map[string][]byte{
+ "current-version": currentPayload,
+ "historical-version": historicalPayload,
+ },
+ }
+ repairer := &repairerImpl{store: store, bucket: "repair-bucket"}
+
+ version, err := repairer.inspectSingleBlockObjectVersion(context.Background(), candidate)
+ require.NoError(t, err)
+ require.Equal(t, ObjectVersion{
+ VersionID: "current-version",
+ ETag: "current-etag",
+ Bytes: uint64(len(currentPayload)),
+ }, version)
+ require.Empty(t, store.readVersionIDs)
+}
+
+func TestInspectSingleBlockObjectVersionDoesNotReadSingleVersionTwice(t *testing.T) {
+ candidate := singleBlockVersionCandidate()
+ store := &versionedObjectStore{
+ topology: retirement.ObjectVersionTopology{Versions: []retirement.ObjectVersion{
+ {VersionID: "current-version", ETag: "current-etag", Bytes: 4, IsLatest: true},
+ }},
+ }
+ repairer := &repairerImpl{store: store, bucket: "repair-bucket"}
+
+ version, err := repairer.inspectSingleBlockObjectVersion(context.Background(), candidate)
+ require.NoError(t, err)
+ require.Equal(t, "current-version", version.VersionID)
+ require.Empty(t, store.readVersionIDs)
+}
+
+func TestInspectSingleBlockObjectVersionAllowsManyImmutableVersionsWithoutReading(t *testing.T) {
+ candidate := singleBlockVersionCandidate()
+ versions := make([]retirement.ObjectVersion, 20)
+ for i := range versions {
+ versions[i] = retirement.ObjectVersion{
+ VersionID: fmt.Sprintf("version-%d", i),
+ ETag: "same-etag",
+ Bytes: 4,
+ IsLatest: i == 0,
+ }
+ }
+ store := &versionedObjectStore{
+ topology: retirement.ObjectVersionTopology{Versions: versions},
+ }
+ repairer := &repairerImpl{store: store, bucket: "repair-bucket"}
+
+ version, err := repairer.inspectSingleBlockObjectVersion(context.Background(), candidate)
+ require.NoError(t, err)
+ require.Equal(t, "version-0", version.VersionID)
+ require.Empty(t, store.readVersionIDs)
+}
+
+func TestInspectSingleBlockObjectVersionRejectsDeleteMarker(t *testing.T) {
+ candidate := singleBlockVersionCandidate()
+ store := &versionedObjectStore{
+ topology: retirement.ObjectVersionTopology{
+ Versions: []retirement.ObjectVersion{
+ {VersionID: "current-version", ETag: "same-etag", Bytes: 4, IsLatest: true},
+ },
+ DeleteMarkers: []retirement.ObjectDeleteMarker{{VersionID: "delete-marker"}},
+ },
+ }
+ repairer := &repairerImpl{store: store, bucket: "repair-bucket"}
+
+ _, err := repairer.inspectSingleBlockObjectVersion(context.Background(), candidate)
+ require.ErrorContains(t, err, "zero delete markers")
+}
+
+func TestRequirePinnedSingleBlockObjectVersionRejectsNewLatestVersion(t *testing.T) {
+ pinned := ObjectVersion{VersionID: "pinned-version", ETag: "same-etag", Bytes: 4}
+ store := &versionedObjectStore{
+ topology: retirement.ObjectVersionTopology{Versions: []retirement.ObjectVersion{
+ {VersionID: "new-version", ETag: "same-etag", Bytes: 4, IsLatest: true},
+ {VersionID: pinned.VersionID, ETag: pinned.ETag, Bytes: pinned.Bytes},
+ }},
+ }
+ repairer := &repairerImpl{store: store, bucket: "repair-bucket"}
+
+ err := repairer.requirePinnedSingleBlockObjectVersion(context.Background(), "single/1000.zstd", pinned)
+ require.ErrorContains(t, err, "current version changed")
+}
+
+func TestRequirePinnedSingleBlockObjectVersionAllowsHistoricalDuplicateRemoval(t *testing.T) {
+ pinned := ObjectVersion{VersionID: "pinned-version", ETag: "same-etag", Bytes: 4}
+ store := &versionedObjectStore{
+ topology: retirement.ObjectVersionTopology{Versions: []retirement.ObjectVersion{
+ {VersionID: pinned.VersionID, ETag: pinned.ETag, Bytes: pinned.Bytes, IsLatest: true},
+ }},
+ }
+ repairer := &repairerImpl{store: store, bucket: "repair-bucket"}
+
+ require.NoError(t, repairer.requirePinnedSingleBlockObjectVersion(context.Background(), "single/1000.zstd", pinned))
+}
+
+func TestRestoreZeroCanonicalRepairResumesWithHistoricalSingleBlockVersion(t *testing.T) {
+ manifest := completionManifest(t)
+ manifest.State = StatePrepared
+ repository := &phaseBoundaryRepository{manifest: manifest}
+ store := &completionObjectStore{topologies: map[string]retirement.ObjectVersionTopology{
+ manifest.OldConsolidatedObjectKey: {
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(manifest.OldConsolidatedObjectVersion)},
+ },
+ manifest.Blocks[0].SingleBlockObjectKey: {
+ Versions: []retirement.ObjectVersion{
+ objectTopologyVersion(manifest.Blocks[0].SingleBlockObjectVersion),
+ {VersionID: "historical-version", ETag: "historical-etag", Bytes: 99},
+ },
+ },
+ }}
+
+ _, err := NewRepairer(repository, store, manifest.Bucket).Restore(context.Background(), manifest.ID, nil)
+ require.NoError(t, err)
+ require.True(t, repository.restoreCalled)
+}
+
+func TestVisitPinnedPayloadsReadsPinnedCurrentVersionAndStripsStoragePlacement(t *testing.T) {
+ manifest := phaseBoundaryManifest(t, StatePrepared)
+ block := &manifest.Blocks[0]
+ rawBlock := &api.Block{Metadata: &api.BlockMetadata{
+ Tag: block.Tag,
+ Height: block.Height,
+ Hash: block.Hash,
+ ObjectKeyMain: block.SingleBlockObjectKey,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK,
+ ByteOffset: 10,
+ ByteLength: 20,
+ UncompressedLength: 30,
+ }}
+ normalized := storageutils.CloneBlockWithoutStoragePlacement(rawBlock)
+ normalizedPayload, err := (proto.MarshalOptions{Deterministic: true}).Marshal(normalized)
+ require.NoError(t, err)
+ digest := sha256.Sum256(normalizedPayload)
+ block.PayloadSHA256 = hex.EncodeToString(digest[:])
+ payload, err := proto.Marshal(rawBlock)
+ require.NoError(t, err)
+ compressed, err := storageutils.Compress(payload, api.Compression_GZIP)
+ require.NoError(t, err)
+ block.SingleBlockObjectVersion.Bytes = uint64(len(compressed))
+
+ repository := &phaseBoundaryRepository{manifest: manifest}
+ store := &completionObjectStore{
+ topologies: map[string]retirement.ObjectVersionTopology{
+ manifest.OldConsolidatedObjectKey: {
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(manifest.OldConsolidatedObjectVersion)},
+ },
+ block.SingleBlockObjectKey: {
+ Versions: []retirement.ObjectVersion{
+ objectTopologyVersion(block.SingleBlockObjectVersion),
+ {VersionID: "historical-version", ETag: "historical-etag", Bytes: 99},
+ },
+ },
+ },
+ objects: map[string][]byte{block.SingleBlockObjectKey: compressed},
+ }
+
+ var pinned PinnedPayload
+ err = NewRepairer(repository, store, manifest.Bucket).VisitPinnedPayloads(
+ context.Background(),
+ manifest.ID,
+ nil,
+ func(payload PinnedPayload) error {
+ pinned = payload
+ return nil
+ },
+ )
+ require.NoError(t, err)
+ require.Equal(t, []string{block.SingleBlockObjectVersion.VersionID}, store.readVersionIDs)
+ require.Equal(t, block.BlockMetadataID, pinned.BlockMetadataID)
+ require.Equal(t, block.Tag, pinned.Metadata.GetTag())
+ require.Equal(t, block.Height, pinned.Metadata.GetHeight())
+ require.Equal(t, block.Hash, pinned.Metadata.GetHash())
+ var rebuilt api.Block
+ require.NoError(t, proto.Unmarshal(pinned.RawBlockPayload, &rebuilt))
+ require.False(t, storageutils.HasBlockStoragePlacement(&rebuilt))
+}
+
+func TestVisitPinnedPayloadsRejectsPersistedDigestMismatch(t *testing.T) {
+ manifest := phaseBoundaryManifest(t, StatePrepared)
+ block := &manifest.Blocks[0]
+ rawBlock := &api.Block{Metadata: &api.BlockMetadata{
+ Tag: block.Tag,
+ Height: block.Height,
+ Hash: block.Hash,
+ }}
+ payload, err := proto.Marshal(rawBlock)
+ require.NoError(t, err)
+ compressed, err := storageutils.Compress(payload, api.Compression_GZIP)
+ require.NoError(t, err)
+ block.SingleBlockObjectVersion.Bytes = uint64(len(compressed))
+ block.PayloadSHA256 = strings.Repeat("0", 64)
+
+ repository := &phaseBoundaryRepository{manifest: manifest}
+ store := &completionObjectStore{
+ topologies: map[string]retirement.ObjectVersionTopology{
+ manifest.OldConsolidatedObjectKey: {
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(manifest.OldConsolidatedObjectVersion)},
+ },
+ block.SingleBlockObjectKey: {
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(block.SingleBlockObjectVersion)},
+ },
+ },
+ objects: map[string][]byte{block.SingleBlockObjectKey: compressed},
+ }
+
+ err = NewRepairer(repository, store, manifest.Bucket).VisitPinnedPayloads(
+ context.Background(), manifest.ID, nil, func(PinnedPayload) error { return nil },
+ )
+ require.ErrorContains(t, err, "single-block payload digest changed before rebuild")
+}
+
+func TestRepairPhasesRejectSingleBlockTopologyDriftBeforeMutation(t *testing.T) {
+ phases := []struct {
+ name string
+ state State
+ invoke func(Repairer, int64) (*Manifest, error)
+ }{
+ {
+ name: "restore",
+ state: StatePrepared,
+ invoke: func(repairer Repairer, repairID int64) (*Manifest, error) {
+ return repairer.Restore(context.Background(), repairID, nil)
+ },
+ },
+ {
+ name: "verify rebuilt",
+ state: StateRestored,
+ invoke: func(repairer Repairer, repairID int64) (*Manifest, error) {
+ return repairer.VerifyRebuilt(context.Background(), repairID, nil)
+ },
+ },
+ {
+ name: "complete",
+ state: StateVerified,
+ invoke: func(repairer Repairer, repairID int64) (*Manifest, error) {
+ return repairer.Complete(context.Background(), repairID, nil)
+ },
+ },
+ }
+ drifts := []struct {
+ name string
+ topology func(ObjectVersion) retirement.ObjectVersionTopology
+ }{
+ {
+ name: "new latest version",
+ topology: func(pinned ObjectVersion) retirement.ObjectVersionTopology {
+ return retirement.ObjectVersionTopology{Versions: []retirement.ObjectVersion{
+ {VersionID: "new-version", ETag: pinned.ETag, Bytes: pinned.Bytes, IsLatest: true},
+ objectTopologyVersion(pinned),
+ }}
+ },
+ },
+ {
+ name: "delete marker",
+ topology: func(pinned ObjectVersion) retirement.ObjectVersionTopology {
+ return retirement.ObjectVersionTopology{
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(pinned)},
+ DeleteMarkers: []retirement.ObjectDeleteMarker{{VersionID: "delete-marker", IsLatest: true}},
+ }
+ },
+ },
+ }
+
+ for _, phase := range phases {
+ for _, drift := range drifts {
+ t.Run(phase.name+"/"+drift.name, func(t *testing.T) {
+ manifest := phaseBoundaryManifest(t, phase.state)
+ repository := &phaseBoundaryRepository{manifest: manifest}
+ store := &completionObjectStore{topologies: map[string]retirement.ObjectVersionTopology{
+ manifest.OldConsolidatedObjectKey: {
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(manifest.OldConsolidatedObjectVersion)},
+ },
+ manifest.NewConsolidatedObjectKey: {
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(manifest.NewConsolidatedObjectVersion)},
+ },
+ manifest.Blocks[0].SingleBlockObjectKey: drift.topology(manifest.Blocks[0].SingleBlockObjectVersion),
+ }}
+
+ _, err := phase.invoke(NewRepairer(repository, store, "repair-bucket"), manifest.ID)
+ require.ErrorContains(t, err, "single-block object changed")
+ require.False(t, repository.restoreCalled)
+ require.False(t, repository.recordVerifiedCalled)
+ require.False(t, repository.completeCalled)
+ })
+ }
+ }
+}
+
+func TestVerifyRebuiltResumesManifestWithPreSemanticSolanaDigest(t *testing.T) {
+ manifest := phaseBoundaryManifest(t, StateRestored)
+ block := &manifest.Blocks[0]
+ rawBlock := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: block.Tag,
+ Height: block.Height,
+ Hash: block.Hash,
+ },
+ Blobdata: &api.Block_Solana{Solana: &api.SolanaBlobdata{
+ Header: []byte("{\n \"transactions\": [],\n \"blockhash\": \"block-hash\"\n}"),
+ }},
+ }
+ payload, err := (proto.MarshalOptions{Deterministic: true}).Marshal(rawBlock)
+ require.NoError(t, err)
+ preSemanticDigest := sha256.Sum256(payload)
+ block.PayloadSHA256 = hex.EncodeToString(preSemanticDigest[:])
+ block.NewByteOffset = 0
+ block.NewByteLength = uint64(len(payload))
+ block.NewUncompressedLength = uint64(len(payload))
+
+ singleObject, err := storageutils.Compress(payload, api.Compression_GZIP)
+ require.NoError(t, err)
+ block.SingleBlockObjectVersion.Bytes = uint64(len(singleObject))
+ newCSCB := buildRepairSingleBlockCSCB(t, retirement.Candidate{
+ BlockMetadataID: block.BlockMetadataID,
+ Tag: block.Tag,
+ Height: block.Height,
+ Hash: block.Hash,
+ ByteOffset: block.NewByteOffset,
+ }, payload)
+ manifest.NewConsolidatedObjectVersion.Bytes = uint64(len(newCSCB))
+
+ repository := &phaseBoundaryRepository{manifest: manifest}
+ store := &completionObjectStore{
+ topologies: map[string]retirement.ObjectVersionTopology{
+ manifest.NewConsolidatedObjectKey: {
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(manifest.NewConsolidatedObjectVersion)},
+ },
+ block.SingleBlockObjectKey: {
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(block.SingleBlockObjectVersion)},
+ },
+ },
+ objects: map[string][]byte{
+ manifest.NewConsolidatedObjectKey: newCSCB,
+ block.SingleBlockObjectKey: singleObject,
+ },
+ }
+
+ _, err = NewRepairer(repository, store, manifest.Bucket).VerifyRebuilt(context.Background(), manifest.ID, nil)
+ require.NoError(t, err)
+ require.True(t, repository.recordVerifiedCalled)
+ require.Equal(t, hex.EncodeToString(preSemanticDigest[:]), block.PayloadSHA256)
+}
+
+func TestCompleteRejectsRetainedObjectTopologyDrift(t *testing.T) {
+ manifest := completionManifest(t)
+ repository := &completionRepository{manifest: manifest}
+ store := &completionObjectStore{
+ topologies: map[string]retirement.ObjectVersionTopology{
+ manifest.OldConsolidatedObjectKey: {
+ Versions: []retirement.ObjectVersion{
+ objectTopologyVersion(manifest.OldConsolidatedObjectVersion),
+ {VersionID: "unexpected-version", ETag: "unexpected-etag", Bytes: 1},
+ },
+ },
+ },
+ }
+
+ _, err := NewRepairer(repository, store, "repair-bucket").Complete(context.Background(), manifest.ID, nil)
+ require.ErrorContains(t, err, "retained CSCB repair source changed before completion")
+ require.ErrorContains(t, err, "expected one data version and zero delete markers")
+ require.False(t, repository.completeCalled)
+ require.False(t, repository.committed)
+}
+
+func TestCompletePropagatesDatabaseFailureAfterValidation(t *testing.T) {
+ manifest := completionManifest(t)
+ completeErr := errors.New("database completion failed")
+ repository := &completionRepository{manifest: manifest, completeErr: completeErr}
+ store := &completionObjectStore{
+ topologies: map[string]retirement.ObjectVersionTopology{
+ manifest.OldConsolidatedObjectKey: {
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(manifest.OldConsolidatedObjectVersion)},
+ },
+ manifest.Blocks[0].SingleBlockObjectKey: {
+ Versions: []retirement.ObjectVersion{objectTopologyVersion(manifest.Blocks[0].SingleBlockObjectVersion)},
+ },
+ },
+ objects: map[string][]byte{
+ manifest.Blocks[0].SingleBlockObjectKey: completionSingleBlockPayload(t, manifest.Blocks[0]),
+ },
+ }
+
+ _, err := NewRepairer(repository, store, "repair-bucket").Complete(context.Background(), manifest.ID, nil)
+ require.ErrorIs(t, err, completeErr)
+ require.True(t, repository.completeCalled)
+ require.False(t, repository.committed)
+}
+
+func TestValidateRebuiltMetadataRequiresFreshRetentionAndCleanNonCanonicalRows(t *testing.T) {
+ restoredAt := time.Now().UTC()
+ validatedAt := restoredAt.Add(time.Second)
+ retentionStartedAt := restoredAt.Add(2 * time.Second)
+ deleteAfter := retentionStartedAt.Add(72 * time.Hour)
+ manifest := &Manifest{
+ ID: 8,
+ RestoredAt: &restoredAt,
+ CanonicalBlockCount: 1,
+ Blocks: []Block{
+ {
+ BlockMetadataID: 1,
+ Canonical: true,
+ ActiveObjectKey: "consolidated/clean.cscb.zstd",
+ ActiveObjectFormat: int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH),
+ NewConsolidatedObjectKey: "consolidated/clean.cscb.zstd",
+ NewByteLength: 100,
+ NewUncompressedLength: 200,
+ NewValidatedAt: &validatedAt,
+ NewRetentionStartedAt: &retentionStartedAt,
+ NewSingleBlockDeleteAfter: &deleteAfter,
+ },
+ {
+ BlockMetadataID: 2,
+ SingleBlockObjectKey: "single/2.gzip",
+ ActiveObjectKey: "single/2.gzip",
+ ActiveObjectFormat: int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK),
+ },
+ },
+ }
+ require.NoError(t, validateRebuiltMetadata(manifest, "consolidated/clean.cscb.zstd"))
+
+ staleStartedAt := restoredAt.Add(-time.Second)
+ manifest.Blocks[0].NewRetentionStartedAt = &staleStartedAt
+ require.ErrorContains(t, validateRebuiltMetadata(manifest, "consolidated/clean.cscb.zstd"), "freshly promoted")
+
+ manifest.Blocks[0].NewRetentionStartedAt = &retentionStartedAt
+ manifest.Blocks[1].NewRetentionStartedAt = &retentionStartedAt
+ require.ErrorContains(t, validateRebuiltMetadata(manifest, "consolidated/clean.cscb.zstd"), "did not remain on single-block storage")
+
+ manifest.Blocks[1].NewRetentionStartedAt = nil
+ tooSoon := retentionStartedAt.Add(71*time.Hour + 59*time.Minute)
+ manifest.Blocks[0].NewSingleBlockDeleteAfter = &tooSoon
+ require.ErrorContains(t, validateRebuiltMetadata(manifest, "consolidated/clean.cscb.zstd"), "required 72-hour")
+}
+
+func validRepairCandidate() *Manifest {
+ return &Manifest{
+ Tag: 2,
+ OldConsolidatedObjectKey: "consolidated/dirty.cscb.zstd",
+ StartHeight: 1000,
+ EndHeight: 1001,
+ CanonicalBlockCount: 1,
+ TotalBlockCount: 1,
+ Blocks: []Block{{
+ BlockMetadataID: 1,
+ Canonical: true,
+ Tag: 2,
+ Height: 1000,
+ SingleBlockObjectKey: "single/1000.gzip",
+ OldConsolidatedObjectKey: "consolidated/dirty.cscb.zstd",
+ OldByteLength: 100,
+ OldUncompressedLength: 200,
+ }},
+ }
+}
+
+func singleBlockVersionCandidate() retirement.Candidate {
+ return retirement.Candidate{
+ Bucket: "repair-bucket",
+ Key: "single/1000.gzip",
+ Tag: 2,
+ Height: 1000,
+ Hash: "block-hash",
+ }
+}
+
+func singleBlockVersionPayload(t *testing.T, candidate retirement.Candidate, header string) []byte {
+ t.Helper()
+ payload, err := proto.Marshal(&api.Block{
+ Metadata: &api.BlockMetadata{
+ Tag: candidate.Tag,
+ Height: candidate.Height,
+ Hash: candidate.Hash,
+ },
+ Blobdata: &api.Block_Solana{
+ Solana: &api.SolanaBlobdata{Header: []byte(header)},
+ },
+ })
+ require.NoError(t, err)
+ compressed, err := storageutils.Compress(payload, api.Compression_GZIP)
+ require.NoError(t, err)
+ return compressed
+}
+
+type pendingRepairRepository struct {
+ Repository
+ pending *Manifest
+}
+
+type boundRepairRepository struct {
+ Repository
+ manifest *Manifest
+}
+
+func (r *boundRepairRepository) FindByExecutionKey(context.Context, string) (*Manifest, bool, error) {
+ return r.manifest, true, nil
+}
+
+type candidateListRepository struct {
+ Repository
+ objectKeys []string
+ limit int
+}
+
+type completedObjectRepository struct {
+ Repository
+ manifest *Manifest
+ executionKey string
+}
+
+func (r *completedObjectRepository) FindByExecutionKey(context.Context, string) (*Manifest, bool, error) {
+ return nil, false, nil
+}
+
+func (r *completedObjectRepository) FindByObjectKey(context.Context, uint32, string) (*Manifest, error) {
+ return r.manifest, nil
+}
+
+func (r *completedObjectRepository) BindExecutionKey(_ context.Context, executionKey string, repairID int64) (*Manifest, error) {
+ if repairID != r.manifest.ID {
+ return nil, errors.New("unexpected repair id")
+ }
+ r.executionKey = executionKey
+ return r.manifest, nil
+}
+
+func (r *candidateListRepository) ListCandidateObjectKeys(_ context.Context, _ uint32, _ uint64, _ uint64, limit int) ([]string, error) {
+ r.limit = limit
+ return r.objectKeys, nil
+}
+
+func (r *pendingRepairRepository) FindPending(context.Context, uint32, uint64, uint64) (*Manifest, error) {
+ return r.pending, nil
+}
+
+func (r *pendingRepairRepository) FindByExecutionKey(context.Context, string) (*Manifest, bool, error) {
+ return nil, false, nil
+}
+
+func executionKey(seed string) string {
+ value := make([]byte, 64)
+ for i := range value {
+ value[i] = "0123456789abcdef"[(i+len(seed))%16]
+ }
+ return string(value)
+}
+
+type pendingObjectStore struct{ retirement.ObjectStore }
+
+type versionedObjectStore struct {
+ retirement.ObjectStore
+ topology retirement.ObjectVersionTopology
+ objects map[string][]byte
+ readVersionIDs []string
+}
+
+func (s *versionedObjectStore) ListObjectVersions(
+ context.Context,
+ string,
+ string,
+) (retirement.ObjectVersionTopology, error) {
+ return s.topology, nil
+}
+
+func (s *versionedObjectStore) ReadObjectVersion(
+ _ context.Context,
+ _ string,
+ _ string,
+ versionID string,
+) ([]byte, error) {
+ s.readVersionIDs = append(s.readVersionIDs, versionID)
+ payload, ok := s.objects[versionID]
+ if !ok {
+ return nil, errors.New("object version not found")
+ }
+ return append([]byte(nil), payload...), nil
+}
+
+type preparationRepository struct {
+ Repository
+ candidate *Manifest
+ manifest *Manifest
+ executionKey string
+ fenced bool
+ bound bool
+ inspectionRecorded bool
+ alreadyClean bool
+ fenceCalls int
+ requestedObjectKey string
+}
+
+func (r *preparationRepository) FindByExecutionKey(_ context.Context, executionKey string) (*Manifest, bool, error) {
+ if r.bound && executionKey == r.executionKey {
+ return r.manifest, true, nil
+ }
+ return nil, false, nil
+}
+
+func (r *preparationRepository) FindPending(context.Context, uint32, uint64, uint64) (*Manifest, error) {
+ return nil, nil
+}
+
+func (r *preparationRepository) FindByObjectKey(_ context.Context, _ uint32, objectKey string) (*Manifest, error) {
+ r.requestedObjectKey = objectKey
+ return nil, nil
+}
+
+func (r *preparationRepository) FindNextCandidate(context.Context, uint32, uint64, uint64) (*Manifest, error) {
+ return r.candidate, nil
+}
+
+func (r *preparationRepository) FindCandidateByObjectKey(_ context.Context, _ uint32, objectKey string) (*Manifest, error) {
+ r.requestedObjectKey = objectKey
+ if r.candidate != nil && r.candidate.OldConsolidatedObjectKey == objectKey {
+ return r.candidate, nil
+ }
+ return nil, nil
+}
+
+func (r *preparationRepository) FenceCandidate(_ context.Context, manifest *Manifest) (*Manifest, error) {
+ r.fenceCalls++
+ r.fenced = true
+ copy := *manifest
+ copy.ID = 41
+ copy.Blocks = append([]Block(nil), manifest.Blocks...)
+ r.manifest = ©
+ return r.manifest, nil
+}
+
+func (r *preparationRepository) BindExecutionKey(_ context.Context, executionKey string, repairID int64) (*Manifest, error) {
+ if !r.fenced || r.manifest == nil || repairID != r.manifest.ID {
+ return nil, errors.New("execution bound before durable fence")
+ }
+ r.bound = true
+ r.executionKey = executionKey
+ return r.manifest, nil
+}
+
+func (r *preparationRepository) RecordInspection(
+ _ context.Context,
+ repairID int64,
+ oldObject ObjectVersion,
+ blocks []Block,
+ alreadyClean bool,
+) (*Manifest, error) {
+ if !r.bound || r.manifest == nil || repairID != r.manifest.ID {
+ return nil, errors.New("inspection recorded before durable execution binding")
+ }
+ r.inspectionRecorded = true
+ r.alreadyClean = alreadyClean
+ r.manifest.OldConsolidatedObjectVersion = oldObject
+ r.manifest.Blocks = append([]Block(nil), blocks...)
+ if alreadyClean {
+ now := time.Now().UTC()
+ r.manifest.State = StateCompleted
+ r.manifest.Outcome = alreadyCleanOutcome
+ r.manifest.VerifiedAt = &now
+ r.manifest.CompletedAt = &now
+ } else {
+ r.manifest.State = StatePrepared
+ }
+ return r.manifest, nil
+}
+
+func (r *preparationRepository) Get(context.Context, int64) (*Manifest, error) {
+ return r.manifest, nil
+}
+
+type preparationObjectStore struct {
+ retirement.ObjectStore
+ repository *preparationRepository
+ listErr error
+ listCalls int
+ topologies map[string]retirement.ObjectVersionTopology
+ objects map[string][]byte
+}
+
+func (s *preparationObjectStore) ListObjectVersions(
+ _ context.Context,
+ _ string,
+ key string,
+) (retirement.ObjectVersionTopology, error) {
+ s.listCalls++
+ if s.repository == nil || !s.repository.fenced || !s.repository.bound {
+ return retirement.ObjectVersionTopology{}, errors.New("S3 inspected before durable repair fence")
+ }
+ if s.listErr != nil {
+ return retirement.ObjectVersionTopology{}, s.listErr
+ }
+ return s.topologies[key], nil
+}
+
+func (s *preparationObjectStore) ReadObjectVersion(
+ _ context.Context,
+ _ string,
+ key string,
+ _ string,
+) ([]byte, error) {
+ object, ok := s.objects[key]
+ if !ok {
+ return nil, errors.New("object not found")
+ }
+ return object, nil
+}
+
+func (s *preparationObjectStore) ReadObjectVersionRange(
+ _ context.Context,
+ _ string,
+ key string,
+ _ string,
+ offset uint64,
+ length uint64,
+) ([]byte, error) {
+ object, ok := s.objects[key]
+ if !ok {
+ return nil, errors.New("object not found")
+ }
+ end := offset + length
+ if end < offset || offset > uint64(len(object)) {
+ return nil, errors.New("object range out of bounds")
+ }
+ if end > uint64(len(object)) {
+ end = uint64(len(object))
+ }
+ return append([]byte(nil), object[offset:end]...), nil
+}
+
+type completionRepository struct {
+ Repository
+ manifest *Manifest
+ completeErr error
+ completeCalled bool
+ committed bool
+}
+
+type phaseBoundaryRepository struct {
+ Repository
+ manifest *Manifest
+ restoreCalled bool
+ recordVerifiedCalled bool
+ completeCalled bool
+}
+
+func (r *phaseBoundaryRepository) Get(context.Context, int64) (*Manifest, error) {
+ return r.manifest, nil
+}
+
+func (r *phaseBoundaryRepository) GetRebuilt(context.Context, int64) (*Manifest, error) {
+ return r.manifest, nil
+}
+
+func (r *phaseBoundaryRepository) RestoreToSingleBlock(context.Context, int64) (*Manifest, error) {
+ r.restoreCalled = true
+ return r.manifest, nil
+}
+
+func (r *phaseBoundaryRepository) RecordVerified(context.Context, int64, string, ObjectVersion) (*Manifest, error) {
+ r.recordVerifiedCalled = true
+ return r.manifest, nil
+}
+
+func (r *phaseBoundaryRepository) CompleteRetainingOldObject(context.Context, int64, string) (*Manifest, error) {
+ r.completeCalled = true
+ return r.manifest, nil
+}
+
+func (r *completionRepository) Get(context.Context, int64) (*Manifest, error) {
+ return r.manifest, nil
+}
+
+func (r *completionRepository) GetRebuilt(context.Context, int64) (*Manifest, error) {
+ return r.manifest, nil
+}
+
+func (r *completionRepository) CompleteRetainingOldObject(
+ _ context.Context,
+ _ int64,
+ _ string,
+) (*Manifest, error) {
+ r.completeCalled = true
+ if r.completeErr != nil {
+ return nil, r.completeErr
+ }
+ r.committed = true
+ return r.manifest, nil
+}
+
+type completionObjectStore struct {
+ retirement.ObjectStore
+ topologies map[string]retirement.ObjectVersionTopology
+ objects map[string][]byte
+ readVersionIDs []string
+}
+
+func (s *completionObjectStore) ListObjectVersions(
+ _ context.Context,
+ _ string,
+ key string,
+) (retirement.ObjectVersionTopology, error) {
+ return s.topologies[key], nil
+}
+
+func (s *completionObjectStore) ReadObjectVersion(
+ _ context.Context,
+ _ string,
+ key string,
+ versionID string,
+) ([]byte, error) {
+ s.readVersionIDs = append(s.readVersionIDs, versionID)
+ return s.objects[key], nil
+}
+
+func (s *completionObjectStore) ReadObjectVersionRange(
+ _ context.Context,
+ _ string,
+ key string,
+ _ string,
+ offset uint64,
+ length uint64,
+) ([]byte, error) {
+ object, ok := s.objects[key]
+ if !ok {
+ return nil, errors.New("object not found")
+ }
+ end := offset + length
+ if end < offset || offset > uint64(len(object)) {
+ return nil, errors.New("object range out of bounds")
+ }
+ if end > uint64(len(object)) {
+ end = uint64(len(object))
+ }
+ return append([]byte(nil), object[offset:end]...), nil
+}
+
+func completionManifest(t *testing.T) *Manifest {
+ t.Helper()
+ block := Block{
+ BlockMetadataID: 1,
+ Canonical: false,
+ Tag: 2,
+ Height: 1000,
+ Hash: "block-hash",
+ SingleBlockObjectKey: "single/1000.gzip",
+ SingleBlockObjectVersion: ObjectVersion{VersionID: "single-version", ETag: "single-etag", Bytes: 100},
+ }
+ raw := &api.Block{Metadata: &api.BlockMetadata{Tag: block.Tag, Height: block.Height, Hash: block.Hash}}
+ normalized, err := (proto.MarshalOptions{Deterministic: true}).Marshal(storageutils.CloneBlockWithoutStoragePlacement(raw))
+ require.NoError(t, err)
+ digest := sha256.Sum256(normalized)
+ block.PayloadSHA256 = hex.EncodeToString(digest[:])
+ return &Manifest{
+ ID: 7,
+ State: StateRestored,
+ Bucket: "repair-bucket",
+ OldConsolidatedObjectKey: "consolidated/dirty.cscb.zstd",
+ OldConsolidatedObjectVersion: ObjectVersion{VersionID: "old-version", ETag: "old-etag", Bytes: 200},
+ CanonicalBlockCount: 0,
+ TotalBlockCount: 1,
+ Blocks: []Block{block},
+ }
+}
+
+func phaseBoundaryManifest(t *testing.T, state State) *Manifest {
+ t.Helper()
+ manifest := completionManifest(t)
+ manifest.State = state
+ manifest.CanonicalBlockCount = 1
+ manifest.NewConsolidatedObjectKey = "consolidated/clean.cscb.zstd"
+ manifest.NewConsolidatedObjectVersion = ObjectVersion{VersionID: "new-version", ETag: "new-etag", Bytes: 300}
+ manifest.Blocks[0].Canonical = true
+ manifest.Blocks[0].ActiveObjectKey = manifest.NewConsolidatedObjectKey
+ manifest.Blocks[0].NewConsolidatedObjectKey = manifest.NewConsolidatedObjectKey
+ return manifest
+}
+
+func completionSingleBlockPayload(t *testing.T, block Block) []byte {
+ t.Helper()
+ payload, err := proto.Marshal(&api.Block{Metadata: &api.BlockMetadata{
+ Tag: block.Tag,
+ Height: block.Height,
+ Hash: block.Hash,
+ }})
+ require.NoError(t, err)
+ compressed, err := storageutils.Compress(payload, api.Compression_GZIP)
+ require.NoError(t, err)
+ return compressed
+}
+
+func buildRepairSingleBlockCSCB(t *testing.T, candidate retirement.Candidate, payload []byte) []byte {
+ t.Helper()
+ compressed := gzipRepairPayload(t, payload)
+ envelope := make([]byte, cscb.EnvelopeHeaderSize+cscb.BlockIndexRecordSize+cscb.ChunkIndexRecordSize)
+ copy(envelope[0:4], []byte("ENV1"))
+ binary.LittleEndian.PutUint64(envelope[8:16], 1)
+ binary.LittleEndian.PutUint64(envelope[16:24], 1)
+ binary.LittleEndian.PutUint64(envelope[24:32], candidate.Height)
+ binary.LittleEndian.PutUint64(envelope[32:40], candidate.Height+1)
+ binary.LittleEndian.PutUint64(envelope[40:48], cscb.EnvelopeHeaderSize)
+ binary.LittleEndian.PutUint64(envelope[48:56], cscb.BlockIndexRecordSize)
+ chunkOffset := cscb.EnvelopeHeaderSize + cscb.BlockIndexRecordSize
+ binary.LittleEndian.PutUint64(envelope[56:64], uint64(chunkOffset))
+ binary.LittleEndian.PutUint64(envelope[64:72], cscb.ChunkIndexRecordSize)
+
+ blockRecord := envelope[cscb.EnvelopeHeaderSize:chunkOffset]
+ binary.LittleEndian.PutUint64(blockRecord[0:8], candidate.Height)
+ binary.LittleEndian.PutUint64(blockRecord[8:16], candidate.ByteOffset)
+ binary.LittleEndian.PutUint64(blockRecord[16:24], uint64(len(payload)))
+ binary.LittleEndian.PutUint32(blockRecord[24:28], crc32.ChecksumIEEE(payload))
+ hash := sha256.Sum256([]byte(candidate.Hash))
+ copy(blockRecord[48:80], hash[:])
+ binary.LittleEndian.PutUint64(blockRecord[80:88], uint64(candidate.BlockMetadataID))
+
+ payloadOffset := uint64(cscb.HeaderSize + len(envelope))
+ chunkRecord := envelope[chunkOffset:]
+ binary.LittleEndian.PutUint64(chunkRecord[8:16], candidate.Height)
+ binary.LittleEndian.PutUint64(chunkRecord[16:24], candidate.Height+1)
+ binary.LittleEndian.PutUint64(chunkRecord[24:32], payloadOffset)
+ binary.LittleEndian.PutUint64(chunkRecord[32:40], uint64(len(compressed)))
+ binary.LittleEndian.PutUint64(chunkRecord[48:56], uint64(len(payload)))
+ binary.LittleEndian.PutUint32(chunkRecord[56:60], crc32.ChecksumIEEE(payload))
+ binary.LittleEndian.PutUint32(chunkRecord[60:64], 1)
+
+ header := make([]byte, cscb.HeaderSize)
+ copy(header[0:4], []byte("CSCB"))
+ header[4] = 1
+ header[5] = 1
+ header[6] = 2
+ binary.LittleEndian.PutUint32(header[8:12], 1)
+ binary.LittleEndian.PutUint32(header[12:16], 1)
+ binary.LittleEndian.PutUint64(header[16:24], candidate.Height)
+ binary.LittleEndian.PutUint64(header[24:32], candidate.Height+1)
+ binary.LittleEndian.PutUint64(header[32:40], cscb.HeaderSize)
+ binary.LittleEndian.PutUint64(header[40:48], uint64(len(envelope)))
+ binary.LittleEndian.PutUint32(header[48:52], crc32.ChecksumIEEE(envelope))
+ binary.LittleEndian.PutUint32(header[52:56], cscb.BlockIndexRecordSize)
+ binary.LittleEndian.PutUint32(header[56:60], cscb.ChunkIndexRecordSize)
+ binary.LittleEndian.PutUint64(header[60:68], payloadOffset)
+ binary.LittleEndian.PutUint64(header[68:76], uint64(len(compressed)))
+ binary.LittleEndian.PutUint64(header[76:84], uint64(len(payload)))
+ binary.LittleEndian.PutUint32(header[84:88], crc32.ChecksumIEEE(payload))
+
+ object := make([]byte, 0, len(header)+len(envelope)+len(compressed))
+ object = append(object, header...)
+ object = append(object, envelope...)
+ object = append(object, compressed...)
+ return object
+}
+
+func gzipRepairPayload(t *testing.T, payload []byte) []byte {
+ t.Helper()
+ var buffer bytes.Buffer
+ writer := gzip.NewWriter(&buffer)
+ _, err := writer.Write(payload)
+ require.NoError(t, err)
+ require.NoError(t, writer.Close())
+ return buffer.Bytes()
+}
+
+func objectTopologyVersion(version ObjectVersion) retirement.ObjectVersion {
+ return retirement.ObjectVersion{
+ VersionID: version.VersionID,
+ ETag: version.ETag,
+ Bytes: version.Bytes,
+ IsLatest: true,
+ }
+}
diff --git a/internal/storage/cscbrepair/types.go b/internal/storage/cscbrepair/types.go
new file mode 100644
index 00000000..1f1c53a5
--- /dev/null
+++ b/internal/storage/cscbrepair/types.go
@@ -0,0 +1,134 @@
+package cscbrepair
+
+import (
+ "context"
+ "time"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ State string
+
+ Progress func(stage string, completed int, total int, height uint64)
+
+ ObjectVersion struct {
+ VersionID string
+ ETag string
+ Bytes uint64
+ }
+
+ PinnedPayload struct {
+ BlockMetadataID int64
+ Metadata *api.BlockMetadata
+ RawBlockPayload []byte
+ }
+
+ RebuiltPlacement struct {
+ BlockMetadataID int64
+ Height uint64
+ Hash string
+ ObjectFormat api.BlockObjectFormat
+ ByteOffset uint64
+ ByteLength uint64
+ UncompressedLength uint64
+ }
+
+ Block struct {
+ BlockMetadataID int64
+ Canonical bool
+ Tag uint32
+ Height uint64
+ Hash string
+ Skipped bool
+ RetirementFenced bool
+ RetirementManifestExists bool
+ SingleBlockObjectKey string
+ SingleBlockObjectKeySHA256 string
+ SingleBlockObjectDeleted bool
+ SingleBlockObjectVersion ObjectVersion
+ PayloadSHA256 string
+ OldConsolidatedObjectKey string
+ OldByteOffset uint64
+ OldByteLength uint64
+ OldUncompressedLength uint64
+ ActiveObjectKey string
+ ActiveObjectFormat int32
+ NewConsolidatedObjectKey string
+ NewByteOffset uint64
+ NewByteLength uint64
+ NewUncompressedLength uint64
+ NewValidatedAt *time.Time
+ NewRetentionStartedAt *time.Time
+ NewSingleBlockDeleteAfter *time.Time
+ }
+
+ Manifest struct {
+ ID int64
+ Tag uint32
+ State State
+ Bucket string
+ OldConsolidatedObjectKey string
+ OldConsolidatedObjectVersion ObjectVersion
+ StartHeight uint64
+ EndHeight uint64
+ CanonicalBlockCount uint64
+ TotalBlockCount uint64
+ RowSetSHA256 string
+ NewConsolidatedObjectKey string
+ NewConsolidatedObjectVersion ObjectVersion
+ Outcome string
+ PreparedAt time.Time
+ RestoredAt *time.Time
+ VerifiedAt *time.Time
+ CompletedAt *time.Time
+ Blocks []Block
+ }
+
+ Repository interface {
+ FindByExecutionKey(ctx context.Context, executionKey string) (*Manifest, bool, error)
+ FindPending(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) (*Manifest, error)
+ FindByObjectKey(ctx context.Context, tag uint32, objectKey string) (*Manifest, error)
+ FindNextCandidate(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) (*Manifest, error)
+ FindCandidateByObjectKey(ctx context.Context, tag uint32, objectKey string) (*Manifest, error)
+ ListCandidateObjectKeys(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, limit int) ([]string, error)
+ FenceCandidate(ctx context.Context, manifest *Manifest) (*Manifest, error)
+ RecordInspection(ctx context.Context, repairID int64, oldObject ObjectVersion, blocks []Block, alreadyClean bool) (*Manifest, error)
+ BindExecutionKey(ctx context.Context, executionKey string, repairID int64) (*Manifest, error)
+ BindNoCandidateExecution(ctx context.Context, executionKey string, tag uint32, startHeight uint64, endHeight uint64) (*Manifest, error)
+ Get(ctx context.Context, repairID int64) (*Manifest, error)
+ RestoreToSingleBlock(ctx context.Context, repairID int64) (*Manifest, error)
+ GetRebuilt(ctx context.Context, repairID int64) (*Manifest, error)
+ RecordVerified(ctx context.Context, repairID int64, objectKey string, object ObjectVersion) (*Manifest, error)
+ PromoteCompleted(ctx context.Context, repairID int64, objectKey string, object ObjectVersion, placements []RebuiltPlacement, singleBlockObjectRetention time.Duration) (*Manifest, error)
+ CompleteRetainingOldObject(ctx context.Context, repairID int64, outcome string) (*Manifest, error)
+ }
+
+ Repairer interface {
+ ListCandidates(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, limit int) ([]string, error)
+ PrepareNext(ctx context.Context, executionKey string, tag uint32, startHeight uint64, endHeight uint64, maxBlocks uint64, progress Progress) (*Manifest, error)
+ PrepareObject(ctx context.Context, executionKey string, tag uint32, startHeight uint64, endHeight uint64, maxBlocks uint64, objectKey string, progress Progress) (*Manifest, error)
+ Restore(ctx context.Context, repairID int64, progress Progress) (*Manifest, error)
+ Get(ctx context.Context, repairID int64) (*Manifest, error)
+ VerifyRebuilt(ctx context.Context, repairID int64, progress Progress) (*Manifest, error)
+ VisitPinnedPayloads(ctx context.Context, repairID int64, progress Progress, visit func(PinnedPayload) error) error
+ VerifyAndPromote(ctx context.Context, repairID int64, objectKey string, placements []RebuiltPlacement, singleBlockObjectRetention time.Duration, progress Progress) (*Manifest, error)
+ Complete(ctx context.Context, repairID int64, progress Progress) (*Manifest, error)
+ }
+)
+
+const (
+ StatePreparing State = "preparing"
+ StatePrepared State = "prepared"
+ StateRestored State = "restored"
+ StateVerified State = "verified"
+ StateCompleted State = "completed"
+
+ OutcomeAlreadyCleanStorageNeutral = "already_clean_storage_neutral"
+)
+
+func reportProgress(progress Progress, stage string, completed int, total int, height uint64) {
+ if progress != nil {
+ progress(stage, completed, total, height)
+ }
+}
diff --git a/internal/storage/cscbrepairlock/lock.go b/internal/storage/cscbrepairlock/lock.go
new file mode 100644
index 00000000..112ca4a4
--- /dev/null
+++ b/internal/storage/cscbrepairlock/lock.go
@@ -0,0 +1,39 @@
+package cscbrepairlock
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "sort"
+)
+
+// AcquireTag serializes repair transitions with metadata placement writers.
+// All callers must acquire this lock before locking block metadata or shadow rows.
+func AcquireTag(ctx context.Context, tx *sql.Tx, tag uint32) error {
+ key := fmt.Sprintf("cscb_repair_tag/%d", tag)
+ if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 2))`, key); err != nil {
+ return fmt.Errorf("failed to acquire CSCB repair tag lock: %w", err)
+ }
+ return nil
+}
+
+// AcquireTags locks tags in ascending order so transactions spanning multiple
+// tags cannot invert their advisory-lock order.
+func AcquireTags(ctx context.Context, tx *sql.Tx, tags []uint32) error {
+ unique := make(map[uint32]struct{}, len(tags))
+ ordered := make([]uint32, 0, len(tags))
+ for _, tag := range tags {
+ if _, ok := unique[tag]; ok {
+ continue
+ }
+ unique[tag] = struct{}{}
+ ordered = append(ordered, tag)
+ }
+ sort.Slice(ordered, func(i, j int) bool { return ordered[i] < ordered[j] })
+ for _, tag := range ordered {
+ if err := AcquireTag(ctx, tx, tag); err != nil {
+ return err
+ }
+ }
+ return nil
+}
diff --git a/internal/storage/metastorage/dynamodb/block_storage.go b/internal/storage/metastorage/dynamodb/block_storage.go
index a298fc75..f21e472b 100644
--- a/internal/storage/metastorage/dynamodb/block_storage.go
+++ b/internal/storage/metastorage/dynamodb/block_storage.go
@@ -5,9 +5,10 @@ import (
"fmt"
"reflect"
"sort"
+ "time"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/service/dynamodb"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"golang.org/x/xerrors"
"github.com/coinbase/chainstorage/internal/blockchain/parser"
@@ -42,7 +43,7 @@ type (
)
func newBlockStorage(params Params) (internal.BlockStorage, error) {
- attrDefs := []*dynamodb.AttributeDefinition{
+ attrDefs := []types.AttributeDefinition{
{
AttributeName: aws.String(blockPidKeyName),
AttributeType: awsStringType,
@@ -52,7 +53,7 @@ func newBlockStorage(params Params) (internal.BlockStorage, error) {
AttributeType: awsStringType,
},
}
- keySchema := []*dynamodb.KeySchemaElement{
+ keySchema := []types.KeySchemaElement{
{
AttributeName: aws.String(blockPidKeyName),
KeyType: hashKeyType,
@@ -62,7 +63,7 @@ func newBlockStorage(params Params) (internal.BlockStorage, error) {
KeyType: rangeKeyType,
},
}
- var globalSecondaryIndexes []*dynamodb.GlobalSecondaryIndex
+ var globalSecondaryIndexes []types.GlobalSecondaryIndex
metadataTable, err := newDDBTable(
params.Config.AWS.DynamoDB.BlockTable,
reflect.TypeOf(model.BlockMetaDataDDBEntry{}),
@@ -272,18 +273,67 @@ func (a *blockStorageImpl) validateHeight(height uint64) error {
return nil
}
+func (a *blockStorageImpl) GetBlockByTimestamp(ctx context.Context, tag uint32, timestamp uint64) (*api.BlockMetadata, error) {
+ // Placeholder implementation - return error indicating not implemented
+ return nil, xerrors.New("GetBlockByTimestamp not implemented for DynamoDB")
+}
+
+func (a *blockStorageImpl) GetBlockConsolidationShadow(ctx context.Context, block *api.BlockMetadata) (*api.BlockMetadata, error) {
+ return nil, xerrors.New("GetBlockConsolidationShadow not implemented for DynamoDB")
+}
+
+func (a *blockStorageImpl) GetBlocksConsolidationShadow(ctx context.Context, blocks []*api.BlockMetadata) ([]*api.BlockMetadata, error) {
+ return nil, xerrors.New("GetBlocksConsolidationShadow not implemented for DynamoDB")
+}
+
+func (a *blockStorageImpl) GetBlocksMissingConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64, limit uint64) ([]*internal.BlockMetadataRecord, error) {
+ return nil, xerrors.New("GetBlocksMissingConsolidationShadow not implemented for DynamoDB")
+}
+
+func (a *blockStorageImpl) GetBlockConsolidationShadowStats(ctx context.Context, tag uint32, startHeight, endHeight uint64) (*internal.ConsolidationShadowStats, error) {
+ return nil, xerrors.New("GetBlockConsolidationShadowStats not implemented for DynamoDB")
+}
+
+func (a *blockStorageImpl) GetFirstBlockMissingConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64) (uint64, bool, error) {
+ return 0, false, xerrors.New("GetFirstBlockMissingConsolidationShadow not implemented for DynamoDB")
+}
+
+func (a *blockStorageImpl) GetFirstPromotableBlockConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64) (uint64, bool, error) {
+ return 0, false, xerrors.New("GetFirstPromotableBlockConsolidationShadow not implemented for DynamoDB")
+}
+
+func (a *blockStorageImpl) GetBlockConsolidationCursor(ctx context.Context, name string, tag uint32) (uint64, bool, error) {
+ return 0, false, xerrors.New("GetBlockConsolidationCursor not implemented for DynamoDB")
+}
+
+func (a *blockStorageImpl) SetBlockConsolidationCursor(ctx context.Context, name string, tag uint32, height uint64) error {
+ return xerrors.New("SetBlockConsolidationCursor not implemented for DynamoDB")
+}
+
+func (a *blockStorageImpl) PersistBlockConsolidationShadows(ctx context.Context, placements []*internal.ConsolidationShadowPlacement) error {
+ return xerrors.New("PersistBlockConsolidationShadows not implemented for DynamoDB")
+}
+
+func (a *blockStorageImpl) PromoteBlockConsolidationShadows(ctx context.Context, tag uint32, startHeight, endHeight uint64, limit uint64, singleBlockObjectRetention time.Duration) (*internal.ConsolidationPromotionResult, error) {
+ return nil, xerrors.New("PromoteBlockConsolidationShadows not implemented for DynamoDB")
+}
+
func makeBlockMetaDataDDBEntry(block *api.BlockMetadata) *model.BlockMetaDataDDBEntry {
blockMetaDataDDBEntry := model.BlockMetaDataDDBEntry{
- BlockPid: getBlockPidForHeight(block.Tag, block.Height),
- BlockRid: getBlockRidWithBlockHash(block.Hash),
- Tag: block.Tag,
- Hash: block.Hash,
- ParentHash: block.ParentHash,
- Height: block.Height,
- ParentHeight: block.ParentHeight,
- ObjectKeyMain: block.ObjectKeyMain,
- Skipped: block.Skipped,
- Timestamp: block.GetTimestamp().GetSeconds(),
+ BlockPid: getBlockPidForHeight(block.Tag, block.Height),
+ BlockRid: getBlockRidWithBlockHash(block.Hash),
+ Tag: block.Tag,
+ Hash: block.Hash,
+ ParentHash: block.ParentHash,
+ Height: block.Height,
+ ParentHeight: block.ParentHeight,
+ ObjectKeyMain: block.ObjectKeyMain,
+ Skipped: block.Skipped,
+ Timestamp: block.GetTimestamp().GetSeconds(),
+ ObjectFormat: int32(block.GetObjectFormat()),
+ ByteOffset: block.GetByteOffset(),
+ ByteLength: block.GetByteLength(),
+ UncompressedLength: block.GetUncompressedLength(),
}
return &blockMetaDataDDBEntry
}
diff --git a/internal/storage/metastorage/dynamodb/block_storage_test.go b/internal/storage/metastorage/dynamodb/block_storage_test.go
new file mode 100644
index 00000000..4ea3cf8f
--- /dev/null
+++ b/internal/storage/metastorage/dynamodb/block_storage_test.go
@@ -0,0 +1,36 @@
+package dynamodb
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ ddbmodel "github.com/coinbase/chainstorage/internal/storage/metastorage/dynamodb/model"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestMakeBlockMetaDataDDBEntries_ConsolidatedFields(t *testing.T) {
+ block := testutil.MakeBlockMetadata(100, tag)
+ block.ObjectKeyMain = "consolidated/v=1/shard=00000000000000000000-00000000000000009999/00000000000000000100-00000000000000000199-deadbeef.cscb.zstd"
+ block.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH
+ block.ByteOffset = 4096
+ block.ByteLength = 8192
+ block.UncompressedLength = 8192
+
+ entries := (&blockStorageImpl{}).makeBlockMetaDataDDBEntries(true, []*api.BlockMetadata{block})
+
+ require.Len(t, entries, 3)
+ for _, entry := range entries {
+ blockEntry, ok := entry.(*ddbmodel.BlockMetaDataDDBEntry)
+ require.True(t, ok)
+ require.Equal(t, int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH), blockEntry.ObjectFormat)
+ require.Equal(t, uint64(4096), blockEntry.ByteOffset)
+ require.Equal(t, uint64(8192), blockEntry.ByteLength)
+ require.Equal(t, uint64(8192), blockEntry.UncompressedLength)
+ }
+ require.Equal(t, getBlockRidWithBlockHash(block.Hash), entries[0].(*ddbmodel.BlockMetaDataDDBEntry).BlockRid)
+ require.Equal(t, getCanonicalBlockRid(), entries[1].(*ddbmodel.BlockMetaDataDDBEntry).BlockRid)
+ require.Equal(t, getBlockPidForLatest(block.Tag), entries[2].(*ddbmodel.BlockMetaDataDDBEntry).BlockPid)
+ require.Equal(t, getCanonicalBlockRid(), entries[2].(*ddbmodel.BlockMetaDataDDBEntry).BlockRid)
+}
diff --git a/internal/storage/metastorage/dynamodb/ddb_table.go b/internal/storage/metastorage/dynamodb/ddb_table.go
index 6d75820b..07e3ba8a 100644
--- a/internal/storage/metastorage/dynamodb/ddb_table.go
+++ b/internal/storage/metastorage/dynamodb/ddb_table.go
@@ -2,28 +2,25 @@ package dynamodb
import (
"context"
+ "errors"
"reflect"
- "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
-
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/awserr"
- "github.com/aws/aws-sdk-go/aws/request"
- "github.com/aws/aws-sdk-go/aws/session"
- "github.com/aws/aws-sdk-go/service/dynamodb"
- "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"golang.org/x/xerrors"
- "github.com/coinbase/chainstorage/internal/storage/internal/errors"
+ storageerrors "github.com/coinbase/chainstorage/internal/storage/internal/errors"
"github.com/coinbase/chainstorage/internal/utils/retry"
"github.com/coinbase/chainstorage/internal/utils/syncgroup"
)
var (
- awsStringType = aws.String("S")
- awsNumberType = aws.String("N")
- hashKeyType = aws.String("HASH")
- rangeKeyType = aws.String("RANGE")
+ awsStringType = types.ScalarAttributeTypeS
+ awsNumberType = types.ScalarAttributeTypeN
+ hashKeyType = types.KeyTypeHash
+ rangeKeyType = types.KeyTypeRange
)
const (
@@ -48,8 +45,18 @@ type (
BatchWriteItems(ctx context.Context, items []any, parallelism int) error
}
- // DynamoAPI For mock generation for testing purpose
- DynamoAPI = dynamodbiface.DynamoDBAPI
+ // DynamoAPI interface for mock generation for testing purpose
+ DynamoAPI interface {
+ PutItem(ctx context.Context, params *dynamodb.PutItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error)
+ GetItem(ctx context.Context, params *dynamodb.GetItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error)
+ Query(ctx context.Context, params *dynamodb.QueryInput, optFns ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error)
+ TransactWriteItems(ctx context.Context, params *dynamodb.TransactWriteItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error)
+ TransactGetItems(ctx context.Context, params *dynamodb.TransactGetItemsInput, optFns ...func(*dynamodb.Options)) (*dynamodb.TransactGetItemsOutput, error)
+ BatchWriteItem(ctx context.Context, params *dynamodb.BatchWriteItemInput, optFns ...func(*dynamodb.Options)) (*dynamodb.BatchWriteItemOutput, error)
+ CreateTable(ctx context.Context, params *dynamodb.CreateTableInput, optFns ...func(*dynamodb.Options)) (*dynamodb.CreateTableOutput, error)
+ DeleteTable(ctx context.Context, params *dynamodb.DeleteTableInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DeleteTableOutput, error)
+ DescribeTable(ctx context.Context, params *dynamodb.DescribeTableInput, optFns ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error)
+ }
ddbTableImpl struct {
table *tableDBAPI
@@ -63,10 +70,10 @@ type (
}
QueryItemsRequest struct {
- ExclusiveStartKey map[string]*dynamodb.AttributeValue
+ ExclusiveStartKey map[string]types.AttributeValue
KeyConditionExpression *string
- ExpressionAttributeNames map[string]*string
- ExpressionAttributeValues map[string]*dynamodb.AttributeValue
+ ExpressionAttributeNames map[string]string
+ ExpressionAttributeValues map[string]types.AttributeValue
IndexName string
ConsistentRead bool
}
@@ -77,21 +84,19 @@ type (
func newDDBTable(
tableName string,
ddbEntryType reflect.Type,
- keySchema []*dynamodb.KeySchemaElement,
- attrDefs []*dynamodb.AttributeDefinition,
- globalSecondaryIndexes []*dynamodb.GlobalSecondaryIndex,
+ keySchema []types.KeySchemaElement,
+ attrDefs []types.AttributeDefinition,
+ globalSecondaryIndexes []types.GlobalSecondaryIndex,
params Params,
) (ddbTable, error) {
- //logger := log.WithPackage(params.Logger)
- retry := retry.New()
+ retryClient := retry.New()
- //TODO: enable assume-role if params.Config.AWS.DynamoDB.Arn is not empty
- awsTable := newTableAPI(tableName, params.Session)
+ awsTable := newTableAPI(tableName, params.AWSConfig)
table := ddbTableImpl{
table: awsTable,
ddbEntryType: ddbEntryType,
- retry: retry,
+ retry: retryClient,
}
if params.Config.AWS.IsLocalStack {
err := initLocalDb(
@@ -108,21 +113,21 @@ func newDDBTable(
return &table, nil
}
-func newTableAPI(tableName string, session *session.Session) *tableDBAPI {
+func newTableAPI(tableName string, cfg aws.Config) *tableDBAPI {
return &tableDBAPI{
TableName: tableName,
- DBAPI: dynamodb.New(session),
+ DBAPI: dynamodb.NewFromConfig(cfg),
}
}
func (d *ddbTableImpl) getTransactWriteItem(
- ddbEntry any) (*dynamodb.TransactWriteItem, error) {
- item, err := dynamodbattribute.MarshalMap(ddbEntry)
+ ddbEntry any) (*types.TransactWriteItem, error) {
+ item, err := attributevalue.MarshalMap(ddbEntry)
if err != nil {
return nil, xerrors.Errorf("failed to get marshal ddb entry (%v): %w", ddbEntry, err)
}
- writeItem := &dynamodb.TransactWriteItem{
- Put: &dynamodb.Put{
+ writeItem := &types.TransactWriteItem{
+ Put: &types.Put{
TableName: aws.String(d.table.TableName),
Item: item,
},
@@ -131,11 +136,11 @@ func (d *ddbTableImpl) getTransactWriteItem(
}
func (d *ddbTableImpl) WriteItem(ctx context.Context, item any) error {
- mItem, err := dynamodbattribute.MarshalMap(item)
+ mItem, err := attributevalue.MarshalMap(item)
if err != nil {
return xerrors.Errorf("failed to get marshal ddb entry (%v): %w", item, err)
}
- _, err = d.table.DBAPI.PutItemWithContext(
+ _, err = d.table.DBAPI.PutItem(
ctx,
&dynamodb.PutItemInput{
Item: mItem,
@@ -143,8 +148,8 @@ func (d *ddbTableImpl) WriteItem(ctx context.Context, item any) error {
},
)
if err != nil {
- if aerr, ok := err.(awserr.Error); ok && aerr.Code() == request.CanceledErrorCode {
- return errors.ErrRequestCanceled
+ if errors.Is(err, context.Canceled) {
+ return storageerrors.ErrRequestCanceled
}
return xerrors.Errorf("failed to write item: %w", err)
}
@@ -155,24 +160,25 @@ func (d *ddbTableImpl) TransactWriteItems(ctx context.Context, items []any) erro
if len(items) == 0 {
return nil
}
- batchWriteItems := make([]*dynamodb.TransactWriteItem, len(items))
+ batchWriteItems := make([]types.TransactWriteItem, len(items))
var err error
for i, item := range items {
- batchWriteItems[i], err = d.getTransactWriteItem(item)
+ writeItem, err := d.getTransactWriteItem(item)
if err != nil {
return xerrors.Errorf("failed to transact write items: %w", err)
}
+ batchWriteItems[i] = *writeItem
}
- _, err = d.table.DBAPI.TransactWriteItemsWithContext(
+ _, err = d.table.DBAPI.TransactWriteItems(
ctx,
&dynamodb.TransactWriteItemsInput{
TransactItems: batchWriteItems,
},
)
if err != nil {
- if aerr, ok := err.(awserr.Error); ok && aerr.Code() == request.CanceledErrorCode {
- return errors.ErrRequestCanceled
+ if errors.Is(err, context.Canceled) {
+ return storageerrors.ErrRequestCanceled
}
return xerrors.Errorf("failed to transact write items: %w", err)
}
@@ -207,14 +213,14 @@ func (d *ddbTableImpl) transactGetItems(ctx context.Context, inputKeys []StringM
if len(inputKeys) == 0 {
return nil
}
- inputItems := make([]*dynamodb.TransactGetItem, len(inputKeys))
+ inputItems := make([]types.TransactGetItem, len(inputKeys))
for i, keyMap := range inputKeys {
- dynamodbKey, err := dynamodbattribute.MarshalMap(keyMap)
+ dynamodbKey, err := attributevalue.MarshalMap(keyMap)
if err != nil {
return xerrors.Errorf("could not marshal given key(%v):%w", keyMap, err)
}
- inputItems[i] = &dynamodb.TransactGetItem{
- Get: &dynamodb.Get{
+ inputItems[i] = types.TransactGetItem{
+ Get: &types.Get{
Key: dynamodbKey,
TableName: aws.String(d.table.TableName),
},
@@ -222,23 +228,24 @@ func (d *ddbTableImpl) transactGetItems(ctx context.Context, inputKeys []StringM
}
return d.retry.Retry(ctx, func(ctx context.Context) error {
- output, err := d.table.DBAPI.TransactGetItemsWithContext(ctx, &dynamodb.TransactGetItemsInput{
+ output, err := d.table.DBAPI.TransactGetItems(ctx, &dynamodb.TransactGetItemsInput{
TransactItems: inputItems,
})
if err != nil {
- if transactionCanceledException, ok := err.(*dynamodb.TransactionCanceledException); ok {
+ var transactionCanceledException *types.TransactionCanceledException
+ if errors.As(err, &transactionCanceledException) {
reasons := transactionCanceledException.CancellationReasons
for _, reason := range reasons {
- if reason.Code != nil && *reason.Code == dynamodb.BatchStatementErrorCodeEnumTransactionConflict {
+ if reason.Code != nil && *reason.Code == "TransactionConflict" {
return retry.Retryable(
xerrors.Errorf("failed to TransactGetItems because of transaction conflict, reason=(%v)", reason))
}
}
}
- if aerr, ok := err.(awserr.Error); ok && aerr.Code() == request.CanceledErrorCode {
- return errors.ErrRequestCanceled
+ if errors.Is(err, context.Canceled) {
+ return storageerrors.ErrRequestCanceled
}
return err
}
@@ -247,9 +254,9 @@ func (d *ddbTableImpl) transactGetItems(ctx context.Context, inputKeys []StringM
// if missing then corresponding ItemResponse at same index will be empty
for index, item := range inputItems {
if len(output.Responses[index].Item) == 0 {
- return xerrors.Errorf("missing item key=%v: %w", item.Get.ProjectionExpression, errors.ErrItemNotFound)
+ return xerrors.Errorf("missing item key=%v: %w", item.Get.ProjectionExpression, storageerrors.ErrItemNotFound)
}
- err = dynamodbattribute.UnmarshalMap(output.Responses[index].Item, outputItems[index])
+ err = attributevalue.UnmarshalMap(output.Responses[index].Item, outputItems[index])
if err != nil {
return xerrors.Errorf("failed to unmarshal item (%v, %v): %w", output.Responses[index].Item, outputItems[index], err)
}
@@ -283,7 +290,7 @@ func (d *ddbTableImpl) GetItems(ctx context.Context,
}
func (d *ddbTableImpl) GetItem(ctx context.Context, keyMap StringMap) (any, error) {
- dynamodbKey, err := dynamodbattribute.MarshalMap(keyMap)
+ dynamodbKey, err := attributevalue.MarshalMap(keyMap)
if err != nil {
return nil, xerrors.Errorf("could not marshal given key(%v):%w", keyMap, err)
}
@@ -292,18 +299,18 @@ func (d *ddbTableImpl) GetItem(ctx context.Context, keyMap StringMap) (any, erro
TableName: aws.String(d.table.TableName),
ConsistentRead: aws.Bool(true),
}
- output, err := d.table.DBAPI.GetItemWithContext(ctx, input)
+ output, err := d.table.DBAPI.GetItem(ctx, input)
if err != nil {
- if aerr, ok := err.(awserr.Error); ok && aerr.Code() == request.CanceledErrorCode {
- return nil, errors.ErrRequestCanceled
+ if errors.Is(err, context.Canceled) {
+ return nil, storageerrors.ErrRequestCanceled
}
return nil, xerrors.Errorf("failed to get item for key (%v): %w", keyMap, err)
}
if output.Item == nil {
- return nil, errors.ErrItemNotFound
+ return nil, storageerrors.ErrItemNotFound
}
outputItem := reflect.New(d.ddbEntryType).Interface()
- err = dynamodbattribute.UnmarshalMap(output.Item, outputItem)
+ err = attributevalue.UnmarshalMap(output.Item, outputItem)
if err != nil {
return nil, xerrors.Errorf("failed to unmarshal item (%v): %w", output.Item, err)
}
@@ -314,7 +321,7 @@ func (d *ddbTableImpl) QueryItems(ctx context.Context, req *QueryItemsRequest) (
queryInput := &dynamodb.QueryInput{
ExclusiveStartKey: req.ExclusiveStartKey,
KeyConditionExpression: req.KeyConditionExpression,
- Select: aws.String(dynamodb.SelectAllAttributes),
+ Select: types.SelectAllAttributes,
ExpressionAttributeNames: req.ExpressionAttributeNames,
ExpressionAttributeValues: req.ExpressionAttributeValues,
TableName: aws.String(d.table.TableName),
@@ -328,21 +335,21 @@ func (d *ddbTableImpl) QueryItems(ctx context.Context, req *QueryItemsRequest) (
outputItems := make([]any, 0)
iterations := 0
for true {
- queryOutput, err := d.table.DBAPI.QueryWithContext(ctx, queryInput)
+ queryOutput, err := d.table.DBAPI.Query(ctx, queryInput)
if err != nil {
- if aerr, ok := err.(awserr.Error); ok && aerr.Code() == request.CanceledErrorCode {
- return nil, errors.ErrRequestCanceled
+ if errors.Is(err, context.Canceled) {
+ return nil, storageerrors.ErrRequestCanceled
}
return nil, xerrors.Errorf("failed to get query items (index=%v, keyConditionExpression=%v): %w", req.IndexName, req.KeyConditionExpression, err)
}
if len(queryOutput.Items) == 0 {
- return nil, errors.ErrItemNotFound
+ return nil, storageerrors.ErrItemNotFound
}
for _, item := range queryOutput.Items {
outputItem := reflect.New(d.ddbEntryType).Interface()
- err = dynamodbattribute.UnmarshalMap(item, outputItem)
+ err = attributevalue.UnmarshalMap(item, outputItem)
if err != nil {
return nil, xerrors.Errorf("failed to unmarshal item (%v): %w", item, err)
}
@@ -399,25 +406,25 @@ func (d *ddbTableImpl) batchWriteItemsWithLimit(ctx context.Context, items []any
return xerrors.Errorf("too many items: %v", numItems)
}
- writeRequests := make([]*dynamodb.WriteRequest, numItems)
+ writeRequests := make([]types.WriteRequest, numItems)
for i, item := range items {
writeRequest, err := d.getWriteRequest(item)
if err != nil {
return xerrors.Errorf("failed to prepare write items: %w", err)
}
- writeRequests[i] = writeRequest
+ writeRequests[i] = *writeRequest
}
tableName := d.table.TableName
numProcessed := 0
return d.retry.Retry(ctx, func(ctx context.Context) error {
input := &dynamodb.BatchWriteItemInput{
- RequestItems: map[string][]*dynamodb.WriteRequest{
+ RequestItems: map[string][]types.WriteRequest{
tableName: writeRequests,
},
}
- output, err := d.table.DBAPI.BatchWriteItemWithContext(ctx, input)
+ output, err := d.table.DBAPI.BatchWriteItem(ctx, input)
if err != nil {
return xerrors.Errorf("failed to batch write items: %w", err)
}
@@ -440,13 +447,13 @@ func (d *ddbTableImpl) batchWriteItemsWithLimit(ctx context.Context, items []any
}
func (d *ddbTableImpl) getWriteRequest(
- ddbEntry any) (*dynamodb.WriteRequest, error) {
- item, err := dynamodbattribute.MarshalMap(ddbEntry)
+ ddbEntry any) (*types.WriteRequest, error) {
+ item, err := attributevalue.MarshalMap(ddbEntry)
if err != nil {
return nil, xerrors.Errorf("failed to get marshal ddb entry (%v): %w", ddbEntry, err)
}
- writeRequest := &dynamodb.WriteRequest{
- PutRequest: &dynamodb.PutRequest{
+ writeRequest := &types.WriteRequest{
+ PutRequest: &types.PutRequest{
Item: item,
},
}
diff --git a/internal/storage/metastorage/dynamodb/ddb_table_test.go b/internal/storage/metastorage/dynamodb/ddb_table_test.go
index 64f66a65..71d2cd7a 100644
--- a/internal/storage/metastorage/dynamodb/ddb_table_test.go
+++ b/internal/storage/metastorage/dynamodb/ddb_table_test.go
@@ -9,20 +9,16 @@ import (
"sync"
"testing"
- "github.com/aws/aws-sdk-go/aws/awserr"
- "golang.org/x/xerrors"
-
- "github.com/coinbase/chainstorage/internal/storage/internal/errors"
-
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/service/dynamodb"
- "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbattribute"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
+ "github.com/aws/smithy-go"
"github.com/stretchr/testify/suite"
"go.uber.org/mock/gomock"
+ "golang.org/x/xerrors"
- awsrequest "github.com/aws/aws-sdk-go/aws/request"
-
- "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
+ "github.com/coinbase/chainstorage/internal/storage/internal/errors"
dynamodbmocks "github.com/coinbase/chainstorage/internal/storage/metastorage/dynamodb/mocks"
"github.com/coinbase/chainstorage/internal/storage/metastorage/dynamodb/model"
"github.com/coinbase/chainstorage/internal/utils/retry"
@@ -79,24 +75,24 @@ func (s *DDBTableTestSuite) TestGetItems_TransactionConflict_RetrySuccess() {
seen := sync.Map{}
attempts := 0
- s.dynamoAPI.EXPECT().TransactGetItemsWithContext(gomock.Any(), gomock.Any()).
- DoAndReturn(func(ctx context.Context, input *dynamodb.TransactGetItemsInput, opts ...jsonrpc.Option) (
+ s.dynamoAPI.EXPECT().TransactGetItems(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, input *dynamodb.TransactGetItemsInput, opts ...func(*dynamodb.Options)) (
*dynamodb.TransactGetItemsOutput, error) {
attempts += 1
if attempts == 1 {
require.Equal(numItems, len(input.TransactItems))
- mockCancelReasons := make([]*dynamodb.CancellationReason, numItems)
+ mockCancelReasons := make([]types.CancellationReason, numItems)
for i := range mockCancelReasons {
- mockCancelReasons[i] = &dynamodb.CancellationReason{
+ mockCancelReasons[i] = types.CancellationReason{
Code: aws.String("None"),
}
}
- mockCancelReasons[1] = &dynamodb.CancellationReason{
+ mockCancelReasons[1] = types.CancellationReason{
Code: aws.String("TransactionConflict"),
Message: aws.String("Transaction is ongoing for the item"),
}
- return &dynamodb.TransactGetItemsOutput{}, &dynamodb.TransactionCanceledException{
+ return &dynamodb.TransactGetItemsOutput{}, &types.TransactionCanceledException{
CancellationReasons: mockCancelReasons,
}
}
@@ -107,7 +103,7 @@ func (s *DDBTableTestSuite) TestGetItems_TransactionConflict_RetrySuccess() {
for _, item := range transactItems {
var out StringMap
require.Equal(tableName, *item.Get.TableName)
- err := dynamodbattribute.UnmarshalMap(item.Get.Key, &out)
+ err := attributevalue.UnmarshalMap(item.Get.Key, &out)
require.NoError(err)
require.NotEmpty(out["pk"])
require.NotEmpty(out["sk"])
@@ -126,9 +122,9 @@ func (s *DDBTableTestSuite) TestGetItems_TransactionConflict_RetrySuccess() {
}
}
- responses := make([]*dynamodb.ItemResponse, end+1-begin)
+ responses := make([]types.ItemResponse, end+1-begin)
for i := begin; i <= end; i++ {
- responses[i-begin] = &dynamodb.ItemResponse{Item: attributesMap[i]}
+ responses[i-begin] = types.ItemResponse{Item: attributesMap[i]}
}
return &dynamodb.TransactGetItemsOutput{
Responses: responses,
@@ -152,22 +148,22 @@ func (s *DDBTableTestSuite) TestGetItems_TransactionConflict_RetryFailure() {
ctx := context.Background()
keyMaps := makeKeyMapsForTestDDBEntries(numItems)
- s.dynamoAPI.EXPECT().TransactGetItemsWithContext(gomock.Any(), gomock.Any()).
- DoAndReturn(func(ctx context.Context, input *dynamodb.TransactGetItemsInput, opts ...jsonrpc.Option) (
+ s.dynamoAPI.EXPECT().TransactGetItems(gomock.Any(), gomock.Any()).
+ DoAndReturn(func(ctx context.Context, input *dynamodb.TransactGetItemsInput, opts ...func(*dynamodb.Options)) (
*dynamodb.TransactGetItemsOutput, error) {
require.Equal(numItems, len(input.TransactItems))
- mockCancelReasons := make([]*dynamodb.CancellationReason, numItems)
+ mockCancelReasons := make([]types.CancellationReason, numItems)
for i := range mockCancelReasons {
- mockCancelReasons[i] = &dynamodb.CancellationReason{
+ mockCancelReasons[i] = types.CancellationReason{
Code: aws.String("None"),
}
}
- mockCancelReasons[1] = &dynamodb.CancellationReason{
+ mockCancelReasons[1] = types.CancellationReason{
Code: aws.String("TransactionConflict"),
Message: aws.String("Transaction is ongoing for the item"),
}
- return &dynamodb.TransactGetItemsOutput{}, &dynamodb.TransactionCanceledException{
+ return &dynamodb.TransactGetItemsOutput{}, &types.TransactionCanceledException{
CancellationReasons: mockCancelReasons,
}
}).AnyTimes()
@@ -178,10 +174,8 @@ func (s *DDBTableTestSuite) TestGetItems_TransactionConflict_RetryFailure() {
func (s *DDBTableTestSuite) TestQueryItem_RequestCanceledFailure() {
require := testutil.Require(s.T())
- s.dynamoAPI.EXPECT().QueryWithContext(gomock.Any(), gomock.Any()).
- Return(nil, awserr.New(awsrequest.CanceledErrorCode,
- "canceled",
- nil))
+ s.dynamoAPI.EXPECT().Query(gomock.Any(), gomock.Any()).
+ Return(nil, &smithy.CanceledError{Err: context.Canceled})
queryResult, err := s.table.QueryItems(context.Background(), &QueryItemsRequest{})
@@ -193,10 +187,8 @@ func (s *DDBTableTestSuite) TestQueryItem_RequestCanceledFailure() {
func (s *DDBTableTestSuite) TestQueryItem_QueryFailure() {
require := testutil.Require(s.T())
- s.dynamoAPI.EXPECT().QueryWithContext(gomock.Any(), gomock.Any()).
- Return(nil, awserr.New(awsrequest.ErrCodeRequestError,
- "Validation error",
- nil))
+ s.dynamoAPI.EXPECT().Query(gomock.Any(), gomock.Any()).
+ Return(nil, &smithy.GenericAPIError{Code: "ValidationException", Message: "Validation error"})
queryResult, err := s.table.QueryItems(context.Background(), &QueryItemsRequest{})
@@ -207,7 +199,7 @@ func (s *DDBTableTestSuite) TestQueryItem_QueryFailure() {
func (s *DDBTableTestSuite) TestQueryItem_ItemNotFoundErr() {
require := testutil.Require(s.T())
- s.dynamoAPI.EXPECT().QueryWithContext(gomock.Any(), gomock.Any()).
+ s.dynamoAPI.EXPECT().Query(gomock.Any(), gomock.Any()).
Return(&dynamodb.QueryOutput{}, nil)
updateResult, err := s.table.QueryItems(context.Background(), &QueryItemsRequest{})
@@ -219,10 +211,10 @@ func (s *DDBTableTestSuite) TestQueryItem_ItemNotFoundErr() {
func testDDBEntriesToAttributeMaps(
entries []any,
-) ([]map[string]*dynamodb.AttributeValue, error) {
- attributes := make([]map[string]*dynamodb.AttributeValue, len(entries))
+) ([]map[string]types.AttributeValue, error) {
+ attributes := make([]map[string]types.AttributeValue, len(entries))
for i := range entries {
- attribute, err := dynamodbattribute.MarshalMap(entries[i])
+ attribute, err := attributevalue.MarshalMap(entries[i])
if err != nil {
return nil, err
}
diff --git a/internal/storage/metastorage/dynamodb/event_storage.go b/internal/storage/metastorage/dynamodb/event_storage.go
index 20783249..d2ffb875 100644
--- a/internal/storage/metastorage/dynamodb/event_storage.go
+++ b/internal/storage/metastorage/dynamodb/event_storage.go
@@ -7,10 +7,10 @@ import (
"reflect"
"strings"
- "github.com/aws/aws-sdk-go/service/dynamodb/expression"
+ "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/service/dynamodb"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"golang.org/x/xerrors"
"github.com/coinbase/chainstorage/internal/storage/internal/errors"
@@ -96,7 +96,7 @@ func newEventStorage(params Params) (internal.EventStorage, error) {
func createEventTable(params Params) (ddbTable, error) {
heightIndexName := params.Config.AWS.DynamoDB.EventTableHeightIndex
- attrDefs := []*dynamodb.AttributeDefinition{
+ attrDefs := []types.AttributeDefinition{
{
AttributeName: aws.String(eventIdKeyName),
AttributeType: awsNumberType,
@@ -106,25 +106,25 @@ func createEventTable(params Params) (ddbTable, error) {
AttributeType: awsNumberType,
},
}
- keySchema := []*dynamodb.KeySchemaElement{
+ keySchema := []types.KeySchemaElement{
{
AttributeName: aws.String(eventIdKeyName),
KeyType: hashKeyType,
},
}
- globalSecondaryIndexes := []*dynamodb.GlobalSecondaryIndex{
+ globalSecondaryIndexes := []types.GlobalSecondaryIndex{
{
IndexName: aws.String(heightIndexName),
- KeySchema: []*dynamodb.KeySchemaElement{
+ KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String(heightKeyName),
KeyType: hashKeyType,
},
},
- Projection: &dynamodb.Projection{
- ProjectionType: aws.String(dynamodb.ProjectionTypeAll),
+ Projection: &types.Projection{
+ ProjectionType: types.ProjectionTypeAll,
},
- ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
+ ProvisionedThroughput: &types.ProvisionedThroughput{
ReadCapacityUnits: aws.Int64(1),
WriteCapacityUnits: aws.Int64(1),
},
@@ -150,7 +150,7 @@ func createVersionedEventTable(params Params) (ddbTable, error) {
}
versionedEventBlockIndexName := params.Config.AWS.DynamoDB.VersionedEventTableBlockIndex
- attrDefs := []*dynamodb.AttributeDefinition{
+ attrDefs := []types.AttributeDefinition{
{
AttributeName: aws.String(eventIdKeyName),
AttributeType: awsStringType,
@@ -160,25 +160,25 @@ func createVersionedEventTable(params Params) (ddbTable, error) {
AttributeType: awsStringType,
},
}
- keySchema := []*dynamodb.KeySchemaElement{
+ keySchema := []types.KeySchemaElement{
{
AttributeName: aws.String(eventIdKeyName),
KeyType: hashKeyType,
},
}
- globalSecondaryIndexes := []*dynamodb.GlobalSecondaryIndex{
+ globalSecondaryIndexes := []types.GlobalSecondaryIndex{
{
IndexName: aws.String(versionedEventBlockIndexName),
- KeySchema: []*dynamodb.KeySchemaElement{
+ KeySchema: []types.KeySchemaElement{
{
AttributeName: aws.String(blockIdKeyName),
KeyType: hashKeyType,
},
},
- Projection: &dynamodb.Projection{
- ProjectionType: aws.String(dynamodb.ProjectionTypeAll),
+ Projection: &types.Projection{
+ ProjectionType: types.ProjectionTypeAll,
},
- ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
+ ProvisionedThroughput: &types.ProvisionedThroughput{
ReadCapacityUnits: aws.Int64(1),
WriteCapacityUnits: aws.Int64(1),
},
diff --git a/internal/storage/metastorage/dynamodb/meta_storage.go b/internal/storage/metastorage/dynamodb/meta_storage.go
index 6e031d57..b79fee14 100644
--- a/internal/storage/metastorage/dynamodb/meta_storage.go
+++ b/internal/storage/metastorage/dynamodb/meta_storage.go
@@ -1,7 +1,9 @@
package dynamodb
import (
- "github.com/aws/aws-sdk-go/aws/session"
+ "context"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
"golang.org/x/xerrors"
"go.uber.org/fx"
@@ -10,6 +12,16 @@ import (
"github.com/coinbase/chainstorage/internal/utils/fxparams"
)
+func (m *metaStorageImpl) AcquireSingleBlockUploadGuard(
+ ctx context.Context,
+ tag uint32,
+ height uint64,
+ hash string,
+) (internal.SingleBlockUploadGuard, error) {
+ // Durable retirement is Postgres-only, so DynamoDB has no retirement fence.
+ return internal.NewUnfencedSingleBlockUploadGuard(), nil
+}
+
type (
metaStorageImpl struct {
internal.BlockStorage
@@ -20,7 +32,7 @@ type (
Params struct {
fx.In
fxparams.Params
- Session *session.Session
+ AWSConfig aws.Config
}
metaStorageFactory struct {
diff --git a/internal/storage/metastorage/dynamodb/meta_storage_local.go b/internal/storage/metastorage/dynamodb/meta_storage_local.go
index ba6dbfba..6ea3c723 100644
--- a/internal/storage/metastorage/dynamodb/meta_storage_local.go
+++ b/internal/storage/metastorage/dynamodb/meta_storage_local.go
@@ -1,26 +1,29 @@
package dynamodb
import (
+ "context"
+ "errors"
+
"go.uber.org/zap"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/aws/awserr"
- "github.com/aws/aws-sdk-go/service/dynamodb"
- "github.com/aws/aws-sdk-go/service/dynamodb/dynamodbiface"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)
func initLocalDb(
- db dynamodbiface.DynamoDBAPI,
+ db DynamoAPI,
log *zap.Logger,
tableName string,
- keySchema []*dynamodb.KeySchemaElement,
- attrDefs []*dynamodb.AttributeDefinition,
- globalSecondaryIndexes []*dynamodb.GlobalSecondaryIndex,
+ keySchema []types.KeySchemaElement,
+ attrDefs []types.AttributeDefinition,
+ globalSecondaryIndexes []types.GlobalSecondaryIndex,
reset bool,
) error {
log.Debug("Initializing local db")
+ ctx := context.Background()
- exists, err := doesTableExist(db, tableName)
+ exists, err := doesTableExist(ctx, db, tableName)
if err != nil {
log.Info("Failed to check if the table exists in local db or not")
return err
@@ -32,22 +35,22 @@ func initLocalDb(
}
log.Info("Table already exists in local db, so dropping it first.")
- if err = deleteTable(db, log, tableName); err != nil {
+ if err = deleteTable(ctx, db, log, tableName); err != nil {
return err
}
- return createTable(db, log, tableName, keySchema, attrDefs, globalSecondaryIndexes)
+ return createTable(ctx, db, log, tableName, keySchema, attrDefs, globalSecondaryIndexes)
}
log.Info("Table does not exist.")
- return createTable(db, log, tableName, keySchema, attrDefs, globalSecondaryIndexes)
+ return createTable(ctx, db, log, tableName, keySchema, attrDefs, globalSecondaryIndexes)
}
-func createTable(db dynamodbiface.DynamoDBAPI, log *zap.Logger,
+func createTable(ctx context.Context, db DynamoAPI, log *zap.Logger,
tableName string,
- keySchema []*dynamodb.KeySchemaElement,
- attrDefs []*dynamodb.AttributeDefinition,
- globalSecondaryIndexes []*dynamodb.GlobalSecondaryIndex,
+ keySchema []types.KeySchemaElement,
+ attrDefs []types.AttributeDefinition,
+ globalSecondaryIndexes []types.GlobalSecondaryIndex,
) error {
log.Info("Creating table " + tableName)
@@ -55,40 +58,39 @@ func createTable(db dynamodbiface.DynamoDBAPI, log *zap.Logger,
TableName: aws.String(tableName),
AttributeDefinitions: attrDefs,
KeySchema: keySchema,
- ProvisionedThroughput: &dynamodb.ProvisionedThroughput{
+ ProvisionedThroughput: &types.ProvisionedThroughput{
ReadCapacityUnits: aws.Int64(1),
WriteCapacityUnits: aws.Int64(1),
},
GlobalSecondaryIndexes: globalSecondaryIndexes,
}
- _, err := db.CreateTable(input)
+ _, err := db.CreateTable(ctx, input)
return err
}
-func deleteTable(db dynamodbiface.DynamoDBAPI, log *zap.Logger, tableName string) error {
+func deleteTable(ctx context.Context, db DynamoAPI, log *zap.Logger, tableName string) error {
log.Info("Deleting table " + tableName)
input := &dynamodb.DeleteTableInput{
TableName: aws.String(tableName),
}
- _, err := db.DeleteTable(input)
+ _, err := db.DeleteTable(ctx, input)
if err == nil {
return nil
}
- if aerr, ok := err.(awserr.Error); ok {
- if aerr.Code() == dynamodb.ErrCodeResourceNotFoundException {
- return nil
- }
+ var resourceNotFoundErr *types.ResourceNotFoundException
+ if errors.As(err, &resourceNotFoundErr) {
+ return nil
}
return err
}
-func doesTableExist(db dynamodbiface.DynamoDBAPI, tableName string) (exists bool, err error) {
- _, err = db.DescribeTable(&dynamodb.DescribeTableInput{
+func doesTableExist(ctx context.Context, db DynamoAPI, tableName string) (exists bool, err error) {
+ _, err = db.DescribeTable(ctx, &dynamodb.DescribeTableInput{
TableName: aws.String(tableName),
})
@@ -96,10 +98,9 @@ func doesTableExist(db dynamodbiface.DynamoDBAPI, tableName string) (exists bool
return true, nil
}
- if aerr, ok := err.(awserr.Error); ok {
- if aerr.Code() == dynamodb.ErrCodeResourceNotFoundException {
- return false, nil
- }
+ var resourceNotFoundErr *types.ResourceNotFoundException
+ if errors.As(err, &resourceNotFoundErr) {
+ return false, nil
}
return false, err
diff --git a/internal/storage/metastorage/dynamodb/mocks/mocks.go b/internal/storage/metastorage/dynamodb/mocks/mocks.go
index a5a1ec83..b0b8e487 100644
--- a/internal/storage/metastorage/dynamodb/mocks/mocks.go
+++ b/internal/storage/metastorage/dynamodb/mocks/mocks.go
@@ -13,8 +13,7 @@ import (
context "context"
reflect "reflect"
- request "github.com/aws/aws-sdk-go/aws/request"
- dynamodb "github.com/aws/aws-sdk-go/service/dynamodb"
+ dynamodb "github.com/aws/aws-sdk-go-v2/service/dynamodb"
gomock "go.uber.org/mock/gomock"
)
@@ -41,2999 +40,182 @@ func (m *MockDynamoAPI) EXPECT() *MockDynamoAPIMockRecorder {
return m.recorder
}
-// BatchExecuteStatement mocks base method.
-func (m *MockDynamoAPI) BatchExecuteStatement(arg0 *dynamodb.BatchExecuteStatementInput) (*dynamodb.BatchExecuteStatementOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "BatchExecuteStatement", arg0)
- ret0, _ := ret[0].(*dynamodb.BatchExecuteStatementOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// BatchExecuteStatement indicates an expected call of BatchExecuteStatement.
-func (mr *MockDynamoAPIMockRecorder) BatchExecuteStatement(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchExecuteStatement", reflect.TypeOf((*MockDynamoAPI)(nil).BatchExecuteStatement), arg0)
-}
-
-// BatchExecuteStatementRequest mocks base method.
-func (m *MockDynamoAPI) BatchExecuteStatementRequest(arg0 *dynamodb.BatchExecuteStatementInput) (*request.Request, *dynamodb.BatchExecuteStatementOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "BatchExecuteStatementRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.BatchExecuteStatementOutput)
- return ret0, ret1
-}
-
-// BatchExecuteStatementRequest indicates an expected call of BatchExecuteStatementRequest.
-func (mr *MockDynamoAPIMockRecorder) BatchExecuteStatementRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchExecuteStatementRequest", reflect.TypeOf((*MockDynamoAPI)(nil).BatchExecuteStatementRequest), arg0)
-}
-
-// BatchExecuteStatementWithContext mocks base method.
-func (m *MockDynamoAPI) BatchExecuteStatementWithContext(arg0 context.Context, arg1 *dynamodb.BatchExecuteStatementInput, arg2 ...request.Option) (*dynamodb.BatchExecuteStatementOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "BatchExecuteStatementWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.BatchExecuteStatementOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// BatchExecuteStatementWithContext indicates an expected call of BatchExecuteStatementWithContext.
-func (mr *MockDynamoAPIMockRecorder) BatchExecuteStatementWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchExecuteStatementWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).BatchExecuteStatementWithContext), varargs...)
-}
-
-// BatchGetItem mocks base method.
-func (m *MockDynamoAPI) BatchGetItem(arg0 *dynamodb.BatchGetItemInput) (*dynamodb.BatchGetItemOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "BatchGetItem", arg0)
- ret0, _ := ret[0].(*dynamodb.BatchGetItemOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// BatchGetItem indicates an expected call of BatchGetItem.
-func (mr *MockDynamoAPIMockRecorder) BatchGetItem(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetItem", reflect.TypeOf((*MockDynamoAPI)(nil).BatchGetItem), arg0)
-}
-
-// BatchGetItemPages mocks base method.
-func (m *MockDynamoAPI) BatchGetItemPages(arg0 *dynamodb.BatchGetItemInput, arg1 func(*dynamodb.BatchGetItemOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "BatchGetItemPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// BatchGetItemPages indicates an expected call of BatchGetItemPages.
-func (mr *MockDynamoAPIMockRecorder) BatchGetItemPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetItemPages", reflect.TypeOf((*MockDynamoAPI)(nil).BatchGetItemPages), arg0, arg1)
-}
-
-// BatchGetItemPagesWithContext mocks base method.
-func (m *MockDynamoAPI) BatchGetItemPagesWithContext(arg0 context.Context, arg1 *dynamodb.BatchGetItemInput, arg2 func(*dynamodb.BatchGetItemOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "BatchGetItemPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// BatchGetItemPagesWithContext indicates an expected call of BatchGetItemPagesWithContext.
-func (mr *MockDynamoAPIMockRecorder) BatchGetItemPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetItemPagesWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).BatchGetItemPagesWithContext), varargs...)
-}
-
-// BatchGetItemRequest mocks base method.
-func (m *MockDynamoAPI) BatchGetItemRequest(arg0 *dynamodb.BatchGetItemInput) (*request.Request, *dynamodb.BatchGetItemOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "BatchGetItemRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.BatchGetItemOutput)
- return ret0, ret1
-}
-
-// BatchGetItemRequest indicates an expected call of BatchGetItemRequest.
-func (mr *MockDynamoAPIMockRecorder) BatchGetItemRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetItemRequest", reflect.TypeOf((*MockDynamoAPI)(nil).BatchGetItemRequest), arg0)
-}
-
-// BatchGetItemWithContext mocks base method.
-func (m *MockDynamoAPI) BatchGetItemWithContext(arg0 context.Context, arg1 *dynamodb.BatchGetItemInput, arg2 ...request.Option) (*dynamodb.BatchGetItemOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "BatchGetItemWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.BatchGetItemOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// BatchGetItemWithContext indicates an expected call of BatchGetItemWithContext.
-func (mr *MockDynamoAPIMockRecorder) BatchGetItemWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchGetItemWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).BatchGetItemWithContext), varargs...)
-}
-
// BatchWriteItem mocks base method.
-func (m *MockDynamoAPI) BatchWriteItem(arg0 *dynamodb.BatchWriteItemInput) (*dynamodb.BatchWriteItemOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "BatchWriteItem", arg0)
- ret0, _ := ret[0].(*dynamodb.BatchWriteItemOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// BatchWriteItem indicates an expected call of BatchWriteItem.
-func (mr *MockDynamoAPIMockRecorder) BatchWriteItem(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchWriteItem", reflect.TypeOf((*MockDynamoAPI)(nil).BatchWriteItem), arg0)
-}
-
-// BatchWriteItemRequest mocks base method.
-func (m *MockDynamoAPI) BatchWriteItemRequest(arg0 *dynamodb.BatchWriteItemInput) (*request.Request, *dynamodb.BatchWriteItemOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "BatchWriteItemRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.BatchWriteItemOutput)
- return ret0, ret1
-}
-
-// BatchWriteItemRequest indicates an expected call of BatchWriteItemRequest.
-func (mr *MockDynamoAPIMockRecorder) BatchWriteItemRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchWriteItemRequest", reflect.TypeOf((*MockDynamoAPI)(nil).BatchWriteItemRequest), arg0)
-}
-
-// BatchWriteItemWithContext mocks base method.
-func (m *MockDynamoAPI) BatchWriteItemWithContext(arg0 context.Context, arg1 *dynamodb.BatchWriteItemInput, arg2 ...request.Option) (*dynamodb.BatchWriteItemOutput, error) {
+func (m *MockDynamoAPI) BatchWriteItem(arg0 context.Context, arg1 *dynamodb.BatchWriteItemInput, arg2 ...func(*dynamodb.Options)) (*dynamodb.BatchWriteItemOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "BatchWriteItemWithContext", varargs...)
+ ret := m.ctrl.Call(m, "BatchWriteItem", varargs...)
ret0, _ := ret[0].(*dynamodb.BatchWriteItemOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// BatchWriteItemWithContext indicates an expected call of BatchWriteItemWithContext.
-func (mr *MockDynamoAPIMockRecorder) BatchWriteItemWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// BatchWriteItem indicates an expected call of BatchWriteItem.
+func (mr *MockDynamoAPIMockRecorder) BatchWriteItem(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchWriteItemWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).BatchWriteItemWithContext), varargs...)
-}
-
-// CreateBackup mocks base method.
-func (m *MockDynamoAPI) CreateBackup(arg0 *dynamodb.CreateBackupInput) (*dynamodb.CreateBackupOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateBackup", arg0)
- ret0, _ := ret[0].(*dynamodb.CreateBackupOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// CreateBackup indicates an expected call of CreateBackup.
-func (mr *MockDynamoAPIMockRecorder) CreateBackup(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBackup", reflect.TypeOf((*MockDynamoAPI)(nil).CreateBackup), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BatchWriteItem", reflect.TypeOf((*MockDynamoAPI)(nil).BatchWriteItem), varargs...)
}
-// CreateBackupRequest mocks base method.
-func (m *MockDynamoAPI) CreateBackupRequest(arg0 *dynamodb.CreateBackupInput) (*request.Request, *dynamodb.CreateBackupOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateBackupRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.CreateBackupOutput)
- return ret0, ret1
-}
-
-// CreateBackupRequest indicates an expected call of CreateBackupRequest.
-func (mr *MockDynamoAPIMockRecorder) CreateBackupRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBackupRequest", reflect.TypeOf((*MockDynamoAPI)(nil).CreateBackupRequest), arg0)
-}
-
-// CreateBackupWithContext mocks base method.
-func (m *MockDynamoAPI) CreateBackupWithContext(arg0 context.Context, arg1 *dynamodb.CreateBackupInput, arg2 ...request.Option) (*dynamodb.CreateBackupOutput, error) {
+// CreateTable mocks base method.
+func (m *MockDynamoAPI) CreateTable(arg0 context.Context, arg1 *dynamodb.CreateTableInput, arg2 ...func(*dynamodb.Options)) (*dynamodb.CreateTableOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "CreateBackupWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.CreateBackupOutput)
+ ret := m.ctrl.Call(m, "CreateTable", varargs...)
+ ret0, _ := ret[0].(*dynamodb.CreateTableOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// CreateBackupWithContext indicates an expected call of CreateBackupWithContext.
-func (mr *MockDynamoAPIMockRecorder) CreateBackupWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// CreateTable indicates an expected call of CreateTable.
+func (mr *MockDynamoAPIMockRecorder) CreateTable(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBackupWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).CreateBackupWithContext), varargs...)
-}
-
-// CreateGlobalTable mocks base method.
-func (m *MockDynamoAPI) CreateGlobalTable(arg0 *dynamodb.CreateGlobalTableInput) (*dynamodb.CreateGlobalTableOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateGlobalTable", arg0)
- ret0, _ := ret[0].(*dynamodb.CreateGlobalTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// CreateGlobalTable indicates an expected call of CreateGlobalTable.
-func (mr *MockDynamoAPIMockRecorder) CreateGlobalTable(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGlobalTable", reflect.TypeOf((*MockDynamoAPI)(nil).CreateGlobalTable), arg0)
-}
-
-// CreateGlobalTableRequest mocks base method.
-func (m *MockDynamoAPI) CreateGlobalTableRequest(arg0 *dynamodb.CreateGlobalTableInput) (*request.Request, *dynamodb.CreateGlobalTableOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateGlobalTableRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.CreateGlobalTableOutput)
- return ret0, ret1
-}
-
-// CreateGlobalTableRequest indicates an expected call of CreateGlobalTableRequest.
-func (mr *MockDynamoAPIMockRecorder) CreateGlobalTableRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGlobalTableRequest", reflect.TypeOf((*MockDynamoAPI)(nil).CreateGlobalTableRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTable", reflect.TypeOf((*MockDynamoAPI)(nil).CreateTable), varargs...)
}
-// CreateGlobalTableWithContext mocks base method.
-func (m *MockDynamoAPI) CreateGlobalTableWithContext(arg0 context.Context, arg1 *dynamodb.CreateGlobalTableInput, arg2 ...request.Option) (*dynamodb.CreateGlobalTableOutput, error) {
+// DeleteTable mocks base method.
+func (m *MockDynamoAPI) DeleteTable(arg0 context.Context, arg1 *dynamodb.DeleteTableInput, arg2 ...func(*dynamodb.Options)) (*dynamodb.DeleteTableOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "CreateGlobalTableWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.CreateGlobalTableOutput)
+ ret := m.ctrl.Call(m, "DeleteTable", varargs...)
+ ret0, _ := ret[0].(*dynamodb.DeleteTableOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// CreateGlobalTableWithContext indicates an expected call of CreateGlobalTableWithContext.
-func (mr *MockDynamoAPIMockRecorder) CreateGlobalTableWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// DeleteTable indicates an expected call of DeleteTable.
+func (mr *MockDynamoAPIMockRecorder) DeleteTable(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateGlobalTableWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).CreateGlobalTableWithContext), varargs...)
-}
-
-// CreateTable mocks base method.
-func (m *MockDynamoAPI) CreateTable(arg0 *dynamodb.CreateTableInput) (*dynamodb.CreateTableOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateTable", arg0)
- ret0, _ := ret[0].(*dynamodb.CreateTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// CreateTable indicates an expected call of CreateTable.
-func (mr *MockDynamoAPIMockRecorder) CreateTable(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTable", reflect.TypeOf((*MockDynamoAPI)(nil).CreateTable), arg0)
-}
-
-// CreateTableRequest mocks base method.
-func (m *MockDynamoAPI) CreateTableRequest(arg0 *dynamodb.CreateTableInput) (*request.Request, *dynamodb.CreateTableOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "CreateTableRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.CreateTableOutput)
- return ret0, ret1
-}
-
-// CreateTableRequest indicates an expected call of CreateTableRequest.
-func (mr *MockDynamoAPIMockRecorder) CreateTableRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTableRequest", reflect.TypeOf((*MockDynamoAPI)(nil).CreateTableRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTable", reflect.TypeOf((*MockDynamoAPI)(nil).DeleteTable), varargs...)
}
-// CreateTableWithContext mocks base method.
-func (m *MockDynamoAPI) CreateTableWithContext(arg0 context.Context, arg1 *dynamodb.CreateTableInput, arg2 ...request.Option) (*dynamodb.CreateTableOutput, error) {
+// DescribeTable mocks base method.
+func (m *MockDynamoAPI) DescribeTable(arg0 context.Context, arg1 *dynamodb.DescribeTableInput, arg2 ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "CreateTableWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.CreateTableOutput)
+ ret := m.ctrl.Call(m, "DescribeTable", varargs...)
+ ret0, _ := ret[0].(*dynamodb.DescribeTableOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// CreateTableWithContext indicates an expected call of CreateTableWithContext.
-func (mr *MockDynamoAPIMockRecorder) CreateTableWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// DescribeTable indicates an expected call of DescribeTable.
+func (mr *MockDynamoAPIMockRecorder) DescribeTable(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTableWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).CreateTableWithContext), varargs...)
-}
-
-// DeleteBackup mocks base method.
-func (m *MockDynamoAPI) DeleteBackup(arg0 *dynamodb.DeleteBackupInput) (*dynamodb.DeleteBackupOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBackup", arg0)
- ret0, _ := ret[0].(*dynamodb.DeleteBackupOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteBackup indicates an expected call of DeleteBackup.
-func (mr *MockDynamoAPIMockRecorder) DeleteBackup(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBackup", reflect.TypeOf((*MockDynamoAPI)(nil).DeleteBackup), arg0)
-}
-
-// DeleteBackupRequest mocks base method.
-func (m *MockDynamoAPI) DeleteBackupRequest(arg0 *dynamodb.DeleteBackupInput) (*request.Request, *dynamodb.DeleteBackupOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteBackupRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DeleteBackupOutput)
- return ret0, ret1
-}
-
-// DeleteBackupRequest indicates an expected call of DeleteBackupRequest.
-func (mr *MockDynamoAPIMockRecorder) DeleteBackupRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBackupRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DeleteBackupRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTable", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeTable), varargs...)
}
-// DeleteBackupWithContext mocks base method.
-func (m *MockDynamoAPI) DeleteBackupWithContext(arg0 context.Context, arg1 *dynamodb.DeleteBackupInput, arg2 ...request.Option) (*dynamodb.DeleteBackupOutput, error) {
+// GetItem mocks base method.
+func (m *MockDynamoAPI) GetItem(arg0 context.Context, arg1 *dynamodb.GetItemInput, arg2 ...func(*dynamodb.Options)) (*dynamodb.GetItemOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DeleteBackupWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DeleteBackupOutput)
+ ret := m.ctrl.Call(m, "GetItem", varargs...)
+ ret0, _ := ret[0].(*dynamodb.GetItemOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DeleteBackupWithContext indicates an expected call of DeleteBackupWithContext.
-func (mr *MockDynamoAPIMockRecorder) DeleteBackupWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// GetItem indicates an expected call of GetItem.
+func (mr *MockDynamoAPIMockRecorder) GetItem(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBackupWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DeleteBackupWithContext), varargs...)
-}
-
-// DeleteItem mocks base method.
-func (m *MockDynamoAPI) DeleteItem(arg0 *dynamodb.DeleteItemInput) (*dynamodb.DeleteItemOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteItem", arg0)
- ret0, _ := ret[0].(*dynamodb.DeleteItemOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteItem indicates an expected call of DeleteItem.
-func (mr *MockDynamoAPIMockRecorder) DeleteItem(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteItem", reflect.TypeOf((*MockDynamoAPI)(nil).DeleteItem), arg0)
-}
-
-// DeleteItemRequest mocks base method.
-func (m *MockDynamoAPI) DeleteItemRequest(arg0 *dynamodb.DeleteItemInput) (*request.Request, *dynamodb.DeleteItemOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteItemRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DeleteItemOutput)
- return ret0, ret1
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItem", reflect.TypeOf((*MockDynamoAPI)(nil).GetItem), varargs...)
}
-// DeleteItemRequest indicates an expected call of DeleteItemRequest.
-func (mr *MockDynamoAPIMockRecorder) DeleteItemRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteItemRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DeleteItemRequest), arg0)
-}
-
-// DeleteItemWithContext mocks base method.
-func (m *MockDynamoAPI) DeleteItemWithContext(arg0 context.Context, arg1 *dynamodb.DeleteItemInput, arg2 ...request.Option) (*dynamodb.DeleteItemOutput, error) {
+// PutItem mocks base method.
+func (m *MockDynamoAPI) PutItem(arg0 context.Context, arg1 *dynamodb.PutItemInput, arg2 ...func(*dynamodb.Options)) (*dynamodb.PutItemOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DeleteItemWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DeleteItemOutput)
+ ret := m.ctrl.Call(m, "PutItem", varargs...)
+ ret0, _ := ret[0].(*dynamodb.PutItemOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DeleteItemWithContext indicates an expected call of DeleteItemWithContext.
-func (mr *MockDynamoAPIMockRecorder) DeleteItemWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// PutItem indicates an expected call of PutItem.
+func (mr *MockDynamoAPIMockRecorder) PutItem(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteItemWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DeleteItemWithContext), varargs...)
-}
-
-// DeleteTable mocks base method.
-func (m *MockDynamoAPI) DeleteTable(arg0 *dynamodb.DeleteTableInput) (*dynamodb.DeleteTableOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteTable", arg0)
- ret0, _ := ret[0].(*dynamodb.DeleteTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DeleteTable indicates an expected call of DeleteTable.
-func (mr *MockDynamoAPIMockRecorder) DeleteTable(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTable", reflect.TypeOf((*MockDynamoAPI)(nil).DeleteTable), arg0)
-}
-
-// DeleteTableRequest mocks base method.
-func (m *MockDynamoAPI) DeleteTableRequest(arg0 *dynamodb.DeleteTableInput) (*request.Request, *dynamodb.DeleteTableOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DeleteTableRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DeleteTableOutput)
- return ret0, ret1
-}
-
-// DeleteTableRequest indicates an expected call of DeleteTableRequest.
-func (mr *MockDynamoAPIMockRecorder) DeleteTableRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTableRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DeleteTableRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutItem", reflect.TypeOf((*MockDynamoAPI)(nil).PutItem), varargs...)
}
-// DeleteTableWithContext mocks base method.
-func (m *MockDynamoAPI) DeleteTableWithContext(arg0 context.Context, arg1 *dynamodb.DeleteTableInput, arg2 ...request.Option) (*dynamodb.DeleteTableOutput, error) {
+// Query mocks base method.
+func (m *MockDynamoAPI) Query(arg0 context.Context, arg1 *dynamodb.QueryInput, arg2 ...func(*dynamodb.Options)) (*dynamodb.QueryOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DeleteTableWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DeleteTableOutput)
+ ret := m.ctrl.Call(m, "Query", varargs...)
+ ret0, _ := ret[0].(*dynamodb.QueryOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DeleteTableWithContext indicates an expected call of DeleteTableWithContext.
-func (mr *MockDynamoAPIMockRecorder) DeleteTableWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// Query indicates an expected call of Query.
+func (mr *MockDynamoAPIMockRecorder) Query(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteTableWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DeleteTableWithContext), varargs...)
-}
-
-// DescribeBackup mocks base method.
-func (m *MockDynamoAPI) DescribeBackup(arg0 *dynamodb.DescribeBackupInput) (*dynamodb.DescribeBackupOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeBackup", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeBackupOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeBackup indicates an expected call of DescribeBackup.
-func (mr *MockDynamoAPIMockRecorder) DescribeBackup(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeBackup", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeBackup), arg0)
-}
-
-// DescribeBackupRequest mocks base method.
-func (m *MockDynamoAPI) DescribeBackupRequest(arg0 *dynamodb.DescribeBackupInput) (*request.Request, *dynamodb.DescribeBackupOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeBackupRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeBackupOutput)
- return ret0, ret1
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDynamoAPI)(nil).Query), varargs...)
}
-// DescribeBackupRequest indicates an expected call of DescribeBackupRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeBackupRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeBackupRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeBackupRequest), arg0)
-}
-
-// DescribeBackupWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeBackupWithContext(arg0 context.Context, arg1 *dynamodb.DescribeBackupInput, arg2 ...request.Option) (*dynamodb.DescribeBackupOutput, error) {
+// TransactGetItems mocks base method.
+func (m *MockDynamoAPI) TransactGetItems(arg0 context.Context, arg1 *dynamodb.TransactGetItemsInput, arg2 ...func(*dynamodb.Options)) (*dynamodb.TransactGetItemsOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DescribeBackupWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeBackupOutput)
+ ret := m.ctrl.Call(m, "TransactGetItems", varargs...)
+ ret0, _ := ret[0].(*dynamodb.TransactGetItemsOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DescribeBackupWithContext indicates an expected call of DescribeBackupWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeBackupWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// TransactGetItems indicates an expected call of TransactGetItems.
+func (mr *MockDynamoAPIMockRecorder) TransactGetItems(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeBackupWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeBackupWithContext), varargs...)
-}
-
-// DescribeContinuousBackups mocks base method.
-func (m *MockDynamoAPI) DescribeContinuousBackups(arg0 *dynamodb.DescribeContinuousBackupsInput) (*dynamodb.DescribeContinuousBackupsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeContinuousBackups", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeContinuousBackupsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeContinuousBackups indicates an expected call of DescribeContinuousBackups.
-func (mr *MockDynamoAPIMockRecorder) DescribeContinuousBackups(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeContinuousBackups", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeContinuousBackups), arg0)
-}
-
-// DescribeContinuousBackupsRequest mocks base method.
-func (m *MockDynamoAPI) DescribeContinuousBackupsRequest(arg0 *dynamodb.DescribeContinuousBackupsInput) (*request.Request, *dynamodb.DescribeContinuousBackupsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeContinuousBackupsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeContinuousBackupsOutput)
- return ret0, ret1
-}
-
-// DescribeContinuousBackupsRequest indicates an expected call of DescribeContinuousBackupsRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeContinuousBackupsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeContinuousBackupsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeContinuousBackupsRequest), arg0)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactGetItems", reflect.TypeOf((*MockDynamoAPI)(nil).TransactGetItems), varargs...)
}
-// DescribeContinuousBackupsWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeContinuousBackupsWithContext(arg0 context.Context, arg1 *dynamodb.DescribeContinuousBackupsInput, arg2 ...request.Option) (*dynamodb.DescribeContinuousBackupsOutput, error) {
+// TransactWriteItems mocks base method.
+func (m *MockDynamoAPI) TransactWriteItems(arg0 context.Context, arg1 *dynamodb.TransactWriteItemsInput, arg2 ...func(*dynamodb.Options)) (*dynamodb.TransactWriteItemsOutput, error) {
m.ctrl.T.Helper()
varargs := []any{arg0, arg1}
for _, a := range arg2 {
varargs = append(varargs, a)
}
- ret := m.ctrl.Call(m, "DescribeContinuousBackupsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeContinuousBackupsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeContinuousBackupsWithContext indicates an expected call of DescribeContinuousBackupsWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeContinuousBackupsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeContinuousBackupsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeContinuousBackupsWithContext), varargs...)
-}
-
-// DescribeContributorInsights mocks base method.
-func (m *MockDynamoAPI) DescribeContributorInsights(arg0 *dynamodb.DescribeContributorInsightsInput) (*dynamodb.DescribeContributorInsightsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeContributorInsights", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeContributorInsightsOutput)
+ ret := m.ctrl.Call(m, "TransactWriteItems", varargs...)
+ ret0, _ := ret[0].(*dynamodb.TransactWriteItemsOutput)
ret1, _ := ret[1].(error)
return ret0, ret1
}
-// DescribeContributorInsights indicates an expected call of DescribeContributorInsights.
-func (mr *MockDynamoAPIMockRecorder) DescribeContributorInsights(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeContributorInsights", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeContributorInsights), arg0)
-}
-
-// DescribeContributorInsightsRequest mocks base method.
-func (m *MockDynamoAPI) DescribeContributorInsightsRequest(arg0 *dynamodb.DescribeContributorInsightsInput) (*request.Request, *dynamodb.DescribeContributorInsightsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeContributorInsightsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeContributorInsightsOutput)
- return ret0, ret1
-}
-
-// DescribeContributorInsightsRequest indicates an expected call of DescribeContributorInsightsRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeContributorInsightsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeContributorInsightsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeContributorInsightsRequest), arg0)
-}
-
-// DescribeContributorInsightsWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeContributorInsightsWithContext(arg0 context.Context, arg1 *dynamodb.DescribeContributorInsightsInput, arg2 ...request.Option) (*dynamodb.DescribeContributorInsightsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeContributorInsightsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeContributorInsightsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeContributorInsightsWithContext indicates an expected call of DescribeContributorInsightsWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeContributorInsightsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeContributorInsightsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeContributorInsightsWithContext), varargs...)
-}
-
-// DescribeEndpoints mocks base method.
-func (m *MockDynamoAPI) DescribeEndpoints(arg0 *dynamodb.DescribeEndpointsInput) (*dynamodb.DescribeEndpointsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeEndpoints", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeEndpointsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeEndpoints indicates an expected call of DescribeEndpoints.
-func (mr *MockDynamoAPIMockRecorder) DescribeEndpoints(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeEndpoints", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeEndpoints), arg0)
-}
-
-// DescribeEndpointsRequest mocks base method.
-func (m *MockDynamoAPI) DescribeEndpointsRequest(arg0 *dynamodb.DescribeEndpointsInput) (*request.Request, *dynamodb.DescribeEndpointsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeEndpointsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeEndpointsOutput)
- return ret0, ret1
-}
-
-// DescribeEndpointsRequest indicates an expected call of DescribeEndpointsRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeEndpointsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeEndpointsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeEndpointsRequest), arg0)
-}
-
-// DescribeEndpointsWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeEndpointsWithContext(arg0 context.Context, arg1 *dynamodb.DescribeEndpointsInput, arg2 ...request.Option) (*dynamodb.DescribeEndpointsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeEndpointsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeEndpointsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeEndpointsWithContext indicates an expected call of DescribeEndpointsWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeEndpointsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeEndpointsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeEndpointsWithContext), varargs...)
-}
-
-// DescribeExport mocks base method.
-func (m *MockDynamoAPI) DescribeExport(arg0 *dynamodb.DescribeExportInput) (*dynamodb.DescribeExportOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeExport", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeExportOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeExport indicates an expected call of DescribeExport.
-func (mr *MockDynamoAPIMockRecorder) DescribeExport(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeExport", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeExport), arg0)
-}
-
-// DescribeExportRequest mocks base method.
-func (m *MockDynamoAPI) DescribeExportRequest(arg0 *dynamodb.DescribeExportInput) (*request.Request, *dynamodb.DescribeExportOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeExportRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeExportOutput)
- return ret0, ret1
-}
-
-// DescribeExportRequest indicates an expected call of DescribeExportRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeExportRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeExportRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeExportRequest), arg0)
-}
-
-// DescribeExportWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeExportWithContext(arg0 context.Context, arg1 *dynamodb.DescribeExportInput, arg2 ...request.Option) (*dynamodb.DescribeExportOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeExportWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeExportOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeExportWithContext indicates an expected call of DescribeExportWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeExportWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeExportWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeExportWithContext), varargs...)
-}
-
-// DescribeGlobalTable mocks base method.
-func (m *MockDynamoAPI) DescribeGlobalTable(arg0 *dynamodb.DescribeGlobalTableInput) (*dynamodb.DescribeGlobalTableOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeGlobalTable", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeGlobalTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeGlobalTable indicates an expected call of DescribeGlobalTable.
-func (mr *MockDynamoAPIMockRecorder) DescribeGlobalTable(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeGlobalTable", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeGlobalTable), arg0)
-}
-
-// DescribeGlobalTableRequest mocks base method.
-func (m *MockDynamoAPI) DescribeGlobalTableRequest(arg0 *dynamodb.DescribeGlobalTableInput) (*request.Request, *dynamodb.DescribeGlobalTableOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeGlobalTableRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeGlobalTableOutput)
- return ret0, ret1
-}
-
-// DescribeGlobalTableRequest indicates an expected call of DescribeGlobalTableRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeGlobalTableRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeGlobalTableRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeGlobalTableRequest), arg0)
-}
-
-// DescribeGlobalTableSettings mocks base method.
-func (m *MockDynamoAPI) DescribeGlobalTableSettings(arg0 *dynamodb.DescribeGlobalTableSettingsInput) (*dynamodb.DescribeGlobalTableSettingsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeGlobalTableSettings", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeGlobalTableSettingsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeGlobalTableSettings indicates an expected call of DescribeGlobalTableSettings.
-func (mr *MockDynamoAPIMockRecorder) DescribeGlobalTableSettings(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeGlobalTableSettings", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeGlobalTableSettings), arg0)
-}
-
-// DescribeGlobalTableSettingsRequest mocks base method.
-func (m *MockDynamoAPI) DescribeGlobalTableSettingsRequest(arg0 *dynamodb.DescribeGlobalTableSettingsInput) (*request.Request, *dynamodb.DescribeGlobalTableSettingsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeGlobalTableSettingsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeGlobalTableSettingsOutput)
- return ret0, ret1
-}
-
-// DescribeGlobalTableSettingsRequest indicates an expected call of DescribeGlobalTableSettingsRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeGlobalTableSettingsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeGlobalTableSettingsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeGlobalTableSettingsRequest), arg0)
-}
-
-// DescribeGlobalTableSettingsWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeGlobalTableSettingsWithContext(arg0 context.Context, arg1 *dynamodb.DescribeGlobalTableSettingsInput, arg2 ...request.Option) (*dynamodb.DescribeGlobalTableSettingsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeGlobalTableSettingsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeGlobalTableSettingsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeGlobalTableSettingsWithContext indicates an expected call of DescribeGlobalTableSettingsWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeGlobalTableSettingsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeGlobalTableSettingsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeGlobalTableSettingsWithContext), varargs...)
-}
-
-// DescribeGlobalTableWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeGlobalTableWithContext(arg0 context.Context, arg1 *dynamodb.DescribeGlobalTableInput, arg2 ...request.Option) (*dynamodb.DescribeGlobalTableOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeGlobalTableWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeGlobalTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeGlobalTableWithContext indicates an expected call of DescribeGlobalTableWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeGlobalTableWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeGlobalTableWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeGlobalTableWithContext), varargs...)
-}
-
-// DescribeImport mocks base method.
-func (m *MockDynamoAPI) DescribeImport(arg0 *dynamodb.DescribeImportInput) (*dynamodb.DescribeImportOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeImport", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeImportOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeImport indicates an expected call of DescribeImport.
-func (mr *MockDynamoAPIMockRecorder) DescribeImport(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImport", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeImport), arg0)
-}
-
-// DescribeImportRequest mocks base method.
-func (m *MockDynamoAPI) DescribeImportRequest(arg0 *dynamodb.DescribeImportInput) (*request.Request, *dynamodb.DescribeImportOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeImportRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeImportOutput)
- return ret0, ret1
-}
-
-// DescribeImportRequest indicates an expected call of DescribeImportRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeImportRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImportRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeImportRequest), arg0)
-}
-
-// DescribeImportWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeImportWithContext(arg0 context.Context, arg1 *dynamodb.DescribeImportInput, arg2 ...request.Option) (*dynamodb.DescribeImportOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeImportWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeImportOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeImportWithContext indicates an expected call of DescribeImportWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeImportWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeImportWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeImportWithContext), varargs...)
-}
-
-// DescribeKinesisStreamingDestination mocks base method.
-func (m *MockDynamoAPI) DescribeKinesisStreamingDestination(arg0 *dynamodb.DescribeKinesisStreamingDestinationInput) (*dynamodb.DescribeKinesisStreamingDestinationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeKinesisStreamingDestination", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeKinesisStreamingDestinationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeKinesisStreamingDestination indicates an expected call of DescribeKinesisStreamingDestination.
-func (mr *MockDynamoAPIMockRecorder) DescribeKinesisStreamingDestination(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeKinesisStreamingDestination", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeKinesisStreamingDestination), arg0)
-}
-
-// DescribeKinesisStreamingDestinationRequest mocks base method.
-func (m *MockDynamoAPI) DescribeKinesisStreamingDestinationRequest(arg0 *dynamodb.DescribeKinesisStreamingDestinationInput) (*request.Request, *dynamodb.DescribeKinesisStreamingDestinationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeKinesisStreamingDestinationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeKinesisStreamingDestinationOutput)
- return ret0, ret1
-}
-
-// DescribeKinesisStreamingDestinationRequest indicates an expected call of DescribeKinesisStreamingDestinationRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeKinesisStreamingDestinationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeKinesisStreamingDestinationRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeKinesisStreamingDestinationRequest), arg0)
-}
-
-// DescribeKinesisStreamingDestinationWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeKinesisStreamingDestinationWithContext(arg0 context.Context, arg1 *dynamodb.DescribeKinesisStreamingDestinationInput, arg2 ...request.Option) (*dynamodb.DescribeKinesisStreamingDestinationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeKinesisStreamingDestinationWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeKinesisStreamingDestinationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeKinesisStreamingDestinationWithContext indicates an expected call of DescribeKinesisStreamingDestinationWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeKinesisStreamingDestinationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeKinesisStreamingDestinationWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeKinesisStreamingDestinationWithContext), varargs...)
-}
-
-// DescribeLimits mocks base method.
-func (m *MockDynamoAPI) DescribeLimits(arg0 *dynamodb.DescribeLimitsInput) (*dynamodb.DescribeLimitsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeLimits", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeLimitsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeLimits indicates an expected call of DescribeLimits.
-func (mr *MockDynamoAPIMockRecorder) DescribeLimits(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeLimits", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeLimits), arg0)
-}
-
-// DescribeLimitsRequest mocks base method.
-func (m *MockDynamoAPI) DescribeLimitsRequest(arg0 *dynamodb.DescribeLimitsInput) (*request.Request, *dynamodb.DescribeLimitsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeLimitsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeLimitsOutput)
- return ret0, ret1
-}
-
-// DescribeLimitsRequest indicates an expected call of DescribeLimitsRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeLimitsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeLimitsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeLimitsRequest), arg0)
-}
-
-// DescribeLimitsWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeLimitsWithContext(arg0 context.Context, arg1 *dynamodb.DescribeLimitsInput, arg2 ...request.Option) (*dynamodb.DescribeLimitsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeLimitsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeLimitsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeLimitsWithContext indicates an expected call of DescribeLimitsWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeLimitsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeLimitsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeLimitsWithContext), varargs...)
-}
-
-// DescribeTable mocks base method.
-func (m *MockDynamoAPI) DescribeTable(arg0 *dynamodb.DescribeTableInput) (*dynamodb.DescribeTableOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeTable", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeTable indicates an expected call of DescribeTable.
-func (mr *MockDynamoAPIMockRecorder) DescribeTable(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTable", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeTable), arg0)
-}
-
-// DescribeTableReplicaAutoScaling mocks base method.
-func (m *MockDynamoAPI) DescribeTableReplicaAutoScaling(arg0 *dynamodb.DescribeTableReplicaAutoScalingInput) (*dynamodb.DescribeTableReplicaAutoScalingOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeTableReplicaAutoScaling", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeTableReplicaAutoScalingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeTableReplicaAutoScaling indicates an expected call of DescribeTableReplicaAutoScaling.
-func (mr *MockDynamoAPIMockRecorder) DescribeTableReplicaAutoScaling(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTableReplicaAutoScaling", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeTableReplicaAutoScaling), arg0)
-}
-
-// DescribeTableReplicaAutoScalingRequest mocks base method.
-func (m *MockDynamoAPI) DescribeTableReplicaAutoScalingRequest(arg0 *dynamodb.DescribeTableReplicaAutoScalingInput) (*request.Request, *dynamodb.DescribeTableReplicaAutoScalingOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeTableReplicaAutoScalingRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeTableReplicaAutoScalingOutput)
- return ret0, ret1
-}
-
-// DescribeTableReplicaAutoScalingRequest indicates an expected call of DescribeTableReplicaAutoScalingRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeTableReplicaAutoScalingRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTableReplicaAutoScalingRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeTableReplicaAutoScalingRequest), arg0)
-}
-
-// DescribeTableReplicaAutoScalingWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeTableReplicaAutoScalingWithContext(arg0 context.Context, arg1 *dynamodb.DescribeTableReplicaAutoScalingInput, arg2 ...request.Option) (*dynamodb.DescribeTableReplicaAutoScalingOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeTableReplicaAutoScalingWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeTableReplicaAutoScalingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeTableReplicaAutoScalingWithContext indicates an expected call of DescribeTableReplicaAutoScalingWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeTableReplicaAutoScalingWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTableReplicaAutoScalingWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeTableReplicaAutoScalingWithContext), varargs...)
-}
-
-// DescribeTableRequest mocks base method.
-func (m *MockDynamoAPI) DescribeTableRequest(arg0 *dynamodb.DescribeTableInput) (*request.Request, *dynamodb.DescribeTableOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeTableRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeTableOutput)
- return ret0, ret1
-}
-
-// DescribeTableRequest indicates an expected call of DescribeTableRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeTableRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTableRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeTableRequest), arg0)
-}
-
-// DescribeTableWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeTableWithContext(arg0 context.Context, arg1 *dynamodb.DescribeTableInput, arg2 ...request.Option) (*dynamodb.DescribeTableOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeTableWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeTableWithContext indicates an expected call of DescribeTableWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeTableWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTableWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeTableWithContext), varargs...)
-}
-
-// DescribeTimeToLive mocks base method.
-func (m *MockDynamoAPI) DescribeTimeToLive(arg0 *dynamodb.DescribeTimeToLiveInput) (*dynamodb.DescribeTimeToLiveOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeTimeToLive", arg0)
- ret0, _ := ret[0].(*dynamodb.DescribeTimeToLiveOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeTimeToLive indicates an expected call of DescribeTimeToLive.
-func (mr *MockDynamoAPIMockRecorder) DescribeTimeToLive(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTimeToLive", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeTimeToLive), arg0)
-}
-
-// DescribeTimeToLiveRequest mocks base method.
-func (m *MockDynamoAPI) DescribeTimeToLiveRequest(arg0 *dynamodb.DescribeTimeToLiveInput) (*request.Request, *dynamodb.DescribeTimeToLiveOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DescribeTimeToLiveRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DescribeTimeToLiveOutput)
- return ret0, ret1
-}
-
-// DescribeTimeToLiveRequest indicates an expected call of DescribeTimeToLiveRequest.
-func (mr *MockDynamoAPIMockRecorder) DescribeTimeToLiveRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTimeToLiveRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeTimeToLiveRequest), arg0)
-}
-
-// DescribeTimeToLiveWithContext mocks base method.
-func (m *MockDynamoAPI) DescribeTimeToLiveWithContext(arg0 context.Context, arg1 *dynamodb.DescribeTimeToLiveInput, arg2 ...request.Option) (*dynamodb.DescribeTimeToLiveOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DescribeTimeToLiveWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DescribeTimeToLiveOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DescribeTimeToLiveWithContext indicates an expected call of DescribeTimeToLiveWithContext.
-func (mr *MockDynamoAPIMockRecorder) DescribeTimeToLiveWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DescribeTimeToLiveWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DescribeTimeToLiveWithContext), varargs...)
-}
-
-// DisableKinesisStreamingDestination mocks base method.
-func (m *MockDynamoAPI) DisableKinesisStreamingDestination(arg0 *dynamodb.DisableKinesisStreamingDestinationInput) (*dynamodb.DisableKinesisStreamingDestinationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DisableKinesisStreamingDestination", arg0)
- ret0, _ := ret[0].(*dynamodb.DisableKinesisStreamingDestinationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DisableKinesisStreamingDestination indicates an expected call of DisableKinesisStreamingDestination.
-func (mr *MockDynamoAPIMockRecorder) DisableKinesisStreamingDestination(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableKinesisStreamingDestination", reflect.TypeOf((*MockDynamoAPI)(nil).DisableKinesisStreamingDestination), arg0)
-}
-
-// DisableKinesisStreamingDestinationRequest mocks base method.
-func (m *MockDynamoAPI) DisableKinesisStreamingDestinationRequest(arg0 *dynamodb.DisableKinesisStreamingDestinationInput) (*request.Request, *dynamodb.DisableKinesisStreamingDestinationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "DisableKinesisStreamingDestinationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.DisableKinesisStreamingDestinationOutput)
- return ret0, ret1
-}
-
-// DisableKinesisStreamingDestinationRequest indicates an expected call of DisableKinesisStreamingDestinationRequest.
-func (mr *MockDynamoAPIMockRecorder) DisableKinesisStreamingDestinationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableKinesisStreamingDestinationRequest", reflect.TypeOf((*MockDynamoAPI)(nil).DisableKinesisStreamingDestinationRequest), arg0)
-}
-
-// DisableKinesisStreamingDestinationWithContext mocks base method.
-func (m *MockDynamoAPI) DisableKinesisStreamingDestinationWithContext(arg0 context.Context, arg1 *dynamodb.DisableKinesisStreamingDestinationInput, arg2 ...request.Option) (*dynamodb.DisableKinesisStreamingDestinationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "DisableKinesisStreamingDestinationWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.DisableKinesisStreamingDestinationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// DisableKinesisStreamingDestinationWithContext indicates an expected call of DisableKinesisStreamingDestinationWithContext.
-func (mr *MockDynamoAPIMockRecorder) DisableKinesisStreamingDestinationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DisableKinesisStreamingDestinationWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).DisableKinesisStreamingDestinationWithContext), varargs...)
-}
-
-// EnableKinesisStreamingDestination mocks base method.
-func (m *MockDynamoAPI) EnableKinesisStreamingDestination(arg0 *dynamodb.EnableKinesisStreamingDestinationInput) (*dynamodb.EnableKinesisStreamingDestinationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "EnableKinesisStreamingDestination", arg0)
- ret0, _ := ret[0].(*dynamodb.EnableKinesisStreamingDestinationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// EnableKinesisStreamingDestination indicates an expected call of EnableKinesisStreamingDestination.
-func (mr *MockDynamoAPIMockRecorder) EnableKinesisStreamingDestination(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableKinesisStreamingDestination", reflect.TypeOf((*MockDynamoAPI)(nil).EnableKinesisStreamingDestination), arg0)
-}
-
-// EnableKinesisStreamingDestinationRequest mocks base method.
-func (m *MockDynamoAPI) EnableKinesisStreamingDestinationRequest(arg0 *dynamodb.EnableKinesisStreamingDestinationInput) (*request.Request, *dynamodb.EnableKinesisStreamingDestinationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "EnableKinesisStreamingDestinationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.EnableKinesisStreamingDestinationOutput)
- return ret0, ret1
-}
-
-// EnableKinesisStreamingDestinationRequest indicates an expected call of EnableKinesisStreamingDestinationRequest.
-func (mr *MockDynamoAPIMockRecorder) EnableKinesisStreamingDestinationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableKinesisStreamingDestinationRequest", reflect.TypeOf((*MockDynamoAPI)(nil).EnableKinesisStreamingDestinationRequest), arg0)
-}
-
-// EnableKinesisStreamingDestinationWithContext mocks base method.
-func (m *MockDynamoAPI) EnableKinesisStreamingDestinationWithContext(arg0 context.Context, arg1 *dynamodb.EnableKinesisStreamingDestinationInput, arg2 ...request.Option) (*dynamodb.EnableKinesisStreamingDestinationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "EnableKinesisStreamingDestinationWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.EnableKinesisStreamingDestinationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// EnableKinesisStreamingDestinationWithContext indicates an expected call of EnableKinesisStreamingDestinationWithContext.
-func (mr *MockDynamoAPIMockRecorder) EnableKinesisStreamingDestinationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnableKinesisStreamingDestinationWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).EnableKinesisStreamingDestinationWithContext), varargs...)
-}
-
-// ExecuteStatement mocks base method.
-func (m *MockDynamoAPI) ExecuteStatement(arg0 *dynamodb.ExecuteStatementInput) (*dynamodb.ExecuteStatementOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ExecuteStatement", arg0)
- ret0, _ := ret[0].(*dynamodb.ExecuteStatementOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ExecuteStatement indicates an expected call of ExecuteStatement.
-func (mr *MockDynamoAPIMockRecorder) ExecuteStatement(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteStatement", reflect.TypeOf((*MockDynamoAPI)(nil).ExecuteStatement), arg0)
-}
-
-// ExecuteStatementRequest mocks base method.
-func (m *MockDynamoAPI) ExecuteStatementRequest(arg0 *dynamodb.ExecuteStatementInput) (*request.Request, *dynamodb.ExecuteStatementOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ExecuteStatementRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ExecuteStatementOutput)
- return ret0, ret1
-}
-
-// ExecuteStatementRequest indicates an expected call of ExecuteStatementRequest.
-func (mr *MockDynamoAPIMockRecorder) ExecuteStatementRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteStatementRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ExecuteStatementRequest), arg0)
-}
-
-// ExecuteStatementWithContext mocks base method.
-func (m *MockDynamoAPI) ExecuteStatementWithContext(arg0 context.Context, arg1 *dynamodb.ExecuteStatementInput, arg2 ...request.Option) (*dynamodb.ExecuteStatementOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ExecuteStatementWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ExecuteStatementOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ExecuteStatementWithContext indicates an expected call of ExecuteStatementWithContext.
-func (mr *MockDynamoAPIMockRecorder) ExecuteStatementWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteStatementWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ExecuteStatementWithContext), varargs...)
-}
-
-// ExecuteTransaction mocks base method.
-func (m *MockDynamoAPI) ExecuteTransaction(arg0 *dynamodb.ExecuteTransactionInput) (*dynamodb.ExecuteTransactionOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ExecuteTransaction", arg0)
- ret0, _ := ret[0].(*dynamodb.ExecuteTransactionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ExecuteTransaction indicates an expected call of ExecuteTransaction.
-func (mr *MockDynamoAPIMockRecorder) ExecuteTransaction(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteTransaction", reflect.TypeOf((*MockDynamoAPI)(nil).ExecuteTransaction), arg0)
-}
-
-// ExecuteTransactionRequest mocks base method.
-func (m *MockDynamoAPI) ExecuteTransactionRequest(arg0 *dynamodb.ExecuteTransactionInput) (*request.Request, *dynamodb.ExecuteTransactionOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ExecuteTransactionRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ExecuteTransactionOutput)
- return ret0, ret1
-}
-
-// ExecuteTransactionRequest indicates an expected call of ExecuteTransactionRequest.
-func (mr *MockDynamoAPIMockRecorder) ExecuteTransactionRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteTransactionRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ExecuteTransactionRequest), arg0)
-}
-
-// ExecuteTransactionWithContext mocks base method.
-func (m *MockDynamoAPI) ExecuteTransactionWithContext(arg0 context.Context, arg1 *dynamodb.ExecuteTransactionInput, arg2 ...request.Option) (*dynamodb.ExecuteTransactionOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ExecuteTransactionWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ExecuteTransactionOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ExecuteTransactionWithContext indicates an expected call of ExecuteTransactionWithContext.
-func (mr *MockDynamoAPIMockRecorder) ExecuteTransactionWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExecuteTransactionWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ExecuteTransactionWithContext), varargs...)
-}
-
-// ExportTableToPointInTime mocks base method.
-func (m *MockDynamoAPI) ExportTableToPointInTime(arg0 *dynamodb.ExportTableToPointInTimeInput) (*dynamodb.ExportTableToPointInTimeOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ExportTableToPointInTime", arg0)
- ret0, _ := ret[0].(*dynamodb.ExportTableToPointInTimeOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ExportTableToPointInTime indicates an expected call of ExportTableToPointInTime.
-func (mr *MockDynamoAPIMockRecorder) ExportTableToPointInTime(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportTableToPointInTime", reflect.TypeOf((*MockDynamoAPI)(nil).ExportTableToPointInTime), arg0)
-}
-
-// ExportTableToPointInTimeRequest mocks base method.
-func (m *MockDynamoAPI) ExportTableToPointInTimeRequest(arg0 *dynamodb.ExportTableToPointInTimeInput) (*request.Request, *dynamodb.ExportTableToPointInTimeOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ExportTableToPointInTimeRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ExportTableToPointInTimeOutput)
- return ret0, ret1
-}
-
-// ExportTableToPointInTimeRequest indicates an expected call of ExportTableToPointInTimeRequest.
-func (mr *MockDynamoAPIMockRecorder) ExportTableToPointInTimeRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportTableToPointInTimeRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ExportTableToPointInTimeRequest), arg0)
-}
-
-// ExportTableToPointInTimeWithContext mocks base method.
-func (m *MockDynamoAPI) ExportTableToPointInTimeWithContext(arg0 context.Context, arg1 *dynamodb.ExportTableToPointInTimeInput, arg2 ...request.Option) (*dynamodb.ExportTableToPointInTimeOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ExportTableToPointInTimeWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ExportTableToPointInTimeOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ExportTableToPointInTimeWithContext indicates an expected call of ExportTableToPointInTimeWithContext.
-func (mr *MockDynamoAPIMockRecorder) ExportTableToPointInTimeWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExportTableToPointInTimeWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ExportTableToPointInTimeWithContext), varargs...)
-}
-
-// GetItem mocks base method.
-func (m *MockDynamoAPI) GetItem(arg0 *dynamodb.GetItemInput) (*dynamodb.GetItemOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetItem", arg0)
- ret0, _ := ret[0].(*dynamodb.GetItemOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetItem indicates an expected call of GetItem.
-func (mr *MockDynamoAPIMockRecorder) GetItem(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItem", reflect.TypeOf((*MockDynamoAPI)(nil).GetItem), arg0)
-}
-
-// GetItemRequest mocks base method.
-func (m *MockDynamoAPI) GetItemRequest(arg0 *dynamodb.GetItemInput) (*request.Request, *dynamodb.GetItemOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "GetItemRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.GetItemOutput)
- return ret0, ret1
-}
-
-// GetItemRequest indicates an expected call of GetItemRequest.
-func (mr *MockDynamoAPIMockRecorder) GetItemRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemRequest", reflect.TypeOf((*MockDynamoAPI)(nil).GetItemRequest), arg0)
-}
-
-// GetItemWithContext mocks base method.
-func (m *MockDynamoAPI) GetItemWithContext(arg0 context.Context, arg1 *dynamodb.GetItemInput, arg2 ...request.Option) (*dynamodb.GetItemOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "GetItemWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.GetItemOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// GetItemWithContext indicates an expected call of GetItemWithContext.
-func (mr *MockDynamoAPIMockRecorder) GetItemWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetItemWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).GetItemWithContext), varargs...)
-}
-
-// ImportTable mocks base method.
-func (m *MockDynamoAPI) ImportTable(arg0 *dynamodb.ImportTableInput) (*dynamodb.ImportTableOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ImportTable", arg0)
- ret0, _ := ret[0].(*dynamodb.ImportTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ImportTable indicates an expected call of ImportTable.
-func (mr *MockDynamoAPIMockRecorder) ImportTable(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportTable", reflect.TypeOf((*MockDynamoAPI)(nil).ImportTable), arg0)
-}
-
-// ImportTableRequest mocks base method.
-func (m *MockDynamoAPI) ImportTableRequest(arg0 *dynamodb.ImportTableInput) (*request.Request, *dynamodb.ImportTableOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ImportTableRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ImportTableOutput)
- return ret0, ret1
-}
-
-// ImportTableRequest indicates an expected call of ImportTableRequest.
-func (mr *MockDynamoAPIMockRecorder) ImportTableRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportTableRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ImportTableRequest), arg0)
-}
-
-// ImportTableWithContext mocks base method.
-func (m *MockDynamoAPI) ImportTableWithContext(arg0 context.Context, arg1 *dynamodb.ImportTableInput, arg2 ...request.Option) (*dynamodb.ImportTableOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ImportTableWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ImportTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ImportTableWithContext indicates an expected call of ImportTableWithContext.
-func (mr *MockDynamoAPIMockRecorder) ImportTableWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ImportTableWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ImportTableWithContext), varargs...)
-}
-
-// ListBackups mocks base method.
-func (m *MockDynamoAPI) ListBackups(arg0 *dynamodb.ListBackupsInput) (*dynamodb.ListBackupsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBackups", arg0)
- ret0, _ := ret[0].(*dynamodb.ListBackupsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBackups indicates an expected call of ListBackups.
-func (mr *MockDynamoAPIMockRecorder) ListBackups(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBackups", reflect.TypeOf((*MockDynamoAPI)(nil).ListBackups), arg0)
-}
-
-// ListBackupsRequest mocks base method.
-func (m *MockDynamoAPI) ListBackupsRequest(arg0 *dynamodb.ListBackupsInput) (*request.Request, *dynamodb.ListBackupsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListBackupsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ListBackupsOutput)
- return ret0, ret1
-}
-
-// ListBackupsRequest indicates an expected call of ListBackupsRequest.
-func (mr *MockDynamoAPIMockRecorder) ListBackupsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBackupsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ListBackupsRequest), arg0)
-}
-
-// ListBackupsWithContext mocks base method.
-func (m *MockDynamoAPI) ListBackupsWithContext(arg0 context.Context, arg1 *dynamodb.ListBackupsInput, arg2 ...request.Option) (*dynamodb.ListBackupsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListBackupsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ListBackupsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListBackupsWithContext indicates an expected call of ListBackupsWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListBackupsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListBackupsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListBackupsWithContext), varargs...)
-}
-
-// ListContributorInsights mocks base method.
-func (m *MockDynamoAPI) ListContributorInsights(arg0 *dynamodb.ListContributorInsightsInput) (*dynamodb.ListContributorInsightsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListContributorInsights", arg0)
- ret0, _ := ret[0].(*dynamodb.ListContributorInsightsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListContributorInsights indicates an expected call of ListContributorInsights.
-func (mr *MockDynamoAPIMockRecorder) ListContributorInsights(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListContributorInsights", reflect.TypeOf((*MockDynamoAPI)(nil).ListContributorInsights), arg0)
-}
-
-// ListContributorInsightsPages mocks base method.
-func (m *MockDynamoAPI) ListContributorInsightsPages(arg0 *dynamodb.ListContributorInsightsInput, arg1 func(*dynamodb.ListContributorInsightsOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListContributorInsightsPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListContributorInsightsPages indicates an expected call of ListContributorInsightsPages.
-func (mr *MockDynamoAPIMockRecorder) ListContributorInsightsPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListContributorInsightsPages", reflect.TypeOf((*MockDynamoAPI)(nil).ListContributorInsightsPages), arg0, arg1)
-}
-
-// ListContributorInsightsPagesWithContext mocks base method.
-func (m *MockDynamoAPI) ListContributorInsightsPagesWithContext(arg0 context.Context, arg1 *dynamodb.ListContributorInsightsInput, arg2 func(*dynamodb.ListContributorInsightsOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListContributorInsightsPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListContributorInsightsPagesWithContext indicates an expected call of ListContributorInsightsPagesWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListContributorInsightsPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListContributorInsightsPagesWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListContributorInsightsPagesWithContext), varargs...)
-}
-
-// ListContributorInsightsRequest mocks base method.
-func (m *MockDynamoAPI) ListContributorInsightsRequest(arg0 *dynamodb.ListContributorInsightsInput) (*request.Request, *dynamodb.ListContributorInsightsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListContributorInsightsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ListContributorInsightsOutput)
- return ret0, ret1
-}
-
-// ListContributorInsightsRequest indicates an expected call of ListContributorInsightsRequest.
-func (mr *MockDynamoAPIMockRecorder) ListContributorInsightsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListContributorInsightsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ListContributorInsightsRequest), arg0)
-}
-
-// ListContributorInsightsWithContext mocks base method.
-func (m *MockDynamoAPI) ListContributorInsightsWithContext(arg0 context.Context, arg1 *dynamodb.ListContributorInsightsInput, arg2 ...request.Option) (*dynamodb.ListContributorInsightsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListContributorInsightsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ListContributorInsightsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListContributorInsightsWithContext indicates an expected call of ListContributorInsightsWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListContributorInsightsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListContributorInsightsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListContributorInsightsWithContext), varargs...)
-}
-
-// ListExports mocks base method.
-func (m *MockDynamoAPI) ListExports(arg0 *dynamodb.ListExportsInput) (*dynamodb.ListExportsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListExports", arg0)
- ret0, _ := ret[0].(*dynamodb.ListExportsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListExports indicates an expected call of ListExports.
-func (mr *MockDynamoAPIMockRecorder) ListExports(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExports", reflect.TypeOf((*MockDynamoAPI)(nil).ListExports), arg0)
-}
-
-// ListExportsPages mocks base method.
-func (m *MockDynamoAPI) ListExportsPages(arg0 *dynamodb.ListExportsInput, arg1 func(*dynamodb.ListExportsOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListExportsPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListExportsPages indicates an expected call of ListExportsPages.
-func (mr *MockDynamoAPIMockRecorder) ListExportsPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExportsPages", reflect.TypeOf((*MockDynamoAPI)(nil).ListExportsPages), arg0, arg1)
-}
-
-// ListExportsPagesWithContext mocks base method.
-func (m *MockDynamoAPI) ListExportsPagesWithContext(arg0 context.Context, arg1 *dynamodb.ListExportsInput, arg2 func(*dynamodb.ListExportsOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListExportsPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListExportsPagesWithContext indicates an expected call of ListExportsPagesWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListExportsPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExportsPagesWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListExportsPagesWithContext), varargs...)
-}
-
-// ListExportsRequest mocks base method.
-func (m *MockDynamoAPI) ListExportsRequest(arg0 *dynamodb.ListExportsInput) (*request.Request, *dynamodb.ListExportsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListExportsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ListExportsOutput)
- return ret0, ret1
-}
-
-// ListExportsRequest indicates an expected call of ListExportsRequest.
-func (mr *MockDynamoAPIMockRecorder) ListExportsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExportsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ListExportsRequest), arg0)
-}
-
-// ListExportsWithContext mocks base method.
-func (m *MockDynamoAPI) ListExportsWithContext(arg0 context.Context, arg1 *dynamodb.ListExportsInput, arg2 ...request.Option) (*dynamodb.ListExportsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListExportsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ListExportsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListExportsWithContext indicates an expected call of ListExportsWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListExportsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListExportsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListExportsWithContext), varargs...)
-}
-
-// ListGlobalTables mocks base method.
-func (m *MockDynamoAPI) ListGlobalTables(arg0 *dynamodb.ListGlobalTablesInput) (*dynamodb.ListGlobalTablesOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListGlobalTables", arg0)
- ret0, _ := ret[0].(*dynamodb.ListGlobalTablesOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListGlobalTables indicates an expected call of ListGlobalTables.
-func (mr *MockDynamoAPIMockRecorder) ListGlobalTables(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListGlobalTables", reflect.TypeOf((*MockDynamoAPI)(nil).ListGlobalTables), arg0)
-}
-
-// ListGlobalTablesRequest mocks base method.
-func (m *MockDynamoAPI) ListGlobalTablesRequest(arg0 *dynamodb.ListGlobalTablesInput) (*request.Request, *dynamodb.ListGlobalTablesOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListGlobalTablesRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ListGlobalTablesOutput)
- return ret0, ret1
-}
-
-// ListGlobalTablesRequest indicates an expected call of ListGlobalTablesRequest.
-func (mr *MockDynamoAPIMockRecorder) ListGlobalTablesRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListGlobalTablesRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ListGlobalTablesRequest), arg0)
-}
-
-// ListGlobalTablesWithContext mocks base method.
-func (m *MockDynamoAPI) ListGlobalTablesWithContext(arg0 context.Context, arg1 *dynamodb.ListGlobalTablesInput, arg2 ...request.Option) (*dynamodb.ListGlobalTablesOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListGlobalTablesWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ListGlobalTablesOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListGlobalTablesWithContext indicates an expected call of ListGlobalTablesWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListGlobalTablesWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListGlobalTablesWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListGlobalTablesWithContext), varargs...)
-}
-
-// ListImports mocks base method.
-func (m *MockDynamoAPI) ListImports(arg0 *dynamodb.ListImportsInput) (*dynamodb.ListImportsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListImports", arg0)
- ret0, _ := ret[0].(*dynamodb.ListImportsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListImports indicates an expected call of ListImports.
-func (mr *MockDynamoAPIMockRecorder) ListImports(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImports", reflect.TypeOf((*MockDynamoAPI)(nil).ListImports), arg0)
-}
-
-// ListImportsPages mocks base method.
-func (m *MockDynamoAPI) ListImportsPages(arg0 *dynamodb.ListImportsInput, arg1 func(*dynamodb.ListImportsOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListImportsPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListImportsPages indicates an expected call of ListImportsPages.
-func (mr *MockDynamoAPIMockRecorder) ListImportsPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImportsPages", reflect.TypeOf((*MockDynamoAPI)(nil).ListImportsPages), arg0, arg1)
-}
-
-// ListImportsPagesWithContext mocks base method.
-func (m *MockDynamoAPI) ListImportsPagesWithContext(arg0 context.Context, arg1 *dynamodb.ListImportsInput, arg2 func(*dynamodb.ListImportsOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListImportsPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListImportsPagesWithContext indicates an expected call of ListImportsPagesWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListImportsPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImportsPagesWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListImportsPagesWithContext), varargs...)
-}
-
-// ListImportsRequest mocks base method.
-func (m *MockDynamoAPI) ListImportsRequest(arg0 *dynamodb.ListImportsInput) (*request.Request, *dynamodb.ListImportsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListImportsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ListImportsOutput)
- return ret0, ret1
-}
-
-// ListImportsRequest indicates an expected call of ListImportsRequest.
-func (mr *MockDynamoAPIMockRecorder) ListImportsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImportsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ListImportsRequest), arg0)
-}
-
-// ListImportsWithContext mocks base method.
-func (m *MockDynamoAPI) ListImportsWithContext(arg0 context.Context, arg1 *dynamodb.ListImportsInput, arg2 ...request.Option) (*dynamodb.ListImportsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListImportsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ListImportsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListImportsWithContext indicates an expected call of ListImportsWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListImportsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListImportsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListImportsWithContext), varargs...)
-}
-
-// ListTables mocks base method.
-func (m *MockDynamoAPI) ListTables(arg0 *dynamodb.ListTablesInput) (*dynamodb.ListTablesOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListTables", arg0)
- ret0, _ := ret[0].(*dynamodb.ListTablesOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListTables indicates an expected call of ListTables.
-func (mr *MockDynamoAPIMockRecorder) ListTables(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTables", reflect.TypeOf((*MockDynamoAPI)(nil).ListTables), arg0)
-}
-
-// ListTablesPages mocks base method.
-func (m *MockDynamoAPI) ListTablesPages(arg0 *dynamodb.ListTablesInput, arg1 func(*dynamodb.ListTablesOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListTablesPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListTablesPages indicates an expected call of ListTablesPages.
-func (mr *MockDynamoAPIMockRecorder) ListTablesPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTablesPages", reflect.TypeOf((*MockDynamoAPI)(nil).ListTablesPages), arg0, arg1)
-}
-
-// ListTablesPagesWithContext mocks base method.
-func (m *MockDynamoAPI) ListTablesPagesWithContext(arg0 context.Context, arg1 *dynamodb.ListTablesInput, arg2 func(*dynamodb.ListTablesOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListTablesPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ListTablesPagesWithContext indicates an expected call of ListTablesPagesWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListTablesPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTablesPagesWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListTablesPagesWithContext), varargs...)
-}
-
-// ListTablesRequest mocks base method.
-func (m *MockDynamoAPI) ListTablesRequest(arg0 *dynamodb.ListTablesInput) (*request.Request, *dynamodb.ListTablesOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListTablesRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ListTablesOutput)
- return ret0, ret1
-}
-
-// ListTablesRequest indicates an expected call of ListTablesRequest.
-func (mr *MockDynamoAPIMockRecorder) ListTablesRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTablesRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ListTablesRequest), arg0)
-}
-
-// ListTablesWithContext mocks base method.
-func (m *MockDynamoAPI) ListTablesWithContext(arg0 context.Context, arg1 *dynamodb.ListTablesInput, arg2 ...request.Option) (*dynamodb.ListTablesOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListTablesWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ListTablesOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListTablesWithContext indicates an expected call of ListTablesWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListTablesWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTablesWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListTablesWithContext), varargs...)
-}
-
-// ListTagsOfResource mocks base method.
-func (m *MockDynamoAPI) ListTagsOfResource(arg0 *dynamodb.ListTagsOfResourceInput) (*dynamodb.ListTagsOfResourceOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListTagsOfResource", arg0)
- ret0, _ := ret[0].(*dynamodb.ListTagsOfResourceOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListTagsOfResource indicates an expected call of ListTagsOfResource.
-func (mr *MockDynamoAPIMockRecorder) ListTagsOfResource(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTagsOfResource", reflect.TypeOf((*MockDynamoAPI)(nil).ListTagsOfResource), arg0)
-}
-
-// ListTagsOfResourceRequest mocks base method.
-func (m *MockDynamoAPI) ListTagsOfResourceRequest(arg0 *dynamodb.ListTagsOfResourceInput) (*request.Request, *dynamodb.ListTagsOfResourceOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ListTagsOfResourceRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ListTagsOfResourceOutput)
- return ret0, ret1
-}
-
-// ListTagsOfResourceRequest indicates an expected call of ListTagsOfResourceRequest.
-func (mr *MockDynamoAPIMockRecorder) ListTagsOfResourceRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTagsOfResourceRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ListTagsOfResourceRequest), arg0)
-}
-
-// ListTagsOfResourceWithContext mocks base method.
-func (m *MockDynamoAPI) ListTagsOfResourceWithContext(arg0 context.Context, arg1 *dynamodb.ListTagsOfResourceInput, arg2 ...request.Option) (*dynamodb.ListTagsOfResourceOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ListTagsOfResourceWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ListTagsOfResourceOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ListTagsOfResourceWithContext indicates an expected call of ListTagsOfResourceWithContext.
-func (mr *MockDynamoAPIMockRecorder) ListTagsOfResourceWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTagsOfResourceWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ListTagsOfResourceWithContext), varargs...)
-}
-
-// PutItem mocks base method.
-func (m *MockDynamoAPI) PutItem(arg0 *dynamodb.PutItemInput) (*dynamodb.PutItemOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutItem", arg0)
- ret0, _ := ret[0].(*dynamodb.PutItemOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutItem indicates an expected call of PutItem.
-func (mr *MockDynamoAPIMockRecorder) PutItem(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutItem", reflect.TypeOf((*MockDynamoAPI)(nil).PutItem), arg0)
-}
-
-// PutItemRequest mocks base method.
-func (m *MockDynamoAPI) PutItemRequest(arg0 *dynamodb.PutItemInput) (*request.Request, *dynamodb.PutItemOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "PutItemRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.PutItemOutput)
- return ret0, ret1
-}
-
-// PutItemRequest indicates an expected call of PutItemRequest.
-func (mr *MockDynamoAPIMockRecorder) PutItemRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutItemRequest", reflect.TypeOf((*MockDynamoAPI)(nil).PutItemRequest), arg0)
-}
-
-// PutItemWithContext mocks base method.
-func (m *MockDynamoAPI) PutItemWithContext(arg0 context.Context, arg1 *dynamodb.PutItemInput, arg2 ...request.Option) (*dynamodb.PutItemOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "PutItemWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.PutItemOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// PutItemWithContext indicates an expected call of PutItemWithContext.
-func (mr *MockDynamoAPIMockRecorder) PutItemWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutItemWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).PutItemWithContext), varargs...)
-}
-
-// Query mocks base method.
-func (m *MockDynamoAPI) Query(arg0 *dynamodb.QueryInput) (*dynamodb.QueryOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Query", arg0)
- ret0, _ := ret[0].(*dynamodb.QueryOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// Query indicates an expected call of Query.
-func (mr *MockDynamoAPIMockRecorder) Query(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Query", reflect.TypeOf((*MockDynamoAPI)(nil).Query), arg0)
-}
-
-// QueryPages mocks base method.
-func (m *MockDynamoAPI) QueryPages(arg0 *dynamodb.QueryInput, arg1 func(*dynamodb.QueryOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "QueryPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// QueryPages indicates an expected call of QueryPages.
-func (mr *MockDynamoAPIMockRecorder) QueryPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryPages", reflect.TypeOf((*MockDynamoAPI)(nil).QueryPages), arg0, arg1)
-}
-
-// QueryPagesWithContext mocks base method.
-func (m *MockDynamoAPI) QueryPagesWithContext(arg0 context.Context, arg1 *dynamodb.QueryInput, arg2 func(*dynamodb.QueryOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "QueryPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// QueryPagesWithContext indicates an expected call of QueryPagesWithContext.
-func (mr *MockDynamoAPIMockRecorder) QueryPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryPagesWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).QueryPagesWithContext), varargs...)
-}
-
-// QueryRequest mocks base method.
-func (m *MockDynamoAPI) QueryRequest(arg0 *dynamodb.QueryInput) (*request.Request, *dynamodb.QueryOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "QueryRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.QueryOutput)
- return ret0, ret1
-}
-
-// QueryRequest indicates an expected call of QueryRequest.
-func (mr *MockDynamoAPIMockRecorder) QueryRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryRequest", reflect.TypeOf((*MockDynamoAPI)(nil).QueryRequest), arg0)
-}
-
-// QueryWithContext mocks base method.
-func (m *MockDynamoAPI) QueryWithContext(arg0 context.Context, arg1 *dynamodb.QueryInput, arg2 ...request.Option) (*dynamodb.QueryOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "QueryWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.QueryOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// QueryWithContext indicates an expected call of QueryWithContext.
-func (mr *MockDynamoAPIMockRecorder) QueryWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "QueryWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).QueryWithContext), varargs...)
-}
-
-// RestoreTableFromBackup mocks base method.
-func (m *MockDynamoAPI) RestoreTableFromBackup(arg0 *dynamodb.RestoreTableFromBackupInput) (*dynamodb.RestoreTableFromBackupOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "RestoreTableFromBackup", arg0)
- ret0, _ := ret[0].(*dynamodb.RestoreTableFromBackupOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// RestoreTableFromBackup indicates an expected call of RestoreTableFromBackup.
-func (mr *MockDynamoAPIMockRecorder) RestoreTableFromBackup(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreTableFromBackup", reflect.TypeOf((*MockDynamoAPI)(nil).RestoreTableFromBackup), arg0)
-}
-
-// RestoreTableFromBackupRequest mocks base method.
-func (m *MockDynamoAPI) RestoreTableFromBackupRequest(arg0 *dynamodb.RestoreTableFromBackupInput) (*request.Request, *dynamodb.RestoreTableFromBackupOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "RestoreTableFromBackupRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.RestoreTableFromBackupOutput)
- return ret0, ret1
-}
-
-// RestoreTableFromBackupRequest indicates an expected call of RestoreTableFromBackupRequest.
-func (mr *MockDynamoAPIMockRecorder) RestoreTableFromBackupRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreTableFromBackupRequest", reflect.TypeOf((*MockDynamoAPI)(nil).RestoreTableFromBackupRequest), arg0)
-}
-
-// RestoreTableFromBackupWithContext mocks base method.
-func (m *MockDynamoAPI) RestoreTableFromBackupWithContext(arg0 context.Context, arg1 *dynamodb.RestoreTableFromBackupInput, arg2 ...request.Option) (*dynamodb.RestoreTableFromBackupOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "RestoreTableFromBackupWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.RestoreTableFromBackupOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// RestoreTableFromBackupWithContext indicates an expected call of RestoreTableFromBackupWithContext.
-func (mr *MockDynamoAPIMockRecorder) RestoreTableFromBackupWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreTableFromBackupWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).RestoreTableFromBackupWithContext), varargs...)
-}
-
-// RestoreTableToPointInTime mocks base method.
-func (m *MockDynamoAPI) RestoreTableToPointInTime(arg0 *dynamodb.RestoreTableToPointInTimeInput) (*dynamodb.RestoreTableToPointInTimeOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "RestoreTableToPointInTime", arg0)
- ret0, _ := ret[0].(*dynamodb.RestoreTableToPointInTimeOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// RestoreTableToPointInTime indicates an expected call of RestoreTableToPointInTime.
-func (mr *MockDynamoAPIMockRecorder) RestoreTableToPointInTime(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreTableToPointInTime", reflect.TypeOf((*MockDynamoAPI)(nil).RestoreTableToPointInTime), arg0)
-}
-
-// RestoreTableToPointInTimeRequest mocks base method.
-func (m *MockDynamoAPI) RestoreTableToPointInTimeRequest(arg0 *dynamodb.RestoreTableToPointInTimeInput) (*request.Request, *dynamodb.RestoreTableToPointInTimeOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "RestoreTableToPointInTimeRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.RestoreTableToPointInTimeOutput)
- return ret0, ret1
-}
-
-// RestoreTableToPointInTimeRequest indicates an expected call of RestoreTableToPointInTimeRequest.
-func (mr *MockDynamoAPIMockRecorder) RestoreTableToPointInTimeRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreTableToPointInTimeRequest", reflect.TypeOf((*MockDynamoAPI)(nil).RestoreTableToPointInTimeRequest), arg0)
-}
-
-// RestoreTableToPointInTimeWithContext mocks base method.
-func (m *MockDynamoAPI) RestoreTableToPointInTimeWithContext(arg0 context.Context, arg1 *dynamodb.RestoreTableToPointInTimeInput, arg2 ...request.Option) (*dynamodb.RestoreTableToPointInTimeOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "RestoreTableToPointInTimeWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.RestoreTableToPointInTimeOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// RestoreTableToPointInTimeWithContext indicates an expected call of RestoreTableToPointInTimeWithContext.
-func (mr *MockDynamoAPIMockRecorder) RestoreTableToPointInTimeWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RestoreTableToPointInTimeWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).RestoreTableToPointInTimeWithContext), varargs...)
-}
-
-// Scan mocks base method.
-func (m *MockDynamoAPI) Scan(arg0 *dynamodb.ScanInput) (*dynamodb.ScanOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "Scan", arg0)
- ret0, _ := ret[0].(*dynamodb.ScanOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// Scan indicates an expected call of Scan.
-func (mr *MockDynamoAPIMockRecorder) Scan(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Scan", reflect.TypeOf((*MockDynamoAPI)(nil).Scan), arg0)
-}
-
-// ScanPages mocks base method.
-func (m *MockDynamoAPI) ScanPages(arg0 *dynamodb.ScanInput, arg1 func(*dynamodb.ScanOutput, bool) bool) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ScanPages", arg0, arg1)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ScanPages indicates an expected call of ScanPages.
-func (mr *MockDynamoAPIMockRecorder) ScanPages(arg0, arg1 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanPages", reflect.TypeOf((*MockDynamoAPI)(nil).ScanPages), arg0, arg1)
-}
-
-// ScanPagesWithContext mocks base method.
-func (m *MockDynamoAPI) ScanPagesWithContext(arg0 context.Context, arg1 *dynamodb.ScanInput, arg2 func(*dynamodb.ScanOutput, bool) bool, arg3 ...request.Option) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1, arg2}
- for _, a := range arg3 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ScanPagesWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// ScanPagesWithContext indicates an expected call of ScanPagesWithContext.
-func (mr *MockDynamoAPIMockRecorder) ScanPagesWithContext(arg0, arg1, arg2 any, arg3 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1, arg2}, arg3...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanPagesWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ScanPagesWithContext), varargs...)
-}
-
-// ScanRequest mocks base method.
-func (m *MockDynamoAPI) ScanRequest(arg0 *dynamodb.ScanInput) (*request.Request, *dynamodb.ScanOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ScanRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.ScanOutput)
- return ret0, ret1
-}
-
-// ScanRequest indicates an expected call of ScanRequest.
-func (mr *MockDynamoAPIMockRecorder) ScanRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanRequest", reflect.TypeOf((*MockDynamoAPI)(nil).ScanRequest), arg0)
-}
-
-// ScanWithContext mocks base method.
-func (m *MockDynamoAPI) ScanWithContext(arg0 context.Context, arg1 *dynamodb.ScanInput, arg2 ...request.Option) (*dynamodb.ScanOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "ScanWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.ScanOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// ScanWithContext indicates an expected call of ScanWithContext.
-func (mr *MockDynamoAPIMockRecorder) ScanWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ScanWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).ScanWithContext), varargs...)
-}
-
-// TagResource mocks base method.
-func (m *MockDynamoAPI) TagResource(arg0 *dynamodb.TagResourceInput) (*dynamodb.TagResourceOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "TagResource", arg0)
- ret0, _ := ret[0].(*dynamodb.TagResourceOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// TagResource indicates an expected call of TagResource.
-func (mr *MockDynamoAPIMockRecorder) TagResource(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TagResource", reflect.TypeOf((*MockDynamoAPI)(nil).TagResource), arg0)
-}
-
-// TagResourceRequest mocks base method.
-func (m *MockDynamoAPI) TagResourceRequest(arg0 *dynamodb.TagResourceInput) (*request.Request, *dynamodb.TagResourceOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "TagResourceRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.TagResourceOutput)
- return ret0, ret1
-}
-
-// TagResourceRequest indicates an expected call of TagResourceRequest.
-func (mr *MockDynamoAPIMockRecorder) TagResourceRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TagResourceRequest", reflect.TypeOf((*MockDynamoAPI)(nil).TagResourceRequest), arg0)
-}
-
-// TagResourceWithContext mocks base method.
-func (m *MockDynamoAPI) TagResourceWithContext(arg0 context.Context, arg1 *dynamodb.TagResourceInput, arg2 ...request.Option) (*dynamodb.TagResourceOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "TagResourceWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.TagResourceOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// TagResourceWithContext indicates an expected call of TagResourceWithContext.
-func (mr *MockDynamoAPIMockRecorder) TagResourceWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TagResourceWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).TagResourceWithContext), varargs...)
-}
-
-// TransactGetItems mocks base method.
-func (m *MockDynamoAPI) TransactGetItems(arg0 *dynamodb.TransactGetItemsInput) (*dynamodb.TransactGetItemsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "TransactGetItems", arg0)
- ret0, _ := ret[0].(*dynamodb.TransactGetItemsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// TransactGetItems indicates an expected call of TransactGetItems.
-func (mr *MockDynamoAPIMockRecorder) TransactGetItems(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactGetItems", reflect.TypeOf((*MockDynamoAPI)(nil).TransactGetItems), arg0)
-}
-
-// TransactGetItemsRequest mocks base method.
-func (m *MockDynamoAPI) TransactGetItemsRequest(arg0 *dynamodb.TransactGetItemsInput) (*request.Request, *dynamodb.TransactGetItemsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "TransactGetItemsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.TransactGetItemsOutput)
- return ret0, ret1
-}
-
-// TransactGetItemsRequest indicates an expected call of TransactGetItemsRequest.
-func (mr *MockDynamoAPIMockRecorder) TransactGetItemsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactGetItemsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).TransactGetItemsRequest), arg0)
-}
-
-// TransactGetItemsWithContext mocks base method.
-func (m *MockDynamoAPI) TransactGetItemsWithContext(arg0 context.Context, arg1 *dynamodb.TransactGetItemsInput, arg2 ...request.Option) (*dynamodb.TransactGetItemsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "TransactGetItemsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.TransactGetItemsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// TransactGetItemsWithContext indicates an expected call of TransactGetItemsWithContext.
-func (mr *MockDynamoAPIMockRecorder) TransactGetItemsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactGetItemsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).TransactGetItemsWithContext), varargs...)
-}
-
-// TransactWriteItems mocks base method.
-func (m *MockDynamoAPI) TransactWriteItems(arg0 *dynamodb.TransactWriteItemsInput) (*dynamodb.TransactWriteItemsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "TransactWriteItems", arg0)
- ret0, _ := ret[0].(*dynamodb.TransactWriteItemsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// TransactWriteItems indicates an expected call of TransactWriteItems.
-func (mr *MockDynamoAPIMockRecorder) TransactWriteItems(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactWriteItems", reflect.TypeOf((*MockDynamoAPI)(nil).TransactWriteItems), arg0)
-}
-
-// TransactWriteItemsRequest mocks base method.
-func (m *MockDynamoAPI) TransactWriteItemsRequest(arg0 *dynamodb.TransactWriteItemsInput) (*request.Request, *dynamodb.TransactWriteItemsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "TransactWriteItemsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.TransactWriteItemsOutput)
- return ret0, ret1
-}
-
-// TransactWriteItemsRequest indicates an expected call of TransactWriteItemsRequest.
-func (mr *MockDynamoAPIMockRecorder) TransactWriteItemsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactWriteItemsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).TransactWriteItemsRequest), arg0)
-}
-
-// TransactWriteItemsWithContext mocks base method.
-func (m *MockDynamoAPI) TransactWriteItemsWithContext(arg0 context.Context, arg1 *dynamodb.TransactWriteItemsInput, arg2 ...request.Option) (*dynamodb.TransactWriteItemsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "TransactWriteItemsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.TransactWriteItemsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// TransactWriteItemsWithContext indicates an expected call of TransactWriteItemsWithContext.
-func (mr *MockDynamoAPIMockRecorder) TransactWriteItemsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactWriteItemsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).TransactWriteItemsWithContext), varargs...)
-}
-
-// UntagResource mocks base method.
-func (m *MockDynamoAPI) UntagResource(arg0 *dynamodb.UntagResourceInput) (*dynamodb.UntagResourceOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UntagResource", arg0)
- ret0, _ := ret[0].(*dynamodb.UntagResourceOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UntagResource indicates an expected call of UntagResource.
-func (mr *MockDynamoAPIMockRecorder) UntagResource(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UntagResource", reflect.TypeOf((*MockDynamoAPI)(nil).UntagResource), arg0)
-}
-
-// UntagResourceRequest mocks base method.
-func (m *MockDynamoAPI) UntagResourceRequest(arg0 *dynamodb.UntagResourceInput) (*request.Request, *dynamodb.UntagResourceOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UntagResourceRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.UntagResourceOutput)
- return ret0, ret1
-}
-
-// UntagResourceRequest indicates an expected call of UntagResourceRequest.
-func (mr *MockDynamoAPIMockRecorder) UntagResourceRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UntagResourceRequest", reflect.TypeOf((*MockDynamoAPI)(nil).UntagResourceRequest), arg0)
-}
-
-// UntagResourceWithContext mocks base method.
-func (m *MockDynamoAPI) UntagResourceWithContext(arg0 context.Context, arg1 *dynamodb.UntagResourceInput, arg2 ...request.Option) (*dynamodb.UntagResourceOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UntagResourceWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.UntagResourceOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UntagResourceWithContext indicates an expected call of UntagResourceWithContext.
-func (mr *MockDynamoAPIMockRecorder) UntagResourceWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UntagResourceWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).UntagResourceWithContext), varargs...)
-}
-
-// UpdateContinuousBackups mocks base method.
-func (m *MockDynamoAPI) UpdateContinuousBackups(arg0 *dynamodb.UpdateContinuousBackupsInput) (*dynamodb.UpdateContinuousBackupsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateContinuousBackups", arg0)
- ret0, _ := ret[0].(*dynamodb.UpdateContinuousBackupsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateContinuousBackups indicates an expected call of UpdateContinuousBackups.
-func (mr *MockDynamoAPIMockRecorder) UpdateContinuousBackups(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContinuousBackups", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateContinuousBackups), arg0)
-}
-
-// UpdateContinuousBackupsRequest mocks base method.
-func (m *MockDynamoAPI) UpdateContinuousBackupsRequest(arg0 *dynamodb.UpdateContinuousBackupsInput) (*request.Request, *dynamodb.UpdateContinuousBackupsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateContinuousBackupsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.UpdateContinuousBackupsOutput)
- return ret0, ret1
-}
-
-// UpdateContinuousBackupsRequest indicates an expected call of UpdateContinuousBackupsRequest.
-func (mr *MockDynamoAPIMockRecorder) UpdateContinuousBackupsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContinuousBackupsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateContinuousBackupsRequest), arg0)
-}
-
-// UpdateContinuousBackupsWithContext mocks base method.
-func (m *MockDynamoAPI) UpdateContinuousBackupsWithContext(arg0 context.Context, arg1 *dynamodb.UpdateContinuousBackupsInput, arg2 ...request.Option) (*dynamodb.UpdateContinuousBackupsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UpdateContinuousBackupsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.UpdateContinuousBackupsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateContinuousBackupsWithContext indicates an expected call of UpdateContinuousBackupsWithContext.
-func (mr *MockDynamoAPIMockRecorder) UpdateContinuousBackupsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContinuousBackupsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateContinuousBackupsWithContext), varargs...)
-}
-
-// UpdateContributorInsights mocks base method.
-func (m *MockDynamoAPI) UpdateContributorInsights(arg0 *dynamodb.UpdateContributorInsightsInput) (*dynamodb.UpdateContributorInsightsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateContributorInsights", arg0)
- ret0, _ := ret[0].(*dynamodb.UpdateContributorInsightsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateContributorInsights indicates an expected call of UpdateContributorInsights.
-func (mr *MockDynamoAPIMockRecorder) UpdateContributorInsights(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContributorInsights", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateContributorInsights), arg0)
-}
-
-// UpdateContributorInsightsRequest mocks base method.
-func (m *MockDynamoAPI) UpdateContributorInsightsRequest(arg0 *dynamodb.UpdateContributorInsightsInput) (*request.Request, *dynamodb.UpdateContributorInsightsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateContributorInsightsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.UpdateContributorInsightsOutput)
- return ret0, ret1
-}
-
-// UpdateContributorInsightsRequest indicates an expected call of UpdateContributorInsightsRequest.
-func (mr *MockDynamoAPIMockRecorder) UpdateContributorInsightsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContributorInsightsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateContributorInsightsRequest), arg0)
-}
-
-// UpdateContributorInsightsWithContext mocks base method.
-func (m *MockDynamoAPI) UpdateContributorInsightsWithContext(arg0 context.Context, arg1 *dynamodb.UpdateContributorInsightsInput, arg2 ...request.Option) (*dynamodb.UpdateContributorInsightsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UpdateContributorInsightsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.UpdateContributorInsightsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateContributorInsightsWithContext indicates an expected call of UpdateContributorInsightsWithContext.
-func (mr *MockDynamoAPIMockRecorder) UpdateContributorInsightsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateContributorInsightsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateContributorInsightsWithContext), varargs...)
-}
-
-// UpdateGlobalTable mocks base method.
-func (m *MockDynamoAPI) UpdateGlobalTable(arg0 *dynamodb.UpdateGlobalTableInput) (*dynamodb.UpdateGlobalTableOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateGlobalTable", arg0)
- ret0, _ := ret[0].(*dynamodb.UpdateGlobalTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateGlobalTable indicates an expected call of UpdateGlobalTable.
-func (mr *MockDynamoAPIMockRecorder) UpdateGlobalTable(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGlobalTable", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateGlobalTable), arg0)
-}
-
-// UpdateGlobalTableRequest mocks base method.
-func (m *MockDynamoAPI) UpdateGlobalTableRequest(arg0 *dynamodb.UpdateGlobalTableInput) (*request.Request, *dynamodb.UpdateGlobalTableOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateGlobalTableRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.UpdateGlobalTableOutput)
- return ret0, ret1
-}
-
-// UpdateGlobalTableRequest indicates an expected call of UpdateGlobalTableRequest.
-func (mr *MockDynamoAPIMockRecorder) UpdateGlobalTableRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGlobalTableRequest", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateGlobalTableRequest), arg0)
-}
-
-// UpdateGlobalTableSettings mocks base method.
-func (m *MockDynamoAPI) UpdateGlobalTableSettings(arg0 *dynamodb.UpdateGlobalTableSettingsInput) (*dynamodb.UpdateGlobalTableSettingsOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateGlobalTableSettings", arg0)
- ret0, _ := ret[0].(*dynamodb.UpdateGlobalTableSettingsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateGlobalTableSettings indicates an expected call of UpdateGlobalTableSettings.
-func (mr *MockDynamoAPIMockRecorder) UpdateGlobalTableSettings(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGlobalTableSettings", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateGlobalTableSettings), arg0)
-}
-
-// UpdateGlobalTableSettingsRequest mocks base method.
-func (m *MockDynamoAPI) UpdateGlobalTableSettingsRequest(arg0 *dynamodb.UpdateGlobalTableSettingsInput) (*request.Request, *dynamodb.UpdateGlobalTableSettingsOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateGlobalTableSettingsRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.UpdateGlobalTableSettingsOutput)
- return ret0, ret1
-}
-
-// UpdateGlobalTableSettingsRequest indicates an expected call of UpdateGlobalTableSettingsRequest.
-func (mr *MockDynamoAPIMockRecorder) UpdateGlobalTableSettingsRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGlobalTableSettingsRequest", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateGlobalTableSettingsRequest), arg0)
-}
-
-// UpdateGlobalTableSettingsWithContext mocks base method.
-func (m *MockDynamoAPI) UpdateGlobalTableSettingsWithContext(arg0 context.Context, arg1 *dynamodb.UpdateGlobalTableSettingsInput, arg2 ...request.Option) (*dynamodb.UpdateGlobalTableSettingsOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UpdateGlobalTableSettingsWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.UpdateGlobalTableSettingsOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateGlobalTableSettingsWithContext indicates an expected call of UpdateGlobalTableSettingsWithContext.
-func (mr *MockDynamoAPIMockRecorder) UpdateGlobalTableSettingsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGlobalTableSettingsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateGlobalTableSettingsWithContext), varargs...)
-}
-
-// UpdateGlobalTableWithContext mocks base method.
-func (m *MockDynamoAPI) UpdateGlobalTableWithContext(arg0 context.Context, arg1 *dynamodb.UpdateGlobalTableInput, arg2 ...request.Option) (*dynamodb.UpdateGlobalTableOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UpdateGlobalTableWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.UpdateGlobalTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateGlobalTableWithContext indicates an expected call of UpdateGlobalTableWithContext.
-func (mr *MockDynamoAPIMockRecorder) UpdateGlobalTableWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateGlobalTableWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateGlobalTableWithContext), varargs...)
-}
-
-// UpdateItem mocks base method.
-func (m *MockDynamoAPI) UpdateItem(arg0 *dynamodb.UpdateItemInput) (*dynamodb.UpdateItemOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateItem", arg0)
- ret0, _ := ret[0].(*dynamodb.UpdateItemOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateItem indicates an expected call of UpdateItem.
-func (mr *MockDynamoAPIMockRecorder) UpdateItem(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateItem", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateItem), arg0)
-}
-
-// UpdateItemRequest mocks base method.
-func (m *MockDynamoAPI) UpdateItemRequest(arg0 *dynamodb.UpdateItemInput) (*request.Request, *dynamodb.UpdateItemOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateItemRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.UpdateItemOutput)
- return ret0, ret1
-}
-
-// UpdateItemRequest indicates an expected call of UpdateItemRequest.
-func (mr *MockDynamoAPIMockRecorder) UpdateItemRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateItemRequest", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateItemRequest), arg0)
-}
-
-// UpdateItemWithContext mocks base method.
-func (m *MockDynamoAPI) UpdateItemWithContext(arg0 context.Context, arg1 *dynamodb.UpdateItemInput, arg2 ...request.Option) (*dynamodb.UpdateItemOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UpdateItemWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.UpdateItemOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateItemWithContext indicates an expected call of UpdateItemWithContext.
-func (mr *MockDynamoAPIMockRecorder) UpdateItemWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateItemWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateItemWithContext), varargs...)
-}
-
-// UpdateKinesisStreamingDestination mocks base method.
-func (m *MockDynamoAPI) UpdateKinesisStreamingDestination(arg0 *dynamodb.UpdateKinesisStreamingDestinationInput) (*dynamodb.UpdateKinesisStreamingDestinationOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateKinesisStreamingDestination", arg0)
- ret0, _ := ret[0].(*dynamodb.UpdateKinesisStreamingDestinationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateKinesisStreamingDestination indicates an expected call of UpdateKinesisStreamingDestination.
-func (mr *MockDynamoAPIMockRecorder) UpdateKinesisStreamingDestination(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateKinesisStreamingDestination", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateKinesisStreamingDestination), arg0)
-}
-
-// UpdateKinesisStreamingDestinationRequest mocks base method.
-func (m *MockDynamoAPI) UpdateKinesisStreamingDestinationRequest(arg0 *dynamodb.UpdateKinesisStreamingDestinationInput) (*request.Request, *dynamodb.UpdateKinesisStreamingDestinationOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateKinesisStreamingDestinationRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.UpdateKinesisStreamingDestinationOutput)
- return ret0, ret1
-}
-
-// UpdateKinesisStreamingDestinationRequest indicates an expected call of UpdateKinesisStreamingDestinationRequest.
-func (mr *MockDynamoAPIMockRecorder) UpdateKinesisStreamingDestinationRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateKinesisStreamingDestinationRequest", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateKinesisStreamingDestinationRequest), arg0)
-}
-
-// UpdateKinesisStreamingDestinationWithContext mocks base method.
-func (m *MockDynamoAPI) UpdateKinesisStreamingDestinationWithContext(arg0 context.Context, arg1 *dynamodb.UpdateKinesisStreamingDestinationInput, arg2 ...request.Option) (*dynamodb.UpdateKinesisStreamingDestinationOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UpdateKinesisStreamingDestinationWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.UpdateKinesisStreamingDestinationOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateKinesisStreamingDestinationWithContext indicates an expected call of UpdateKinesisStreamingDestinationWithContext.
-func (mr *MockDynamoAPIMockRecorder) UpdateKinesisStreamingDestinationWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateKinesisStreamingDestinationWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateKinesisStreamingDestinationWithContext), varargs...)
-}
-
-// UpdateTable mocks base method.
-func (m *MockDynamoAPI) UpdateTable(arg0 *dynamodb.UpdateTableInput) (*dynamodb.UpdateTableOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateTable", arg0)
- ret0, _ := ret[0].(*dynamodb.UpdateTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateTable indicates an expected call of UpdateTable.
-func (mr *MockDynamoAPIMockRecorder) UpdateTable(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTable", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateTable), arg0)
-}
-
-// UpdateTableReplicaAutoScaling mocks base method.
-func (m *MockDynamoAPI) UpdateTableReplicaAutoScaling(arg0 *dynamodb.UpdateTableReplicaAutoScalingInput) (*dynamodb.UpdateTableReplicaAutoScalingOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateTableReplicaAutoScaling", arg0)
- ret0, _ := ret[0].(*dynamodb.UpdateTableReplicaAutoScalingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateTableReplicaAutoScaling indicates an expected call of UpdateTableReplicaAutoScaling.
-func (mr *MockDynamoAPIMockRecorder) UpdateTableReplicaAutoScaling(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTableReplicaAutoScaling", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateTableReplicaAutoScaling), arg0)
-}
-
-// UpdateTableReplicaAutoScalingRequest mocks base method.
-func (m *MockDynamoAPI) UpdateTableReplicaAutoScalingRequest(arg0 *dynamodb.UpdateTableReplicaAutoScalingInput) (*request.Request, *dynamodb.UpdateTableReplicaAutoScalingOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateTableReplicaAutoScalingRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.UpdateTableReplicaAutoScalingOutput)
- return ret0, ret1
-}
-
-// UpdateTableReplicaAutoScalingRequest indicates an expected call of UpdateTableReplicaAutoScalingRequest.
-func (mr *MockDynamoAPIMockRecorder) UpdateTableReplicaAutoScalingRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTableReplicaAutoScalingRequest", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateTableReplicaAutoScalingRequest), arg0)
-}
-
-// UpdateTableReplicaAutoScalingWithContext mocks base method.
-func (m *MockDynamoAPI) UpdateTableReplicaAutoScalingWithContext(arg0 context.Context, arg1 *dynamodb.UpdateTableReplicaAutoScalingInput, arg2 ...request.Option) (*dynamodb.UpdateTableReplicaAutoScalingOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UpdateTableReplicaAutoScalingWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.UpdateTableReplicaAutoScalingOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateTableReplicaAutoScalingWithContext indicates an expected call of UpdateTableReplicaAutoScalingWithContext.
-func (mr *MockDynamoAPIMockRecorder) UpdateTableReplicaAutoScalingWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTableReplicaAutoScalingWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateTableReplicaAutoScalingWithContext), varargs...)
-}
-
-// UpdateTableRequest mocks base method.
-func (m *MockDynamoAPI) UpdateTableRequest(arg0 *dynamodb.UpdateTableInput) (*request.Request, *dynamodb.UpdateTableOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateTableRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.UpdateTableOutput)
- return ret0, ret1
-}
-
-// UpdateTableRequest indicates an expected call of UpdateTableRequest.
-func (mr *MockDynamoAPIMockRecorder) UpdateTableRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTableRequest", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateTableRequest), arg0)
-}
-
-// UpdateTableWithContext mocks base method.
-func (m *MockDynamoAPI) UpdateTableWithContext(arg0 context.Context, arg1 *dynamodb.UpdateTableInput, arg2 ...request.Option) (*dynamodb.UpdateTableOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UpdateTableWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.UpdateTableOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateTableWithContext indicates an expected call of UpdateTableWithContext.
-func (mr *MockDynamoAPIMockRecorder) UpdateTableWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTableWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateTableWithContext), varargs...)
-}
-
-// UpdateTimeToLive mocks base method.
-func (m *MockDynamoAPI) UpdateTimeToLive(arg0 *dynamodb.UpdateTimeToLiveInput) (*dynamodb.UpdateTimeToLiveOutput, error) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateTimeToLive", arg0)
- ret0, _ := ret[0].(*dynamodb.UpdateTimeToLiveOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateTimeToLive indicates an expected call of UpdateTimeToLive.
-func (mr *MockDynamoAPIMockRecorder) UpdateTimeToLive(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTimeToLive", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateTimeToLive), arg0)
-}
-
-// UpdateTimeToLiveRequest mocks base method.
-func (m *MockDynamoAPI) UpdateTimeToLiveRequest(arg0 *dynamodb.UpdateTimeToLiveInput) (*request.Request, *dynamodb.UpdateTimeToLiveOutput) {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "UpdateTimeToLiveRequest", arg0)
- ret0, _ := ret[0].(*request.Request)
- ret1, _ := ret[1].(*dynamodb.UpdateTimeToLiveOutput)
- return ret0, ret1
-}
-
-// UpdateTimeToLiveRequest indicates an expected call of UpdateTimeToLiveRequest.
-func (mr *MockDynamoAPIMockRecorder) UpdateTimeToLiveRequest(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTimeToLiveRequest", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateTimeToLiveRequest), arg0)
-}
-
-// UpdateTimeToLiveWithContext mocks base method.
-func (m *MockDynamoAPI) UpdateTimeToLiveWithContext(arg0 context.Context, arg1 *dynamodb.UpdateTimeToLiveInput, arg2 ...request.Option) (*dynamodb.UpdateTimeToLiveOutput, error) {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "UpdateTimeToLiveWithContext", varargs...)
- ret0, _ := ret[0].(*dynamodb.UpdateTimeToLiveOutput)
- ret1, _ := ret[1].(error)
- return ret0, ret1
-}
-
-// UpdateTimeToLiveWithContext indicates an expected call of UpdateTimeToLiveWithContext.
-func (mr *MockDynamoAPIMockRecorder) UpdateTimeToLiveWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateTimeToLiveWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).UpdateTimeToLiveWithContext), varargs...)
-}
-
-// WaitUntilTableExists mocks base method.
-func (m *MockDynamoAPI) WaitUntilTableExists(arg0 *dynamodb.DescribeTableInput) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "WaitUntilTableExists", arg0)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilTableExists indicates an expected call of WaitUntilTableExists.
-func (mr *MockDynamoAPIMockRecorder) WaitUntilTableExists(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilTableExists", reflect.TypeOf((*MockDynamoAPI)(nil).WaitUntilTableExists), arg0)
-}
-
-// WaitUntilTableExistsWithContext mocks base method.
-func (m *MockDynamoAPI) WaitUntilTableExistsWithContext(arg0 context.Context, arg1 *dynamodb.DescribeTableInput, arg2 ...request.WaiterOption) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "WaitUntilTableExistsWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilTableExistsWithContext indicates an expected call of WaitUntilTableExistsWithContext.
-func (mr *MockDynamoAPIMockRecorder) WaitUntilTableExistsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilTableExistsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).WaitUntilTableExistsWithContext), varargs...)
-}
-
-// WaitUntilTableNotExists mocks base method.
-func (m *MockDynamoAPI) WaitUntilTableNotExists(arg0 *dynamodb.DescribeTableInput) error {
- m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "WaitUntilTableNotExists", arg0)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilTableNotExists indicates an expected call of WaitUntilTableNotExists.
-func (mr *MockDynamoAPIMockRecorder) WaitUntilTableNotExists(arg0 any) *gomock.Call {
- mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilTableNotExists", reflect.TypeOf((*MockDynamoAPI)(nil).WaitUntilTableNotExists), arg0)
-}
-
-// WaitUntilTableNotExistsWithContext mocks base method.
-func (m *MockDynamoAPI) WaitUntilTableNotExistsWithContext(arg0 context.Context, arg1 *dynamodb.DescribeTableInput, arg2 ...request.WaiterOption) error {
- m.ctrl.T.Helper()
- varargs := []any{arg0, arg1}
- for _, a := range arg2 {
- varargs = append(varargs, a)
- }
- ret := m.ctrl.Call(m, "WaitUntilTableNotExistsWithContext", varargs...)
- ret0, _ := ret[0].(error)
- return ret0
-}
-
-// WaitUntilTableNotExistsWithContext indicates an expected call of WaitUntilTableNotExistsWithContext.
-func (mr *MockDynamoAPIMockRecorder) WaitUntilTableNotExistsWithContext(arg0, arg1 any, arg2 ...any) *gomock.Call {
+// TransactWriteItems indicates an expected call of TransactWriteItems.
+func (mr *MockDynamoAPIMockRecorder) TransactWriteItems(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
varargs := append([]any{arg0, arg1}, arg2...)
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "WaitUntilTableNotExistsWithContext", reflect.TypeOf((*MockDynamoAPI)(nil).WaitUntilTableNotExistsWithContext), varargs...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TransactWriteItems", reflect.TypeOf((*MockDynamoAPI)(nil).TransactWriteItems), varargs...)
}
diff --git a/internal/storage/metastorage/dynamodb/model/block_metadata.go b/internal/storage/metastorage/dynamodb/model/block_metadata.go
index 5ab2f335..04fa0f21 100644
--- a/internal/storage/metastorage/dynamodb/model/block_metadata.go
+++ b/internal/storage/metastorage/dynamodb/model/block_metadata.go
@@ -6,28 +6,36 @@ import (
)
type BlockMetaDataDDBEntry struct {
- BlockPid string `dynamodbav:"block_pid"`
- BlockRid string `dynamodbav:"block_rid"`
- Tag uint32 `dynamodbav:"tag"`
- Hash string `dynamodbav:"block_hash"`
- ParentHash string `dynamodbav:"parent_hash"`
- Height uint64 `dynamodbav:"height"`
- ParentHeight uint64 `dynamodbav:"parent_height"`
- ObjectKeyMain string `dynamodbav:"object_key_main"`
- Skipped bool `dynamodbav:"skipped"`
- Timestamp int64 `dynamodbav:"timestamp"`
+ BlockPid string `dynamodbav:"block_pid"`
+ BlockRid string `dynamodbav:"block_rid"`
+ Tag uint32 `dynamodbav:"tag"`
+ Hash string `dynamodbav:"block_hash"`
+ ParentHash string `dynamodbav:"parent_hash"`
+ Height uint64 `dynamodbav:"height"`
+ ParentHeight uint64 `dynamodbav:"parent_height"`
+ ObjectKeyMain string `dynamodbav:"object_key_main"`
+ Skipped bool `dynamodbav:"skipped"`
+ Timestamp int64 `dynamodbav:"timestamp"`
+ ObjectFormat int32 `dynamodbav:"object_format,omitempty"`
+ ByteOffset uint64 `dynamodbav:"byte_offset,omitempty"`
+ ByteLength uint64 `dynamodbav:"byte_length,omitempty"`
+ UncompressedLength uint64 `dynamodbav:"uncompressed_length,omitempty"`
}
func BlockMetadataToProto(bm *BlockMetaDataDDBEntry) *api.BlockMetadata {
v := &api.BlockMetadata{
- Tag: bm.Tag,
- Hash: bm.Hash,
- ParentHash: bm.ParentHash,
- Height: bm.Height,
- ParentHeight: bm.ParentHeight,
- ObjectKeyMain: bm.ObjectKeyMain,
- Skipped: bm.Skipped,
- Timestamp: utils.ToTimestamp(bm.Timestamp),
+ Tag: bm.Tag,
+ Hash: bm.Hash,
+ ParentHash: bm.ParentHash,
+ Height: bm.Height,
+ ParentHeight: bm.ParentHeight,
+ ObjectKeyMain: bm.ObjectKeyMain,
+ Skipped: bm.Skipped,
+ Timestamp: utils.ToTimestamp(bm.Timestamp),
+ ObjectFormat: api.BlockObjectFormat(bm.ObjectFormat),
+ ByteOffset: bm.ByteOffset,
+ ByteLength: bm.ByteLength,
+ UncompressedLength: bm.UncompressedLength,
}
return v
diff --git a/internal/storage/metastorage/dynamodb/model/block_metadata_test.go b/internal/storage/metastorage/dynamodb/model/block_metadata_test.go
new file mode 100644
index 00000000..fe82f93e
--- /dev/null
+++ b/internal/storage/metastorage/dynamodb/model/block_metadata_test.go
@@ -0,0 +1,52 @@
+package model
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestBlockMetadataToProto_ConsolidatedFields(t *testing.T) {
+ entry := &BlockMetaDataDDBEntry{
+ Tag: 1,
+ Hash: "0xabc",
+ ParentHash: "0xdef",
+ Height: 100,
+ ParentHeight: 99,
+ ObjectKeyMain: "consolidated/v=1/shard=00000000000000000000-00000000000000009999/00000000000000000100-00000000000000000199-deadbeef.cscb.zstd",
+ Timestamp: 123456,
+ ObjectFormat: int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH),
+ ByteOffset: 4096,
+ ByteLength: 8192,
+ UncompressedLength: 8192,
+ }
+
+ actual := BlockMetadataToProto(entry)
+
+ require.Equal(t, api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, actual.GetObjectFormat())
+ require.Equal(t, uint64(4096), actual.GetByteOffset())
+ require.Equal(t, uint64(8192), actual.GetByteLength())
+ require.Equal(t, uint64(8192), actual.GetUncompressedLength())
+ require.Equal(t, entry.ObjectKeyMain, actual.GetObjectKeyMain())
+}
+
+func TestBlockMetadataToProto_SingleBlockMissingFields(t *testing.T) {
+ entry := &BlockMetaDataDDBEntry{
+ Tag: 1,
+ Hash: "0xabc",
+ ParentHash: "0xdef",
+ Height: 100,
+ ParentHeight: 99,
+ ObjectKeyMain: "1/100/0xabc.gzip",
+ Timestamp: 123456,
+ }
+
+ actual := BlockMetadataToProto(entry)
+
+ require.Equal(t, api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK, actual.GetObjectFormat())
+ require.Zero(t, actual.GetByteOffset())
+ require.Zero(t, actual.GetByteLength())
+ require.Zero(t, actual.GetUncompressedLength())
+}
diff --git a/internal/storage/metastorage/dynamodb/transaction_storage.go b/internal/storage/metastorage/dynamodb/transaction_storage.go
index 0af445d7..46ebf494 100644
--- a/internal/storage/metastorage/dynamodb/transaction_storage.go
+++ b/internal/storage/metastorage/dynamodb/transaction_storage.go
@@ -4,9 +4,9 @@ import (
"context"
"reflect"
- "github.com/aws/aws-sdk-go/aws"
- "github.com/aws/aws-sdk-go/service/dynamodb"
- "github.com/aws/aws-sdk-go/service/dynamodb/expression"
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/feature/dynamodb/expression"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"golang.org/x/xerrors"
ddbmodel "github.com/coinbase/chainstorage/internal/storage/metastorage/dynamodb/model"
@@ -26,7 +26,7 @@ type (
var _ internal.TransactionStorage = (*transactionStorageImpl)(nil)
func newTransactionStorage(params Params) (internal.TransactionStorage, error) {
- attrDefs := []*dynamodb.AttributeDefinition{
+ attrDefs := []types.AttributeDefinition{
{
AttributeName: aws.String(ddbmodel.TransactionPidKeyName),
AttributeType: awsStringType,
@@ -36,7 +36,7 @@ func newTransactionStorage(params Params) (internal.TransactionStorage, error) {
AttributeType: awsStringType,
},
}
- keySchema := []*dynamodb.KeySchemaElement{
+ keySchema := []types.KeySchemaElement{
{
AttributeName: aws.String(ddbmodel.TransactionPidKeyName),
KeyType: hashKeyType,
diff --git a/internal/storage/metastorage/firestore/block_storage.go b/internal/storage/metastorage/firestore/block_storage.go
index b36572aa..d8ab0f9b 100644
--- a/internal/storage/metastorage/firestore/block_storage.go
+++ b/internal/storage/metastorage/firestore/block_storage.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"sort"
+ "time"
"cloud.google.com/go/firestore"
"golang.org/x/xerrors"
@@ -245,6 +246,51 @@ func (b *blockStorageImpl) validateHeight(height uint64) error {
return nil
}
+func (b *blockStorageImpl) GetBlockByTimestamp(ctx context.Context, tag uint32, timestamp uint64) (*chainstorage.BlockMetadata, error) {
+ // Placeholder implementation - return error indicating not implemented
+ return nil, xerrors.New("GetBlockByTimestamp not implemented for Firestore")
+}
+
+func (b *blockStorageImpl) GetBlockConsolidationShadow(ctx context.Context, block *chainstorage.BlockMetadata) (*chainstorage.BlockMetadata, error) {
+ return nil, xerrors.New("GetBlockConsolidationShadow not implemented for Firestore")
+}
+
+func (b *blockStorageImpl) GetBlocksConsolidationShadow(ctx context.Context, blocks []*chainstorage.BlockMetadata) ([]*chainstorage.BlockMetadata, error) {
+ return nil, xerrors.New("GetBlocksConsolidationShadow not implemented for Firestore")
+}
+
+func (b *blockStorageImpl) GetBlocksMissingConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64, limit uint64) ([]*internal.BlockMetadataRecord, error) {
+ return nil, xerrors.New("GetBlocksMissingConsolidationShadow not implemented for Firestore")
+}
+
+func (b *blockStorageImpl) GetBlockConsolidationShadowStats(ctx context.Context, tag uint32, startHeight, endHeight uint64) (*internal.ConsolidationShadowStats, error) {
+ return nil, xerrors.New("GetBlockConsolidationShadowStats not implemented for Firestore")
+}
+
+func (b *blockStorageImpl) GetFirstBlockMissingConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64) (uint64, bool, error) {
+ return 0, false, xerrors.New("GetFirstBlockMissingConsolidationShadow not implemented for Firestore")
+}
+
+func (b *blockStorageImpl) GetFirstPromotableBlockConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64) (uint64, bool, error) {
+ return 0, false, xerrors.New("GetFirstPromotableBlockConsolidationShadow not implemented for Firestore")
+}
+
+func (b *blockStorageImpl) GetBlockConsolidationCursor(ctx context.Context, name string, tag uint32) (uint64, bool, error) {
+ return 0, false, xerrors.New("GetBlockConsolidationCursor not implemented for Firestore")
+}
+
+func (b *blockStorageImpl) SetBlockConsolidationCursor(ctx context.Context, name string, tag uint32, height uint64) error {
+ return xerrors.New("SetBlockConsolidationCursor not implemented for Firestore")
+}
+
+func (b *blockStorageImpl) PersistBlockConsolidationShadows(ctx context.Context, placements []*internal.ConsolidationShadowPlacement) error {
+ return xerrors.New("PersistBlockConsolidationShadows not implemented for Firestore")
+}
+
+func (b *blockStorageImpl) PromoteBlockConsolidationShadows(ctx context.Context, tag uint32, startHeight, endHeight uint64, limit uint64, singleBlockObjectRetention time.Duration) (*internal.ConsolidationPromotionResult, error) {
+ return nil, xerrors.New("PromoteBlockConsolidationShadows not implemented for Firestore")
+}
+
func (b *blockStorageImpl) getLatestBlockDocRef(tag uint32) *firestore.DocumentRef {
return b.client.Doc(fmt.Sprintf("env/%s/blocks/%d-latest", b.env, tag))
}
diff --git a/internal/storage/metastorage/firestore/block_storage_integration_test.go b/internal/storage/metastorage/firestore/block_storage_integration_test.go
index 5caf68e0..f3eb2016 100644
--- a/internal/storage/metastorage/firestore/block_storage_integration_test.go
+++ b/internal/storage/metastorage/firestore/block_storage_integration_test.go
@@ -312,6 +312,7 @@ func (s *blockStorageTestSuite) equalProto(x, y any) {
}
func TestIntegrationBlockStorageTestSuite(t *testing.T) {
+ t.Skip()
// TODO: speed up the tests before re-enabling TestAllEnvs.
// testapp.TestAllEnvs(t, func(t *testing.T, cfg *config.Config) {
// suite.Run(t, &blockStorageTestSuite{config: cfg})
diff --git a/internal/storage/metastorage/firestore/event_storage_integration_test.go b/internal/storage/metastorage/firestore/event_storage_integration_test.go
index d7dcdd65..c84e1986 100644
--- a/internal/storage/metastorage/firestore/event_storage_integration_test.go
+++ b/internal/storage/metastorage/firestore/event_storage_integration_test.go
@@ -423,6 +423,7 @@ func (s *eventStorageTestSuite) TestGetEventsByBlockHeight() {
}
func TestIntegrationEventStorageTestSuite(t *testing.T) {
+ t.Skip()
require := testutil.Require(t)
// Test with eth-mainnet for stream version
cfg, err := config.New()
diff --git a/internal/storage/metastorage/firestore/meta_storage.go b/internal/storage/metastorage/firestore/meta_storage.go
index 8350656b..c01ff3a6 100644
--- a/internal/storage/metastorage/firestore/meta_storage.go
+++ b/internal/storage/metastorage/firestore/meta_storage.go
@@ -11,6 +11,16 @@ import (
"github.com/coinbase/chainstorage/internal/utils/fxparams"
)
+func (m *metaStorageImpl) AcquireSingleBlockUploadGuard(
+ ctx context.Context,
+ tag uint32,
+ height uint64,
+ hash string,
+) (internal.SingleBlockUploadGuard, error) {
+ // Durable retirement is Postgres-only, so Firestore has no retirement fence.
+ return internal.NewUnfencedSingleBlockUploadGuard(), nil
+}
+
type (
metaStorageImpl struct {
internal.BlockStorage
diff --git a/internal/storage/metastorage/internal/meta_storage.go b/internal/storage/metastorage/internal/meta_storage.go
index 70a044b6..71f1c15a 100644
--- a/internal/storage/metastorage/internal/meta_storage.go
+++ b/internal/storage/metastorage/internal/meta_storage.go
@@ -2,6 +2,7 @@ package internal
import (
"context"
+ "time"
"go.uber.org/fx"
"golang.org/x/xerrors"
@@ -22,6 +23,18 @@ type (
// GetBlocksByHeights gets blocks by heights. Results is an ordered array that matches the order in `heights` array
// i.e. if the heights is [100,2,3], it will return the metadata in order: [block 100, block 2, block 3]
GetBlocksByHeights(ctx context.Context, tag uint32, heights []uint64) ([]*api.BlockMetadata, error)
+ // GetBlockByTimestamp gets the latest block before or at the given timestamp
+ GetBlockByTimestamp(ctx context.Context, tag uint32, timestamp uint64) (*api.BlockMetadata, error)
+ GetBlockConsolidationShadow(ctx context.Context, block *api.BlockMetadata) (*api.BlockMetadata, error)
+ GetBlocksConsolidationShadow(ctx context.Context, blocks []*api.BlockMetadata) ([]*api.BlockMetadata, error)
+ GetBlocksMissingConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64, limit uint64) ([]*BlockMetadataRecord, error)
+ GetBlockConsolidationShadowStats(ctx context.Context, tag uint32, startHeight, endHeight uint64) (*ConsolidationShadowStats, error)
+ GetFirstBlockMissingConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64) (uint64, bool, error)
+ GetFirstPromotableBlockConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64) (uint64, bool, error)
+ GetBlockConsolidationCursor(ctx context.Context, name string, tag uint32) (uint64, bool, error)
+ SetBlockConsolidationCursor(ctx context.Context, name string, tag uint32, height uint64) error
+ PersistBlockConsolidationShadows(ctx context.Context, placements []*ConsolidationShadowPlacement) error
+ PromoteBlockConsolidationShadows(ctx context.Context, tag uint32, startHeight, endHeight uint64, limit uint64, singleBlockObjectRetention time.Duration) (*ConsolidationPromotionResult, error)
}
EventStorage interface {
@@ -53,6 +66,18 @@ type (
BlockStorage
EventStorage
TransactionStorage
+ SingleBlockUploadGuardStorage
+ }
+
+ // SingleBlockUploadGuard serializes a single-block PUT with retirement
+ // preparation, including when its metadata row does not exist yet.
+ SingleBlockUploadGuard interface {
+ RetirementFenced() bool
+ Release() error
+ }
+
+ SingleBlockUploadGuardStorage interface {
+ AcquireSingleBlockUploadGuard(ctx context.Context, tag uint32, height uint64, hash string) (SingleBlockUploadGuard, error)
}
Result struct {
@@ -63,6 +88,34 @@ type (
TransactionStorage TransactionStorage
}
+ BlockMetadataRecord struct {
+ ID int64
+ Metadata *api.BlockMetadata
+ }
+
+ ConsolidationShadowPlacement struct {
+ BlockMetadataID int64
+ Tag uint32
+ Height uint64
+ Hash string
+ SingleBlockObjectKeyMain string
+ ConsolidatedObjectKeyMain string
+ ObjectFormat api.BlockObjectFormat
+ ByteOffset uint64
+ ByteLength uint64
+ UncompressedLength uint64
+ }
+
+ ConsolidationShadowStats struct {
+ Objects uint64
+ Blocks uint64
+ EligibleBlocks uint64
+ }
+
+ ConsolidationPromotionResult struct {
+ Blocks uint64
+ }
+
MetaStorageFactory interface {
Create() (Result, error)
}
@@ -72,9 +125,28 @@ type (
fxparams.Params
DynamoDB MetaStorageFactory `name:"metastorage/dynamodb"`
Firestore MetaStorageFactory `name:"metastorage/firestore"`
+ Postgres MetaStorageFactory `name:"metastorage/postgres"`
}
)
+type unfencedSingleBlockUploadGuard struct{}
+
+func NewUnfencedSingleBlockUploadGuard() SingleBlockUploadGuard {
+ return unfencedSingleBlockUploadGuard{}
+}
+
+func (unfencedSingleBlockUploadGuard) RetirementFenced() bool {
+ return false
+}
+
+func (unfencedSingleBlockUploadGuard) Release() error {
+ return nil
+}
+
+const (
+ BatchConsolidatorAutoConsolidateCursor = "batch_consolidator_auto_consolidate_processed_exclusive"
+)
+
func WithMetaStorageFactory(params MetaStorageFactoryParams) (Result, error) {
var factory MetaStorageFactory
storageType := params.Config.StorageType.MetaStorageType
@@ -83,6 +155,8 @@ func WithMetaStorageFactory(params MetaStorageFactoryParams) (Result, error) {
factory = params.DynamoDB
case config.MetaStorageType_FIRESTORE:
factory = params.Firestore
+ case config.MetaStorageType_POSTGRES:
+ factory = params.Postgres
}
if factory == nil {
return Result{}, xerrors.Errorf("meta storage type is not implemented: %v", storageType)
diff --git a/internal/storage/metastorage/mocks/mocks.go b/internal/storage/metastorage/mocks/mocks.go
index 1edebbf0..27a6366e 100644
--- a/internal/storage/metastorage/mocks/mocks.go
+++ b/internal/storage/metastorage/mocks/mocks.go
@@ -12,7 +12,9 @@ package metastoragemocks
import (
context "context"
reflect "reflect"
+ time "time"
+ internal "github.com/coinbase/chainstorage/internal/storage/metastorage/internal"
model "github.com/coinbase/chainstorage/internal/storage/metastorage/model"
chainstorage "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
gomock "go.uber.org/mock/gomock"
@@ -41,6 +43,21 @@ func (m *MockMetaStorage) EXPECT() *MockMetaStorageMockRecorder {
return m.recorder
}
+// AcquireSingleBlockUploadGuard mocks base method.
+func (m *MockMetaStorage) AcquireSingleBlockUploadGuard(arg0 context.Context, arg1 uint32, arg2 uint64, arg3 string) (internal.SingleBlockUploadGuard, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "AcquireSingleBlockUploadGuard", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(internal.SingleBlockUploadGuard)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// AcquireSingleBlockUploadGuard indicates an expected call of AcquireSingleBlockUploadGuard.
+func (mr *MockMetaStorageMockRecorder) AcquireSingleBlockUploadGuard(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AcquireSingleBlockUploadGuard", reflect.TypeOf((*MockMetaStorage)(nil).AcquireSingleBlockUploadGuard), arg0, arg1, arg2, arg3)
+}
+
// AddEventEntries mocks base method.
func (m *MockMetaStorage) AddEventEntries(arg0 context.Context, arg1 uint32, arg2 []*model.EventEntry) error {
m.ctrl.T.Helper()
@@ -113,6 +130,67 @@ func (mr *MockMetaStorageMockRecorder) GetBlockByHeight(arg0, arg1, arg2 any) *g
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByHeight", reflect.TypeOf((*MockMetaStorage)(nil).GetBlockByHeight), arg0, arg1, arg2)
}
+// GetBlockByTimestamp mocks base method.
+func (m *MockMetaStorage) GetBlockByTimestamp(arg0 context.Context, arg1 uint32, arg2 uint64) (*chainstorage.BlockMetadata, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlockByTimestamp", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*chainstorage.BlockMetadata)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlockByTimestamp indicates an expected call of GetBlockByTimestamp.
+func (mr *MockMetaStorageMockRecorder) GetBlockByTimestamp(arg0, arg1, arg2 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByTimestamp", reflect.TypeOf((*MockMetaStorage)(nil).GetBlockByTimestamp), arg0, arg1, arg2)
+}
+
+// GetBlockConsolidationCursor mocks base method.
+func (m *MockMetaStorage) GetBlockConsolidationCursor(arg0 context.Context, arg1 string, arg2 uint32) (uint64, bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlockConsolidationCursor", arg0, arg1, arg2)
+ ret0, _ := ret[0].(uint64)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// GetBlockConsolidationCursor indicates an expected call of GetBlockConsolidationCursor.
+func (mr *MockMetaStorageMockRecorder) GetBlockConsolidationCursor(arg0, arg1, arg2 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockConsolidationCursor", reflect.TypeOf((*MockMetaStorage)(nil).GetBlockConsolidationCursor), arg0, arg1, arg2)
+}
+
+// GetBlockConsolidationShadow mocks base method.
+func (m *MockMetaStorage) GetBlockConsolidationShadow(arg0 context.Context, arg1 *chainstorage.BlockMetadata) (*chainstorage.BlockMetadata, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlockConsolidationShadow", arg0, arg1)
+ ret0, _ := ret[0].(*chainstorage.BlockMetadata)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlockConsolidationShadow indicates an expected call of GetBlockConsolidationShadow.
+func (mr *MockMetaStorageMockRecorder) GetBlockConsolidationShadow(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockConsolidationShadow", reflect.TypeOf((*MockMetaStorage)(nil).GetBlockConsolidationShadow), arg0, arg1)
+}
+
+// GetBlockConsolidationShadowStats mocks base method.
+func (m *MockMetaStorage) GetBlockConsolidationShadowStats(arg0 context.Context, arg1 uint32, arg2, arg3 uint64) (*internal.ConsolidationShadowStats, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlockConsolidationShadowStats", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*internal.ConsolidationShadowStats)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlockConsolidationShadowStats indicates an expected call of GetBlockConsolidationShadowStats.
+func (mr *MockMetaStorageMockRecorder) GetBlockConsolidationShadowStats(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockConsolidationShadowStats", reflect.TypeOf((*MockMetaStorage)(nil).GetBlockConsolidationShadowStats), arg0, arg1, arg2, arg3)
+}
+
// GetBlocksByHeightRange mocks base method.
func (m *MockMetaStorage) GetBlocksByHeightRange(arg0 context.Context, arg1 uint32, arg2, arg3 uint64) ([]*chainstorage.BlockMetadata, error) {
m.ctrl.T.Helper()
@@ -143,6 +221,36 @@ func (mr *MockMetaStorageMockRecorder) GetBlocksByHeights(arg0, arg1, arg2 any)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksByHeights", reflect.TypeOf((*MockMetaStorage)(nil).GetBlocksByHeights), arg0, arg1, arg2)
}
+// GetBlocksConsolidationShadow mocks base method.
+func (m *MockMetaStorage) GetBlocksConsolidationShadow(arg0 context.Context, arg1 []*chainstorage.BlockMetadata) ([]*chainstorage.BlockMetadata, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlocksConsolidationShadow", arg0, arg1)
+ ret0, _ := ret[0].([]*chainstorage.BlockMetadata)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlocksConsolidationShadow indicates an expected call of GetBlocksConsolidationShadow.
+func (mr *MockMetaStorageMockRecorder) GetBlocksConsolidationShadow(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksConsolidationShadow", reflect.TypeOf((*MockMetaStorage)(nil).GetBlocksConsolidationShadow), arg0, arg1)
+}
+
+// GetBlocksMissingConsolidationShadow mocks base method.
+func (m *MockMetaStorage) GetBlocksMissingConsolidationShadow(arg0 context.Context, arg1 uint32, arg2, arg3, arg4 uint64) ([]*internal.BlockMetadataRecord, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlocksMissingConsolidationShadow", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].([]*internal.BlockMetadataRecord)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlocksMissingConsolidationShadow indicates an expected call of GetBlocksMissingConsolidationShadow.
+func (mr *MockMetaStorageMockRecorder) GetBlocksMissingConsolidationShadow(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksMissingConsolidationShadow", reflect.TypeOf((*MockMetaStorage)(nil).GetBlocksMissingConsolidationShadow), arg0, arg1, arg2, arg3, arg4)
+}
+
// GetEventByEventId mocks base method.
func (m *MockMetaStorage) GetEventByEventId(arg0 context.Context, arg1 uint32, arg2 int64) (*model.EventEntry, error) {
m.ctrl.T.Helper()
@@ -203,6 +311,22 @@ func (mr *MockMetaStorageMockRecorder) GetEventsByEventIdRange(arg0, arg1, arg2,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetEventsByEventIdRange", reflect.TypeOf((*MockMetaStorage)(nil).GetEventsByEventIdRange), arg0, arg1, arg2, arg3)
}
+// GetFirstBlockMissingConsolidationShadow mocks base method.
+func (m *MockMetaStorage) GetFirstBlockMissingConsolidationShadow(arg0 context.Context, arg1 uint32, arg2, arg3 uint64) (uint64, bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetFirstBlockMissingConsolidationShadow", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(uint64)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// GetFirstBlockMissingConsolidationShadow indicates an expected call of GetFirstBlockMissingConsolidationShadow.
+func (mr *MockMetaStorageMockRecorder) GetFirstBlockMissingConsolidationShadow(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFirstBlockMissingConsolidationShadow", reflect.TypeOf((*MockMetaStorage)(nil).GetFirstBlockMissingConsolidationShadow), arg0, arg1, arg2, arg3)
+}
+
// GetFirstEventIdByBlockHeight mocks base method.
func (m *MockMetaStorage) GetFirstEventIdByBlockHeight(arg0 context.Context, arg1 uint32, arg2 uint64) (int64, error) {
m.ctrl.T.Helper()
@@ -218,6 +342,22 @@ func (mr *MockMetaStorageMockRecorder) GetFirstEventIdByBlockHeight(arg0, arg1,
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFirstEventIdByBlockHeight", reflect.TypeOf((*MockMetaStorage)(nil).GetFirstEventIdByBlockHeight), arg0, arg1, arg2)
}
+// GetFirstPromotableBlockConsolidationShadow mocks base method.
+func (m *MockMetaStorage) GetFirstPromotableBlockConsolidationShadow(arg0 context.Context, arg1 uint32, arg2, arg3 uint64) (uint64, bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetFirstPromotableBlockConsolidationShadow", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(uint64)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// GetFirstPromotableBlockConsolidationShadow indicates an expected call of GetFirstPromotableBlockConsolidationShadow.
+func (mr *MockMetaStorageMockRecorder) GetFirstPromotableBlockConsolidationShadow(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFirstPromotableBlockConsolidationShadow", reflect.TypeOf((*MockMetaStorage)(nil).GetFirstPromotableBlockConsolidationShadow), arg0, arg1, arg2, arg3)
+}
+
// GetLatestBlock mocks base method.
func (m *MockMetaStorage) GetLatestBlock(arg0 context.Context, arg1 uint32) (*chainstorage.BlockMetadata, error) {
m.ctrl.T.Helper()
@@ -263,6 +403,20 @@ func (mr *MockMetaStorageMockRecorder) GetTransaction(arg0, arg1, arg2 any) *gom
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTransaction", reflect.TypeOf((*MockMetaStorage)(nil).GetTransaction), arg0, arg1, arg2)
}
+// PersistBlockConsolidationShadows mocks base method.
+func (m *MockMetaStorage) PersistBlockConsolidationShadows(arg0 context.Context, arg1 []*internal.ConsolidationShadowPlacement) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PersistBlockConsolidationShadows", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// PersistBlockConsolidationShadows indicates an expected call of PersistBlockConsolidationShadows.
+func (mr *MockMetaStorageMockRecorder) PersistBlockConsolidationShadows(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PersistBlockConsolidationShadows", reflect.TypeOf((*MockMetaStorage)(nil).PersistBlockConsolidationShadows), arg0, arg1)
+}
+
// PersistBlockMetas mocks base method.
func (m *MockMetaStorage) PersistBlockMetas(arg0 context.Context, arg1 bool, arg2 []*chainstorage.BlockMetadata, arg3 *chainstorage.BlockMetadata) error {
m.ctrl.T.Helper()
@@ -277,6 +431,35 @@ func (mr *MockMetaStorageMockRecorder) PersistBlockMetas(arg0, arg1, arg2, arg3
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PersistBlockMetas", reflect.TypeOf((*MockMetaStorage)(nil).PersistBlockMetas), arg0, arg1, arg2, arg3)
}
+// PromoteBlockConsolidationShadows mocks base method.
+func (m *MockMetaStorage) PromoteBlockConsolidationShadows(arg0 context.Context, arg1 uint32, arg2, arg3, arg4 uint64, arg5 time.Duration) (*internal.ConsolidationPromotionResult, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PromoteBlockConsolidationShadows", arg0, arg1, arg2, arg3, arg4, arg5)
+ ret0, _ := ret[0].(*internal.ConsolidationPromotionResult)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// PromoteBlockConsolidationShadows indicates an expected call of PromoteBlockConsolidationShadows.
+func (mr *MockMetaStorageMockRecorder) PromoteBlockConsolidationShadows(arg0, arg1, arg2, arg3, arg4, arg5 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PromoteBlockConsolidationShadows", reflect.TypeOf((*MockMetaStorage)(nil).PromoteBlockConsolidationShadows), arg0, arg1, arg2, arg3, arg4, arg5)
+}
+
+// SetBlockConsolidationCursor mocks base method.
+func (m *MockMetaStorage) SetBlockConsolidationCursor(arg0 context.Context, arg1 string, arg2 uint32, arg3 uint64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetBlockConsolidationCursor", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SetBlockConsolidationCursor indicates an expected call of SetBlockConsolidationCursor.
+func (mr *MockMetaStorageMockRecorder) SetBlockConsolidationCursor(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBlockConsolidationCursor", reflect.TypeOf((*MockMetaStorage)(nil).SetBlockConsolidationCursor), arg0, arg1, arg2, arg3)
+}
+
// SetMaxEventId mocks base method.
func (m *MockMetaStorage) SetMaxEventId(arg0 context.Context, arg1 uint32, arg2 int64) error {
m.ctrl.T.Helper()
@@ -499,6 +682,67 @@ func (mr *MockBlockStorageMockRecorder) GetBlockByHeight(arg0, arg1, arg2 any) *
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByHeight", reflect.TypeOf((*MockBlockStorage)(nil).GetBlockByHeight), arg0, arg1, arg2)
}
+// GetBlockByTimestamp mocks base method.
+func (m *MockBlockStorage) GetBlockByTimestamp(arg0 context.Context, arg1 uint32, arg2 uint64) (*chainstorage.BlockMetadata, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlockByTimestamp", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*chainstorage.BlockMetadata)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlockByTimestamp indicates an expected call of GetBlockByTimestamp.
+func (mr *MockBlockStorageMockRecorder) GetBlockByTimestamp(arg0, arg1, arg2 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByTimestamp", reflect.TypeOf((*MockBlockStorage)(nil).GetBlockByTimestamp), arg0, arg1, arg2)
+}
+
+// GetBlockConsolidationCursor mocks base method.
+func (m *MockBlockStorage) GetBlockConsolidationCursor(arg0 context.Context, arg1 string, arg2 uint32) (uint64, bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlockConsolidationCursor", arg0, arg1, arg2)
+ ret0, _ := ret[0].(uint64)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// GetBlockConsolidationCursor indicates an expected call of GetBlockConsolidationCursor.
+func (mr *MockBlockStorageMockRecorder) GetBlockConsolidationCursor(arg0, arg1, arg2 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockConsolidationCursor", reflect.TypeOf((*MockBlockStorage)(nil).GetBlockConsolidationCursor), arg0, arg1, arg2)
+}
+
+// GetBlockConsolidationShadow mocks base method.
+func (m *MockBlockStorage) GetBlockConsolidationShadow(arg0 context.Context, arg1 *chainstorage.BlockMetadata) (*chainstorage.BlockMetadata, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlockConsolidationShadow", arg0, arg1)
+ ret0, _ := ret[0].(*chainstorage.BlockMetadata)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlockConsolidationShadow indicates an expected call of GetBlockConsolidationShadow.
+func (mr *MockBlockStorageMockRecorder) GetBlockConsolidationShadow(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockConsolidationShadow", reflect.TypeOf((*MockBlockStorage)(nil).GetBlockConsolidationShadow), arg0, arg1)
+}
+
+// GetBlockConsolidationShadowStats mocks base method.
+func (m *MockBlockStorage) GetBlockConsolidationShadowStats(arg0 context.Context, arg1 uint32, arg2, arg3 uint64) (*internal.ConsolidationShadowStats, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlockConsolidationShadowStats", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*internal.ConsolidationShadowStats)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlockConsolidationShadowStats indicates an expected call of GetBlockConsolidationShadowStats.
+func (mr *MockBlockStorageMockRecorder) GetBlockConsolidationShadowStats(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockConsolidationShadowStats", reflect.TypeOf((*MockBlockStorage)(nil).GetBlockConsolidationShadowStats), arg0, arg1, arg2, arg3)
+}
+
// GetBlocksByHeightRange mocks base method.
func (m *MockBlockStorage) GetBlocksByHeightRange(arg0 context.Context, arg1 uint32, arg2, arg3 uint64) ([]*chainstorage.BlockMetadata, error) {
m.ctrl.T.Helper()
@@ -529,6 +773,68 @@ func (mr *MockBlockStorageMockRecorder) GetBlocksByHeights(arg0, arg1, arg2 any)
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksByHeights", reflect.TypeOf((*MockBlockStorage)(nil).GetBlocksByHeights), arg0, arg1, arg2)
}
+// GetBlocksConsolidationShadow mocks base method.
+func (m *MockBlockStorage) GetBlocksConsolidationShadow(arg0 context.Context, arg1 []*chainstorage.BlockMetadata) ([]*chainstorage.BlockMetadata, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlocksConsolidationShadow", arg0, arg1)
+ ret0, _ := ret[0].([]*chainstorage.BlockMetadata)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlocksConsolidationShadow indicates an expected call of GetBlocksConsolidationShadow.
+func (mr *MockBlockStorageMockRecorder) GetBlocksConsolidationShadow(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksConsolidationShadow", reflect.TypeOf((*MockBlockStorage)(nil).GetBlocksConsolidationShadow), arg0, arg1)
+}
+
+// GetBlocksMissingConsolidationShadow mocks base method.
+func (m *MockBlockStorage) GetBlocksMissingConsolidationShadow(arg0 context.Context, arg1 uint32, arg2, arg3, arg4 uint64) ([]*internal.BlockMetadataRecord, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlocksMissingConsolidationShadow", arg0, arg1, arg2, arg3, arg4)
+ ret0, _ := ret[0].([]*internal.BlockMetadataRecord)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlocksMissingConsolidationShadow indicates an expected call of GetBlocksMissingConsolidationShadow.
+func (mr *MockBlockStorageMockRecorder) GetBlocksMissingConsolidationShadow(arg0, arg1, arg2, arg3, arg4 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlocksMissingConsolidationShadow", reflect.TypeOf((*MockBlockStorage)(nil).GetBlocksMissingConsolidationShadow), arg0, arg1, arg2, arg3, arg4)
+}
+
+// GetFirstBlockMissingConsolidationShadow mocks base method.
+func (m *MockBlockStorage) GetFirstBlockMissingConsolidationShadow(arg0 context.Context, arg1 uint32, arg2, arg3 uint64) (uint64, bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetFirstBlockMissingConsolidationShadow", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(uint64)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// GetFirstBlockMissingConsolidationShadow indicates an expected call of GetFirstBlockMissingConsolidationShadow.
+func (mr *MockBlockStorageMockRecorder) GetFirstBlockMissingConsolidationShadow(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFirstBlockMissingConsolidationShadow", reflect.TypeOf((*MockBlockStorage)(nil).GetFirstBlockMissingConsolidationShadow), arg0, arg1, arg2, arg3)
+}
+
+// GetFirstPromotableBlockConsolidationShadow mocks base method.
+func (m *MockBlockStorage) GetFirstPromotableBlockConsolidationShadow(arg0 context.Context, arg1 uint32, arg2, arg3 uint64) (uint64, bool, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetFirstPromotableBlockConsolidationShadow", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(uint64)
+ ret1, _ := ret[1].(bool)
+ ret2, _ := ret[2].(error)
+ return ret0, ret1, ret2
+}
+
+// GetFirstPromotableBlockConsolidationShadow indicates an expected call of GetFirstPromotableBlockConsolidationShadow.
+func (mr *MockBlockStorageMockRecorder) GetFirstPromotableBlockConsolidationShadow(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetFirstPromotableBlockConsolidationShadow", reflect.TypeOf((*MockBlockStorage)(nil).GetFirstPromotableBlockConsolidationShadow), arg0, arg1, arg2, arg3)
+}
+
// GetLatestBlock mocks base method.
func (m *MockBlockStorage) GetLatestBlock(arg0 context.Context, arg1 uint32) (*chainstorage.BlockMetadata, error) {
m.ctrl.T.Helper()
@@ -544,6 +850,20 @@ func (mr *MockBlockStorageMockRecorder) GetLatestBlock(arg0, arg1 any) *gomock.C
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetLatestBlock", reflect.TypeOf((*MockBlockStorage)(nil).GetLatestBlock), arg0, arg1)
}
+// PersistBlockConsolidationShadows mocks base method.
+func (m *MockBlockStorage) PersistBlockConsolidationShadows(arg0 context.Context, arg1 []*internal.ConsolidationShadowPlacement) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PersistBlockConsolidationShadows", arg0, arg1)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// PersistBlockConsolidationShadows indicates an expected call of PersistBlockConsolidationShadows.
+func (mr *MockBlockStorageMockRecorder) PersistBlockConsolidationShadows(arg0, arg1 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PersistBlockConsolidationShadows", reflect.TypeOf((*MockBlockStorage)(nil).PersistBlockConsolidationShadows), arg0, arg1)
+}
+
// PersistBlockMetas mocks base method.
func (m *MockBlockStorage) PersistBlockMetas(arg0 context.Context, arg1 bool, arg2 []*chainstorage.BlockMetadata, arg3 *chainstorage.BlockMetadata) error {
m.ctrl.T.Helper()
@@ -558,6 +878,35 @@ func (mr *MockBlockStorageMockRecorder) PersistBlockMetas(arg0, arg1, arg2, arg3
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PersistBlockMetas", reflect.TypeOf((*MockBlockStorage)(nil).PersistBlockMetas), arg0, arg1, arg2, arg3)
}
+// PromoteBlockConsolidationShadows mocks base method.
+func (m *MockBlockStorage) PromoteBlockConsolidationShadows(arg0 context.Context, arg1 uint32, arg2, arg3, arg4 uint64, arg5 time.Duration) (*internal.ConsolidationPromotionResult, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "PromoteBlockConsolidationShadows", arg0, arg1, arg2, arg3, arg4, arg5)
+ ret0, _ := ret[0].(*internal.ConsolidationPromotionResult)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// PromoteBlockConsolidationShadows indicates an expected call of PromoteBlockConsolidationShadows.
+func (mr *MockBlockStorageMockRecorder) PromoteBlockConsolidationShadows(arg0, arg1, arg2, arg3, arg4, arg5 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PromoteBlockConsolidationShadows", reflect.TypeOf((*MockBlockStorage)(nil).PromoteBlockConsolidationShadows), arg0, arg1, arg2, arg3, arg4, arg5)
+}
+
+// SetBlockConsolidationCursor mocks base method.
+func (m *MockBlockStorage) SetBlockConsolidationCursor(arg0 context.Context, arg1 string, arg2 uint32, arg3 uint64) error {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "SetBlockConsolidationCursor", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(error)
+ return ret0
+}
+
+// SetBlockConsolidationCursor indicates an expected call of SetBlockConsolidationCursor.
+func (mr *MockBlockStorageMockRecorder) SetBlockConsolidationCursor(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "SetBlockConsolidationCursor", reflect.TypeOf((*MockBlockStorage)(nil).SetBlockConsolidationCursor), arg0, arg1, arg2, arg3)
+}
+
// MockTransactionStorage is a mock of TransactionStorage interface.
type MockTransactionStorage struct {
ctrl *gomock.Controller
diff --git a/internal/storage/metastorage/module.go b/internal/storage/metastorage/module.go
index e8deed27..80e0ea1c 100644
--- a/internal/storage/metastorage/module.go
+++ b/internal/storage/metastorage/module.go
@@ -7,21 +7,34 @@ import (
"github.com/coinbase/chainstorage/internal/storage/metastorage/firestore"
"github.com/coinbase/chainstorage/internal/storage/metastorage/internal"
"github.com/coinbase/chainstorage/internal/storage/metastorage/model"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
)
type (
- MetaStorage = internal.MetaStorage
- BlockStorage = internal.BlockStorage
- EventStorage = internal.EventStorage
- TransactionStorage = internal.TransactionStorage
- EventsToChainAdaptor = internal.EventsToChainAdaptor
+ MetaStorage = internal.MetaStorage
+ BlockStorage = internal.BlockStorage
+ EventStorage = internal.EventStorage
+ TransactionStorage = internal.TransactionStorage
+ EventsToChainAdaptor = internal.EventsToChainAdaptor
+ BlockMetadataRecord = internal.BlockMetadataRecord
+ ConsolidationShadowPlacement = internal.ConsolidationShadowPlacement
+ ConsolidationShadowStats = internal.ConsolidationShadowStats
+ ConsolidationPromotionResult = internal.ConsolidationPromotionResult
+ SingleBlockUploadGuard = internal.SingleBlockUploadGuard
+ SingleBlockUploadGuardStorage = internal.SingleBlockUploadGuardStorage
)
const (
EventIdStartValue = model.EventIdStartValue
EventIdDeleted = model.EventIdDeleted
+
+ BatchConsolidatorAutoConsolidateCursor = internal.BatchConsolidatorAutoConsolidateCursor
)
+func NewUnfencedSingleBlockUploadGuard() SingleBlockUploadGuard {
+ return internal.NewUnfencedSingleBlockUploadGuard()
+}
+
func NewEventsToChainAdaptor() *EventsToChainAdaptor {
return internal.NewEventsToChainAdaptor()
}
@@ -29,5 +42,6 @@ func NewEventsToChainAdaptor() *EventsToChainAdaptor {
var Module = fx.Options(
dynamodb.Module,
firestore.Module,
+ postgres.Module,
fx.Provide(internal.WithMetaStorageFactory),
)
diff --git a/internal/storage/metastorage/postgres/admin.go b/internal/storage/metastorage/postgres/admin.go
new file mode 100644
index 00000000..910f0b18
--- /dev/null
+++ b/internal/storage/metastorage/postgres/admin.go
@@ -0,0 +1,174 @@
+package postgres
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+
+ "github.com/lib/pq"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/utils/log"
+)
+
+// SetupDatabase creates a database and roles for a new network in PostgreSQL
+// This function is intended to be called by administrators during setup
+func SetupDatabase(ctx context.Context, masterCfg *config.PostgresConfig, workerUser string, workerPassword string, serverUser string, serverPassword string, dbName string) error {
+ logger := log.WithPackage(log.NewDevelopment())
+
+ logger.Info("Setting up PostgreSQL database",
+ zap.String("database", dbName),
+ zap.String("worker_user", workerUser),
+ zap.String("server_user", serverUser))
+
+ // Connect to the default 'postgres' database with master credentials
+ dsn := fmt.Sprintf(
+ "host=%s port=%d dbname=postgres user=%s password=%s sslmode=%s",
+ masterCfg.Host, masterCfg.Port, masterCfg.User, masterCfg.Password, masterCfg.SSLMode,
+ )
+ if masterCfg.ConnectTimeout > 0 {
+ dsn += fmt.Sprintf(" connect_timeout=%d", int(masterCfg.ConnectTimeout.Seconds()))
+ }
+
+ adminDB, err := sql.Open("postgres", dsn)
+ if err != nil {
+ return xerrors.Errorf("failed to connect to postgres db with master user: %w", err)
+ }
+ defer func() {
+ if closeErr := adminDB.Close(); closeErr != nil {
+ logger.Warn("Failed to close admin database connection", zap.Error(closeErr))
+ }
+ }()
+
+ if err := adminDB.PingContext(ctx); err != nil {
+ return xerrors.Errorf("failed to ping postgres db with master user: %w", err)
+ }
+
+ logger.Info("Successfully connected to PostgreSQL as master user")
+
+ // Create worker role with LOGIN capability and password
+ logger.Info("Creating worker role", zap.String("username", workerUser))
+ workerQuery := fmt.Sprintf("CREATE ROLE %s WITH LOGIN PASSWORD %s",
+ pq.QuoteIdentifier(workerUser), pq.QuoteLiteral(workerPassword))
+ if _, err := adminDB.ExecContext(ctx, workerQuery); err != nil {
+ if pgErr, ok := err.(*pq.Error); ok && pgErr.Code.Name() == "duplicate_object" {
+ logger.Info("Worker role already exists, updating password", zap.String("username", workerUser))
+ // Update password for existing role
+ alterQuery := fmt.Sprintf("ALTER ROLE %s WITH PASSWORD %s",
+ pq.QuoteIdentifier(workerUser), pq.QuoteLiteral(workerPassword))
+ if _, err := adminDB.ExecContext(ctx, alterQuery); err != nil {
+ return xerrors.Errorf("failed to update password for worker role %s: %w", workerUser, err)
+ }
+ } else {
+ return xerrors.Errorf("failed to create worker role %s: %w", workerUser, err)
+ }
+ } else {
+ logger.Info("Successfully created worker role", zap.String("username", workerUser))
+ }
+
+ // Create server role with LOGIN capability and password
+ logger.Info("Creating server role", zap.String("username", serverUser))
+ serverQuery := fmt.Sprintf("CREATE ROLE %s WITH LOGIN PASSWORD %s",
+ pq.QuoteIdentifier(serverUser), pq.QuoteLiteral(serverPassword))
+ if _, err := adminDB.ExecContext(ctx, serverQuery); err != nil {
+ if pgErr, ok := err.(*pq.Error); ok && pgErr.Code.Name() == "duplicate_object" {
+ logger.Info("Server role already exists, updating password", zap.String("username", serverUser))
+ // Update password for existing role
+ alterQuery := fmt.Sprintf("ALTER ROLE %s WITH PASSWORD %s",
+ pq.QuoteIdentifier(serverUser), pq.QuoteLiteral(serverPassword))
+ if _, err := adminDB.ExecContext(ctx, alterQuery); err != nil {
+ return xerrors.Errorf("failed to update password for server role %s: %w", serverUser, err)
+ }
+ } else {
+ return xerrors.Errorf("failed to create server role %s: %w", serverUser, err)
+ }
+ } else {
+ logger.Info("Successfully created server role", zap.String("username", serverUser))
+ }
+
+ // Create application database owned by the worker role
+ logger.Info("Creating database", zap.String("database", dbName))
+ ownerOpt := fmt.Sprintf("OWNER = %s", pq.QuoteIdentifier(workerUser))
+ if _, err := adminDB.ExecContext(ctx, fmt.Sprintf(`CREATE DATABASE %s WITH %s`, pq.QuoteIdentifier(dbName), ownerOpt)); err != nil {
+ if pgErr, ok := err.(*pq.Error); ok && pgErr.Code.Name() == "duplicate_database" {
+ logger.Info("Database already exists, skipping creation", zap.String("database", dbName))
+ } else {
+ return xerrors.Errorf("failed to create database %s: %w", dbName, err)
+ }
+ } else {
+ logger.Info("Successfully created database", zap.String("database", dbName))
+ }
+
+ // Connect to the application database to set up permissions
+ logger.Info("Setting up permissions for database", zap.String("database", dbName))
+ appDsn := fmt.Sprintf(
+ "host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
+ masterCfg.Host, masterCfg.Port, dbName, masterCfg.User, masterCfg.Password, masterCfg.SSLMode,
+ )
+ if masterCfg.ConnectTimeout > 0 {
+ appDsn += fmt.Sprintf(" connect_timeout=%d", int(masterCfg.ConnectTimeout.Seconds()))
+ }
+
+ appDB, err := sql.Open("postgres", appDsn)
+ if err != nil {
+ return xerrors.Errorf("failed to connect to app database %s: %w", dbName, err)
+ }
+ defer func() {
+ if closeErr := appDB.Close(); closeErr != nil {
+ logger.Warn("Failed to close app database connection", zap.Error(closeErr))
+ }
+ }()
+
+ if err := appDB.PingContext(ctx); err != nil {
+ return xerrors.Errorf("failed to ping app database %s: %w", dbName, err)
+ }
+
+ // Grant connect permissions to server role
+ logger.Info("Granting CONNECT permission on database to server role",
+ zap.String("database", dbName),
+ zap.String("server_user", serverUser))
+ if _, err := appDB.ExecContext(ctx, fmt.Sprintf("GRANT CONNECT ON DATABASE %s TO %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(serverUser))); err != nil {
+ return xerrors.Errorf("failed to grant connect on db %s to role %s: %w", dbName, serverUser, err)
+ }
+
+ // Grant usage on public schema
+ logger.Info("Granting USAGE on schema public to server role", zap.String("server_user", serverUser))
+ if _, err := appDB.ExecContext(ctx, fmt.Sprintf("GRANT USAGE ON SCHEMA public TO %s", pq.QuoteIdentifier(serverUser))); err != nil {
+ return xerrors.Errorf("failed to grant usage on schema public to role %s: %w", serverUser, err)
+ }
+
+ // Grant SELECT on all current tables to server role
+ logger.Info("Granting SELECT on all existing tables to server role", zap.String("server_user", serverUser))
+ if _, err := appDB.ExecContext(ctx, fmt.Sprintf("GRANT SELECT ON ALL TABLES IN SCHEMA public TO %s", pq.QuoteIdentifier(serverUser))); err != nil {
+ // This might fail if no tables exist yet, which is fine
+ logger.Warn("Failed to grant select on existing tables (this is normal if no tables exist yet)", zap.Error(err))
+ }
+
+ // Grant SELECT on all future tables to server role
+ logger.Info("Setting up default privileges for future tables for server role", zap.String("server_user", serverUser))
+ if _, err := appDB.ExecContext(ctx, fmt.Sprintf("ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO %s", pq.QuoteIdentifier(serverUser))); err != nil {
+ return xerrors.Errorf("failed to alter default privileges for role %s: %w", serverUser, err)
+ }
+
+ // Also need to ensure the worker role has the necessary permissions on the database
+ logger.Info("Ensuring worker role has full access to database",
+ zap.String("worker_user", workerUser),
+ zap.String("database", dbName))
+ if _, err := appDB.ExecContext(ctx, fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE %s TO %s", pq.QuoteIdentifier(dbName), pq.QuoteIdentifier(workerUser))); err != nil {
+ logger.Warn("Failed to grant all privileges on database to worker role", zap.Error(err))
+ }
+
+ if _, err := appDB.ExecContext(ctx, fmt.Sprintf("GRANT ALL PRIVILEGES ON SCHEMA public TO %s", pq.QuoteIdentifier(workerUser))); err != nil {
+ logger.Warn("Failed to grant all privileges on schema to worker role", zap.Error(err))
+ }
+
+ logger.Info("Successfully set up database with roles",
+ zap.String("database", dbName),
+ zap.String("worker_user", workerUser),
+ zap.String("server_user", serverUser))
+ logger.Info("Database ready for use with the provided credentials")
+
+ return nil
+}
diff --git a/internal/storage/metastorage/postgres/block_storage.go b/internal/storage/metastorage/postgres/block_storage.go
new file mode 100644
index 00000000..23b9e7fb
--- /dev/null
+++ b/internal/storage/metastorage/postgres/block_storage.go
@@ -0,0 +1,1395 @@
+package postgres
+
+import (
+ "context"
+ "database/sql"
+ stderrors "errors"
+ "fmt"
+ "math/rand"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "golang.org/x/xerrors"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser"
+ "github.com/coinbase/chainstorage/internal/storage/cscbrepairlock"
+ "github.com/coinbase/chainstorage/internal/storage/internal/errors"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/internal"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres/model"
+ "github.com/coinbase/chainstorage/internal/storage/retirementlock"
+ "github.com/coinbase/chainstorage/internal/utils/instrument"
+ "github.com/coinbase/chainstorage/internal/utils/utils"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ blockStorageImpl struct {
+ db *sql.DB
+ blockStartHeight uint64
+ instrumentPersistBlockMetas instrument.Instrument
+ instrumentGetLatestBlock instrument.InstrumentWithResult[*api.BlockMetadata]
+ instrumentGetBlockByHash instrument.InstrumentWithResult[*api.BlockMetadata]
+ instrumentGetBlockByHeight instrument.InstrumentWithResult[*api.BlockMetadata]
+ instrumentGetBlocksByHeightRange instrument.InstrumentWithResult[[]*api.BlockMetadata]
+ instrumentGetBlocksByHeights instrument.InstrumentWithResult[[]*api.BlockMetadata]
+ instrumentGetBlockByTimestamp instrument.InstrumentWithResult[*api.BlockMetadata]
+ }
+
+ singleBlockUploadGuard struct {
+ conn *sql.Conn
+ tx *sql.Tx
+ fenced bool
+ once sync.Once
+ releaseErr error
+ }
+)
+
+const (
+ blockMetadataColumns = `
+ id, height, tag, hash, parent_hash, parent_height, object_key_main,
+ timestamp, skipped, object_format, byte_offset, byte_length, uncompressed_length`
+ canonicalBlockMetadataColumns = `
+ bm.id, bm.height, bm.tag, bm.hash, bm.parent_hash, bm.parent_height, bm.object_key_main,
+ bm.timestamp, bm.skipped, bm.object_format, bm.byte_offset, bm.byte_length, bm.uncompressed_length`
+)
+
+func blockMetadataByHashQuery() string {
+ return fmt.Sprintf(`
+ SELECT %s
+ FROM block_metadata
+ WHERE tag = $1 AND height = $2 AND hash = $3 AND skipped = false
+ LIMIT 1`, blockMetadataColumns)
+}
+
+func singleBlockUploadGuardByHashQuery() string {
+ return `
+ SELECT
+ bm.single_block_retention_fenced_at IS NOT NULL
+ OR EXISTS (
+ SELECT 1
+ FROM cscb_repair_block repair_block
+ JOIN cscb_repair_manifest repair ON repair.id = repair_block.repair_id
+ WHERE repair_block.block_metadata_id = bm.id
+ AND repair.state <> 'completed'
+ )
+ FROM block_metadata bm
+ WHERE bm.tag = $1
+ AND bm.height = $2
+ AND bm.hash = $3
+ AND bm.skipped = FALSE
+ FOR UPDATE`
+}
+
+// A block without a hash can only be identified by its canonical tag and height.
+// Resolve that row through canonical_blocks so the lookup remains indexed.
+func singleBlockUploadGuardWithoutHashQuery() string {
+ return `
+ SELECT
+ bm.single_block_retention_fenced_at IS NOT NULL
+ OR EXISTS (
+ SELECT 1
+ FROM cscb_repair_block repair_block
+ JOIN cscb_repair_manifest repair ON repair.id = repair_block.repair_id
+ WHERE repair_block.block_metadata_id = bm.id
+ AND repair.state <> 'completed'
+ )
+ FROM block_metadata bm
+ WHERE bm.id = (
+ SELECT cb.block_metadata_id
+ FROM canonical_blocks cb
+ WHERE cb.tag = $1
+ AND cb.height = $2
+ FOR UPDATE
+ )
+ AND bm.tag = $1
+ AND bm.height = $2
+ AND bm.hash IS NULL
+ AND bm.skipped = FALSE
+ FOR UPDATE OF bm`
+}
+
+func newBlockStorage(db *sql.DB, params Params) (internal.BlockStorage, error) {
+ metrics := params.Metrics.SubScope("block_storage").Tagged(map[string]string{
+ "storage_type": "postgres",
+ })
+ accessor := blockStorageImpl{
+ db: db,
+ blockStartHeight: params.Config.Chain.BlockStartHeight,
+ instrumentPersistBlockMetas: instrument.New(metrics, "persist_block_metas"),
+ instrumentGetLatestBlock: instrument.NewWithResult[*api.BlockMetadata](metrics, "get_latest_block"),
+ instrumentGetBlockByHash: instrument.NewWithResult[*api.BlockMetadata](metrics, "get_block_by_hash"),
+ instrumentGetBlockByHeight: instrument.NewWithResult[*api.BlockMetadata](metrics, "get_block_by_height"),
+ instrumentGetBlocksByHeightRange: instrument.NewWithResult[[]*api.BlockMetadata](metrics, "get_blocks_by_height_range"),
+ instrumentGetBlocksByHeights: instrument.NewWithResult[[]*api.BlockMetadata](metrics, "get_blocks_by_heights"),
+ instrumentGetBlockByTimestamp: instrument.NewWithResult[*api.BlockMetadata](metrics, "get_block_by_timestamp"),
+ }
+ return &accessor, nil
+}
+
+func (b *blockStorageImpl) AcquireSingleBlockUploadGuard(
+ ctx context.Context,
+ tag uint32,
+ height uint64,
+ hash string,
+) (internal.SingleBlockUploadGuard, error) {
+ conn, err := b.db.Conn(ctx)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to acquire single-block upload connection: %w", err)
+ }
+ tx, err := conn.BeginTx(context.Background(), nil)
+ if err != nil {
+ _ = conn.Close()
+ return nil, xerrors.Errorf("failed to begin single-block upload guard: %w", err)
+ }
+ if err := retirementlock.Acquire(ctx, tx, tag, height, hash); err != nil {
+ _ = tx.Rollback()
+ _ = conn.Close()
+ return nil, err
+ }
+ var singleBlockWriteFenced bool
+ if hash == "" {
+ err = tx.QueryRowContext(ctx, singleBlockUploadGuardWithoutHashQuery(), tag, height).Scan(&singleBlockWriteFenced)
+ } else {
+ err = tx.QueryRowContext(ctx, singleBlockUploadGuardByHashQuery(), tag, height, hash).Scan(&singleBlockWriteFenced)
+ }
+ if err != nil && err != sql.ErrNoRows {
+ _ = tx.Rollback()
+ _ = conn.Close()
+ return nil, xerrors.Errorf("failed to acquire single-block upload guard: %w", err)
+ }
+ return &singleBlockUploadGuard{
+ conn: conn,
+ tx: tx,
+ fenced: err == nil && singleBlockWriteFenced,
+ }, nil
+}
+
+func (g *singleBlockUploadGuard) RetirementFenced() bool {
+ return g != nil && g.fenced
+}
+
+func (g *singleBlockUploadGuard) Release() error {
+ if g == nil || g.tx == nil {
+ return nil
+ }
+ g.once.Do(func() {
+ g.releaseErr = g.tx.Rollback()
+ if g.releaseErr == sql.ErrTxDone {
+ g.releaseErr = nil
+ }
+ if g.conn != nil {
+ g.releaseErr = stderrors.Join(g.releaseErr, g.conn.Close())
+ }
+ })
+ return g.releaseErr
+}
+
+func (b *blockStorageImpl) PersistBlockMetas(
+ ctx context.Context, updateWatermark bool, blocks []*api.BlockMetadata, lastBlock *api.BlockMetadata) error {
+ return b.instrumentPersistBlockMetas.Instrument(ctx, func(ctx context.Context) error {
+ if len(blocks) == 0 {
+ return nil
+ }
+
+ // Sort blocks by height for chain validation.
+ // IMPORTANT: When multiple blocks have the same height (e.g., during a reorg), their relative
+ // order after sorting is not guaranteed to be stable. However, this implementation follows the
+ // "last block wins" principle - the last block processed for a given height will become the
+ // canonical block for that height. This behavior is consistent with the DynamoDB implementation
+ // where the last block overwrites the canonical entry.
+ //
+ // The canonical_blocks table uses "ON CONFLICT (height, tag) DO UPDATE" which means:
+ // - If multiple blocks in the input have the same height, the last one processed will
+ // overwrite previous entries in canonical_blocks
+ // - All blocks are still stored in block_metadata (allowing retrieval by specific hash)
+ // - Only the last block for each height becomes the canonical one
+ //
+ // Callers should ensure that when multiple blocks exist for the same height, the desired
+ // canonical block is placed last in the blocks array for that height.
+ sort.Slice(blocks, func(i, j int) bool {
+ return blocks[i].Height < blocks[j].Height
+ })
+ if err := parser.ValidateChain(blocks, lastBlock); err != nil {
+ return xerrors.Errorf("failed to validate chain: %w", err)
+ }
+
+ // Create transaction with timeout context
+ // Use a reasonable timeout for block persistence operations
+ txCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
+ defer cancel()
+
+ tx, err := b.db.BeginTx(txCtx, nil)
+ if err != nil {
+ return xerrors.Errorf("failed to begin transaction: %w", err)
+ }
+ committed := false
+ defer func() {
+ if !committed {
+ if rollbackErr := tx.Rollback(); rollbackErr != nil {
+ // Log the rollback error but don't override the original error
+ // In a production environment, you might want to use a proper logger here
+ // For now, we'll just ignore the rollback error as it's already a failure case
+ _ = rollbackErr
+ }
+ }
+ }()
+
+ tags := make([]uint32, 0, len(blocks))
+ for _, block := range blocks {
+ if block != nil {
+ tags = append(tags, block.Tag)
+ }
+ }
+ if err := cscbrepairlock.AcquireTags(txCtx, tx, tags); err != nil {
+ return err
+ }
+
+ // Different queries for skipped vs non-skipped blocks due to different conflict resolution
+ blockMetadataSkippedQuery := `
+ INSERT INTO block_metadata (
+ height, tag, hash, parent_hash, parent_height, object_key_main, timestamp, skipped,
+ object_format, byte_offset, byte_length, uncompressed_length
+ )
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
+ ON CONFLICT (tag, height) WHERE skipped = true DO UPDATE SET
+ hash = EXCLUDED.hash,
+ parent_hash = EXCLUDED.parent_hash,
+ parent_height = EXCLUDED.parent_height,
+ object_key_main = EXCLUDED.object_key_main,
+ timestamp = EXCLUDED.timestamp,
+ skipped = EXCLUDED.skipped,
+ object_format = EXCLUDED.object_format,
+ byte_offset = EXCLUDED.byte_offset,
+ byte_length = EXCLUDED.byte_length,
+ uncompressed_length = EXCLUDED.uncompressed_length
+ RETURNING id`
+
+ blockMetadataRegularQuery := `
+ INSERT INTO block_metadata (
+ height, tag, hash, parent_hash, parent_height, object_key_main, timestamp, skipped,
+ object_format, byte_offset, byte_length, uncompressed_length
+ )
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
+ ON CONFLICT (tag, hash) WHERE hash IS NOT NULL AND NOT skipped DO UPDATE SET
+ parent_hash = EXCLUDED.parent_hash,
+ parent_height = EXCLUDED.parent_height,
+ object_key_main = CASE
+ WHEN block_metadata.single_block_retention_fenced_at IS NOT NULL
+ OR EXISTS (
+ SELECT 1 FROM cscb_repair_block repair_block
+ WHERE repair_block.block_metadata_id = block_metadata.id
+ ) THEN block_metadata.object_key_main
+ ELSE EXCLUDED.object_key_main
+ END,
+ timestamp = EXCLUDED.timestamp,
+ skipped = EXCLUDED.skipped,
+ object_format = CASE
+ WHEN block_metadata.single_block_retention_fenced_at IS NOT NULL
+ OR EXISTS (
+ SELECT 1 FROM cscb_repair_block repair_block
+ WHERE repair_block.block_metadata_id = block_metadata.id
+ ) THEN block_metadata.object_format
+ ELSE EXCLUDED.object_format
+ END,
+ byte_offset = CASE
+ WHEN block_metadata.single_block_retention_fenced_at IS NOT NULL
+ OR EXISTS (
+ SELECT 1 FROM cscb_repair_block repair_block
+ WHERE repair_block.block_metadata_id = block_metadata.id
+ ) THEN block_metadata.byte_offset
+ ELSE EXCLUDED.byte_offset
+ END,
+ byte_length = CASE
+ WHEN block_metadata.single_block_retention_fenced_at IS NOT NULL
+ OR EXISTS (
+ SELECT 1 FROM cscb_repair_block repair_block
+ WHERE repair_block.block_metadata_id = block_metadata.id
+ ) THEN block_metadata.byte_length
+ ELSE EXCLUDED.byte_length
+ END,
+ uncompressed_length = CASE
+ WHEN block_metadata.single_block_retention_fenced_at IS NOT NULL
+ OR EXISTS (
+ SELECT 1 FROM cscb_repair_block repair_block
+ WHERE repair_block.block_metadata_id = block_metadata.id
+ ) THEN block_metadata.uncompressed_length
+ ELSE EXCLUDED.uncompressed_length
+ END
+ RETURNING id`
+
+ // Simply insert or update canonical blocks like DynamoDB does
+ // The "last write wins" behavior matches DynamoDB's TransactWriteItems
+ // Chain validation happens in update_watermark activity, not here
+ canonicalQuery := `
+ INSERT INTO canonical_blocks (height, block_metadata_id, tag)
+ VALUES ($1, $2, $3)
+ ON CONFLICT (height, tag) DO UPDATE
+ SET block_metadata_id = EXCLUDED.block_metadata_id`
+
+ for _, block := range blocks {
+ tsProto := block.GetTimestamp()
+ var unixTimestamp int64
+ if tsProto == nil { // special case for genesis block
+ unixTimestamp = 0
+ } else {
+ unixTimestamp = tsProto.GetSeconds() // directly get seconds from protobuf timestamp
+ }
+
+ var parentHeight uint64
+ if block.Height == 0 {
+ // Genesis block has no parent, set parent height to 0
+ parentHeight = 0
+ } else {
+ parentHeight = block.ParentHeight
+ }
+
+ var blockId int64
+ var query string
+ if block.Skipped {
+ query = blockMetadataSkippedQuery
+ } else {
+ query = blockMetadataRegularQuery
+ }
+
+ byteOffset, byteLength, uncompressedLength := blockObjectByteFields(block)
+ err = tx.QueryRowContext(txCtx, query,
+ block.Height,
+ block.Tag,
+ block.Hash,
+ block.ParentHash,
+ parentHeight,
+ block.ObjectKeyMain,
+ unixTimestamp,
+ block.Skipped,
+ int32(block.GetObjectFormat()),
+ byteOffset,
+ byteLength,
+ uncompressedLength,
+ ).Scan(&blockId)
+ if err != nil {
+ return xerrors.Errorf("failed to insert block metadata for height %d: %w", block.Height, err)
+ }
+
+ // Insert into canonical_blocks
+ // Always insert/update canonical blocks like DynamoDB does
+ _, err = tx.ExecContext(txCtx, canonicalQuery,
+ block.Height,
+ blockId,
+ block.Tag,
+ )
+ if err != nil {
+ return xerrors.Errorf("failed to insert canonical block for height %d: %w", block.Height, err)
+ }
+ }
+
+ // Update watermark if requested
+ // Set is_watermark=TRUE for the highest block to mark it as validated
+ // This prevents canonical chain leakage to streamer before validation
+ if updateWatermark && len(blocks) > 0 {
+ highestBlock := blocks[len(blocks)-1]
+
+ // Probabilistically clear old watermarks (1 in 5000 chance)
+ // This prevents unbounded accumulation while keeping the operation rare enough
+ // to have negligible performance impact
+ if rand.Intn(5000) == 0 {
+ // Clear all old watermarks for this tag, keeping only the current one
+ // This is safe because only GetLatestBlock uses is_watermark filter,
+ // and it only needs the highest watermarked block
+ clearQuery := `
+ UPDATE canonical_blocks
+ SET is_watermark = FALSE
+ WHERE tag = $1 AND is_watermark = TRUE`
+ _, err = tx.ExecContext(txCtx, clearQuery, highestBlock.Tag)
+ if err != nil {
+ // Log but don't fail - cleanup is best-effort
+ // In production, you might want to use a proper logger here
+ _ = err
+ }
+ }
+
+ // Set the new watermark
+ watermarkQuery := `
+ UPDATE canonical_blocks
+ SET is_watermark = TRUE
+ WHERE tag = $1 AND height = $2`
+ _, err = tx.ExecContext(txCtx, watermarkQuery, highestBlock.Tag, highestBlock.Height)
+ if err != nil {
+ return xerrors.Errorf("failed to update watermark for height %d: %w", highestBlock.Height, err)
+ }
+ }
+
+ // Commit transaction
+ err = tx.Commit()
+ if err != nil {
+ return xerrors.Errorf("failed to commit transaction: %w", err)
+ }
+ committed = true
+ return nil
+ })
+}
+
+func (b *blockStorageImpl) GetLatestBlock(ctx context.Context, tag uint32) (*api.BlockMetadata, error) {
+ return b.instrumentGetLatestBlock.Instrument(ctx, func(ctx context.Context) (*api.BlockMetadata, error) {
+ // Get the latest canonical block by highest height
+ // Only return blocks that have been validated (is_watermark=TRUE)
+ // This prevents canonical chain leakage to streamer before validation
+ query := fmt.Sprintf(`
+ SELECT %s
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON cb.block_metadata_id = bm.id
+ WHERE cb.tag = $1 AND cb.is_watermark = TRUE
+ ORDER BY cb.height DESC
+ LIMIT 1`, canonicalBlockMetadataColumns)
+ row := b.db.QueryRowContext(ctx, query, tag)
+ block, err := model.BlockMetadataFromCanonicalRow(b.db, row)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, xerrors.Errorf("no latest block found: %w", errors.ErrItemNotFound)
+ }
+ return nil, xerrors.Errorf("failed to get latest block: %w", err)
+ }
+ return block, nil
+ })
+}
+
+func (b *blockStorageImpl) GetBlockByHash(ctx context.Context, tag uint32, height uint64, blockHash string) (*api.BlockMetadata, error) {
+ return b.instrumentGetBlockByHash.Instrument(ctx, func(ctx context.Context) (*api.BlockMetadata, error) {
+ if err := b.validateHeight(height); err != nil {
+ return nil, err
+ }
+ var row *sql.Row
+ if blockHash == "" {
+ // Get the canonical block at this height (could be regular or skipped)
+ query := fmt.Sprintf(`
+ SELECT %s
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON cb.block_metadata_id = bm.id
+ WHERE cb.tag = $1 AND cb.height = $2
+ LIMIT 1`, canonicalBlockMetadataColumns)
+ row = b.db.QueryRowContext(ctx, query, tag, height)
+ } else {
+ // Query block_metadata directly for the specific hash
+ row = b.db.QueryRowContext(ctx, blockMetadataByHashQuery(), tag, height, blockHash)
+ }
+
+ block, err := model.BlockMetadataFromRow(b.db, row)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, xerrors.Errorf("block not found: %w", errors.ErrItemNotFound)
+ }
+ return nil, xerrors.Errorf("failed to get block by hash: %w", err)
+ }
+ return block, nil
+ })
+}
+
+func (b *blockStorageImpl) GetBlockByHeight(ctx context.Context, tag uint32, height uint64) (*api.BlockMetadata, error) {
+ return b.instrumentGetBlockByHeight.Instrument(ctx, func(ctx context.Context) (*api.BlockMetadata, error) {
+ if err := b.validateHeight(height); err != nil {
+ return nil, err
+ }
+ // Get block from canonical_blocks table (includes both regular and skipped blocks)
+ query := fmt.Sprintf(`
+ SELECT %s
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON cb.block_metadata_id = bm.id
+ WHERE cb.tag = $1 AND cb.height = $2
+ LIMIT 1`, canonicalBlockMetadataColumns)
+ row := b.db.QueryRowContext(ctx, query, tag, height)
+ block, err := model.BlockMetadataFromCanonicalRow(b.db, row)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, xerrors.Errorf("block at height %d not found: %w", height, errors.ErrItemNotFound)
+ }
+ return nil, xerrors.Errorf("failed to get block by height: %w", err)
+ }
+ return block, nil
+ })
+}
+
+func (b *blockStorageImpl) GetBlocksByHeightRange(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) ([]*api.BlockMetadata, error) {
+ return b.instrumentGetBlocksByHeightRange.Instrument(ctx, func(ctx context.Context) ([]*api.BlockMetadata, error) {
+ if startHeight >= endHeight {
+ return nil, errors.ErrOutOfRange
+ }
+ if err := b.validateHeight(startHeight); err != nil {
+ return nil, err
+ }
+
+ // Get all blocks (canonical and skipped) from canonical_blocks table
+ query := fmt.Sprintf(`
+ SELECT %s
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON cb.block_metadata_id = bm.id
+ WHERE cb.tag = $1 AND cb.height >= $2 AND cb.height < $3
+ ORDER BY cb.height ASC`, canonicalBlockMetadataColumns)
+ rows, err := b.db.QueryContext(ctx, query, tag, startHeight, endHeight)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to query blocks by height range: %w", err)
+ }
+ defer func() {
+ if closeErr := rows.Close(); closeErr != nil && err == nil {
+ err = xerrors.Errorf("failed to close rows: %w", closeErr)
+ }
+ }()
+
+ blocks, err := model.BlockMetadataFromCanonicalRows(b.db, rows)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to scan block rows: %w", err)
+ }
+
+ // Check if we have all blocks in the range (no gaps)
+ expectedCount := int(endHeight - startHeight)
+ if len(blocks) != expectedCount {
+ return nil, xerrors.Errorf("missing blocks in range [%d, %d): expected %d, got %d: %w",
+ startHeight, endHeight, expectedCount, len(blocks), errors.ErrItemNotFound)
+ }
+
+ // Verify no gaps in heights
+ for i, block := range blocks {
+ expectedHeight := startHeight + uint64(i)
+ if block.Height != expectedHeight {
+ return nil, xerrors.Errorf("gap in block heights: expected %d, got %d: %w",
+ expectedHeight, block.Height, errors.ErrItemNotFound)
+ }
+ }
+
+ // Validate chain continuity (parent hash matching) like DynamoDB does
+ // This is critical for detecting reorgs and triggering recovery logic
+ if err = parser.ValidateChain(blocks, nil); err != nil {
+ return nil, xerrors.Errorf("failed to validate chain: %w", err)
+ }
+
+ return blocks, nil
+ })
+}
+
+func (b *blockStorageImpl) GetBlocksByHeights(ctx context.Context, tag uint32, heights []uint64) ([]*api.BlockMetadata, error) {
+ return b.instrumentGetBlocksByHeights.Instrument(ctx, func(ctx context.Context) ([]*api.BlockMetadata, error) {
+ for _, height := range heights {
+ if err := b.validateHeight(height); err != nil {
+ return nil, err
+ }
+ }
+ if len(heights) == 0 {
+ return []*api.BlockMetadata{}, nil
+ }
+ // Build dynamic query with placeholders for IN clause
+ placeholders := make([]string, len(heights))
+ args := make([]interface{}, len(heights)+1)
+ args[0] = tag // First argument is tag
+ for i, height := range heights {
+ placeholders[i] = fmt.Sprintf("$%d", i+2) // Start from $2 since $1 is tag
+ args[i+1] = height
+ }
+ query := fmt.Sprintf(`
+ SELECT %s
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON cb.block_metadata_id = bm.id
+ WHERE cb.tag = $1 AND cb.height IN (%s)
+ ORDER BY cb.height ASC`,
+ canonicalBlockMetadataColumns,
+ strings.Join(placeholders, ", "))
+
+ rows, err := b.db.QueryContext(ctx, query, args...)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to query blocks by heights: %w", err)
+ }
+ defer func() {
+ if closeErr := rows.Close(); closeErr != nil && err == nil {
+ err = xerrors.Errorf("failed to close rows: %w", closeErr)
+ }
+ }()
+
+ blocks, err := model.BlockMetadataFromCanonicalRows(b.db, rows)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to scan block rows: %w", err)
+ }
+
+ // Verify we got all requested blocks and return them in the same order as requested
+ blockMap := make(map[uint64]*api.BlockMetadata)
+ for _, block := range blocks {
+ blockMap[block.Height] = block
+ }
+
+ orderedBlocks := make([]*api.BlockMetadata, len(heights))
+ for i, height := range heights {
+ block, exists := blockMap[height]
+ if !exists {
+ return nil, xerrors.Errorf("block at height %d not found: %w", height, errors.ErrItemNotFound)
+ }
+ orderedBlocks[i] = block
+ }
+
+ return orderedBlocks, nil
+ })
+}
+
+func (b *blockStorageImpl) validateHeight(height uint64) error {
+ if height < b.blockStartHeight {
+ return xerrors.Errorf("height(%d) should be no less than blockStartHeight(%d): %w",
+ height, b.blockStartHeight, errors.ErrInvalidHeight)
+ }
+ return nil
+}
+
+// GetWatermarkCount returns the number of watermarked blocks for monitoring purposes
+// This metric helps track watermark accumulation and determine if cleanup is needed
+func (b *blockStorageImpl) GetWatermarkCount(ctx context.Context, tag uint32) (int64, error) {
+ var count int64
+ query := `SELECT COUNT(*) FROM canonical_blocks WHERE tag = $1 AND is_watermark = TRUE`
+ err := b.db.QueryRowContext(ctx, query, tag).Scan(&count)
+ if err != nil {
+ return 0, xerrors.Errorf("failed to get watermark count: %w", err)
+ }
+ return count, nil
+}
+
+func (b *blockStorageImpl) GetBlockByTimestamp(ctx context.Context, tag uint32, timestamp uint64) (*api.BlockMetadata, error) {
+ return b.instrumentGetBlockByTimestamp.Instrument(ctx, func(ctx context.Context) (*api.BlockMetadata, error) {
+ // Query to get the latest block before or at the given timestamp
+ query := fmt.Sprintf(`
+ SELECT %s
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON cb.block_metadata_id = bm.id
+ WHERE cb.tag = $1 AND bm.timestamp <= $2
+ ORDER BY bm.timestamp DESC, bm.height DESC
+ LIMIT 1
+ `, canonicalBlockMetadataColumns)
+
+ var blockId int64
+ var height uint64
+ var blockTag uint32
+ var hash, parentHash, objectKeyMain sql.NullString
+ var parentHeight uint64
+ var blockTimestamp int64
+ var skipped bool
+ var objectFormat int32
+ var byteOffset, byteLength, uncompressedLength sql.NullInt64
+
+ err := b.db.QueryRowContext(ctx, query, tag, timestamp).Scan(
+ &blockId, &height, &blockTag, &hash, &parentHash, &parentHeight, &objectKeyMain, &blockTimestamp, &skipped,
+ &objectFormat, &byteOffset, &byteLength, &uncompressedLength)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ fmt.Printf("[DEBUG] No block found for tag=%d, timestamp=%d\n", tag, timestamp)
+ return nil, xerrors.Errorf("no block found before timestamp %d: %w", timestamp, errors.ErrItemNotFound)
+ }
+ fmt.Printf("[DEBUG] Failed to get block by timestamp: %v\n", err)
+ return nil, xerrors.Errorf("failed to get block by timestamp: %w", err)
+ }
+
+ if !hash.Valid || blockTimestamp == 0 {
+ fmt.Printf("[DEBUG] Invalid block data: height=%d, blockTimestamp=%d, hash.Valid=%v\n", height, blockTimestamp, hash.Valid)
+ return nil, xerrors.Errorf("no block found before timestamp %d: %w", timestamp, errors.ErrItemNotFound)
+ }
+
+ return &api.BlockMetadata{
+ Tag: blockTag,
+ Hash: hash.String,
+ ParentHash: parentHash.String,
+ Height: height,
+ ParentHeight: parentHeight,
+ ObjectKeyMain: objectKeyMain.String,
+ Timestamp: utils.ToTimestamp(blockTimestamp),
+ Skipped: skipped,
+ ObjectFormat: api.BlockObjectFormat(objectFormat),
+ ByteOffset: uint64Value(byteOffset),
+ ByteLength: uint64Value(byteLength),
+ UncompressedLength: uint64Value(uncompressedLength),
+ }, nil
+ })
+}
+
+func (b *blockStorageImpl) GetBlockConsolidationShadow(ctx context.Context, block *api.BlockMetadata) (*api.BlockMetadata, error) {
+ shadows, err := b.GetBlocksConsolidationShadow(ctx, []*api.BlockMetadata{block})
+ if err != nil {
+ return nil, err
+ }
+ if len(shadows) == 0 || shadows[0] == nil {
+ return nil, xerrors.Errorf("consolidation shadow not found: %w", errors.ErrItemNotFound)
+ }
+ return shadows[0], nil
+}
+
+func (b *blockStorageImpl) GetBlocksConsolidationShadow(ctx context.Context, blocks []*api.BlockMetadata) ([]*api.BlockMetadata, error) {
+ shadows := make([]*api.BlockMetadata, len(blocks))
+ placeholders := make([]string, 0, len(blocks))
+ args := make([]interface{}, 0, len(blocks)*5)
+ nextArg := 1
+ for i, block := range blocks {
+ if block.GetSkipped() {
+ continue
+ }
+ placeholders = append(placeholders, fmt.Sprintf(
+ "($%d::int, $%d::int, $%d::bigint, $%d::varchar, $%d::text)",
+ nextArg,
+ nextArg+1,
+ nextArg+2,
+ nextArg+3,
+ nextArg+4,
+ ))
+ args = append(args, i, block.GetTag(), block.GetHeight(), block.GetHash(), block.GetObjectKeyMain())
+ nextArg += 5
+ }
+ if len(placeholders) == 0 {
+ return shadows, nil
+ }
+
+ query := fmt.Sprintf(`
+ WITH input(ord, tag, height, hash, single_block_object_key_main) AS (
+ VALUES %s
+ )
+ SELECT
+ input.ord,
+ shadow.consolidated_object_key_main,
+ shadow.object_format,
+ shadow.byte_offset,
+ shadow.byte_length,
+ shadow.uncompressed_length
+ FROM input
+ LEFT JOIN LATERAL (
+ SELECT consolidated_object_key_main, object_format, byte_offset, byte_length, uncompressed_length
+ FROM block_consolidation_shadow
+ WHERE tag = input.tag
+ AND height = input.height
+ AND hash = input.hash
+ AND single_block_object_key_main = input.single_block_object_key_main
+ AND validated_at IS NOT NULL
+ ORDER BY created_at DESC
+ LIMIT 1
+ ) shadow ON true
+ ORDER BY input.ord ASC`, strings.Join(placeholders, ", "))
+ rows, err := b.db.QueryContext(ctx, query, args...)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get consolidation shadows: %w", err)
+ }
+ defer func() {
+ _ = rows.Close()
+ }()
+
+ for rows.Next() {
+ var ord int
+ var objectKey sql.NullString
+ var objectFormat sql.NullInt64
+ var byteOffset, byteLength sql.NullInt64
+ var uncompressedLength sql.NullInt64
+ if err := rows.Scan(
+ &ord,
+ &objectKey,
+ &objectFormat,
+ &byteOffset,
+ &byteLength,
+ &uncompressedLength,
+ ); err != nil {
+ return nil, xerrors.Errorf("failed to scan consolidation shadow: %w", err)
+ }
+ if !objectKey.Valid {
+ continue
+ }
+ if ord < 0 || ord >= len(blocks) {
+ return nil, xerrors.Errorf("consolidation shadow input order out of range: %d", ord)
+ }
+ shadows[ord] = makeConsolidationShadowBlockMetadata(
+ blocks[ord],
+ objectKey.String,
+ api.BlockObjectFormat(objectFormat.Int64),
+ uint64Value(byteOffset),
+ uint64Value(byteLength),
+ uint64Value(uncompressedLength),
+ )
+ }
+ if err := rows.Err(); err != nil {
+ return nil, xerrors.Errorf("failed to iterate consolidation shadows: %w", err)
+ }
+ return shadows, nil
+}
+
+func (b *blockStorageImpl) GetBlocksMissingConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64, limit uint64) ([]*internal.BlockMetadataRecord, error) {
+ if endHeight <= startHeight {
+ return nil, nil
+ }
+ if limit == 0 {
+ return nil, xerrors.New("consolidation shadow scan limit must be positive")
+ }
+ query := fmt.Sprintf(`
+ SELECT %s
+ FROM block_metadata bm
+ JOIN canonical_blocks cb ON cb.block_metadata_id = bm.id
+ AND cb.tag = bm.tag
+ AND cb.height = bm.height
+ WHERE bm.tag = $1
+ AND bm.height >= $2
+ AND bm.height < $3
+ AND bm.skipped = false
+ AND bm.byte_length IS NULL
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ AND NOT EXISTS (
+ SELECT 1
+ FROM block_consolidation_shadow shadow
+ WHERE shadow.block_metadata_id = bm.id
+ AND shadow.single_block_object_key_main = bm.object_key_main
+ AND shadow.validated_at IS NOT NULL
+ )
+ ORDER BY bm.height ASC
+ LIMIT $4`, canonicalBlockMetadataColumns)
+
+ rows, err := b.db.QueryContext(ctx, query, tag, startHeight, endHeight, limit)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to scan unconsolidated blocks: %w", err)
+ }
+ defer func() {
+ _ = rows.Close()
+ }()
+
+ records := make([]*internal.BlockMetadataRecord, 0)
+ for rows.Next() {
+ var id int64
+ var height uint64
+ var blockTag uint32
+ var hash, parentHash, objectKeyMain sql.NullString
+ var parentHeight uint64
+ var blockTimestamp int64
+ var skipped bool
+ var objectFormat int32
+ var byteOffset, byteLength, uncompressedLength sql.NullInt64
+ if err := rows.Scan(
+ &id,
+ &height,
+ &blockTag,
+ &hash,
+ &parentHash,
+ &parentHeight,
+ &objectKeyMain,
+ &blockTimestamp,
+ &skipped,
+ &objectFormat,
+ &byteOffset,
+ &byteLength,
+ &uncompressedLength,
+ ); err != nil {
+ return nil, xerrors.Errorf("failed to scan unconsolidated block: %w", err)
+ }
+ records = append(records, &internal.BlockMetadataRecord{
+ ID: id,
+ Metadata: &api.BlockMetadata{
+ Tag: blockTag,
+ Hash: hash.String,
+ ParentHash: parentHash.String,
+ Height: height,
+ ParentHeight: parentHeight,
+ ObjectKeyMain: objectKeyMain.String,
+ Timestamp: utils.ToTimestamp(blockTimestamp),
+ Skipped: skipped,
+ ObjectFormat: api.BlockObjectFormat(objectFormat),
+ ByteOffset: uint64Value(byteOffset),
+ ByteLength: uint64Value(byteLength),
+ UncompressedLength: uint64Value(uncompressedLength),
+ },
+ })
+ }
+ if err := rows.Err(); err != nil {
+ return nil, xerrors.Errorf("failed to iterate unconsolidated blocks: %w", err)
+ }
+ return records, nil
+}
+
+func (b *blockStorageImpl) GetFirstBlockMissingConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64) (uint64, bool, error) {
+ if endHeight <= startHeight {
+ return 0, false, nil
+ }
+ const query = `
+ SELECT bm.height
+ FROM block_metadata bm
+ JOIN canonical_blocks cb ON cb.block_metadata_id = bm.id
+ AND cb.tag = bm.tag
+ AND cb.height = bm.height
+ WHERE bm.tag = $1
+ AND bm.height >= $2
+ AND bm.height < $3
+ AND bm.skipped = false
+ AND bm.byte_length IS NULL
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ AND NOT EXISTS (
+ SELECT 1
+ FROM block_consolidation_shadow shadow
+ WHERE shadow.block_metadata_id = bm.id
+ AND shadow.single_block_object_key_main = bm.object_key_main
+ AND shadow.validated_at IS NOT NULL
+ )
+ ORDER BY bm.height ASC
+ LIMIT 1`
+ var height uint64
+ if err := b.db.QueryRowContext(ctx, query, tag, startHeight, endHeight).Scan(&height); err != nil {
+ if err == sql.ErrNoRows {
+ return 0, false, nil
+ }
+ return 0, false, xerrors.Errorf("failed to get first block missing consolidation shadow: %w", err)
+ }
+ return height, true, nil
+}
+
+func (b *blockStorageImpl) GetBlockConsolidationShadowStats(ctx context.Context, tag uint32, startHeight, endHeight uint64) (*internal.ConsolidationShadowStats, error) {
+ if endHeight <= startHeight {
+ return &internal.ConsolidationShadowStats{}, nil
+ }
+ const query = `
+ WITH eligible AS (
+ SELECT
+ bm.id,
+ bm.tag,
+ bm.height,
+ bm.hash,
+ bm.object_key_main,
+ bm.object_format,
+ bm.byte_offset,
+ bm.byte_length,
+ bm.uncompressed_length
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON bm.id = cb.block_metadata_id
+ WHERE cb.tag = $1
+ AND cb.height >= $2
+ AND cb.height < $3
+ AND bm.skipped = false
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ )
+ SELECT
+ COUNT(DISTINCT shadow.consolidated_object_key_main),
+ COUNT(shadow.block_metadata_id),
+ COUNT(*)
+ FROM eligible
+ LEFT JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = eligible.id
+ AND shadow.tag = eligible.tag
+ AND shadow.height = eligible.height
+ AND shadow.hash = eligible.hash
+ AND shadow.tag = $1
+ AND shadow.height >= $2
+ AND shadow.height < $3
+ AND (
+ (
+ shadow.single_block_object_key_main = eligible.object_key_main
+ AND eligible.byte_length IS NULL
+ AND shadow.object_format = $4
+ AND shadow.byte_offset >= 0
+ AND shadow.byte_length > 0
+ AND shadow.uncompressed_length IS NOT NULL
+ AND shadow.uncompressed_length > 0
+ )
+ OR (
+ eligible.object_key_main = shadow.consolidated_object_key_main
+ AND eligible.object_format = $4
+ AND eligible.object_format = shadow.object_format
+ AND eligible.byte_offset = shadow.byte_offset
+ AND eligible.byte_length = shadow.byte_length
+ AND eligible.byte_length > 0
+ AND eligible.uncompressed_length = shadow.uncompressed_length
+ AND eligible.uncompressed_length IS NOT NULL
+ AND eligible.uncompressed_length > 0
+ )
+ )
+ AND shadow.validated_at IS NOT NULL
+ AND shadow.consolidated_object_key_main IS NOT NULL
+ AND shadow.consolidated_object_key_main <> ''`
+
+ var objects, blocks, eligibleBlocks uint64
+ if err := b.db.QueryRowContext(ctx, query, tag, startHeight, endHeight, int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH)).Scan(&objects, &blocks, &eligibleBlocks); err != nil {
+ return nil, xerrors.Errorf("failed to get consolidation shadow stats: %w", err)
+ }
+ return &internal.ConsolidationShadowStats{
+ Objects: objects,
+ Blocks: blocks,
+ EligibleBlocks: eligibleBlocks,
+ }, nil
+}
+
+func (b *blockStorageImpl) GetFirstPromotableBlockConsolidationShadow(ctx context.Context, tag uint32, startHeight, endHeight uint64) (uint64, bool, error) {
+ if endHeight <= startHeight {
+ return 0, false, nil
+ }
+ const query = `
+ SELECT shadow.height
+ FROM block_consolidation_shadow shadow
+ JOIN block_metadata bm ON bm.id = shadow.block_metadata_id
+ AND shadow.tag = bm.tag
+ AND shadow.height = bm.height
+ AND shadow.hash = bm.hash
+ AND shadow.single_block_object_key_main = bm.object_key_main
+ JOIN canonical_blocks cb ON cb.block_metadata_id = bm.id
+ AND cb.tag = bm.tag
+ AND cb.height = bm.height
+ WHERE shadow.tag = $1
+ AND shadow.height >= $2
+ AND shadow.height < $3
+ AND bm.skipped = false
+ AND bm.byte_length IS NULL
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ AND shadow.validated_at IS NOT NULL
+ AND shadow.consolidated_object_key_main IS NOT NULL
+ AND shadow.consolidated_object_key_main <> ''
+ AND shadow.object_format = $4
+ AND shadow.byte_offset >= 0
+ AND shadow.byte_length > 0
+ AND shadow.uncompressed_length IS NOT NULL
+ AND shadow.uncompressed_length > 0
+ ORDER BY shadow.height ASC
+ LIMIT 1`
+ var height uint64
+ if err := b.db.QueryRowContext(
+ ctx,
+ query,
+ tag,
+ startHeight,
+ endHeight,
+ int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH),
+ ).Scan(&height); err != nil {
+ if err == sql.ErrNoRows {
+ return 0, false, nil
+ }
+ return 0, false, xerrors.Errorf("failed to get first promotable consolidation shadow: %w", err)
+ }
+ return height, true, nil
+}
+
+func (b *blockStorageImpl) GetBlockConsolidationCursor(ctx context.Context, name string, tag uint32) (uint64, bool, error) {
+ if name == "" {
+ return 0, false, xerrors.New("block consolidation cursor name must be non-empty")
+ }
+ const query = `
+ SELECT height
+ FROM block_consolidation_cursor
+ WHERE name = $1 AND tag = $2`
+ var height uint64
+ if err := b.db.QueryRowContext(ctx, query, name, tag).Scan(&height); err != nil {
+ if err == sql.ErrNoRows {
+ return 0, false, nil
+ }
+ return 0, false, xerrors.Errorf("failed to get block consolidation cursor %q for tag %d: %w", name, tag, err)
+ }
+ return height, true, nil
+}
+
+func (b *blockStorageImpl) SetBlockConsolidationCursor(ctx context.Context, name string, tag uint32, height uint64) error {
+ if name == "" {
+ return xerrors.New("block consolidation cursor name must be non-empty")
+ }
+ const query = `
+ INSERT INTO block_consolidation_cursor (name, tag, height, updated_at)
+ VALUES ($1, $2, $3, NOW())
+ ON CONFLICT (name, tag) DO UPDATE SET
+ height = GREATEST(block_consolidation_cursor.height, EXCLUDED.height),
+ updated_at = NOW()`
+ if _, err := b.db.ExecContext(ctx, query, name, tag, height); err != nil {
+ return xerrors.Errorf("failed to set block consolidation cursor %q for tag %d to height %d: %w", name, tag, height, err)
+ }
+ return nil
+}
+
+func (b *blockStorageImpl) PersistBlockConsolidationShadows(ctx context.Context, placements []*internal.ConsolidationShadowPlacement) error {
+ if len(placements) == 0 {
+ return nil
+ }
+ tx, err := b.db.BeginTx(ctx, nil)
+ if err != nil {
+ return xerrors.Errorf("failed to begin consolidation shadow transaction: %w", err)
+ }
+ committed := false
+ defer func() {
+ if !committed {
+ _ = tx.Rollback()
+ }
+ }()
+ tags := make([]uint32, 0, len(placements))
+ for _, placement := range placements {
+ if placement != nil {
+ tags = append(tags, placement.Tag)
+ }
+ }
+ if err := cscbrepairlock.AcquireTags(ctx, tx, tags); err != nil {
+ return err
+ }
+
+ const query = `
+ INSERT INTO block_consolidation_shadow (
+ block_metadata_id, tag, height, hash, single_block_object_key_main, consolidated_object_key_main,
+ object_format, byte_offset, byte_length, uncompressed_length, validated_at
+ )
+ SELECT
+ bm.id, bm.tag, bm.height, bm.hash, bm.object_key_main, $6,
+ $7, $8, $9, $10, NOW()
+ FROM block_metadata bm
+ WHERE bm.id = $1
+ AND bm.tag = $2
+ AND bm.height = $3
+ AND bm.hash = $4
+ AND bm.object_key_main = $5
+ AND bm.skipped = false
+ ON CONFLICT (block_metadata_id) DO UPDATE SET
+ tag = EXCLUDED.tag,
+ height = EXCLUDED.height,
+ hash = EXCLUDED.hash,
+ single_block_object_key_main = EXCLUDED.single_block_object_key_main,
+ consolidated_object_key_main = EXCLUDED.consolidated_object_key_main,
+ object_format = EXCLUDED.object_format,
+ byte_offset = EXCLUDED.byte_offset,
+ byte_length = EXCLUDED.byte_length,
+ uncompressed_length = EXCLUDED.uncompressed_length,
+ validated_at = EXCLUDED.validated_at
+ WHERE block_consolidation_shadow.single_block_object_key_main = EXCLUDED.single_block_object_key_main
+ RETURNING block_metadata_id`
+
+ for _, placement := range placements {
+ if placement == nil {
+ continue
+ }
+ var id int64
+ err := tx.QueryRowContext(
+ ctx,
+ query,
+ placement.BlockMetadataID,
+ placement.Tag,
+ placement.Height,
+ placement.Hash,
+ placement.SingleBlockObjectKeyMain,
+ placement.ConsolidatedObjectKeyMain,
+ int32(placement.ObjectFormat),
+ placement.ByteOffset,
+ placement.ByteLength,
+ placement.UncompressedLength,
+ ).Scan(&id)
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return xerrors.Errorf("failed to persist consolidation shadow for metadata_id=%d height=%d: primary metadata identity mismatch", placement.BlockMetadataID, placement.Height)
+ }
+ return xerrors.Errorf("failed to persist consolidation shadow for metadata_id=%d height=%d: %w", placement.BlockMetadataID, placement.Height, err)
+ }
+ }
+ if err := tx.Commit(); err != nil {
+ return xerrors.Errorf("failed to commit consolidation shadows: %w", err)
+ }
+ committed = true
+ return nil
+}
+
+func (b *blockStorageImpl) PromoteBlockConsolidationShadows(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ limit uint64,
+ singleBlockObjectRetention time.Duration,
+) (*internal.ConsolidationPromotionResult, error) {
+ if endHeight <= startHeight {
+ return &internal.ConsolidationPromotionResult{}, nil
+ }
+ if limit == 0 {
+ return nil, xerrors.New("consolidation promotion limit must be positive")
+ }
+ if singleBlockObjectRetention <= 0 {
+ return nil, xerrors.New("single-block object retention must be positive")
+ }
+ if err := b.validateHeight(startHeight); err != nil {
+ return nil, err
+ }
+ tx, err := b.db.BeginTx(ctx, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to begin consolidation promotion transaction: %w", err)
+ }
+ committed := false
+ defer func() {
+ if !committed {
+ _ = tx.Rollback()
+ }
+ }()
+ if err := cscbrepairlock.AcquireTag(ctx, tx, tag); err != nil {
+ return nil, err
+ }
+
+ const invalidShadowQuery = `
+ SELECT shadow.block_metadata_id, shadow.height
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON bm.id = cb.block_metadata_id
+ JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ AND shadow.tag = bm.tag
+ AND shadow.height = bm.height
+ AND shadow.hash = bm.hash
+ AND shadow.single_block_object_key_main = bm.object_key_main
+ WHERE cb.tag = $1
+ AND cb.height >= $2
+ AND cb.height < $3
+ AND bm.skipped = false
+ AND bm.byte_length IS NULL
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ AND shadow.validated_at IS NOT NULL
+ AND (
+ shadow.consolidated_object_key_main IS NULL
+ OR shadow.consolidated_object_key_main = ''
+ OR shadow.object_format <> $4
+ OR shadow.byte_offset < 0
+ OR shadow.byte_length <= 0
+ OR shadow.uncompressed_length IS NULL
+ OR shadow.uncompressed_length <= 0
+ )
+ ORDER BY cb.height ASC
+ LIMIT 1`
+ var invalidMetadataID int64
+ var invalidHeight uint64
+ err = tx.QueryRowContext(
+ ctx,
+ invalidShadowQuery,
+ tag,
+ startHeight,
+ endHeight,
+ int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH),
+ ).Scan(&invalidMetadataID, &invalidHeight)
+ if err != nil && err != sql.ErrNoRows {
+ return nil, xerrors.Errorf("failed to validate consolidation promotion shadows: %w", err)
+ }
+ if err == nil {
+ return nil, xerrors.Errorf("invalid consolidation shadow metadata for metadata_id=%d height=%d", invalidMetadataID, invalidHeight)
+ }
+
+ const promoteQuery = `
+ WITH database_clock AS (
+ SELECT clock_timestamp() AS retention_started_at
+ ), candidates AS (
+ SELECT
+ bm.id AS block_metadata_id,
+ bm.tag,
+ bm.height,
+ bm.hash,
+ bm.object_key_main AS single_block_object_key_main,
+ shadow.consolidated_object_key_main,
+ shadow.object_format,
+ shadow.byte_offset,
+ shadow.byte_length,
+ shadow.uncompressed_length
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON bm.id = cb.block_metadata_id
+ JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ AND shadow.tag = bm.tag
+ AND shadow.height = bm.height
+ AND shadow.hash = bm.hash
+ AND shadow.single_block_object_key_main = bm.object_key_main
+ WHERE cb.tag = $1
+ AND cb.height >= $2
+ AND cb.height < $3
+ AND bm.skipped = false
+ AND bm.byte_length IS NULL
+ AND bm.object_key_main IS NOT NULL
+ AND bm.object_key_main <> ''
+ AND shadow.validated_at IS NOT NULL
+ AND shadow.consolidated_object_key_main IS NOT NULL
+ AND shadow.consolidated_object_key_main <> ''
+ AND shadow.object_format = $5
+ AND shadow.byte_offset >= 0
+ AND shadow.byte_length > 0
+ AND shadow.uncompressed_length IS NOT NULL
+ AND shadow.uncompressed_length > 0
+ ORDER BY cb.height ASC
+ LIMIT $4
+ FOR UPDATE OF cb, bm, shadow
+ ),
+ updated AS (
+ UPDATE block_metadata bm
+ SET
+ object_key_main = candidates.consolidated_object_key_main,
+ object_format = candidates.object_format,
+ byte_offset = candidates.byte_offset,
+ byte_length = candidates.byte_length,
+ uncompressed_length = candidates.uncompressed_length
+ FROM candidates
+ WHERE bm.id = candidates.block_metadata_id
+ AND bm.tag = candidates.tag
+ AND bm.height = candidates.height
+ AND bm.hash = candidates.hash
+ AND bm.object_key_main = candidates.single_block_object_key_main
+ AND bm.skipped = false
+ AND bm.byte_length IS NULL
+ RETURNING bm.id
+ ),
+ retired AS (
+ UPDATE block_consolidation_shadow shadow
+ SET
+ single_block_retention_started_at = database_clock.retention_started_at,
+ single_block_delete_after = database_clock.retention_started_at + ($6 * INTERVAL '1 microsecond')
+ FROM candidates, database_clock
+ WHERE shadow.block_metadata_id = candidates.block_metadata_id
+ AND shadow.tag = candidates.tag
+ AND shadow.height = candidates.height
+ AND shadow.hash = candidates.hash
+ AND shadow.single_block_object_key_main = candidates.single_block_object_key_main
+ RETURNING shadow.block_metadata_id
+ )
+ SELECT
+ (SELECT COUNT(*) FROM candidates),
+ (SELECT COUNT(*) FROM updated),
+ (SELECT COUNT(*) FROM retired)`
+
+ var candidates, promoted, retired uint64
+ if err := tx.QueryRowContext(
+ ctx,
+ promoteQuery,
+ tag,
+ startHeight,
+ endHeight,
+ limit,
+ int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH),
+ singleBlockObjectRetention.Microseconds(),
+ ).Scan(&candidates, &promoted, &retired); err != nil {
+ return nil, xerrors.Errorf("failed to promote consolidation shadows: %w", err)
+ }
+ if candidates != promoted {
+ return nil, xerrors.Errorf("consolidation promotion guard failed: selected %d rows but promoted %d", candidates, promoted)
+ }
+ if candidates != retired {
+ return nil, xerrors.Errorf("consolidation retirement guard failed: selected %d rows but marked %d for retirement", candidates, retired)
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, xerrors.Errorf("failed to commit consolidation promotion: %w", err)
+ }
+ committed = true
+ return &internal.ConsolidationPromotionResult{
+ Blocks: promoted,
+ }, nil
+}
+
+func makeConsolidationShadowBlockMetadata(
+ block *api.BlockMetadata,
+ objectKey string,
+ objectFormat api.BlockObjectFormat,
+ byteOffset uint64,
+ byteLength uint64,
+ uncompressedLength uint64,
+) *api.BlockMetadata {
+ shadow := proto.Clone(block).(*api.BlockMetadata)
+ shadow.ObjectKeyMain = objectKey
+ shadow.ObjectFormat = objectFormat
+ shadow.ByteOffset = byteOffset
+ shadow.ByteLength = byteLength
+ shadow.UncompressedLength = uncompressedLength
+ return shadow
+}
+
+func uint64Value(value sql.NullInt64) uint64 {
+ if !value.Valid {
+ return 0
+ }
+ return uint64(value.Int64)
+}
+
+func blockObjectByteFields(block *api.BlockMetadata) (sql.NullInt64, sql.NullInt64, sql.NullInt64) {
+ if block.GetObjectFormat() != api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH {
+ return sql.NullInt64{}, sql.NullInt64{}, sql.NullInt64{}
+ }
+ return sql.NullInt64{Int64: int64(block.GetByteOffset()), Valid: true},
+ sql.NullInt64{Int64: int64(block.GetByteLength()), Valid: true},
+ sql.NullInt64{Int64: int64(block.GetUncompressedLength()), Valid: true}
+}
diff --git a/internal/storage/metastorage/postgres/block_storage_integration_test.go b/internal/storage/metastorage/postgres/block_storage_integration_test.go
new file mode 100644
index 00000000..bfc40c0f
--- /dev/null
+++ b/internal/storage/metastorage/postgres/block_storage_integration_test.go
@@ -0,0 +1,1633 @@
+package postgres
+
+import (
+ "context"
+ "crypto/sha256"
+ "database/sql"
+ "encoding/hex"
+ "fmt"
+ "math/rand/v2"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/google/go-cmp/cmp"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "go.uber.org/zap/zaptest"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/testing/protocmp"
+
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/internal/errors"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/internal"
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+const (
+ tag = 1
+)
+
+type blockStorageTestSuite struct {
+ suite.Suite
+ accessor internal.MetaStorage
+ config *config.Config
+ db *sql.DB
+}
+
+func (s *blockStorageTestSuite) SetupSuite() {
+ require := testutil.Require(s.T())
+ cfg, err := config.New()
+ require.NoError(err)
+ if !cfg.IsIntegrationTest() || cfg.AWS.Postgres == nil {
+ return
+ }
+
+ db, err := newDBConnection(context.Background(), cfg.AWS.Postgres)
+ require.NoError(err)
+ defer db.Close()
+ require.NoError(runMigrations(context.Background(), db))
+}
+
+func (s *blockStorageTestSuite) SetupTest() {
+ require := testutil.Require(s.T())
+
+ var accessor internal.MetaStorage
+ cfg, err := config.New()
+ require.NoError(err)
+
+ // Skip tests if Postgres is not configured
+ if cfg.AWS.Postgres == nil {
+ s.T().Skip("Postgres not configured, skipping test suite")
+ return
+ }
+
+ // Set the starting block height
+ cfg.Chain.BlockStartHeight = 10
+ s.config = cfg
+ // Create a new test application with Postgres configuration
+ app := testapp.New(
+ s.T(),
+ fx.Provide(NewMetaStorage),
+ testapp.WithIntegration(),
+ testapp.WithConfig(s.config),
+ fx.Populate(&accessor),
+ )
+ defer app.Close()
+ s.accessor = accessor
+
+ // Get database connection for cleanup
+ db, err := newDBConnection(context.Background(), cfg.AWS.Postgres)
+ require.NoError(err)
+ s.db = db
+}
+
+func (s *blockStorageTestSuite) TearDownTest() {
+ if s.db != nil {
+ ctx := context.Background()
+ s.T().Log("Clearing database tables after test")
+ _, err := s.db.ExecContext(ctx, `ALTER TABLE block_single_block_retention DISABLE TRIGGER block_single_block_retention_delete_trigger`)
+ if err != nil {
+ s.T().Errorf("Failed to disable retirement audit delete trigger for test cleanup: %v", err)
+ return
+ }
+ defer func() {
+ if _, err := s.db.ExecContext(ctx, `ALTER TABLE block_single_block_retention ENABLE TRIGGER block_single_block_retention_delete_trigger`); err != nil {
+ s.T().Errorf("Failed to restore retirement audit delete trigger after test cleanup: %v", err)
+ }
+ }()
+ // Clear all tables in reverse order due to foreign key constraints
+ tables := []string{"block_events", "block_single_block_retention", "block_consolidation_shadow", "canonical_blocks", "block_metadata"}
+ for _, table := range tables {
+ _, err := s.db.ExecContext(ctx, fmt.Sprintf("DELETE FROM %s", table))
+ if err != nil {
+ s.T().Logf("Failed to clear table %s: %v", table, err)
+ }
+ }
+ }
+}
+
+func (s *blockStorageTestSuite) TearDownSuite() {
+ if s.db != nil {
+ s.db.Close()
+ }
+}
+
+func (s *blockStorageTestSuite) TestPersistBlockMetasPreservesRetirementFencedCSCBPlacement() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ block := testutil.MakeBlockMetadatasFromStartHeight(s.config.Chain.BlockStartHeight, 1, tag)[0]
+ block.ObjectKeyMain = "single-block/fenced-block.gzip"
+ block.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK
+ block.ByteOffset = 0
+ block.ByteLength = 0
+ block.UncompressedLength = 0
+ guardStorage, ok := s.accessor.(internal.SingleBlockUploadGuardStorage)
+ require.True(ok)
+ uploadContext, cancelUpload := context.WithCancel(ctx)
+ uploadGuard, err := guardStorage.AcquireSingleBlockUploadGuard(uploadContext, block.Tag, block.Height, block.Hash)
+ require.NoError(err)
+ require.NotNil(uploadGuard)
+ require.False(uploadGuard.RetirementFenced())
+ cancelUpload()
+ defer func() { _ = uploadGuard.Release() }()
+ require.NoError(s.accessor.PersistBlockMetas(ctx, true, []*api.BlockMetadata{block}, nil))
+
+ const cscbKey = "consolidated/fenced-batch.cscb.gzip"
+ var blockMetadataID int64
+ err = s.db.QueryRowContext(ctx, `
+ UPDATE block_metadata
+ SET object_key_main = $1,
+ object_format = $2,
+ byte_offset = $3,
+ byte_length = $4,
+ uncompressed_length = $5
+ WHERE tag = $6 AND hash = $7
+ RETURNING id`,
+ cscbKey,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 64,
+ 128,
+ 256,
+ block.Tag,
+ block.Hash,
+ ).Scan(&blockMetadataID)
+ require.NoError(err)
+
+ validatedAt := time.Now().UTC().Add(-96 * time.Hour)
+ retireAfter := validatedAt.Add(72 * time.Hour)
+ _, err = s.db.ExecContext(ctx, `
+ INSERT INTO block_consolidation_shadow (
+ block_metadata_id, tag, height, hash, single_block_object_key_main,
+ consolidated_object_key_main, object_format, byte_offset, byte_length,
+ uncompressed_length, validated_at, single_block_retention_started_at, single_block_delete_after
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11, $12)`,
+ blockMetadataID,
+ block.Tag,
+ block.Height,
+ block.Hash,
+ block.ObjectKeyMain,
+ cscbKey,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 64,
+ 128,
+ 256,
+ validatedAt,
+ retireAfter,
+ )
+ require.NoError(err)
+
+ preparedAt := time.Now().UTC()
+ repo := retirement.NewPostgresRepository(s.db)
+ manifest := retirement.RetirementManifest{
+ BlockMetadataID: blockMetadataID,
+ Tag: block.Tag,
+ Height: block.Height,
+ Hash: block.Hash,
+ State: retirement.RetirementStateEligible,
+ Bucket: "integration-bucket",
+ SingleBlockObjectKey: block.ObjectKeyMain,
+ SingleBlockObjectKeySHA256: sha256Hex(block.ObjectKeyMain),
+ SingleBlockObjectVersionIDs: []string{"single-block-v1"},
+ SingleBlockObjectETag: "single-block-etag",
+ SingleBlockObjectBytes: 512,
+ ConsolidatedObjectKey: cscbKey,
+ ConsolidatedObjectVersionID: "cscb-v1",
+ ConsolidatedObjectETag: "cscb-etag",
+ ConsolidatedByteOffset: 64,
+ ConsolidatedByteLength: 128,
+ ConsolidatedUncompressedLength: 256,
+ PayloadSHA256: strings.Repeat("a", 64),
+ PreparedAt: preparedAt,
+ }
+ prepareDone := make(chan error, 1)
+ go func() {
+ prepareDone <- repo.PrepareRetirement(ctx, manifest)
+ }()
+ select {
+ case err := <-prepareDone:
+ require.Failf("retirement preparation bypassed upload guard", "unexpected result: %v", err)
+ case <-time.After(100 * time.Millisecond):
+ }
+ require.NoError(uploadGuard.Release())
+ require.NoError(<-prepareDone)
+
+ fencedGuard, err := guardStorage.AcquireSingleBlockUploadGuard(ctx, block.Tag, block.Height, block.Hash)
+ require.NoError(err)
+ require.NotNil(fencedGuard)
+ require.True(fencedGuard.RetirementFenced())
+ require.NoError(fencedGuard.Release())
+ _, err = s.db.ExecContext(ctx, `
+ UPDATE block_metadata
+ SET object_key_main = $2,
+ object_format = $3,
+ byte_offset = NULL,
+ byte_length = NULL,
+ uncompressed_length = NULL
+ WHERE id = $1`,
+ blockMetadataID,
+ "single-block/direct-sql-regression.gzip",
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK,
+ )
+ require.Error(err)
+ require.Contains(err.Error(), "cannot change CSCB placement after single-block object retirement is fenced")
+
+ replayedSingleBlock := proto.Clone(block).(*api.BlockMetadata)
+ replayedSingleBlock.ObjectKeyMain = "single-block/replayed-block.gzip"
+ replayedSingleBlock.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK
+ require.NoError(s.accessor.PersistBlockMetas(ctx, false, []*api.BlockMetadata{replayedSingleBlock}, nil))
+
+ for _, fetched := range []*api.BlockMetadata{
+ mustGetBlockByHash(s.T(), s.accessor, block),
+ mustGetBlockByHeight(s.T(), s.accessor, block),
+ } {
+ require.Equal(cscbKey, fetched.ObjectKeyMain)
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, fetched.ObjectFormat)
+ require.Equal(uint64(64), fetched.ByteOffset)
+ require.Equal(uint64(128), fetched.ByteLength)
+ require.Equal(uint64(256), fetched.UncompressedLength)
+ }
+
+ claimToken := "normal-read-path-claim"
+ claimedAt := time.Now().UTC()
+ require.NoError(repo.ClaimRetirement(ctx, blockMetadataID, claimToken, claimedAt, claimedAt.Add(time.Hour)))
+ _, err = repo.RecordRetirementObjectDeleted(ctx, blockMetadataID, claimToken, retirement.ActionDeletedObjectVersion)
+ require.NoError(err)
+ require.NoError(s.accessor.PersistBlockMetas(ctx, false, []*api.BlockMetadata{replayedSingleBlock}, nil))
+ for _, fetched := range []*api.BlockMetadata{
+ mustGetBlockByHash(s.T(), s.accessor, block),
+ mustGetBlockByHeight(s.T(), s.accessor, block),
+ } {
+ require.Equal(cscbKey, fetched.ObjectKeyMain)
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, fetched.ObjectFormat)
+ }
+ _, err = repo.FinalizeRetirement(ctx, blockMetadataID, claimToken, retirement.ActionDeletedVerified)
+ require.NoError(err)
+ require.NoError(s.accessor.PersistBlockMetas(ctx, false, []*api.BlockMetadata{replayedSingleBlock}, nil))
+
+ for _, fetched := range []*api.BlockMetadata{
+ mustGetBlockByHash(s.T(), s.accessor, block),
+ mustGetBlockByHeight(s.T(), s.accessor, block),
+ } {
+ require.Equal(cscbKey, fetched.ObjectKeyMain)
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, fetched.ObjectFormat)
+ require.Equal(uint64(64), fetched.ByteOffset)
+ require.Equal(uint64(128), fetched.ByteLength)
+ require.Equal(uint64(256), fetched.UncompressedLength)
+ }
+
+ var clearedSingleBlockPath sql.NullString
+ var singleBlockDeletedAt sql.NullTime
+ err = s.db.QueryRowContext(ctx, `
+ SELECT single_block_object_key_main, single_block_object_deleted_at
+ FROM block_consolidation_shadow
+ WHERE block_metadata_id = $1`, blockMetadataID).Scan(&clearedSingleBlockPath, &singleBlockDeletedAt)
+ require.NoError(err)
+ require.False(clearedSingleBlockPath.Valid)
+ require.True(singleBlockDeletedAt.Valid)
+
+ _, err = s.db.ExecContext(ctx, `
+ UPDATE block_consolidation_shadow
+ SET single_block_object_key_main = $2,
+ single_block_object_deleted_at = NULL
+ WHERE block_metadata_id = $1`, blockMetadataID, "single-block/direct-shadow-regression.gzip")
+ require.Error(err)
+ require.Contains(err.Error(), "cannot restore or rewrite deleted single-block object metadata")
+
+ _, err = s.db.ExecContext(ctx, `
+ UPDATE block_consolidation_shadow
+ SET single_block_object_deleted_at = single_block_object_deleted_at + INTERVAL '1 second'
+ WHERE block_metadata_id = $1`, blockMetadataID)
+ require.Error(err)
+ require.Contains(err.Error(), "cannot restore or rewrite deleted single-block object metadata")
+}
+
+func (s *blockStorageTestSuite) TestSingleBlockUploadGuardMatchesNullableHash() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ height := s.config.Chain.BlockStartHeight + 1
+
+ var blockMetadataID int64
+ err := s.db.QueryRowContext(ctx, `
+ INSERT INTO block_metadata (
+ height, tag, hash, parent_height, object_key_main, timestamp, skipped,
+ object_format, byte_offset, byte_length, uncompressed_length
+ ) VALUES ($1, $2, NULL, $3, $4, $5, FALSE, $6, 0, 128, 128)
+ RETURNING id`,
+ height,
+ tag,
+ height-1,
+ "consolidated/null-hash.cscb.gzip",
+ time.Now().UTC().Unix(),
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ).Scan(&blockMetadataID)
+ require.NoError(err)
+ _, err = s.db.ExecContext(ctx, `
+ INSERT INTO canonical_blocks (height, block_metadata_id, tag)
+ VALUES ($1, $2, $3)`, height, blockMetadataID, tag)
+ require.NoError(err)
+ var nonCanonicalBlockMetadataID int64
+ err = s.db.QueryRowContext(ctx, `
+ INSERT INTO block_metadata (
+ height, tag, hash, parent_height, object_key_main, timestamp, skipped,
+ object_format, byte_offset, byte_length, uncompressed_length
+ ) VALUES ($1, $2, NULL, $3, $4, $5, FALSE, $6, 0, 128, 128)
+ RETURNING id`,
+ height,
+ tag,
+ height-1,
+ "consolidated/non-canonical-null-hash.cscb.gzip",
+ time.Now().UTC().Unix(),
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ).Scan(&nonCanonicalBlockMetadataID)
+ require.NoError(err)
+ _, err = s.db.ExecContext(ctx, `
+ UPDATE block_metadata
+ SET single_block_retention_fenced_at = CURRENT_TIMESTAMP
+ WHERE id = $1`, nonCanonicalBlockMetadataID)
+ require.NoError(err)
+
+ guardStorage, ok := s.accessor.(internal.SingleBlockUploadGuardStorage)
+ require.True(ok)
+ guard, err := guardStorage.AcquireSingleBlockUploadGuard(ctx, tag, height, "")
+ require.NoError(err)
+ require.NotNil(guard)
+ require.False(guard.RetirementFenced())
+ canonicalUpdateConn, err := s.db.Conn(ctx)
+ require.NoError(err)
+ defer canonicalUpdateConn.Close()
+ _, err = canonicalUpdateConn.ExecContext(ctx, "SET lock_timeout = '100ms'")
+ require.NoError(err)
+ _, err = canonicalUpdateConn.ExecContext(ctx, `
+ UPDATE canonical_blocks
+ SET block_metadata_id = $1
+ WHERE tag = $2 AND height = $3`, nonCanonicalBlockMetadataID, tag, height)
+ require.ErrorContains(err, "canceling statement due to lock timeout")
+ require.NoError(guard.Release())
+ _, err = canonicalUpdateConn.ExecContext(ctx, "SET lock_timeout = 0")
+ require.NoError(err)
+ _, err = canonicalUpdateConn.ExecContext(ctx, `
+ UPDATE canonical_blocks
+ SET block_metadata_id = $1
+ WHERE tag = $2 AND height = $3`, nonCanonicalBlockMetadataID, tag, height)
+ require.NoError(err)
+ fencedGuard, err := guardStorage.AcquireSingleBlockUploadGuard(ctx, tag, height, "")
+ require.NoError(err)
+ require.NotNil(fencedGuard)
+ require.True(fencedGuard.RetirementFenced())
+ require.NoError(fencedGuard.Release())
+}
+
+func (s *blockStorageTestSuite) TestSingleBlockUploadGuardMatchesExactHash() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ height := s.config.Chain.BlockStartHeight + 3
+
+ var fencedBlockMetadataID int64
+ for _, blockHash := range []string{"unfenced-hash", "fenced-hash"} {
+ var blockMetadataID int64
+ err := s.db.QueryRowContext(ctx, `
+ INSERT INTO block_metadata (
+ height, tag, hash, parent_height, object_key_main, timestamp, skipped,
+ object_format, byte_offset, byte_length, uncompressed_length
+ ) VALUES ($1, $2, $3, $4, $5, $6, FALSE, $7, 0, 128, 128)
+ RETURNING id`,
+ height,
+ tag,
+ blockHash,
+ height-1,
+ "consolidated/"+blockHash+".cscb.gzip",
+ time.Now().UTC().Unix(),
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ).Scan(&blockMetadataID)
+ require.NoError(err)
+ if blockHash == "fenced-hash" {
+ fencedBlockMetadataID = blockMetadataID
+ }
+ }
+ _, err := s.db.ExecContext(ctx, `
+ UPDATE block_metadata
+ SET single_block_retention_fenced_at = CURRENT_TIMESTAMP
+ WHERE id = $1`, fencedBlockMetadataID)
+ require.NoError(err)
+
+ guardStorage, ok := s.accessor.(internal.SingleBlockUploadGuardStorage)
+ require.True(ok)
+ unfencedGuard, err := guardStorage.AcquireSingleBlockUploadGuard(ctx, tag, height, "unfenced-hash")
+ require.NoError(err)
+ require.False(unfencedGuard.RetirementFenced())
+ require.NoError(unfencedGuard.Release())
+ fencedGuard, err := guardStorage.AcquireSingleBlockUploadGuard(ctx, tag, height, "fenced-hash")
+ require.NoError(err)
+ require.True(fencedGuard.RetirementFenced())
+ require.NoError(fencedGuard.Release())
+}
+
+func (s *blockStorageTestSuite) TestSingleBlockUploadGuardWithoutHashAllowsMissingCanonicalMetadata() {
+ require := testutil.Require(s.T())
+ guardStorage, ok := s.accessor.(internal.SingleBlockUploadGuardStorage)
+ require.True(ok)
+
+ guard, err := guardStorage.AcquireSingleBlockUploadGuard(
+ context.Background(),
+ tag,
+ s.config.Chain.BlockStartHeight+4,
+ "",
+ )
+ require.NoError(err)
+ require.NotNil(guard)
+ require.False(guard.RetirementFenced())
+ require.NoError(guard.Release())
+}
+
+func (s *blockStorageTestSuite) TestSingleBlockShadowCompatibilityColumnsStaySynchronized() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ height := s.config.Chain.BlockStartHeight + 2
+ validatedAt := time.Now().UTC().Add(-96 * time.Hour).Truncate(time.Microsecond)
+ deleteAfter := validatedAt.Add(72 * time.Hour)
+
+ var blockMetadataID int64
+ err := s.db.QueryRowContext(ctx, `
+ INSERT INTO block_metadata (
+ height, tag, hash, parent_height, object_key_main, timestamp, skipped,
+ object_format, byte_offset, byte_length, uncompressed_length
+ ) VALUES ($1, $2, $3, $4, $5, $6, FALSE, $7, $8, $9, $10)
+ RETURNING id`,
+ height,
+ tag,
+ "compatibility-hash",
+ height-1,
+ "consolidated/compatibility.cscb.gzip",
+ time.Now().UTC().Unix(),
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 64,
+ 128,
+ 256,
+ ).Scan(&blockMetadataID)
+ require.NoError(err)
+
+ _, err = s.db.ExecContext(ctx, `
+ INSERT INTO block_consolidation_shadow (
+ block_metadata_id, tag, height, hash, legacy_object_key_main,
+ consolidated_object_key_main, object_format, byte_offset, byte_length,
+ uncompressed_length, validated_at, legacy_object_retired_at, legacy_object_retire_after
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $11, $12)`,
+ blockMetadataID,
+ tag,
+ height,
+ "compatibility-hash",
+ "single-block/compatibility.gzip",
+ "consolidated/compatibility.cscb.gzip",
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 64,
+ 128,
+ 256,
+ validatedAt,
+ deleteAfter,
+ )
+ require.NoError(err)
+
+ var canonicalKey string
+ var canonicalStartedAt time.Time
+ var canonicalDeleteAfter time.Time
+ err = s.db.QueryRowContext(ctx, `
+ SELECT single_block_object_key_main, single_block_retention_started_at, single_block_delete_after
+ FROM block_consolidation_shadow
+ WHERE block_metadata_id = $1`, blockMetadataID).Scan(&canonicalKey, &canonicalStartedAt, &canonicalDeleteAfter)
+ require.NoError(err)
+ require.Equal("single-block/compatibility.gzip", canonicalKey)
+ require.WithinDuration(validatedAt, canonicalStartedAt, 0)
+ require.WithinDuration(deleteAfter, canonicalDeleteAfter, 0)
+
+ deletedAt := time.Now().UTC().Truncate(time.Microsecond)
+ _, err = s.db.ExecContext(ctx, `
+ UPDATE block_consolidation_shadow
+ SET single_block_object_key_main = NULL,
+ single_block_object_deleted_at = $2
+ WHERE block_metadata_id = $1`, blockMetadataID, deletedAt)
+ require.NoError(err)
+
+ var compatibilityKey sql.NullString
+ var canonicalDeletedAt time.Time
+ err = s.db.QueryRowContext(ctx, `
+ SELECT legacy_object_key_main, single_block_object_deleted_at
+ FROM block_consolidation_shadow
+ WHERE block_metadata_id = $1`, blockMetadataID).Scan(&compatibilityKey, &canonicalDeletedAt)
+ require.NoError(err)
+ require.False(compatibilityKey.Valid)
+ require.WithinDuration(deletedAt, canonicalDeletedAt, 0)
+}
+
+func sha256Hex(value string) string {
+ digest := sha256.Sum256([]byte(value))
+ return hex.EncodeToString(digest[:])
+}
+
+func mustGetBlockByHash(t *testing.T, accessor internal.MetaStorage, block *api.BlockMetadata) *api.BlockMetadata {
+ t.Helper()
+ result, err := accessor.GetBlockByHash(context.Background(), block.Tag, block.Height, block.Hash)
+ require.NoError(t, err)
+ return result
+}
+
+func mustGetBlockByHeight(t *testing.T, accessor internal.MetaStorage, block *api.BlockMetadata) *api.BlockMetadata {
+ t.Helper()
+ result, err := accessor.GetBlockByHeight(context.Background(), block.Tag, block.Height)
+ require.NoError(t, err)
+ return result
+}
+
+func (s *blockStorageTestSuite) TestPersistBlockMetasByMaxWriteSize() {
+ tests := []struct {
+ totalBlocks int
+ }{
+ {totalBlocks: 2},
+ {totalBlocks: 4},
+ {totalBlocks: 8},
+ {totalBlocks: 64},
+ }
+ for _, test := range tests {
+ s.T().Run(fmt.Sprintf("test %d blocks", test.totalBlocks), func(t *testing.T) {
+ s.runTestPersistBlockMetas(test.totalBlocks)
+ })
+ }
+}
+
+func (s *blockStorageTestSuite) runTestPersistBlockMetas(totalBlocks int) {
+ require := testutil.Require(s.T())
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, totalBlocks, tag)
+ zaptest.NewLogger(s.T())
+ ctx := context.TODO()
+
+ // shuffle it to make sure it still works
+ shuffleSeed := time.Now().UnixNano()
+ rand.Shuffle(len(blocks), func(i, j int) { blocks[i], blocks[j] = blocks[j], blocks[i] })
+ logger := zaptest.NewLogger(s.T())
+ logger.Info("shuffled blocks", zap.Int64("seed", shuffleSeed))
+
+ fmt.Println("Persisting blocks")
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ if err != nil {
+ panic(err)
+ }
+
+ expectedLatestBlock := proto.Clone(blocks[totalBlocks-1])
+
+ // fetch range with missing item
+ fmt.Println("Fetching range with missing item")
+ _, err = s.accessor.GetBlocksByHeightRange(ctx, tag, startHeight, startHeight+uint64(totalBlocks+100))
+ require.Error(err)
+ require.True(xerrors.Is(err, errors.ErrItemNotFound))
+
+ // fetch valid range
+ fmt.Println("Fetching valid range")
+ fetchedBlocks, err := s.accessor.GetBlocksByHeightRange(ctx, tag, startHeight, startHeight+uint64(totalBlocks))
+ if err != nil {
+ panic(err)
+ }
+ sort.Slice(fetchedBlocks, func(i, j int) bool {
+ return fetchedBlocks[i].Height < fetchedBlocks[j].Height
+ })
+ assert.Len(s.T(), fetchedBlocks, int(totalBlocks))
+
+ for i := 0; i < len(blocks); i++ {
+ //get block by height
+ // fetch block through three ways, should always return identical result
+ fetchedBlockMeta, err := s.accessor.GetBlockByHeight(ctx, tag, blocks[i].Height)
+ if err != nil {
+ panic(err)
+ }
+ s.equalProto(blocks[i], fetchedBlockMeta)
+
+ fetchedBlockMeta, err = s.accessor.GetBlockByHash(ctx, tag, blocks[i].Height, blocks[i].Hash)
+ if err != nil {
+ panic(err)
+ }
+ s.equalProto(blocks[i], fetchedBlockMeta)
+
+ fetchedBlockMeta, err = s.accessor.GetBlockByHash(ctx, tag, blocks[i].Height, "")
+ if err != nil {
+ panic(err)
+ }
+ s.equalProto(blocks[i], fetchedBlockMeta)
+
+ s.equalProto(blocks[i], fetchedBlocks[i])
+ }
+
+ fetchedBlocksMeta, err := s.accessor.GetBlocksByHeights(ctx, tag, []uint64{startHeight + 1, startHeight + uint64(totalBlocks/2), startHeight, startHeight + uint64(totalBlocks) - 1})
+ if err != nil {
+ fmt.Println("Error fetching blocks by heights", err)
+ panic(err)
+ }
+ assert.Len(s.T(), fetchedBlocksMeta, 4)
+ s.equalProto(blocks[1], fetchedBlocksMeta[0])
+ s.equalProto(blocks[totalBlocks/2], fetchedBlocksMeta[1])
+ s.equalProto(blocks[0], fetchedBlocksMeta[2])
+ s.equalProto(blocks[totalBlocks-1], fetchedBlocksMeta[3])
+
+ fetchedBlockMeta, err := s.accessor.GetLatestBlock(ctx, tag)
+ if err != nil {
+ fmt.Println("Error fetching latest block", err)
+ panic(err)
+ }
+ s.equalProto(expectedLatestBlock, fetchedBlockMeta)
+
+}
+
+func (s *blockStorageTestSuite) TestPersistBlockMetasByInvalidChain() {
+ require := testutil.Require(s.T())
+ blocks := testutil.MakeBlockMetadatas(100, tag)
+ blocks[73].Hash = "0xdeadbeef"
+ err := s.accessor.PersistBlockMetas(context.Background(), true, blocks, nil)
+ require.Error(err)
+ require.True(xerrors.Is(err, parser.ErrInvalidChain))
+}
+
+func (s *blockStorageTestSuite) TestPersistBlockMetasByInvalidLastBlock() {
+ require := testutil.Require(s.T())
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(1_000_000, 100, tag)
+ lastBlock := testutil.MakeBlockMetadata(999_999, tag)
+ lastBlock.Hash = "0xdeadbeef"
+ err := s.accessor.PersistBlockMetas(context.Background(), true, blocks, lastBlock)
+ require.Error(err)
+ require.True(xerrors.Is(err, parser.ErrInvalidChain))
+}
+
+func (s *blockStorageTestSuite) TestPersistBlockMetasWithSkippedBlocks() {
+ require := testutil.Require(s.T())
+
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 100, tag)
+ // Mark 37th block as skipped and point the next block to the previous block.
+ blocks[37] = &api.BlockMetadata{
+ Tag: tag,
+ Height: startHeight + 37,
+ Skipped: true,
+ }
+ blocks[38].ParentHeight = blocks[36].Height
+ blocks[38].ParentHash = blocks[36].Hash
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ fetchedBlocks, err := s.accessor.GetBlocksByHeightRange(ctx, tag, startHeight, startHeight+100)
+ require.NoError(err)
+ require.Equal(blocks, fetchedBlocks)
+}
+
+func (s *blockStorageTestSuite) TestPersistBlockMetas() {
+ s.runTestPersistBlockMetas(10)
+}
+
+func (s *blockStorageTestSuite) TestPersistBlockMetasNotContinuous() {
+ blocks := testutil.MakeBlockMetadatas(10, tag)
+ blocks[2] = blocks[9]
+ err := s.accessor.PersistBlockMetas(context.TODO(), true, blocks[:9], nil)
+ assert.NotNil(s.T(), err)
+}
+
+func (s *blockStorageTestSuite) TestPersistBlockMetasDuplicatedHeights() {
+ blocks := testutil.MakeBlockMetadatas(10, tag)
+ blocks[9].Height = 2
+ err := s.accessor.PersistBlockMetas(context.TODO(), true, blocks, nil)
+ assert.NotNil(s.T(), err)
+}
+
+func (s *blockStorageTestSuite) TestGetBlocksNotExist() {
+ _, err := s.accessor.GetLatestBlock(context.TODO(), tag)
+ assert.True(s.T(), xerrors.Is(err, errors.ErrItemNotFound))
+}
+
+func (s *blockStorageTestSuite) TestGetBlockByHeightInvalidHeight() {
+ _, err := s.accessor.GetBlockByHeight(context.TODO(), tag, 0)
+ assert.True(s.T(), xerrors.Is(err, errors.ErrInvalidHeight))
+}
+
+func (s *blockStorageTestSuite) TestGetBlocksByHeightsInvalidHeight() {
+ _, err := s.accessor.GetBlocksByHeights(context.TODO(), tag, []uint64{0})
+ assert.True(s.T(), xerrors.Is(err, errors.ErrInvalidHeight))
+}
+
+func (s *blockStorageTestSuite) TestGetBlocksByHeightsBlockNotFound() {
+ _, err := s.accessor.GetBlocksByHeights(context.TODO(), tag, []uint64{15})
+ assert.True(s.T(), xerrors.Is(err, errors.ErrItemNotFound))
+}
+
+func (s *blockStorageTestSuite) TestGetBlockByHashInvalidHeight() {
+ _, err := s.accessor.GetBlockByHash(context.TODO(), tag, 0, "0x0")
+ assert.True(s.T(), xerrors.Is(err, errors.ErrInvalidHeight))
+}
+
+func (s *blockStorageTestSuite) TestGetBlockByHashQueryUsesPartialIndex() {
+ require := testutil.Require(s.T())
+
+ tx, err := s.db.BeginTx(context.Background(), nil)
+ require.NoError(err)
+ defer func() { _ = tx.Rollback() }()
+ _, err = tx.ExecContext(context.Background(), "SET LOCAL enable_seqscan = off")
+ require.NoError(err)
+
+ rows, err := tx.QueryContext(context.Background(), "EXPLAIN "+blockMetadataByHashQuery(), tag, s.config.Chain.BlockStartHeight, "0x0")
+ require.NoError(err)
+ defer rows.Close()
+
+ var lines []string
+ for rows.Next() {
+ var line string
+ require.NoError(rows.Scan(&line))
+ lines = append(lines, line)
+ }
+ require.NoError(rows.Err())
+
+ plan := strings.Join(lines, "\n")
+ assert.Contains(s.T(), plan, "unique_tag_hash_regular")
+ assert.Contains(s.T(), plan, "Index Cond: ((tag = 1) AND ((hash)::text = '0x0'::text))")
+ assert.NotContains(s.T(), plan, "Seq Scan")
+}
+
+func (s *blockStorageTestSuite) TestSingleBlockUploadGuardByHashQueryUsesPartialIndex() {
+ require := testutil.Require(s.T())
+
+ tx, err := s.db.BeginTx(context.Background(), nil)
+ require.NoError(err)
+ defer func() { _ = tx.Rollback() }()
+ _, err = tx.ExecContext(context.Background(), "SET LOCAL enable_seqscan = off")
+ require.NoError(err)
+
+ rows, err := tx.QueryContext(
+ context.Background(),
+ "EXPLAIN "+singleBlockUploadGuardByHashQuery(),
+ tag,
+ s.config.Chain.BlockStartHeight,
+ "0x0",
+ )
+ require.NoError(err)
+ defer rows.Close()
+
+ var lines []string
+ for rows.Next() {
+ var line string
+ require.NoError(rows.Scan(&line))
+ lines = append(lines, line)
+ }
+ require.NoError(rows.Err())
+
+ plan := strings.Join(lines, "\n")
+ assert.Contains(s.T(), plan, "unique_tag_hash_regular")
+ assert.Contains(s.T(), plan, "Index Cond: ((tag = 1) AND ((hash)::text = '0x0'::text))")
+ assert.NotContains(s.T(), plan, "Seq Scan")
+}
+
+func (s *blockStorageTestSuite) TestSingleBlockUploadGuardWithoutHashQueryUsesCanonicalIndexes() {
+ require := testutil.Require(s.T())
+
+ tx, err := s.db.BeginTx(context.Background(), nil)
+ require.NoError(err)
+ defer func() { _ = tx.Rollback() }()
+ _, err = tx.ExecContext(context.Background(), "SET LOCAL enable_seqscan = off")
+ require.NoError(err)
+
+ rows, err := tx.QueryContext(
+ context.Background(),
+ "EXPLAIN "+singleBlockUploadGuardWithoutHashQuery(),
+ tag,
+ s.config.Chain.BlockStartHeight,
+ )
+ require.NoError(err)
+ defer rows.Close()
+
+ var lines []string
+ for rows.Next() {
+ var line string
+ require.NoError(rows.Scan(&line))
+ lines = append(lines, line)
+ }
+ require.NoError(rows.Err())
+
+ plan := strings.Join(lines, "\n")
+ assert.Contains(s.T(), plan, "Index Scan using canonical_blocks_")
+ assert.Contains(s.T(), plan, "Index Cond: ((height = '10'::bigint) AND (tag = 1))")
+ assert.Contains(s.T(), plan, "block_metadata_pkey")
+ assert.Regexp(s.T(), `Index Cond: \(id = \$[0-9]+\)`, plan)
+ assert.NotContains(s.T(), plan, "Seq Scan")
+}
+
+func (s *blockStorageTestSuite) TestGetBlocksByHeightRangeInvalidRange() {
+ _, err := s.accessor.GetBlocksByHeightRange(context.TODO(), tag, 100, 100)
+ assert.True(s.T(), xerrors.Is(err, errors.ErrOutOfRange))
+
+ _, err = s.accessor.GetBlocksByHeightRange(context.TODO(), tag, 0, s.config.Chain.BlockStartHeight)
+ assert.True(s.T(), xerrors.Is(err, errors.ErrInvalidHeight))
+}
+
+func (s *blockStorageTestSuite) equalProto(x, y any) {
+ if diff := cmp.Diff(x, y, protocmp.Transform()); diff != "" {
+ assert.FailNow(s.T(), diff)
+ }
+}
+
+func (s *blockStorageTestSuite) getBlockMetadataID(ctx context.Context, block *api.BlockMetadata) int64 {
+ require := testutil.Require(s.T())
+ var blockMetadataID int64
+ err := s.db.QueryRowContext(
+ ctx,
+ `SELECT id FROM block_metadata WHERE tag = $1 AND height = $2 AND hash = $3 AND object_key_main = $4`,
+ block.GetTag(),
+ block.GetHeight(),
+ block.GetHash(),
+ block.GetObjectKeyMain(),
+ ).Scan(&blockMetadataID)
+ require.NoError(err)
+ return blockMetadataID
+}
+
+type consolidationShadowAudit struct {
+ SingleBlockObjectKey string
+ ConsolidatedObjectKey string
+ RetiredAt *time.Time
+ RetireAfter *time.Time
+}
+
+func (s *blockStorageTestSuite) getConsolidationShadowAudit(ctx context.Context, block *api.BlockMetadata) consolidationShadowAudit {
+ require := testutil.Require(s.T())
+ var singleBlockObjectKey, consolidatedObjectKey string
+ var retiredAt sql.NullTime
+ var retireAfter sql.NullTime
+ err := s.db.QueryRowContext(
+ ctx,
+ `SELECT shadow.single_block_object_key_main, shadow.consolidated_object_key_main,
+ shadow.single_block_retention_started_at, shadow.single_block_delete_after
+ FROM block_metadata bm
+ JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ WHERE bm.tag = $1 AND bm.height = $2 AND bm.hash = $3`,
+ block.GetTag(),
+ block.GetHeight(),
+ block.GetHash(),
+ ).Scan(&singleBlockObjectKey, &consolidatedObjectKey, &retiredAt, &retireAfter)
+ require.NoError(err)
+ audit := consolidationShadowAudit{
+ SingleBlockObjectKey: singleBlockObjectKey,
+ ConsolidatedObjectKey: consolidatedObjectKey,
+ }
+ if retiredAt.Valid {
+ value := retiredAt.Time
+ audit.RetiredAt = &value
+ }
+ if retireAfter.Valid {
+ value := retireAfter.Time
+ audit.RetireAfter = &value
+ }
+ return audit
+}
+
+func (s *blockStorageTestSuite) insertConsolidationShadow(
+ ctx context.Context,
+ block *api.BlockMetadata,
+ consolidatedObjectKey string,
+ byteOffset uint64,
+ byteLength uint64,
+ uncompressedLength uint64,
+ validated bool,
+ singleBlockObjectKeyMain string,
+) {
+ if singleBlockObjectKeyMain == "" {
+ singleBlockObjectKeyMain = block.GetObjectKeyMain()
+ }
+ s.insertConsolidationShadowWithIdentity(
+ ctx,
+ block,
+ consolidatedObjectKey,
+ byteOffset,
+ byteLength,
+ uncompressedLength,
+ validated,
+ singleBlockObjectKeyMain,
+ block.GetTag(),
+ block.GetHeight(),
+ block.GetHash(),
+ )
+}
+
+func (s *blockStorageTestSuite) insertConsolidationShadowWithIdentity(
+ ctx context.Context,
+ block *api.BlockMetadata,
+ consolidatedObjectKey string,
+ byteOffset uint64,
+ byteLength uint64,
+ uncompressedLength uint64,
+ validated bool,
+ singleBlockObjectKeyMain string,
+ shadowTag uint32,
+ shadowHeight uint64,
+ shadowHash string,
+) {
+ require := testutil.Require(s.T())
+ var validatedAt any
+ if validated {
+ validatedAt = time.Now().UTC()
+ }
+ _, err := s.db.ExecContext(
+ ctx,
+ `INSERT INTO block_consolidation_shadow (
+ block_metadata_id, tag, height, hash, single_block_object_key_main, consolidated_object_key_main,
+ object_format, byte_offset, byte_length, uncompressed_length, validated_at
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
+ s.getBlockMetadataID(ctx, block),
+ shadowTag,
+ shadowHeight,
+ shadowHash,
+ singleBlockObjectKeyMain,
+ consolidatedObjectKey,
+ int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH),
+ byteOffset,
+ byteLength,
+ uncompressedLength,
+ validatedAt,
+ )
+ require.NoError(err)
+}
+
+func (s *blockStorageTestSuite) insertInvalidConsolidationShadow(
+ ctx context.Context,
+ block *api.BlockMetadata,
+ consolidatedObjectKey string,
+ objectFormat api.BlockObjectFormat,
+ byteOffset uint64,
+ byteLength uint64,
+ uncompressedLength uint64,
+) {
+ require := testutil.Require(s.T())
+ _, err := s.db.ExecContext(
+ ctx,
+ `INSERT INTO block_consolidation_shadow (
+ block_metadata_id, tag, height, hash, single_block_object_key_main, consolidated_object_key_main,
+ object_format, byte_offset, byte_length, uncompressed_length, validated_at
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
+ s.getBlockMetadataID(ctx, block),
+ block.GetTag(),
+ block.GetHeight(),
+ block.GetHash(),
+ block.GetObjectKeyMain(),
+ consolidatedObjectKey,
+ int32(objectFormat),
+ byteOffset,
+ byteLength,
+ uncompressedLength,
+ time.Now().UTC(),
+ )
+ require.NoError(err)
+}
+
+func expectedConsolidationShadow(
+ block *api.BlockMetadata,
+ consolidatedObjectKey string,
+ byteOffset uint64,
+ byteLength uint64,
+ uncompressedLength uint64,
+) *api.BlockMetadata {
+ shadow := proto.Clone(block).(*api.BlockMetadata)
+ shadow.ObjectKeyMain = consolidatedObjectKey
+ shadow.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH
+ shadow.ByteOffset = byteOffset
+ shadow.ByteLength = byteLength
+ shadow.UncompressedLength = uncompressedLength
+ return shadow
+}
+
+func (s *blockStorageTestSuite) TestGetConsolidationShadowPredicates() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 4, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ s.insertConsolidationShadow(ctx, blocks[0], "consolidated/validated.cscb.zstd", 10, 20, 20, true, "")
+ s.insertConsolidationShadow(ctx, blocks[1], "consolidated/unvalidated.cscb.zstd", 30, 40, 40, false, "")
+ s.insertConsolidationShadow(ctx, blocks[2], "consolidated/wrong-single-block-key.cscb.zstd", 50, 60, 60, true, "single-block/key/does/not/match")
+
+ expected := expectedConsolidationShadow(blocks[0], "consolidated/validated.cscb.zstd", 10, 20, 20)
+ actual, err := s.accessor.GetBlockConsolidationShadow(ctx, blocks[0])
+ require.NoError(err)
+ s.equalProto(expected, actual)
+
+ _, err = s.accessor.GetBlockConsolidationShadow(ctx, blocks[1])
+ require.Error(err)
+ require.True(xerrors.Is(err, errors.ErrItemNotFound))
+
+ _, err = s.accessor.GetBlockConsolidationShadow(ctx, blocks[2])
+ require.Error(err)
+ require.True(xerrors.Is(err, errors.ErrItemNotFound))
+
+ _, err = s.accessor.GetBlockConsolidationShadow(ctx, blocks[3])
+ require.Error(err)
+ require.True(xerrors.Is(err, errors.ErrItemNotFound))
+
+ skipped := &api.BlockMetadata{Tag: tag, Height: startHeight + 10, Skipped: true}
+ _, err = s.accessor.GetBlockConsolidationShadow(ctx, skipped)
+ require.Error(err)
+ require.True(xerrors.Is(err, errors.ErrItemNotFound))
+}
+
+func (s *blockStorageTestSuite) TestGetBlocksConsolidationShadowPreservesOrderAndMisses() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 3, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ s.insertConsolidationShadow(ctx, blocks[0], "consolidated/first.cscb.zstd", 100, 200, 200, true, "")
+ s.insertConsolidationShadow(ctx, blocks[2], "consolidated/third.cscb.zstd", 300, 400, 400, true, "")
+ skipped := &api.BlockMetadata{Tag: tag, Height: startHeight + 99, Skipped: true}
+
+ actual, err := s.accessor.GetBlocksConsolidationShadow(ctx, []*api.BlockMetadata{blocks[2], blocks[1], skipped, blocks[0]})
+ require.NoError(err)
+ require.Len(actual, 4)
+ s.equalProto(expectedConsolidationShadow(blocks[2], "consolidated/third.cscb.zstd", 300, 400, 400), actual[0])
+ require.Nil(actual[1])
+ require.Nil(actual[2])
+ s.equalProto(expectedConsolidationShadow(blocks[0], "consolidated/first.cscb.zstd", 100, 200, 200), actual[3])
+}
+
+func (s *blockStorageTestSuite) TestGetBlocksMissingConsolidationShadowFiltersAndLimits() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 5, tag)
+ blocks[2].ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH
+ blocks[2].ByteOffset = 10
+ blocks[2].ByteLength = 20
+ blocks[2].UncompressedLength = 20
+ blocks[4] = &api.BlockMetadata{
+ Tag: tag,
+ Height: startHeight + 4,
+ Skipped: true,
+ }
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ s.insertConsolidationShadow(ctx, blocks[0], "consolidated/validated.cscb.zstd", 10, 20, 20, true, "")
+ s.insertConsolidationShadow(ctx, blocks[1], "consolidated/unvalidated.cscb.zstd", 30, 40, 40, false, "")
+
+ actual, err := s.accessor.GetBlocksMissingConsolidationShadow(ctx, tag, startHeight, startHeight+5, 2)
+ require.NoError(err)
+ require.Len(actual, 2)
+ require.Equal(s.getBlockMetadataID(ctx, blocks[1]), actual[0].ID)
+ s.equalProto(blocks[1], actual[0].Metadata)
+ require.Equal(s.getBlockMetadataID(ctx, blocks[3]), actual[1].ID)
+ s.equalProto(blocks[3], actual[1].Metadata)
+ require.False(actual[0].Metadata.GetSkipped())
+ require.False(actual[1].Metadata.GetSkipped())
+
+ height, found, err := s.accessor.GetFirstBlockMissingConsolidationShadow(ctx, tag, startHeight, startHeight+5)
+ require.NoError(err)
+ require.True(found)
+ require.Equal(blocks[1].GetHeight(), height)
+
+ height, found, err = s.accessor.GetFirstBlockMissingConsolidationShadow(ctx, tag, blocks[2].GetHeight(), startHeight+5)
+ require.NoError(err)
+ require.True(found)
+ require.Equal(blocks[3].GetHeight(), height)
+
+ height, found, err = s.accessor.GetFirstBlockMissingConsolidationShadow(ctx, tag, startHeight, blocks[1].GetHeight())
+ require.NoError(err)
+ require.False(found)
+ require.Zero(height)
+}
+
+func (s *blockStorageTestSuite) TestGetBlockConsolidationShadowStats() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 8, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ s.insertConsolidationShadow(ctx, blocks[0], "consolidated/first.cscb.zstd", 10, 20, 20, true, "")
+ s.insertConsolidationShadow(ctx, blocks[1], "consolidated/first.cscb.zstd", 30, 40, 40, true, "")
+ s.insertConsolidationShadow(ctx, blocks[2], "consolidated/second.cscb.zstd", 50, 60, 60, true, "")
+ s.insertConsolidationShadow(ctx, blocks[3], "consolidated/unvalidated.cscb.zstd", 70, 80, 80, false, "")
+ s.insertConsolidationShadow(ctx, blocks[4], "consolidated/wrong-single-block-key.cscb.zstd", 90, 100, 100, true, "single-block/key/does/not/match")
+ s.insertConsolidationShadowWithIdentity(ctx, blocks[5], "consolidated/wrong-tag.cscb.zstd", 110, 120, 120, true, blocks[5].GetObjectKeyMain(), tag+1, blocks[5].GetHeight(), blocks[5].GetHash())
+ s.insertConsolidationShadowWithIdentity(ctx, blocks[6], "consolidated/wrong-height.cscb.zstd", 130, 140, 140, true, blocks[6].GetObjectKeyMain(), tag, blocks[6].GetHeight()+1000, blocks[6].GetHash())
+ s.insertConsolidationShadowWithIdentity(ctx, blocks[7], "consolidated/wrong-hash.cscb.zstd", 150, 160, 160, true, blocks[7].GetObjectKeyMain(), tag, blocks[7].GetHeight(), "wrong-hash")
+
+ stats, err := s.accessor.GetBlockConsolidationShadowStats(ctx, tag, startHeight, startHeight+8)
+ require.NoError(err)
+ require.Equal(uint64(2), stats.Objects)
+ require.Equal(uint64(3), stats.Blocks)
+ require.Equal(uint64(8), stats.EligibleBlocks)
+
+ stats, err = s.accessor.GetBlockConsolidationShadowStats(ctx, tag, startHeight+1, startHeight+3)
+ require.NoError(err)
+ require.Equal(uint64(2), stats.Objects)
+ require.Equal(uint64(2), stats.Blocks)
+ require.Equal(uint64(2), stats.EligibleBlocks)
+}
+
+func (s *blockStorageTestSuite) TestGetBlockConsolidationShadowStatsExcludesSkippedBlocks() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 3, tag)
+ blocks[1] = &api.BlockMetadata{
+ Tag: tag,
+ Height: startHeight + 1,
+ Skipped: true,
+ }
+ blocks[2].ParentHeight = blocks[0].Height
+ blocks[2].ParentHash = blocks[0].Hash
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ s.insertConsolidationShadow(ctx, blocks[0], "consolidated/first.cscb.zstd", 10, 20, 20, true, "")
+ s.insertConsolidationShadow(ctx, blocks[1], "consolidated/skipped.cscb.zstd", 30, 40, 40, true, "")
+ s.insertConsolidationShadow(ctx, blocks[2], "consolidated/second.cscb.zstd", 50, 60, 60, true, "")
+
+ stats, err := s.accessor.GetBlockConsolidationShadowStats(ctx, tag, startHeight, startHeight+3)
+ require.NoError(err)
+ require.Equal(uint64(2), stats.Objects)
+ require.Equal(uint64(2), stats.Blocks)
+ require.Equal(uint64(2), stats.EligibleBlocks)
+}
+
+func (s *blockStorageTestSuite) TestGetBlockConsolidationShadowStatsRequiresPromotionValidShadows() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 5, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ s.insertConsolidationShadow(ctx, blocks[0], "consolidated/valid.cscb.zstd", 10, 20, 20, true, "")
+ s.insertInvalidConsolidationShadow(
+ ctx,
+ blocks[1],
+ "consolidated/wrong-format.cscb.zstd",
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK,
+ 30,
+ 40,
+ 40,
+ )
+ s.insertInvalidConsolidationShadow(
+ ctx,
+ blocks[2],
+ "consolidated/zero-byte-length.cscb.zstd",
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 50,
+ 0,
+ 60,
+ )
+ s.insertInvalidConsolidationShadow(
+ ctx,
+ blocks[3],
+ "consolidated/zero-uncompressed-length.cscb.zstd",
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 70,
+ 80,
+ 0,
+ )
+ s.insertConsolidationShadow(ctx, blocks[4], "consolidated/promoted.cscb.zstd", 90, 100, 100, true, "")
+ result, err := s.accessor.PromoteBlockConsolidationShadows(ctx, tag, blocks[4].GetHeight(), blocks[4].GetHeight()+1, 10, config.DefaultSingleBlockObjectRetention)
+ require.NoError(err)
+ require.Equal(uint64(1), result.Blocks)
+
+ stats, err := s.accessor.GetBlockConsolidationShadowStats(ctx, tag, startHeight, startHeight+5)
+ require.NoError(err)
+ require.Equal(uint64(2), stats.Objects)
+ require.Equal(uint64(2), stats.Blocks)
+ require.Equal(uint64(5), stats.EligibleBlocks)
+}
+
+func (s *blockStorageTestSuite) TestPersistBlockConsolidationShadowsGuardsPrimaryIdentity() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 2, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ err = s.accessor.PersistBlockConsolidationShadows(ctx, []*internal.ConsolidationShadowPlacement{
+ {
+ BlockMetadataID: s.getBlockMetadataID(ctx, blocks[0]),
+ Tag: blocks[0].GetTag(),
+ Height: blocks[0].GetHeight(),
+ Hash: blocks[0].GetHash(),
+ SingleBlockObjectKeyMain: blocks[0].GetObjectKeyMain(),
+ ConsolidatedObjectKeyMain: "consolidated/first.cscb.zstd",
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 100,
+ ByteLength: 200,
+ UncompressedLength: 200,
+ },
+ })
+ require.NoError(err)
+
+ shadow, err := s.accessor.GetBlockConsolidationShadow(ctx, blocks[0])
+ require.NoError(err)
+ s.equalProto(expectedConsolidationShadow(blocks[0], "consolidated/first.cscb.zstd", 100, 200, 200), shadow)
+
+ primary, err := s.accessor.GetBlockByHeight(ctx, blocks[0].GetTag(), blocks[0].GetHeight())
+ require.NoError(err)
+ s.equalProto(blocks[0], primary)
+
+ err = s.accessor.PersistBlockConsolidationShadows(ctx, []*internal.ConsolidationShadowPlacement{
+ {
+ BlockMetadataID: s.getBlockMetadataID(ctx, blocks[1]),
+ Tag: blocks[1].GetTag(),
+ Height: blocks[1].GetHeight(),
+ Hash: blocks[1].GetHash(),
+ SingleBlockObjectKeyMain: "single-block/key/does/not/match",
+ ConsolidatedObjectKeyMain: "consolidated/wrong.cscb.zstd",
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 300,
+ ByteLength: 400,
+ UncompressedLength: 400,
+ },
+ })
+ require.Error(err)
+
+ _, err = s.accessor.GetBlockConsolidationShadow(ctx, blocks[1])
+ require.Error(err)
+ require.True(xerrors.Is(err, errors.ErrItemNotFound))
+}
+
+func (s *blockStorageTestSuite) TestPromoteBlockConsolidationShadowsPromotesValidatedShadows() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 3, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ s.insertConsolidationShadow(ctx, blocks[0], "consolidated/first.cscb.zstd", 10, 20, 20, true, "")
+ s.insertConsolidationShadow(ctx, blocks[1], "consolidated/second.cscb.zstd", 30, 40, 40, true, "")
+
+ beforePromotion := time.Now().UTC()
+ result, err := s.accessor.PromoteBlockConsolidationShadows(ctx, tag, startHeight, startHeight+3, 10, config.DefaultSingleBlockObjectRetention)
+ require.NoError(err)
+ require.Equal(uint64(2), result.Blocks)
+
+ primary, err := s.accessor.GetBlockByHeight(ctx, tag, blocks[0].GetHeight())
+ require.NoError(err)
+ s.equalProto(expectedConsolidationShadow(blocks[0], "consolidated/first.cscb.zstd", 10, 20, 20), primary)
+
+ primary, err = s.accessor.GetBlockByHeight(ctx, tag, blocks[1].GetHeight())
+ require.NoError(err)
+ s.equalProto(expectedConsolidationShadow(blocks[1], "consolidated/second.cscb.zstd", 30, 40, 40), primary)
+
+ primary, err = s.accessor.GetBlockByHeight(ctx, tag, blocks[2].GetHeight())
+ require.NoError(err)
+ s.equalProto(blocks[2], primary)
+
+ audit := s.getConsolidationShadowAudit(ctx, blocks[0])
+ require.Equal(blocks[0].GetObjectKeyMain(), audit.SingleBlockObjectKey)
+ require.Equal("consolidated/first.cscb.zstd", audit.ConsolidatedObjectKey)
+ require.NotNil(audit.RetiredAt)
+ require.NotNil(audit.RetireAfter)
+ require.WithinDuration(beforePromotion, *audit.RetiredAt, time.Minute)
+ require.WithinDuration(audit.RetiredAt.Add(config.DefaultSingleBlockObjectRetention), *audit.RetireAfter, time.Minute)
+
+ stats, err := s.accessor.GetBlockConsolidationShadowStats(ctx, tag, startHeight, startHeight+3)
+ require.NoError(err)
+ require.Equal(uint64(2), stats.Objects)
+ require.Equal(uint64(2), stats.Blocks)
+ require.Equal(uint64(3), stats.EligibleBlocks)
+}
+
+func (s *blockStorageTestSuite) TestGetFirstPromotableBlockConsolidationShadowFiltersCandidates() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 6, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ s.insertInvalidConsolidationShadow(
+ ctx,
+ blocks[0],
+ "consolidated/invalid.cscb.zstd",
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 10,
+ 0,
+ 20,
+ )
+ s.insertConsolidationShadow(ctx, blocks[1], "consolidated/skipped.cscb.zstd", 10, 20, 20, true, "")
+ _, err = s.db.ExecContext(
+ ctx,
+ `UPDATE block_metadata SET skipped = true WHERE tag = $1 AND height = $2 AND hash = $3`,
+ blocks[1].GetTag(),
+ blocks[1].GetHeight(),
+ blocks[1].GetHash(),
+ )
+ require.NoError(err)
+ s.insertConsolidationShadow(ctx, blocks[2], "consolidated/unvalidated.cscb.zstd", 10, 20, 20, false, "")
+ s.insertConsolidationShadow(ctx, blocks[3], "consolidated/promoted.cscb.zstd", 10, 20, 20, true, "")
+ result, err := s.accessor.PromoteBlockConsolidationShadows(ctx, tag, blocks[3].GetHeight(), blocks[3].GetHeight()+1, 10, config.DefaultSingleBlockObjectRetention)
+ require.NoError(err)
+ require.Equal(uint64(1), result.Blocks)
+ s.insertConsolidationShadow(ctx, blocks[4], "consolidated/first-promotable.cscb.zstd", 10, 20, 20, true, "")
+
+ height, found, err := s.accessor.GetFirstPromotableBlockConsolidationShadow(ctx, tag, startHeight, startHeight+uint64(len(blocks)))
+ require.NoError(err)
+ require.True(found)
+ require.Equal(blocks[4].GetHeight(), height)
+
+ height, found, err = s.accessor.GetFirstPromotableBlockConsolidationShadow(ctx, tag, startHeight, blocks[4].GetHeight())
+ require.NoError(err)
+ require.False(found)
+ require.Zero(height)
+
+ height, found, err = s.accessor.GetFirstPromotableBlockConsolidationShadow(ctx, tag, blocks[4].GetHeight()+1, startHeight+uint64(len(blocks)))
+ require.NoError(err)
+ require.False(found)
+ require.Zero(height)
+}
+
+func (s *blockStorageTestSuite) TestPromoteBlockConsolidationShadowsMissingShadowNoOps() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 1, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ result, err := s.accessor.PromoteBlockConsolidationShadows(ctx, tag, startHeight, startHeight+1, 10, config.DefaultSingleBlockObjectRetention)
+ require.NoError(err)
+ require.Equal(uint64(0), result.Blocks)
+
+ primary, err := s.accessor.GetBlockByHeight(ctx, tag, blocks[0].GetHeight())
+ require.NoError(err)
+ s.equalProto(blocks[0], primary)
+}
+
+func (s *blockStorageTestSuite) TestPromoteBlockConsolidationShadowsRejectsInvalidShadowMetadata() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 1, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+ s.insertInvalidConsolidationShadow(
+ ctx,
+ blocks[0],
+ "consolidated/invalid.cscb.zstd",
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK,
+ 10,
+ 20,
+ 20,
+ )
+
+ result, err := s.accessor.PromoteBlockConsolidationShadows(ctx, tag, startHeight, startHeight+1, 10, config.DefaultSingleBlockObjectRetention)
+ require.Error(err)
+ require.Nil(result)
+ require.Contains(err.Error(), "invalid consolidation shadow metadata")
+
+ primary, err := s.accessor.GetBlockByHeight(ctx, tag, blocks[0].GetHeight())
+ require.NoError(err)
+ s.equalProto(blocks[0], primary)
+}
+
+func (s *blockStorageTestSuite) TestPromoteBlockConsolidationShadowsSkipsStaleReorgMetadata() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 4, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+ s.insertConsolidationShadow(ctx, blocks[3], "consolidated/stale.cscb.zstd", 10, 20, 20, true, "")
+
+ reorgBlock := proto.Clone(blocks[3]).(*api.BlockMetadata)
+ reorgBlock.Hash = "0xreorg"
+ reorgBlock.ParentHash = blocks[2].GetHash()
+ reorgBlock.ObjectKeyMain = "single-block/reorg.gzip"
+ err = s.accessor.PersistBlockMetas(ctx, true, []*api.BlockMetadata{reorgBlock}, blocks[2])
+ require.NoError(err)
+
+ result, err := s.accessor.PromoteBlockConsolidationShadows(ctx, tag, reorgBlock.GetHeight(), reorgBlock.GetHeight()+1, 10, config.DefaultSingleBlockObjectRetention)
+ require.NoError(err)
+ require.Equal(uint64(0), result.Blocks)
+
+ primary, err := s.accessor.GetBlockByHeight(ctx, tag, reorgBlock.GetHeight())
+ require.NoError(err)
+ s.equalProto(reorgBlock, primary)
+}
+
+func (s *blockStorageTestSuite) TestPromoteBlockConsolidationShadowsIdempotentRetry() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 1, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+ s.insertConsolidationShadow(ctx, blocks[0], "consolidated/first.cscb.zstd", 10, 20, 20, true, "")
+
+ result, err := s.accessor.PromoteBlockConsolidationShadows(ctx, tag, startHeight, startHeight+1, 10, config.DefaultSingleBlockObjectRetention)
+ require.NoError(err)
+ require.Equal(uint64(1), result.Blocks)
+
+ result, err = s.accessor.PromoteBlockConsolidationShadows(ctx, tag, startHeight, startHeight+1, 10, config.DefaultSingleBlockObjectRetention)
+ require.NoError(err)
+ require.Equal(uint64(0), result.Blocks)
+
+ primary, err := s.accessor.GetBlockByHeight(ctx, tag, blocks[0].GetHeight())
+ require.NoError(err)
+ s.equalProto(expectedConsolidationShadow(blocks[0], "consolidated/first.cscb.zstd", 10, 20, 20), primary)
+}
+
+func (s *blockStorageTestSuite) TestPromoteBlockConsolidationShadowsRollsBackOnInvalidCandidate() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 2, tag)
+
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+ s.insertConsolidationShadow(ctx, blocks[0], "consolidated/valid.cscb.zstd", 10, 20, 20, true, "")
+ s.insertInvalidConsolidationShadow(
+ ctx,
+ blocks[1],
+ "consolidated/invalid.cscb.zstd",
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 30,
+ 0,
+ 40,
+ )
+
+ _, err = s.accessor.PromoteBlockConsolidationShadows(ctx, tag, startHeight, startHeight+2, 10, config.DefaultSingleBlockObjectRetention)
+ require.Error(err)
+ require.Contains(err.Error(), "invalid consolidation shadow metadata")
+
+ primary, err := s.accessor.GetBlockByHeight(ctx, tag, blocks[0].GetHeight())
+ require.NoError(err)
+ s.equalProto(blocks[0], primary)
+
+ primary, err = s.accessor.GetBlockByHeight(ctx, tag, blocks[1].GetHeight())
+ require.NoError(err)
+ s.equalProto(blocks[1], primary)
+}
+
+func (s *blockStorageTestSuite) TestWatermarkVisibilityControl() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+
+ // Create blocks
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 10, tag)
+
+ // Persist blocks WITHOUT watermark (updateWatermark=false)
+ err := s.accessor.PersistBlockMetas(ctx, false, blocks, nil)
+ require.NoError(err)
+
+ // GetLatestBlock should return ErrItemNotFound because no blocks are watermarked
+ _, err = s.accessor.GetLatestBlock(ctx, tag)
+ require.Error(err)
+ require.True(xerrors.Is(err, errors.ErrItemNotFound), "Expected ErrItemNotFound when no watermark exists")
+
+ // Now persist the same blocks WITH watermark (updateWatermark=true)
+ err = s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ // GetLatestBlock should now return the highest block
+ latestBlock, err := s.accessor.GetLatestBlock(ctx, tag)
+ require.NoError(err)
+ require.NotNil(latestBlock)
+ require.Equal(blocks[9].Height, latestBlock.Height)
+ require.Equal(blocks[9].Hash, latestBlock.Hash)
+
+ // Add more blocks with watermark
+ moreBlocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight+10, 5, tag)
+ err = s.accessor.PersistBlockMetas(ctx, true, moreBlocks, nil)
+ require.NoError(err)
+
+ // GetLatestBlock should return the new highest watermarked block
+ latestBlock, err = s.accessor.GetLatestBlock(ctx, tag)
+ require.NoError(err)
+ require.Equal(moreBlocks[4].Height, latestBlock.Height)
+ require.Equal(moreBlocks[4].Hash, latestBlock.Hash)
+}
+
+func (s *blockStorageTestSuite) TestWatermarkWithReorg() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+
+ // Create initial chain
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 10, tag)
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks, nil)
+ require.NoError(err)
+
+ // Verify latest block
+ latestBlock, err := s.accessor.GetLatestBlock(ctx, tag)
+ require.NoError(err)
+ require.Equal(blocks[9].Height, latestBlock.Height)
+
+ // Simulate reorg: create alternative chain from height startHeight+7
+ // This represents the reorg scenario where we need to replace blocks
+ reorgBlocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight+7, 3, tag)
+ // Change hashes to simulate different blocks
+ for i := range reorgBlocks {
+ reorgBlocks[i].Hash = fmt.Sprintf("0xreorg%d", i)
+ if i > 0 {
+ reorgBlocks[i].ParentHash = reorgBlocks[i-1].Hash
+ } else {
+ reorgBlocks[i].ParentHash = blocks[6].Hash // Link to pre-reorg chain
+ }
+ }
+
+ // Persist reorg blocks with watermark
+ err = s.accessor.PersistBlockMetas(ctx, true, reorgBlocks, blocks[6])
+ require.NoError(err)
+
+ // GetLatestBlock should return the new reorg tip
+ latestBlock, err = s.accessor.GetLatestBlock(ctx, tag)
+ require.NoError(err)
+ require.Equal(reorgBlocks[2].Height, latestBlock.Height)
+ require.Equal(reorgBlocks[2].Hash, latestBlock.Hash)
+}
+
+func (s *blockStorageTestSuite) TestWatermarkMultipleTags() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+
+ tag1 := uint32(1)
+ tag2 := uint32(2)
+
+ // Create blocks for tag1
+ blocks1 := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 5, tag1)
+ err := s.accessor.PersistBlockMetas(ctx, true, blocks1, nil)
+ require.NoError(err)
+
+ // Create blocks for tag2
+ blocks2 := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 10, tag2)
+ err = s.accessor.PersistBlockMetas(ctx, true, blocks2, nil)
+ require.NoError(err)
+
+ // Verify each tag has its own latest block
+ latestBlock1, err := s.accessor.GetLatestBlock(ctx, tag1)
+ require.NoError(err)
+ require.Equal(blocks1[4].Height, latestBlock1.Height)
+
+ latestBlock2, err := s.accessor.GetLatestBlock(ctx, tag2)
+ require.NoError(err)
+ require.Equal(blocks2[9].Height, latestBlock2.Height)
+}
+
+func (s *blockStorageTestSuite) TestGetBlocksByHeightRangeStillWorks() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+ startHeight := s.config.Chain.BlockStartHeight
+
+ // Create blocks without watermark
+ blocks := testutil.MakeBlockMetadatasFromStartHeight(startHeight, 10, tag)
+ err := s.accessor.PersistBlockMetas(ctx, false, blocks, nil)
+ require.NoError(err)
+
+ // GetBlocksByHeightRange should still work even without watermark
+ // This is important for defense-in-depth validation
+ fetchedBlocks, err := s.accessor.GetBlocksByHeightRange(ctx, tag, startHeight, startHeight+10)
+ require.NoError(err)
+ require.Len(fetchedBlocks, 10)
+
+ // Verify the blocks are correct
+ sort.Slice(fetchedBlocks, func(i, j int) bool {
+ return fetchedBlocks[i].Height < fetchedBlocks[j].Height
+ })
+ for i := 0; i < 10; i++ {
+ s.equalProto(blocks[i], fetchedBlocks[i])
+ }
+}
+
+func TestIntegrationBlockStorageTestSuite(t *testing.T) {
+ require := testutil.Require(t)
+ cfg, err := config.New()
+ require.NoError(err)
+ suite.Run(t, &blockStorageTestSuite{config: cfg})
+}
diff --git a/internal/storage/metastorage/postgres/block_storage_test.go b/internal/storage/metastorage/postgres/block_storage_test.go
new file mode 100644
index 00000000..5726dd71
--- /dev/null
+++ b/internal/storage/metastorage/postgres/block_storage_test.go
@@ -0,0 +1,37 @@
+package postgres
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestBlockObjectByteFields_SingleBlockUsesNullSentinel(t *testing.T) {
+ block := testutil.MakeBlockMetadata(100, tag)
+
+ byteOffset, byteLength, uncompressedLength := blockObjectByteFields(block)
+
+ require.False(t, byteOffset.Valid)
+ require.False(t, byteLength.Valid)
+ require.False(t, uncompressedLength.Valid)
+}
+
+func TestBlockObjectByteFields_ConsolidatedFields(t *testing.T) {
+ block := testutil.MakeBlockMetadata(100, tag)
+ block.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH
+ block.ByteOffset = 4096
+ block.ByteLength = 8192
+ block.UncompressedLength = 8192
+
+ byteOffset, byteLength, uncompressedLength := blockObjectByteFields(block)
+
+ require.True(t, byteOffset.Valid)
+ require.Equal(t, int64(4096), byteOffset.Int64)
+ require.True(t, byteLength.Valid)
+ require.Equal(t, int64(8192), byteLength.Int64)
+ require.True(t, uncompressedLength.Valid)
+ require.Equal(t, int64(8192), uncompressedLength.Int64)
+}
diff --git a/internal/storage/metastorage/postgres/connection.go b/internal/storage/metastorage/postgres/connection.go
new file mode 100644
index 00000000..13d6796f
--- /dev/null
+++ b/internal/storage/metastorage/postgres/connection.go
@@ -0,0 +1,61 @@
+package postgres
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+
+ _ "github.com/lib/pq"
+ "go.uber.org/zap"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/utils/log"
+)
+
+func newDBConnection(ctx context.Context, cfg *config.PostgresConfig) (*sql.DB, error) {
+ logger := log.WithPackage(log.NewDevelopment())
+
+ // Build PostgreSQL connection string with timeout
+ dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
+ cfg.Host, cfg.Port, cfg.Database, cfg.User, cfg.Password, cfg.SSLMode)
+
+ // Add connect_timeout if specified
+ if cfg.ConnectTimeout > 0 {
+ dsn += fmt.Sprintf(" connect_timeout=%d", int(cfg.ConnectTimeout.Seconds()))
+ }
+ if cfg.StatementTimeout > 0 {
+ dsn += fmt.Sprintf(" statement_timeout=%d", cfg.StatementTimeout.Milliseconds())
+ }
+
+ // Debug output for CI troubleshooting
+ logger.Debug("Connecting to PostgreSQL",
+ zap.String("host", cfg.Host),
+ zap.Int("port", cfg.Port),
+ zap.String("database", cfg.Database),
+ zap.String("ssl_mode", cfg.SSLMode))
+
+ // Open database connection
+ db, err := sql.Open("postgres", dsn)
+ if err != nil {
+ logger.Error("Failed to open connection", zap.Error(err))
+ return nil, err
+ }
+
+ if pingErr := db.PingContext(ctx); pingErr != nil {
+ logger.Error("Failed to ping database", zap.Error(pingErr))
+ return nil, pingErr
+ }
+
+ logger.Debug("Successfully connected to PostgreSQL")
+
+ // Configure connection pool and timeouts
+ db.SetMaxOpenConns(cfg.MaxConnections)
+ db.SetMaxIdleConns(cfg.MinConnections)
+ db.SetConnMaxLifetime(cfg.MaxLifetime)
+ db.SetConnMaxIdleTime(cfg.MaxIdleTime)
+
+ // Do not run schema migrations from runtime service connections.
+ // Server pods may connect through read replicas, and worker users may not own
+ // existing tables. Run migrations explicitly through admin db-init/db-migrate.
+ return db, nil
+}
diff --git a/internal/storage/metastorage/postgres/connection_integration_test.go b/internal/storage/metastorage/postgres/connection_integration_test.go
new file mode 100644
index 00000000..79f0db0f
--- /dev/null
+++ b/internal/storage/metastorage/postgres/connection_integration_test.go
@@ -0,0 +1,42 @@
+package postgres
+
+import (
+ "context"
+ "database/sql"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/coinbase/chainstorage/internal/config"
+)
+
+func TestIntegrationNewDBConnectionAppliesStatementTimeoutToEveryConnection(t *testing.T) {
+ cfg, err := config.New()
+ require.NoError(t, err)
+ if !cfg.IsIntegrationTest() || cfg.AWS.Postgres == nil {
+ t.Skip("Postgres is not configured for integration tests")
+ }
+
+ postgresCfg := *cfg.AWS.Postgres
+ postgresCfg.MaxConnections = 2
+ postgresCfg.MinConnections = 0
+ postgresCfg.StatementTimeout = 1234 * time.Millisecond
+
+ db, err := newDBConnection(context.Background(), &postgresCfg)
+ require.NoError(t, err)
+ defer db.Close()
+
+ first, err := db.Conn(context.Background())
+ require.NoError(t, err)
+ defer first.Close()
+ second, err := db.Conn(context.Background())
+ require.NoError(t, err)
+ defer second.Close()
+
+ for _, conn := range []*sql.Conn{first, second} {
+ var timeout string
+ require.NoError(t, conn.QueryRowContext(context.Background(), "SHOW statement_timeout").Scan(&timeout))
+ require.Equal(t, "1234ms", timeout)
+ }
+}
diff --git a/internal/storage/metastorage/postgres/connection_pool.go b/internal/storage/metastorage/postgres/connection_pool.go
new file mode 100644
index 00000000..af61b0e0
--- /dev/null
+++ b/internal/storage/metastorage/postgres/connection_pool.go
@@ -0,0 +1,185 @@
+package postgres
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "sync"
+
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/utils/log"
+)
+
+// ConnectionPool manages a shared database connection pool
+type ConnectionPool struct {
+ db *sql.DB
+ config *config.PostgresConfig
+ mu sync.RWMutex
+ closed bool
+ logger *zap.Logger
+}
+
+// connectionPoolManager manages singleton connection pools by connection string
+var (
+ poolManager = &ConnectionPoolManager{
+ pools: make(map[string]*ConnectionPool),
+ }
+)
+
+// ConnectionPoolManager manages multiple connection pools
+type ConnectionPoolManager struct {
+ mu sync.RWMutex
+ pools map[string]*ConnectionPool
+}
+
+// GetConnectionPool returns a shared connection pool for the given config
+func GetConnectionPool(ctx context.Context, cfg *config.PostgresConfig) (*ConnectionPool, error) {
+ return poolManager.GetOrCreate(ctx, cfg)
+}
+
+// GetOrCreate returns an existing connection pool or creates a new one
+func (cpm *ConnectionPoolManager) GetOrCreate(ctx context.Context, cfg *config.PostgresConfig) (*ConnectionPool, error) {
+ // Create a unique key for this configuration
+ key := fmt.Sprintf("%s:%d/%s?user=%s", cfg.Host, cfg.Port, cfg.Database, cfg.User)
+
+ cpm.mu.RLock()
+ if pool, exists := cpm.pools[key]; exists && !pool.closed {
+ cpm.mu.RUnlock()
+ return pool, nil
+ }
+ cpm.mu.RUnlock()
+
+ // Need to create new connection pool
+ cpm.mu.Lock()
+ defer cpm.mu.Unlock()
+
+ // Double-check pattern
+ if pool, exists := cpm.pools[key]; exists && !pool.closed {
+ return pool, nil
+ }
+
+ // Create new connection pool
+ pool, err := NewConnectionPool(ctx, cfg)
+ if err != nil {
+ return nil, err
+ }
+
+ cpm.pools[key] = pool
+ return pool, nil
+}
+
+// CloseAll closes all connection pools
+func (cpm *ConnectionPoolManager) CloseAll() error {
+ cpm.mu.Lock()
+ defer cpm.mu.Unlock()
+
+ var errors []error
+ for key, pool := range cpm.pools {
+ if err := pool.Close(); err != nil {
+ errors = append(errors, xerrors.Errorf("failed to close pool %s: %w", key, err))
+ }
+ }
+
+ // Clear the pools map
+ cpm.pools = make(map[string]*ConnectionPool)
+
+ if len(errors) > 0 {
+ return xerrors.Errorf("errors closing connection pools: %v", errors)
+ }
+ return nil
+}
+
+// NewConnectionPool creates a new connection pool
+func NewConnectionPool(ctx context.Context, cfg *config.PostgresConfig) (*ConnectionPool, error) {
+ logger := log.WithPackage(log.NewDevelopment())
+
+ db, err := newDBConnection(ctx, cfg)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create database connection: %w", err)
+ }
+
+ pool := &ConnectionPool{
+ db: db,
+ config: cfg,
+ logger: logger,
+ }
+
+ logger.Debug("Created new connection pool",
+ zap.String("host", cfg.Host),
+ zap.Int("port", cfg.Port),
+ zap.String("database", cfg.Database),
+ zap.Int("max_connections", cfg.MaxConnections),
+ )
+
+ return pool, nil
+}
+
+// DB returns the underlying database connection
+func (cp *ConnectionPool) DB() *sql.DB {
+ cp.mu.RLock()
+ defer cp.mu.RUnlock()
+
+ if cp.closed {
+ return nil
+ }
+ return cp.db
+}
+
+// Close closes the connection pool
+func (cp *ConnectionPool) Close() error {
+ cp.mu.Lock()
+ defer cp.mu.Unlock()
+
+ if cp.closed {
+ return nil
+ }
+
+ cp.closed = true
+
+ if cp.db != nil {
+ if err := cp.db.Close(); err != nil {
+ cp.logger.Error("Failed to close database connection", zap.Error(err))
+ return err
+ }
+ cp.logger.Debug("Connection pool closed successfully")
+ }
+
+ return nil
+}
+
+// Stats returns connection pool statistics
+func (cp *ConnectionPool) Stats() sql.DBStats {
+ cp.mu.RLock()
+ defer cp.mu.RUnlock()
+
+ if cp.closed || cp.db == nil {
+ return sql.DBStats{}
+ }
+
+ return cp.db.Stats()
+}
+
+// Health checks if the connection pool is healthy
+func (cp *ConnectionPool) Health(ctx context.Context) error {
+ cp.mu.RLock()
+ defer cp.mu.RUnlock()
+
+ if cp.closed {
+ return xerrors.New("connection pool is closed")
+ }
+
+ if cp.db == nil {
+ return xerrors.New("database connection is nil")
+ }
+
+ return cp.db.PingContext(ctx)
+}
+
+// CloseAllConnectionPools closes all managed connection pools
+// This should be called during application shutdown
+func CloseAllConnectionPools() error {
+ return poolManager.CloseAll()
+}
diff --git a/internal/storage/metastorage/postgres/db/migrations/20240101000002_init_schema.sql b/internal/storage/metastorage/postgres/db/migrations/20240101000002_init_schema.sql
new file mode 100644
index 00000000..125ac701
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20240101000002_init_schema.sql
@@ -0,0 +1,72 @@
+-- +goose Up
+-- Create block_metadata table (append-only storage for every block ever observed)
+CREATE TABLE block_metadata (
+ id BIGSERIAL PRIMARY KEY, -- for canonical_blocks and event fk reference
+ height BIGINT NOT NULL,
+ tag INT NOT NULL,
+ hash VARCHAR(66), -- can hold a "0x"+64-hex string
+ parent_hash VARCHAR(66),
+ parent_height BIGINT NOT NULL DEFAULT 0,
+ object_key_main VARCHAR(255),
+ timestamp BIGINT NOT NULL, -- Unix timestamp in seconds
+ skipped BOOLEAN NOT NULL DEFAULT FALSE
+);
+
+-- Enforce uniqueness rules based on block processing status:
+-- 1. For normal blocks (skipped = FALSE), (tag, hash) must be unique, even if hash is NULL.
+-- 2. For skipped blocks (skipped = TRUE), hash is always NULL, so enforce uniqueness on (tag, height) instead.
+-- These partial unique indexes ensure correct behavior for both cases without violating constraints on NULLs.
+CREATE UNIQUE INDEX unique_tag_hash_regular ON block_metadata(tag, hash)
+WHERE hash IS NOT NULL AND NOT skipped;
+CREATE UNIQUE INDEX unique_tag_height_skipped ON block_metadata(tag, height)
+WHERE skipped = true;
+
+-- Create canonical_blocks table (track the "winning" block at each height)
+CREATE TABLE canonical_blocks (
+ height BIGINT NOT NULL,
+ block_metadata_id BIGINT NOT NULL,
+ tag INT NOT NULL,
+ -- Constraints
+ PRIMARY KEY (height, tag),
+ UNIQUE (
+ height,
+ tag,
+ block_metadata_id
+ ), -- Prevent same block from being canonical multiple times
+ FOREIGN KEY (block_metadata_id) REFERENCES block_metadata (id) ON DELETE RESTRICT
+);
+
+-- Supports: JOINs between canonical_blocks and block_metadata tables
+CREATE INDEX idx_canonical_block_metadata_fk ON canonical_blocks (block_metadata_id);
+
+-- Create block_events table (append-only stream of all blockchain state changes)
+CREATE TYPE event_type_enum AS ENUM ('BLOCK_ADDED', 'BLOCK_REMOVED', 'UNKNOWN');
+
+CREATE TABLE block_events (
+ event_tag INT NOT NULL DEFAULT 0, -- version
+ event_sequence BIGINT NOT NULL, -- monotonically-increasing per tag
+ event_type event_type_enum NOT NULL,
+ block_metadata_id BIGINT NOT NULL, -- fk referencing block_metadata
+ height BIGINT NOT NULL,
+ hash VARCHAR(66),
+ -- Constraints
+ PRIMARY KEY (event_tag, event_sequence),
+ FOREIGN KEY (block_metadata_id) REFERENCES block_metadata (id) ON DELETE RESTRICT
+);
+
+-- Supports: GetEventsByBlockHeight(), GetFirstEventIdByBlockHeight()
+CREATE INDEX idx_events_height_tag ON block_events (height, event_tag);
+-- Supports: JOINs to get full block details from events
+CREATE INDEX idx_events_block_meta ON block_events (block_metadata_id);
+
+-- +goose Down
+-- Drop tables in reverse order due to foreign key constraints
+
+DROP TABLE IF EXISTS block_events;
+
+DROP TABLE IF EXISTS canonical_blocks;
+
+DROP TABLE IF EXISTS block_metadata;
+
+-- Drop custom types
+DROP TYPE IF EXISTS event_type_enum;
\ No newline at end of file
diff --git a/internal/storage/metastorage/postgres/db/migrations/20240101000003_add_timestamp_index.sql b/internal/storage/metastorage/postgres/db/migrations/20240101000003_add_timestamp_index.sql
new file mode 100644
index 00000000..08d3e3b1
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20240101000003_add_timestamp_index.sql
@@ -0,0 +1,7 @@
+-- +goose Up
+-- Add timestamp index to block_metadata table for efficient time-based queries
+CREATE INDEX idx_block_metadata_timestamp ON block_metadata(timestamp);
+
+-- +goose Down
+-- Remove the timestamp index
+DROP INDEX IF EXISTS idx_block_metadata_timestamp;
\ No newline at end of file
diff --git a/internal/storage/metastorage/postgres/db/migrations/20250129000001_add_watermark.sql b/internal/storage/metastorage/postgres/db/migrations/20250129000001_add_watermark.sql
new file mode 100644
index 00000000..bc3421a2
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20250129000001_add_watermark.sql
@@ -0,0 +1,30 @@
+-- +goose Up
+-- Add is_watermark column to canonical_blocks table
+-- This column controls visibility of blocks to the streamer
+-- Blocks are written with is_watermark=FALSE initially, then set to TRUE after validation
+ALTER TABLE canonical_blocks ADD COLUMN is_watermark BOOLEAN NOT NULL DEFAULT FALSE;
+
+-- Create partial index for efficient watermark queries
+-- This index only includes watermarked blocks, keeping it small
+CREATE INDEX idx_canonical_watermark ON canonical_blocks (tag, height DESC) WHERE is_watermark = TRUE;
+
+-- Set watermark on the current highest block for each tag to prevent GetLatestBlock failures
+-- This ensures continuity during migration by marking existing latest blocks as validated
+-- Use CTE for better performance on large tables (avoids correlated subquery)
+WITH max_heights AS (
+ SELECT tag, MAX(height) as max_height
+ FROM canonical_blocks
+ GROUP BY tag
+)
+UPDATE canonical_blocks
+SET is_watermark = TRUE
+FROM max_heights
+WHERE canonical_blocks.tag = max_heights.tag
+ AND canonical_blocks.height = max_heights.max_height;
+
+-- +goose Down
+-- Drop the partial index first
+DROP INDEX IF EXISTS idx_canonical_watermark;
+
+-- Drop the is_watermark column
+ALTER TABLE canonical_blocks DROP COLUMN IF EXISTS is_watermark;
\ No newline at end of file
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260617000001_add_block_object_metadata.sql b/internal/storage/metastorage/postgres/db/migrations/20260617000001_add_block_object_metadata.sql
new file mode 100644
index 00000000..ea330432
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260617000001_add_block_object_metadata.sql
@@ -0,0 +1,38 @@
+-- +goose Up
+-- Consolidated block objects need longer object keys and per-block byte metadata.
+ALTER TABLE block_metadata
+ ALTER COLUMN object_key_main TYPE TEXT,
+ ADD COLUMN byte_offset BIGINT,
+ ADD COLUMN byte_length BIGINT,
+ ADD COLUMN uncompressed_length BIGINT,
+ ADD COLUMN object_format SMALLINT NOT NULL DEFAULT 0;
+
+-- Shadow placements are written after the primary legacy block commit and validation.
+CREATE TABLE block_consolidation_shadow (
+ block_metadata_id BIGINT PRIMARY KEY REFERENCES block_metadata (id) ON DELETE RESTRICT,
+ tag INT NOT NULL,
+ height BIGINT NOT NULL,
+ hash VARCHAR(66),
+ legacy_object_key_main TEXT NOT NULL,
+ consolidated_object_key_main TEXT NOT NULL,
+ object_format SMALLINT NOT NULL DEFAULT 1,
+ byte_offset BIGINT NOT NULL,
+ byte_length BIGINT NOT NULL,
+ uncompressed_length BIGINT,
+ format_version INT NOT NULL DEFAULT 1,
+ created_at TIMESTAMP NOT NULL DEFAULT NOW(),
+ validated_at TIMESTAMP
+);
+
+CREATE INDEX idx_block_consolidation_shadow_tag_height
+ ON block_consolidation_shadow (tag, height);
+
+-- +goose Down
+DROP INDEX IF EXISTS idx_block_consolidation_shadow_tag_height;
+DROP TABLE IF EXISTS block_consolidation_shadow;
+
+ALTER TABLE block_metadata
+ DROP COLUMN IF EXISTS uncompressed_length,
+ DROP COLUMN IF EXISTS byte_length,
+ DROP COLUMN IF EXISTS byte_offset,
+ DROP COLUMN IF EXISTS object_format;
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260617000002_add_unconsolidated_block_metadata_index.sql b/internal/storage/metastorage/postgres/db/migrations/20260617000002_add_unconsolidated_block_metadata_index.sql
new file mode 100644
index 00000000..86e97e54
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260617000002_add_unconsolidated_block_metadata_index.sql
@@ -0,0 +1,9 @@
+-- +goose NO TRANSACTION
+
+-- +goose Up
+CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_block_metadata_unconsolidated
+ ON block_metadata (tag, height)
+ WHERE byte_length IS NULL AND skipped = false;
+
+-- +goose Down
+DROP INDEX CONCURRENTLY IF EXISTS idx_block_metadata_unconsolidated;
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260701000001_add_consolidation_shadow_promotable_index.sql b/internal/storage/metastorage/postgres/db/migrations/20260701000001_add_consolidation_shadow_promotable_index.sql
new file mode 100644
index 00000000..a1b65de5
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260701000001_add_consolidation_shadow_promotable_index.sql
@@ -0,0 +1,16 @@
+-- +goose NO TRANSACTION
+
+-- +goose Up
+CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_block_consolidation_shadow_promotable
+ ON block_consolidation_shadow (tag, height, block_metadata_id)
+ WHERE validated_at IS NOT NULL
+ AND consolidated_object_key_main IS NOT NULL
+ AND consolidated_object_key_main <> ''
+ AND object_format = 1
+ AND byte_offset >= 0
+ AND byte_length > 0
+ AND uncompressed_length IS NOT NULL
+ AND uncompressed_length > 0;
+
+-- +goose Down
+DROP INDEX CONCURRENTLY IF EXISTS idx_block_consolidation_shadow_promotable;
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260702000001_add_block_consolidation_cursor.sql b/internal/storage/metastorage/postgres/db/migrations/20260702000001_add_block_consolidation_cursor.sql
new file mode 100644
index 00000000..5013cb5f
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260702000001_add_block_consolidation_cursor.sql
@@ -0,0 +1,11 @@
+-- +goose Up
+CREATE TABLE IF NOT EXISTS block_consolidation_cursor (
+ name TEXT NOT NULL,
+ tag INTEGER NOT NULL,
+ height BIGINT NOT NULL CHECK (height >= 0),
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ PRIMARY KEY (name, tag)
+);
+
+-- +goose Down
+DROP TABLE IF EXISTS block_consolidation_cursor;
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260703000001_add_consolidation_shadow_retirement.sql b/internal/storage/metastorage/postgres/db/migrations/20260703000001_add_consolidation_shadow_retirement.sql
new file mode 100644
index 00000000..0a6ae11b
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260703000001_add_consolidation_shadow_retirement.sql
@@ -0,0 +1,9 @@
+-- +goose Up
+ALTER TABLE block_consolidation_shadow
+ ADD COLUMN IF NOT EXISTS legacy_object_retired_at TIMESTAMP,
+ ADD COLUMN IF NOT EXISTS legacy_object_retire_after TIMESTAMP;
+
+-- +goose Down
+ALTER TABLE block_consolidation_shadow
+ DROP COLUMN IF EXISTS legacy_object_retired_at,
+ DROP COLUMN IF EXISTS legacy_object_retire_after;
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260713000001_add_single_block_retention_manifest.sql b/internal/storage/metastorage/postgres/db/migrations/20260713000001_add_single_block_retention_manifest.sql
new file mode 100644
index 00000000..1717d0fc
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260713000001_add_single_block_retention_manifest.sql
@@ -0,0 +1,187 @@
+-- +goose Up
+-- The three compatibility columns were deployed before the single-block
+-- terminology was standardized. Keep schema reads and writes synchronized
+-- during rolling deployments. This compatibility does not authorize retirement
+-- execution until every single-block writer runs the guarded uploader.
+ALTER TABLE block_consolidation_shadow
+ ALTER COLUMN legacy_object_key_main DROP NOT NULL,
+ ADD COLUMN single_block_object_key_main TEXT,
+ ADD COLUMN single_block_retention_started_at TIMESTAMP,
+ ADD COLUMN single_block_delete_after TIMESTAMP,
+ ADD COLUMN single_block_object_deleted_at TIMESTAMPTZ;
+
+UPDATE block_consolidation_shadow
+SET single_block_object_key_main = legacy_object_key_main,
+ single_block_retention_started_at = legacy_object_retired_at,
+ single_block_delete_after = legacy_object_retire_after;
+
+-- +goose StatementBegin
+CREATE FUNCTION sync_block_consolidation_shadow_single_block_columns()
+RETURNS TRIGGER AS $$
+DECLARE
+ compatibility_key_changed BOOLEAN;
+ canonical_key_changed BOOLEAN;
+ compatibility_started_at_changed BOOLEAN;
+ canonical_started_at_changed BOOLEAN;
+ compatibility_delete_after_changed BOOLEAN;
+ canonical_delete_after_changed BOOLEAN;
+BEGIN
+ IF TG_OP = 'INSERT' THEN
+ IF NEW.single_block_object_key_main IS NULL THEN
+ NEW.single_block_object_key_main := NEW.legacy_object_key_main;
+ ELSIF NEW.legacy_object_key_main IS NULL THEN
+ NEW.legacy_object_key_main := NEW.single_block_object_key_main;
+ ELSIF NEW.single_block_object_key_main IS DISTINCT FROM NEW.legacy_object_key_main THEN
+ RAISE EXCEPTION 'single-block object key compatibility columns disagree for block_metadata id %', NEW.block_metadata_id;
+ END IF;
+
+ IF NEW.single_block_retention_started_at IS NULL THEN
+ NEW.single_block_retention_started_at := NEW.legacy_object_retired_at;
+ ELSIF NEW.legacy_object_retired_at IS NULL THEN
+ NEW.legacy_object_retired_at := NEW.single_block_retention_started_at;
+ ELSIF NEW.single_block_retention_started_at IS DISTINCT FROM NEW.legacy_object_retired_at THEN
+ RAISE EXCEPTION 'single-block retention start compatibility columns disagree for block_metadata id %', NEW.block_metadata_id;
+ END IF;
+
+ IF NEW.single_block_delete_after IS NULL THEN
+ NEW.single_block_delete_after := NEW.legacy_object_retire_after;
+ ELSIF NEW.legacy_object_retire_after IS NULL THEN
+ NEW.legacy_object_retire_after := NEW.single_block_delete_after;
+ ELSIF NEW.single_block_delete_after IS DISTINCT FROM NEW.legacy_object_retire_after THEN
+ RAISE EXCEPTION 'single-block delete-after compatibility columns disagree for block_metadata id %', NEW.block_metadata_id;
+ END IF;
+ ELSE
+ compatibility_key_changed := NEW.legacy_object_key_main IS DISTINCT FROM OLD.legacy_object_key_main;
+ canonical_key_changed := NEW.single_block_object_key_main IS DISTINCT FROM OLD.single_block_object_key_main;
+ IF compatibility_key_changed AND canonical_key_changed
+ AND NEW.legacy_object_key_main IS DISTINCT FROM NEW.single_block_object_key_main THEN
+ RAISE EXCEPTION 'single-block object key compatibility columns changed to different values for block_metadata id %', NEW.block_metadata_id;
+ ELSIF compatibility_key_changed THEN
+ NEW.single_block_object_key_main := NEW.legacy_object_key_main;
+ ELSIF canonical_key_changed THEN
+ NEW.legacy_object_key_main := NEW.single_block_object_key_main;
+ ELSIF NEW.legacy_object_key_main IS DISTINCT FROM NEW.single_block_object_key_main THEN
+ RAISE EXCEPTION 'single-block object key compatibility columns disagree for block_metadata id %', NEW.block_metadata_id;
+ END IF;
+
+ compatibility_started_at_changed := NEW.legacy_object_retired_at IS DISTINCT FROM OLD.legacy_object_retired_at;
+ canonical_started_at_changed := NEW.single_block_retention_started_at IS DISTINCT FROM OLD.single_block_retention_started_at;
+ IF compatibility_started_at_changed AND canonical_started_at_changed
+ AND NEW.legacy_object_retired_at IS DISTINCT FROM NEW.single_block_retention_started_at THEN
+ RAISE EXCEPTION 'single-block retention start compatibility columns changed to different values for block_metadata id %', NEW.block_metadata_id;
+ ELSIF compatibility_started_at_changed THEN
+ NEW.single_block_retention_started_at := NEW.legacy_object_retired_at;
+ ELSIF canonical_started_at_changed THEN
+ NEW.legacy_object_retired_at := NEW.single_block_retention_started_at;
+ ELSIF NEW.legacy_object_retired_at IS DISTINCT FROM NEW.single_block_retention_started_at THEN
+ RAISE EXCEPTION 'single-block retention start compatibility columns disagree for block_metadata id %', NEW.block_metadata_id;
+ END IF;
+
+ compatibility_delete_after_changed := NEW.legacy_object_retire_after IS DISTINCT FROM OLD.legacy_object_retire_after;
+ canonical_delete_after_changed := NEW.single_block_delete_after IS DISTINCT FROM OLD.single_block_delete_after;
+ IF compatibility_delete_after_changed AND canonical_delete_after_changed
+ AND NEW.legacy_object_retire_after IS DISTINCT FROM NEW.single_block_delete_after THEN
+ RAISE EXCEPTION 'single-block delete-after compatibility columns changed to different values for block_metadata id %', NEW.block_metadata_id;
+ ELSIF compatibility_delete_after_changed THEN
+ NEW.single_block_delete_after := NEW.legacy_object_retire_after;
+ ELSIF canonical_delete_after_changed THEN
+ NEW.legacy_object_retire_after := NEW.single_block_delete_after;
+ ELSIF NEW.legacy_object_retire_after IS DISTINCT FROM NEW.single_block_delete_after THEN
+ RAISE EXCEPTION 'single-block delete-after compatibility columns disagree for block_metadata id %', NEW.block_metadata_id;
+ END IF;
+ END IF;
+
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER block_consolidation_shadow_single_block_compatibility_trigger
+BEFORE INSERT OR UPDATE OF
+ legacy_object_key_main,
+ legacy_object_retired_at,
+ legacy_object_retire_after,
+ single_block_object_key_main,
+ single_block_retention_started_at,
+ single_block_delete_after
+ON block_consolidation_shadow
+FOR EACH ROW
+EXECUTE FUNCTION sync_block_consolidation_shadow_single_block_columns();
+
+ALTER TABLE block_consolidation_shadow
+ ADD CONSTRAINT block_consolidation_shadow_single_block_retention_state CHECK (
+ legacy_object_key_main IS NOT DISTINCT FROM single_block_object_key_main
+ AND legacy_object_retired_at IS NOT DISTINCT FROM single_block_retention_started_at
+ AND legacy_object_retire_after IS NOT DISTINCT FROM single_block_delete_after
+ AND (
+ (single_block_object_key_main IS NOT NULL AND single_block_object_deleted_at IS NULL)
+ OR
+ (single_block_object_key_main IS NULL AND single_block_object_deleted_at IS NOT NULL)
+ )
+ ) NOT VALID;
+
+CREATE TABLE block_single_block_retention (
+ block_metadata_id BIGINT PRIMARY KEY REFERENCES block_consolidation_shadow (block_metadata_id) ON DELETE RESTRICT,
+ tag INTEGER NOT NULL,
+ height BIGINT NOT NULL CHECK (height >= 0),
+ hash VARCHAR(66),
+ state TEXT NOT NULL CHECK (state IN ('eligible', 'deleting', 'deleted_verified')),
+ bucket TEXT NOT NULL CHECK (bucket <> ''),
+ single_block_object_key_main TEXT,
+ single_block_object_key_sha256 CHAR(64) NOT NULL CHECK (single_block_object_key_sha256 ~ '^[0-9a-f]{64}$'),
+ single_block_object_version_ids TEXT[] NOT NULL CHECK (cardinality(single_block_object_version_ids) = 1 AND single_block_object_version_ids[1] <> ''),
+ single_block_object_etag TEXT NOT NULL,
+ single_block_object_bytes BIGINT NOT NULL CHECK (single_block_object_bytes > 0),
+ consolidated_object_key_main TEXT NOT NULL CHECK (consolidated_object_key_main <> ''),
+ consolidated_object_version_id TEXT NOT NULL CHECK (consolidated_object_version_id <> ''),
+ consolidated_object_etag TEXT NOT NULL CHECK (consolidated_object_etag <> ''),
+ consolidated_byte_offset BIGINT NOT NULL CHECK (consolidated_byte_offset >= 0),
+ consolidated_byte_length BIGINT NOT NULL CHECK (consolidated_byte_length > 0),
+ consolidated_uncompressed_length BIGINT NOT NULL CHECK (consolidated_uncompressed_length > 0),
+ payload_sha256 CHAR(64) NOT NULL CHECK (payload_sha256 ~ '^[0-9a-f]{64}$'),
+ outcome TEXT NOT NULL DEFAULT '',
+ attempt_count INTEGER NOT NULL DEFAULT 0 CHECK (attempt_count >= 0),
+ prepared_at TIMESTAMPTZ NOT NULL,
+ delete_started_at TIMESTAMPTZ,
+ last_attempt_at TIMESTAMPTZ,
+ deleted_at TIMESTAMPTZ,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
+ CHECK (
+ (state = 'eligible' AND single_block_object_key_main IS NOT NULL AND single_block_object_key_main <> '' AND single_block_object_etag <> '' AND delete_started_at IS NULL AND deleted_at IS NULL)
+ OR
+ (state = 'deleting' AND single_block_object_key_main IS NOT NULL AND single_block_object_key_main <> '' AND single_block_object_etag <> '' AND delete_started_at IS NOT NULL AND last_attempt_at IS NOT NULL AND attempt_count > 0 AND deleted_at IS NULL)
+ OR
+ (state = 'deleted_verified' AND single_block_object_key_main IS NULL AND single_block_object_etag = '' AND outcome <> '' AND delete_started_at IS NOT NULL AND last_attempt_at IS NOT NULL AND attempt_count > 0 AND deleted_at IS NOT NULL)
+ )
+);
+
+CREATE INDEX idx_block_single_block_retention_pending
+ ON block_single_block_retention (tag, height, block_metadata_id)
+ WHERE state IN ('eligible', 'deleting');
+
+-- +goose Down
+DO $$
+BEGIN
+ IF EXISTS (SELECT 1 FROM block_single_block_retention) THEN
+ RAISE EXCEPTION 'cannot roll back single-block retention migration while retirement manifests exist';
+ END IF;
+ IF EXISTS (SELECT 1 FROM block_consolidation_shadow WHERE single_block_object_key_main IS NULL) THEN
+ RAISE EXCEPTION 'cannot roll back single-block retention migration after a verified deletion';
+ END IF;
+END $$;
+
+DROP INDEX IF EXISTS idx_block_single_block_retention_pending;
+DROP TABLE IF EXISTS block_single_block_retention;
+
+ALTER TABLE block_consolidation_shadow
+ DROP CONSTRAINT IF EXISTS block_consolidation_shadow_single_block_retention_state;
+
+DROP TRIGGER IF EXISTS block_consolidation_shadow_single_block_compatibility_trigger ON block_consolidation_shadow;
+DROP FUNCTION IF EXISTS sync_block_consolidation_shadow_single_block_columns();
+
+ALTER TABLE block_consolidation_shadow
+ ALTER COLUMN legacy_object_key_main SET NOT NULL,
+ DROP COLUMN IF EXISTS single_block_object_deleted_at,
+ DROP COLUMN IF EXISTS single_block_delete_after,
+ DROP COLUMN IF EXISTS single_block_retention_started_at,
+ DROP COLUMN IF EXISTS single_block_object_key_main;
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260713000002_harden_single_block_retention.sql b/internal/storage/metastorage/postgres/db/migrations/20260713000002_harden_single_block_retention.sql
new file mode 100644
index 00000000..ab12700a
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260713000002_harden_single_block_retention.sql
@@ -0,0 +1,416 @@
+-- +goose Up
+-- This migration hardens the preceding retirement manifest migration.
+-- Fail closed if that draft schema was ever used; its in-flight rows cannot be
+-- assigned trustworthy claims or verification timestamps retroactively.
+-- +goose StatementBegin
+DO $$
+BEGIN
+ IF EXISTS (SELECT 1 FROM block_single_block_retention) THEN
+ RAISE EXCEPTION 'cannot harden single-block retirement schema while retirement manifests exist';
+ END IF;
+END $$;
+-- +goose StatementEnd
+
+ALTER TABLE block_metadata
+ ADD COLUMN single_block_retention_fenced_at TIMESTAMPTZ;
+
+ALTER TABLE block_metadata
+ ADD CONSTRAINT block_metadata_single_block_retirement_fence CHECK (
+ single_block_retention_fenced_at IS NULL
+ OR (
+ object_key_main IS NOT NULL
+ AND object_key_main <> ''
+ AND object_format = 1
+ AND byte_offset IS NOT NULL
+ AND byte_offset >= 0
+ AND byte_length IS NOT NULL
+ AND byte_length > 0
+ AND uncompressed_length IS NOT NULL
+ AND uncompressed_length > 0
+ )
+ ) NOT VALID;
+
+-- +goose StatementBegin
+CREATE FUNCTION enforce_block_metadata_single_block_retirement_fence()
+RETURNS TRIGGER AS $$
+BEGIN
+ IF OLD.single_block_retention_fenced_at IS NOT NULL
+ AND (
+ NEW.single_block_retention_fenced_at IS DISTINCT FROM OLD.single_block_retention_fenced_at
+ OR NEW.object_key_main IS DISTINCT FROM OLD.object_key_main
+ OR NEW.object_format IS DISTINCT FROM OLD.object_format
+ OR NEW.byte_offset IS DISTINCT FROM OLD.byte_offset
+ OR NEW.byte_length IS DISTINCT FROM OLD.byte_length
+ OR NEW.uncompressed_length IS DISTINCT FROM OLD.uncompressed_length
+ ) THEN
+ RAISE EXCEPTION 'cannot change CSCB placement after single-block object retirement is fenced for block_metadata id %', OLD.id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER block_metadata_single_block_retirement_fence_trigger
+BEFORE UPDATE OF single_block_retention_fenced_at, object_key_main, object_format, byte_offset, byte_length, uncompressed_length ON block_metadata
+FOR EACH ROW
+WHEN (OLD.single_block_retention_fenced_at IS NOT NULL)
+EXECUTE FUNCTION enforce_block_metadata_single_block_retirement_fence();
+
+-- +goose StatementBegin
+CREATE FUNCTION enforce_block_consolidation_shadow_single_block_retirement()
+RETURNS TRIGGER AS $$
+BEGIN
+ IF OLD.single_block_object_deleted_at IS NOT NULL
+ AND (
+ NEW.single_block_object_key_main IS DISTINCT FROM OLD.single_block_object_key_main
+ OR NEW.single_block_object_deleted_at IS DISTINCT FROM OLD.single_block_object_deleted_at
+ ) THEN
+ RAISE EXCEPTION 'cannot restore or rewrite deleted single-block object metadata for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER block_consolidation_shadow_single_block_retirement_trigger
+BEFORE UPDATE OF single_block_object_key_main, single_block_object_deleted_at ON block_consolidation_shadow
+FOR EACH ROW
+WHEN (OLD.single_block_object_deleted_at IS NOT NULL)
+EXECUTE FUNCTION enforce_block_consolidation_shadow_single_block_retirement();
+
+ALTER TABLE block_single_block_retention
+ ADD COLUMN claim_token TEXT,
+ ADD COLUMN claim_expires_at TIMESTAMPTZ,
+ ADD COLUMN verified_at TIMESTAMPTZ;
+
+ALTER TABLE block_single_block_retention
+ DROP CONSTRAINT block_single_block_retention_state_check,
+ DROP CONSTRAINT block_single_block_retention_check;
+
+ALTER TABLE block_single_block_retention
+ ADD CONSTRAINT block_single_block_retention_state_check CHECK (
+ state IN ('eligible', 'deleting', 'deleted_pending_verification', 'deleted_verified')
+ ),
+ ADD CONSTRAINT block_single_block_retention_immutable_version_ids_check CHECK (
+ cardinality(single_block_object_version_ids) = 1
+ AND single_block_object_version_ids[1] IS NOT NULL
+ AND BTRIM(single_block_object_version_ids[1]) <> ''
+ AND LOWER(BTRIM(single_block_object_version_ids[1])) <> 'null'
+ AND consolidated_object_version_id IS NOT NULL
+ AND BTRIM(consolidated_object_version_id) <> ''
+ AND LOWER(BTRIM(consolidated_object_version_id)) <> 'null'
+ ),
+ ADD CONSTRAINT block_single_block_retention_lifecycle_check CHECK (
+ (
+ state = 'eligible'
+ AND single_block_object_key_main IS NOT NULL
+ AND single_block_object_key_main <> ''
+ AND single_block_object_etag <> ''
+ AND claim_token IS NULL
+ AND claim_expires_at IS NULL
+ AND delete_started_at IS NULL
+ AND deleted_at IS NULL
+ AND verified_at IS NULL
+ )
+ OR (
+ state = 'deleting'
+ AND single_block_object_key_main IS NOT NULL
+ AND single_block_object_key_main <> ''
+ AND single_block_object_etag <> ''
+ AND claim_token IS NOT NULL
+ AND claim_token <> ''
+ AND claim_expires_at IS NOT NULL
+ AND delete_started_at IS NOT NULL
+ AND delete_started_at >= prepared_at
+ AND last_attempt_at IS NOT NULL
+ AND last_attempt_at >= delete_started_at
+ AND attempt_count > 0
+ AND deleted_at IS NULL
+ AND verified_at IS NULL
+ )
+ OR (
+ state = 'deleted_pending_verification'
+ AND single_block_object_key_main IS NULL
+ AND single_block_object_etag = ''
+ AND outcome <> ''
+ AND claim_token IS NOT NULL
+ AND claim_token <> ''
+ AND claim_expires_at IS NOT NULL
+ AND delete_started_at IS NOT NULL
+ AND last_attempt_at IS NOT NULL
+ AND attempt_count > 0
+ AND deleted_at IS NOT NULL
+ AND deleted_at >= delete_started_at
+ AND last_attempt_at >= deleted_at
+ AND verified_at IS NULL
+ )
+ OR (
+ state = 'deleted_verified'
+ AND single_block_object_key_main IS NULL
+ AND single_block_object_etag = ''
+ AND outcome <> ''
+ AND claim_token IS NULL
+ AND claim_expires_at IS NULL
+ AND delete_started_at IS NOT NULL
+ AND last_attempt_at IS NOT NULL
+ AND attempt_count > 0
+ AND deleted_at IS NOT NULL
+ AND deleted_at >= delete_started_at
+ AND verified_at IS NOT NULL
+ AND verified_at >= deleted_at
+ AND last_attempt_at >= verified_at
+ )
+ );
+
+CREATE TABLE cscb_retirement_safety_observation (
+ bucket TEXT NOT NULL CHECK (bucket <> ''),
+ consolidated_object_key_main TEXT NOT NULL CHECK (consolidated_object_key_main <> ''),
+ configuration_sha256 CHAR(64) NOT NULL CHECK (configuration_sha256 ~ '^[0-9a-f]{64}$'),
+ first_observed_at TIMESTAMPTZ NOT NULL,
+ last_observed_at TIMESTAMPTZ NOT NULL CHECK (last_observed_at >= first_observed_at),
+ PRIMARY KEY (bucket, consolidated_object_key_main)
+);
+
+-- +goose StatementBegin
+CREATE FUNCTION enforce_single_block_object_retirement_insert()
+RETURNS TRIGGER AS $$
+DECLARE
+ metadata_matches BOOLEAN;
+BEGIN
+ -- Let an idempotent INSERT ... ON CONFLICT reach its conflict handler. The
+ -- existing row remains protected by the update and delete triggers.
+ IF EXISTS (
+ SELECT 1
+ FROM block_single_block_retention
+ WHERE block_metadata_id = NEW.block_metadata_id
+ ) THEN
+ RETURN NEW;
+ END IF;
+
+ IF NEW.state <> 'eligible'
+ OR NEW.outcome <> ''
+ OR NEW.attempt_count <> 0
+ OR NEW.claim_token IS NOT NULL
+ OR NEW.claim_expires_at IS NOT NULL
+ OR NEW.delete_started_at IS NOT NULL
+ OR NEW.last_attempt_at IS NOT NULL
+ OR NEW.deleted_at IS NOT NULL
+ OR NEW.verified_at IS NOT NULL THEN
+ RAISE EXCEPTION 'single-block retirement manifest must be inserted in eligible state for block_metadata id %', NEW.block_metadata_id;
+ END IF;
+
+ SELECT EXISTS (
+ SELECT 1
+ FROM canonical_blocks cb
+ JOIN block_metadata bm
+ ON bm.id = cb.block_metadata_id
+ AND bm.tag = cb.tag
+ AND bm.height = cb.height
+ JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ WHERE bm.id = NEW.block_metadata_id
+ AND bm.tag = NEW.tag
+ AND bm.height = NEW.height
+ AND bm.hash IS NOT DISTINCT FROM NEW.hash
+ AND NOT bm.skipped
+ AND bm.object_key_main = NEW.consolidated_object_key_main
+ AND bm.object_format = 1
+ AND bm.byte_offset = NEW.consolidated_byte_offset
+ AND bm.byte_length = NEW.consolidated_byte_length
+ AND bm.uncompressed_length = NEW.consolidated_uncompressed_length
+ AND bm.single_block_retention_fenced_at IS NOT NULL
+ AND shadow.tag = NEW.tag
+ AND shadow.height = NEW.height
+ AND shadow.hash IS NOT DISTINCT FROM NEW.hash
+ AND shadow.single_block_object_key_main = NEW.single_block_object_key_main
+ AND shadow.consolidated_object_key_main = NEW.consolidated_object_key_main
+ AND shadow.object_format = 1
+ AND shadow.byte_offset = NEW.consolidated_byte_offset
+ AND shadow.byte_length = NEW.consolidated_byte_length
+ AND shadow.uncompressed_length = NEW.consolidated_uncompressed_length
+ AND shadow.validated_at IS NOT NULL
+ AND shadow.single_block_retention_started_at IS NOT NULL
+ AND shadow.single_block_delete_after IS NOT NULL
+ AND shadow.single_block_delete_after <= CURRENT_TIMESTAMP
+ AND shadow.single_block_object_deleted_at IS NULL
+ ) INTO metadata_matches;
+ IF NOT metadata_matches THEN
+ RAISE EXCEPTION 'single-block retirement manifest does not match fenced canonical CSCB metadata for block_metadata id %', NEW.block_metadata_id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER block_single_block_retention_insert_trigger
+BEFORE INSERT ON block_single_block_retention
+FOR EACH ROW
+EXECUTE FUNCTION enforce_single_block_object_retirement_insert();
+
+-- +goose StatementBegin
+CREATE FUNCTION enforce_single_block_object_retirement_transition()
+RETURNS TRIGGER AS $$
+DECLARE
+ shadow_single_block_key TEXT;
+ shadow_deleted_at TIMESTAMPTZ;
+BEGIN
+ IF NEW.block_metadata_id IS DISTINCT FROM OLD.block_metadata_id
+ OR NEW.tag IS DISTINCT FROM OLD.tag
+ OR NEW.height IS DISTINCT FROM OLD.height
+ OR NEW.hash IS DISTINCT FROM OLD.hash
+ OR NEW.bucket IS DISTINCT FROM OLD.bucket
+ OR NEW.single_block_object_key_sha256 IS DISTINCT FROM OLD.single_block_object_key_sha256
+ OR NEW.single_block_object_version_ids IS DISTINCT FROM OLD.single_block_object_version_ids
+ OR NEW.single_block_object_bytes IS DISTINCT FROM OLD.single_block_object_bytes
+ OR NEW.consolidated_object_key_main IS DISTINCT FROM OLD.consolidated_object_key_main
+ OR NEW.consolidated_object_version_id IS DISTINCT FROM OLD.consolidated_object_version_id
+ OR NEW.consolidated_object_etag IS DISTINCT FROM OLD.consolidated_object_etag
+ OR NEW.consolidated_byte_offset IS DISTINCT FROM OLD.consolidated_byte_offset
+ OR NEW.consolidated_byte_length IS DISTINCT FROM OLD.consolidated_byte_length
+ OR NEW.consolidated_uncompressed_length IS DISTINCT FROM OLD.consolidated_uncompressed_length
+ OR NEW.payload_sha256 IS DISTINCT FROM OLD.payload_sha256
+ OR NEW.prepared_at IS DISTINCT FROM OLD.prepared_at THEN
+ RAISE EXCEPTION 'cannot change pinned retirement manifest fields for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+
+ IF NOT (
+ (OLD.state = 'eligible' AND NEW.state IN ('eligible', 'deleting'))
+ OR (OLD.state = 'deleting' AND NEW.state IN ('deleting', 'deleted_pending_verification'))
+ OR (OLD.state = 'deleted_pending_verification' AND NEW.state IN ('deleted_pending_verification', 'deleted_verified'))
+ OR (OLD.state = 'deleted_verified' AND NEW.state = 'deleted_verified')
+ ) THEN
+ RAISE EXCEPTION 'invalid single-block retirement transition from % to % for block_metadata id %', OLD.state, NEW.state, OLD.block_metadata_id;
+ END IF;
+
+ IF OLD.state IN ('eligible', 'deleting')
+ AND NEW.state IN ('eligible', 'deleting')
+ AND (
+ NEW.single_block_object_key_main IS DISTINCT FROM OLD.single_block_object_key_main
+ OR NEW.single_block_object_etag IS DISTINCT FROM OLD.single_block_object_etag
+ ) THEN
+ RAISE EXCEPTION 'cannot change single-block object identity before verified deletion for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+
+ IF OLD.state IN ('deleting', 'deleted_pending_verification')
+ AND NEW.state = OLD.state
+ AND NEW.claim_token IS DISTINCT FROM OLD.claim_token
+ AND OLD.claim_expires_at > CURRENT_TIMESTAMP THEN
+ RAISE EXCEPTION 'cannot replace an active single-block retirement claim for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+ IF OLD.state = 'deleting'
+ AND NEW.state = 'deleted_pending_verification'
+ AND NEW.claim_token IS DISTINCT FROM OLD.claim_token THEN
+ RAISE EXCEPTION 'cannot change claim owner while recording single-block object deletion for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+
+ IF OLD.state = 'deleting' AND NEW.state = 'deleted_pending_verification' THEN
+ SELECT single_block_object_key_main, single_block_object_deleted_at
+ INTO shadow_single_block_key, shadow_deleted_at
+ FROM block_consolidation_shadow
+ WHERE block_metadata_id = OLD.block_metadata_id;
+ IF shadow_single_block_key IS NOT NULL
+ OR shadow_deleted_at IS NULL
+ OR shadow_deleted_at IS DISTINCT FROM NEW.deleted_at THEN
+ RAISE EXCEPTION 'shadow metadata is not durably cleared for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+ END IF;
+
+ IF OLD.delete_started_at IS NOT NULL AND NEW.delete_started_at IS DISTINCT FROM OLD.delete_started_at THEN
+ RAISE EXCEPTION 'cannot change single-block retirement delete_started_at for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+ IF OLD.deleted_at IS NOT NULL AND NEW.deleted_at IS DISTINCT FROM OLD.deleted_at THEN
+ RAISE EXCEPTION 'cannot change single-block retirement deleted_at for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+ IF NEW.attempt_count < OLD.attempt_count THEN
+ RAISE EXCEPTION 'cannot decrease single-block retirement attempt_count for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+ IF OLD.state = 'deleted_verified' AND NEW IS DISTINCT FROM OLD THEN
+ RAISE EXCEPTION 'cannot change a verified single-block retirement for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER block_single_block_retention_transition_trigger
+BEFORE UPDATE ON block_single_block_retention
+FOR EACH ROW
+EXECUTE FUNCTION enforce_single_block_object_retirement_transition();
+
+-- +goose StatementBegin
+CREATE FUNCTION forbid_single_block_object_retirement_delete()
+RETURNS TRIGGER AS $$
+BEGIN
+ RAISE EXCEPTION 'single-block retirement audit manifests cannot be deleted for block_metadata id %', OLD.block_metadata_id;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER block_single_block_retention_delete_trigger
+BEFORE DELETE ON block_single_block_retention
+FOR EACH ROW
+EXECUTE FUNCTION forbid_single_block_object_retirement_delete();
+
+DROP INDEX idx_block_single_block_retention_pending;
+CREATE INDEX idx_block_single_block_retention_pending
+ ON block_single_block_retention (tag, height, block_metadata_id)
+ WHERE state IN ('eligible', 'deleting', 'deleted_pending_verification');
+
+-- +goose Down
+-- +goose StatementBegin
+DO $$
+BEGIN
+ IF EXISTS (SELECT 1 FROM block_single_block_retention) THEN
+ RAISE EXCEPTION 'cannot roll back hardened single-block retirement schema while retirement manifests exist';
+ END IF;
+ IF EXISTS (SELECT 1 FROM block_metadata WHERE single_block_retention_fenced_at IS NOT NULL) THEN
+ RAISE EXCEPTION 'cannot roll back hardened single-block retirement schema while metadata fences exist';
+ END IF;
+END $$;
+-- +goose StatementEnd
+
+DROP INDEX IF EXISTS idx_block_single_block_retention_pending;
+
+DROP TABLE IF EXISTS cscb_retirement_safety_observation;
+
+DROP TRIGGER IF EXISTS block_single_block_retention_delete_trigger ON block_single_block_retention;
+DROP FUNCTION IF EXISTS forbid_single_block_object_retirement_delete();
+
+DROP TRIGGER IF EXISTS block_single_block_retention_transition_trigger ON block_single_block_retention;
+DROP FUNCTION IF EXISTS enforce_single_block_object_retirement_transition();
+
+DROP TRIGGER IF EXISTS block_single_block_retention_insert_trigger ON block_single_block_retention;
+DROP FUNCTION IF EXISTS enforce_single_block_object_retirement_insert();
+
+ALTER TABLE block_single_block_retention
+ DROP CONSTRAINT IF EXISTS block_single_block_retention_lifecycle_check,
+ DROP CONSTRAINT IF EXISTS block_single_block_retention_immutable_version_ids_check,
+ DROP CONSTRAINT IF EXISTS block_single_block_retention_state_check,
+ DROP COLUMN IF EXISTS verified_at,
+ DROP COLUMN IF EXISTS claim_expires_at,
+ DROP COLUMN IF EXISTS claim_token;
+
+ALTER TABLE block_single_block_retention
+ ADD CONSTRAINT block_single_block_retention_state_check CHECK (
+ state IN ('eligible', 'deleting', 'deleted_verified')
+ ),
+ ADD CONSTRAINT block_single_block_retention_check CHECK (
+ (state = 'eligible' AND single_block_object_key_main IS NOT NULL AND single_block_object_key_main <> '' AND single_block_object_etag <> '' AND delete_started_at IS NULL AND deleted_at IS NULL)
+ OR
+ (state = 'deleting' AND single_block_object_key_main IS NOT NULL AND single_block_object_key_main <> '' AND single_block_object_etag <> '' AND delete_started_at IS NOT NULL AND last_attempt_at IS NOT NULL AND attempt_count > 0 AND deleted_at IS NULL)
+ OR
+ (state = 'deleted_verified' AND single_block_object_key_main IS NULL AND single_block_object_etag = '' AND outcome <> '' AND delete_started_at IS NOT NULL AND last_attempt_at IS NOT NULL AND attempt_count > 0 AND deleted_at IS NOT NULL)
+ );
+
+CREATE INDEX idx_block_single_block_retention_pending
+ ON block_single_block_retention (tag, height, block_metadata_id)
+ WHERE state IN ('eligible', 'deleting');
+
+DROP TRIGGER IF EXISTS block_consolidation_shadow_single_block_retirement_trigger ON block_consolidation_shadow;
+DROP FUNCTION IF EXISTS enforce_block_consolidation_shadow_single_block_retirement();
+
+DROP TRIGGER IF EXISTS block_metadata_single_block_retirement_fence_trigger ON block_metadata;
+DROP FUNCTION IF EXISTS enforce_block_metadata_single_block_retirement_fence();
+
+ALTER TABLE block_metadata
+ DROP CONSTRAINT IF EXISTS block_metadata_single_block_retirement_fence,
+ DROP COLUMN IF EXISTS single_block_retention_fenced_at;
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260714000001_add_cscb_repair_manifest.sql b/internal/storage/metastorage/postgres/db/migrations/20260714000001_add_cscb_repair_manifest.sql
new file mode 100644
index 00000000..f4f7319d
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260714000001_add_cscb_repair_manifest.sql
@@ -0,0 +1,527 @@
+-- +goose Up
+CREATE TABLE cscb_repair_manifest (
+ id BIGSERIAL PRIMARY KEY,
+ tag INTEGER NOT NULL,
+ state TEXT NOT NULL CHECK (state IN ('preparing', 'prepared', 'restored', 'verified', 'completed')),
+ bucket TEXT NOT NULL CHECK (bucket <> ''),
+ old_consolidated_object_key_main TEXT NOT NULL CHECK (old_consolidated_object_key_main <> ''),
+ old_consolidated_object_version_id TEXT,
+ old_consolidated_object_etag TEXT,
+ old_consolidated_object_bytes BIGINT,
+ start_height BIGINT NOT NULL CHECK (start_height >= 0),
+ end_height BIGINT NOT NULL CHECK (end_height > start_height),
+ canonical_block_count BIGINT NOT NULL CHECK (canonical_block_count >= 0),
+ total_block_count BIGINT NOT NULL CHECK (total_block_count >= canonical_block_count),
+ row_set_sha256 CHAR(64) NOT NULL CHECK (row_set_sha256 ~ '^[0-9a-f]{64}$'),
+ new_consolidated_object_key_main TEXT,
+ new_consolidated_object_version_id TEXT,
+ new_consolidated_object_etag TEXT,
+ new_consolidated_object_bytes BIGINT,
+ outcome TEXT NOT NULL DEFAULT '',
+ prepared_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ restored_at TIMESTAMPTZ,
+ verified_at TIMESTAMPTZ,
+ completed_at TIMESTAMPTZ,
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE (tag, old_consolidated_object_key_main),
+ CHECK (
+ (
+ state = 'preparing'
+ AND old_consolidated_object_version_id IS NULL
+ AND old_consolidated_object_etag IS NULL
+ AND old_consolidated_object_bytes IS NULL
+ )
+ OR (
+ state <> 'preparing'
+ AND old_consolidated_object_version_id IS NOT NULL
+ AND BTRIM(old_consolidated_object_version_id) <> ''
+ AND LOWER(BTRIM(old_consolidated_object_version_id)) <> 'null'
+ AND old_consolidated_object_etag IS NOT NULL
+ AND old_consolidated_object_etag <> ''
+ AND old_consolidated_object_bytes IS NOT NULL
+ AND old_consolidated_object_bytes > 0
+ )
+ ),
+ CHECK (
+ (
+ state IN ('preparing', 'prepared')
+ AND outcome = ''
+ AND restored_at IS NULL
+ AND verified_at IS NULL
+ AND completed_at IS NULL
+ AND new_consolidated_object_key_main IS NULL
+ AND new_consolidated_object_version_id IS NULL
+ AND new_consolidated_object_etag IS NULL
+ AND new_consolidated_object_bytes IS NULL
+ )
+ OR (
+ state = 'restored'
+ AND outcome = ''
+ AND restored_at IS NOT NULL
+ AND verified_at IS NULL
+ AND completed_at IS NULL
+ AND new_consolidated_object_key_main IS NULL
+ AND new_consolidated_object_version_id IS NULL
+ AND new_consolidated_object_etag IS NULL
+ AND new_consolidated_object_bytes IS NULL
+ )
+ OR (
+ state = 'verified'
+ AND outcome = ''
+ AND restored_at IS NOT NULL
+ AND verified_at IS NOT NULL
+ AND verified_at >= restored_at
+ AND completed_at IS NULL
+ AND canonical_block_count > 0
+ AND new_consolidated_object_key_main IS NOT NULL
+ AND new_consolidated_object_key_main <> ''
+ AND new_consolidated_object_key_main <> old_consolidated_object_key_main
+ AND new_consolidated_object_version_id IS NOT NULL
+ AND BTRIM(new_consolidated_object_version_id) <> ''
+ AND LOWER(BTRIM(new_consolidated_object_version_id)) <> 'null'
+ AND new_consolidated_object_etag IS NOT NULL
+ AND new_consolidated_object_etag <> ''
+ AND new_consolidated_object_bytes IS NOT NULL
+ AND new_consolidated_object_bytes > 0
+ )
+ OR (
+ state = 'completed'
+ AND outcome = 'already_clean_storage_neutral'
+ AND restored_at IS NULL
+ AND verified_at IS NOT NULL
+ AND completed_at IS NOT NULL
+ AND completed_at >= verified_at
+ AND new_consolidated_object_key_main IS NULL
+ AND new_consolidated_object_version_id IS NULL
+ AND new_consolidated_object_etag IS NULL
+ AND new_consolidated_object_bytes IS NULL
+ )
+ OR (
+ state = 'completed'
+ AND outcome <> ''
+ AND outcome <> 'already_clean_storage_neutral'
+ AND restored_at IS NOT NULL
+ AND completed_at IS NOT NULL
+ AND completed_at >= restored_at
+ AND (
+ (
+ canonical_block_count > 0
+ AND verified_at IS NOT NULL
+ AND verified_at >= restored_at
+ AND completed_at >= verified_at
+ AND new_consolidated_object_key_main IS NOT NULL
+ AND new_consolidated_object_key_main <> ''
+ AND new_consolidated_object_key_main <> old_consolidated_object_key_main
+ AND new_consolidated_object_version_id IS NOT NULL
+ AND BTRIM(new_consolidated_object_version_id) <> ''
+ AND LOWER(BTRIM(new_consolidated_object_version_id)) <> 'null'
+ AND new_consolidated_object_etag IS NOT NULL
+ AND new_consolidated_object_etag <> ''
+ AND new_consolidated_object_bytes IS NOT NULL
+ AND new_consolidated_object_bytes > 0
+ )
+ OR (
+ canonical_block_count = 0
+ AND verified_at IS NULL
+ AND new_consolidated_object_key_main IS NULL
+ AND new_consolidated_object_version_id IS NULL
+ AND new_consolidated_object_etag IS NULL
+ AND new_consolidated_object_bytes IS NULL
+ )
+ )
+ )
+ )
+);
+
+CREATE TABLE cscb_repair_block (
+ repair_id BIGINT NOT NULL REFERENCES cscb_repair_manifest (id) ON DELETE RESTRICT,
+ block_metadata_id BIGINT NOT NULL REFERENCES block_metadata (id) ON DELETE RESTRICT,
+ canonical BOOLEAN NOT NULL,
+ tag INTEGER NOT NULL,
+ height BIGINT NOT NULL CHECK (height >= 0),
+ hash VARCHAR(66),
+ single_block_object_key_main TEXT CHECK (single_block_object_key_main IS NULL OR single_block_object_key_main <> ''),
+ single_block_object_key_sha256 CHAR(64) NOT NULL CHECK (single_block_object_key_sha256 ~ '^[0-9a-f]{64}$'),
+ single_block_object_version_id TEXT,
+ single_block_object_etag TEXT,
+ single_block_object_bytes BIGINT,
+ payload_sha256 CHAR(64),
+ old_byte_offset BIGINT NOT NULL CHECK (old_byte_offset >= 0),
+ old_byte_length BIGINT NOT NULL CHECK (old_byte_length > 0),
+ old_uncompressed_length BIGINT NOT NULL CHECK (old_uncompressed_length > 0),
+ PRIMARY KEY (repair_id, block_metadata_id),
+ UNIQUE (block_metadata_id),
+ CHECK (
+ (
+ single_block_object_version_id IS NULL
+ AND single_block_object_etag IS NULL
+ AND single_block_object_bytes IS NULL
+ AND payload_sha256 IS NULL
+ )
+ OR (
+ single_block_object_version_id IS NOT NULL
+ AND BTRIM(single_block_object_version_id) <> ''
+ AND LOWER(BTRIM(single_block_object_version_id)) <> 'null'
+ AND single_block_object_etag IS NOT NULL
+ AND single_block_object_etag <> ''
+ AND single_block_object_bytes IS NOT NULL
+ AND single_block_object_bytes > 0
+ AND payload_sha256 IS NOT NULL
+ AND payload_sha256 ~ '^[0-9a-f]{64}$'
+ )
+ )
+);
+
+CREATE TABLE cscb_repair_execution (
+ execution_key CHAR(64) PRIMARY KEY CHECK (execution_key ~ '^[0-9a-f]{64}$'),
+ -- NULL is a durable "no candidate" result. This prevents a lost Temporal
+ -- activity response from selecting newly appeared work on retry.
+ repair_id BIGINT REFERENCES cscb_repair_manifest (id) ON DELETE RESTRICT,
+ created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE INDEX idx_cscb_repair_manifest_pending
+ ON cscb_repair_manifest (tag, end_height DESC, id)
+ WHERE state <> 'completed';
+
+CREATE INDEX idx_cscb_repair_manifest_old_object
+ ON cscb_repair_manifest (old_consolidated_object_key_main);
+
+CREATE INDEX idx_cscb_repair_block_repair_height
+ ON cscb_repair_block (repair_id, height, block_metadata_id);
+
+CREATE INDEX idx_cscb_repair_block_metadata
+ ON cscb_repair_block (block_metadata_id);
+
+CREATE INDEX idx_cscb_repair_execution_repair
+ ON cscb_repair_execution (repair_id);
+
+-- Application writers acquire the repair tag lock before touching rows. These
+-- triggers independently prevent any pinned old object key from being
+-- reintroduced, including callers that claim a non-CSCB format.
+-- +goose StatementBegin
+CREATE FUNCTION prevent_cscb_repair_old_block_metadata_reference()
+RETURNS TRIGGER AS $$
+DECLARE
+ pinned_old BOOLEAN;
+BEGIN
+ IF NEW.object_key_main IS NULL THEN
+ RETURN NEW;
+ END IF;
+
+ SELECT EXISTS (
+ SELECT 1
+ FROM cscb_repair_manifest repair
+ WHERE repair.old_consolidated_object_key_main = NEW.object_key_main
+ AND NOT (
+ repair.state = 'completed'
+ AND repair.outcome = 'already_clean_storage_neutral'
+ )
+ ) INTO pinned_old;
+ IF NEW.object_format = 1 OR pinned_old THEN
+ PERFORM pg_advisory_xact_lock(hashtextextended(NEW.object_key_main, 1));
+ SELECT EXISTS (
+ SELECT 1
+ FROM cscb_repair_manifest repair
+ WHERE repair.old_consolidated_object_key_main = NEW.object_key_main
+ AND NOT (
+ repair.state = 'completed'
+ AND repair.outcome = 'already_clean_storage_neutral'
+ )
+ ) INTO pinned_old;
+ END IF;
+ IF pinned_old THEN
+ IF TG_OP = 'UPDATE'
+ AND OLD.object_key_main IS NOT DISTINCT FROM NEW.object_key_main
+ AND OLD.object_format IS NOT DISTINCT FROM NEW.object_format
+ AND OLD.byte_offset IS NOT DISTINCT FROM NEW.byte_offset
+ AND OLD.byte_length IS NOT DISTINCT FROM NEW.byte_length
+ AND OLD.uncompressed_length IS NOT DISTINCT FROM NEW.uncompressed_length THEN
+ RETURN NEW;
+ END IF;
+ RAISE EXCEPTION 'cannot reference a pinned old CSCB object from block_metadata id %', NEW.id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER cscb_repair_old_block_metadata_reference_trigger
+BEFORE INSERT OR UPDATE OF object_key_main, object_format, byte_offset, byte_length, uncompressed_length ON block_metadata
+FOR EACH ROW
+EXECUTE FUNCTION prevent_cscb_repair_old_block_metadata_reference();
+
+-- +goose StatementBegin
+CREATE FUNCTION prevent_cscb_repair_old_shadow_reference()
+RETURNS TRIGGER AS $$
+DECLARE
+ pinned_old BOOLEAN;
+BEGIN
+ IF NEW.consolidated_object_key_main IS NULL THEN
+ RETURN NEW;
+ END IF;
+
+ SELECT EXISTS (
+ SELECT 1
+ FROM cscb_repair_manifest repair
+ WHERE repair.old_consolidated_object_key_main = NEW.consolidated_object_key_main
+ AND NOT (
+ repair.state = 'completed'
+ AND repair.outcome = 'already_clean_storage_neutral'
+ )
+ ) INTO pinned_old;
+ IF NEW.object_format = 1 OR pinned_old THEN
+ PERFORM pg_advisory_xact_lock(hashtextextended(NEW.consolidated_object_key_main, 1));
+ SELECT EXISTS (
+ SELECT 1
+ FROM cscb_repair_manifest repair
+ WHERE repair.old_consolidated_object_key_main = NEW.consolidated_object_key_main
+ AND NOT (
+ repair.state = 'completed'
+ AND repair.outcome = 'already_clean_storage_neutral'
+ )
+ ) INTO pinned_old;
+ END IF;
+ IF pinned_old THEN
+ IF TG_OP = 'UPDATE'
+ AND OLD.consolidated_object_key_main IS NOT DISTINCT FROM NEW.consolidated_object_key_main
+ AND OLD.object_format IS NOT DISTINCT FROM NEW.object_format
+ AND OLD.byte_offset IS NOT DISTINCT FROM NEW.byte_offset
+ AND OLD.byte_length IS NOT DISTINCT FROM NEW.byte_length
+ AND OLD.uncompressed_length IS NOT DISTINCT FROM NEW.uncompressed_length THEN
+ RETURN NEW;
+ END IF;
+ RAISE EXCEPTION 'cannot reference a pinned old CSCB object from consolidation shadow for block_metadata id %', NEW.block_metadata_id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER cscb_repair_old_shadow_reference_trigger
+BEFORE INSERT OR UPDATE OF consolidated_object_key_main, object_format, byte_offset, byte_length, uncompressed_length ON block_consolidation_shadow
+FOR EACH ROW
+EXECUTE FUNCTION prevent_cscb_repair_old_shadow_reference();
+
+-- +goose StatementBegin
+CREATE FUNCTION enforce_cscb_repair_manifest_transition()
+RETURNS TRIGGER AS $$
+BEGIN
+ IF NEW.id IS DISTINCT FROM OLD.id
+ OR NEW.tag IS DISTINCT FROM OLD.tag
+ OR NEW.bucket IS DISTINCT FROM OLD.bucket
+ OR NEW.old_consolidated_object_key_main IS DISTINCT FROM OLD.old_consolidated_object_key_main
+ OR NEW.start_height IS DISTINCT FROM OLD.start_height
+ OR NEW.end_height IS DISTINCT FROM OLD.end_height
+ OR NEW.canonical_block_count IS DISTINCT FROM OLD.canonical_block_count
+ OR NEW.total_block_count IS DISTINCT FROM OLD.total_block_count
+ OR NEW.row_set_sha256 IS DISTINCT FROM OLD.row_set_sha256
+ OR NEW.prepared_at IS DISTINCT FROM OLD.prepared_at THEN
+ RAISE EXCEPTION 'cannot change fenced CSCB repair manifest fields for repair id %', OLD.id;
+ END IF;
+
+ IF OLD.state = 'preparing' THEN
+ IF NOT (
+ OLD.old_consolidated_object_version_id IS NULL
+ AND OLD.old_consolidated_object_etag IS NULL
+ AND OLD.old_consolidated_object_bytes IS NULL
+ AND NEW.old_consolidated_object_version_id IS NOT NULL
+ AND NEW.old_consolidated_object_etag IS NOT NULL
+ AND NEW.old_consolidated_object_bytes IS NOT NULL
+ ) THEN
+ RAISE EXCEPTION 'invalid CSCB repair source inspection for repair id %', OLD.id;
+ END IF;
+ ELSIF NEW.old_consolidated_object_version_id IS DISTINCT FROM OLD.old_consolidated_object_version_id
+ OR NEW.old_consolidated_object_etag IS DISTINCT FROM OLD.old_consolidated_object_etag
+ OR NEW.old_consolidated_object_bytes IS DISTINCT FROM OLD.old_consolidated_object_bytes THEN
+ RAISE EXCEPTION 'cannot change inspected CSCB repair source for repair id %', OLD.id;
+ END IF;
+
+ IF NOT (
+ (OLD.state = 'preparing' AND NEW.state IN ('prepared', 'completed'))
+ OR (OLD.state = 'prepared' AND NEW.state IN ('prepared', 'restored'))
+ OR (OLD.state = 'restored' AND NEW.state IN ('restored', 'verified', 'completed'))
+ OR (OLD.state = 'verified' AND NEW.state IN ('verified', 'completed'))
+ OR (OLD.state = 'completed' AND NEW.state = 'completed')
+ ) THEN
+ RAISE EXCEPTION 'invalid CSCB repair transition from % to % for repair id %', OLD.state, NEW.state, OLD.id;
+ END IF;
+
+ IF OLD.new_consolidated_object_key_main IS NOT NULL
+ AND (
+ NEW.new_consolidated_object_key_main IS DISTINCT FROM OLD.new_consolidated_object_key_main
+ OR NEW.new_consolidated_object_version_id IS DISTINCT FROM OLD.new_consolidated_object_version_id
+ OR NEW.new_consolidated_object_etag IS DISTINCT FROM OLD.new_consolidated_object_etag
+ OR NEW.new_consolidated_object_bytes IS DISTINCT FROM OLD.new_consolidated_object_bytes
+ OR NEW.verified_at IS DISTINCT FROM OLD.verified_at
+ ) THEN
+ RAISE EXCEPTION 'cannot change verified CSCB repair target for repair id %', OLD.id;
+ END IF;
+
+ IF OLD.restored_at IS NOT NULL AND NEW.restored_at IS DISTINCT FROM OLD.restored_at THEN
+ RAISE EXCEPTION 'cannot change CSCB repair restore timestamp for repair id %', OLD.id;
+ END IF;
+
+ IF NEW.outcome IS DISTINCT FROM OLD.outcome
+ AND NOT (
+ (OLD.state = 'preparing' AND NEW.state = 'completed'
+ AND NEW.outcome = 'already_clean_storage_neutral')
+ OR ((OLD.state = 'verified' OR (OLD.state = 'restored' AND OLD.canonical_block_count = 0))
+ AND NEW.state = 'completed' AND OLD.outcome = '' AND NEW.outcome <> '')
+ ) THEN
+ RAISE EXCEPTION 'cannot change CSCB repair outcome outside completion for repair id %', OLD.id;
+ END IF;
+
+ IF OLD.completed_at IS NOT NULL AND NEW.completed_at IS DISTINCT FROM OLD.completed_at THEN
+ RAISE EXCEPTION 'cannot change completed CSCB repair timestamp for repair id %', OLD.id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER cscb_repair_manifest_transition_trigger
+BEFORE UPDATE ON cscb_repair_manifest
+FOR EACH ROW
+EXECUTE FUNCTION enforce_cscb_repair_manifest_transition();
+
+-- Inspection fills S3 observations exactly once. Retirement may later scrub
+-- the exact single-block path, retaining its hash and immutable object audit.
+-- +goose StatementBegin
+CREATE FUNCTION enforce_cscb_repair_block_audit_update()
+RETURNS TRIGGER AS $$
+DECLARE
+ repair_state TEXT;
+ retirement_deleted BOOLEAN;
+BEGIN
+ SELECT state INTO repair_state FROM cscb_repair_manifest WHERE id = OLD.repair_id;
+
+ IF repair_state = 'preparing'
+ AND NEW.repair_id IS NOT DISTINCT FROM OLD.repair_id
+ AND NEW.block_metadata_id IS NOT DISTINCT FROM OLD.block_metadata_id
+ AND NEW.canonical IS NOT DISTINCT FROM OLD.canonical
+ AND NEW.tag IS NOT DISTINCT FROM OLD.tag
+ AND NEW.height IS NOT DISTINCT FROM OLD.height
+ AND NEW.hash IS NOT DISTINCT FROM OLD.hash
+ AND NEW.single_block_object_key_main IS NOT DISTINCT FROM OLD.single_block_object_key_main
+ AND NEW.single_block_object_key_sha256 IS NOT DISTINCT FROM OLD.single_block_object_key_sha256
+ AND OLD.single_block_object_version_id IS NULL
+ AND OLD.single_block_object_etag IS NULL
+ AND OLD.single_block_object_bytes IS NULL
+ AND OLD.payload_sha256 IS NULL
+ AND NEW.single_block_object_version_id IS NOT NULL
+ AND NEW.single_block_object_etag IS NOT NULL
+ AND NEW.single_block_object_bytes IS NOT NULL
+ AND NEW.payload_sha256 IS NOT NULL
+ AND NEW.old_byte_offset IS NOT DISTINCT FROM OLD.old_byte_offset
+ AND NEW.old_byte_length IS NOT DISTINCT FROM OLD.old_byte_length
+ AND NEW.old_uncompressed_length IS NOT DISTINCT FROM OLD.old_uncompressed_length THEN
+ RETURN NEW;
+ END IF;
+
+ IF repair_state = 'completed'
+ AND OLD.single_block_object_key_main IS NOT NULL
+ AND NEW.single_block_object_key_main IS NULL
+ AND NEW.repair_id IS NOT DISTINCT FROM OLD.repair_id
+ AND NEW.block_metadata_id IS NOT DISTINCT FROM OLD.block_metadata_id
+ AND NEW.canonical IS NOT DISTINCT FROM OLD.canonical
+ AND NEW.tag IS NOT DISTINCT FROM OLD.tag
+ AND NEW.height IS NOT DISTINCT FROM OLD.height
+ AND NEW.hash IS NOT DISTINCT FROM OLD.hash
+ AND NEW.single_block_object_key_sha256 IS NOT DISTINCT FROM OLD.single_block_object_key_sha256
+ AND NEW.single_block_object_version_id IS NOT DISTINCT FROM OLD.single_block_object_version_id
+ AND NEW.single_block_object_etag IS NOT DISTINCT FROM OLD.single_block_object_etag
+ AND NEW.single_block_object_bytes IS NOT DISTINCT FROM OLD.single_block_object_bytes
+ AND NEW.payload_sha256 IS NOT DISTINCT FROM OLD.payload_sha256
+ AND NEW.old_byte_offset IS NOT DISTINCT FROM OLD.old_byte_offset
+ AND NEW.old_byte_length IS NOT DISTINCT FROM OLD.old_byte_length
+ AND NEW.old_uncompressed_length IS NOT DISTINCT FROM OLD.old_uncompressed_length THEN
+ SELECT EXISTS (
+ SELECT 1
+ FROM block_single_block_retention retirement
+ WHERE retirement.block_metadata_id = OLD.block_metadata_id
+ AND retirement.state IN ('deleted_pending_verification', 'completed')
+ AND retirement.deleted_at IS NOT NULL
+ ) INTO retirement_deleted;
+ IF retirement_deleted THEN
+ RETURN NEW;
+ END IF;
+ END IF;
+
+ RAISE EXCEPTION 'cannot mutate CSCB repair block audit for metadata id %', OLD.block_metadata_id;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER cscb_repair_block_update_trigger
+BEFORE UPDATE ON cscb_repair_block
+FOR EACH ROW
+EXECUTE FUNCTION enforce_cscb_repair_block_audit_update();
+
+-- +goose StatementBegin
+CREATE FUNCTION prevent_cscb_repair_audit_mutation()
+RETURNS TRIGGER AS $$
+BEGIN
+ RAISE EXCEPTION 'CSCB repair audit rows are immutable';
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER cscb_repair_block_delete_trigger
+BEFORE DELETE ON cscb_repair_block
+FOR EACH ROW
+EXECUTE FUNCTION prevent_cscb_repair_audit_mutation();
+
+CREATE TRIGGER cscb_repair_execution_update_trigger
+BEFORE UPDATE ON cscb_repair_execution
+FOR EACH ROW
+EXECUTE FUNCTION prevent_cscb_repair_audit_mutation();
+
+CREATE TRIGGER cscb_repair_execution_delete_trigger
+BEFORE DELETE ON cscb_repair_execution
+FOR EACH ROW
+EXECUTE FUNCTION prevent_cscb_repair_audit_mutation();
+
+CREATE TRIGGER cscb_repair_manifest_delete_trigger
+BEFORE DELETE ON cscb_repair_manifest
+FOR EACH ROW
+EXECUTE FUNCTION prevent_cscb_repair_audit_mutation();
+
+-- +goose Down
+-- +goose StatementBegin
+DO $$
+BEGIN
+ LOCK TABLE cscb_repair_execution, cscb_repair_block, cscb_repair_manifest
+ IN ACCESS EXCLUSIVE MODE;
+ IF EXISTS (SELECT 1 FROM cscb_repair_manifest)
+ OR EXISTS (SELECT 1 FROM cscb_repair_execution) THEN
+ RAISE EXCEPTION 'cannot roll back CSCB repair migration while repair manifests or execution bindings exist';
+ END IF;
+END $$;
+-- +goose StatementEnd
+
+DROP TRIGGER IF EXISTS cscb_repair_manifest_delete_trigger ON cscb_repair_manifest;
+DROP TRIGGER IF EXISTS cscb_repair_execution_delete_trigger ON cscb_repair_execution;
+DROP TRIGGER IF EXISTS cscb_repair_execution_update_trigger ON cscb_repair_execution;
+DROP TRIGGER IF EXISTS cscb_repair_block_delete_trigger ON cscb_repair_block;
+DROP FUNCTION IF EXISTS prevent_cscb_repair_audit_mutation();
+
+DROP TRIGGER IF EXISTS cscb_repair_block_update_trigger ON cscb_repair_block;
+DROP FUNCTION IF EXISTS enforce_cscb_repair_block_audit_update();
+
+DROP TRIGGER IF EXISTS cscb_repair_old_shadow_reference_trigger ON block_consolidation_shadow;
+DROP FUNCTION IF EXISTS prevent_cscb_repair_old_shadow_reference();
+
+DROP TRIGGER IF EXISTS cscb_repair_old_block_metadata_reference_trigger ON block_metadata;
+DROP FUNCTION IF EXISTS prevent_cscb_repair_old_block_metadata_reference();
+
+DROP TRIGGER IF EXISTS cscb_repair_manifest_transition_trigger ON cscb_repair_manifest;
+DROP FUNCTION IF EXISTS enforce_cscb_repair_manifest_transition();
+
+DROP INDEX IF EXISTS idx_cscb_repair_execution_repair;
+DROP INDEX IF EXISTS idx_cscb_repair_block_repair_height;
+DROP INDEX IF EXISTS idx_cscb_repair_manifest_old_object;
+DROP INDEX IF EXISTS idx_cscb_repair_manifest_pending;
+DROP TABLE IF EXISTS cscb_repair_execution;
+DROP TABLE IF EXISTS cscb_repair_block;
+DROP TABLE IF EXISTS cscb_repair_manifest;
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260714000002_add_cscb_repair_indexes.sql b/internal/storage/metastorage/postgres/db/migrations/20260714000002_add_cscb_repair_indexes.sql
new file mode 100644
index 00000000..3044e08d
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260714000002_add_cscb_repair_indexes.sql
@@ -0,0 +1,28 @@
+-- +goose NO TRANSACTION
+
+-- +goose Up
+CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_block_metadata_cscb_repair_candidate
+ ON block_metadata (tag, height DESC, object_key_main, id)
+ WHERE object_format = 1
+ AND object_key_main IS NOT NULL
+ AND object_key_main <> '';
+
+-- CSCB repair only performs exact object-key reference checks. B-trees over
+-- the full keys and row IDs consume unnecessary space and the block_metadata
+-- index exceeds Aurora local scratch storage on large chains such as Solana.
+-- Hash indexes preserve the required equality lookups without storing or
+-- sorting the full keys.
+CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_block_metadata_object_key_reference
+ ON block_metadata USING HASH (object_key_main)
+ WHERE object_key_main IS NOT NULL
+ AND object_key_main <> '';
+
+CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_block_consolidation_shadow_object_key_reference
+ ON block_consolidation_shadow USING HASH (consolidated_object_key_main)
+ WHERE consolidated_object_key_main IS NOT NULL
+ AND consolidated_object_key_main <> '';
+
+-- +goose Down
+DROP INDEX CONCURRENTLY IF EXISTS idx_block_consolidation_shadow_object_key_reference;
+DROP INDEX CONCURRENTLY IF EXISTS idx_block_metadata_object_key_reference;
+DROP INDEX CONCURRENTLY IF EXISTS idx_block_metadata_cscb_repair_candidate;
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260721000001_support_multi_version_retirement.sql b/internal/storage/metastorage/postgres/db/migrations/20260721000001_support_multi_version_retirement.sql
new file mode 100644
index 00000000..1052e629
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260721000001_support_multi_version_retirement.sql
@@ -0,0 +1,88 @@
+-- +goose Up
+ALTER TABLE block_single_block_retention
+ ADD COLUMN single_block_delete_marker_version_ids TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
+
+-- +goose StatementBegin
+CREATE FUNCTION validate_single_block_retirement_version_ids(version_ids TEXT[], allow_empty BOOLEAN)
+RETURNS BOOLEAN AS $$
+ SELECT version_ids IS NOT NULL
+ AND (allow_empty OR cardinality(version_ids) > 0)
+ AND NOT EXISTS (
+ SELECT 1
+ FROM unnest(version_ids) AS version_id
+ WHERE version_id IS NULL
+ OR BTRIM(version_id) = ''
+ OR LOWER(BTRIM(version_id)) = 'null'
+ )
+ AND cardinality(version_ids) = (
+ SELECT COUNT(DISTINCT version_id)
+ FROM unnest(version_ids) AS version_id
+ );
+$$ LANGUAGE SQL IMMUTABLE PARALLEL SAFE;
+-- +goose StatementEnd
+
+ALTER TABLE block_single_block_retention
+ DROP CONSTRAINT block_single_block_retention_single_block_object_version__check,
+ DROP CONSTRAINT block_single_block_retention_immutable_version_ids_check,
+ ADD CONSTRAINT block_single_block_retention_immutable_version_ids_check CHECK (
+ validate_single_block_retirement_version_ids(single_block_object_version_ids, FALSE)
+ AND validate_single_block_retirement_version_ids(single_block_delete_marker_version_ids, TRUE)
+ AND consolidated_object_version_id IS NOT NULL
+ AND BTRIM(consolidated_object_version_id) <> ''
+ AND LOWER(BTRIM(consolidated_object_version_id)) <> 'null'
+ );
+
+-- +goose StatementBegin
+CREATE FUNCTION enforce_single_block_retirement_delete_markers_immutable()
+RETURNS TRIGGER AS $$
+BEGIN
+ IF NEW.single_block_delete_marker_version_ids IS DISTINCT FROM OLD.single_block_delete_marker_version_ids THEN
+ RAISE EXCEPTION 'cannot change pinned retirement delete-marker versions for block_metadata id %', OLD.block_metadata_id;
+ END IF;
+ RETURN NEW;
+END;
+$$ LANGUAGE plpgsql;
+-- +goose StatementEnd
+
+CREATE TRIGGER block_single_block_retention_delete_markers_immutable_trigger
+BEFORE UPDATE OF single_block_delete_marker_version_ids ON block_single_block_retention
+FOR EACH ROW
+EXECUTE FUNCTION enforce_single_block_retirement_delete_markers_immutable();
+
+-- +goose Down
+-- +goose StatementBegin
+DO $$
+BEGIN
+ IF EXISTS (
+ SELECT 1
+ FROM block_single_block_retention
+ WHERE cardinality(single_block_object_version_ids) <> 1
+ OR cardinality(single_block_delete_marker_version_ids) <> 0
+ ) THEN
+ RAISE EXCEPTION 'cannot roll back multi-version retirement while multi-version manifests exist';
+ END IF;
+END $$;
+-- +goose StatementEnd
+
+DROP TRIGGER IF EXISTS block_single_block_retention_delete_markers_immutable_trigger
+ ON block_single_block_retention;
+DROP FUNCTION IF EXISTS enforce_single_block_retirement_delete_markers_immutable();
+
+ALTER TABLE block_single_block_retention
+ DROP CONSTRAINT block_single_block_retention_immutable_version_ids_check,
+ ADD CONSTRAINT block_single_block_retention_single_block_object_version__check CHECK (
+ cardinality(single_block_object_version_ids) = 1
+ AND single_block_object_version_ids[1] <> ''
+ ),
+ ADD CONSTRAINT block_single_block_retention_immutable_version_ids_check CHECK (
+ cardinality(single_block_object_version_ids) = 1
+ AND single_block_object_version_ids[1] IS NOT NULL
+ AND BTRIM(single_block_object_version_ids[1]) <> ''
+ AND LOWER(BTRIM(single_block_object_version_ids[1])) <> 'null'
+ AND consolidated_object_version_id IS NOT NULL
+ AND BTRIM(consolidated_object_version_id) <> ''
+ AND LOWER(BTRIM(consolidated_object_version_id)) <> 'null'
+ ),
+ DROP COLUMN single_block_delete_marker_version_ids;
+
+DROP FUNCTION IF EXISTS validate_single_block_retirement_version_ids(TEXT[], BOOLEAN);
diff --git a/internal/storage/metastorage/postgres/db/migrations/20260723000001_add_single_block_retention_due_index.sql b/internal/storage/metastorage/postgres/db/migrations/20260723000001_add_single_block_retention_due_index.sql
new file mode 100644
index 00000000..88d5299b
--- /dev/null
+++ b/internal/storage/metastorage/postgres/db/migrations/20260723000001_add_single_block_retention_due_index.sql
@@ -0,0 +1,23 @@
+-- +goose NO TRANSACTION
+
+-- +goose Up
+-- The retirement selector starts from delete-after time rather than
+-- height because repaired historical CSCBs become eligible out of order. Keep
+-- the full CSCB object key out of this sorted B-tree; the existing hash index
+-- serves the exact-key join without repeating large keys here.
+CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_block_consolidation_shadow_retention_due
+ ON block_consolidation_shadow (
+ tag,
+ single_block_delete_after,
+ height
+ )
+ WHERE validated_at IS NOT NULL
+ AND single_block_delete_after IS NOT NULL
+ AND single_block_object_deleted_at IS NULL
+ AND single_block_object_key_main IS NOT NULL
+ AND single_block_object_key_main <> ''
+ AND consolidated_object_key_main IS NOT NULL
+ AND consolidated_object_key_main <> '';
+
+-- +goose Down
+DROP INDEX CONCURRENTLY IF EXISTS idx_block_consolidation_shadow_retention_due;
diff --git a/internal/storage/metastorage/postgres/event_storage.go b/internal/storage/metastorage/postgres/event_storage.go
new file mode 100644
index 00000000..75af5804
--- /dev/null
+++ b/internal/storage/metastorage/postgres/event_storage.go
@@ -0,0 +1,651 @@
+package postgres
+
+import (
+ "context"
+ "database/sql"
+ "time"
+
+ "golang.org/x/xerrors"
+
+ "github.com/lib/pq"
+
+ "github.com/coinbase/chainstorage/internal/storage/internal/errors"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/internal"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/model"
+ pgmodel "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres/model"
+ "github.com/coinbase/chainstorage/internal/utils/instrument"
+)
+
+const (
+ addEventsSafePadding = int64(20)
+)
+
+type (
+ eventStorageImpl struct {
+ db *sql.DB
+ instrumentAddEvents instrument.Instrument
+ instrumentGetEventByEventId instrument.InstrumentWithResult[*model.EventEntry]
+ instrumentGetEventsAfterEventId instrument.InstrumentWithResult[[]*model.EventEntry]
+ instrumentGetEventsByEventIdRange instrument.InstrumentWithResult[[]*model.EventEntry]
+ instrumentGetMaxEventId instrument.InstrumentWithResult[int64]
+ instrumentSetMaxEventId instrument.Instrument
+ instrumentGetFirstEventIdByBlockHeight instrument.InstrumentWithResult[int64]
+ instrumentGetEventsByBlockHeight instrument.InstrumentWithResult[[]*model.EventEntry]
+ }
+)
+
+func newEventStorage(db *sql.DB, params Params) (internal.EventStorage, error) {
+ metrics := params.Metrics.SubScope("event_storage").Tagged(map[string]string{
+ "storage_type": "postgres",
+ })
+ storage := &eventStorageImpl{
+ db: db,
+ instrumentAddEvents: instrument.New(metrics, "add_events"),
+ instrumentGetEventByEventId: instrument.NewWithResult[*model.EventEntry](metrics, "get_event_by_event_id"),
+ instrumentGetEventsAfterEventId: instrument.NewWithResult[[]*model.EventEntry](metrics, "get_events_after_event_id"),
+ instrumentGetEventsByEventIdRange: instrument.NewWithResult[[]*model.EventEntry](metrics, "get_events_by_event_id_range"),
+ instrumentGetMaxEventId: instrument.NewWithResult[int64](metrics, "get_max_event_id"),
+ instrumentSetMaxEventId: instrument.New(metrics, "set_max_event_id"),
+ instrumentGetFirstEventIdByBlockHeight: instrument.NewWithResult[int64](metrics, "get_first_event_id_by_block_height"),
+ instrumentGetEventsByBlockHeight: instrument.NewWithResult[[]*model.EventEntry](metrics, "get_events_by_block_height"),
+ }
+ return storage, nil
+}
+
+func (e *eventStorageImpl) AddEvents(ctx context.Context, eventTag uint32, events []*model.BlockEvent) error {
+ if len(events) == 0 {
+ return nil
+ }
+ return e.instrumentAddEvents.Instrument(ctx, func(ctx context.Context) error {
+ maxEventId, err := e.GetMaxEventId(ctx, eventTag)
+ var startEventId int64
+ if err != nil {
+ if !xerrors.Is(err, errors.ErrNoEventHistory) {
+ return xerrors.Errorf("failed to get max event id: %w", err)
+ }
+ startEventId = model.EventIdStartValue
+ } else {
+ startEventId = maxEventId + 1
+ }
+
+ eventEntries := model.ConvertBlockEventsToEventEntries(events, eventTag, startEventId)
+ return e.AddEventEntries(ctx, eventTag, eventEntries)
+ })
+}
+
+func (e *eventStorageImpl) AddEventEntries(ctx context.Context, eventTag uint32, eventEntries []*model.EventEntry) error {
+ if len(eventEntries) == 0 {
+ return nil
+ }
+ return e.instrumentAddEvents.Instrument(ctx, func(ctx context.Context) error {
+ startEventId := eventEntries[0].EventId
+ var eventsToValidate []*model.EventEntry
+ startFetchId := startEventId - addEventsSafePadding
+ if startFetchId < model.EventIdStartValue {
+ startFetchId = model.EventIdStartValue
+ }
+ if startFetchId < startEventId {
+ beforeEvents, err := e.GetEventsByEventIdRange(ctx, eventTag, startFetchId, startEventId)
+ if err != nil {
+ return xerrors.Errorf("failed to fetch events: %w", err)
+ }
+ eventsToValidate = append(beforeEvents, eventEntries...)
+ } else {
+ eventsToValidate = eventEntries
+ }
+
+ if err := internal.ValidateEvents(eventsToValidate); err != nil {
+ return xerrors.Errorf("events failed validation: %w", err)
+ }
+
+ // Create transaction with timeout context for event operations
+ txCtx, cancel := context.WithTimeout(ctx, 180*time.Second)
+ defer cancel()
+
+ tx, err := e.db.BeginTx(txCtx, nil)
+ if err != nil {
+ return xerrors.Errorf("failed to start transaction: %w", err)
+ }
+ committed := false
+ defer func() {
+ if !committed {
+ if rollbackErr := tx.Rollback(); rollbackErr != nil {
+ _ = rollbackErr
+ }
+ }
+ }()
+
+ // Stage incoming events (no per-row lookups) for set-based processing
+ if _, err := tx.ExecContext(ctx, `
+ CREATE TEMP TABLE temp_events (
+ event_tag INT NOT NULL,
+ event_sequence BIGINT NOT NULL,
+ event_type TEXT NOT NULL,
+ height BIGINT NOT NULL,
+ hash VARCHAR(66),
+ bm_tag INT NOT NULL,
+ skipped BOOLEAN NOT NULL
+ ) ON COMMIT DROP
+ `); err != nil {
+ return xerrors.Errorf("failed to create temp_events: %w", err)
+ }
+
+ stmt, err := tx.Prepare(pq.CopyIn("temp_events",
+ "event_tag", "event_sequence", "event_type", "height", "hash", "bm_tag", "skipped"))
+ if err != nil {
+ return xerrors.Errorf("failed to prepare COPY for temp_events: %w", err)
+ }
+
+ for _, e := range eventEntries {
+ if _, err := stmt.Exec(
+ eventTag,
+ e.EventId,
+ pgmodel.EventTypeToString(e.EventType),
+ e.BlockHeight,
+ e.BlockHash,
+ int(e.Tag),
+ e.BlockSkipped,
+ ); err != nil {
+ _ = stmt.Close()
+ return xerrors.Errorf("failed to buffer temp_events row: %w", err)
+ }
+ }
+
+ if _, err := stmt.Exec(); err != nil {
+ _ = stmt.Close()
+ return xerrors.Errorf("failed to finalize COPY temp_events: %w", err)
+ }
+ if err := stmt.Close(); err != nil {
+ return xerrors.Errorf("failed to close COPY statement: %w", err)
+ }
+
+ // Ensure skipped block_metadata rows exist in bulk
+ if _, err := tx.ExecContext(ctx, `
+ INSERT INTO block_metadata (height, tag, hash, parent_hash, parent_height, object_key_main, timestamp, skipped)
+ SELECT DISTINCT e.height, e.bm_tag, NULL, NULL, 0, NULL, 0, true
+ FROM temp_events e
+ WHERE e.skipped = true
+ ON CONFLICT (tag, height) WHERE skipped = true DO NOTHING
+ `); err != nil {
+ return xerrors.Errorf("failed to upsert skipped block_metadata: %w", err)
+ }
+
+ // Insert non-skipped events by joining on (tag, hash) with fallback for DefaultBlockTag via UNION ALL
+ if _, err := tx.ExecContext(ctx, `
+ INSERT INTO block_events (event_tag, event_sequence, event_type, block_metadata_id, height, hash)
+ SELECT e.event_tag, e.event_sequence, e.event_type::event_type_enum, bm.id, e.height, e.hash
+ FROM temp_events e
+ JOIN block_metadata bm
+ ON bm.tag = e.bm_tag AND bm.hash = e.hash AND bm.skipped = false
+ WHERE e.skipped = false
+ UNION ALL
+ SELECT e.event_tag, e.event_sequence, e.event_type::event_type_enum, bm.id, e.height, e.hash
+ FROM temp_events e
+ JOIN block_metadata bm
+ ON e.bm_tag = $1 AND bm.tag = 0 AND bm.hash = e.hash AND bm.skipped = false
+ WHERE e.skipped = false
+ ON CONFLICT (event_tag, event_sequence) DO NOTHING
+ `, model.DefaultBlockTag); err != nil {
+ return xerrors.Errorf("failed to insert non-skipped events: %w", err)
+ }
+
+ // Insert skipped events by joining on (tag, height, skipped=true) with fallback for DefaultBlockTag via UNION ALL
+ if _, err := tx.ExecContext(ctx, `
+ INSERT INTO block_events (event_tag, event_sequence, event_type, block_metadata_id, height, hash)
+ SELECT e.event_tag, e.event_sequence, e.event_type::event_type_enum, bm.id, e.height, e.hash
+ FROM temp_events e
+ JOIN block_metadata bm
+ ON bm.tag = e.bm_tag AND bm.height = e.height AND bm.skipped = true
+ WHERE e.skipped = true
+ UNION ALL
+ SELECT e.event_tag, e.event_sequence, e.event_type::event_type_enum, bm.id, e.height, e.hash
+ FROM temp_events e
+ JOIN block_metadata bm
+ ON e.bm_tag = $1 AND bm.tag = 0 AND bm.height = e.height AND bm.skipped = true
+ WHERE e.skipped = true
+ ON CONFLICT (event_tag, event_sequence) DO NOTHING
+ `, model.DefaultBlockTag); err != nil {
+ return xerrors.Errorf("failed to insert skipped events: %w", err)
+ }
+
+ if err := tx.Commit(); err != nil {
+ return xerrors.Errorf("failed to commit transaction: %w", err)
+ }
+ committed = true
+ return nil
+ })
+}
+
+func (e *eventStorageImpl) GetEventByEventId(ctx context.Context, eventTag uint32, eventId int64) (*model.EventEntry, error) {
+ return e.instrumentGetEventByEventId.Instrument(ctx, func(ctx context.Context) (*model.EventEntry, error) {
+ var eventEntry model.EventEntry
+ var eventTypeStr string
+ var blockHash sql.NullString
+ var tag sql.NullInt32
+ var parentHash sql.NullString
+ var skipped sql.NullBool
+ var timestamp sql.NullInt64
+
+ err := e.db.QueryRowContext(ctx, `
+ SELECT be.event_sequence, be.event_type, be.height, be.hash,
+ bm.tag, bm.parent_hash, bm.skipped, bm.timestamp, be.event_tag
+ FROM block_events be
+ LEFT JOIN block_metadata bm ON be.block_metadata_id = bm.id
+ WHERE be.event_tag = $1 AND be.event_sequence = $2
+ `, eventTag, eventId).Scan(
+ &eventEntry.EventId,
+ &eventTypeStr,
+ &eventEntry.BlockHeight,
+ &blockHash,
+ &tag,
+ &parentHash,
+ &skipped,
+ ×tamp,
+ &eventEntry.EventTag,
+ )
+
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return nil, errors.ErrItemNotFound
+ }
+ return nil, xerrors.Errorf("failed to get event by event id: %w", err)
+ }
+
+ // Handle null values from LEFT JOIN
+ if blockHash.Valid {
+ eventEntry.BlockHash = blockHash.String
+ } else {
+ eventEntry.BlockHash = ""
+ }
+ if tag.Valid {
+ eventEntry.Tag = uint32(tag.Int32)
+ } else {
+ eventEntry.Tag = model.DefaultBlockTag
+ }
+ if parentHash.Valid {
+ eventEntry.ParentHash = parentHash.String
+ } else {
+ eventEntry.ParentHash = ""
+ }
+ if skipped.Valid {
+ eventEntry.BlockSkipped = skipped.Bool
+ } else {
+ eventEntry.BlockSkipped = false
+ }
+ if timestamp.Valid {
+ eventEntry.BlockTimestamp = timestamp.Int64
+ } else {
+ eventEntry.BlockTimestamp = 0
+ }
+
+ // switch to defaultTag is not set
+ if eventEntry.Tag == 0 {
+ eventEntry.Tag = model.DefaultBlockTag
+ }
+
+ eventEntry.EventType = pgmodel.ParseEventType(eventTypeStr)
+ return &eventEntry, nil
+ })
+}
+
+func (e *eventStorageImpl) GetEventsAfterEventId(ctx context.Context, eventTag uint32, eventId int64, maxEvents uint64) ([]*model.EventEntry, error) {
+ return e.instrumentGetEventsAfterEventId.Instrument(ctx, func(ctx context.Context) ([]*model.EventEntry, error) {
+ rows, err := e.db.QueryContext(ctx, `
+ SELECT be.event_sequence, be.event_type, be.height, be.hash, bm.tag, bm.parent_hash,
+ bm.skipped, bm.timestamp, be.event_tag
+ FROM block_events be
+ LEFT JOIN block_metadata bm ON be.block_metadata_id = bm.id
+ WHERE be.event_tag = $1 AND be.event_sequence > $2
+ ORDER BY be.event_sequence ASC
+ LIMIT $3
+ `, eventTag, eventId, maxEvents)
+
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get events after event id: %w", err)
+ }
+
+ var result []*model.EventEntry
+ var scanErr error
+ defer func() {
+ if closeErr := rows.Close(); closeErr != nil && scanErr == nil {
+ scanErr = xerrors.Errorf("failed to close rows: %w", closeErr)
+ }
+ }()
+
+ result, scanErr = e.scanEventEntries(rows)
+ return result, scanErr
+ })
+}
+
+func (e *eventStorageImpl) GetEventsByEventIdRange(ctx context.Context, eventTag uint32, minEventId int64, maxEventId int64) ([]*model.EventEntry, error) {
+ return e.instrumentGetEventsByEventIdRange.Instrument(ctx, func(ctx context.Context) ([]*model.EventEntry, error) {
+ rows, err := e.db.QueryContext(ctx, `
+ SELECT be.event_sequence, be.event_type, be.height, be.hash, bm.tag, bm.parent_hash,
+ bm.skipped, bm.timestamp, be.event_tag
+ FROM block_events be
+ LEFT JOIN block_metadata bm ON be.block_metadata_id = bm.id
+ WHERE be.event_tag = $1 AND be.event_sequence >= $2 AND be.event_sequence < $3
+ ORDER BY be.event_sequence ASC
+ `, eventTag, minEventId, maxEventId)
+
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get events by event id range: %w", err)
+ }
+
+ var events []*model.EventEntry
+ var scanErr error
+ defer func() {
+ if closeErr := rows.Close(); closeErr != nil && scanErr == nil && events != nil {
+ scanErr = xerrors.Errorf("failed to close rows: %w", closeErr)
+ }
+ }()
+
+ events, scanErr = e.scanEventEntries(rows)
+ if scanErr != nil {
+ return nil, scanErr
+ }
+
+ // Validate that we have all events in the range
+ expectedCount := maxEventId - minEventId
+ if int64(len(events)) != expectedCount {
+ return nil, errors.ErrItemNotFound
+ }
+
+ // Check for close error one more time
+ if scanErr != nil {
+ return nil, scanErr
+ }
+
+ return events, nil
+ })
+}
+
+func (e *eventStorageImpl) GetMaxEventId(ctx context.Context, eventTag uint32) (int64, error) {
+ return e.instrumentGetMaxEventId.Instrument(ctx, func(ctx context.Context) (int64, error) {
+ var maxEventId sql.NullInt64
+ err := e.db.QueryRowContext(ctx, `
+ SELECT MAX(event_sequence) FROM block_events WHERE event_tag = $1
+ `, eventTag).Scan(&maxEventId) //watermark
+ if err != nil {
+ return 0, xerrors.Errorf("failed to get max event id: %w", err)
+ }
+ if !maxEventId.Valid {
+ return 0, errors.ErrNoEventHistory
+ }
+ return maxEventId.Int64, nil
+ })
+}
+
+// basically if we have events 1,2,3,4,5,6,7 and call SetMaxEventId(ctx, eventTag, 4), then we will delete all events after 4
+func (e *eventStorageImpl) SetMaxEventId(ctx context.Context, eventTag uint32, maxEventId int64) error {
+ return e.instrumentSetMaxEventId.Instrument(ctx, func(ctx context.Context) error {
+ if maxEventId < model.EventIdStartValue && maxEventId != model.EventIdDeleted {
+ return xerrors.Errorf("invalid max event id: %d", maxEventId)
+ }
+
+ txCtx, cancel := context.WithTimeout(ctx, 60*time.Second)
+ defer cancel()
+
+ tx, err := e.db.BeginTx(txCtx, nil)
+ if err != nil {
+ return xerrors.Errorf("failed to start transaction: %w", err)
+ }
+ committed := false
+ defer func() {
+ if !committed {
+ if rollbackErr := tx.Rollback(); rollbackErr != nil {
+ // Log the rollback error but don't override the original error
+ _ = rollbackErr
+ }
+ }
+ }()
+
+ if maxEventId == model.EventIdDeleted {
+ // Delete all events for this tag
+ _, err = tx.ExecContext(txCtx, `
+ DELETE FROM block_events WHERE event_tag = $1
+ `, eventTag)
+ if err != nil {
+ return xerrors.Errorf("failed to delete events: %w", err)
+ }
+ } else {
+ // Validate the new max event ID exists
+ var exists bool
+ err = tx.QueryRowContext(txCtx, `
+ SELECT EXISTS(SELECT 1 FROM block_events WHERE event_tag = $1 AND event_sequence = $2)
+ `, eventTag, maxEventId).Scan(&exists)
+ if err != nil {
+ return xerrors.Errorf("failed to validate max event id: %w", err)
+ }
+ if !exists {
+ return xerrors.Errorf("event entry with max event id %d does not exist", maxEventId)
+ }
+ // Delete events beyond the max event ID
+ _, err = tx.ExecContext(txCtx, `
+ DELETE FROM block_events WHERE event_tag = $1 AND event_sequence > $2
+ `, eventTag, maxEventId)
+ if err != nil {
+ return xerrors.Errorf("failed to delete events beyond max event id: %w", err)
+ }
+ }
+
+ err = tx.Commit()
+ if err != nil {
+ return xerrors.Errorf("failed to commit transaction: %w", err)
+ }
+ committed = true
+ return nil
+ })
+}
+
+func (e *eventStorageImpl) GetFirstEventIdByBlockHeight(ctx context.Context, eventTag uint32, blockHeight uint64) (int64, error) {
+ return e.instrumentGetFirstEventIdByBlockHeight.Instrument(ctx, func(ctx context.Context) (int64, error) {
+ var firstEventId int64
+
+ err := e.db.QueryRowContext(ctx, `
+ SELECT MIN(be.event_sequence)
+ FROM block_events be
+ WHERE be.event_tag = $1 AND be.height = $2
+ `, eventTag, blockHeight).Scan(&firstEventId)
+
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return 0, errors.ErrItemNotFound
+ }
+ return 0, xerrors.Errorf("failed to get first event id by block height: %w", err)
+ }
+
+ return firstEventId, nil
+ })
+}
+
+func (e *eventStorageImpl) GetEventsByBlockHeight(ctx context.Context, eventTag uint32, blockHeight uint64) ([]*model.EventEntry, error) {
+ return e.instrumentGetEventsByBlockHeight.Instrument(ctx, func(ctx context.Context) ([]*model.EventEntry, error) {
+ rows, err := e.db.QueryContext(ctx, `
+ SELECT be.event_sequence, be.event_type, be.height, be.hash, bm.tag, bm.parent_hash,
+ bm.skipped, bm.timestamp, be.event_tag
+ FROM block_events be
+ LEFT JOIN block_metadata bm ON be.block_metadata_id = bm.id
+ WHERE be.event_tag = $1 AND be.height = $2
+ ORDER BY be.event_sequence ASC
+ `, eventTag, blockHeight)
+
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get events by block height: %w", err)
+ }
+
+ var events []*model.EventEntry
+ var scanErr error
+ defer func() {
+ if closeErr := rows.Close(); closeErr != nil && scanErr == nil {
+ scanErr = xerrors.Errorf("failed to close rows: %w", closeErr)
+ }
+ }()
+
+ events, scanErr = e.scanEventEntries(rows)
+ if scanErr != nil {
+ return nil, scanErr
+ }
+
+ if len(events) == 0 {
+ return nil, errors.ErrItemNotFound
+ }
+
+ // Check for close error one more time
+ if scanErr != nil {
+ return nil, scanErr
+ }
+
+ return events, nil
+ })
+}
+
+// Helper functions
+func (e *eventStorageImpl) getOrCreateBlockMetadataId(ctx context.Context, tx *sql.Tx, eventEntry *model.EventEntry) (int64, error) {
+ // For skipped blocks, create or find block metadata with specific fields
+ if eventEntry.BlockSkipped {
+ // Try to find existing block metadata for this skipped event
+ var blockMetadataId int64
+ err := tx.QueryRowContext(ctx, `
+ SELECT id FROM block_metadata WHERE tag = $1 AND height = $2 AND skipped = true
+ `, eventEntry.Tag, eventEntry.BlockHeight).Scan(&blockMetadataId)
+ if err == nil {
+ return blockMetadataId, nil
+ }
+ // If not found and eventEntry.Tag is DefaultBlockTag, try with tag = 0
+ if err == sql.ErrNoRows && eventEntry.Tag == model.DefaultBlockTag {
+ err = tx.QueryRowContext(ctx, `
+ SELECT id FROM block_metadata WHERE tag = $1 AND height = $2 AND skipped = true
+ `, uint32(0), eventEntry.BlockHeight).Scan(&blockMetadataId)
+
+ if err == nil {
+ return blockMetadataId, nil
+ }
+ }
+
+ // If block metadata not found for skipped event, create it
+ if err == sql.ErrNoRows {
+ return e.createSkippedBlockMetadata(ctx, tx, eventEntry)
+ }
+ return 0, xerrors.Errorf("failed to query block metadata: %w", err)
+ }
+
+ // For non-skipped blocks, look up by tag and hash
+ // First try with the eventEntry.Tag
+ var blockMetadataId int64
+ err := tx.QueryRowContext(ctx, ` SELECT id FROM block_metadata WHERE tag = $1 AND hash = $2
+ `, eventEntry.Tag, eventEntry.BlockHash).Scan(&blockMetadataId)
+ if err == nil {
+ return blockMetadataId, nil
+ }
+ // If not found and eventEntry.Tag is DefaultBlockTag, try with tag = 0
+ if err == sql.ErrNoRows && eventEntry.Tag == model.DefaultBlockTag {
+ err = tx.QueryRowContext(ctx, `
+ SELECT id FROM block_metadata WHERE tag = $1 AND hash = $2
+ `, uint32(0), eventEntry.BlockHash).Scan(&blockMetadataId)
+
+ if err == nil {
+ return blockMetadataId, nil
+ }
+ }
+
+ // If we get here, the block metadata was not found
+ if err == sql.ErrNoRows {
+ return 0, xerrors.Errorf("block metadata not found for tag %d and hash %s", eventEntry.Tag, eventEntry.BlockHash)
+ }
+ return 0, xerrors.Errorf("failed to query block metadata: %w", err)
+}
+
+// createSkippedBlockMetadata creates a new block_metadata entry for a skipped block
+func (e *eventStorageImpl) createSkippedBlockMetadata(ctx context.Context, tx *sql.Tx, eventEntry *model.EventEntry) (int64, error) {
+ // Create block metadata for skipped block with NULL values as specified
+ var blockMetadataId int64
+ err := tx.QueryRowContext(ctx, `
+ INSERT INTO block_metadata (height, tag, hash, parent_hash, parent_height, object_key_main, timestamp, skipped)
+ VALUES ($1, $2, NULL, NULL, $3, NULL, $4, true)
+ ON CONFLICT (tag, height) WHERE skipped = true DO UPDATE SET
+ hash = EXCLUDED.hash,
+ parent_hash = EXCLUDED.parent_hash,
+ parent_height = EXCLUDED.parent_height,
+ object_key_main = EXCLUDED.object_key_main,
+ timestamp = EXCLUDED.timestamp,
+ skipped = EXCLUDED.skipped
+ RETURNING id
+ `, eventEntry.BlockHeight, eventEntry.Tag, 0, 0).Scan(&blockMetadataId)
+ if err != nil {
+ return 0, xerrors.Errorf("failed to create block metadata for skipped block: %w", err)
+ }
+
+ return blockMetadataId, nil
+}
+
+func (e *eventStorageImpl) scanEventEntries(rows *sql.Rows) ([]*model.EventEntry, error) {
+ var events []*model.EventEntry
+
+ for rows.Next() {
+ var eventEntry model.EventEntry
+ var eventTypeStr string
+ var blockHash sql.NullString
+ var tag sql.NullInt32
+ var parentHash sql.NullString
+ var skipped sql.NullBool
+ var timestamp sql.NullInt64
+
+ err := rows.Scan(
+ &eventEntry.EventId,
+ &eventTypeStr,
+ &eventEntry.BlockHeight,
+ &blockHash,
+ &tag,
+ &parentHash,
+ &skipped,
+ ×tamp,
+ &eventEntry.EventTag,
+ )
+
+ if err != nil {
+ return nil, xerrors.Errorf("failed to scan event entry: %w", err)
+ }
+
+ // Handle null values from LEFT JOIN
+ if blockHash.Valid {
+ eventEntry.BlockHash = blockHash.String
+ } else {
+ eventEntry.BlockHash = ""
+ }
+ if tag.Valid {
+ eventEntry.Tag = uint32(tag.Int32)
+ } else {
+ eventEntry.Tag = model.DefaultBlockTag
+ }
+ if parentHash.Valid {
+ eventEntry.ParentHash = parentHash.String
+ } else {
+ eventEntry.ParentHash = ""
+ }
+ if skipped.Valid {
+ eventEntry.BlockSkipped = skipped.Bool
+ } else {
+ eventEntry.BlockSkipped = false
+ }
+ if timestamp.Valid {
+ eventEntry.BlockTimestamp = timestamp.Int64
+ } else {
+ eventEntry.BlockTimestamp = 0
+ }
+
+ // switch to defaultTag is not set
+ if eventEntry.Tag == 0 {
+ eventEntry.Tag = model.DefaultBlockTag
+ }
+
+ eventEntry.EventType = pgmodel.ParseEventType(eventTypeStr)
+ events = append(events, &eventEntry)
+ }
+
+ if err := rows.Err(); err != nil {
+ return nil, xerrors.Errorf("error iterating over rows: %w", err)
+ }
+
+ return events, nil
+}
diff --git a/internal/storage/metastorage/postgres/event_storage_integration_test.go b/internal/storage/metastorage/postgres/event_storage_integration_test.go
new file mode 100644
index 00000000..c29f48b5
--- /dev/null
+++ b/internal/storage/metastorage/postgres/event_storage_integration_test.go
@@ -0,0 +1,612 @@
+package postgres
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/fx"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/internal/errors"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/internal"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/model"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type eventStorageTestSuite struct {
+ suite.Suite
+ accessor internal.MetaStorage
+ config *config.Config
+ tag uint32
+ eventTag uint32
+ db *sql.DB
+}
+
+func (s *eventStorageTestSuite) SetupSuite() {
+ require := testutil.Require(s.T())
+ cfg, err := config.New()
+ require.NoError(err)
+ if !cfg.IsIntegrationTest() || cfg.AWS.Postgres == nil {
+ return
+ }
+
+ db, err := newDBConnection(context.Background(), cfg.AWS.Postgres)
+ require.NoError(err)
+ defer db.Close()
+ require.NoError(runMigrations(context.Background(), db))
+}
+
+func (s *eventStorageTestSuite) SetupTest() {
+ require := testutil.Require(s.T())
+ var accessor internal.MetaStorage
+ cfg, err := config.New()
+ require.NoError(err)
+
+ // Skip tests if Postgres is not configured
+ if cfg.AWS.Postgres == nil {
+ s.T().Skip("Postgres not configured, skipping test suite")
+ return
+ }
+
+ app := testapp.New(
+ s.T(),
+ fx.Provide(NewMetaStorage),
+ testapp.WithIntegration(),
+ testapp.WithConfig(s.config),
+ fx.Populate(&accessor),
+ )
+ defer app.Close()
+ s.accessor = accessor
+ s.tag = 1
+ s.eventTag = 0
+
+ // Get database connection for cleanup
+ db, err := newDBConnection(context.Background(), cfg.AWS.Postgres)
+ require.NoError(err)
+ s.db = db
+}
+
+func (s *eventStorageTestSuite) TearDownTest() {
+ if s.db != nil {
+ ctx := context.Background()
+ s.T().Log("Clearing database tables after test")
+ // Clear all tables in reverse order due to foreign key constraints
+ tables := []string{"block_events", "block_consolidation_shadow", "canonical_blocks", "block_metadata"}
+ for _, table := range tables {
+ _, err := s.db.ExecContext(ctx, fmt.Sprintf("DELETE FROM %s", table))
+ if err != nil {
+ s.T().Logf("Failed to clear table %s: %v", table, err)
+ }
+ }
+ }
+}
+
+func (s *eventStorageTestSuite) TearDownSuite() {
+ if s.db != nil {
+ s.db.Close()
+ }
+}
+func (s *eventStorageTestSuite) addEvents(eventTag uint32, startHeight uint64, numEvents uint64, tag uint32) {
+ // First, add block metadata for the events
+ blockMetas := testutil.MakeBlockMetadatasFromStartHeight(startHeight, int(numEvents), tag)
+ ctx := context.TODO()
+ err := s.accessor.PersistBlockMetas(ctx, true, blockMetas, nil)
+ if err != nil {
+ panic(err)
+ }
+
+ // Then add events
+ blockEvents := testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, startHeight, startHeight+numEvents, tag)
+ err = s.accessor.AddEvents(ctx, eventTag, blockEvents)
+ if err != nil {
+ panic(err)
+ }
+}
+func (s *eventStorageTestSuite) verifyEvents(eventTag uint32, numEvents uint64, tag uint32) {
+ require := testutil.Require(s.T())
+ ctx := context.TODO()
+
+ watermark, err := s.accessor.GetMaxEventId(ctx, eventTag)
+ if err != nil {
+ panic(err)
+ }
+ require.Equal(watermark-model.EventIdStartValue, int64(numEvents-1))
+
+ // fetch range with missing item
+ _, err = s.accessor.GetEventsByEventIdRange(ctx, eventTag, model.EventIdStartValue, model.EventIdStartValue+int64(numEvents+100))
+ require.Error(err)
+ require.True(xerrors.Is(err, errors.ErrItemNotFound))
+
+ // fetch valid range
+ fetchedEvents, err := s.accessor.GetEventsByEventIdRange(ctx, eventTag, model.EventIdStartValue, model.EventIdStartValue+int64(numEvents))
+ if err != nil {
+ panic(err)
+ }
+ require.NotNil(fetchedEvents)
+ require.Equal(uint64(len(fetchedEvents)), numEvents)
+
+ numFollowingEventsToFetch := uint64(10)
+ for i, event := range fetchedEvents {
+ require.Equal(int64(i)+model.EventIdStartValue, event.EventId)
+ require.Equal(uint64(i), event.BlockHeight)
+ require.Equal(api.BlockchainEvent_BLOCK_ADDED, event.EventType)
+ require.Equal(tag, event.Tag)
+ require.Equal(eventTag, event.EventTag)
+
+ expectedNumEvents := numFollowingEventsToFetch
+ if uint64(event.EventId)+numFollowingEventsToFetch >= numEvents {
+ expectedNumEvents = numEvents - 1 - uint64(event.EventId-model.EventIdStartValue)
+ }
+ followingEvents, err := s.accessor.GetEventsAfterEventId(ctx, eventTag, event.EventId, numFollowingEventsToFetch)
+ if err != nil {
+ panic(err)
+ }
+ require.Equal(uint64(len(followingEvents)), expectedNumEvents)
+ for j, followingEvent := range followingEvents {
+ require.Equal(int64(i+j+1)+model.EventIdStartValue, followingEvent.EventId)
+ require.Equal(uint64(i+j+1), followingEvent.BlockHeight)
+ require.Equal(api.BlockchainEvent_BLOCK_ADDED, followingEvent.EventType)
+ require.Equal(eventTag, followingEvent.EventTag)
+ }
+ }
+}
+
+func (s *eventStorageTestSuite) TestSetMaxEventId() {
+ require := testutil.Require(s.T())
+ ctx := context.TODO()
+ numEvents := uint64(100)
+ s.addEvents(s.eventTag, 0, numEvents, s.tag)
+ watermark, err := s.accessor.GetMaxEventId(ctx, s.eventTag)
+ require.NoError(err)
+ require.Equal(model.EventIdStartValue+int64(numEvents-1), watermark)
+
+ // reset it to a new value
+ newEventId := int64(5)
+ err = s.accessor.SetMaxEventId(ctx, s.eventTag, newEventId)
+ require.NoError(err)
+ watermark, err = s.accessor.GetMaxEventId(ctx, s.eventTag)
+ require.NoError(err)
+ require.Equal(watermark, newEventId)
+
+ // reset it to invalid value
+ invalidEventId := int64(-1)
+ err = s.accessor.SetMaxEventId(ctx, s.eventTag, invalidEventId)
+ require.Error(err)
+
+ // reset it to value bigger than current max
+ invalidEventId = newEventId + 10
+ err = s.accessor.SetMaxEventId(ctx, s.eventTag, invalidEventId)
+ require.Error(err)
+
+ // reset it to EventIdDeleted
+ err = s.accessor.SetMaxEventId(ctx, s.eventTag, model.EventIdDeleted)
+ require.NoError(err)
+ _, err = s.accessor.GetMaxEventId(ctx, s.eventTag)
+ require.Error(err)
+ require.Equal(errors.ErrNoEventHistory, err)
+}
+
+func (s *eventStorageTestSuite) TestSetMaxEventIdNonDefaultEventTag() {
+ require := testutil.Require(s.T())
+ ctx := context.TODO()
+ numEvents := uint64(100)
+ eventTag := uint32(1)
+ s.addEvents(eventTag, 0, numEvents, s.tag)
+ watermark, err := s.accessor.GetMaxEventId(ctx, eventTag)
+ require.NoError(err)
+ require.Equal(model.EventIdStartValue+int64(numEvents-1), watermark)
+
+ // reset it to a new value
+ newEventId := int64(5)
+ err = s.accessor.SetMaxEventId(ctx, eventTag, newEventId)
+ require.NoError(err)
+ watermark, err = s.accessor.GetMaxEventId(ctx, eventTag)
+ require.NoError(err)
+ require.Equal(watermark, newEventId)
+
+ // reset it to invalid value
+ invalidEventId := int64(-1)
+ err = s.accessor.SetMaxEventId(ctx, eventTag, invalidEventId)
+ require.Error(err)
+
+ // reset it to value bigger than current max
+ invalidEventId = newEventId + 10
+ err = s.accessor.SetMaxEventId(ctx, eventTag, invalidEventId)
+ require.Error(err)
+
+ // reset it to EventIdDeleted
+ err = s.accessor.SetMaxEventId(ctx, eventTag, model.EventIdDeleted)
+ require.NoError(err)
+ _, err = s.accessor.GetMaxEventId(ctx, eventTag)
+ require.Error(err)
+ require.Equal(errors.ErrNoEventHistory, err)
+}
+
+////////////////////////////////////////////////////////////
+
+func (s *eventStorageTestSuite) TestAddEvents() {
+ numEvents := uint64(100)
+ s.addEvents(s.eventTag, 0, numEvents, s.tag)
+ s.verifyEvents(s.eventTag, numEvents, s.tag)
+}
+
+func (s *eventStorageTestSuite) TestAddEventsNonDefaultEventTag() {
+ numEvents := uint64(100)
+ s.addEvents(uint32(1), 0, numEvents, s.tag)
+ s.verifyEvents(uint32(1), numEvents, s.tag)
+}
+
+func (s *eventStorageTestSuite) TestAddEventsDefaultTag() {
+ numEvents := uint64(100)
+ s.addEvents(s.eventTag, 0, numEvents, 0)
+ s.verifyEvents(s.eventTag, numEvents, model.DefaultBlockTag)
+}
+
+func (s *eventStorageTestSuite) TestAddEventsNonDefaultTag() {
+ numEvents := uint64(100)
+ s.addEvents(s.eventTag, 0, numEvents, 2)
+ s.verifyEvents(s.eventTag, numEvents, 2)
+}
+
+func (s *eventStorageTestSuite) TestAddEventsMultipleTimes() {
+ numEvents := uint64(100)
+ s.addEvents(s.eventTag, 0, numEvents, s.tag)
+ s.addEvents(s.eventTag, numEvents, numEvents, s.tag)
+ numEvents = numEvents * 2
+ s.verifyEvents(s.eventTag, numEvents, s.tag)
+}
+
+func (s *eventStorageTestSuite) TestAddEventsMultipleTimesNonDefaultEventTag() {
+ numEvents := uint64(100)
+ eventTag := uint32(1)
+ s.addEvents(eventTag, 0, numEvents, s.tag)
+ s.addEvents(eventTag, numEvents, numEvents, s.tag)
+ numEvents = numEvents * 2
+ s.verifyEvents(eventTag, numEvents, s.tag)
+}
+
+////////////////////////////////////////////////////////////
+
+func (s *eventStorageTestSuite) TestAddEventsDiscontinuousChain_NotSkipped() {
+ require := testutil.Require(s.T())
+ numEvents := uint64(100)
+
+ // First, add block metadata for the initial events
+ blockMetas := testutil.MakeBlockMetadatasFromStartHeight(0, int(numEvents), s.tag)
+ ctx := context.TODO()
+ err := s.accessor.PersistBlockMetas(ctx, true, blockMetas, nil)
+ require.NoError(err)
+
+ blockEvents := testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, 0, numEvents, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ if err != nil {
+ panic(err)
+ }
+
+ // Add block metadata for the additional events that will be tested
+ additionalBlockMetas := testutil.MakeBlockMetadatasFromStartHeight(numEvents, 10, s.tag)
+ err = s.accessor.PersistBlockMetas(ctx, true, additionalBlockMetas, nil)
+ require.NoError(err)
+
+ // have add event for height numEvents-1 again, invalid
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents-1, numEvents+4, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.Error(err)
+
+ // missing event for height numEvents, invalid
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+2, numEvents+7, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.Error(err)
+
+ // hash mismatch, invalid
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+2, numEvents+7, s.tag, testutil.WithBlockHashFormat("HashMismatch0x%s"))
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.Error(err)
+
+ // continuous, should be able to add them
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents, numEvents+7, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+}
+
+func (s *eventStorageTestSuite) TestAddEventsDiscontinuousChain_Skipped() {
+ require := testutil.Require(s.T())
+ numEvents := uint64(100)
+ ctx := context.TODO()
+
+ // Use the helper method to create initial events
+ s.addEvents(s.eventTag, 0, numEvents, s.tag)
+
+ // Create block metadata ONLY for non-skipped heights
+ // Heights 101, 102, 106, 107, 108, 109 will have block metadata (non-skipped)
+ // Heights 100, 103, 104, 105 will be skipped (automatically created as skipped)
+ nonSkippedBlockMetas := []*api.BlockMetadata{
+ testutil.MakeBlockMetadata(numEvents+1, s.tag), // height 101
+ testutil.MakeBlockMetadata(numEvents+2, s.tag), // height 102
+ testutil.MakeBlockMetadata(numEvents+6, s.tag), // height 106
+ testutil.MakeBlockMetadata(numEvents+7, s.tag), // height 107
+ testutil.MakeBlockMetadata(numEvents+8, s.tag), // height 108
+ testutil.MakeBlockMetadata(numEvents+9, s.tag), // height 109
+ }
+
+ // Set proper parent relationships for blocks that come after gaps
+ // Block 106 should point to block 102 (since 103, 104, 105 are skipped/missing)
+ nonSkippedBlockMetas[2].ParentHeight = nonSkippedBlockMetas[1].Height // 106 -> 102
+ nonSkippedBlockMetas[2].ParentHash = nonSkippedBlockMetas[1].Hash
+
+ // Block 107 should point to block 106
+ nonSkippedBlockMetas[3].ParentHeight = nonSkippedBlockMetas[2].Height // 107 -> 106
+ nonSkippedBlockMetas[3].ParentHash = nonSkippedBlockMetas[2].Hash
+
+ // Block 108 should point to block 107
+ nonSkippedBlockMetas[4].ParentHeight = nonSkippedBlockMetas[3].Height // 108 -> 107
+ nonSkippedBlockMetas[4].ParentHash = nonSkippedBlockMetas[3].Hash
+
+ // Block 109 should point to block 108
+ nonSkippedBlockMetas[5].ParentHeight = nonSkippedBlockMetas[4].Height // 109 -> 108
+ nonSkippedBlockMetas[5].ParentHash = nonSkippedBlockMetas[4].Hash
+
+ err := s.accessor.PersistBlockMetas(ctx, true, nonSkippedBlockMetas, nil)
+ require.NoError(err)
+
+ // Test case: chain normal growing case, [+0(skipped), +1]
+ // Height 100 is skipped, height 101 is normal
+ blockEvents := testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents, numEvents+1, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+1, numEvents+2, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ // Test case: chain normal growing case, +0(skipped), +1, [+2, +3(skipped)]
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+2, numEvents+3, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+3, numEvents+4, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ // Test case: chain normal growing case, +0(skipped), +1, +2, +3(skipped), [+4(skipped), +5(skipped)]
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+4, numEvents+5, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+5, numEvents+6, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ // Test case: rollback case, +6, +7, +8(skipped), [-8(skipped), -7]
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+6, numEvents+8, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+8, numEvents+9, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_REMOVED, numEvents+8, numEvents+9, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_REMOVED, numEvents+7, numEvents+8, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ // Test case: rollback case, +7(skipped), +8, [-8, -7(skipped)]
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+7, numEvents+8, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+8, numEvents+9, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_REMOVED, numEvents+8, numEvents+9, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_REMOVED, numEvents+7, numEvents+8, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ // Test case: rollback case, +7(skipped), +8(skipped), [-8(skipped), -7(skipped)]
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+7, numEvents+8, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_ADDED, numEvents+8, numEvents+9, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_REMOVED, numEvents+8, numEvents+9, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ blockEvents = testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_REMOVED, numEvents+7, numEvents+8, s.tag, testutil.WithBlockSkipped())
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ // Verify that skipped blocks created block_metadata entries but not canonical_blocks entries
+ s.verifySkippedBlockHandling(ctx, numEvents, s.tag)
+}
+
+// verifySkippedBlockHandling verifies that skipped blocks have block_metadata entries but not canonical_blocks entries
+func (s *eventStorageTestSuite) verifySkippedBlockHandling(ctx context.Context, numEvents uint64, tag uint32) {
+ require := testutil.Require(s.T())
+
+ // Check that skipped heights have block_metadata entries with skipped=true
+ skippedHeights := []uint64{numEvents, numEvents + 3, numEvents + 4, numEvents + 5}
+ for _, height := range skippedHeights {
+ var count int
+ err := s.db.QueryRowContext(ctx, `
+ SELECT COUNT(*) FROM block_metadata
+ WHERE tag = $1 AND height = $2 AND skipped = true AND hash IS NULL
+ `, tag, height).Scan(&count)
+ require.NoError(err)
+ require.Greater(count, 0, "Expected skipped block metadata for height %d", height)
+
+ // Verify that skipped blocks do NOT have canonical_blocks entries
+ err = s.db.QueryRowContext(ctx, `
+ SELECT COUNT(*) FROM canonical_blocks
+ WHERE tag = $1 AND height = $2
+ `, tag, height).Scan(&count)
+ require.NoError(err)
+ require.Equal(0, count, "Skipped blocks should not have canonical entries for height %d", height)
+ }
+
+ // Check that non-skipped heights have both block_metadata and canonical_blocks entries
+ nonSkippedHeights := []uint64{numEvents + 1, numEvents + 2, numEvents + 6, numEvents + 7, numEvents + 8, numEvents + 9}
+ for _, height := range nonSkippedHeights {
+ var count int
+ // Should have block_metadata entry with skipped=false
+ err := s.db.QueryRowContext(ctx, `
+ SELECT COUNT(*) FROM block_metadata
+ WHERE tag = $1 AND height = $2 AND skipped = false AND hash IS NOT NULL
+ `, tag, height).Scan(&count)
+ require.NoError(err)
+ require.Greater(count, 0, "Expected non-skipped block metadata for height %d", height)
+
+ // Should have canonical_blocks entry
+ err = s.db.QueryRowContext(ctx, `
+ SELECT COUNT(*) FROM canonical_blocks
+ WHERE tag = $1 AND height = $2
+ `, tag, height).Scan(&count)
+ require.NoError(err)
+ require.Greater(count, 0, "Expected canonical entry for non-skipped height %d", height)
+ }
+}
+
+////////////////////////////////////////////////////////////
+
+func (s *eventStorageTestSuite) TestGetFirstEventIdByBlockHeight() {
+ require := testutil.Require(s.T())
+ numEvents := uint64(100)
+ s.addEvents(s.eventTag, 0, numEvents, s.tag)
+
+ // add the remove events again so for each height, there should be two events
+ for i := int64(numEvents - 1); i >= 0; i-- {
+ // Add block metadata for the removal event
+ blockMetas := testutil.MakeBlockMetadatasFromStartHeight(uint64(i), 1, s.tag)
+ ctx := context.TODO()
+ err := s.accessor.PersistBlockMetas(ctx, true, blockMetas, nil)
+ if err != nil {
+ panic(err)
+ }
+
+ removeEvents := testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_REMOVED, uint64(i), uint64(i+1), s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, removeEvents)
+ if err != nil {
+ panic(err)
+ }
+ eventId, err := s.accessor.GetFirstEventIdByBlockHeight(ctx, s.eventTag, uint64(i))
+ if err != nil {
+ panic(err)
+ }
+ require.Equal(i+model.EventIdStartValue, eventId)
+ }
+}
+
+func (s *eventStorageTestSuite) TestGetFirstEventIdByBlockHeightNonDefaultEventTag() {
+ require := testutil.Require(s.T())
+ numEvents := uint64(100)
+ eventTag := uint32(1)
+ ctx := context.TODO()
+ s.addEvents(eventTag, 0, numEvents, s.tag)
+
+ // fetch event for blockHeight=0
+ eventId, err := s.accessor.GetFirstEventIdByBlockHeight(ctx, eventTag, uint64(0))
+ require.NoError(err)
+ require.Equal(eventId, model.EventIdStartValue)
+
+ // add the remove events again so for each height, there should be two events
+ for i := int64(numEvents - 1); i >= 0; i-- {
+ // Add block metadata for the removal event
+ blockMetas := testutil.MakeBlockMetadatasFromStartHeight(uint64(i), 1, s.tag)
+ err := s.accessor.PersistBlockMetas(ctx, true, blockMetas, nil)
+ require.NoError(err)
+
+ removeEvents := testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_REMOVED, uint64(i), uint64(i+1), s.tag)
+ err = s.accessor.AddEvents(ctx, eventTag, removeEvents)
+ require.NoError(err)
+ eventId, err := s.accessor.GetFirstEventIdByBlockHeight(ctx, eventTag, uint64(i))
+ require.NoError(err)
+ require.Equal(i+model.EventIdStartValue, eventId)
+ }
+}
+
+func (s *eventStorageTestSuite) TestGetEventByEventId() {
+ const (
+ eventId = int64(10)
+ numEvents = uint64(20)
+ )
+
+ require := testutil.Require(s.T())
+ ctx := context.TODO()
+
+ s.addEvents(s.eventTag, 0, numEvents, s.tag)
+
+ event, err := s.accessor.GetEventByEventId(ctx, s.eventTag, eventId)
+ require.NoError(err)
+ require.Equal(event.EventId, eventId)
+ require.Equal(event.BlockHeight, uint64(eventId-1))
+}
+
+func (s *eventStorageTestSuite) TestGetEventByEventId_InvalidEventId() {
+ const (
+ eventId = int64(30)
+ numEvents = uint64(20)
+ )
+
+ require := testutil.Require(s.T())
+ ctx := context.TODO()
+
+ s.addEvents(s.eventTag, 0, numEvents, s.tag)
+
+ _, err := s.accessor.GetEventByEventId(ctx, s.eventTag, eventId)
+ require.Error(err)
+}
+
+func (s *eventStorageTestSuite) TestGetEventsByBlockHeight() {
+ const (
+ blockHeight = uint64(19)
+ numEvents = uint64(20)
+ )
+
+ require := testutil.Require(s.T())
+ ctx := context.TODO()
+
+ // +0, +1, ..., +19, -19,
+ s.addEvents(s.eventTag, 0, numEvents, s.tag)
+
+ // Add block metadata for the removal event
+ blockMetas := testutil.MakeBlockMetadatasFromStartHeight(numEvents-1, 1, s.tag)
+ err := s.accessor.PersistBlockMetas(ctx, true, blockMetas, nil)
+ require.NoError(err)
+
+ blockEvents := testutil.MakeBlockEvents(api.BlockchainEvent_BLOCK_REMOVED, numEvents-1, numEvents, s.tag)
+ err = s.accessor.AddEvents(ctx, s.eventTag, blockEvents)
+ require.NoError(err)
+
+ events, err := s.accessor.GetEventsByBlockHeight(ctx, s.eventTag, blockHeight)
+ require.NoError(err)
+ require.Equal(2, len(events))
+ for _, event := range events {
+ require.Equal(blockHeight, event.BlockHeight)
+ }
+}
+
+func TestIntegrationEventStorageTestSuite(t *testing.T) {
+ require := testutil.Require(t)
+ // Test with eth-mainnet for stream version
+ cfg, err := config.New()
+ require.NoError(err)
+ suite.Run(t, &eventStorageTestSuite{config: cfg})
+}
diff --git a/internal/storage/metastorage/postgres/event_storage_test.go b/internal/storage/metastorage/postgres/event_storage_test.go
new file mode 100644
index 00000000..defade1f
--- /dev/null
+++ b/internal/storage/metastorage/postgres/event_storage_test.go
@@ -0,0 +1,103 @@
+package postgres
+
+import (
+ "testing"
+
+ pgmodel "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres/model"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestEventTypeToString(t *testing.T) {
+ require := testutil.Require(t)
+
+ tests := []struct {
+ name string
+ eventType api.BlockchainEvent_Type
+ expected string
+ }{
+ {
+ name: "BLOCK_ADDED",
+ eventType: api.BlockchainEvent_BLOCK_ADDED,
+ expected: "BLOCK_ADDED",
+ },
+ {
+ name: "BLOCK_REMOVED",
+ eventType: api.BlockchainEvent_BLOCK_REMOVED,
+ expected: "BLOCK_REMOVED",
+ },
+ {
+ name: "UNKNOWN",
+ eventType: api.BlockchainEvent_UNKNOWN,
+ expected: "UNKNOWN",
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ result := pgmodel.EventTypeToString(test.eventType)
+ require.Equal(test.expected, result)
+ })
+ }
+}
+
+func TestParseEventType(t *testing.T) {
+ require := testutil.Require(t)
+
+ tests := []struct {
+ name string
+ input string
+ expected api.BlockchainEvent_Type
+ }{
+ {
+ name: "BLOCK_ADDED",
+ input: "BLOCK_ADDED",
+ expected: api.BlockchainEvent_BLOCK_ADDED,
+ },
+ {
+ name: "BLOCK_REMOVED",
+ input: "BLOCK_REMOVED",
+ expected: api.BlockchainEvent_BLOCK_REMOVED,
+ },
+ {
+ name: "UNKNOWN",
+ input: "UNKNOWN",
+ expected: api.BlockchainEvent_UNKNOWN,
+ },
+ {
+ name: "Invalid string",
+ input: "INVALID_TYPE",
+ expected: api.BlockchainEvent_UNKNOWN,
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ result := pgmodel.ParseEventType(test.input)
+ require.Equal(test.expected, result)
+ })
+ }
+}
+
+func TestEventTypeConversion(t *testing.T) {
+ require := testutil.Require(t)
+
+ eventTypes := []api.BlockchainEvent_Type{
+ api.BlockchainEvent_BLOCK_ADDED,
+ api.BlockchainEvent_BLOCK_REMOVED,
+ api.BlockchainEvent_UNKNOWN,
+ }
+
+ for _, eventType := range eventTypes {
+ t.Run(eventType.String(), func(t *testing.T) {
+ // Convert to string
+ eventTypeStr := pgmodel.EventTypeToString(eventType)
+
+ // Convert back to enum
+ convertedEventType := pgmodel.ParseEventType(eventTypeStr)
+
+ // Should be the same
+ require.Equal(eventType, convertedEventType)
+ })
+ }
+}
diff --git a/internal/storage/metastorage/postgres/meta_storage.go b/internal/storage/metastorage/postgres/meta_storage.go
new file mode 100644
index 00000000..d3e9dd06
--- /dev/null
+++ b/internal/storage/metastorage/postgres/meta_storage.go
@@ -0,0 +1,91 @@
+package postgres
+
+import (
+ "context"
+
+ "go.uber.org/fx"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/internal"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+)
+
+type (
+ metaStorageImpl struct {
+ internal.BlockStorage
+ internal.EventStorage
+ internal.TransactionStorage
+ }
+
+ Params struct {
+ fx.In
+ fxparams.Params
+ }
+
+ metaStorageFactory struct {
+ params Params
+ }
+)
+
+func NewMetaStorage(params Params) (internal.Result, error) {
+ // Use shared connection pool instead of creating new connections
+ pool, err := GetConnectionPool(context.Background(), params.Config.AWS.Postgres)
+ if err != nil {
+ return internal.Result{}, err
+ }
+
+ db := pool.DB()
+ if db == nil {
+ return internal.Result{}, xerrors.New("connection pool returned nil database connection")
+ }
+ // Create storage implementations with database connection
+ blockStorage, err := newBlockStorage(db, params)
+ if err != nil {
+ return internal.Result{}, err
+ }
+
+ eventStorage, err := newEventStorage(db, params)
+ if err != nil {
+ return internal.Result{}, err
+ }
+
+ transactionStorage, err := newTransactionStorage(db, params)
+ if err != nil {
+ return internal.Result{}, err
+ }
+
+ // Combine into meta storage
+ metaStorage := &metaStorageImpl{
+ BlockStorage: blockStorage,
+ EventStorage: eventStorage,
+ TransactionStorage: transactionStorage,
+ }
+
+ return internal.Result{
+ BlockStorage: blockStorage,
+ EventStorage: eventStorage,
+ TransactionStorage: transactionStorage,
+ MetaStorage: metaStorage,
+ }, nil
+}
+
+func (m *metaStorageImpl) AcquireSingleBlockUploadGuard(
+ ctx context.Context,
+ tag uint32,
+ height uint64,
+ hash string,
+) (internal.SingleBlockUploadGuard, error) {
+ storage, ok := m.BlockStorage.(internal.SingleBlockUploadGuardStorage)
+ if !ok {
+ return nil, xerrors.New("postgres block storage does not support single-block upload guards")
+ }
+ return storage.AcquireSingleBlockUploadGuard(ctx, tag, height, hash)
+}
+
+func (f *metaStorageFactory) Create() (internal.Result, error) {
+ return NewMetaStorage(f.params)
+}
+
+func NewFactory(params Params) internal.MetaStorageFactory {
+ return &metaStorageFactory{params}
+}
diff --git a/internal/storage/metastorage/postgres/migrator.go b/internal/storage/metastorage/postgres/migrator.go
new file mode 100644
index 00000000..b96c15de
--- /dev/null
+++ b/internal/storage/metastorage/postgres/migrator.go
@@ -0,0 +1,33 @@
+package postgres
+
+import (
+ "context"
+ "database/sql"
+ "embed"
+
+ "github.com/pressly/goose/v3"
+ "golang.org/x/xerrors"
+)
+
+//go:embed db/migrations/*.sql
+var embedMigrations embed.FS
+
+// GetEmbeddedMigrations returns the embedded migrations filesystem
+// This is used by the admin db-migrate command
+func GetEmbeddedMigrations() embed.FS {
+ return embedMigrations
+}
+
+func runMigrations(ctx context.Context, db *sql.DB) error {
+ goose.SetBaseFS(embedMigrations)
+
+ if err := goose.SetDialect("postgres"); err != nil {
+ return xerrors.Errorf("failed to set goose dialect: %w", err)
+ }
+
+ if err := goose.UpContext(ctx, db, "db/migrations"); err != nil {
+ return xerrors.Errorf("failed to run migrations: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/storage/metastorage/postgres/model/block_event.go b/internal/storage/metastorage/postgres/model/block_event.go
new file mode 100644
index 00000000..ca845435
--- /dev/null
+++ b/internal/storage/metastorage/postgres/model/block_event.go
@@ -0,0 +1,61 @@
+package model
+
+import (
+ "database/sql"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func BlockEventFromRow(row *sql.Row) (*api.BlockchainEvent, error) {
+ var event api.BlockchainEvent
+ var eventTypeStr string
+ var blockHash string
+ var blockHeight uint64
+ var eventSequence int64
+ var eventTag uint32
+
+ err := row.Scan(
+ &eventSequence,
+ &eventTypeStr,
+ &blockHeight,
+ &blockHash,
+ &eventTag,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ event.Type = ParseEventType(eventTypeStr)
+ event.SequenceNum = eventSequence
+ event.EventTag = eventTag
+ event.Block = &api.BlockIdentifier{
+ Hash: blockHash,
+ Height: blockHeight,
+ }
+
+ return &event, nil
+}
+
+// ParseEventType converts a string representation of event type to the protobuf enum
+func ParseEventType(eventTypeStr string) api.BlockchainEvent_Type {
+ switch eventTypeStr {
+ case "BLOCK_ADDED":
+ return api.BlockchainEvent_BLOCK_ADDED
+ case "BLOCK_REMOVED":
+ return api.BlockchainEvent_BLOCK_REMOVED
+ default:
+ return api.BlockchainEvent_UNKNOWN
+ }
+}
+
+// EventTypeToString converts the protobuf enum to string representation
+func EventTypeToString(eventType api.BlockchainEvent_Type) string {
+ switch eventType {
+ case api.BlockchainEvent_BLOCK_ADDED:
+ return "BLOCK_ADDED"
+ case api.BlockchainEvent_BLOCK_REMOVED:
+ return "BLOCK_REMOVED"
+ default:
+ return "UNKNOWN"
+ }
+}
diff --git a/internal/storage/metastorage/postgres/model/block_metadata.go b/internal/storage/metastorage/postgres/model/block_metadata.go
new file mode 100644
index 00000000..b2bf0703
--- /dev/null
+++ b/internal/storage/metastorage/postgres/model/block_metadata.go
@@ -0,0 +1,95 @@
+package model
+
+import (
+ "database/sql"
+
+ "github.com/coinbase/chainstorage/internal/utils/utils"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// Scanner interface that both *sql.Row and *sql.Rows implement
+type Scanner interface {
+ Scan(dest ...interface{}) error
+}
+
+// scanBlockMetadata scans a single row into a BlockMetadata struct.
+// Schema: id, height, tag, hash, parent_hash, parent_height, object_key_main,
+// timestamp, skipped, object_format, byte_offset, byte_length, uncompressed_length
+func scanBlockMetadata(scanner Scanner) (*api.BlockMetadata, error) {
+ var block api.BlockMetadata
+ var timestamp int64
+ var objectFormat int32
+ var byteOffset, byteLength, uncompressedLength sql.NullInt64
+ var id int64 // We get this but don't need it in the result
+
+ err := scanner.Scan(
+ &id,
+ &block.Height,
+ &block.Tag,
+ &block.Hash,
+ &block.ParentHash,
+ &block.ParentHeight,
+ &block.ObjectKeyMain,
+ ×tamp,
+ &block.Skipped,
+ &objectFormat,
+ &byteOffset,
+ &byteLength,
+ &uncompressedLength,
+ )
+ if err != nil {
+ return nil, err
+ }
+
+ block.Timestamp = utils.ToTimestamp(timestamp)
+ block.ObjectFormat = api.BlockObjectFormat(objectFormat)
+ if byteOffset.Valid {
+ block.ByteOffset = uint64(byteOffset.Int64)
+ }
+ if byteLength.Valid {
+ block.ByteLength = uint64(byteLength.Int64)
+ }
+ if uncompressedLength.Valid {
+ block.UncompressedLength = uint64(uncompressedLength.Int64)
+ }
+ return &block, nil
+}
+
+// BlockMetadataFromRow converts a postgres row into a BlockMetadata proto
+// Used for direct block_metadata table queries
+// Schema: see scanBlockMetadata
+func BlockMetadataFromRow(db *sql.DB, row *sql.Row) (*api.BlockMetadata, error) {
+ return scanBlockMetadata(row)
+}
+
+// BlockMetadataFromCanonicalRow converts a postgres row from canonical join into a BlockMetadata proto
+// Used for queries that join canonical_blocks with block_metadata
+// Schema: see scanBlockMetadata
+func BlockMetadataFromCanonicalRow(db *sql.DB, row *sql.Row) (*api.BlockMetadata, error) {
+ return scanBlockMetadata(row)
+}
+
+// scanBlockMetadataRows scans multiple rows into BlockMetadata structs
+func scanBlockMetadataRows(rows *sql.Rows) ([]*api.BlockMetadata, error) {
+ var blocks []*api.BlockMetadata
+ for rows.Next() {
+ block, err := scanBlockMetadata(rows)
+ if err != nil {
+ return nil, err
+ }
+ blocks = append(blocks, block)
+ }
+ return blocks, nil
+}
+
+// BlockMetadataFromRows converts multiple postgres rows into BlockMetadata protos
+// Used for direct block_metadata table queries
+func BlockMetadataFromRows(db *sql.DB, rows *sql.Rows) ([]*api.BlockMetadata, error) {
+ return scanBlockMetadataRows(rows)
+}
+
+// BlockMetadataFromCanonicalRows converts multiple postgres rows from canonical joins into BlockMetadata protos
+// Used for queries that join canonical_blocks with block_metadata
+func BlockMetadataFromCanonicalRows(db *sql.DB, rows *sql.Rows) ([]*api.BlockMetadata, error) {
+ return scanBlockMetadataRows(rows)
+}
diff --git a/internal/storage/metastorage/postgres/model/block_metadata_test.go b/internal/storage/metastorage/postgres/model/block_metadata_test.go
new file mode 100644
index 00000000..466ff0ac
--- /dev/null
+++ b/internal/storage/metastorage/postgres/model/block_metadata_test.go
@@ -0,0 +1,59 @@
+package model
+
+import (
+ "database/sql"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type blockMetadataScanner struct {
+ objectFormat int32
+ byteOffset sql.NullInt64
+ byteLength sql.NullInt64
+ uncompressedLength sql.NullInt64
+}
+
+func (s blockMetadataScanner) Scan(dest ...interface{}) error {
+ *dest[0].(*int64) = 123
+ *dest[1].(*uint64) = 100
+ *dest[2].(*uint32) = 1
+ *dest[3].(*string) = "0xabc"
+ *dest[4].(*string) = "0xdef"
+ *dest[5].(*uint64) = 99
+ *dest[6].(*string) = "1/100/0xabc.gzip"
+ *dest[7].(*int64) = 123456
+ *dest[8].(*bool) = false
+ *dest[9].(*int32) = s.objectFormat
+ *dest[10].(*sql.NullInt64) = s.byteOffset
+ *dest[11].(*sql.NullInt64) = s.byteLength
+ *dest[12].(*sql.NullInt64) = s.uncompressedLength
+ return nil
+}
+
+func TestScanBlockMetadata_SingleBlockNullByteFields(t *testing.T) {
+ actual, err := scanBlockMetadata(blockMetadataScanner{})
+
+ require.NoError(t, err)
+ require.Equal(t, api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK, actual.GetObjectFormat())
+ require.Zero(t, actual.GetByteOffset())
+ require.Zero(t, actual.GetByteLength())
+ require.Zero(t, actual.GetUncompressedLength())
+}
+
+func TestScanBlockMetadata_ConsolidatedFields(t *testing.T) {
+ actual, err := scanBlockMetadata(blockMetadataScanner{
+ objectFormat: int32(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH),
+ byteOffset: sql.NullInt64{Int64: 4096, Valid: true},
+ byteLength: sql.NullInt64{Int64: 8192, Valid: true},
+ uncompressedLength: sql.NullInt64{Int64: 8192, Valid: true},
+ })
+
+ require.NoError(t, err)
+ require.Equal(t, api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, actual.GetObjectFormat())
+ require.Equal(t, uint64(4096), actual.GetByteOffset())
+ require.Equal(t, uint64(8192), actual.GetByteLength())
+ require.Equal(t, uint64(8192), actual.GetUncompressedLength())
+}
diff --git a/internal/storage/metastorage/postgres/module.go b/internal/storage/metastorage/postgres/module.go
new file mode 100644
index 00000000..502ab6eb
--- /dev/null
+++ b/internal/storage/metastorage/postgres/module.go
@@ -0,0 +1,30 @@
+package postgres
+
+import (
+ "context"
+
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+)
+
+var Module = fx.Options(
+ fx.Provide(fx.Annotated{
+ Name: "metastorage/postgres",
+ Target: NewFactory,
+ }),
+ fx.Invoke(registerClosePoolsHook),
+)
+
+func registerClosePoolsHook(lc fx.Lifecycle, logger *zap.Logger) {
+ lc.Append(fx.Hook{
+ OnStart: func(context.Context) error { return nil },
+ OnStop: func(ctx context.Context) error {
+ if err := CloseAllConnectionPools(); err != nil {
+ logger.Error("failed to close PostgreSQL connection pools", zap.Error(err))
+ return err
+ }
+ logger.Info("PostgreSQL connection pools closed successfully")
+ return nil
+ },
+ })
+}
diff --git a/internal/storage/metastorage/postgres/transaction_storage.go b/internal/storage/metastorage/postgres/transaction_storage.go
new file mode 100644
index 00000000..ea0a297a
--- /dev/null
+++ b/internal/storage/metastorage/postgres/transaction_storage.go
@@ -0,0 +1,33 @@
+package postgres
+
+import (
+ "context"
+ "database/sql"
+ "errors"
+
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/internal"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/model"
+)
+
+type (
+ transactionStorageImpl struct {
+ db *sql.DB
+ }
+)
+
+func newTransactionStorage(db *sql.DB, params Params) (internal.TransactionStorage, error) {
+ accessor := &transactionStorageImpl{
+ db: db,
+ }
+ return accessor, nil
+}
+
+func (t *transactionStorageImpl) AddTransactions(ctx context.Context, transaction []*model.Transaction, parallelism int) error {
+ // TODO: Implement transaction insertion
+ return errors.New("not implemented")
+}
+
+func (t *transactionStorageImpl) GetTransaction(ctx context.Context, tag uint32, transactionHash string) ([]*model.Transaction, error) {
+ // TODO: Implement get transaction
+ return nil, errors.New("not implemented")
+}
diff --git a/internal/storage/metastorage/single_block_upload_guard.go b/internal/storage/metastorage/single_block_upload_guard.go
new file mode 100644
index 00000000..62869da2
--- /dev/null
+++ b/internal/storage/metastorage/single_block_upload_guard.go
@@ -0,0 +1,58 @@
+package metastorage
+
+import (
+ "context"
+ "errors"
+
+ "golang.org/x/xerrors"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+var ErrSingleBlockRetirementFenced = errors.New("single-block upload blocked by retirement fence")
+
+type singleBlockUploadStorage interface {
+ GetBlockByHash(ctx context.Context, tag uint32, height uint64, blockHash string) (*api.BlockMetadata, error)
+ SingleBlockUploadGuardStorage
+}
+
+// UploadSingleBlockObject serializes a single-block PUT with retirement
+// preparation. Storage implementations must return a non-nil guard even when
+// canonical metadata does not exist yet.
+func UploadSingleBlockObject(
+ ctx context.Context,
+ metadataStorage singleBlockUploadStorage,
+ metadata *api.BlockMetadata,
+ upload func() (string, error),
+) (objectKey string, err error) {
+ if metadata == nil {
+ return "", xerrors.New("block metadata is required for guarded upload")
+ }
+ if metadataStorage == nil {
+ return "", xerrors.New("metadata storage is required for guarded upload")
+ }
+ guard, err := metadataStorage.AcquireSingleBlockUploadGuard(ctx, metadata.Tag, metadata.Height, metadata.Hash)
+ if err != nil {
+ return "", err
+ }
+ if guard == nil {
+ return "", xerrors.New("single-block upload guard is required")
+ }
+ defer func() {
+ if releaseErr := guard.Release(); releaseErr != nil {
+ err = errors.Join(err, releaseErr)
+ }
+ }()
+
+ if !guard.RetirementFenced() {
+ objectKey, err = upload()
+ return objectKey, err
+ }
+ return "", xerrors.Errorf(
+ "%w: tag=%d height=%d hash=%s",
+ ErrSingleBlockRetirementFenced,
+ metadata.Tag,
+ metadata.Height,
+ metadata.Hash,
+ )
+}
diff --git a/internal/storage/metastorage/single_block_upload_guard_test.go b/internal/storage/metastorage/single_block_upload_guard_test.go
new file mode 100644
index 00000000..d7b764cc
--- /dev/null
+++ b/internal/storage/metastorage/single_block_upload_guard_test.go
@@ -0,0 +1,129 @@
+package metastorage
+
+import (
+ "context"
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type uploadGuardTestStorage struct {
+ guard SingleBlockUploadGuard
+ existing *api.BlockMetadata
+ acquireErr error
+ metadataErr error
+}
+
+func (s *uploadGuardTestStorage) AcquireSingleBlockUploadGuard(
+ ctx context.Context,
+ tag uint32,
+ height uint64,
+ hash string,
+) (SingleBlockUploadGuard, error) {
+ return s.guard, s.acquireErr
+}
+
+func (s *uploadGuardTestStorage) GetBlockByHash(
+ ctx context.Context,
+ tag uint32,
+ height uint64,
+ blockHash string,
+) (*api.BlockMetadata, error) {
+ return s.existing, s.metadataErr
+}
+
+type uploadGuardTestLease struct {
+ fenced bool
+ releaseErr error
+ releaseCall int
+}
+
+func (g *uploadGuardTestLease) RetirementFenced() bool {
+ return g.fenced
+}
+
+func (g *uploadGuardTestLease) Release() error {
+ g.releaseCall++
+ return g.releaseErr
+}
+
+func TestUploadSingleBlockObjectFailsClosedForRetirementFence(t *testing.T) {
+ require := require.New(t)
+ metadata := testGuardMetadata()
+ lease := &uploadGuardTestLease{fenced: true}
+ storage := &uploadGuardTestStorage{guard: lease}
+ uploadCalls := 0
+
+ objectKey, err := UploadSingleBlockObject(context.Background(), storage, metadata, func() (string, error) {
+ uploadCalls++
+ return "single-block/new.gzip", nil
+ })
+
+ require.ErrorIs(err, ErrSingleBlockRetirementFenced)
+ require.Empty(objectKey)
+ require.Zero(uploadCalls)
+ require.Equal(1, lease.releaseCall)
+}
+
+func TestUploadSingleBlockObjectHoldsGuardThroughPut(t *testing.T) {
+ require := require.New(t)
+ metadata := testGuardMetadata()
+ lease := &uploadGuardTestLease{}
+ storage := &uploadGuardTestStorage{guard: lease}
+ uploadCalls := 0
+
+ objectKey, err := UploadSingleBlockObject(context.Background(), storage, metadata, func() (string, error) {
+ uploadCalls++
+ require.Zero(lease.releaseCall)
+ return "single-block/new.gzip", nil
+ })
+
+ require.NoError(err)
+ require.Equal("single-block/new.gzip", objectKey)
+ require.Equal(1, uploadCalls)
+ require.Equal(1, lease.releaseCall)
+}
+
+func TestUploadSingleBlockObjectFailsClosedForMissingGuard(t *testing.T) {
+ require := require.New(t)
+ storage := &uploadGuardTestStorage{}
+ uploadCalls := 0
+
+ objectKey, err := UploadSingleBlockObject(context.Background(), storage, testGuardMetadata(), func() (string, error) {
+ uploadCalls++
+ return "single-block/new.gzip", nil
+ })
+
+ require.ErrorContains(err, "single-block upload guard is required")
+ require.Empty(objectKey)
+ require.Zero(uploadCalls)
+}
+
+func TestUploadSingleBlockObjectJoinsReleaseFailure(t *testing.T) {
+ require := require.New(t)
+ metadata := testGuardMetadata()
+ uploadErr := errors.New("put failed")
+ releaseErr := errors.New("release failed")
+ lease := &uploadGuardTestLease{releaseErr: releaseErr}
+ storage := &uploadGuardTestStorage{guard: lease}
+
+ _, err := UploadSingleBlockObject(context.Background(), storage, metadata, func() (string, error) {
+ return "", uploadErr
+ })
+
+ require.ErrorIs(err, uploadErr)
+ require.ErrorIs(err, releaseErr)
+ require.Equal(1, lease.releaseCall)
+}
+
+func testGuardMetadata() *api.BlockMetadata {
+ return &api.BlockMetadata{
+ Tag: 2,
+ Height: 429600000,
+ Hash: "hash-429600000",
+ ParentHash: "hash-429599999",
+ }
+}
diff --git a/internal/storage/retirement/full_lifecycle_integration_test.go b/internal/storage/retirement/full_lifecycle_integration_test.go
new file mode 100644
index 00000000..82a76d70
--- /dev/null
+++ b/internal/storage/retirement/full_lifecycle_integration_test.go
@@ -0,0 +1,423 @@
+package retirement_test
+
+import (
+ "bytes"
+ "context"
+ "database/sql"
+ "fmt"
+ "io"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awsconfig "github.com/aws/aws-sdk-go-v2/config"
+ "github.com/aws/aws-sdk-go-v2/credentials"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ awss3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/pressly/goose/v3"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/fx"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ chains3 "github.com/coinbase/chainstorage/internal/s3"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/cscb"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ metapostgres "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+ storageutils "github.com/coinbase/chainstorage/internal/storage/utils"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestIntegrationRetirementFullStorageLifecycle(t *testing.T) {
+ require := require.New(t)
+ ctx := context.Background()
+ unique := time.Now().UTC().UnixNano()
+ tag := uint32(1_500_000_000 + unique%100_000_000)
+ height := uint64(9_000_000_000 + unique%100_000_000)
+ bucket := fmt.Sprintf("chainstorage-retirement-lifecycle-%d", unique)
+
+ cfg, err := config.New(
+ config.WithEnvironment(config.EnvLocal),
+ config.WithBlockchain(common.Blockchain_BLOCKCHAIN_SOLANA),
+ config.WithNetwork(common.Network_NETWORK_SOLANA_MAINNET),
+ )
+ require.NoError(err)
+ if cfg.AWS.Postgres == nil {
+ t.Skip("Postgres is not configured")
+ }
+ cfg.StorageType.BlobStorageType = config.BlobStorageType_S3
+ cfg.StorageType.MetaStorageType = config.MetaStorageType_POSTGRES
+ cfg.AWS.Bucket = bucket
+ cfg.AWS.IsLocalStack = true
+ cfg.AWS.IsResetLocal = true
+ localStackEndpoint := retirementLocalStackEndpoint(t, cfg.AWS.Postgres)
+ cfg.Chain.BlockStartHeight = height
+
+ db, err := openLifecycleDB(ctx, cfg.AWS.Postgres)
+ if err != nil {
+ t.Skipf("Postgres integration database is unavailable: %v", err)
+ }
+ defer func() { _ = db.Close() }()
+ goose.SetBaseFS(metapostgres.GetEmbeddedMigrations())
+ require.NoError(goose.SetDialect("postgres"))
+ require.NoError(goose.UpContext(ctx, db, "db/migrations"))
+
+ var (
+ meta metastorage.MetaStorage
+ blob blobstorage.BlobStorage
+ singleBlockUploader blobstorage.SingleBlockUploader
+ s3Session *chains3.S3
+ )
+ app := testapp.New(
+ t,
+ testapp.WithIntegration(),
+ testapp.WithConfig(cfg),
+ chains3.Module,
+ metastorage.Module,
+ blobstorage.Module,
+ fx.Replace(newRetirementLocalStackConfig(ctx, localStackEndpoint)),
+ fx.Populate(&meta, &blob, &singleBlockUploader, &s3Session),
+ )
+ defer app.Close()
+ require.NotNil(meta)
+ require.NotNil(blob)
+ require.NotNil(singleBlockUploader)
+ require.NotNil(s3Session)
+
+ rawS3 := awss3.NewFromConfig(s3Session.Config)
+ _, err = rawS3.PutBucketVersioning(ctx, &awss3.PutBucketVersioningInput{
+ Bucket: aws.String(bucket),
+ VersioningConfiguration: &awss3types.VersioningConfiguration{
+ Status: awss3types.BucketVersioningStatusEnabled,
+ },
+ })
+ require.NoError(err)
+ defer cleanupLifecycleBucket(t, rawS3, bucket)
+
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: tag,
+ Height: height,
+ Hash: fmt.Sprintf("solana-hash-%d", unique),
+ ParentHash: fmt.Sprintf("solana-parent-%d", unique),
+ ParentHeight: height - 1,
+ Timestamp: timestamppb.New(time.Now().UTC().Truncate(time.Second)),
+ },
+ Blobdata: &api.Block_Solana{Solana: &api.SolanaBlobdata{Header: []byte(`{"slot":9000000000,"transactions":["one","two"]}`)}},
+ }
+
+ singleBlockKey, err := singleBlockUploader.Upload(ctx, block, api.Compression_GZIP)
+ require.NoError(err)
+ require.NotEmpty(singleBlockKey)
+ singleBlockObject, err := rawS3.GetObject(ctx, &awss3.GetObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(singleBlockKey),
+ })
+ require.NoError(err)
+ singleBlockBytes, err := io.ReadAll(singleBlockObject.Body)
+ require.NoError(err)
+ require.NoError(singleBlockObject.Body.Close())
+ block.Metadata.ObjectKeyMain = singleBlockKey
+ require.NoError(meta.PersistBlockMetas(ctx, true, []*api.BlockMetadata{block.Metadata}, nil))
+
+ records, err := meta.GetBlocksMissingConsolidationShadow(ctx, tag, height, height+1, 1)
+ require.NoError(err)
+ require.Len(records, 1)
+ blockMetadataID := records[0].ID
+ defer cleanupLifecycleMetadata(t, db, blockMetadataID, bucket)
+
+ downloadedSingleBlock, err := blob.Download(ctx, records[0].Metadata)
+ require.NoError(err)
+ require.True(proto.Equal(block, downloadedSingleBlock))
+ consolidatedPayload, err := proto.Marshal(storageutils.CloneBlockWithoutStoragePlacement(downloadedSingleBlock))
+ require.NoError(err)
+ consolidatedKey, placements, err := blob.UploadConsolidated(ctx, []blobstorage.ConsolidatedBlockPayload{{
+ Metadata: records[0].Metadata,
+ MetadataID: blockMetadataID,
+ RawBlockPayload: blobstorage.BytesPayloadSource(consolidatedPayload),
+ UncompressedLength: uint64(len(consolidatedPayload)),
+ }})
+ require.NoError(err)
+ require.Len(placements, 1)
+ require.Contains(consolidatedKey, "/consolidated/")
+ placement := placements[0]
+ require.NoError(meta.PersistBlockConsolidationShadows(ctx, []*metastorage.ConsolidationShadowPlacement{{
+ BlockMetadataID: blockMetadataID,
+ Tag: tag,
+ Height: height,
+ Hash: block.Metadata.Hash,
+ SingleBlockObjectKeyMain: singleBlockKey,
+ ConsolidatedObjectKeyMain: consolidatedKey,
+ ObjectFormat: placement.ObjectFormat,
+ ByteOffset: placement.ByteOffset,
+ ByteLength: placement.ByteLength,
+ UncompressedLength: placement.UncompressedLength,
+ }}))
+ promotion, err := meta.PromoteBlockConsolidationShadows(ctx, tag, height, height+1, 1, 72*time.Hour)
+ require.NoError(err)
+ require.Equal(uint64(1), promotion.Blocks)
+
+ var retiredAt, retireAfter time.Time
+ require.NoError(db.QueryRowContext(ctx, `
+ SELECT single_block_retention_started_at, single_block_delete_after
+ FROM block_consolidation_shadow
+ WHERE block_metadata_id = $1`, blockMetadataID).Scan(&retiredAt, &retireAfter))
+ require.WithinDuration(retiredAt.Add(72*time.Hour), retireAfter, time.Microsecond)
+ _, err = db.ExecContext(ctx, `
+ UPDATE block_consolidation_shadow
+ SET single_block_retention_started_at = clock_timestamp() - INTERVAL '96 hours',
+ single_block_delete_after = clock_timestamp() - INTERVAL '24 hours'
+ WHERE block_metadata_id = $1`, blockMetadataID)
+ require.NoError(err)
+
+ active, err := meta.GetBlockByHeight(ctx, tag, height)
+ require.NoError(err)
+ require.Equal(consolidatedKey, active.ObjectKeyMain)
+ require.Equal(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH, active.ObjectFormat)
+ parsedCSCB, err := blob.Download(ctx, active)
+ require.NoError(err)
+ require.True(proto.Equal(storageutils.CloneBlockWithoutStoragePlacement(block), storageutils.CloneBlockWithoutStoragePlacement(parsedCSCB)))
+
+ _, err = rawS3.PutObject(ctx, &awss3.PutObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(singleBlockKey),
+ Body: bytes.NewReader(singleBlockBytes),
+ })
+ require.NoError(err)
+ _, err = rawS3.DeleteObject(ctx, &awss3.DeleteObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(singleBlockKey),
+ })
+ require.NoError(err)
+
+ policy := retentionSafeBucketPolicy(bucket)
+ _, err = rawS3.PutBucketPolicy(ctx, &awss3.PutBucketPolicyInput{Bucket: aws.String(bucket), Policy: aws.String(policy)})
+ require.NoError(err)
+ store := retirement.NewS3ObjectStore(rawS3)
+ repo := retirement.NewPostgresRepository(db)
+ planner := retirement.NewPlanner(repo, store)
+ req := retirement.PlanRequest{
+ Environment: "local",
+ Blockchain: "solana",
+ Network: "mainnet",
+ Bucket: bucket,
+ Tag: tag,
+ StartHeight: height,
+ EndHeight: height + 1,
+ Limit: 1,
+ Now: time.Now().UTC(),
+ Execute: true,
+ ProductionDeleteEnabled: false,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackErrorCount: 0,
+ Approval: retirement.Approval{
+ Chain: "solana-mainnet",
+ StartHeight: height,
+ EndHeight: height + 1,
+ },
+ }
+ report, err := planner.Plan(ctx, req)
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(retirement.ActionDeleteObjectVersion, report.Items[0].Action)
+ require.Empty(report.Items[0].SkipReason)
+ require.Equal(2, report.Items[0].SingleBlockVersions)
+ require.Equal(1, report.Items[0].DeleteMarkers)
+ require.True(report.SafetyGates.CSCBWriteOncePolicyVerified)
+
+ snapshot, err := store.InspectObjectRetentionSafety(ctx, bucket, consolidatedKey)
+ require.NoError(err)
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO cscb_retirement_safety_observation (
+ bucket, consolidated_object_key_main, configuration_sha256,
+ first_observed_at, last_observed_at
+ ) VALUES ($1, $2, $3, clock_timestamp() - INTERVAL '16 minutes', clock_timestamp() - INTERVAL '16 minutes')
+ ON CONFLICT (bucket, consolidated_object_key_main) DO UPDATE SET
+ configuration_sha256 = EXCLUDED.configuration_sha256,
+ first_observed_at = EXCLUDED.first_observed_at,
+ last_observed_at = EXCLUDED.last_observed_at`,
+ bucket, consolidatedKey, snapshot.ConfigurationSHA256)
+ require.NoError(err)
+
+ require.NoError(planner.Apply(ctx, req, report))
+ require.Equal(retirement.ActionDeletedVerified, report.Items[0].Action)
+ require.Equal(retirement.RetirementStateDeletedVerified, report.Items[0].RetirementState)
+
+ singleBlockTopology, err := store.ListObjectVersions(ctx, bucket, singleBlockKey)
+ require.NoError(err)
+ require.Empty(singleBlockTopology.Versions)
+ require.Empty(singleBlockTopology.DeleteMarkers)
+ cscbHead, err := store.HeadObject(ctx, bucket, consolidatedKey)
+ require.NoError(err)
+ require.True(cscbHead.Exists)
+
+ var (
+ shadowSingleBlockKey sql.NullString
+ deletedAt sql.NullTime
+ state string
+ manifestKey sql.NullString
+ keyHash string
+ )
+ require.NoError(db.QueryRowContext(ctx, `
+ SELECT shadow.single_block_object_key_main, shadow.single_block_object_deleted_at,
+ retirement.state, retirement.single_block_object_key_main, retirement.single_block_object_key_sha256
+ FROM block_consolidation_shadow shadow
+ JOIN block_single_block_retention retirement
+ ON retirement.block_metadata_id = shadow.block_metadata_id
+ WHERE shadow.block_metadata_id = $1`, blockMetadataID).Scan(
+ &shadowSingleBlockKey, &deletedAt, &state, &manifestKey, &keyHash,
+ ))
+ require.False(shadowSingleBlockKey.Valid)
+ require.True(deletedAt.Valid)
+ require.Equal(retirement.RetirementStateDeletedVerified, state)
+ require.False(manifestKey.Valid)
+ require.Len(keyHash, 64)
+
+ activeAfterDelete, err := meta.GetBlockByHeight(ctx, tag, height)
+ require.NoError(err)
+ require.Equal(consolidatedKey, activeAfterDelete.ObjectKeyMain)
+ parsedAfterDelete, err := blob.Download(ctx, activeAfterDelete)
+ require.NoError(err)
+ require.True(proto.Equal(storageutils.CloneBlockWithoutStoragePlacement(block), storageutils.CloneBlockWithoutStoragePlacement(parsedAfterDelete)))
+ rawCSCBBlock, err := readRawLifecycleCSCBBlock(ctx, rawS3, bucket, consolidatedKey, activeAfterDelete)
+ require.NoError(err)
+ require.False(storageutils.HasBlockStoragePlacement(rawCSCBBlock))
+ require.True(proto.Equal(storageutils.CloneBlockWithoutStoragePlacement(block), rawCSCBBlock))
+}
+
+func readRawLifecycleCSCBBlock(
+ ctx context.Context,
+ client *awss3.Client,
+ bucket string,
+ key string,
+ metadata *api.BlockMetadata,
+) (*api.Block, error) {
+ object, err := client.GetObject(ctx, &awss3.GetObjectInput{Bucket: aws.String(bucket), Key: aws.String(key)})
+ if err != nil {
+ return nil, err
+ }
+ defer func() { _ = object.Body.Close() }()
+ data, err := io.ReadAll(object.Body)
+ if err != nil {
+ return nil, err
+ }
+ index, err := cscb.ParseIndex(data)
+ if err != nil {
+ return nil, err
+ }
+ block, chunk, err := index.LookupBlock(metadata)
+ if err != nil {
+ return nil, err
+ }
+ start := chunk.CompressedPayloadOffset
+ end := start + chunk.CompressedLength
+ if end < start || end > uint64(len(data)) {
+ return nil, fmt.Errorf("CSCB chunk bounds are invalid: start=%d end=%d bytes=%d", start, end, len(data))
+ }
+ chunkPayload, err := cscb.DecodeChunkFrame(bytes.NewReader(data[start:end]), index.Header.Codec)
+ if err != nil {
+ return nil, err
+ }
+ if err := cscb.ValidateChunkPayload(chunkPayload, chunk); err != nil {
+ return nil, err
+ }
+ payload, err := cscb.ExtractBlockPayload(chunkPayload, block)
+ if err != nil {
+ return nil, err
+ }
+ var result api.Block
+ if err := proto.Unmarshal(payload, &result); err != nil {
+ return nil, err
+ }
+ return &result, nil
+}
+
+func openLifecycleDB(ctx context.Context, cfg *config.PostgresConfig) (*sql.DB, error) {
+ dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s", cfg.Host, cfg.Port, cfg.Database, cfg.User, cfg.Password, cfg.SSLMode)
+ db, err := sql.Open("postgres", dsn)
+ if err != nil {
+ return nil, err
+ }
+ if err := db.PingContext(ctx); err != nil {
+ _ = db.Close()
+ return nil, err
+ }
+ return db, nil
+}
+
+func retirementLocalStackEndpoint(t *testing.T, postgres *config.PostgresConfig) string {
+ t.Helper()
+ switch postgres.Host {
+ case "localhost", "127.0.0.1", "::1":
+ return "http://localhost:4566"
+ case "postgres":
+ return "http://localstack:4566"
+ default:
+ t.Fatalf("refusing to run retirement lifecycle test against PostgreSQL host %q", postgres.Host)
+ return ""
+ }
+}
+
+func newRetirementLocalStackConfig(ctx context.Context, endpoint string) aws.Config {
+ cfg, err := awsconfig.LoadDefaultConfig(
+ ctx,
+ awsconfig.WithRegion("us-east-1"),
+ awsconfig.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("test", "test", "")),
+ awsconfig.WithEndpointResolverWithOptions(
+ aws.EndpointResolverWithOptionsFunc(func(string, string, ...interface{}) (aws.Endpoint, error) {
+ return aws.Endpoint{URL: endpoint, HostnameImmutable: true}, nil
+ }),
+ ),
+ )
+ if err != nil {
+ panic(fmt.Sprintf("failed to configure LocalStack: %v", err))
+ }
+ return cfg
+}
+
+func retentionSafeBucketPolicy(bucket string) string {
+ return fmt.Sprintf(`{
+ "Version":"2012-10-17",
+ "Statement":[
+ {"Effect":"Deny","Principal":"*","Action":"s3:PutObject","Resource":"arn:aws:s3:::%[1]s/*/consolidated/*","Condition":{"Null":{"s3:if-none-match":"true"}}},
+ {"Effect":"Deny","Principal":"*","Action":["s3:DeleteObject","s3:DeleteObjectVersion","s3:ReplicateObject","s3:ReplicateDelete"],"Resource":"arn:aws:s3:::%[1]s/*/consolidated/*"},
+ {"Effect":"Deny","Principal":"*","Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::%[1]s"}
+ ]
+ }`, bucket)
+}
+
+func cleanupLifecycleMetadata(t *testing.T, db *sql.DB, blockMetadataID int64, bucket string) {
+ t.Helper()
+ ctx := context.Background()
+ _, _ = db.ExecContext(ctx, `DELETE FROM cscb_retirement_safety_observation WHERE bucket = $1`, bucket)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE block_single_block_retention DISABLE TRIGGER block_single_block_retention_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_single_block_retention WHERE block_metadata_id = $1`, blockMetadataID)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE block_single_block_retention ENABLE TRIGGER block_single_block_retention_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_consolidation_shadow WHERE block_metadata_id = $1`, blockMetadataID)
+ _, _ = db.ExecContext(ctx, `DELETE FROM canonical_blocks WHERE block_metadata_id = $1`, blockMetadataID)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_metadata WHERE id = $1`, blockMetadataID)
+}
+
+func cleanupLifecycleBucket(t *testing.T, client *awss3.Client, bucket string) {
+ t.Helper()
+ ctx := context.Background()
+ _, _ = client.DeleteBucketPolicy(ctx, &awss3.DeleteBucketPolicyInput{Bucket: aws.String(bucket)})
+ versions, err := client.ListObjectVersions(ctx, &awss3.ListObjectVersionsInput{Bucket: aws.String(bucket)})
+ if err == nil {
+ for _, version := range versions.Versions {
+ _, _ = client.DeleteObject(ctx, &awss3.DeleteObjectInput{Bucket: aws.String(bucket), Key: version.Key, VersionId: version.VersionId})
+ }
+ for _, marker := range versions.DeleteMarkers {
+ _, _ = client.DeleteObject(ctx, &awss3.DeleteObjectInput{Bucket: aws.String(bucket), Key: marker.Key, VersionId: marker.VersionId})
+ }
+ }
+ _, _ = client.DeleteBucket(ctx, &awss3.DeleteBucketInput{Bucket: aws.String(bucket)})
+}
diff --git a/internal/storage/retirement/output.go b/internal/storage/retirement/output.go
new file mode 100644
index 00000000..3fb8dbf6
--- /dev/null
+++ b/internal/storage/retirement/output.go
@@ -0,0 +1,12 @@
+package retirement
+
+import (
+ "encoding/json"
+ "io"
+)
+
+func WriteReportJSON(w io.Writer, report *Report) error {
+ encoder := json.NewEncoder(w)
+ encoder.SetIndent("", " ")
+ return encoder.Encode(report)
+}
diff --git a/internal/storage/retirement/payload_verifier.go b/internal/storage/retirement/payload_verifier.go
new file mode 100644
index 00000000..4ce8d88b
--- /dev/null
+++ b/internal/storage/retirement/payload_verifier.go
@@ -0,0 +1,278 @@
+package retirement
+
+import (
+ "bytes"
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "strconv"
+
+ "golang.org/x/xerrors"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/cscb"
+ storageutils "github.com/coinbase/chainstorage/internal/storage/utils"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+const retirementInitialIndexReadSize = 64 * 1024
+
+type blockPayloadVerifier interface {
+ Verify(ctx context.Context, candidate Candidate) (string, error)
+ VerifyConsolidated(ctx context.Context, candidate Candidate) (string, error)
+}
+
+type (
+ PinnedPayloadInspection struct {
+ CanonicalSHA256 string
+ HasStoragePlacement bool
+ }
+
+ PinnedPayloadVerifier interface {
+ ReadSingleBlock(ctx context.Context, candidate Candidate) (*api.Block, PinnedPayloadInspection, error)
+ InspectSingleBlock(ctx context.Context, candidate Candidate) (PinnedPayloadInspection, error)
+ InspectConsolidated(ctx context.Context, candidate Candidate) (PinnedPayloadInspection, error)
+ }
+)
+
+type payloadVerifier struct {
+ store ObjectStore
+
+ indexes map[string]*cscb.Index
+ lastChunkKey string
+ lastChunkData []byte
+}
+
+func newPayloadVerifier(store ObjectStore) *payloadVerifier {
+ return &payloadVerifier{
+ store: store,
+ indexes: make(map[string]*cscb.Index),
+ }
+}
+
+func NewPinnedPayloadVerifier(store ObjectStore) PinnedPayloadVerifier {
+ return newPayloadVerifier(store)
+}
+
+func (v *payloadVerifier) Verify(ctx context.Context, candidate Candidate) (string, error) {
+ singleBlockInspection, err := v.InspectSingleBlock(ctx, candidate)
+ if err != nil {
+ return "", err
+ }
+ consolidatedInspection, err := v.InspectConsolidated(ctx, candidate)
+ if err != nil {
+ return "", err
+ }
+ if consolidatedInspection.HasStoragePlacement {
+ return "", xerrors.Errorf("pinned CSCB block payload retains storage placement metadata at height %d", candidate.Height)
+ }
+ if singleBlockInspection.CanonicalSHA256 != consolidatedInspection.CanonicalSHA256 {
+ return "", xerrors.Errorf("single-block and CSCB block payloads differ at height %d", candidate.Height)
+ }
+ return singleBlockInspection.CanonicalSHA256, nil
+}
+
+func (v *payloadVerifier) InspectSingleBlock(ctx context.Context, candidate Candidate) (PinnedPayloadInspection, error) {
+ _, inspection, err := v.ReadSingleBlock(ctx, candidate)
+ return inspection, err
+}
+
+func (v *payloadVerifier) ReadSingleBlock(
+ ctx context.Context,
+ candidate Candidate,
+) (*api.Block, PinnedPayloadInspection, error) {
+ singleBlockCompressed, err := v.store.ReadObjectVersion(ctx, candidate.Bucket, candidate.Key, candidate.VersionID)
+ if err != nil {
+ return nil, PinnedPayloadInspection{}, err
+ }
+ singleBlockPayload, err := storageutils.Decompress(singleBlockCompressed, storageutils.GetCompressionType(candidate.Key))
+ if err != nil {
+ return nil, PinnedPayloadInspection{}, xerrors.Errorf("failed to decompress pinned single-block object: %w", err)
+ }
+ var singleBlockBlock api.Block
+ if err := proto.Unmarshal(singleBlockPayload, &singleBlockBlock); err != nil {
+ return nil, PinnedPayloadInspection{}, xerrors.Errorf("failed to parse pinned single-block block payload: %w", err)
+ }
+ if err := validatePayloadIdentity(&singleBlockBlock, candidate); err != nil {
+ return nil, PinnedPayloadInspection{}, xerrors.Errorf("pinned single-block block identity mismatch: %w", err)
+ }
+ normalized := storageutils.CloneBlockWithoutStoragePlacement(&singleBlockBlock)
+ digest, err := canonicalBlockDigest(normalized)
+ if err != nil {
+ return nil, PinnedPayloadInspection{}, err
+ }
+ return &singleBlockBlock, PinnedPayloadInspection{
+ CanonicalSHA256: digest,
+ HasStoragePlacement: storageutils.HasBlockStoragePlacement(&singleBlockBlock),
+ }, nil
+}
+
+func (v *payloadVerifier) InspectConsolidated(ctx context.Context, candidate Candidate) (PinnedPayloadInspection, error) {
+ _, cscbBlock, err := v.readConsolidatedPayload(ctx, candidate)
+ if err != nil {
+ return PinnedPayloadInspection{}, err
+ }
+ if err := validatePayloadIdentity(cscbBlock, candidate); err != nil {
+ return PinnedPayloadInspection{}, xerrors.Errorf("pinned CSCB block identity mismatch: %w", err)
+ }
+ normalized := storageutils.CloneBlockWithoutStoragePlacement(cscbBlock)
+ digest, err := canonicalBlockDigest(normalized)
+ if err != nil {
+ return PinnedPayloadInspection{}, err
+ }
+ return PinnedPayloadInspection{
+ CanonicalSHA256: digest,
+ HasStoragePlacement: storageutils.HasBlockStoragePlacement(cscbBlock),
+ }, nil
+}
+
+func (v *payloadVerifier) VerifyConsolidated(ctx context.Context, candidate Candidate) (string, error) {
+ inspection, err := v.InspectConsolidated(ctx, candidate)
+ if err != nil {
+ return "", err
+ }
+ if inspection.HasStoragePlacement {
+ return "", xerrors.Errorf("pinned CSCB block payload retains storage placement metadata at height %d", candidate.Height)
+ }
+ return inspection.CanonicalSHA256, nil
+}
+
+func canonicalBlockDigest(block *api.Block) (string, error) {
+ // This digest is persisted in retirement and repair manifests. Keep its
+ // deterministic-protobuf algorithm stable across upgrades and rollbacks.
+ payload, err := (proto.MarshalOptions{Deterministic: true}).Marshal(block)
+ if err != nil {
+ return "", xerrors.Errorf("failed to marshal canonical block payload: %w", err)
+ }
+ digest := sha256.Sum256(payload)
+ return hex.EncodeToString(digest[:]), nil
+}
+
+func validatePayloadIdentity(block *api.Block, candidate Candidate) error {
+ metadata := block.GetMetadata()
+ if metadata == nil {
+ return xerrors.New("block metadata is missing")
+ }
+ if metadata.GetTag() != candidate.Tag || metadata.GetHeight() != candidate.Height || metadata.GetHash() != candidate.Hash {
+ return xerrors.Errorf(
+ "got tag=%d height=%d hash=%q; want tag=%d height=%d hash=%q",
+ metadata.GetTag(),
+ metadata.GetHeight(),
+ metadata.GetHash(),
+ candidate.Tag,
+ candidate.Height,
+ candidate.Hash,
+ )
+ }
+ return nil
+}
+
+func (v *payloadVerifier) readConsolidatedPayload(ctx context.Context, candidate Candidate) ([]byte, *api.Block, error) {
+ index, err := v.readIndex(ctx, candidate)
+ if err != nil {
+ return nil, nil, err
+ }
+ block, chunk, err := index.LookupBlock(&api.BlockMetadata{
+ Tag: candidate.Tag,
+ Height: candidate.Height,
+ Hash: candidate.Hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: candidate.ByteOffset,
+ ByteLength: candidate.ByteLength,
+ UncompressedLength: candidate.UncompressedLength,
+ })
+ if err != nil {
+ return nil, nil, xerrors.Errorf("failed to resolve pinned CSCB placement: %w", err)
+ }
+ chunkData, err := v.readChunk(ctx, candidate, index, chunk)
+ if err != nil {
+ return nil, nil, err
+ }
+ payload, err := cscb.ExtractBlockPayload(chunkData, block)
+ if err != nil {
+ return nil, nil, xerrors.Errorf("failed to extract pinned CSCB block payload: %w", err)
+ }
+ var parsed api.Block
+ if err := proto.Unmarshal(payload, &parsed); err != nil {
+ return nil, nil, xerrors.Errorf("failed to parse pinned CSCB block payload: %w", err)
+ }
+ return payload, &parsed, nil
+}
+
+func (v *payloadVerifier) readIndex(ctx context.Context, candidate Candidate) (*cscb.Index, error) {
+ cacheKey := candidate.Bucket + "\x00" + candidate.ConsolidatedKey + "\x00" + candidate.CSCBVersionID
+ if index, ok := v.indexes[cacheKey]; ok {
+ return index, nil
+ }
+ first, err := v.store.ReadObjectVersionRange(
+ ctx,
+ candidate.Bucket,
+ candidate.ConsolidatedKey,
+ candidate.CSCBVersionID,
+ 0,
+ retirementInitialIndexReadSize,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to read pinned CSCB index prefix: %w", err)
+ }
+ required, err := cscb.HeaderEnvelopeLength(first)
+ if err != nil {
+ return nil, err
+ }
+ data := first
+ if required > uint64(len(first)) {
+ remaining, err := v.store.ReadObjectVersionRange(
+ ctx,
+ candidate.Bucket,
+ candidate.ConsolidatedKey,
+ candidate.CSCBVersionID,
+ uint64(len(first)),
+ required-uint64(len(first)),
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to read pinned CSCB index remainder: %w", err)
+ }
+ data = make([]byte, 0, required)
+ data = append(data, first...)
+ data = append(data, remaining...)
+ }
+ index, err := cscb.ParseIndex(data)
+ if err != nil {
+ return nil, err
+ }
+ v.indexes[cacheKey] = index
+ return index, nil
+}
+
+func (v *payloadVerifier) readChunk(
+ ctx context.Context,
+ candidate Candidate,
+ index *cscb.Index,
+ chunk *cscb.ChunkDescriptor,
+) ([]byte, error) {
+ cacheKey := candidate.Bucket + "\x00" + candidate.ConsolidatedKey + "\x00" + candidate.CSCBVersionID + "\x00" + strconv.FormatUint(uint64(chunk.Index), 10)
+ if cacheKey == v.lastChunkKey {
+ return v.lastChunkData, nil
+ }
+ frame, err := v.store.ReadObjectVersionRange(
+ ctx,
+ candidate.Bucket,
+ candidate.ConsolidatedKey,
+ candidate.CSCBVersionID,
+ chunk.CompressedPayloadOffset,
+ chunk.CompressedLength,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to read pinned CSCB chunk: %w", err)
+ }
+ data, err := cscb.DecodeChunkFrame(bytes.NewReader(frame), index.Header.Codec)
+ if err != nil {
+ return nil, err
+ }
+ if err := cscb.ValidateChunkPayload(data, chunk); err != nil {
+ return nil, err
+ }
+ v.lastChunkKey = cacheKey
+ v.lastChunkData = data
+ return data, nil
+}
diff --git a/internal/storage/retirement/payload_verifier_test.go b/internal/storage/retirement/payload_verifier_test.go
new file mode 100644
index 00000000..12821a25
--- /dev/null
+++ b/internal/storage/retirement/payload_verifier_test.go
@@ -0,0 +1,278 @@
+package retirement
+
+import (
+ "bytes"
+ "compress/gzip"
+ "context"
+ "crypto/sha256"
+ "encoding/binary"
+ "hash/crc32"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/cscb"
+ storageutils "github.com/coinbase/chainstorage/internal/storage/utils"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestPayloadVerifier_ParsesAndMatchesExactPinnedVersions(t *testing.T) {
+ require := require.New(t)
+ candidate, store, payload := retirementPayloadFixture(t)
+ verifier := newPayloadVerifier(store)
+
+ digest, err := verifier.Verify(context.Background(), candidate)
+ require.NoError(err)
+ var block api.Block
+ require.NoError(proto.Unmarshal(payload, &block))
+ expected, err := canonicalBlockDigest(storageutils.CloneBlockWithoutStoragePlacement(&block))
+ require.NoError(err)
+ require.Equal(expected, digest)
+
+ delete(store.objects, versionObjectKey(candidate.Key, candidate.VersionID))
+ digest, err = verifier.VerifyConsolidated(context.Background(), candidate)
+ require.NoError(err)
+ require.Equal(expected, digest)
+}
+
+func TestPayloadVerifier_RejectsCSCBPayloadWithStoragePlacementMetadata(t *testing.T) {
+ require := require.New(t)
+ candidate, store, payload := retirementPayloadFixture(t)
+
+ var consolidated api.Block
+ require.NoError(proto.Unmarshal(payload, &consolidated))
+ consolidated.Metadata.ObjectKeyMain = candidate.Key
+ consolidated.Metadata.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH
+ consolidated.Metadata.ByteOffset = 123
+ consolidated.Metadata.ByteLength = 456
+ consolidated.Metadata.UncompressedLength = 789
+ consolidatedPayload, err := proto.Marshal(&consolidated)
+ require.NoError(err)
+ candidate.ByteLength = uint64(len(consolidatedPayload))
+ candidate.UncompressedLength = uint64(len(consolidatedPayload))
+ store.objects[versionObjectKey(candidate.ConsolidatedKey, candidate.CSCBVersionID)] = buildSingleBlockCSCB(t, candidate, consolidatedPayload)
+
+ _, err = newPayloadVerifier(store).Verify(context.Background(), candidate)
+ require.Error(err)
+ require.Contains(err.Error(), "retains storage placement metadata")
+
+ _, err = newPayloadVerifier(store).VerifyConsolidated(context.Background(), candidate)
+ require.Error(err)
+ require.Contains(err.Error(), "retains storage placement metadata")
+}
+
+func TestPayloadVerifier_FreshInstanceDoesNotReuseCachedCSCBBytes(t *testing.T) {
+ require := require.New(t)
+ candidate, store, _ := retirementPayloadFixture(t)
+ verifier := newPayloadVerifier(store)
+
+ _, err := verifier.VerifyConsolidated(context.Background(), candidate)
+ require.NoError(err)
+ delete(store.objects, versionObjectKey(candidate.ConsolidatedKey, candidate.CSCBVersionID))
+
+ _, err = verifier.VerifyConsolidated(context.Background(), candidate)
+ require.NoError(err, "the same verifier intentionally caches its parsed index and chunk")
+ _, err = newPayloadVerifier(store).VerifyConsolidated(context.Background(), candidate)
+ require.Error(err)
+}
+
+func TestPayloadVerifier_ParsesSupportedSingleBlockCompression(t *testing.T) {
+ tests := []struct {
+ name string
+ suffix string
+ compression api.Compression
+ }{
+ {name: "none", compression: api.Compression_NONE},
+ {name: "gzip", suffix: storageutils.GzipFileSuffix, compression: api.Compression_GZIP},
+ {name: "zstd", suffix: storageutils.ZstdFileSuffix, compression: api.Compression_ZSTD},
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ require := require.New(t)
+ candidate, store, payload := retirementPayloadFixture(t)
+ delete(store.objects, versionObjectKey(candidate.Key, candidate.VersionID))
+ candidate.Key = "single-block/429600000" + test.suffix
+ compressed, err := storageutils.Compress(payload, test.compression)
+ require.NoError(err)
+ store.objects[versionObjectKey(candidate.Key, candidate.VersionID)] = compressed
+
+ _, err = newPayloadVerifier(store).Verify(context.Background(), candidate)
+ require.NoError(err)
+ })
+ }
+}
+
+func TestPayloadVerifier_RejectsSerializedMismatch(t *testing.T) {
+ require := require.New(t)
+ candidate, store, _ := retirementPayloadFixture(t)
+ different := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ETHEREUM,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: candidate.Tag,
+ Height: candidate.Height,
+ Hash: candidate.Hash,
+ },
+ }
+ differentPayload, err := proto.Marshal(different)
+ require.NoError(err)
+ store.objects[versionObjectKey(candidate.Key, candidate.VersionID)] = gzipPayload(t, differentPayload)
+
+ _, err = newPayloadVerifier(store).Verify(context.Background(), candidate)
+ require.Error(err)
+ require.Contains(err.Error(), "payloads differ")
+}
+
+func TestPayloadVerifier_RejectsMatchingPayloadWithWrongBlockIdentity(t *testing.T) {
+ require := require.New(t)
+ candidate, store, _ := retirementPayloadFixture(t)
+ wrong := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: candidate.Tag,
+ Height: candidate.Height + 1,
+ Hash: candidate.Hash,
+ },
+ }
+ wrongPayload, err := proto.Marshal(wrong)
+ require.NoError(err)
+ store.objects[versionObjectKey(candidate.Key, candidate.VersionID)] = gzipPayload(t, wrongPayload)
+ store.objects[versionObjectKey(candidate.ConsolidatedKey, candidate.CSCBVersionID)] = buildSingleBlockCSCB(t, candidate, wrongPayload)
+
+ _, err = newPayloadVerifier(store).Verify(context.Background(), candidate)
+ require.Error(err)
+ require.Contains(err.Error(), "identity mismatch")
+}
+
+func TestPayloadVerifier_RejectsMalformedSingleBlockAndCSCBPayloads(t *testing.T) {
+ t.Run("single-block protobuf", func(t *testing.T) {
+ require := require.New(t)
+ candidate, store, _ := retirementPayloadFixture(t)
+ store.objects[versionObjectKey(candidate.Key, candidate.VersionID)] = gzipPayload(t, []byte("not-a-protobuf"))
+ _, err := newPayloadVerifier(store).Verify(context.Background(), candidate)
+ require.Error(err)
+ require.Contains(err.Error(), "parse pinned single-block")
+ })
+
+ t.Run("CSCB chunk checksum", func(t *testing.T) {
+ require := require.New(t)
+ candidate, store, _ := retirementPayloadFixture(t)
+ key := versionObjectKey(candidate.ConsolidatedKey, candidate.CSCBVersionID)
+ object := append([]byte(nil), store.objects[key]...)
+ object[len(object)-1] ^= 0xff
+ store.objects[key] = object
+ _, err := newPayloadVerifier(store).Verify(context.Background(), candidate)
+ require.Error(err)
+ })
+}
+
+func retirementPayloadFixture(t *testing.T) (Candidate, *fakeStore, []byte) {
+ t.Helper()
+ require := require.New(t)
+ block := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 2,
+ Height: 429600000,
+ Hash: "solana-hash-429600000",
+ },
+ }
+ payload, err := proto.Marshal(block)
+ require.NoError(err)
+ candidate := Candidate{
+ Bucket: "bucket",
+ Key: "single-block/429600000.gzip",
+ VersionID: "single-block-v1",
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ BlockMetadataID: 9001,
+ Tag: block.Metadata.Tag,
+ ConsolidatedKey: "consolidated/canary.cscb.gzip",
+ CSCBVersionID: "cscb-v1",
+ ByteOffset: 0,
+ ByteLength: uint64(len(payload)),
+ UncompressedLength: uint64(len(payload)),
+ }
+ store := newFakeStore()
+ store.objects[versionObjectKey(candidate.Key, candidate.VersionID)] = gzipPayload(t, payload)
+ store.objects[versionObjectKey(candidate.ConsolidatedKey, candidate.CSCBVersionID)] = buildSingleBlockCSCB(t, candidate, payload)
+ return candidate, store, payload
+}
+
+func buildSingleBlockCSCB(t *testing.T, candidate Candidate, payload []byte) []byte {
+ t.Helper()
+ compressed := gzipPayload(t, payload)
+ envelope := make([]byte, cscb.EnvelopeHeaderSize+cscb.BlockIndexRecordSize+cscb.ChunkIndexRecordSize)
+ copy(envelope[0:4], []byte("ENV1"))
+ binary.LittleEndian.PutUint64(envelope[8:16], 1)
+ binary.LittleEndian.PutUint64(envelope[16:24], 1)
+ binary.LittleEndian.PutUint64(envelope[24:32], candidate.Height)
+ binary.LittleEndian.PutUint64(envelope[32:40], candidate.Height+1)
+ binary.LittleEndian.PutUint64(envelope[40:48], cscb.EnvelopeHeaderSize)
+ binary.LittleEndian.PutUint64(envelope[48:56], cscb.BlockIndexRecordSize)
+ chunkOffset := cscb.EnvelopeHeaderSize + cscb.BlockIndexRecordSize
+ binary.LittleEndian.PutUint64(envelope[56:64], uint64(chunkOffset))
+ binary.LittleEndian.PutUint64(envelope[64:72], cscb.ChunkIndexRecordSize)
+
+ blockRecord := envelope[cscb.EnvelopeHeaderSize:chunkOffset]
+ binary.LittleEndian.PutUint64(blockRecord[0:8], candidate.Height)
+ binary.LittleEndian.PutUint64(blockRecord[8:16], candidate.ByteOffset)
+ binary.LittleEndian.PutUint64(blockRecord[16:24], uint64(len(payload)))
+ binary.LittleEndian.PutUint32(blockRecord[24:28], crc32.ChecksumIEEE(payload))
+ binary.LittleEndian.PutUint32(blockRecord[28:32], 0)
+ binary.LittleEndian.PutUint64(blockRecord[32:40], 0)
+ hash := sha256.Sum256([]byte(candidate.Hash))
+ copy(blockRecord[48:80], hash[:])
+ binary.LittleEndian.PutUint64(blockRecord[80:88], uint64(candidate.BlockMetadataID))
+
+ payloadOffset := uint64(cscb.HeaderSize + len(envelope))
+ chunkRecord := envelope[chunkOffset:]
+ binary.LittleEndian.PutUint32(chunkRecord[0:4], 0)
+ binary.LittleEndian.PutUint64(chunkRecord[8:16], candidate.Height)
+ binary.LittleEndian.PutUint64(chunkRecord[16:24], candidate.Height+1)
+ binary.LittleEndian.PutUint64(chunkRecord[24:32], payloadOffset)
+ binary.LittleEndian.PutUint64(chunkRecord[32:40], uint64(len(compressed)))
+ binary.LittleEndian.PutUint64(chunkRecord[40:48], 0)
+ binary.LittleEndian.PutUint64(chunkRecord[48:56], uint64(len(payload)))
+ binary.LittleEndian.PutUint32(chunkRecord[56:60], crc32.ChecksumIEEE(payload))
+ binary.LittleEndian.PutUint32(chunkRecord[60:64], 1)
+
+ header := make([]byte, cscb.HeaderSize)
+ copy(header[0:4], []byte("CSCB"))
+ header[4] = 1
+ header[5] = 1
+ header[6] = 2
+ binary.LittleEndian.PutUint32(header[8:12], 1)
+ binary.LittleEndian.PutUint32(header[12:16], 1)
+ binary.LittleEndian.PutUint64(header[16:24], candidate.Height)
+ binary.LittleEndian.PutUint64(header[24:32], candidate.Height+1)
+ binary.LittleEndian.PutUint64(header[32:40], cscb.HeaderSize)
+ binary.LittleEndian.PutUint64(header[40:48], uint64(len(envelope)))
+ binary.LittleEndian.PutUint32(header[48:52], crc32.ChecksumIEEE(envelope))
+ binary.LittleEndian.PutUint32(header[52:56], cscb.BlockIndexRecordSize)
+ binary.LittleEndian.PutUint32(header[56:60], cscb.ChunkIndexRecordSize)
+ binary.LittleEndian.PutUint64(header[60:68], payloadOffset)
+ binary.LittleEndian.PutUint64(header[68:76], uint64(len(compressed)))
+ binary.LittleEndian.PutUint64(header[76:84], uint64(len(payload)))
+ binary.LittleEndian.PutUint32(header[84:88], crc32.ChecksumIEEE(payload))
+
+ object := make([]byte, 0, len(header)+len(envelope)+len(compressed))
+ object = append(object, header...)
+ object = append(object, envelope...)
+ object = append(object, compressed...)
+ return object
+}
+
+func gzipPayload(t *testing.T, payload []byte) []byte {
+ t.Helper()
+ var buffer bytes.Buffer
+ writer := gzip.NewWriter(&buffer)
+ _, err := writer.Write(payload)
+ require.NoError(t, err)
+ require.NoError(t, writer.Close())
+ return buffer.Bytes()
+}
diff --git a/internal/storage/retirement/planner.go b/internal/storage/retirement/planner.go
new file mode 100644
index 00000000..5905a7c0
--- /dev/null
+++ b/internal/storage/retirement/planner.go
@@ -0,0 +1,1579 @@
+package retirement
+
+import (
+ "context"
+ cryptorand "crypto/rand"
+ "crypto/sha256"
+ "encoding/hex"
+ "errors"
+ "sort"
+ "strconv"
+ "strings"
+ "time"
+
+ "golang.org/x/xerrors"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+const (
+ sha256HexLength = sha256.Size * 2
+ cscbFormatMetadataKey = "chainstorage-format"
+ cscbCompressionScopeMetadataKey = "chainstorage-compression-scope"
+ cscbUncompressedLengthMetadataKey = "chainstorage-uncompressed-length"
+ cscbFormatMetadataValue = "cscb"
+ cscbCompressionScopeMetadataValue = "batch-chunked"
+ retirementClaimTokenBytes = 16
+ RetirementClaimLease = 15 * time.Minute
+ RetentionSafetyQuiescencePeriod = 15 * time.Minute
+ s3MutableNullVersionID = "null"
+ versionedDeleteMode = "exact_pinned_versions_and_delete_markers"
+)
+
+var unverifiedRetentionSafetySHA256 = keySHA256("unverified-retention-safety-configuration")
+
+type Planner struct {
+ repo Repository
+ store ObjectStore
+ verifierFactory func() blockPayloadVerifier
+ heartbeat func(ctx context.Context, details ...any)
+}
+
+type PlannerOption func(*Planner)
+
+// WithHeartbeat wires a liveness callback invoked from the long-running
+// row/version loops, so a Temporal activity heartbeat timeout can bound the
+// planner and deliver cancellation between destructive steps.
+func WithHeartbeat(heartbeat func(ctx context.Context, details ...any)) PlannerOption {
+ return func(p *Planner) {
+ p.heartbeat = heartbeat
+ }
+}
+
+func NewPlanner(repo Repository, store ObjectStore, opts ...PlannerOption) *Planner {
+ p := &Planner{
+ repo: repo,
+ store: store,
+ verifierFactory: func() blockPayloadVerifier {
+ return newPayloadVerifier(store)
+ },
+ }
+ for _, opt := range opts {
+ opt(p)
+ }
+ return p
+}
+
+func (p *Planner) recordHeartbeat(ctx context.Context, details ...any) {
+ if p.heartbeat != nil {
+ p.heartbeat(ctx, details...)
+ }
+}
+
+func (p *Planner) Plan(ctx context.Context, req PlanRequest) (*Report, error) {
+ if err := normalizeRequest(&req); err != nil {
+ return nil, err
+ }
+ rows, err := p.repo.ListMetadataRows(ctx, req.Tag, req.StartHeight, req.EndHeight, req.Limit)
+ if err != nil {
+ return nil, err
+ }
+
+ report := newReport(req)
+ cscbHeads := make(map[string]ObjectHead)
+ singleBlockTopologies := make(map[string]ObjectVersionTopology)
+ writeOncePolicies := make(map[string]bool)
+ verifier := p.verifierFactory()
+ for _, row := range rows {
+ if err := ctx.Err(); err != nil {
+ return nil, xerrors.Errorf("single-block retention plan canceled at height %d: %w", row.Height, err)
+ }
+ p.recordHeartbeat(ctx, "retirement.plan.row", row.Height)
+ item := p.planRow(ctx, req, row, cscbHeads, singleBlockTopologies, writeOncePolicies, verifier)
+ report.Items = append(report.Items, item)
+ }
+ report.SafetyGates.CSCBWriteOncePolicyVerified = writeOncePolicyGateVerified(report.Items)
+ report.Summary = summarize(report.Items)
+ return report, nil
+}
+
+func (p *Planner) Apply(ctx context.Context, req PlanRequest, report *Report) error {
+ if report == nil {
+ return xerrors.New("report is required")
+ }
+ if err := normalizeRequest(&req); err != nil {
+ return err
+ }
+ if !req.Execute {
+ return xerrors.New("execute must be enabled to apply a retirement report")
+ }
+ if !req.SingleBlockWritersGuarded {
+ return xerrors.New("retirement execution requires all single-block writers to honor the retirement fence")
+ }
+ if isProduction(req.Environment) && !req.ProductionDeleteEnabled {
+ return xerrors.New("production deletion requires explicit production-delete confirmation")
+ }
+ if err := validateApplyReport(req, report); err != nil {
+ return err
+ }
+ if err := p.preflightApplyRetentionSafety(ctx, report); err != nil {
+ report.SafetyGates.CSCBWriteOncePolicyVerified = writeOncePolicyGateVerified(report.Items)
+ report.Summary = summarize(report.Items)
+ return err
+ }
+
+ verifier := p.verifierFactory()
+ var firstErr error
+ halted := false
+ for i := range report.Items {
+ item := &report.Items[i]
+ if item.Action != ActionDeleteObjectVersion {
+ continue
+ }
+ if !halted {
+ if ctxErr := ctx.Err(); ctxErr != nil {
+ if firstErr == nil {
+ firstErr = xerrors.Errorf("single-block retirement apply canceled: %w", ctxErr)
+ }
+ halted = true
+ }
+ }
+ if halted {
+ item.Action = ActionSkip
+ item.SkipReason = SkipNotAttemptedAfterFailure
+ continue
+ }
+ p.recordHeartbeat(ctx, "retirement.apply.row", item.Height)
+ reason, err := p.applyCandidate(ctx, req, item, verifier)
+ if reason != "" {
+ if item.RetirementState != RetirementStateDeletedPendingVerification {
+ item.Action = ActionSkip
+ }
+ item.SkipReason = reason
+ item.RetirementOutcome = reason
+ }
+ if err != nil {
+ if firstErr == nil {
+ firstErr = err
+ }
+ halted = true
+ }
+ }
+ report.SafetyGates.CSCBWriteOncePolicyVerified = writeOncePolicyGateVerified(report.Items)
+ report.Summary = summarize(report.Items)
+ return firstErr
+}
+
+func (p *Planner) Reconcile(ctx context.Context, req PlanRequest) (*Report, error) {
+ if err := normalizeRequest(&req); err != nil {
+ return nil, err
+ }
+ if req.Execute && isProduction(req.Environment) && !req.ProductionDeleteEnabled {
+ return nil, xerrors.New("production reconciliation requires explicit production-delete confirmation")
+ }
+ if req.Execute && !req.SingleBlockWritersGuarded {
+ return nil, xerrors.New("retirement reconciliation requires all single-block writers to honor the retirement fence")
+ }
+ manifests, err := p.repo.ListPendingRetirements(
+ ctx,
+ req.Tag,
+ req.StartHeight,
+ req.EndHeight,
+ req.EligibilityCutoff,
+ req.Limit,
+ )
+ if err != nil {
+ return nil, err
+ }
+ report := newReport(req)
+ var firstErr error
+ halted := false
+ for _, manifest := range manifests {
+ item := candidateFromManifest(manifest)
+ if !halted {
+ if ctxErr := ctx.Err(); ctxErr != nil {
+ if firstErr == nil {
+ firstErr = xerrors.Errorf("single-block retirement reconciliation canceled: %w", ctxErr)
+ }
+ halted = true
+ }
+ }
+ if halted {
+ item.Action = ActionSkip
+ item.SkipReason = SkipNotAttemptedAfterFailure
+ report.Items = append(report.Items, item)
+ continue
+ }
+ p.recordHeartbeat(ctx, "retirement.reconcile.manifest", item.Height)
+ if manifest.Bucket != req.Bucket || !requestAllowsCandidate(req, item) ||
+ manifest.State != RetirementStateEligible &&
+ manifest.State != RetirementStateDeleting &&
+ manifest.State != RetirementStateDeletedPendingVerification ||
+ !isSHA256Hex(manifest.SingleBlockObjectKeySHA256) || !isSHA256Hex(manifest.PayloadSHA256) ||
+ !validPinnedVersionIDs(
+ firstString(manifest.SingleBlockObjectVersionIDs),
+ manifest.SingleBlockObjectVersionIDs,
+ manifest.SingleBlockDeleteMarkerVersionIDs,
+ ) {
+ markCandidateBlocked(&item, SkipMetadataChanged)
+ report.Items = append(report.Items, item)
+ continue
+ }
+ if approvalGateBlocked(req) {
+ markCandidateBlocked(&item, SkipChainRangeNotApproved)
+ report.Items = append(report.Items, item)
+ continue
+ }
+ if req.FallbackErrorCount > 0 {
+ markCandidateBlocked(&item, SkipActiveFallbackOrReadErrors)
+ report.Items = append(report.Items, item)
+ continue
+ }
+ if req.Execute && !req.DirectStorageClientsGuarded {
+ markCandidateBlocked(&item, SkipDirectStorageClientsNotGuarded)
+ report.Items = append(report.Items, item)
+ continue
+ }
+ if !req.Execute {
+ if reason, err := p.inspectCSCBRetentionSafety(ctx, &item); err != nil {
+ markCandidateBlocked(&item, reason)
+ report.Items = append(report.Items, item)
+ continue
+ }
+ if item.RetirementState == RetirementStateDeletedPendingVerification {
+ item.Action = ActionDeletedObjectVersion
+ item.SkipReason = SkipRetirementVerificationPending
+ } else {
+ item.Action = ActionReportOnly
+ }
+ report.Items = append(report.Items, item)
+ continue
+ }
+ if reason, err := p.verifyCSCBRetentionSafety(ctx, &item); err != nil {
+ markCandidateBlocked(&item, reason)
+ item.RetirementOutcome = reason
+ report.Items = append(report.Items, item)
+ if firstErr == nil {
+ firstErr = err
+ }
+ if reason != SkipCSCBSafetyQuiescenceActive {
+ halted = true
+ }
+ continue
+ }
+ reason, err := p.reconcileManifest(ctx, req, &item, manifest)
+ if reason != "" {
+ if item.RetirementState != RetirementStateDeletedPendingVerification {
+ item.Action = ActionSkip
+ }
+ item.SkipReason = reason
+ item.RetirementOutcome = reason
+ }
+ if err != nil {
+ if firstErr == nil {
+ firstErr = err
+ }
+ halted = true
+ }
+ report.Items = append(report.Items, item)
+ }
+ report.SafetyGates.CSCBWriteOncePolicyVerified = writeOncePolicyGateVerified(report.Items)
+ report.Summary = summarize(report.Items)
+ return report, firstErr
+}
+
+func (p *Planner) planRow(
+ ctx context.Context,
+ req PlanRequest,
+ row MetadataRow,
+ cscbHeads map[string]ObjectHead,
+ singleBlockTopologies map[string]ObjectVersionTopology,
+ writeOncePolicies map[string]bool,
+ verifier blockPayloadVerifier,
+) Candidate {
+ item := Candidate{
+ Bucket: req.Bucket,
+ Key: row.SingleBlockObjectKey,
+ BlockMetadataID: row.BlockMetadataID,
+ Tag: row.Tag,
+ Height: row.Height,
+ Hash: row.Hash,
+ ByteOffset: row.PrimaryByteOffset,
+ ByteLength: row.PrimaryByteLength,
+ UncompressedLength: row.PrimaryUncompressedLength,
+ Action: ActionSkip,
+ }
+ if row.Retirement != nil {
+ item.RetirementState = row.Retirement.State
+ item.RetirementAttempts = row.Retirement.AttemptCount
+ item.RetirementOutcome = row.Retirement.Outcome
+ if row.Retirement.State == RetirementStateDeletedVerified {
+ if !finalizedRetirementMatchesRow(row) {
+ item.SkipReason = SkipMetadataChanged
+ return item
+ }
+ item.Key = ""
+ item.VersionID = firstString(row.Retirement.SingleBlockObjectVersionIDs)
+ item.VersionIDs = append([]string(nil), row.Retirement.SingleBlockObjectVersionIDs...)
+ item.DeleteMarkerVersionIDs = append([]string(nil), row.Retirement.SingleBlockDeleteMarkerVersionIDs...)
+ item.SingleBlockVersions = len(item.VersionIDs)
+ item.DeleteMarkers = len(item.DeleteMarkerVersionIDs)
+ item.SingleBlockETag = row.Retirement.SingleBlockObjectETag
+ item.SingleBlockBytes = row.Retirement.SingleBlockObjectBytes
+ item.ConsolidatedKey = row.Retirement.ConsolidatedObjectKey
+ item.CSCBVersionID = row.Retirement.ConsolidatedObjectVersionID
+ item.CSCBETag = row.Retirement.ConsolidatedObjectETag
+ item.PayloadSHA256 = row.Retirement.PayloadSHA256
+ item.SingleBlockDeletedAt = row.Retirement.DeletedAt
+ item.RetirementVerifiedAt = row.Retirement.VerifiedAt
+ item.Action = ActionAlreadyDeleted
+ item.SkipReason = SkipRetirementAlreadyFinalized
+ return item
+ }
+ if row.Retirement.State == RetirementStateDeletedPendingVerification {
+ if !pendingVerificationRetirementMatchesRow(row) {
+ item.SkipReason = SkipMetadataChanged
+ return item
+ }
+ item.Key = ""
+ item.VersionID = firstString(row.Retirement.SingleBlockObjectVersionIDs)
+ item.VersionIDs = append([]string(nil), row.Retirement.SingleBlockObjectVersionIDs...)
+ item.DeleteMarkerVersionIDs = append([]string(nil), row.Retirement.SingleBlockDeleteMarkerVersionIDs...)
+ item.SingleBlockVersions = len(item.VersionIDs)
+ item.DeleteMarkers = len(item.DeleteMarkerVersionIDs)
+ item.SingleBlockBytes = row.Retirement.SingleBlockObjectBytes
+ item.ConsolidatedKey = row.Retirement.ConsolidatedObjectKey
+ item.CSCBVersionID = row.Retirement.ConsolidatedObjectVersionID
+ item.CSCBETag = row.Retirement.ConsolidatedObjectETag
+ item.PayloadSHA256 = row.Retirement.PayloadSHA256
+ item.SingleBlockDeletedAt = row.Retirement.DeletedAt
+ item.Action = ActionDeletedObjectVersion
+ item.SkipReason = SkipRetirementVerificationPending
+ return item
+ }
+ }
+ if row.Skipped {
+ item.SkipReason = SkipSkippedBlock
+ return item
+ }
+ if row.Shadow == nil {
+ item.SkipReason = SkipMissingConsolidationShadow
+ return item
+ }
+ if row.SingleBlockObjectKey == "" {
+ item.SkipReason = SkipMissingSingleBlockKey
+ return item
+ }
+ shadow := row.Shadow
+ item.ConsolidatedKey = shadow.ConsolidatedObjectKey
+ item.ValidatedAt = shadow.ValidatedAt
+ item.RetiredAt = shadow.SingleBlockRetentionStartedAt
+ if shadow.SingleBlockDeleteAfter != nil {
+ eligibleAt := *shadow.SingleBlockDeleteAfter
+ item.EligibleAt = &eligibleAt
+ }
+ if shadow.ValidatedAt == nil {
+ item.SkipReason = SkipValidationNotPassed
+ return item
+ }
+ if !isPrimaryConsolidated(row) {
+ item.SkipReason = SkipActiveMetadataStillSingleBlock
+ return item
+ }
+ if !validShadowReference(row, shadow) {
+ item.SkipReason = SkipInvalidMetadataReference
+ return item
+ }
+ if shadow.SingleBlockRetentionStartedAt == nil || shadow.SingleBlockDeleteAfter == nil {
+ item.SkipReason = SkipMissingRetirementMarker
+ return item
+ }
+ if item.EligibleAt != nil && req.EligibilityCutoff.Before(*item.EligibleAt) {
+ item.SkipReason = SkipRetentionPeriodActive
+ return item
+ }
+ if approvalGateBlocked(req) {
+ item.SkipReason = SkipChainRangeNotApproved
+ return item
+ }
+ if req.FallbackErrorCount > 0 {
+ item.SkipReason = SkipActiveFallbackOrReadErrors
+ return item
+ }
+ if req.Execute && !req.DirectStorageClientsGuarded {
+ item.SkipReason = SkipDirectStorageClientsNotGuarded
+ return item
+ }
+ if req.Execute && !req.SingleBlockWritersGuarded {
+ item.SkipReason = SkipSingleBlockWritersNotGuarded
+ return item
+ }
+
+ topology, ok := singleBlockTopologies[row.SingleBlockObjectKey]
+ if !ok {
+ var err error
+ topology, err = p.store.ListObjectVersions(ctx, req.Bucket, row.SingleBlockObjectKey)
+ if err != nil {
+ item.SkipReason = SkipObjectInspectionFailed
+ return item
+ }
+ singleBlockTopologies[row.SingleBlockObjectKey] = topology
+ }
+ item.SingleBlockVersions = len(topology.Versions)
+ item.DeleteMarkers = len(topology.DeleteMarkers)
+ if len(topology.Versions) == 0 && len(topology.DeleteMarkers) == 0 {
+ item.SkipReason = SkipSingleBlockObjectMissing
+ return item
+ }
+ version, versionIDs, deleteMarkerVersionIDs, reason := pinSingleBlockTopology(topology)
+ if reason != "" {
+ item.SkipReason = reason
+ return item
+ }
+ item.VersionID = version.VersionID
+ item.VersionIDs = versionIDs
+ item.DeleteMarkerVersionIDs = deleteMarkerVersionIDs
+ item.SingleBlockETag = version.ETag
+ item.SingleBlockBytes = version.Bytes
+
+ head, ok := cscbHeads[shadow.ConsolidatedObjectKey]
+ if !ok {
+ var err error
+ head, err = p.store.HeadObject(ctx, req.Bucket, shadow.ConsolidatedObjectKey)
+ if err != nil {
+ item.SkipReason = SkipObjectInspectionFailed
+ return item
+ }
+ cscbHeads[shadow.ConsolidatedObjectKey] = head
+ }
+ if !head.Exists || head.DeleteMark || !isImmutableVersionID(head.VersionID) || head.ETag == "" {
+ item.SkipReason = SkipMissingCSCBObject
+ return item
+ }
+ if head.Expiration != "" {
+ item.SkipReason = SkipCSCBLifecycleExpirationActive
+ return item
+ }
+ item.CSCBVersionID = head.VersionID
+ item.CSCBETag = head.ETag
+ logicalBytes, ok := cscbLogicalPayloadBytes(head)
+ if head.Bytes == 0 || !ok || shadow.ByteOffset > logicalBytes || shadow.ByteLength > logicalBytes-shadow.ByteOffset {
+ item.SkipReason = SkipInvalidMetadataReference
+ return item
+ }
+ item.SingleBlockKeySHA256 = keySHA256(item.Key)
+ payloadSHA256, err := verifier.Verify(ctx, item)
+ if err != nil {
+ item.SkipReason = SkipSingleBlockPayloadMismatch
+ return item
+ }
+ item.PayloadSHA256 = payloadSHA256
+ writeOncePolicyVerified, checked := writeOncePolicies[shadow.ConsolidatedObjectKey]
+ if !checked {
+ _, policyErr := p.store.InspectObjectRetentionSafety(ctx, req.Bucket, shadow.ConsolidatedObjectKey)
+ writeOncePolicyVerified = policyErr == nil
+ writeOncePolicies[shadow.ConsolidatedObjectKey] = writeOncePolicyVerified
+ }
+ item.CSCBWriteOncePolicy = writeOncePolicyVerified
+ if !writeOncePolicyVerified {
+ item.SkipReason = SkipCSCBWriteOncePolicyUnverified
+ return item
+ }
+
+ if req.Execute {
+ if isProduction(req.Environment) && !req.ProductionDeleteEnabled {
+ item.SkipReason = SkipProductionDeletionDisabled
+ return item
+ }
+ item.Action = ActionDeleteObjectVersion
+ item.SkipReason = ""
+ return item
+ }
+ item.Action = ActionReportOnly
+ item.SkipReason = ""
+ return item
+}
+
+func (p *Planner) applyCandidate(ctx context.Context, req PlanRequest, item *Candidate, verifier blockPayloadVerifier) (string, error) {
+ payloadSHA256, reason, err := p.revalidateCandidate(ctx, req, *item, verifier)
+ if err != nil {
+ return reason, err
+ }
+ item.PayloadSHA256 = payloadSHA256
+ item.SingleBlockKeySHA256 = keySHA256(item.Key)
+ manifest := manifestFromCandidate(*item, req.Now)
+ if err := p.repo.PrepareRetirement(ctx, manifest); err != nil {
+ return SkipMetadataChanged, err
+ }
+ return p.withRetirementClaim(ctx, item, func(claimToken string) (string, error) {
+ preDeleteSHA256, reason, err := p.revalidateCandidate(ctx, req, *item, verifier)
+ if err != nil {
+ return reason, err
+ }
+ if preDeleteSHA256 != item.PayloadSHA256 {
+ return SkipSingleBlockPayloadMismatch, xerrors.Errorf("payload digest changed immediately before retirement: block_metadata_id=%d", item.BlockMetadataID)
+ }
+ if err := p.renewRetirementClaim(ctx, item.BlockMetadataID, claimToken); err != nil {
+ return SkipRetirementClaimActive, err
+ }
+ if reason, err := p.verifyCSCBRetentionSafety(ctx, item); err != nil {
+ return reason, err
+ }
+ if reason, err := p.deletePinnedSingleBlockTopology(ctx, item, claimToken); err != nil {
+ return reason, err
+ }
+ return p.completeObjectDeletion(ctx, req, item, claimToken, item.PayloadSHA256)
+ })
+}
+
+func (p *Planner) reconcileManifest(
+ ctx context.Context,
+ req PlanRequest,
+ item *Candidate,
+ manifest RetirementManifest,
+) (string, error) {
+ if manifest.State == RetirementStateDeletedPendingVerification {
+ if manifest.SingleBlockObjectKey != "" || manifest.SingleBlockObjectETag != "" || manifest.DeletedAt == nil || manifest.VerifiedAt != nil {
+ return SkipMetadataChanged, xerrors.Errorf("pending-verification retirement manifest is inconsistent: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ return p.withRetirementClaim(ctx, item, func(claimToken string) (string, error) {
+ if reason, err := p.verifyCSCBRetentionSafety(ctx, item); err != nil {
+ return reason, err
+ }
+ return p.finalizeRecordedDeletion(ctx, req, item, claimToken, manifest.PayloadSHA256)
+ })
+ }
+ if manifest.SingleBlockObjectKey == "" || keySHA256(manifest.SingleBlockObjectKey) != manifest.SingleBlockObjectKeySHA256 {
+ return SkipMetadataChanged, xerrors.Errorf("retirement manifest single-block key hash mismatch: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ if manifest.State == RetirementStateEligible {
+ topology, err := p.store.ListObjectVersions(ctx, manifest.Bucket, manifest.SingleBlockObjectKey)
+ if err != nil {
+ return SkipObjectInspectionFailed, err
+ }
+ if len(topology.Versions) == 0 && len(topology.DeleteMarkers) == 0 {
+ return SkipMetadataChanged, xerrors.Errorf("single-block object is absent before any delete attempt: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ if !topologyMatchesManifest(topology, manifest) {
+ return SkipUnsafeSingleBlockVersionTopology, xerrors.Errorf("eligible retirement topology changed: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ }
+ return p.withRetirementClaim(ctx, item, func(claimToken string) (string, error) {
+ topology, err := p.store.ListObjectVersions(ctx, manifest.Bucket, manifest.SingleBlockObjectKey)
+ if err != nil {
+ return SkipObjectInspectionFailed, err
+ }
+ if len(topology.Versions) == 0 && len(topology.DeleteMarkers) == 0 {
+ if manifest.State != RetirementStateDeleting {
+ return SkipMetadataChanged, xerrors.Errorf("single-block object is absent while retirement state is %s", manifest.State)
+ }
+ if reason, err := p.verifyCSCBRetentionSafety(ctx, item); err != nil {
+ return reason, err
+ }
+ return p.completeObjectDeletion(ctx, req, item, claimToken, manifest.PayloadSHA256)
+ }
+ if manifest.State == RetirementStateEligible && !topologyMatchesManifest(topology, manifest) ||
+ manifest.State == RetirementStateDeleting && !topologyIsPinnedSubset(topology, manifest) {
+ return SkipUnsafeSingleBlockVersionTopology, xerrors.Errorf("pending retirement topology changed: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ verifier := p.verifierFactory()
+ verify := p.revalidateCandidate
+ if manifest.State == RetirementStateDeleting {
+ verify = p.revalidateConsolidatedCandidate
+ }
+ payloadSHA256, reason, err := verify(ctx, req, *item, verifier)
+ if err != nil {
+ return reason, err
+ }
+ if payloadSHA256 != manifest.PayloadSHA256 {
+ return SkipSingleBlockPayloadMismatch, xerrors.Errorf("pending retirement payload digest changed: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ preDeleteSHA256, reason, err := verify(ctx, req, *item, verifier)
+ if err != nil {
+ return reason, err
+ }
+ if preDeleteSHA256 != manifest.PayloadSHA256 {
+ return SkipSingleBlockPayloadMismatch, xerrors.Errorf("pending retirement payload digest changed immediately before deletion: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ if err := p.renewRetirementClaim(ctx, item.BlockMetadataID, claimToken); err != nil {
+ return SkipRetirementClaimActive, err
+ }
+ if reason, err := p.verifyCSCBRetentionSafety(ctx, item); err != nil {
+ return reason, err
+ }
+ if reason, err := p.deletePinnedSingleBlockTopology(ctx, item, claimToken); err != nil {
+ return reason, err
+ }
+ return p.completeObjectDeletion(ctx, req, item, claimToken, manifest.PayloadSHA256)
+ })
+}
+
+func (p *Planner) verifyCSCBRetentionSafety(ctx context.Context, item *Candidate) (string, error) {
+ item.CSCBWriteOncePolicy = false
+ snapshot, inspectionErr := p.store.InspectObjectRetentionSafety(ctx, item.Bucket, item.ConsolidatedKey)
+ configurationSHA256 := snapshot.ConfigurationSHA256
+ if inspectionErr != nil || !isSHA256Hex(configurationSHA256) {
+ configurationSHA256 = unverifiedRetentionSafetySHA256
+ }
+ firstObservedAt, observedAt, observationErr := p.repo.ObserveRetentionSafety(
+ ctx,
+ item.Bucket,
+ item.ConsolidatedKey,
+ configurationSHA256,
+ )
+ if observationErr != nil {
+ return SkipCSCBWriteOncePolicyUnverified, errors.Join(inspectionErr, observationErr)
+ }
+ if inspectionErr != nil {
+ return SkipCSCBWriteOncePolicyUnverified, inspectionErr
+ }
+ item.CSCBWriteOncePolicy = true
+ if observedAt.Sub(firstObservedAt) < RetentionSafetyQuiescencePeriod {
+ return SkipCSCBSafetyQuiescenceActive, xerrors.Errorf(
+ "CSCB retention safety configuration has not remained stable for %s: bucket=%s key=%s first_observed_at=%s observed_at=%s",
+ RetentionSafetyQuiescencePeriod,
+ item.Bucket,
+ item.ConsolidatedKey,
+ firstObservedAt.Format(time.RFC3339Nano),
+ observedAt.Format(time.RFC3339Nano),
+ )
+ }
+ return "", nil
+}
+
+func (p *Planner) inspectCSCBRetentionSafety(ctx context.Context, item *Candidate) (string, error) {
+ item.CSCBWriteOncePolicy = false
+ if _, err := p.store.InspectObjectRetentionSafety(ctx, item.Bucket, item.ConsolidatedKey); err != nil {
+ return SkipCSCBWriteOncePolicyUnverified, err
+ }
+ item.CSCBWriteOncePolicy = true
+ return "", nil
+}
+
+func (p *Planner) preflightApplyRetentionSafety(ctx context.Context, report *Report) error {
+ type safetyResult struct {
+ reason string
+ err error
+ }
+ results := make(map[string]safetyResult)
+ var firstErr error
+ for i := range report.Items {
+ item := &report.Items[i]
+ if item.Action != ActionDeleteObjectVersion {
+ continue
+ }
+ result, ok := results[item.ConsolidatedKey]
+ if !ok {
+ result.reason, result.err = p.verifyCSCBRetentionSafety(ctx, item)
+ results[item.ConsolidatedKey] = result
+ } else if result.err == nil {
+ item.CSCBWriteOncePolicy = true
+ }
+ if result.err == nil {
+ continue
+ }
+ item.Action = ActionSkip
+ item.SkipReason = result.reason
+ item.RetirementOutcome = result.reason
+ if firstErr == nil {
+ firstErr = result.err
+ }
+ }
+ return firstErr
+}
+
+func (p *Planner) withRetirementClaim(
+ ctx context.Context,
+ item *Candidate,
+ operation func(claimToken string) (string, error),
+) (reason string, err error) {
+ claimToken, err := newRetirementClaimToken()
+ if err != nil {
+ return SkipMetadataChanged, err
+ }
+ claimedAt := time.Now().UTC()
+ if err := p.repo.ClaimRetirement(ctx, item.BlockMetadataID, claimToken, claimedAt, claimedAt.Add(RetirementClaimLease)); err != nil {
+ if errors.Is(err, ErrRetirementClaimUnavailable) {
+ return SkipRetirementClaimActive, nil
+ }
+ return SkipMetadataChanged, err
+ }
+ if item.RetirementState == RetirementStateDeletedPendingVerification {
+ item.RetirementOutcome = "verification_started"
+ } else {
+ item.RetirementState = RetirementStateDeleting
+ item.RetirementOutcome = "delete_started"
+ }
+ item.RetirementAttempts++
+
+ claimOwned := true
+ defer func() {
+ if err == nil || !claimOwned {
+ return
+ }
+ outcome := reason
+ if outcome == "" {
+ outcome = SkipMetadataChanged
+ }
+ // Record the interruption outcome even when the failure is a
+ // cancellation, so a stopped run leaves a durable trace instead of an
+ // unexplained claim that only expires by lease.
+ if outcomeErr := p.repo.RecordRetirementOutcome(context.WithoutCancel(ctx), item.BlockMetadataID, claimToken, outcome, time.Now().UTC()); outcomeErr != nil {
+ err = errors.Join(err, outcomeErr)
+ }
+ }()
+
+ reason, err = operation(claimToken)
+ if err == nil {
+ claimOwned = false
+ }
+ return reason, err
+}
+
+func (p *Planner) renewRetirementClaim(ctx context.Context, blockMetadataID int64, claimToken string) error {
+ renewedAt := time.Now().UTC()
+ return p.repo.RenewRetirementClaim(ctx, blockMetadataID, claimToken, renewedAt, renewedAt.Add(RetirementClaimLease))
+}
+
+func (p *Planner) deletePinnedSingleBlockTopology(
+ ctx context.Context,
+ item *Candidate,
+ claimToken string,
+) (string, error) {
+ topology, err := p.store.ListObjectVersions(ctx, item.Bucket, item.Key)
+ if err != nil {
+ return SkipObjectInspectionFailed, err
+ }
+ if !topologyIsPinnedCandidateSubset(topology, *item) {
+ return SkipUnsafeSingleBlockVersionTopology, xerrors.Errorf(
+ "single-block object version topology changed before exact deletion: block_metadata_id=%d",
+ item.BlockMetadataID,
+ )
+ }
+
+ remainingVersions := make(map[string]struct{}, len(topology.Versions))
+ for _, version := range topology.Versions {
+ remainingVersions[version.VersionID] = struct{}{}
+ }
+ remainingMarkers := make(map[string]struct{}, len(topology.DeleteMarkers))
+ for _, marker := range topology.DeleteMarkers {
+ remainingMarkers[marker.VersionID] = struct{}{}
+ }
+ deleteVersion := func(versionID string) (string, error) {
+ if err := ctx.Err(); err != nil {
+ return SkipRetentionRunCanceled, xerrors.Errorf(
+ "single-block version deletion canceled before version %s: %w",
+ versionID,
+ err,
+ )
+ }
+ p.recordHeartbeat(ctx, "retirement.delete.version", item.Height, versionID)
+ if err := p.renewRetirementClaim(ctx, item.BlockMetadataID, claimToken); err != nil {
+ return SkipRetirementClaimActive, err
+ }
+ if err := p.store.DeleteObjectVersion(ctx, item.Bucket, item.Key, versionID); err != nil {
+ return SkipVersionedDeleteFailed, err
+ }
+ return "", nil
+ }
+
+ // Keep the payload-validation version available until every older data
+ // version is gone, then remove delete markers only after all data versions.
+ for _, versionID := range item.VersionIDs[1:] {
+ if _, ok := remainingVersions[versionID]; !ok {
+ continue
+ }
+ if reason, err := deleteVersion(versionID); err != nil {
+ return reason, err
+ }
+ }
+ if _, ok := remainingVersions[item.VersionID]; ok {
+ if reason, err := deleteVersion(item.VersionID); err != nil {
+ return reason, err
+ }
+ }
+ for _, versionID := range item.DeleteMarkerVersionIDs {
+ if _, ok := remainingMarkers[versionID]; !ok {
+ continue
+ }
+ if reason, err := deleteVersion(versionID); err != nil {
+ return reason, err
+ }
+ }
+ return "", nil
+}
+
+func (p *Planner) completeObjectDeletion(
+ ctx context.Context,
+ req PlanRequest,
+ item *Candidate,
+ claimToken string,
+ expectedPayloadSHA256 string,
+) (string, error) {
+ if err := p.verifySingleBlockDeleted(ctx, item.Bucket, item.Key); err != nil {
+ return SkipPostDeleteVerificationFailed, err
+ }
+ deletedAt, err := p.repo.RecordRetirementObjectDeleted(
+ ctx,
+ item.BlockMetadataID,
+ claimToken,
+ ActionDeletedObjectVersion,
+ )
+ if err != nil {
+ return SkipPostDeleteVerificationFailed, err
+ }
+ item.Action = ActionDeletedObjectVersion
+ item.SkipReason = ""
+ item.RetirementState = RetirementStateDeletedPendingVerification
+ item.RetirementOutcome = ActionDeletedObjectVersion
+ item.SingleBlockDeletedAt = &deletedAt
+ item.Key = ""
+ item.SingleBlockETag = ""
+ return p.finalizeRecordedDeletion(ctx, req, item, claimToken, expectedPayloadSHA256)
+}
+
+func (p *Planner) finalizeRecordedDeletion(
+ ctx context.Context,
+ req PlanRequest,
+ item *Candidate,
+ claimToken string,
+ expectedPayloadSHA256 string,
+) (string, error) {
+ if err := p.renewRetirementClaim(ctx, item.BlockMetadataID, claimToken); err != nil {
+ return SkipRetirementClaimActive, err
+ }
+ verifier := p.verifierFactory()
+ payloadSHA256, reason, err := p.revalidateConsolidatedCandidate(ctx, req, *item, verifier)
+ if err != nil {
+ return reason, err
+ }
+ if payloadSHA256 != expectedPayloadSHA256 {
+ return SkipSingleBlockPayloadMismatch, xerrors.Errorf("pinned CSCB payload digest changed after single-block deletion: block_metadata_id=%d", item.BlockMetadataID)
+ }
+ verifiedAt, err := p.repo.FinalizeRetirement(ctx, item.BlockMetadataID, claimToken, ActionDeletedVerified)
+ if err != nil {
+ return SkipPostDeleteVerificationFailed, err
+ }
+ item.Action = ActionDeletedVerified
+ item.SkipReason = ""
+ item.RetirementState = RetirementStateDeletedVerified
+ item.RetirementOutcome = ActionDeletedVerified
+ item.RetirementVerifiedAt = &verifiedAt
+ return "", nil
+}
+
+func (p *Planner) revalidateCandidate(ctx context.Context, req PlanRequest, candidate Candidate, verifier blockPayloadVerifier) (string, string, error) {
+ if _, reason, err := p.revalidateMetadataAndCSCB(ctx, req, candidate, true); err != nil {
+ return "", reason, err
+ }
+ topology, err := p.store.ListObjectVersions(ctx, candidate.Bucket, candidate.Key)
+ if err != nil {
+ return "", SkipObjectInspectionFailed, err
+ }
+ if !topologyMatchesCandidate(topology, candidate) {
+ return "", SkipUnsafeSingleBlockVersionTopology, xerrors.Errorf("single-block object version topology changed before retirement: block_metadata_id=%d", candidate.BlockMetadataID)
+ }
+ digest, err := verifier.Verify(ctx, candidate)
+ if err != nil {
+ return "", SkipSingleBlockPayloadMismatch, err
+ }
+ return digest, "", nil
+}
+
+func (p *Planner) revalidateConsolidatedCandidate(ctx context.Context, req PlanRequest, candidate Candidate, verifier blockPayloadVerifier) (string, string, error) {
+ if _, reason, err := p.revalidateMetadataAndCSCB(ctx, req, candidate, false); err != nil {
+ return "", reason, err
+ }
+ digest, err := verifier.VerifyConsolidated(ctx, candidate)
+ if err != nil {
+ return "", SkipSingleBlockPayloadMismatch, err
+ }
+ return digest, "", nil
+}
+
+func (p *Planner) revalidateMetadataAndCSCB(ctx context.Context, req PlanRequest, candidate Candidate, requireCanonical bool) (MetadataRow, string, error) {
+ row, err := p.repo.GetMetadataRow(ctx, candidate.BlockMetadataID)
+ if err != nil {
+ return MetadataRow{}, SkipMetadataChanged, err
+ }
+ if !candidateMatchesRow(req, candidate, row, requireCanonical) {
+ return MetadataRow{}, SkipMetadataChanged, xerrors.Errorf("canonical, shadow, or retirement metadata changed before retirement: block_metadata_id=%d", candidate.BlockMetadataID)
+ }
+ current, err := p.store.HeadObject(ctx, candidate.Bucket, candidate.ConsolidatedKey)
+ if err != nil {
+ return MetadataRow{}, SkipObjectInspectionFailed, err
+ }
+ pinned, err := p.store.HeadObjectVersion(ctx, candidate.Bucket, candidate.ConsolidatedKey, candidate.CSCBVersionID)
+ if err != nil {
+ return MetadataRow{}, SkipObjectInspectionFailed, err
+ }
+ if !current.Exists || current.DeleteMark || !isImmutableVersionID(current.VersionID) || current.VersionID != candidate.CSCBVersionID || current.ETag != candidate.CSCBETag ||
+ !pinned.Exists || pinned.DeleteMark || pinned.VersionID != candidate.CSCBVersionID || pinned.ETag != candidate.CSCBETag {
+ return MetadataRow{}, SkipCSCBObjectChanged, xerrors.Errorf("pinned CSCB object changed before retirement: block_metadata_id=%d", candidate.BlockMetadataID)
+ }
+ if current.Expiration != "" || pinned.Expiration != "" {
+ return MetadataRow{}, SkipCSCBLifecycleExpirationActive, xerrors.Errorf("pinned CSCB object has active lifecycle expiration: block_metadata_id=%d", candidate.BlockMetadataID)
+ }
+ logicalBytes, ok := cscbLogicalPayloadBytes(pinned)
+ if pinned.Bytes == 0 || !ok || candidate.ByteOffset > logicalBytes || candidate.ByteLength > logicalBytes-candidate.ByteOffset {
+ return MetadataRow{}, SkipCSCBObjectChanged, xerrors.Errorf("pinned CSCB metadata is invalid before retirement: block_metadata_id=%d", candidate.BlockMetadataID)
+ }
+ return row, "", nil
+}
+
+func (p *Planner) verifySingleBlockDeleted(ctx context.Context, bucket string, key string) error {
+ topology, err := p.store.ListObjectVersions(ctx, bucket, key)
+ if err != nil {
+ return err
+ }
+ if len(topology.Versions) != 0 || len(topology.DeleteMarkers) != 0 {
+ return xerrors.Errorf("single-block key still has versions after delete: versions=%d markers=%d", len(topology.Versions), len(topology.DeleteMarkers))
+ }
+ head, err := p.store.HeadObject(ctx, bucket, key)
+ if err != nil {
+ return err
+ }
+ if head.Exists {
+ return xerrors.New("single-block key still resolves after deleting its pinned version")
+ }
+ return nil
+}
+
+func candidateMatchesRow(req PlanRequest, candidate Candidate, row MetadataRow, requireCanonical bool) bool {
+ if candidate.RetirementState == RetirementStateDeletedPendingVerification {
+ return pendingVerificationCandidateMatchesRow(req, candidate, row, requireCanonical)
+ }
+ if row.Shadow == nil || row.Shadow.ValidatedAt == nil || row.Shadow.SingleBlockRetentionStartedAt == nil ||
+ row.Shadow.SingleBlockDeleteAfter == nil || row.Shadow.SingleBlockObjectDeletedAt != nil ||
+ req.EligibilityCutoff.Before(*row.Shadow.SingleBlockDeleteAfter) {
+ return false
+ }
+ retirementMatches := row.Retirement == nil || pendingRetirementMatchesCandidate(row, candidate)
+ return (!requireCanonical || row.Canonical) &&
+ !row.Skipped &&
+ approvalMatches(req) &&
+ req.FallbackErrorCount == 0 &&
+ req.DirectStorageClientsGuarded &&
+ req.SingleBlockWritersGuarded &&
+ requestAllowsCandidate(req, candidate) &&
+ row.BlockMetadataID == candidate.BlockMetadataID &&
+ row.Tag == candidate.Tag &&
+ row.Height == candidate.Height &&
+ row.Hash == candidate.Hash &&
+ row.SingleBlockObjectKey == candidate.Key &&
+ row.PrimaryObjectKey == candidate.ConsolidatedKey &&
+ row.PrimaryObjectFormat == api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH &&
+ row.PrimaryByteOffset == candidate.ByteOffset &&
+ row.PrimaryByteLength == candidate.ByteLength &&
+ row.PrimaryUncompressedLength == candidate.UncompressedLength &&
+ retirementMatches &&
+ validShadowReference(row, row.Shadow)
+}
+
+func pendingVerificationCandidateMatchesRow(req PlanRequest, candidate Candidate, row MetadataRow, requireCanonical bool) bool {
+ manifest := row.Retirement
+ if manifest == nil || manifest.State != RetirementStateDeletedPendingVerification ||
+ row.Shadow == nil || row.RetirementFencedAt == nil ||
+ row.SingleBlockObjectKey != "" || row.Shadow.SingleBlockObjectKey != "" ||
+ row.Shadow.ValidatedAt == nil || row.Shadow.SingleBlockRetentionStartedAt == nil ||
+ row.Shadow.SingleBlockDeleteAfter == nil || row.Shadow.SingleBlockObjectDeletedAt == nil ||
+ manifest.SingleBlockObjectKey != "" || manifest.SingleBlockObjectETag != "" ||
+ manifest.DeletedAt == nil || manifest.VerifiedAt != nil ||
+ !row.Shadow.SingleBlockObjectDeletedAt.Equal(*manifest.DeletedAt) {
+ return false
+ }
+ return (!requireCanonical || row.Canonical) &&
+ !row.Skipped &&
+ approvalMatches(req) &&
+ req.FallbackErrorCount == 0 &&
+ req.DirectStorageClientsGuarded &&
+ req.SingleBlockWritersGuarded &&
+ requestAllowsCandidate(req, candidate) &&
+ row.BlockMetadataID == candidate.BlockMetadataID &&
+ row.Tag == candidate.Tag &&
+ row.Height == candidate.Height &&
+ row.Hash == candidate.Hash &&
+ row.PrimaryObjectKey == candidate.ConsolidatedKey &&
+ row.PrimaryObjectFormat == api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH &&
+ row.PrimaryByteOffset == candidate.ByteOffset &&
+ row.PrimaryByteLength == candidate.ByteLength &&
+ row.PrimaryUncompressedLength == candidate.UncompressedLength &&
+ pendingRetirementMatchesCandidate(row, candidate) &&
+ validShadowReference(row, row.Shadow)
+}
+
+func pendingRetirementMatchesCandidate(row MetadataRow, candidate Candidate) bool {
+ manifest := row.Retirement
+ if manifest == nil || row.RetirementFencedAt == nil ||
+ manifest.State != RetirementStateEligible && manifest.State != RetirementStateDeleting && manifest.State != RetirementStateDeletedPendingVerification {
+ return false
+ }
+ return manifest.BlockMetadataID == candidate.BlockMetadataID &&
+ manifest.Tag == candidate.Tag &&
+ manifest.Height == candidate.Height &&
+ manifest.Hash == candidate.Hash &&
+ manifest.Bucket == candidate.Bucket &&
+ manifest.SingleBlockObjectKey == candidate.Key &&
+ manifest.SingleBlockObjectKeySHA256 == candidate.SingleBlockKeySHA256 &&
+ sameStrings(manifest.SingleBlockObjectVersionIDs, candidate.VersionIDs) &&
+ sameStrings(manifest.SingleBlockDeleteMarkerVersionIDs, candidate.DeleteMarkerVersionIDs) &&
+ firstString(manifest.SingleBlockObjectVersionIDs) == candidate.VersionID &&
+ manifest.SingleBlockObjectETag == candidate.SingleBlockETag &&
+ manifest.SingleBlockObjectBytes == candidate.SingleBlockBytes &&
+ manifest.ConsolidatedObjectKey == candidate.ConsolidatedKey &&
+ manifest.ConsolidatedObjectVersionID == candidate.CSCBVersionID &&
+ manifest.ConsolidatedObjectETag == candidate.CSCBETag &&
+ manifest.ConsolidatedByteOffset == candidate.ByteOffset &&
+ manifest.ConsolidatedByteLength == candidate.ByteLength &&
+ manifest.ConsolidatedUncompressedLength == candidate.UncompressedLength &&
+ manifest.PayloadSHA256 == candidate.PayloadSHA256
+}
+
+func pendingVerificationRetirementMatchesRow(row MetadataRow) bool {
+ manifest := row.Retirement
+ return manifest != nil &&
+ manifest.State == RetirementStateDeletedPendingVerification &&
+ manifest.SingleBlockObjectKey == "" &&
+ manifest.SingleBlockObjectETag == "" &&
+ manifest.DeletedAt != nil &&
+ manifest.VerifiedAt == nil &&
+ manifest.ClaimToken != "" &&
+ manifest.ClaimExpiresAt != nil &&
+ row.Shadow != nil &&
+ row.RetirementFencedAt != nil &&
+ row.SingleBlockObjectKey == "" &&
+ row.Shadow.SingleBlockObjectKey == "" &&
+ row.Shadow.SingleBlockObjectDeletedAt != nil &&
+ row.Shadow.SingleBlockObjectDeletedAt.Equal(*manifest.DeletedAt) &&
+ manifest.BlockMetadataID == row.BlockMetadataID &&
+ manifest.Tag == row.Tag &&
+ manifest.Height == row.Height &&
+ manifest.Hash == row.Hash &&
+ manifest.ConsolidatedObjectKey == row.PrimaryObjectKey &&
+ manifest.ConsolidatedByteOffset == row.PrimaryByteOffset &&
+ manifest.ConsolidatedByteLength == row.PrimaryByteLength &&
+ manifest.ConsolidatedUncompressedLength == row.PrimaryUncompressedLength &&
+ validShadowReference(row, row.Shadow)
+}
+
+func finalizedRetirementMatchesRow(row MetadataRow) bool {
+ manifest := row.Retirement
+ if manifest == nil || row.Shadow == nil || row.RetirementFencedAt == nil ||
+ manifest.State != RetirementStateDeletedVerified ||
+ manifest.SingleBlockObjectKey != "" || manifest.SingleBlockObjectETag != "" ||
+ manifest.ClaimToken != "" || manifest.ClaimExpiresAt != nil ||
+ manifest.DeletedAt == nil || manifest.VerifiedAt == nil || manifest.Outcome == "" ||
+ row.SingleBlockObjectKey != "" || row.Shadow.SingleBlockObjectKey != "" ||
+ row.Shadow.SingleBlockObjectDeletedAt == nil ||
+ !row.Shadow.SingleBlockObjectDeletedAt.Equal(*manifest.DeletedAt) {
+ return false
+ }
+ return manifest.BlockMetadataID == row.BlockMetadataID &&
+ manifest.Tag == row.Tag &&
+ manifest.Height == row.Height &&
+ manifest.Hash == row.Hash &&
+ manifest.ConsolidatedObjectKey == row.PrimaryObjectKey &&
+ manifest.ConsolidatedByteOffset == row.PrimaryByteOffset &&
+ manifest.ConsolidatedByteLength == row.PrimaryByteLength &&
+ manifest.ConsolidatedUncompressedLength == row.PrimaryUncompressedLength &&
+ validShadowReference(row, row.Shadow)
+}
+
+func requestAllowsCandidate(req PlanRequest, candidate Candidate) bool {
+ return candidate.Bucket == req.Bucket &&
+ candidate.Tag == req.Tag &&
+ candidate.Height >= req.StartHeight &&
+ candidate.Height < req.EndHeight
+}
+
+func validateApplyReport(req PlanRequest, report *Report) error {
+ if report.DryRun ||
+ report.Environment != req.Environment ||
+ report.Blockchain != req.Blockchain ||
+ report.Network != req.Network ||
+ report.Sidechain != req.Sidechain ||
+ report.Bucket != req.Bucket ||
+ report.Tag != req.Tag ||
+ report.StartHeight != req.StartHeight ||
+ report.EndHeight != req.EndHeight ||
+ report.Approval != req.Approval ||
+ report.SafetyGates.DirectStorageClientsGuarded != req.DirectStorageClientsGuarded ||
+ report.SafetyGates.SingleBlockWritersGuarded != req.SingleBlockWritersGuarded ||
+ report.SafetyGates.FallbackReadErrors != req.FallbackErrorCount ||
+ report.SafetyGates.ProductionDeleteEnabled != req.ProductionDeleteEnabled ||
+ report.SafetyGates.VersionedDeleteMode != versionedDeleteMode ||
+ report.SafetyGates.CSCBWriteOncePolicyMode != cscbWriteOncePolicyMode ||
+ !report.SafetyGates.CSCBWriteOncePolicyVerified {
+ return xerrors.New("retirement report does not match the execution request")
+ }
+ if !approvalMatches(req) || req.FallbackErrorCount != 0 || !req.DirectStorageClientsGuarded || !req.SingleBlockWritersGuarded {
+ return xerrors.New("retirement execution safety gates are not satisfied")
+ }
+ for _, item := range report.Items {
+ if item.Action != ActionDeleteObjectVersion {
+ continue
+ }
+ if !requestAllowsCandidate(req, item) || item.BlockMetadataID <= 0 || item.Key == "" ||
+ item.SingleBlockKeySHA256 != keySHA256(item.Key) ||
+ !validPinnedVersionIDs(item.VersionID, item.VersionIDs, item.DeleteMarkerVersionIDs) || item.SingleBlockETag == "" ||
+ item.SingleBlockBytes == 0 || item.SingleBlockVersions != len(item.VersionIDs) || item.DeleteMarkers != len(item.DeleteMarkerVersionIDs) ||
+ item.ConsolidatedKey == "" || !isImmutableVersionID(item.CSCBVersionID) || item.CSCBETag == "" ||
+ !item.CSCBWriteOncePolicy ||
+ item.ByteLength == 0 || item.UncompressedLength == 0 || !isSHA256Hex(item.PayloadSHA256) {
+ return xerrors.Errorf("retirement report contains an invalid delete candidate: block_metadata_id=%d", item.BlockMetadataID)
+ }
+ }
+ return nil
+}
+
+func topologyMatchesCandidate(topology ObjectVersionTopology, candidate Candidate) bool {
+ version, versionIDs, markerIDs, reason := pinSingleBlockTopology(topology)
+ return reason == "" &&
+ version.VersionID == candidate.VersionID &&
+ version.ETag == candidate.SingleBlockETag &&
+ version.Bytes == candidate.SingleBlockBytes &&
+ sameStrings(versionIDs, candidate.VersionIDs) &&
+ sameStrings(markerIDs, candidate.DeleteMarkerVersionIDs)
+}
+
+func topologyMatchesManifest(topology ObjectVersionTopology, manifest RetirementManifest) bool {
+ return topologyMatchesCandidate(topology, candidateFromManifest(manifest))
+}
+
+func topologyIsPinnedSubset(topology ObjectVersionTopology, manifest RetirementManifest) bool {
+ return topologyIsPinnedCandidateSubset(topology, candidateFromManifest(manifest))
+}
+
+func topologyIsPinnedCandidateSubset(topology ObjectVersionTopology, candidate Candidate) bool {
+ if !validPinnedVersionIDs(candidate.VersionID, candidate.VersionIDs, candidate.DeleteMarkerVersionIDs) {
+ return false
+ }
+ versionIDs := make(map[string]struct{}, len(candidate.VersionIDs))
+ for _, versionID := range candidate.VersionIDs {
+ versionIDs[versionID] = struct{}{}
+ }
+ markerIDs := make(map[string]struct{}, len(candidate.DeleteMarkerVersionIDs))
+ for _, versionID := range candidate.DeleteMarkerVersionIDs {
+ markerIDs[versionID] = struct{}{}
+ }
+ seen := make(map[string]struct{}, len(topology.Versions)+len(topology.DeleteMarkers))
+ for _, version := range topology.Versions {
+ if !isImmutableVersionID(version.VersionID) {
+ return false
+ }
+ if _, ok := versionIDs[version.VersionID]; !ok {
+ return false
+ }
+ if _, duplicate := seen[version.VersionID]; duplicate {
+ return false
+ }
+ seen[version.VersionID] = struct{}{}
+ if version.VersionID == candidate.VersionID &&
+ (version.ETag != candidate.SingleBlockETag || version.Bytes != candidate.SingleBlockBytes) {
+ return false
+ }
+ }
+ for _, marker := range topology.DeleteMarkers {
+ if !isImmutableVersionID(marker.VersionID) {
+ return false
+ }
+ if _, ok := markerIDs[marker.VersionID]; !ok {
+ return false
+ }
+ if _, duplicate := seen[marker.VersionID]; duplicate {
+ return false
+ }
+ seen[marker.VersionID] = struct{}{}
+ }
+ return true
+}
+
+func pinSingleBlockTopology(topology ObjectVersionTopology) (ObjectVersion, []string, []string, string) {
+ if len(topology.Versions) == 0 {
+ return ObjectVersion{}, nil, nil, SkipUnsafeSingleBlockVersionTopology
+ }
+ seen := make(map[string]struct{}, len(topology.Versions)+len(topology.DeleteMarkers))
+ latestVersion := -1
+ latestEntries := 0
+ for i, version := range topology.Versions {
+ if !isImmutableVersionID(version.VersionID) {
+ return ObjectVersion{}, nil, nil, SkipSingleBlockVersionIDUnavailable
+ }
+ if _, duplicate := seen[version.VersionID]; duplicate {
+ return ObjectVersion{}, nil, nil, SkipUnsafeSingleBlockVersionTopology
+ }
+ seen[version.VersionID] = struct{}{}
+ if version.IsLatest {
+ latestEntries++
+ latestVersion = i
+ }
+ }
+ for _, marker := range topology.DeleteMarkers {
+ if !isImmutableVersionID(marker.VersionID) {
+ return ObjectVersion{}, nil, nil, SkipSingleBlockVersionIDUnavailable
+ }
+ if _, duplicate := seen[marker.VersionID]; duplicate {
+ return ObjectVersion{}, nil, nil, SkipUnsafeSingleBlockVersionTopology
+ }
+ seen[marker.VersionID] = struct{}{}
+ if marker.IsLatest {
+ latestEntries++
+ }
+ }
+ if latestEntries != 1 {
+ return ObjectVersion{}, nil, nil, SkipUnsafeSingleBlockVersionTopology
+ }
+ if latestVersion < 0 {
+ // ListObjectVersions returns data versions newest first. When a delete
+ // marker is current, the first data version is the last readable payload.
+ latestVersion = 0
+ }
+ selected := topology.Versions[latestVersion]
+ if selected.ETag == "" || selected.Bytes == 0 {
+ return ObjectVersion{}, nil, nil, SkipSingleBlockVersionIDUnavailable
+ }
+
+ versionIDs := make([]string, 0, len(topology.Versions))
+ versionIDs = append(versionIDs, selected.VersionID)
+ remainder := make([]string, 0, len(topology.Versions)-1)
+ for i, version := range topology.Versions {
+ if i != latestVersion {
+ remainder = append(remainder, version.VersionID)
+ }
+ }
+ sort.Strings(remainder)
+ versionIDs = append(versionIDs, remainder...)
+ markerIDs := make([]string, 0, len(topology.DeleteMarkers))
+ for _, marker := range topology.DeleteMarkers {
+ markerIDs = append(markerIDs, marker.VersionID)
+ }
+ sort.Strings(markerIDs)
+ return selected, versionIDs, markerIDs, ""
+}
+
+func validPinnedVersionIDs(validationVersionID string, versionIDs []string, markerIDs []string) bool {
+ if len(versionIDs) == 0 || versionIDs[0] != validationVersionID || !isImmutableVersionID(validationVersionID) ||
+ !sort.StringsAreSorted(versionIDs[1:]) || !sort.StringsAreSorted(markerIDs) {
+ return false
+ }
+ seen := make(map[string]struct{}, len(versionIDs)+len(markerIDs))
+ for _, versionID := range versionIDs {
+ if !isImmutableVersionID(versionID) {
+ return false
+ }
+ if _, duplicate := seen[versionID]; duplicate {
+ return false
+ }
+ seen[versionID] = struct{}{}
+ }
+ for _, markerID := range markerIDs {
+ if !isImmutableVersionID(markerID) {
+ return false
+ }
+ if _, duplicate := seen[markerID]; duplicate {
+ return false
+ }
+ seen[markerID] = struct{}{}
+ }
+ return true
+}
+
+func isImmutableVersionID(versionID string) bool {
+ return versionID != "" && versionID != s3MutableNullVersionID
+}
+
+func manifestFromCandidate(candidate Candidate, preparedAt time.Time) RetirementManifest {
+ return RetirementManifest{
+ BlockMetadataID: candidate.BlockMetadataID,
+ Tag: candidate.Tag,
+ Height: candidate.Height,
+ Hash: candidate.Hash,
+ State: RetirementStateEligible,
+ Bucket: candidate.Bucket,
+ SingleBlockObjectKey: candidate.Key,
+ SingleBlockObjectKeySHA256: keySHA256(candidate.Key),
+ SingleBlockObjectVersionIDs: append([]string(nil), candidate.VersionIDs...),
+ SingleBlockDeleteMarkerVersionIDs: append([]string(nil), candidate.DeleteMarkerVersionIDs...),
+ SingleBlockObjectETag: candidate.SingleBlockETag,
+ SingleBlockObjectBytes: candidate.SingleBlockBytes,
+ ConsolidatedObjectKey: candidate.ConsolidatedKey,
+ ConsolidatedObjectVersionID: candidate.CSCBVersionID,
+ ConsolidatedObjectETag: candidate.CSCBETag,
+ ConsolidatedByteOffset: candidate.ByteOffset,
+ ConsolidatedByteLength: candidate.ByteLength,
+ ConsolidatedUncompressedLength: candidate.UncompressedLength,
+ PayloadSHA256: candidate.PayloadSHA256,
+ PreparedAt: preparedAt,
+ }
+}
+
+func candidateFromManifest(manifest RetirementManifest) Candidate {
+ candidate := Candidate{
+ Bucket: manifest.Bucket,
+ Key: manifest.SingleBlockObjectKey,
+ VersionID: firstString(manifest.SingleBlockObjectVersionIDs),
+ VersionIDs: append([]string(nil), manifest.SingleBlockObjectVersionIDs...),
+ DeleteMarkerVersionIDs: append([]string(nil), manifest.SingleBlockDeleteMarkerVersionIDs...),
+ BlockMetadataID: manifest.BlockMetadataID,
+ Tag: manifest.Tag,
+ Height: manifest.Height,
+ Hash: manifest.Hash,
+ SingleBlockBytes: manifest.SingleBlockObjectBytes,
+ SingleBlockETag: manifest.SingleBlockObjectETag,
+ SingleBlockKeySHA256: manifest.SingleBlockObjectKeySHA256,
+ SingleBlockVersions: len(manifest.SingleBlockObjectVersionIDs),
+ DeleteMarkers: len(manifest.SingleBlockDeleteMarkerVersionIDs),
+ ConsolidatedKey: manifest.ConsolidatedObjectKey,
+ CSCBVersionID: manifest.ConsolidatedObjectVersionID,
+ CSCBETag: manifest.ConsolidatedObjectETag,
+ ByteOffset: manifest.ConsolidatedByteOffset,
+ ByteLength: manifest.ConsolidatedByteLength,
+ UncompressedLength: manifest.ConsolidatedUncompressedLength,
+ PayloadSHA256: manifest.PayloadSHA256,
+ RetirementState: manifest.State,
+ RetirementAttempts: manifest.AttemptCount,
+ RetirementOutcome: manifest.Outcome,
+ SingleBlockDeletedAt: manifest.DeletedAt,
+ RetirementVerifiedAt: manifest.VerifiedAt,
+ Action: ActionDeleteObjectVersion,
+ }
+ if manifest.State == RetirementStateDeletedPendingVerification {
+ candidate.Action = ActionDeletedObjectVersion
+ }
+ return candidate
+}
+
+func markCandidateBlocked(candidate *Candidate, reason string) {
+ if candidate.RetirementState == RetirementStateDeletedPendingVerification {
+ candidate.Action = ActionDeletedObjectVersion
+ } else {
+ candidate.Action = ActionSkip
+ }
+ candidate.SkipReason = reason
+}
+
+func newRetirementClaimToken() (string, error) {
+ value := make([]byte, retirementClaimTokenBytes)
+ if _, err := cryptorand.Read(value); err != nil {
+ return "", xerrors.Errorf("failed to generate retirement claim token: %w", err)
+ }
+ return hex.EncodeToString(value), nil
+}
+
+func keySHA256(key string) string {
+ digest := sha256.Sum256([]byte(key))
+ return hex.EncodeToString(digest[:])
+}
+
+func isSHA256Hex(value string) bool {
+ if len(value) != sha256HexLength {
+ return false
+ }
+ _, err := hex.DecodeString(value)
+ return err == nil
+}
+
+func firstString(values []string) string {
+ if len(values) == 0 {
+ return ""
+ }
+ return values[0]
+}
+
+func newReport(req PlanRequest) *Report {
+ return &Report{
+ GeneratedAt: req.Now,
+ DryRun: !req.Execute,
+ Environment: req.Environment,
+ Blockchain: req.Blockchain,
+ Network: req.Network,
+ Sidechain: req.Sidechain,
+ Bucket: req.Bucket,
+ Tag: req.Tag,
+ StartHeight: req.StartHeight,
+ EndHeight: req.EndHeight,
+ Approval: req.Approval,
+ SafetyGates: SafetyGates{
+ DirectStorageClientsGuarded: req.DirectStorageClientsGuarded,
+ SingleBlockWritersGuarded: req.SingleBlockWritersGuarded,
+ FallbackReadErrors: req.FallbackErrorCount,
+ VersionedDeleteMode: versionedDeleteMode,
+ CSCBWriteOncePolicyMode: cscbWriteOncePolicyMode,
+ ProductionDeleteEnabled: req.ProductionDeleteEnabled,
+ },
+ Items: make([]Candidate, 0),
+ }
+}
+
+func writeOncePolicyGateVerified(items []Candidate) bool {
+ eligible := false
+ for _, item := range items {
+ if item.Action != ActionReportOnly && item.Action != ActionDeleteObjectVersion &&
+ item.Action != ActionDeletedObjectVersion && item.Action != ActionDeletedVerified {
+ continue
+ }
+ eligible = true
+ if !item.CSCBWriteOncePolicy {
+ return false
+ }
+ }
+ return eligible
+}
+
+func normalizeRequest(req *PlanRequest) error {
+ if req == nil {
+ return xerrors.New("request is required")
+ }
+ if req.EndHeight <= req.StartHeight {
+ return xerrors.Errorf("end height must be greater than start height: start=%d end=%d", req.StartHeight, req.EndHeight)
+ }
+ if req.Bucket == "" {
+ return xerrors.New("bucket is required")
+ }
+ if req.Now.IsZero() {
+ req.Now = time.Now().UTC()
+ }
+ if req.EligibilityCutoff.IsZero() {
+ req.EligibilityCutoff = req.Now
+ }
+ return nil
+}
+
+func validShadowReference(row MetadataRow, shadow *ConsolidationShadow) bool {
+ if shadow == nil {
+ return false
+ }
+ if shadow.Tag != row.Tag || shadow.Height != row.Height || shadow.Hash != row.Hash {
+ return false
+ }
+ if shadow.SingleBlockObjectKey != row.SingleBlockObjectKey {
+ return false
+ }
+ if !isPrimaryConsolidated(row) {
+ return false
+ }
+ if row.PrimaryObjectKey != shadow.ConsolidatedObjectKey ||
+ row.PrimaryObjectFormat != shadow.ObjectFormat ||
+ row.PrimaryByteOffset != shadow.ByteOffset ||
+ row.PrimaryByteLength != shadow.ByteLength ||
+ row.PrimaryUncompressedLength != shadow.UncompressedLength {
+ return false
+ }
+ if shadow.ConsolidatedObjectKey == "" {
+ return false
+ }
+ if shadow.ObjectFormat != api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH {
+ return false
+ }
+ return shadow.ByteLength != 0
+}
+
+func cscbLogicalPayloadBytes(head ObjectHead) (uint64, bool) {
+ format, ok := objectMetadataValue(head.Metadata, cscbFormatMetadataKey)
+ if !ok || !strings.EqualFold(strings.TrimSpace(format), cscbFormatMetadataValue) {
+ return 0, false
+ }
+ scope, ok := objectMetadataValue(head.Metadata, cscbCompressionScopeMetadataKey)
+ if !ok || !strings.EqualFold(strings.TrimSpace(scope), cscbCompressionScopeMetadataValue) {
+ return 0, false
+ }
+ value, ok := objectMetadataValue(head.Metadata, cscbUncompressedLengthMetadataKey)
+ if !ok {
+ return 0, false
+ }
+ logicalBytes, err := strconv.ParseUint(strings.TrimSpace(value), 10, 64)
+ if err != nil || logicalBytes == 0 {
+ return 0, false
+ }
+ return logicalBytes, true
+}
+
+func objectMetadataValue(metadata map[string]string, name string) (string, bool) {
+ value, ok := metadata[name]
+ if ok {
+ return value, true
+ }
+ for key, candidate := range metadata {
+ if strings.EqualFold(key, name) {
+ return candidate, true
+ }
+ }
+ return "", false
+}
+
+func isPrimaryConsolidated(row MetadataRow) bool {
+ return row.PrimaryObjectFormat == api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH &&
+ row.PrimaryObjectKey != "" &&
+ row.PrimaryByteLength > 0
+}
+
+func approvalMatches(req PlanRequest) bool {
+ if normalizeApprovalChain(req.Approval.Chain) != normalizeApprovalChain(actualChain(req)) {
+ return false
+ }
+ if req.Approval.AllowContainingRange {
+ return req.Approval.StartHeight <= req.StartHeight &&
+ req.Approval.EndHeight >= req.EndHeight
+ }
+ return req.Approval.StartHeight == req.StartHeight &&
+ req.Approval.EndHeight == req.EndHeight
+}
+
+// approvalGateBlocked reports whether the operator approval blocks this
+// request. Direct planner callers require an exact chain/range approval. The
+// retention workflow may explicitly opt into a containing envelope after it
+// independently validates the selected cohort against that operator input. A
+// read-only run may omit the approval entirely so it can still reach the S3
+// safety inspection, but a supplied approval is validated even on dry runs so
+// the report predicts the execute-time gate.
+func approvalGateBlocked(req PlanRequest) bool {
+ if !req.Execute && req.Approval == (Approval{}) {
+ return false
+ }
+ return !approvalMatches(req)
+}
+
+func actualChain(req PlanRequest) string {
+ parts := []string{req.Blockchain, req.Network}
+ if req.Sidechain != "" {
+ parts = append(parts, req.Sidechain)
+ }
+ return strings.Join(parts, "-")
+}
+
+func normalizeApprovalChain(value string) string {
+ return strings.ToLower(strings.ReplaceAll(value, "_", "-"))
+}
+
+// NormalizeApprovalChain exposes the approval chain normalization so callers
+// can fail fast on an operator approval that names the wrong chain before a
+// plan request is even built.
+func NormalizeApprovalChain(value string) string {
+ return normalizeApprovalChain(value)
+}
+
+func isProduction(env string) bool {
+ env = strings.ToLower(env)
+ return env == "production" || env == "prod"
+}
+
+func summarize(items []Candidate) Summary {
+ var summary Summary
+ summary.TotalRows = len(items)
+ for _, item := range items {
+ summary.SingleBlockBytes += item.SingleBlockBytes
+ if item.DeleteMarkers > 0 || item.SkipReason == SkipSingleBlockCurrentDeleteMarker {
+ summary.DeleteMarkerRows++
+ }
+ switch item.Action {
+ case ActionReportOnly, ActionDeleteObjectVersion:
+ summary.EligibleRows++
+ summary.EligibleBytes += item.SingleBlockBytes
+ case ActionDeletedObjectVersion, ActionDeletedVerified, ActionAlreadyDeleted:
+ summary.DeletedRows++
+ default:
+ summary.SkippedRows++
+ }
+ }
+ return summary
+}
diff --git a/internal/storage/retirement/planner_test.go b/internal/storage/retirement/planner_test.go
new file mode 100644
index 00000000..ee5912ae
--- /dev/null
+++ b/internal/storage/retirement/planner_test.go
@@ -0,0 +1,1973 @@
+package retirement
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "sort"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type fakeRepo struct {
+ rows []MetadataRow
+ manifests map[int64]RetirementManifest
+ safetyObservations map[string]fakeSafetyObservation
+ safetyNow time.Time
+ recordDeletedErr error
+ recordDeletedCalls int
+ finalizeErr error
+ finalizeCalls int
+}
+
+type fakeSafetyObservation struct {
+ configurationSHA256 string
+ firstObservedAt time.Time
+}
+
+func (r *fakeRepo) ListMetadataRows(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, limit uint64) ([]MetadataRow, error) {
+ if limit > 0 && uint64(len(r.rows)) > limit {
+ return r.rows[:limit], nil
+ }
+ return r.rows, nil
+}
+
+func (r *fakeRepo) GetMetadataRow(ctx context.Context, blockMetadataID int64) (MetadataRow, error) {
+ for _, row := range r.rows {
+ if row.BlockMetadataID == blockMetadataID {
+ return row, nil
+ }
+ }
+ return MetadataRow{}, fmt.Errorf("metadata row not found: %d", blockMetadataID)
+}
+
+func (r *fakeRepo) PrepareRetirement(ctx context.Context, manifest RetirementManifest) error {
+ if r.manifests == nil {
+ r.manifests = make(map[int64]RetirementManifest)
+ }
+ if existing, ok := r.manifests[manifest.BlockMetadataID]; ok && !samePreparedManifest(existing, manifest) {
+ return errors.New("manifest conflict")
+ }
+ r.manifests[manifest.BlockMetadataID] = manifest
+ for i := range r.rows {
+ if r.rows[i].BlockMetadataID == manifest.BlockMetadataID {
+ preparedAt := manifest.PreparedAt
+ r.rows[i].RetirementFencedAt = &preparedAt
+ r.rows[i].Retirement = &manifest
+ }
+ }
+ return nil
+}
+
+func (r *fakeRepo) ObserveRetentionSafety(
+ ctx context.Context,
+ bucket string,
+ consolidatedObjectKey string,
+ configurationSHA256 string,
+) (time.Time, time.Time, error) {
+ if r.safetyObservations == nil {
+ r.safetyObservations = make(map[string]fakeSafetyObservation)
+ }
+ observedAt := r.safetyNow
+ if observedAt.IsZero() {
+ observedAt = time.Now().UTC()
+ }
+ key := bucket + "\x00" + consolidatedObjectKey
+ observation, ok := r.safetyObservations[key]
+ if !ok {
+ observation = fakeSafetyObservation{
+ configurationSHA256: configurationSHA256,
+ firstObservedAt: observedAt.Add(-RetentionSafetyQuiescencePeriod),
+ }
+ } else if observation.configurationSHA256 != configurationSHA256 {
+ observation.configurationSHA256 = configurationSHA256
+ observation.firstObservedAt = observedAt
+ }
+ r.safetyObservations[key] = observation
+ return observation.firstObservedAt, observedAt, nil
+}
+
+func (r *fakeRepo) ClaimRetirement(
+ ctx context.Context,
+ blockMetadataID int64,
+ claimToken string,
+ claimedAt time.Time,
+ claimExpiresAt time.Time,
+) error {
+ manifest, ok := r.manifests[blockMetadataID]
+ if !ok {
+ return errors.New("manifest not found")
+ }
+ if (manifest.State == RetirementStateDeleting || manifest.State == RetirementStateDeletedPendingVerification) &&
+ manifest.ClaimExpiresAt != nil && manifest.ClaimExpiresAt.After(claimedAt) {
+ return ErrRetirementClaimUnavailable
+ }
+ if manifest.State != RetirementStateEligible &&
+ manifest.State != RetirementStateDeleting &&
+ manifest.State != RetirementStateDeletedPendingVerification {
+ return ErrRetirementClaimUnavailable
+ }
+ if manifest.State == RetirementStateEligible {
+ manifest.State = RetirementStateDeleting
+ }
+ manifest.ClaimToken = claimToken
+ manifest.ClaimExpiresAt = &claimExpiresAt
+ if manifest.DeleteStartedAt == nil {
+ manifest.DeleteStartedAt = &claimedAt
+ }
+ manifest.LastAttemptAt = &claimedAt
+ manifest.AttemptCount++
+ if manifest.State == RetirementStateDeletedPendingVerification {
+ manifest.Outcome = "verification_started"
+ } else {
+ manifest.Outcome = "delete_started"
+ }
+ r.manifests[blockMetadataID] = manifest
+ r.updateManifestRow(manifest)
+ return nil
+}
+
+func (r *fakeRepo) RenewRetirementClaim(
+ ctx context.Context,
+ blockMetadataID int64,
+ claimToken string,
+ renewedAt time.Time,
+ claimExpiresAt time.Time,
+) error {
+ manifest, ok := r.manifests[blockMetadataID]
+ if !ok || manifest.State != RetirementStateDeleting && manifest.State != RetirementStateDeletedPendingVerification ||
+ manifest.ClaimToken != claimToken ||
+ manifest.ClaimExpiresAt == nil || !manifest.ClaimExpiresAt.After(renewedAt) {
+ return ErrRetirementClaimUnavailable
+ }
+ manifest.ClaimExpiresAt = &claimExpiresAt
+ manifest.LastAttemptAt = &renewedAt
+ r.manifests[blockMetadataID] = manifest
+ r.updateManifestRow(manifest)
+ return nil
+}
+
+func (r *fakeRepo) RecordRetirementOutcome(
+ ctx context.Context,
+ blockMetadataID int64,
+ claimToken string,
+ outcome string,
+ attemptedAt time.Time,
+) error {
+ manifest, ok := r.manifests[blockMetadataID]
+ if !ok || manifest.State != RetirementStateDeleting && manifest.State != RetirementStateDeletedPendingVerification ||
+ manifest.ClaimToken != claimToken {
+ return ErrRetirementClaimUnavailable
+ }
+ manifest.Outcome = outcome
+ manifest.LastAttemptAt = &attemptedAt
+ r.manifests[blockMetadataID] = manifest
+ r.updateManifestRow(manifest)
+ return nil
+}
+
+func (r *fakeRepo) RecordRetirementObjectDeleted(
+ ctx context.Context,
+ blockMetadataID int64,
+ claimToken string,
+ outcome string,
+) (time.Time, error) {
+ r.recordDeletedCalls++
+ if r.recordDeletedErr != nil {
+ return time.Time{}, r.recordDeletedErr
+ }
+ manifest, ok := r.manifests[blockMetadataID]
+ if !ok || manifest.State != RetirementStateDeleting || manifest.ClaimToken != claimToken || manifest.ClaimExpiresAt == nil {
+ return time.Time{}, errors.New("manifest is not deleting")
+ }
+ deletedAt := time.Now().UTC()
+ manifest.State = RetirementStateDeletedPendingVerification
+ manifest.SingleBlockObjectKey = ""
+ manifest.SingleBlockObjectETag = ""
+ manifest.DeletedAt = &deletedAt
+ manifest.LastAttemptAt = &deletedAt
+ manifest.Outcome = outcome
+ r.manifests[blockMetadataID] = manifest
+ for i := range r.rows {
+ if r.rows[i].BlockMetadataID != blockMetadataID {
+ continue
+ }
+ r.rows[i].SingleBlockObjectKey = ""
+ if r.rows[i].Shadow != nil {
+ r.rows[i].Shadow.SingleBlockObjectKey = ""
+ r.rows[i].Shadow.SingleBlockObjectDeletedAt = &deletedAt
+ }
+ r.rows[i].Retirement = &manifest
+ }
+ return deletedAt, nil
+}
+
+func (r *fakeRepo) FinalizeRetirement(
+ ctx context.Context,
+ blockMetadataID int64,
+ claimToken string,
+ outcome string,
+) (time.Time, error) {
+ r.finalizeCalls++
+ if r.finalizeErr != nil {
+ return time.Time{}, r.finalizeErr
+ }
+ manifest, ok := r.manifests[blockMetadataID]
+ if !ok || manifest.State != RetirementStateDeletedPendingVerification || manifest.ClaimToken != claimToken ||
+ manifest.ClaimExpiresAt == nil || manifest.DeletedAt == nil {
+ return time.Time{}, errors.New("manifest is not pending verification")
+ }
+ verifiedAt := time.Now().UTC()
+ manifest.State = RetirementStateDeletedVerified
+ manifest.ClaimToken = ""
+ manifest.ClaimExpiresAt = nil
+ manifest.VerifiedAt = &verifiedAt
+ manifest.LastAttemptAt = &verifiedAt
+ manifest.Outcome = outcome
+ r.manifests[blockMetadataID] = manifest
+ for i := range r.rows {
+ if r.rows[i].BlockMetadataID != blockMetadataID {
+ continue
+ }
+ r.rows[i].Retirement = &manifest
+ }
+ return verifiedAt, nil
+}
+
+func (r *fakeRepo) updateManifestRow(manifest RetirementManifest) {
+ for i := range r.rows {
+ if r.rows[i].BlockMetadataID == manifest.BlockMetadataID {
+ copy := manifest
+ r.rows[i].Retirement = ©
+ }
+ }
+}
+
+func (r *fakeRepo) ListPendingRetirements(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ eligibilityCutoff time.Time,
+ limit uint64,
+) ([]RetirementManifest, error) {
+ var manifests []RetirementManifest
+ for _, manifest := range r.manifests {
+ if manifest.Tag == tag && manifest.Height >= startHeight && manifest.Height < endHeight &&
+ (manifest.State == RetirementStateEligible || manifest.State == RetirementStateDeleting ||
+ manifest.State == RetirementStateDeletedPendingVerification) &&
+ r.retirementEligibleAt(manifest.BlockMetadataID, eligibilityCutoff) {
+ manifests = append(manifests, manifest)
+ }
+ }
+ sort.Slice(manifests, func(i, j int) bool { return manifests[i].Height < manifests[j].Height })
+ if limit > 0 && uint64(len(manifests)) > limit {
+ manifests = manifests[:limit]
+ }
+ return manifests, nil
+}
+
+func (r *fakeRepo) retirementEligibleAt(blockMetadataID int64, eligibilityCutoff time.Time) bool {
+ for _, row := range r.rows {
+ if row.BlockMetadataID != blockMetadataID {
+ continue
+ }
+ return row.Shadow != nil &&
+ row.Shadow.SingleBlockDeleteAfter != nil &&
+ !row.Shadow.SingleBlockDeleteAfter.After(eligibilityCutoff)
+ }
+ return false
+}
+
+type fakeStore struct {
+ heads map[string]ObjectHead
+ versionHeads map[string]ObjectHead
+ topologies map[string]ObjectVersionTopology
+ objects map[string][]byte
+ headCalls map[string]int
+ versionCalls map[string]int
+ policyCalls map[string]int
+ policyErrors map[string]error
+ policyHashes map[string]string
+ policyHook func(key string, call int) (RetentionSafetySnapshot, error)
+ deleted []Candidate
+ deleteMutates bool
+ deleteErrors map[string]error
+ listHook func(key string, call int) ObjectVersionTopology
+ headHook func(key string, call int) ObjectHead
+}
+
+func newFakeStore() *fakeStore {
+ return &fakeStore{
+ heads: make(map[string]ObjectHead),
+ versionHeads: make(map[string]ObjectHead),
+ topologies: make(map[string]ObjectVersionTopology),
+ objects: make(map[string][]byte),
+ headCalls: make(map[string]int),
+ versionCalls: make(map[string]int),
+ policyCalls: make(map[string]int),
+ policyErrors: make(map[string]error),
+ policyHashes: make(map[string]string),
+ deleteErrors: make(map[string]error),
+ }
+}
+
+func (s *fakeStore) InspectObjectRetentionSafety(ctx context.Context, bucket string, key string) (RetentionSafetySnapshot, error) {
+ s.policyCalls[key]++
+ if s.policyHook != nil {
+ return s.policyHook(key, s.policyCalls[key])
+ }
+ configurationSHA256 := s.policyHashes[key]
+ if configurationSHA256 == "" {
+ configurationSHA256 = keySHA256("safe:" + key)
+ }
+ return RetentionSafetySnapshot{ConfigurationSHA256: configurationSHA256}, s.policyErrors[key]
+}
+
+func (s *fakeStore) HeadObject(ctx context.Context, bucket string, key string) (ObjectHead, error) {
+ s.headCalls[key]++
+ if s.headHook != nil {
+ return s.headHook(key, s.headCalls[key]), nil
+ }
+ return s.heads[key], nil
+}
+
+func (s *fakeStore) HeadObjectVersion(ctx context.Context, bucket string, key string, versionID string) (ObjectHead, error) {
+ return s.versionHeads[versionObjectKey(key, versionID)], nil
+}
+
+func (s *fakeStore) ListObjectVersions(ctx context.Context, bucket string, key string) (ObjectVersionTopology, error) {
+ s.versionCalls[key]++
+ if s.listHook != nil {
+ return s.listHook(key, s.versionCalls[key]), nil
+ }
+ return s.topologies[key], nil
+}
+
+func (s *fakeStore) ReadObjectVersion(ctx context.Context, bucket string, key string, versionID string) ([]byte, error) {
+ return append([]byte(nil), s.objects[versionObjectKey(key, versionID)]...), nil
+}
+
+func (s *fakeStore) ReadObjectVersionRange(ctx context.Context, bucket string, key string, versionID string, offset uint64, length uint64) ([]byte, error) {
+ data := s.objects[versionObjectKey(key, versionID)]
+ if offset >= uint64(len(data)) {
+ return nil, errors.New("range starts after object")
+ }
+ end := offset + length
+ if end > uint64(len(data)) {
+ end = uint64(len(data))
+ }
+ return append([]byte(nil), data[offset:end]...), nil
+}
+
+func (s *fakeStore) DeleteObjectVersion(ctx context.Context, bucket string, key string, versionID string) error {
+ if err := s.deleteErrors[versionID]; err != nil {
+ return err
+ }
+ s.deleted = append(s.deleted, Candidate{
+ Bucket: bucket,
+ Key: key,
+ VersionID: versionID,
+ })
+ if s.deleteMutates {
+ topology := s.topologies[key]
+ versions := topology.Versions[:0]
+ for _, version := range topology.Versions {
+ if version.VersionID != versionID {
+ versions = append(versions, version)
+ }
+ }
+ topology.Versions = versions
+ markers := topology.DeleteMarkers[:0]
+ for _, marker := range topology.DeleteMarkers {
+ if marker.VersionID != versionID {
+ markers = append(markers, marker)
+ }
+ }
+ topology.DeleteMarkers = markers
+ if len(topology.Versions) == 0 && len(topology.DeleteMarkers) == 0 {
+ delete(s.topologies, key)
+ delete(s.heads, key)
+ } else {
+ s.topologies[key] = topology
+ }
+ }
+ return nil
+}
+
+type fakePayloadVerifier struct {
+ digest string
+ consolidatedDigest string
+ verifyErr error
+ consolidatedErr error
+ verifyCalls int
+ consolidatedCalls int
+ verifyHook func(call int) (string, error)
+}
+
+func (v *fakePayloadVerifier) Verify(ctx context.Context, candidate Candidate) (string, error) {
+ v.verifyCalls++
+ if v.verifyHook != nil {
+ return v.verifyHook(v.verifyCalls)
+ }
+ return v.digest, v.verifyErr
+}
+
+func (v *fakePayloadVerifier) VerifyConsolidated(ctx context.Context, candidate Candidate) (string, error) {
+ v.consolidatedCalls++
+ return v.consolidatedDigest, v.consolidatedErr
+}
+
+func newTestPlanner(repo *fakeRepo, store *fakeStore) (*Planner, *fakePayloadVerifier) {
+ digest := strings.Repeat("a", sha256HexLength)
+ verifier := &fakePayloadVerifier{digest: digest, consolidatedDigest: digest}
+ planner := &Planner{repo: repo, store: store}
+ planner.verifierFactory = func() blockPayloadVerifier { return verifier }
+ return planner, verifier
+}
+
+func testPlanner(repo *fakeRepo, store *fakeStore) *Planner {
+ planner, _ := newTestPlanner(repo, store)
+ return planner
+}
+
+func versionObjectKey(key string, versionID string) string {
+ return key + "\x00" + versionID
+}
+
+func safeTopology(versionID string, etag string, bytes uint64) ObjectVersionTopology {
+ return ObjectVersionTopology{Versions: []ObjectVersion{{
+ VersionID: versionID,
+ ETag: etag,
+ Bytes: bytes,
+ IsLatest: true,
+ }}}
+}
+
+func TestPlannerPlan_NotEligibleRetentionPeriod(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-time.Hour)
+ store := newFakeStore()
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ planner := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store)
+
+ report, err := planner.Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipRetentionPeriodActive, report.Items[0].SkipReason)
+ require.Zero(store.headCalls[row.Shadow.ConsolidatedObjectKey])
+ require.Zero(store.versionCalls[row.SingleBlockObjectKey])
+}
+
+func TestPlannerPlan_UsesFrozenEligibilityCutoff(t *testing.T) {
+ require := require.New(t)
+ cutoff := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ now := cutoff.Add(2 * time.Hour)
+ row := testRow(
+ 100,
+ "hash-100",
+ "single-block/100.zstd",
+ "consolidated/canary.cscb.zstd",
+ cutoff.Add(-8*24*time.Hour),
+ )
+ deleteAfter := cutoff.Add(time.Hour)
+ row.Shadow.SingleBlockDeleteAfter = &deleteAfter
+ req := testRequest(now, false)
+ req.EligibilityCutoff = cutoff
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, newFakeStore()).Plan(context.Background(), req)
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(SkipRetentionPeriodActive, report.Items[0].SkipReason)
+}
+
+func TestPlannerPlan_MissingRetirementMarkerIsNeverEligible(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-30 * 24 * time.Hour)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ row.Shadow.SingleBlockRetentionStartedAt = nil
+ row.Shadow.SingleBlockDeleteAfter = nil
+ store := newFakeStore()
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipMissingRetirementMarker, report.Items[0].SkipReason)
+ require.Zero(store.headCalls[row.Shadow.ConsolidatedObjectKey])
+ require.Zero(store.versionCalls[row.SingleBlockObjectKey])
+}
+
+func TestPlannerPlan_MissingCSCBObject(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/missing.cscb.zstd", validatedAt)
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ planner := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store)
+
+ report, err := planner.Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipMissingCSCBObject, report.Items[0].SkipReason)
+ require.Equal("single-block-v1", report.Items[0].VersionID)
+ require.Equal(uint64(42), report.Items[0].SingleBlockBytes)
+}
+
+func TestPlannerPlan_RequiresLiveCSCBWriteOncePolicy(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", now.Add(-8*24*time.Hour))
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ store.heads[row.PrimaryObjectKey] = cscbObjectHead(1024, 1024)
+ store.policyErrors[row.PrimaryObjectKey] = errors.New("bucket policy is missing")
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipCSCBWriteOncePolicyUnverified, report.Items[0].SkipReason)
+ require.False(report.Items[0].CSCBWriteOncePolicy)
+ require.False(report.SafetyGates.CSCBWriteOncePolicyVerified)
+ require.Equal(1, store.policyCalls[row.PrimaryObjectKey])
+}
+
+func TestPlannerPlan_InvalidMetadataReference(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ row.Shadow.SingleBlockObjectKey = "single-block/stale.zstd"
+ store := newFakeStore()
+ planner := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store)
+
+ report, err := planner.Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipInvalidMetadataReference, report.Items[0].SkipReason)
+ require.Zero(store.headCalls[row.Shadow.ConsolidatedObjectKey])
+}
+
+func TestPlannerPlan_ActiveSingleBlockMetadataIsNeverRetired(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+ row := activeSingleBlockTestRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ store.heads[row.Shadow.ConsolidatedObjectKey] = cscbObjectHead(1024, 1024)
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipActiveMetadataStillSingleBlock, report.Items[0].SkipReason)
+ require.Zero(store.headCalls[row.Shadow.ConsolidatedObjectKey])
+ require.Zero(store.versionCalls[row.SingleBlockObjectKey])
+}
+
+func TestPlannerPlan_ValidationAndApprovalGates(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+
+ t.Run("validation not passed", func(t *testing.T) {
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ row.Shadow.ValidatedAt = nil
+ store := newFakeStore()
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipValidationNotPassed, report.Items[0].SkipReason)
+ require.Zero(store.headCalls[row.Shadow.ConsolidatedObjectKey])
+ require.Zero(store.versionCalls[row.SingleBlockObjectKey])
+ })
+
+ t.Run("range not approved", func(t *testing.T) {
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ store := newFakeStore()
+ req := testRequest(now, false)
+ req.Approval.EndHeight = req.EndHeight - 1
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), req)
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipChainRangeNotApproved, report.Items[0].SkipReason)
+ require.Zero(store.headCalls[row.Shadow.ConsolidatedObjectKey])
+ require.Zero(store.versionCalls[row.SingleBlockObjectKey])
+ })
+
+ t.Run("dry run may omit approval and still reaches safety inspection", func(t *testing.T) {
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ store.heads[row.PrimaryObjectKey] = cscbObjectHead(1024, 1024)
+ req := testRequest(now, false)
+ req.Approval = Approval{}
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), req)
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionReportOnly, report.Items[0].Action)
+ require.Empty(report.Items[0].SkipReason)
+ })
+
+ t.Run("execution never derives an approval from omitted operator input", func(t *testing.T) {
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ store := newFakeStore()
+ req := testRequest(now, true)
+ req.ProductionDeleteEnabled = true
+ req.Approval = Approval{}
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), req)
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipChainRangeNotApproved, report.Items[0].SkipReason)
+ require.Zero(store.versionCalls[row.SingleBlockObjectKey])
+ })
+}
+
+func TestApprovalMatchesExactRangeUnlessContainmentIsExplicit(t *testing.T) {
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ req := testRequest(now, true)
+ req.Approval.StartHeight = req.StartHeight - 1000
+ req.Approval.EndHeight = req.EndHeight + 1000
+ require.False(t, approvalMatches(req))
+
+ req.Approval.AllowContainingRange = true
+ require.True(t, approvalMatches(req))
+
+ req.Approval.StartHeight = req.StartHeight + 1
+ require.False(t, approvalMatches(req))
+
+ req = testRequest(now, true)
+ req.Approval.EndHeight = req.EndHeight - 1
+ require.False(t, approvalMatches(req))
+}
+
+func TestPlannerPlan_FallbackAndDirectStorageClientGates(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+
+ t.Run("fallback errors", func(t *testing.T) {
+ req := testRequest(now, false)
+ req.FallbackErrorCount = 1
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, newFakeStore()).Plan(context.Background(), req)
+ require.NoError(err)
+ require.Equal(SkipActiveFallbackOrReadErrors, report.Items[0].SkipReason)
+ })
+
+ t.Run("dry run does not require direct storage client assertion", func(t *testing.T) {
+ req := testRequest(now, false)
+ req.DirectStorageClientsGuarded = false
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ store.heads[row.PrimaryObjectKey] = cscbObjectHead(1024, 1024)
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), req)
+ require.NoError(err)
+ require.Equal(ActionReportOnly, report.Items[0].Action)
+ require.Empty(report.Items[0].SkipReason)
+ require.False(report.SafetyGates.DirectStorageClientsGuarded)
+ })
+
+ t.Run("execution requires direct storage client assertion", func(t *testing.T) {
+ req := testRequest(now, true)
+ req.DirectStorageClientsGuarded = false
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, newFakeStore()).Plan(context.Background(), req)
+ require.NoError(err)
+ require.Equal(SkipDirectStorageClientsNotGuarded, report.Items[0].SkipReason)
+ })
+}
+
+func TestPlannerPlan_RequiresGuardedSingleBlockWritersOnlyForExecution(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", now.Add(-8*24*time.Hour))
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ store.heads[row.PrimaryObjectKey] = cscbObjectHead(1024, 1024)
+ planner := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store)
+
+ dryRunReq := testRequest(now, false)
+ dryRunReq.SingleBlockWritersGuarded = false
+ dryRunReport, err := planner.Plan(context.Background(), dryRunReq)
+ require.NoError(err)
+ require.Equal(ActionReportOnly, dryRunReport.Items[0].Action)
+ require.False(dryRunReport.SafetyGates.SingleBlockWritersGuarded)
+
+ executeReq := testRequest(now, true)
+ executeReq.ProductionDeleteEnabled = true
+ executeReq.SingleBlockWritersGuarded = false
+ executeReport, err := planner.Plan(context.Background(), executeReq)
+ require.NoError(err)
+ require.Equal(ActionSkip, executeReport.Items[0].Action)
+ require.Equal(SkipSingleBlockWritersNotGuarded, executeReport.Items[0].SkipReason)
+ require.Error(planner.Apply(context.Background(), executeReq, executeReport))
+}
+
+func TestPlannerPlan_SkippedReorgAndPromotedRows(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+ skipped := testRow(100, "hash-100", "", "consolidated/canary.cscb.zstd", validatedAt)
+ skipped.Skipped = true
+
+ stale := testRow(101, "hash-101", "single-block/101.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ stale.Shadow.Hash = "hash-before-reorg"
+
+ promoted := testRow(102, "hash-102", "consolidated/primary.cscb.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ promoted.PrimaryObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH
+ promoted.PrimaryByteLength = 123
+ promoted.PrimaryObjectKey = "consolidated/primary.cscb.zstd"
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{skipped, stale, promoted}}, newFakeStore()).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Len(report.Items, 3)
+ require.Equal(SkipSkippedBlock, report.Items[0].SkipReason)
+ require.Equal(SkipInvalidMetadataReference, report.Items[1].SkipReason)
+ require.Equal(SkipInvalidMetadataReference, report.Items[2].SkipReason)
+}
+
+func TestPlannerPlan_PromotedRowUsesShadowSingleBlockKeyAndRetireAfter(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-time.Hour)
+ retiredAt := now.Add(-2 * time.Minute)
+ retireAfter := now.Add(-time.Minute)
+ singleBlockKey := "single-block/102.zstd"
+ cscbKey := "consolidated/primary.cscb.zstd"
+ row := testRow(102, "hash-102", singleBlockKey, cscbKey, validatedAt)
+ row.Shadow.SingleBlockRetentionStartedAt = &retiredAt
+ row.Shadow.SingleBlockDeleteAfter = &retireAfter
+ store := newFakeStore()
+ store.topologies[singleBlockKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ store.heads[cscbKey] = cscbObjectHead(1024, 1024)
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionReportOnly, report.Items[0].Action)
+ require.Equal(singleBlockKey, report.Items[0].Key)
+ require.Equal(cscbKey, report.Items[0].ConsolidatedKey)
+ require.Equal(retiredAt, *report.Items[0].RetiredAt)
+ require.Equal(retireAfter, *report.Items[0].EligibleAt)
+ require.Empty(report.Items[0].SkipReason)
+}
+
+func TestPlannerPlan_RejectsMarkerOnlyTopology(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = ObjectVersionTopology{DeleteMarkers: []ObjectDeleteMarker{{
+ VersionID: "delete-marker-v1",
+ IsLatest: true,
+ }}}
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipUnsafeSingleBlockVersionTopology, report.Items[0].SkipReason)
+ require.Equal(1, report.Summary.DeleteMarkerRows)
+ require.Zero(store.headCalls[row.Shadow.ConsolidatedObjectKey])
+}
+
+func TestPlannerPlan_AcceptsImmutableMultiVersionTopology(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = ObjectVersionTopology{
+ Versions: []ObjectVersion{
+ {VersionID: "single-block-v2", ETag: "etag-v2", Bytes: 42, IsLatest: true},
+ {VersionID: "single-block-v1", ETag: "etag-v1", Bytes: 41},
+ },
+ DeleteMarkers: []ObjectDeleteMarker{{VersionID: "delete-marker-v1"}},
+ }
+ store.heads[row.PrimaryObjectKey] = cscbObjectHead(1024, 1024)
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Equal(ActionReportOnly, report.Items[0].Action)
+ require.Empty(report.Items[0].SkipReason)
+ require.Equal("single-block-v2", report.Items[0].VersionID)
+ require.Equal(2, report.Items[0].SingleBlockVersions)
+ require.Equal(1, report.Items[0].DeleteMarkers)
+}
+
+func TestPlannerPlan_AcceptsCurrentDeleteMarkerWithHistoricalDataVersions(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", now.Add(-8*24*time.Hour))
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = ObjectVersionTopology{
+ Versions: []ObjectVersion{
+ {VersionID: "single-block-v2", ETag: "etag-v2", Bytes: 42},
+ {VersionID: "single-block-v1", ETag: "etag-v1", Bytes: 41},
+ },
+ DeleteMarkers: []ObjectDeleteMarker{{VersionID: "delete-marker-v3", IsLatest: true}},
+ }
+ store.heads[row.PrimaryObjectKey] = cscbObjectHead(1024, 1024)
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Equal(ActionReportOnly, report.Items[0].Action)
+ require.Empty(report.Items[0].SkipReason)
+ require.Equal("single-block-v2", report.Items[0].VersionID)
+ require.Equal([]string{"single-block-v2", "single-block-v1"}, report.Items[0].VersionIDs)
+ require.Equal([]string{"delete-marker-v3"}, report.Items[0].DeleteMarkerVersionIDs)
+}
+
+func TestPlannerPlan_RejectsTopologyWithoutLatestEntry(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", now.Add(-8*24*time.Hour))
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = ObjectVersionTopology{Versions: []ObjectVersion{{
+ VersionID: "single-block-v1",
+ ETag: "etag-v1",
+ Bytes: 42,
+ }}}
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipUnsafeSingleBlockVersionTopology, report.Items[0].SkipReason)
+ require.Zero(store.headCalls[row.Shadow.ConsolidatedObjectKey])
+}
+
+func TestPlannerPlan_RejectsMutableNullVersionIDs(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", now.Add(-8*24*time.Hour))
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("null", "single-block-etag", 42)
+ store.heads[row.PrimaryObjectKey] = cscbObjectHead(1024, 1024)
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Equal(SkipSingleBlockVersionIDUnavailable, report.Items[0].SkipReason)
+
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ head := cscbObjectHead(1024, 1024)
+ head.VersionID = "null"
+ store.heads[row.PrimaryObjectKey] = head
+ report, err = testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Equal(SkipMissingCSCBObject, report.Items[0].SkipReason)
+
+ store.topologies[row.SingleBlockObjectKey] = ObjectVersionTopology{
+ Versions: []ObjectVersion{{VersionID: "single-block-v1", ETag: "single-block-etag", Bytes: 42, IsLatest: true}},
+ DeleteMarkers: []ObjectDeleteMarker{{VersionID: "null"}},
+ }
+ store.heads[row.PrimaryObjectKey] = cscbObjectHead(1024, 1024)
+ report, err = testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Equal(SkipSingleBlockVersionIDUnavailable, report.Items[0].SkipReason)
+}
+
+func TestPlannerPlan_RequiresIndependentPayloadComparison(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", now.Add(-8*24*time.Hour))
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ store.heads[row.PrimaryObjectKey] = cscbObjectHead(1024, 1024)
+ planner, verifier := newTestPlanner(&fakeRepo{rows: []MetadataRow{row}}, store)
+ verifier.verifyErr = errors.New("payloads differ")
+
+ report, err := planner.Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipSingleBlockPayloadMismatch, report.Items[0].SkipReason)
+ require.Empty(report.Items[0].PayloadSHA256)
+}
+
+func TestPlannerPlan_DryRunOutput(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ store.heads[row.Shadow.ConsolidatedObjectKey] = cscbObjectHead(1024, 1024)
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+
+ var buf bytes.Buffer
+ require.NoError(WriteReportJSON(&buf, report))
+ var decoded Report
+ require.NoError(json.Unmarshal(buf.Bytes(), &decoded))
+ require.True(decoded.DryRun)
+ require.Equal("production", decoded.Environment)
+ require.Equal("solana", decoded.Blockchain)
+ require.Equal("mainnet", decoded.Network)
+ require.Equal(uint64(428058000), decoded.StartHeight)
+ require.Equal(uint64(428059000), decoded.EndHeight)
+ require.Equal(1, decoded.Summary.EligibleRows)
+ require.Equal(ActionReportOnly, decoded.Items[0].Action)
+ require.Equal("single-block-v1", decoded.Items[0].VersionID)
+ require.Empty(decoded.Items[0].SkipReason)
+}
+
+func TestPlannerPlan_ExactSolanaCanaryReportShape(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+ const (
+ startHeight = uint64(428058000)
+ endHeight = uint64(428059000)
+ cscbKey = "BLOCKCHAIN_SOLANA/NETWORK_SOLANA_MAINNET/consolidated/v=7/shard=00000000042805000000-00000000042806000000/00000000000428058000-00000000000428059000-deadbeef.cscb.zstd"
+ bucket = "co-chainstorage-solana-mainnet-prod"
+ singleBlockBytesEach = uint64(123)
+ )
+
+ rows := make([]MetadataRow, 0, endHeight-startHeight)
+ store := newFakeStore()
+ store.heads[cscbKey] = cscbObjectHead(512<<10, 2<<20)
+ for height := startHeight; height < endHeight; height++ {
+ hash := fmt.Sprintf("solana-hash-%d", height)
+ key := fmt.Sprintf("BLOCKCHAIN_SOLANA/NETWORK_SOLANA_MAINNET/0/%d/%s.zstd", height, hash)
+ rows = append(rows, testRow(height, hash, key, cscbKey, validatedAt))
+ store.topologies[key] = safeTopology(
+ fmt.Sprintf("single-block-version-%d", height),
+ fmt.Sprintf("single-block-etag-%d", height),
+ singleBlockBytesEach,
+ )
+ }
+ req := testRequest(now, false)
+ req.Bucket = bucket
+ req.StartHeight = startHeight
+ req.EndHeight = endHeight
+ report, err := testPlanner(&fakeRepo{rows: rows}, store).Plan(context.Background(), req)
+ require.NoError(err)
+
+ require.Equal(bucket, report.Bucket)
+ require.Equal(startHeight, report.StartHeight)
+ require.Equal(endHeight, report.EndHeight)
+ require.Len(report.Items, 1000)
+ require.Equal(1000, report.Summary.TotalRows)
+ require.Equal(1000, report.Summary.EligibleRows)
+ require.Zero(report.Summary.SkippedRows)
+ require.Equal(uint64(1000)*singleBlockBytesEach, report.Summary.SingleBlockBytes)
+ require.Equal(uint64(1000)*singleBlockBytesEach, report.Summary.EligibleBytes)
+ require.Equal(1, store.headCalls[cscbKey])
+ require.Equal(1, store.policyCalls[cscbKey])
+ require.True(report.SafetyGates.CSCBWriteOncePolicyVerified)
+
+ first := report.Items[0]
+ require.Equal(startHeight, first.Height)
+ require.Equal("solana-hash-428058000", first.Hash)
+ require.Equal("single-block-version-428058000", first.VersionID)
+ require.Equal(singleBlockBytesEach, first.SingleBlockBytes)
+ require.Equal(cscbKey, first.ConsolidatedKey)
+ require.Equal(ActionReportOnly, first.Action)
+ require.Empty(first.SkipReason)
+
+ last := report.Items[len(report.Items)-1]
+ require.Equal(endHeight-1, last.Height)
+ require.Equal("solana-hash-428058999", last.Hash)
+ require.Equal("single-block-version-428058999", last.VersionID)
+ require.Equal(cscbKey, last.ConsolidatedKey)
+ require.Equal(ActionReportOnly, last.Action)
+}
+
+func TestPlannerApply_RefetchesLiveCSCBWriteOncePolicyImmediatelyBeforeDelete(t *testing.T) {
+ require := require.New(t)
+ _, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ key := report.Items[0].ConsolidatedKey
+ require.Equal(1, store.policyCalls[key])
+ store.policyHook = func(hookKey string, call int) (RetentionSafetySnapshot, error) {
+ snapshot := RetentionSafetySnapshot{ConfigurationSHA256: keySHA256("safe:" + hookKey)}
+ if call >= 3 {
+ return snapshot, errors.New("bucket policy was removed")
+ }
+ return snapshot, nil
+ }
+
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Empty(store.deleted)
+ require.Equal(SkipCSCBWriteOncePolicyUnverified, report.Items[0].SkipReason)
+ require.False(report.SafetyGates.CSCBWriteOncePolicyVerified)
+ require.Equal(3, store.policyCalls[key])
+ manifest := repo.manifests[report.Items[0].BlockMetadataID]
+ require.Equal(RetirementStateDeleting, manifest.State)
+ require.Equal(SkipCSCBWriteOncePolicyUnverified, manifest.Outcome)
+}
+
+func TestPlannerApply_RequiresStableRetentionSafetyConfigurationBeforeDelete(t *testing.T) {
+ require := require.New(t)
+ _, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ key := report.Items[0].ConsolidatedKey
+ observedAt := req.Now
+ repo.safetyNow = observedAt
+ repo.safetyObservations = map[string]fakeSafetyObservation{
+ req.Bucket + "\x00" + key: {
+ configurationSHA256: keySHA256("previous-safe-configuration"),
+ firstObservedAt: observedAt.Add(-RetentionSafetyQuiescencePeriod),
+ },
+ }
+ newConfigurationSHA256 := keySHA256("new-safe-configuration")
+ store.policyHashes[key] = newConfigurationSHA256
+
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Empty(store.deleted)
+ require.Equal(SkipCSCBSafetyQuiescenceActive, report.Items[0].SkipReason)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ _, prepared := repo.manifests[report.Items[0].BlockMetadataID]
+ require.False(prepared)
+ observation := repo.safetyObservations[req.Bucket+"\x00"+key]
+ require.Equal(newConfigurationSHA256, observation.configurationSHA256)
+ require.Equal(observedAt, observation.firstObservedAt)
+}
+
+func TestVerifyRetentionSafetyDoesNotCreditFailedInspectionTime(t *testing.T) {
+ require := require.New(t)
+ observedAt := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ key := "consolidated/canary.cscb.zstd"
+ item := Candidate{Bucket: "integration-bucket", ConsolidatedKey: key}
+ observationKey := item.Bucket + "\x00" + key
+ repo := &fakeRepo{
+ safetyNow: observedAt,
+ safetyObservations: map[string]fakeSafetyObservation{
+ observationKey: {
+ configurationSHA256: keySHA256("previous-safe-configuration"),
+ firstObservedAt: observedAt.Add(-time.Hour),
+ },
+ },
+ }
+ store := newFakeStore()
+ store.policyErrors[key] = errors.New("GetBucketLifecycleConfiguration temporarily unavailable")
+ planner := testPlanner(repo, store)
+
+ reason, err := planner.verifyCSCBRetentionSafety(context.Background(), &item)
+ require.Error(err)
+ require.Equal(SkipCSCBWriteOncePolicyUnverified, reason)
+ observation := repo.safetyObservations[observationKey]
+ require.Equal(unverifiedRetentionSafetySHA256, observation.configurationSHA256)
+ require.Equal(observedAt, observation.firstObservedAt)
+
+ repo.safetyNow = observedAt.Add(20 * time.Minute)
+ delete(store.policyErrors, key)
+ reason, err = planner.verifyCSCBRetentionSafety(context.Background(), &item)
+ require.Error(err)
+ require.Equal(SkipCSCBSafetyQuiescenceActive, reason)
+ observation = repo.safetyObservations[observationKey]
+ require.Equal(keySHA256("safe:"+key), observation.configurationSHA256)
+ require.Equal(repo.safetyNow, observation.firstObservedAt)
+}
+
+func TestPlannerApply_RefetchesLiveCSCBWriteOncePolicyForEveryDelete(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ cscbKey := "consolidated/shared.cscb.zstd"
+ first := testRow(428058000, "hash-428058000", "single-block/428058000.zstd", cscbKey, now.Add(-8*24*time.Hour))
+ second := testRow(428058001, "hash-428058001", "single-block/428058001.zstd", cscbKey, now.Add(-8*24*time.Hour))
+ repo := &fakeRepo{rows: []MetadataRow{first, second}}
+ store := newFakeStore()
+ store.deleteMutates = true
+ for _, row := range repo.rows {
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ }
+ cscbHead := cscbObjectHead(1024, 1024)
+ store.heads[cscbKey] = cscbHead
+ store.versionHeads[versionObjectKey(cscbKey, cscbHead.VersionID)] = cscbHead
+ planner := testPlanner(repo, store)
+ req := testRequest(now, true)
+ req.ProductionDeleteEnabled = true
+ report, err := planner.Plan(context.Background(), req)
+ require.NoError(err)
+ require.Equal(1, store.policyCalls[cscbKey])
+
+ require.NoError(planner.Apply(context.Background(), req, report))
+ require.Len(store.deleted, 2)
+ require.Equal(4, store.policyCalls[cscbKey])
+}
+
+func TestPlannerHeartbeatsFromRowAndVersionLoops(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(428058000, "hash-428058000", "single-block/428058000.zstd", "consolidated/canary.cscb.zstd", now.Add(-8*24*time.Hour))
+ repo := &fakeRepo{rows: []MetadataRow{row}}
+ store := newFakeStore()
+ store.deleteMutates = true
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ cscbHead := cscbObjectHead(1024, 1024)
+ store.heads[row.PrimaryObjectKey] = cscbHead
+ store.versionHeads[versionObjectKey(row.PrimaryObjectKey, cscbHead.VersionID)] = cscbHead
+ planner := testPlanner(repo, store)
+ var beats []string
+ WithHeartbeat(func(_ context.Context, details ...any) {
+ require.NotEmpty(details)
+ label, ok := details[0].(string)
+ require.True(ok)
+ beats = append(beats, label)
+ })(planner)
+
+ req := testRequest(now, true)
+ req.ProductionDeleteEnabled = true
+ report, err := planner.Plan(context.Background(), req)
+ require.NoError(err)
+ require.Contains(beats, "retirement.plan.row")
+
+ require.NoError(planner.Apply(context.Background(), req, report))
+ require.Contains(beats, "retirement.apply.row")
+ require.Contains(beats, "retirement.delete.version")
+}
+
+func TestPlannerApply_RefusesToStartWhenAlreadyCanceled(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(428058000, "hash-428058000", "single-block/428058000.zstd", "consolidated/canary.cscb.zstd", now.Add(-8*24*time.Hour))
+ repo := &fakeRepo{rows: []MetadataRow{row}}
+ store := newFakeStore()
+ store.deleteMutates = true
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ cscbHead := cscbObjectHead(1024, 1024)
+ store.heads[row.PrimaryObjectKey] = cscbHead
+ store.versionHeads[versionObjectKey(row.PrimaryObjectKey, cscbHead.VersionID)] = cscbHead
+ planner := testPlanner(repo, store)
+ req := testRequest(now, true)
+ req.ProductionDeleteEnabled = true
+ report, err := planner.Plan(context.Background(), req)
+ require.NoError(err)
+ require.Equal(ActionDeleteObjectVersion, report.Items[0].Action)
+
+ canceledCtx, cancel := context.WithCancel(context.Background())
+ cancel()
+ err = planner.Apply(canceledCtx, req, report)
+ require.ErrorIs(err, context.Canceled)
+ require.Empty(store.deleted)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipNotAttemptedAfterFailure, report.Items[0].SkipReason)
+}
+
+func TestPlannerApply_HaltsBetweenRowsOnCancellation(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ cscbKey := "consolidated/shared.cscb.zstd"
+ first := testRow(428058000, "hash-428058000", "single-block/428058000.zstd", cscbKey, now.Add(-8*24*time.Hour))
+ second := testRow(428058001, "hash-428058001", "single-block/428058001.zstd", cscbKey, now.Add(-8*24*time.Hour))
+ repo := &fakeRepo{rows: []MetadataRow{first, second}}
+ store := newFakeStore()
+ store.deleteMutates = true
+ for _, row := range repo.rows {
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ }
+ cscbHead := cscbObjectHead(1024, 1024)
+ store.heads[cscbKey] = cscbHead
+ store.versionHeads[versionObjectKey(cscbKey, cscbHead.VersionID)] = cscbHead
+ planner := testPlanner(repo, store)
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+ // Cancellation arrives while the first row is deleting; the second row
+ // must never be attempted.
+ WithHeartbeat(func(_ context.Context, details ...any) {
+ if len(details) > 0 && details[0] == "retirement.delete.version" {
+ cancel()
+ }
+ })(planner)
+
+ req := testRequest(now, true)
+ req.ProductionDeleteEnabled = true
+ report, err := planner.Plan(context.Background(), req)
+ require.NoError(err)
+
+ err = planner.Apply(ctx, req, report)
+ require.ErrorIs(err, context.Canceled)
+ require.Len(store.deleted, 1)
+ require.Equal(ActionSkip, report.Items[1].Action)
+ require.Equal(SkipNotAttemptedAfterFailure, report.Items[1].SkipReason)
+}
+
+func TestPlannerApply_RequiresProductionGateAndFinalizesMetadata(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(428058000, "hash-428058000", "single-block/428058000.zstd", "consolidated/canary.cscb.zstd", now.Add(-8*24*time.Hour))
+ repo := &fakeRepo{rows: []MetadataRow{row}}
+ store := newFakeStore()
+ store.deleteMutates = true
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ cscbHead := cscbObjectHead(1024, 1024)
+ store.heads[row.PrimaryObjectKey] = cscbHead
+ store.versionHeads[versionObjectKey(row.PrimaryObjectKey, cscbHead.VersionID)] = cscbHead
+ planner := testPlanner(repo, store)
+ req := testRequest(now, true)
+ req.ProductionDeleteEnabled = true
+ report, err := planner.Plan(context.Background(), req)
+ require.NoError(err)
+ require.Equal(ActionDeleteObjectVersion, report.Items[0].Action)
+
+ blockedReq := req
+ blockedReq.ProductionDeleteEnabled = false
+ err = planner.Apply(context.Background(), blockedReq, report)
+ require.Error(err)
+ require.Empty(store.deleted)
+
+ err = planner.Apply(context.Background(), req, report)
+ require.NoError(err)
+ require.Len(store.deleted, 1)
+ require.Equal("single-block-v1", store.deleted[0].VersionID)
+ require.Equal(ActionDeletedVerified, report.Items[0].Action)
+ require.Empty(report.Items[0].Key)
+ require.NotNil(report.Items[0].SingleBlockDeletedAt)
+ require.NotNil(report.Items[0].RetirementVerifiedAt)
+ manifest := repo.manifests[row.BlockMetadataID]
+ require.Equal(RetirementStateDeletedVerified, manifest.State)
+ require.Empty(manifest.SingleBlockObjectKey)
+ require.NotNil(manifest.DeletedAt)
+ require.Empty(repo.rows[0].SingleBlockObjectKey)
+ require.Empty(repo.rows[0].Shadow.SingleBlockObjectKey)
+ require.NotNil(repo.rows[0].Shadow.SingleBlockObjectDeletedAt)
+
+ dryRunReq := req
+ dryRunReq.Execute = false
+ dryRunReq.ProductionDeleteEnabled = false
+ finalReport, err := planner.Plan(context.Background(), dryRunReq)
+ require.NoError(err)
+ require.Equal(ActionAlreadyDeleted, finalReport.Items[0].Action)
+ require.NotNil(finalReport.Items[0].SingleBlockDeletedAt)
+ require.NotNil(finalReport.Items[0].RetirementVerifiedAt)
+ require.Empty(finalReport.Items[0].Key)
+ require.Equal(keySHA256(row.SingleBlockObjectKey), manifest.SingleBlockObjectKeySHA256)
+}
+
+func TestPlannerApply_DeletesEveryPinnedVersionAndDeleteMarker(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newMultiVersionExecutableTestFixture(t)
+
+ err := planner.Apply(context.Background(), req, report)
+ require.NoError(err)
+ require.Equal(ActionDeletedVerified, report.Items[0].Action)
+ require.Len(store.deleted, 3)
+ require.Equal([]string{"single-block-v1", "single-block-v2", "delete-marker-v1"}, []string{
+ store.deleted[0].VersionID,
+ store.deleted[1].VersionID,
+ store.deleted[2].VersionID,
+ })
+ require.Empty(store.topologies[row.SingleBlockObjectKey].Versions)
+ require.Empty(store.topologies[row.SingleBlockObjectKey].DeleteMarkers)
+ require.Empty(repo.rows[0].SingleBlockObjectKey)
+ require.Empty(repo.rows[0].Shadow.SingleBlockObjectKey)
+ manifest := repo.manifests[row.BlockMetadataID]
+ require.Equal(RetirementStateDeletedVerified, manifest.State)
+ require.Equal([]string{"single-block-v2", "single-block-v1"}, manifest.SingleBlockObjectVersionIDs)
+ require.Equal([]string{"delete-marker-v1"}, manifest.SingleBlockDeleteMarkerVersionIDs)
+}
+
+func TestPlannerReconcile_ResumesPartiallyDeletedPinnedTopology(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newMultiVersionExecutableTestFixture(t)
+ store.deleteErrors["single-block-v2"] = errors.New("temporary S3 failure")
+
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Equal([]string{"single-block-v1"}, []string{store.deleted[0].VersionID})
+ require.Equal(RetirementStateDeleting, repo.manifests[row.BlockMetadataID].State)
+ require.Len(store.topologies[row.SingleBlockObjectKey].Versions, 1)
+ require.Len(store.topologies[row.SingleBlockObjectKey].DeleteMarkers, 1)
+ require.Equal(row.SingleBlockObjectKey, repo.rows[0].SingleBlockObjectKey)
+
+ delete(store.deleteErrors, "single-block-v2")
+ manifest := repo.manifests[row.BlockMetadataID]
+ expiredAt := time.Now().UTC().Add(-time.Second)
+ manifest.ClaimExpiresAt = &expiredAt
+ repo.manifests[row.BlockMetadataID] = manifest
+ repo.updateManifestRow(manifest)
+
+ reconcileReport, err := planner.Reconcile(context.Background(), req)
+ require.NoError(err)
+ require.Equal(ActionDeletedVerified, reconcileReport.Items[0].Action)
+ require.Equal([]string{"single-block-v1", "single-block-v2", "delete-marker-v1"}, []string{
+ store.deleted[0].VersionID,
+ store.deleted[1].VersionID,
+ store.deleted[2].VersionID,
+ })
+ require.Empty(repo.rows[0].SingleBlockObjectKey)
+ require.Empty(repo.rows[0].Shadow.SingleBlockObjectKey)
+ require.Equal(RetirementStateDeletedVerified, repo.manifests[row.BlockMetadataID].State)
+}
+
+func TestPlannerReconcile_ExcludesManifestBeyondFrozenEligibilityCutoff(t *testing.T) {
+ require := require.New(t)
+ _, repo, store, planner, _, req, _ := newMultiVersionExecutableTestFixture(t)
+ cutoff := req.Now
+ deleteAfter := cutoff.Add(time.Hour)
+ repo.rows[0].Shadow.SingleBlockDeleteAfter = &deleteAfter
+ req.Now = cutoff.Add(2 * time.Hour)
+ req.EligibilityCutoff = cutoff
+
+ report, err := planner.Reconcile(context.Background(), req)
+ require.NoError(err)
+ require.Empty(report.Items)
+ require.Empty(store.deleted)
+}
+
+func TestPlannerReconcile_RejectsUnpinnedVersionAfterPartialDeletion(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newMultiVersionExecutableTestFixture(t)
+ store.deleteErrors["single-block-v2"] = errors.New("temporary S3 failure")
+ require.Error(planner.Apply(context.Background(), req, report))
+
+ delete(store.deleteErrors, "single-block-v2")
+ topology := store.topologies[row.SingleBlockObjectKey]
+ topology.Versions = append(topology.Versions, ObjectVersion{
+ VersionID: "unpinned-v3",
+ ETag: "unpinned-etag",
+ Bytes: 40,
+ })
+ store.topologies[row.SingleBlockObjectKey] = topology
+ manifest := repo.manifests[row.BlockMetadataID]
+ expiredAt := time.Now().UTC().Add(-time.Second)
+ manifest.ClaimExpiresAt = &expiredAt
+ repo.manifests[row.BlockMetadataID] = manifest
+ repo.updateManifestRow(manifest)
+
+ reconcileReport, err := planner.Reconcile(context.Background(), req)
+ require.Error(err)
+ require.Equal(ActionSkip, reconcileReport.Items[0].Action)
+ require.Equal(SkipUnsafeSingleBlockVersionTopology, reconcileReport.Items[0].SkipReason)
+ require.Len(store.deleted, 1)
+ require.Equal(row.SingleBlockObjectKey, repo.rows[0].SingleBlockObjectKey)
+}
+
+func TestPlannerApply_RejectsTamperedReportBeforeManifestWrite(t *testing.T) {
+ for _, test := range []struct {
+ name string
+ mutate func(req PlanRequest, report *Report)
+ }{
+ {
+ name: "single-block key",
+ mutate: func(req PlanRequest, report *Report) {
+ report.Items[0].Key = "single-block/other.zstd"
+ },
+ },
+ {
+ name: "height outside approval",
+ mutate: func(req PlanRequest, report *Report) {
+ report.Items[0].Height = req.EndHeight
+ },
+ },
+ {
+ name: "dry-run report",
+ mutate: func(req PlanRequest, report *Report) {
+ report.DryRun = true
+ },
+ },
+ {
+ name: "production gate mismatch",
+ mutate: func(req PlanRequest, report *Report) {
+ report.SafetyGates.ProductionDeleteEnabled = false
+ },
+ },
+ {
+ name: "single-block writer gate mismatch",
+ mutate: func(req PlanRequest, report *Report) {
+ report.SafetyGates.SingleBlockWritersGuarded = false
+ },
+ },
+ {
+ name: "write-once policy gate mismatch",
+ mutate: func(req PlanRequest, report *Report) {
+ report.SafetyGates.CSCBWriteOncePolicyVerified = false
+ },
+ },
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ require := require.New(t)
+ _, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ test.mutate(req, report)
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Empty(repo.manifests)
+ require.Empty(store.deleted)
+ })
+ }
+}
+
+func TestPlannerApply_RevalidatesTopologyAfterManifestPersistence(t *testing.T) {
+ require := require.New(t)
+ _, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ safe := store.topologies[report.Items[0].Key]
+ store.listHook = func(key string, call int) ObjectVersionTopology {
+ if call < 3 {
+ return safe
+ }
+ return ObjectVersionTopology{Versions: []ObjectVersion{
+ safe.Versions[0],
+ {VersionID: "unexpected-v2", ETag: "unexpected-etag", Bytes: 1, IsLatest: false},
+ }}
+ }
+
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Empty(store.deleted)
+ require.Equal(SkipUnsafeSingleBlockVersionTopology, report.Items[0].SkipReason)
+ manifest := repo.manifests[report.Items[0].BlockMetadataID]
+ require.Equal(RetirementStateDeleting, manifest.State)
+ require.NotEmpty(manifest.SingleBlockObjectKey)
+ require.Equal(1, manifest.AttemptCount)
+ require.Equal(SkipUnsafeSingleBlockVersionTopology, manifest.Outcome)
+}
+
+func TestPlannerApply_StopsAfterFirstFailure(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ first := testRow(428058000, "hash-428058000", "single-block/428058000.zstd", "consolidated/first.cscb.zstd", now.Add(-8*24*time.Hour))
+ second := testRow(428058001, "hash-428058001", "single-block/428058001.zstd", "consolidated/second.cscb.zstd", now.Add(-8*24*time.Hour))
+ repo := &fakeRepo{rows: []MetadataRow{first, second}}
+ store := newFakeStore()
+ store.deleteMutates = true
+ for _, row := range repo.rows {
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ head := cscbObjectHead(1024, 1024)
+ store.heads[row.PrimaryObjectKey] = head
+ store.versionHeads[versionObjectKey(row.PrimaryObjectKey, head.VersionID)] = head
+ }
+ planner := testPlanner(repo, store)
+ req := testRequest(now, true)
+ req.ProductionDeleteEnabled = true
+ report, err := planner.Plan(context.Background(), req)
+ require.NoError(err)
+ require.Len(report.Items, 2)
+
+ firstSafe := store.topologies[first.SingleBlockObjectKey]
+ store.listHook = func(key string, call int) ObjectVersionTopology {
+ if key != first.SingleBlockObjectKey || call < 3 {
+ return store.topologies[key]
+ }
+ return ObjectVersionTopology{Versions: []ObjectVersion{
+ firstSafe.Versions[0],
+ {VersionID: "unexpected-v2", ETag: "unexpected-etag", Bytes: 1},
+ }}
+ }
+
+ err = planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Empty(store.deleted)
+ require.Equal(SkipUnsafeSingleBlockVersionTopology, report.Items[0].SkipReason)
+ require.Equal(SkipNotAttemptedAfterFailure, report.Items[1].SkipReason)
+ _, secondManifestExists := repo.manifests[second.BlockMetadataID]
+ require.False(secondManifestExists)
+}
+
+func TestPlannerApply_DoesNotFinalizeWhenCSCBChangesAfterSingleBlockDelete(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ store.headHook = func(key string, call int) ObjectHead {
+ head := store.heads[key]
+ if key == row.PrimaryObjectKey && call >= 4 {
+ head.VersionID = "replacement-version"
+ head.ETag = "replacement-etag"
+ }
+ return head
+ }
+
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Len(store.deleted, 1)
+ require.Equal(ActionDeletedObjectVersion, report.Items[0].Action)
+ require.Equal(1, report.Summary.DeletedRows)
+ require.Equal(SkipCSCBObjectChanged, report.Items[0].SkipReason)
+ require.Equal(RetirementStateDeletedPendingVerification, repo.manifests[row.BlockMetadataID].State)
+ require.Empty(repo.manifests[row.BlockMetadataID].SingleBlockObjectKey)
+ require.Empty(repo.rows[0].SingleBlockObjectKey)
+ require.Empty(repo.rows[0].Shadow.SingleBlockObjectKey)
+ require.NotNil(repo.rows[0].Shadow.SingleBlockObjectDeletedAt)
+ require.Equal(1, repo.recordDeletedCalls)
+ require.Zero(repo.finalizeCalls)
+}
+
+func TestPlannerApply_UsesFreshVerifierAfterRecordingDeletion(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ digest := strings.Repeat("a", sha256HexLength)
+ created := 0
+ planner.verifierFactory = func() blockPayloadVerifier {
+ created++
+ verifier := &fakePayloadVerifier{digest: digest, consolidatedDigest: digest}
+ if created == 2 {
+ verifier.consolidatedErr = errors.New("fresh ranged read failed")
+ }
+ return verifier
+ }
+
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Contains(err.Error(), "fresh ranged read failed")
+ require.Equal(2, created)
+ require.Len(store.deleted, 1)
+ require.Equal(ActionDeletedObjectVersion, report.Items[0].Action)
+ require.Equal(1, report.Summary.DeletedRows)
+ require.Equal(RetirementStateDeletedPendingVerification, repo.manifests[row.BlockMetadataID].State)
+ require.Empty(repo.rows[0].SingleBlockObjectKey)
+ require.Zero(repo.finalizeCalls)
+}
+
+func TestPlannerApply_RejectsBlockThatIsNoLongerCanonical(t *testing.T) {
+ require := require.New(t)
+ _, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ repo.rows[0].Canonical = false
+
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Equal(SkipMetadataChanged, report.Items[0].SkipReason)
+ require.Empty(repo.manifests)
+ require.Empty(store.deleted)
+}
+
+func TestPlannerReconcile_RequiresGuardedSingleBlockWritersForExecution(t *testing.T) {
+ require := require.New(t)
+ _, _, _, planner, _, req, _ := newExecutableTestFixture(t)
+ req.SingleBlockWritersGuarded = false
+
+ report, err := planner.Reconcile(context.Background(), req)
+ require.ErrorContains(err, "single-block writers")
+ require.Nil(report)
+}
+
+func TestPlannerReconcile_CompletesCrashBeforeDeletionRecord(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ repo.recordDeletedErr = errors.New("database unavailable")
+
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipPostDeleteVerificationFailed, report.Items[0].SkipReason)
+ require.Len(store.deleted, 1)
+ require.Equal(RetirementStateDeleting, repo.manifests[row.BlockMetadataID].State)
+ require.Equal(row.SingleBlockObjectKey, repo.rows[0].SingleBlockObjectKey)
+
+ repo.recordDeletedErr = nil
+ manifest := repo.manifests[row.BlockMetadataID]
+ expiredAt := time.Now().UTC().Add(-time.Second)
+ manifest.ClaimExpiresAt = &expiredAt
+ repo.manifests[row.BlockMetadataID] = manifest
+ repo.updateManifestRow(manifest)
+ repo.rows[0].Canonical = false
+ reconcileReport, err := planner.Reconcile(context.Background(), req)
+ require.NoError(err)
+ require.Len(reconcileReport.Items, 1)
+ require.Equal(ActionDeletedVerified, reconcileReport.Items[0].Action)
+ require.Len(store.deleted, 1)
+ require.Equal(RetirementStateDeletedVerified, repo.manifests[row.BlockMetadataID].State)
+ require.Empty(repo.rows[0].SingleBlockObjectKey)
+ require.Empty(repo.rows[0].Shadow.SingleBlockObjectKey)
+}
+
+func TestPlannerReconcile_CompletesCrashAfterDeletionRecord(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, verifier, req, report := newExecutableTestFixture(t)
+ repo.finalizeErr = errors.New("database unavailable")
+
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Equal(ActionDeletedObjectVersion, report.Items[0].Action)
+ require.Equal(SkipPostDeleteVerificationFailed, report.Items[0].SkipReason)
+ require.Len(store.deleted, 1)
+ require.Equal(RetirementStateDeletedPendingVerification, repo.manifests[row.BlockMetadataID].State)
+ require.Equal(SkipPostDeleteVerificationFailed, repo.manifests[row.BlockMetadataID].Outcome)
+ require.Empty(repo.rows[0].SingleBlockObjectKey)
+ require.Empty(repo.rows[0].Shadow.SingleBlockObjectKey)
+ require.NotNil(repo.rows[0].Shadow.SingleBlockObjectDeletedAt)
+ dryRunReq := req
+ dryRunReq.Execute = false
+ dryRunReq.ProductionDeleteEnabled = false
+ dryRunReport, err := planner.Reconcile(context.Background(), dryRunReq)
+ require.NoError(err)
+ require.Equal(ActionDeletedObjectVersion, dryRunReport.Items[0].Action)
+ require.Equal(SkipRetirementVerificationPending, dryRunReport.Items[0].SkipReason)
+ require.Equal(1, dryRunReport.Summary.DeletedRows)
+ require.NotNil(dryRunReport.Items[0].SingleBlockDeletedAt)
+ require.Nil(dryRunReport.Items[0].RetirementVerifiedAt)
+
+ repo.finalizeErr = nil
+ manifest := repo.manifests[row.BlockMetadataID]
+ expiredAt := time.Now().UTC().Add(-time.Second)
+ manifest.ClaimExpiresAt = &expiredAt
+ repo.manifests[row.BlockMetadataID] = manifest
+ repo.updateManifestRow(manifest)
+ repo.rows[0].Canonical = false
+ reconcileReport, err := planner.Reconcile(context.Background(), req)
+ require.NoError(err)
+ require.Len(reconcileReport.Items, 1)
+ require.Equal(ActionDeletedVerified, reconcileReport.Items[0].Action)
+ require.Equal(1, reconcileReport.Summary.DeletedRows)
+ require.Len(store.deleted, 1)
+ require.Equal(2, verifier.consolidatedCalls)
+ require.Equal(RetirementStateDeletedVerified, repo.manifests[row.BlockMetadataID].State)
+ require.Empty(repo.rows[0].SingleBlockObjectKey)
+ require.Empty(repo.rows[0].Shadow.SingleBlockObjectKey)
+ require.NotNil(repo.rows[0].Shadow.SingleBlockObjectDeletedAt)
+}
+
+func TestPlannerReconcile_DoesNotFinalizeExternallyMissingEligibleObject(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ manifest := manifestFromCandidate(report.Items[0], req.Now)
+ require.NoError(repo.PrepareRetirement(context.Background(), manifest))
+ delete(store.topologies, row.SingleBlockObjectKey)
+
+ reconcileReport, err := planner.Reconcile(context.Background(), req)
+ require.Error(err)
+ require.Len(reconcileReport.Items, 1)
+ require.Equal(ActionSkip, reconcileReport.Items[0].Action)
+ require.Equal(SkipMetadataChanged, reconcileReport.Items[0].SkipReason)
+ require.Equal(RetirementStateEligible, repo.manifests[row.BlockMetadataID].State)
+ require.Equal(row.SingleBlockObjectKey, repo.rows[0].SingleBlockObjectKey)
+ require.Zero(repo.finalizeCalls)
+}
+
+func TestPlannerReconcile_SkipsActiveRetirementClaim(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ manifest := manifestFromCandidate(report.Items[0], req.Now)
+ require.NoError(repo.PrepareRetirement(context.Background(), manifest))
+ claimedAt := time.Now().UTC()
+ require.NoError(repo.ClaimRetirement(
+ context.Background(),
+ row.BlockMetadataID,
+ "active-claim",
+ claimedAt,
+ claimedAt.Add(time.Hour),
+ ))
+
+ reconcileReport, err := planner.Reconcile(context.Background(), req)
+ require.NoError(err)
+ require.Len(reconcileReport.Items, 1)
+ require.Equal(ActionSkip, reconcileReport.Items[0].Action)
+ require.Equal(SkipRetirementClaimActive, reconcileReport.Items[0].SkipReason)
+ require.Empty(store.deleted)
+ require.Equal(1, repo.manifests[row.BlockMetadataID].AttemptCount)
+}
+
+func TestPlannerReconcile_TakesOverExpiredRetirementClaim(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ manifest := manifestFromCandidate(report.Items[0], req.Now)
+ require.NoError(repo.PrepareRetirement(context.Background(), manifest))
+ claimedAt := time.Now().UTC().Add(-2 * time.Hour)
+ require.NoError(repo.ClaimRetirement(
+ context.Background(),
+ row.BlockMetadataID,
+ "expired-claim",
+ claimedAt,
+ claimedAt.Add(time.Hour),
+ ))
+
+ reconcileReport, err := planner.Reconcile(context.Background(), req)
+ require.NoError(err)
+ require.Len(reconcileReport.Items, 1)
+ require.Equal(ActionDeletedVerified, reconcileReport.Items[0].Action)
+ require.Len(store.deleted, 1)
+ require.Equal(RetirementStateDeletedVerified, repo.manifests[row.BlockMetadataID].State)
+ require.Equal(2, repo.manifests[row.BlockMetadataID].AttemptCount)
+}
+
+func TestPlannerReconcile_ReturnsLiveCSCBWriteOncePolicyFailure(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ manifest := manifestFromCandidate(report.Items[0], req.Now)
+ require.NoError(repo.PrepareRetirement(context.Background(), manifest))
+ store.policyErrors[manifest.ConsolidatedObjectKey] = errors.New("GetBucketPolicy temporarily unavailable")
+
+ reconcileReport, err := planner.Reconcile(context.Background(), req)
+ require.Error(err)
+ require.Len(reconcileReport.Items, 1)
+ require.Equal(SkipCSCBWriteOncePolicyUnverified, reconcileReport.Items[0].SkipReason)
+ require.Empty(store.deleted)
+ require.Equal(RetirementStateEligible, repo.manifests[row.BlockMetadataID].State)
+ require.Empty(repo.manifests[row.BlockMetadataID].Outcome)
+}
+
+func TestPlannerPlan_RejectsInconsistentFinalizedRetirement(t *testing.T) {
+ require := require.New(t)
+ _, repo, _, planner, _, req, report := newExecutableTestFixture(t)
+ require.NoError(planner.Apply(context.Background(), req, report))
+
+ finalReport, err := planner.Plan(context.Background(), req)
+ require.NoError(err)
+ require.Equal(ActionAlreadyDeleted, finalReport.Items[0].Action)
+
+ repo.rows[0].PrimaryObjectKey = "single-block/regressed.gzip"
+ inconsistentReport, err := planner.Plan(context.Background(), req)
+ require.NoError(err)
+ require.Equal(ActionSkip, inconsistentReport.Items[0].Action)
+ require.Equal(SkipMetadataChanged, inconsistentReport.Items[0].SkipReason)
+}
+
+func TestPlannerPlan_UsesCSCBUncompressedLengthForPlacementBounds(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ row.PrimaryByteOffset = 900
+ row.PrimaryByteLength = 100
+ row.Shadow.ByteOffset = 900
+ row.Shadow.ByteLength = 100
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ store.heads[row.Shadow.ConsolidatedObjectKey] = cscbObjectHead(100, 1000)
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Equal(ActionReportOnly, report.Items[0].Action)
+ require.Empty(report.Items[0].SkipReason)
+}
+
+func TestPlannerPlan_InvalidCSCBUncompressedLengthFailsClosed(t *testing.T) {
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ validatedAt := now.Add(-8 * 24 * time.Hour)
+
+ for _, test := range []struct {
+ name string
+ compressedBytes uint64
+ metadata map[string]string
+ }{
+ {name: "missing", compressedBytes: 1024, metadata: nil},
+ {name: "zero compressed bytes", compressedBytes: 0, metadata: cscbObjectMetadata("100", cscbFormatMetadataValue, cscbCompressionScopeMetadataValue)},
+ {name: "wrong format", compressedBytes: 1024, metadata: cscbObjectMetadata("100", "not-cscb", cscbCompressionScopeMetadataValue)},
+ {name: "wrong compression scope", compressedBytes: 1024, metadata: cscbObjectMetadata("100", cscbFormatMetadataValue, "whole-object")},
+ {name: "malformed length", compressedBytes: 1024, metadata: cscbObjectMetadata("invalid", cscbFormatMetadataValue, cscbCompressionScopeMetadataValue)},
+ {name: "zero length", compressedBytes: 1024, metadata: cscbObjectMetadata("0", cscbFormatMetadataValue, cscbCompressionScopeMetadataValue)},
+ } {
+ t.Run(test.name, func(t *testing.T) {
+ require := require.New(t)
+ row := testRow(100, "hash-100", "single-block/100.zstd", "consolidated/canary.cscb.zstd", validatedAt)
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ store.heads[row.Shadow.ConsolidatedObjectKey] = ObjectHead{
+ Exists: true,
+ Bytes: test.compressedBytes,
+ VersionID: "cscb-v1",
+ ETag: "cscb-etag",
+ Metadata: test.metadata,
+ }
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipInvalidMetadataReference, report.Items[0].SkipReason)
+ })
+ }
+}
+
+func TestPlannerPlan_CSCBLifecycleExpirationFailsClosed(t *testing.T) {
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(428058000, "hash-428058000", "single-block/428058000.zstd", "consolidated/canary.cscb.zstd", now.Add(-8*24*time.Hour))
+ store := newFakeStore()
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ head := cscbObjectHead(1024, 1024)
+ head.Expiration = `expiry-date="Mon, 20 Jul 2026 00:00:00 GMT", rule-id="expire-current"`
+ store.heads[row.PrimaryObjectKey] = head
+
+ report, err := testPlanner(&fakeRepo{rows: []MetadataRow{row}}, store).Plan(context.Background(), testRequest(now, false))
+ require.NoError(err)
+ require.Equal(ActionSkip, report.Items[0].Action)
+ require.Equal(SkipCSCBLifecycleExpirationActive, report.Items[0].SkipReason)
+ require.Zero(store.policyCalls[row.PrimaryObjectKey])
+}
+
+func TestPlannerApply_DoesNotDeleteWhenCSCBLifecycleExpirationAppears(t *testing.T) {
+ require := require.New(t)
+ row, repo, store, planner, _, req, report := newExecutableTestFixture(t)
+ store.headHook = func(key string, call int) ObjectHead {
+ head := store.heads[key]
+ if key == row.PrimaryObjectKey && call >= 3 {
+ head.Expiration = `expiry-date="Mon, 20 Jul 2026 00:00:00 GMT", rule-id="expire-current"`
+ }
+ return head
+ }
+
+ err := planner.Apply(context.Background(), req, report)
+ require.Error(err)
+ require.Empty(store.deleted)
+ require.Equal(SkipCSCBLifecycleExpirationActive, report.Items[0].SkipReason)
+ require.Equal(RetirementStateDeleting, repo.manifests[row.BlockMetadataID].State)
+}
+
+func newExecutableTestFixture(t *testing.T) (
+ MetadataRow,
+ *fakeRepo,
+ *fakeStore,
+ *Planner,
+ *fakePayloadVerifier,
+ PlanRequest,
+ *Report,
+) {
+ t.Helper()
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(
+ 428058000,
+ "hash-428058000",
+ "single-block/428058000.zstd",
+ "consolidated/canary.cscb.zstd",
+ now.Add(-8*24*time.Hour),
+ )
+ repo := &fakeRepo{rows: []MetadataRow{row}}
+ store := newFakeStore()
+ store.deleteMutates = true
+ store.topologies[row.SingleBlockObjectKey] = safeTopology("single-block-v1", "single-block-etag", 42)
+ cscbHead := cscbObjectHead(1024, 1024)
+ store.heads[row.PrimaryObjectKey] = cscbHead
+ store.versionHeads[versionObjectKey(row.PrimaryObjectKey, cscbHead.VersionID)] = cscbHead
+ planner, verifier := newTestPlanner(repo, store)
+ req := testRequest(now, true)
+ req.ProductionDeleteEnabled = true
+ report, err := planner.Plan(context.Background(), req)
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionDeleteObjectVersion, report.Items[0].Action)
+ return row, repo, store, planner, verifier, req, report
+}
+
+func newMultiVersionExecutableTestFixture(t *testing.T) (
+ MetadataRow,
+ *fakeRepo,
+ *fakeStore,
+ *Planner,
+ *fakePayloadVerifier,
+ PlanRequest,
+ *Report,
+) {
+ t.Helper()
+ require := require.New(t)
+ now := time.Date(2026, 6, 21, 12, 0, 0, 0, time.UTC)
+ row := testRow(
+ 428058000,
+ "hash-428058000",
+ "single-block/428058000.zstd",
+ "consolidated/canary.cscb.zstd",
+ now.Add(-8*24*time.Hour),
+ )
+ repo := &fakeRepo{rows: []MetadataRow{row}}
+ store := newFakeStore()
+ store.deleteMutates = true
+ store.topologies[row.SingleBlockObjectKey] = ObjectVersionTopology{
+ Versions: []ObjectVersion{
+ {VersionID: "single-block-v2", ETag: "etag-v2", Bytes: 42, IsLatest: true},
+ {VersionID: "single-block-v1", ETag: "etag-v1", Bytes: 41},
+ },
+ DeleteMarkers: []ObjectDeleteMarker{{VersionID: "delete-marker-v1"}},
+ }
+ cscbHead := cscbObjectHead(1024, 1024)
+ store.heads[row.PrimaryObjectKey] = cscbHead
+ store.versionHeads[versionObjectKey(row.PrimaryObjectKey, cscbHead.VersionID)] = cscbHead
+ planner, verifier := newTestPlanner(repo, store)
+ req := testRequest(now, true)
+ req.ProductionDeleteEnabled = true
+ report, err := planner.Plan(context.Background(), req)
+ require.NoError(err)
+ require.Len(report.Items, 1)
+ require.Equal(ActionDeleteObjectVersion, report.Items[0].Action)
+ return row, repo, store, planner, verifier, req, report
+}
+
+func testRequest(now time.Time, execute bool) PlanRequest {
+ return PlanRequest{
+ Environment: "production",
+ Blockchain: "solana",
+ Network: "mainnet",
+ Bucket: "co-chainstorage-solana-mainnet-prod",
+ Tag: 0,
+ StartHeight: 428058000,
+ EndHeight: 428059000,
+ Now: now,
+ Execute: execute,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ Approval: Approval{
+ Chain: "solana-mainnet",
+ StartHeight: 428058000,
+ EndHeight: 428059000,
+ },
+ }
+}
+
+func testRow(height uint64, hash string, singleBlockKey string, cscbKey string, validatedAt time.Time) MetadataRow {
+ retiredAt := validatedAt
+ retireAfter := validatedAt.Add(72 * time.Hour)
+ return MetadataRow{
+ BlockMetadataID: int64(height),
+ Canonical: true,
+ Tag: 0,
+ Height: height,
+ Hash: hash,
+ PrimaryObjectKey: cscbKey,
+ SingleBlockObjectKey: singleBlockKey,
+ PrimaryObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ PrimaryByteOffset: 100,
+ PrimaryByteLength: 200,
+ PrimaryUncompressedLength: 300,
+ Shadow: &ConsolidationShadow{
+ Tag: 0,
+ Height: height,
+ Hash: hash,
+ SingleBlockObjectKey: singleBlockKey,
+ ConsolidatedObjectKey: cscbKey,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 100,
+ ByteLength: 200,
+ UncompressedLength: 300,
+ ValidatedAt: &validatedAt,
+ SingleBlockRetentionStartedAt: &retiredAt,
+ SingleBlockDeleteAfter: &retireAfter,
+ FormatVersion: 1,
+ },
+ }
+}
+
+func cscbObjectHead(compressedBytes uint64, uncompressedBytes uint64) ObjectHead {
+ return ObjectHead{
+ Exists: true,
+ Bytes: compressedBytes,
+ VersionID: "cscb-v1",
+ ETag: "cscb-etag",
+ Metadata: cscbObjectMetadata(
+ fmt.Sprintf("%d", uncompressedBytes),
+ cscbFormatMetadataValue,
+ cscbCompressionScopeMetadataValue,
+ ),
+ }
+}
+
+func cscbObjectMetadata(uncompressedLength string, format string, compressionScope string) map[string]string {
+ return map[string]string{
+ cscbFormatMetadataKey: format,
+ cscbCompressionScopeMetadataKey: compressionScope,
+ cscbUncompressedLengthMetadataKey: uncompressedLength,
+ }
+}
+
+func activeSingleBlockTestRow(height uint64, hash string, singleBlockKey string, cscbKey string, validatedAt time.Time) MetadataRow {
+ row := testRow(height, hash, singleBlockKey, cscbKey, validatedAt)
+ row.PrimaryObjectKey = singleBlockKey
+ row.PrimaryObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK
+ row.PrimaryByteOffset = 0
+ row.PrimaryByteLength = 0
+ row.PrimaryUncompressedLength = 0
+ return row
+}
diff --git a/internal/storage/retirement/postgres_repository.go b/internal/storage/retirement/postgres_repository.go
new file mode 100644
index 00000000..43403022
--- /dev/null
+++ b/internal/storage/retirement/postgres_repository.go
@@ -0,0 +1,1256 @@
+package retirement
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "time"
+
+ "github.com/lib/pq"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/storage/retirementlock"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+const metadataRowColumns = `
+ bm.id,
+ (cb.block_metadata_id IS NOT NULL) AS is_canonical,
+ bm.tag,
+ bm.height,
+ COALESCE(bm.hash, ''),
+ bm.skipped,
+ COALESCE(bm.object_key_main, ''),
+ bm.object_format,
+ bm.byte_offset,
+ bm.byte_length,
+ bm.uncompressed_length,
+ bm.single_block_retention_fenced_at,
+ shadow.block_metadata_id,
+ shadow.tag,
+ shadow.height,
+ shadow.hash,
+ shadow.single_block_object_key_main,
+ shadow.consolidated_object_key_main,
+ shadow.object_format,
+ shadow.byte_offset,
+ shadow.byte_length,
+ shadow.uncompressed_length,
+ shadow.validated_at,
+ shadow.single_block_retention_started_at,
+ shadow.single_block_delete_after,
+ shadow.single_block_object_deleted_at,
+ shadow.format_version,
+ retirement.block_metadata_id,
+ retirement.state,
+ retirement.bucket,
+ retirement.single_block_object_key_main,
+ retirement.single_block_object_key_sha256,
+ retirement.single_block_object_version_ids,
+ retirement.single_block_delete_marker_version_ids,
+ retirement.single_block_object_etag,
+ retirement.single_block_object_bytes,
+ retirement.consolidated_object_key_main,
+ retirement.consolidated_object_version_id,
+ retirement.consolidated_object_etag,
+ retirement.consolidated_byte_offset,
+ retirement.consolidated_byte_length,
+ retirement.consolidated_uncompressed_length,
+ retirement.payload_sha256,
+ retirement.outcome,
+ retirement.attempt_count,
+ retirement.claim_token,
+ retirement.claim_expires_at,
+ retirement.prepared_at,
+ retirement.delete_started_at,
+ retirement.last_attempt_at,
+ retirement.deleted_at,
+ retirement.verified_at`
+
+const metadataRowFrom = `
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON bm.id = cb.block_metadata_id AND bm.tag = cb.tag AND bm.height = cb.height
+ LEFT JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ LEFT JOIN block_single_block_retention retirement ON retirement.block_metadata_id = bm.id`
+
+const metadataRowByIDFrom = `
+ FROM block_metadata bm
+ LEFT JOIN canonical_blocks cb ON cb.block_metadata_id = bm.id AND cb.tag = bm.tag AND cb.height = bm.height
+ LEFT JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ LEFT JOIN block_single_block_retention retirement ON retirement.block_metadata_id = bm.id`
+
+type PostgresRepository struct {
+ db *sql.DB
+}
+
+func NewPostgresRepository(db *sql.DB) *PostgresRepository {
+ return &PostgresRepository{db: db}
+}
+
+func (r *PostgresRepository) ListMetadataRows(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, limit uint64) ([]MetadataRow, error) {
+ if r.db == nil {
+ return nil, xerrors.New("postgres db is required")
+ }
+ if endHeight <= startHeight {
+ return nil, nil
+ }
+ if limit == 0 {
+ limit = endHeight - startHeight
+ }
+
+ query := fmt.Sprintf(`
+ SELECT %s
+ %s
+ WHERE cb.tag = $1
+ AND cb.height >= $2
+ AND cb.height < $3
+ ORDER BY cb.height ASC
+ LIMIT $4`, metadataRowColumns, metadataRowFrom)
+ rows, err := r.db.QueryContext(ctx, query, tag, startHeight, endHeight, limit)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to query retirement metadata rows: %w", err)
+ }
+ defer func() { _ = rows.Close() }()
+
+ result := make([]MetadataRow, 0)
+ for rows.Next() {
+ row, err := scanMetadataRow(rows)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to scan retirement metadata row: %w", err)
+ }
+ result = append(result, row)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, xerrors.Errorf("failed to iterate retirement metadata rows: %w", err)
+ }
+ return result, nil
+}
+
+func (r *PostgresRepository) GetMetadataRow(ctx context.Context, blockMetadataID int64) (MetadataRow, error) {
+ if r.db == nil {
+ return MetadataRow{}, xerrors.New("postgres db is required")
+ }
+ query := fmt.Sprintf(`
+ SELECT %s
+ %s
+ WHERE bm.id = $1`, metadataRowColumns, metadataRowByIDFrom)
+ row, err := scanMetadataRow(r.db.QueryRowContext(ctx, query, blockMetadataID))
+ if err != nil {
+ if err == sql.ErrNoRows {
+ return MetadataRow{}, xerrors.Errorf("retirement metadata row not found: block_metadata_id=%d", blockMetadataID)
+ }
+ return MetadataRow{}, xerrors.Errorf("failed to get retirement metadata row: %w", err)
+ }
+ return row, nil
+}
+
+func (r *PostgresRepository) PrepareRetirement(ctx context.Context, manifest RetirementManifest) error {
+ if r.db == nil {
+ return xerrors.New("postgres db is required")
+ }
+ if manifest.State != RetirementStateEligible {
+ return xerrors.Errorf("prepared retirement must be eligible, got %q", manifest.State)
+ }
+ if err := validatePreparedManifest(manifest); err != nil {
+ return err
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return xerrors.Errorf("failed to begin retirement preparation: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+ if err := retirementlock.Acquire(ctx, tx, manifest.Tag, manifest.Height, manifest.Hash); err != nil {
+ return err
+ }
+
+ const lockMetadata = `
+ SELECT bm.id, CURRENT_TIMESTAMP
+ FROM canonical_blocks cb
+ JOIN block_metadata bm ON bm.id = cb.block_metadata_id AND bm.tag = cb.tag AND bm.height = cb.height
+ JOIN block_consolidation_shadow shadow ON shadow.block_metadata_id = bm.id
+ WHERE bm.id = $1
+ AND shadow.validated_at IS NOT NULL
+ AND shadow.single_block_retention_started_at IS NOT NULL
+ AND shadow.single_block_delete_after IS NOT NULL
+ AND shadow.single_block_delete_after <= CURRENT_TIMESTAMP
+ AND shadow.single_block_object_deleted_at IS NULL
+ AND NOT EXISTS (
+ SELECT 1
+ FROM cscb_repair_block repair_block
+ JOIN cscb_repair_manifest repair ON repair.id = repair_block.repair_id
+ WHERE repair_block.block_metadata_id = bm.id
+ AND repair.state <> 'completed'
+ )
+ FOR UPDATE OF cb, bm, shadow`
+ var lockedID int64
+ var databasePreparedAt time.Time
+ if err := tx.QueryRowContext(ctx, lockMetadata, manifest.BlockMetadataID).Scan(&lockedID, &databasePreparedAt); err != nil {
+ return xerrors.Errorf("failed to lock canonical retirement metadata: %w", err)
+ }
+ query := fmt.Sprintf(`
+ SELECT %s
+ %s
+ WHERE bm.id = $1`, metadataRowColumns, metadataRowFrom)
+ row, err := scanMetadataRow(tx.QueryRowContext(ctx, query, lockedID))
+ if err != nil {
+ return xerrors.Errorf("failed to lock retirement metadata row: %w", err)
+ }
+ if err := validateManifestMetadata(row, manifest); err != nil {
+ return err
+ }
+ const fenceMetadata = `
+ UPDATE block_metadata
+ SET single_block_retention_fenced_at = COALESCE(single_block_retention_fenced_at, $2)
+ WHERE id = $1
+ AND object_key_main = $3
+ AND object_format = $4
+ AND byte_offset = $5
+ AND byte_length = $6
+ AND uncompressed_length = $7`
+ result, err := tx.ExecContext(
+ ctx,
+ fenceMetadata,
+ manifest.BlockMetadataID,
+ databasePreparedAt,
+ manifest.ConsolidatedObjectKey,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ manifest.ConsolidatedByteOffset,
+ manifest.ConsolidatedByteLength,
+ manifest.ConsolidatedUncompressedLength,
+ )
+ if err != nil {
+ return xerrors.Errorf("failed to fence retired block metadata: %w", err)
+ }
+ if rows, err := result.RowsAffected(); err != nil || rows != 1 {
+ return xerrors.Errorf("retired block metadata fence guard failed: block_metadata_id=%d rows=%d err=%v", manifest.BlockMetadataID, rows, err)
+ }
+
+ const insert = `
+ INSERT INTO block_single_block_retention (
+ block_metadata_id, tag, height, hash, state, bucket,
+ single_block_object_key_main, single_block_object_key_sha256, single_block_object_version_ids,
+ single_block_delete_marker_version_ids,
+ single_block_object_etag, single_block_object_bytes,
+ consolidated_object_key_main, consolidated_object_version_id, consolidated_object_etag,
+ consolidated_byte_offset, consolidated_byte_length, consolidated_uncompressed_length,
+ payload_sha256, outcome, prepared_at, updated_at
+ )
+ VALUES ($1, $2, $3, NULLIF($4, ''), $5, $6, $7, $8, $9, $10, $11,
+ $12, $13, $14, $15, $16, $17, $18, $19, $20, $21, $21)
+ ON CONFLICT (block_metadata_id) DO NOTHING`
+ deleteMarkerVersionIDs := manifest.SingleBlockDeleteMarkerVersionIDs
+ if deleteMarkerVersionIDs == nil {
+ deleteMarkerVersionIDs = []string{}
+ }
+ if _, err := tx.ExecContext(ctx, insert,
+ manifest.BlockMetadataID,
+ manifest.Tag,
+ manifest.Height,
+ manifest.Hash,
+ manifest.State,
+ manifest.Bucket,
+ manifest.SingleBlockObjectKey,
+ manifest.SingleBlockObjectKeySHA256,
+ pq.Array(manifest.SingleBlockObjectVersionIDs),
+ pq.Array(deleteMarkerVersionIDs),
+ manifest.SingleBlockObjectETag,
+ manifest.SingleBlockObjectBytes,
+ manifest.ConsolidatedObjectKey,
+ manifest.ConsolidatedObjectVersionID,
+ manifest.ConsolidatedObjectETag,
+ manifest.ConsolidatedByteOffset,
+ manifest.ConsolidatedByteLength,
+ manifest.ConsolidatedUncompressedLength,
+ manifest.PayloadSHA256,
+ manifest.Outcome,
+ databasePreparedAt,
+ ); err != nil {
+ return xerrors.Errorf("failed to persist retirement manifest: %w", err)
+ }
+
+ existing, err := getManifestForUpdate(ctx, tx, manifest.BlockMetadataID)
+ if err != nil {
+ return err
+ }
+ if !samePreparedManifest(existing, manifest) {
+ return xerrors.Errorf("retirement manifest conflict: block_metadata_id=%d state=%s", manifest.BlockMetadataID, existing.State)
+ }
+ if err := tx.Commit(); err != nil {
+ return xerrors.Errorf("failed to commit retirement manifest: %w", err)
+ }
+ return nil
+}
+
+func (r *PostgresRepository) ObserveRetentionSafety(
+ ctx context.Context,
+ bucket string,
+ consolidatedObjectKey string,
+ configurationSHA256 string,
+) (time.Time, time.Time, error) {
+ if r.db == nil {
+ return time.Time{}, time.Time{}, xerrors.New("postgres db is required")
+ }
+ if bucket == "" || consolidatedObjectKey == "" || !isSHA256Hex(configurationSHA256) {
+ return time.Time{}, time.Time{}, xerrors.New("valid retirement safety observation is required")
+ }
+ const query = `
+ INSERT INTO cscb_retirement_safety_observation (
+ bucket, consolidated_object_key_main, configuration_sha256,
+ first_observed_at, last_observed_at
+ )
+ SELECT $1, $2, $3, database_now, database_now
+ FROM (SELECT clock_timestamp() AS database_now) clock
+ ON CONFLICT (bucket, consolidated_object_key_main) DO UPDATE
+ SET configuration_sha256 = EXCLUDED.configuration_sha256,
+ first_observed_at = CASE
+ WHEN cscb_retirement_safety_observation.configuration_sha256 = EXCLUDED.configuration_sha256
+ THEN cscb_retirement_safety_observation.first_observed_at
+ ELSE EXCLUDED.first_observed_at
+ END,
+ last_observed_at = EXCLUDED.last_observed_at
+ RETURNING first_observed_at, last_observed_at`
+ var firstObservedAt time.Time
+ var observedAt time.Time
+ if err := r.db.QueryRowContext(
+ ctx,
+ query,
+ bucket,
+ consolidatedObjectKey,
+ configurationSHA256,
+ ).Scan(&firstObservedAt, &observedAt); err != nil {
+ return time.Time{}, time.Time{}, xerrors.Errorf("failed to persist CSCB retirement safety observation: %w", err)
+ }
+ return firstObservedAt, observedAt, nil
+}
+
+func (r *PostgresRepository) ClaimRetirement(
+ ctx context.Context,
+ blockMetadataID int64,
+ claimToken string,
+ claimedAt time.Time,
+ claimExpiresAt time.Time,
+) error {
+ if r.db == nil {
+ return xerrors.New("postgres db is required")
+ }
+ if claimToken == "" || claimedAt.IsZero() || !claimExpiresAt.After(claimedAt) {
+ return xerrors.New("valid retirement claim token and lease are required")
+ }
+ leaseMicros := claimExpiresAt.Sub(claimedAt).Microseconds()
+ if leaseMicros <= 0 {
+ return xerrors.New("retirement claim lease must be at least one microsecond")
+ }
+ const query = `
+ UPDATE block_single_block_retention
+ SET state = CASE WHEN state = $2 THEN $3 ELSE state END,
+ claim_token = $4,
+ claim_expires_at = CURRENT_TIMESTAMP + ($5 * INTERVAL '1 microsecond'),
+ delete_started_at = CASE WHEN state = $2 THEN COALESCE(delete_started_at, CURRENT_TIMESTAMP) ELSE delete_started_at END,
+ last_attempt_at = CURRENT_TIMESTAMP,
+ attempt_count = attempt_count + 1,
+ outcome = CASE WHEN state = $6 THEN 'verification_started' ELSE 'delete_started' END,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE block_metadata_id = $1
+ AND (
+ (state = $2 AND claim_token IS NULL AND claim_expires_at IS NULL)
+ OR
+ (state IN ($3, $6) AND claim_expires_at <= CURRENT_TIMESTAMP)
+ )`
+ result, err := r.db.ExecContext(
+ ctx,
+ query,
+ blockMetadataID,
+ RetirementStateEligible,
+ RetirementStateDeleting,
+ claimToken,
+ leaseMicros,
+ RetirementStateDeletedPendingVerification,
+ )
+ if err != nil {
+ return xerrors.Errorf("failed to claim retirement: %w", err)
+ }
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return xerrors.Errorf("failed to inspect retirement claim transition: %w", err)
+ }
+ if rows == 1 {
+ return nil
+ }
+ return xerrors.Errorf("%w: block_metadata_id=%d", ErrRetirementClaimUnavailable, blockMetadataID)
+}
+
+func (r *PostgresRepository) RenewRetirementClaim(
+ ctx context.Context,
+ blockMetadataID int64,
+ claimToken string,
+ renewedAt time.Time,
+ claimExpiresAt time.Time,
+) error {
+ if r.db == nil {
+ return xerrors.New("postgres db is required")
+ }
+ if claimToken == "" || renewedAt.IsZero() || !claimExpiresAt.After(renewedAt) {
+ return xerrors.New("valid retirement claim token and lease are required")
+ }
+ leaseMicros := claimExpiresAt.Sub(renewedAt).Microseconds()
+ if leaseMicros <= 0 {
+ return xerrors.New("retirement claim lease must be at least one microsecond")
+ }
+ const query = `
+ UPDATE block_single_block_retention
+ SET claim_expires_at = CURRENT_TIMESTAMP + ($3 * INTERVAL '1 microsecond'),
+ last_attempt_at = CURRENT_TIMESTAMP,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE block_metadata_id = $1
+ AND state IN ($4, $5)
+ AND claim_token = $2
+ AND claim_expires_at > CURRENT_TIMESTAMP`
+ result, err := r.db.ExecContext(
+ ctx,
+ query,
+ blockMetadataID,
+ claimToken,
+ leaseMicros,
+ RetirementStateDeleting,
+ RetirementStateDeletedPendingVerification,
+ )
+ if err != nil {
+ return xerrors.Errorf("failed to renew retirement claim: %w", err)
+ }
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return xerrors.Errorf("failed to inspect retirement claim renewal: %w", err)
+ }
+ if rows == 1 {
+ return nil
+ }
+ return xerrors.Errorf("%w: block_metadata_id=%d", ErrRetirementClaimUnavailable, blockMetadataID)
+}
+
+func (r *PostgresRepository) RecordRetirementOutcome(
+ ctx context.Context,
+ blockMetadataID int64,
+ claimToken string,
+ outcome string,
+ attemptedAt time.Time,
+) error {
+ if r.db == nil {
+ return xerrors.New("postgres db is required")
+ }
+ if claimToken == "" || outcome == "" {
+ return xerrors.New("retirement claim token and outcome are required")
+ }
+ const query = `
+ UPDATE block_single_block_retention
+ SET outcome = $3,
+ last_attempt_at = CURRENT_TIMESTAMP,
+ updated_at = CURRENT_TIMESTAMP
+ WHERE block_metadata_id = $1
+ AND state IN ($4, $5)
+ AND claim_token = $2`
+ result, err := r.db.ExecContext(
+ ctx,
+ query,
+ blockMetadataID,
+ claimToken,
+ outcome,
+ RetirementStateDeleting,
+ RetirementStateDeletedPendingVerification,
+ )
+ if err != nil {
+ return xerrors.Errorf("failed to record retirement outcome: %w", err)
+ }
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return xerrors.Errorf("failed to inspect retirement outcome update: %w", err)
+ }
+ if rows == 1 {
+ return nil
+ }
+ return xerrors.Errorf("%w: block_metadata_id=%d", ErrRetirementClaimUnavailable, blockMetadataID)
+}
+
+func (r *PostgresRepository) RecordRetirementObjectDeleted(
+ ctx context.Context,
+ blockMetadataID int64,
+ claimToken string,
+ outcome string,
+) (time.Time, error) {
+ if r.db == nil {
+ return time.Time{}, xerrors.New("postgres db is required")
+ }
+ if claimToken == "" || outcome == "" {
+ return time.Time{}, xerrors.New("claim token and outcome are required to record object deletion")
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return time.Time{}, xerrors.Errorf("failed to begin recording retirement object deletion: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ metadata, err := lockFinalizationMetadata(ctx, tx, blockMetadataID)
+ if err != nil {
+ return time.Time{}, err
+ }
+ manifest, err := getManifestForUpdate(ctx, tx, blockMetadataID)
+ if err != nil {
+ return time.Time{}, err
+ }
+ if !finalizationMetadataMatchesManifest(metadata, manifest) {
+ return time.Time{}, xerrors.Errorf("active or shadow CSCB metadata changed before retirement finalization: block_metadata_id=%d", blockMetadataID)
+ }
+ if manifest.State == RetirementStateDeletedPendingVerification {
+ if manifest.SingleBlockObjectKey != "" || manifest.SingleBlockObjectETag != "" || manifest.Outcome == "" ||
+ manifest.DeleteStartedAt == nil || manifest.LastAttemptAt == nil || manifest.DeletedAt == nil ||
+ manifest.VerifiedAt != nil || manifest.ClaimToken != claimToken || manifest.ClaimExpiresAt == nil ||
+ metadata.shadowSingleBlockKey.Valid || !metadata.shadowDeletedAt.Valid ||
+ !metadata.shadowDeletedAt.Time.Equal(*manifest.DeletedAt) {
+ return time.Time{}, xerrors.Errorf("pending-verification retirement metadata is inconsistent: block_metadata_id=%d", blockMetadataID)
+ }
+ if err := clearCompletedRepairSingleBlockPath(ctx, tx, blockMetadataID, ""); err != nil {
+ return time.Time{}, err
+ }
+ if err := tx.Commit(); err != nil {
+ return time.Time{}, xerrors.Errorf("failed to commit idempotent retirement object deletion: %w", err)
+ }
+ return *manifest.DeletedAt, nil
+ }
+ if manifest.State != RetirementStateDeleting {
+ return time.Time{}, xerrors.Errorf("retirement must be deleting before recording object deletion: block_metadata_id=%d state=%s", blockMetadataID, manifest.State)
+ }
+ if manifest.ClaimToken != claimToken || manifest.ClaimExpiresAt == nil {
+ return time.Time{}, xerrors.Errorf("%w: block_metadata_id=%d", ErrRetirementClaimUnavailable, blockMetadataID)
+ }
+ if !metadata.shadowSingleBlockKey.Valid || metadata.shadowSingleBlockKey.String != manifest.SingleBlockObjectKey || metadata.shadowDeletedAt.Valid {
+ return time.Time{}, xerrors.Errorf("single-block shadow metadata changed before retirement finalization: block_metadata_id=%d", blockMetadataID)
+ }
+ var databaseDeletedAt time.Time
+ if err := tx.QueryRowContext(ctx, `
+ SELECT database_now
+ FROM (SELECT clock_timestamp() AS database_now) clock
+ WHERE database_now < $1`, *manifest.ClaimExpiresAt).Scan(&databaseDeletedAt); err != nil {
+ if err == sql.ErrNoRows {
+ return time.Time{}, xerrors.Errorf("%w: block_metadata_id=%d", ErrRetirementClaimUnavailable, blockMetadataID)
+ }
+ return time.Time{}, xerrors.Errorf("failed to read database deletion timestamp: %w", err)
+ }
+
+ const clearShadow = `
+ UPDATE block_consolidation_shadow
+ SET single_block_object_key_main = NULL,
+ single_block_object_deleted_at = $3
+ WHERE block_metadata_id = $1
+ AND single_block_object_key_main = $2
+ AND single_block_object_deleted_at IS NULL`
+ result, err := tx.ExecContext(ctx, clearShadow, blockMetadataID, manifest.SingleBlockObjectKey, databaseDeletedAt)
+ if err != nil {
+ return time.Time{}, xerrors.Errorf("failed to clear retired shadow path: %w", err)
+ }
+ if rows, err := result.RowsAffected(); err != nil || rows != 1 {
+ return time.Time{}, xerrors.Errorf("retired shadow path clear guard failed: block_metadata_id=%d rows=%d err=%v", blockMetadataID, rows, err)
+ }
+
+ const recordDeleted = `
+ UPDATE block_single_block_retention
+ SET state = $2,
+ single_block_object_key_main = NULL,
+ single_block_object_etag = '',
+ outcome = $3,
+ deleted_at = $4,
+ last_attempt_at = $4,
+ updated_at = $4
+ WHERE block_metadata_id = $1
+ AND state = $5
+ AND claim_token = $6
+ AND claim_expires_at > $7`
+ result, err = tx.ExecContext(
+ ctx,
+ recordDeleted,
+ blockMetadataID,
+ RetirementStateDeletedPendingVerification,
+ outcome,
+ databaseDeletedAt,
+ RetirementStateDeleting,
+ claimToken,
+ databaseDeletedAt,
+ )
+ if err != nil {
+ return time.Time{}, xerrors.Errorf("failed to record retirement object deletion: %w", err)
+ }
+ if rows, err := result.RowsAffected(); err != nil || rows != 1 {
+ return time.Time{}, xerrors.Errorf("%w: block_metadata_id=%d rows=%d err=%v", ErrRetirementClaimUnavailable, blockMetadataID, rows, err)
+ }
+ if err := clearCompletedRepairSingleBlockPath(ctx, tx, blockMetadataID, manifest.SingleBlockObjectKey); err != nil {
+ return time.Time{}, err
+ }
+ if err := tx.Commit(); err != nil {
+ return time.Time{}, xerrors.Errorf("failed to commit retirement object deletion: %w", err)
+ }
+ return databaseDeletedAt, nil
+}
+
+func clearCompletedRepairSingleBlockPath(
+ ctx context.Context,
+ tx *sql.Tx,
+ blockMetadataID int64,
+ expectedObjectKey string,
+) error {
+ result, err := tx.ExecContext(ctx, `
+ UPDATE cscb_repair_block
+ SET single_block_object_key_main = NULL
+ WHERE block_metadata_id = $1
+ AND single_block_object_key_main IS NOT NULL
+ AND ($2 = '' OR single_block_object_key_main = $2)`, blockMetadataID, expectedObjectKey)
+ if err != nil {
+ return xerrors.Errorf("failed to clear retired CSCB repair single-block path: %w", err)
+ }
+ rows, err := result.RowsAffected()
+ if err != nil {
+ return xerrors.Errorf("failed to inspect retired CSCB repair path clear: %w", err)
+ }
+ if rows > 1 {
+ return xerrors.Errorf("retired CSCB repair path clear guard failed: block_metadata_id=%d rows=%d", blockMetadataID, rows)
+ }
+ return nil
+}
+
+func (r *PostgresRepository) FinalizeRetirement(
+ ctx context.Context,
+ blockMetadataID int64,
+ claimToken string,
+ outcome string,
+) (time.Time, error) {
+ if r.db == nil {
+ return time.Time{}, xerrors.New("postgres db is required")
+ }
+ if claimToken == "" || outcome == "" {
+ return time.Time{}, xerrors.New("claim token and outcome are required for retirement finalization")
+ }
+ tx, err := r.db.BeginTx(ctx, nil)
+ if err != nil {
+ return time.Time{}, xerrors.Errorf("failed to begin retirement finalization: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ metadata, err := lockFinalizationMetadata(ctx, tx, blockMetadataID)
+ if err != nil {
+ return time.Time{}, err
+ }
+ manifest, err := getManifestForUpdate(ctx, tx, blockMetadataID)
+ if err != nil {
+ return time.Time{}, err
+ }
+ if !finalizationMetadataMatchesManifest(metadata, manifest) {
+ return time.Time{}, xerrors.Errorf("active or shadow CSCB metadata changed before retirement finalization: block_metadata_id=%d", blockMetadataID)
+ }
+ if manifest.State == RetirementStateDeletedVerified {
+ if !finalizedMetadataIsConsistent(metadata, manifest) {
+ return time.Time{}, xerrors.Errorf("finalized retirement metadata is inconsistent: block_metadata_id=%d", blockMetadataID)
+ }
+ if err := tx.Commit(); err != nil {
+ return time.Time{}, xerrors.Errorf("failed to commit idempotent retirement finalization: %w", err)
+ }
+ return *manifest.VerifiedAt, nil
+ }
+ if manifest.State != RetirementStateDeletedPendingVerification {
+ return time.Time{}, xerrors.Errorf("retirement must be pending verification before finalization: block_metadata_id=%d state=%s", blockMetadataID, manifest.State)
+ }
+ if manifest.ClaimToken != claimToken || manifest.ClaimExpiresAt == nil {
+ return time.Time{}, xerrors.Errorf("%w: block_metadata_id=%d", ErrRetirementClaimUnavailable, blockMetadataID)
+ }
+ if manifest.SingleBlockObjectKey != "" || manifest.SingleBlockObjectETag != "" || manifest.DeletedAt == nil || manifest.VerifiedAt != nil ||
+ metadata.shadowSingleBlockKey.Valid || !metadata.shadowDeletedAt.Valid ||
+ !metadata.shadowDeletedAt.Time.Equal(*manifest.DeletedAt) {
+ return time.Time{}, xerrors.Errorf("pending-verification retirement metadata is inconsistent: block_metadata_id=%d", blockMetadataID)
+ }
+ var databaseVerifiedAt time.Time
+ if err := tx.QueryRowContext(ctx, `
+ SELECT database_now
+ FROM (SELECT clock_timestamp() AS database_now) clock
+ WHERE database_now < $1`, *manifest.ClaimExpiresAt).Scan(&databaseVerifiedAt); err != nil {
+ if err == sql.ErrNoRows {
+ return time.Time{}, xerrors.Errorf("%w: block_metadata_id=%d", ErrRetirementClaimUnavailable, blockMetadataID)
+ }
+ return time.Time{}, xerrors.Errorf("failed to read database verification timestamp: %w", err)
+ }
+
+ const finalize = `
+ UPDATE block_single_block_retention
+ SET state = $2,
+ claim_token = NULL,
+ claim_expires_at = NULL,
+ outcome = $3,
+ verified_at = $4,
+ last_attempt_at = $4,
+ updated_at = $4
+ WHERE block_metadata_id = $1
+ AND state = $5
+ AND claim_token = $6
+ AND claim_expires_at > $7`
+ result, err := tx.ExecContext(
+ ctx,
+ finalize,
+ blockMetadataID,
+ RetirementStateDeletedVerified,
+ outcome,
+ databaseVerifiedAt,
+ RetirementStateDeletedPendingVerification,
+ claimToken,
+ databaseVerifiedAt,
+ )
+ if err != nil {
+ return time.Time{}, xerrors.Errorf("failed to finalize retirement manifest: %w", err)
+ }
+ if rows, err := result.RowsAffected(); err != nil || rows != 1 {
+ return time.Time{}, xerrors.Errorf("%w: block_metadata_id=%d rows=%d err=%v", ErrRetirementClaimUnavailable, blockMetadataID, rows, err)
+ }
+ if err := tx.Commit(); err != nil {
+ return time.Time{}, xerrors.Errorf("failed to commit retirement finalization: %w", err)
+ }
+ return databaseVerifiedAt, nil
+}
+
+func finalizedMetadataIsConsistent(metadata finalizationMetadata, manifest RetirementManifest) bool {
+ return manifest.SingleBlockObjectKey == "" &&
+ manifest.SingleBlockObjectETag == "" &&
+ manifest.Outcome != "" &&
+ manifest.DeleteStartedAt != nil &&
+ manifest.LastAttemptAt != nil &&
+ manifest.DeletedAt != nil &&
+ manifest.VerifiedAt != nil &&
+ manifest.ClaimToken == "" &&
+ manifest.ClaimExpiresAt == nil &&
+ !metadata.shadowSingleBlockKey.Valid &&
+ metadata.shadowDeletedAt.Valid &&
+ metadata.shadowDeletedAt.Time.Equal(*manifest.DeletedAt)
+}
+
+type finalizationMetadata struct {
+ shadowTag int64
+ shadowHeight int64
+ shadowHash sql.NullString
+ shadowSingleBlockKey sql.NullString
+ shadowDeletedAt sql.NullTime
+ shadowConsolidatedKey string
+ shadowObjectFormat int64
+ shadowByteOffset int64
+ shadowByteLength int64
+ shadowUncompressedLength int64
+ shadowValidatedAt sql.NullTime
+ primaryTag int64
+ primaryHeight int64
+ primaryHash sql.NullString
+ primaryObjectKey sql.NullString
+ primaryObjectFormat int64
+ primaryByteOffset sql.NullInt64
+ primaryByteLength sql.NullInt64
+ primaryUncompressed sql.NullInt64
+ primarySkipped bool
+ primaryRetirementFenced sql.NullTime
+}
+
+func lockFinalizationMetadata(ctx context.Context, tx *sql.Tx, blockMetadataID int64) (finalizationMetadata, error) {
+ const query = `
+ SELECT
+ shadow.tag,
+ shadow.height,
+ shadow.hash,
+ shadow.single_block_object_key_main,
+ shadow.single_block_object_deleted_at,
+ shadow.consolidated_object_key_main,
+ shadow.object_format,
+ shadow.byte_offset,
+ shadow.byte_length,
+ shadow.uncompressed_length,
+ shadow.validated_at,
+ bm.tag,
+ bm.height,
+ bm.hash,
+ bm.object_key_main,
+ bm.object_format,
+ bm.byte_offset,
+ bm.byte_length,
+ bm.uncompressed_length,
+ bm.skipped,
+ bm.single_block_retention_fenced_at
+ FROM block_consolidation_shadow shadow
+ JOIN block_metadata bm ON bm.id = shadow.block_metadata_id
+ WHERE shadow.block_metadata_id = $1
+ FOR UPDATE OF shadow, bm`
+ var metadata finalizationMetadata
+ if err := tx.QueryRowContext(ctx, query, blockMetadataID).Scan(
+ &metadata.shadowTag,
+ &metadata.shadowHeight,
+ &metadata.shadowHash,
+ &metadata.shadowSingleBlockKey,
+ &metadata.shadowDeletedAt,
+ &metadata.shadowConsolidatedKey,
+ &metadata.shadowObjectFormat,
+ &metadata.shadowByteOffset,
+ &metadata.shadowByteLength,
+ &metadata.shadowUncompressedLength,
+ &metadata.shadowValidatedAt,
+ &metadata.primaryTag,
+ &metadata.primaryHeight,
+ &metadata.primaryHash,
+ &metadata.primaryObjectKey,
+ &metadata.primaryObjectFormat,
+ &metadata.primaryByteOffset,
+ &metadata.primaryByteLength,
+ &metadata.primaryUncompressed,
+ &metadata.primarySkipped,
+ &metadata.primaryRetirementFenced,
+ ); err != nil {
+ return finalizationMetadata{}, xerrors.Errorf("failed to lock retirement finalization metadata: %w", err)
+ }
+ return metadata, nil
+}
+
+func finalizationMetadataMatchesManifest(metadata finalizationMetadata, manifest RetirementManifest) bool {
+ return metadata.shadowValidatedAt.Valid &&
+ metadata.primaryRetirementFenced.Valid &&
+ metadata.shadowTag >= 0 && uint32(metadata.shadowTag) == manifest.Tag &&
+ metadata.shadowHeight >= 0 && uint64(metadata.shadowHeight) == manifest.Height &&
+ nullableHashMatches(metadata.shadowHash, manifest.Hash) &&
+ metadata.primaryTag >= 0 && uint32(metadata.primaryTag) == manifest.Tag &&
+ metadata.primaryHeight >= 0 && uint64(metadata.primaryHeight) == manifest.Height &&
+ nullableHashMatches(metadata.primaryHash, manifest.Hash) &&
+ metadata.shadowConsolidatedKey == manifest.ConsolidatedObjectKey &&
+ metadata.shadowObjectFormat == int64(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH) &&
+ metadata.shadowByteOffset >= 0 && uint64(metadata.shadowByteOffset) == manifest.ConsolidatedByteOffset &&
+ metadata.shadowByteLength > 0 && uint64(metadata.shadowByteLength) == manifest.ConsolidatedByteLength &&
+ metadata.shadowUncompressedLength > 0 && uint64(metadata.shadowUncompressedLength) == manifest.ConsolidatedUncompressedLength &&
+ !metadata.primarySkipped &&
+ metadata.primaryObjectKey.Valid && metadata.primaryObjectKey.String == manifest.ConsolidatedObjectKey &&
+ metadata.primaryObjectFormat == int64(api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH) &&
+ metadata.primaryByteOffset.Valid && metadata.primaryByteOffset.Int64 >= 0 && uint64(metadata.primaryByteOffset.Int64) == manifest.ConsolidatedByteOffset &&
+ metadata.primaryByteLength.Valid && metadata.primaryByteLength.Int64 > 0 && uint64(metadata.primaryByteLength.Int64) == manifest.ConsolidatedByteLength &&
+ metadata.primaryUncompressed.Valid && metadata.primaryUncompressed.Int64 > 0 && uint64(metadata.primaryUncompressed.Int64) == manifest.ConsolidatedUncompressedLength
+}
+
+func nullableHashMatches(value sql.NullString, expected string) bool {
+ if !value.Valid {
+ return expected == ""
+ }
+ return value.String == expected
+}
+
+func (r *PostgresRepository) ListPendingRetirements(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ eligibilityCutoff time.Time,
+ limit uint64,
+) ([]RetirementManifest, error) {
+ if r.db == nil {
+ return nil, xerrors.New("postgres db is required")
+ }
+ if endHeight <= startHeight {
+ return nil, nil
+ }
+ if limit == 0 {
+ limit = endHeight - startHeight
+ }
+ const query = `
+ SELECT
+ retention.block_metadata_id, retention.tag, retention.height, COALESCE(retention.hash, ''),
+ retention.state, retention.bucket,
+ retention.single_block_object_key_main, retention.single_block_object_key_sha256,
+ retention.single_block_object_version_ids, retention.single_block_delete_marker_version_ids,
+ retention.single_block_object_etag, retention.single_block_object_bytes,
+ retention.consolidated_object_key_main, retention.consolidated_object_version_id,
+ retention.consolidated_object_etag, retention.consolidated_byte_offset,
+ retention.consolidated_byte_length, retention.consolidated_uncompressed_length,
+ retention.payload_sha256, retention.outcome, retention.attempt_count,
+ retention.claim_token, retention.claim_expires_at, retention.prepared_at,
+ retention.delete_started_at, retention.last_attempt_at, retention.deleted_at,
+ retention.verified_at
+ FROM block_single_block_retention retention
+ JOIN block_consolidation_shadow shadow
+ ON shadow.block_metadata_id = retention.block_metadata_id
+ AND shadow.tag = retention.tag
+ AND shadow.height = retention.height
+ WHERE retention.tag = $1
+ AND retention.height >= $2
+ AND retention.height < $3
+ AND retention.state IN ($4, $5, $6)
+ AND shadow.single_block_delete_after IS NOT NULL
+ AND shadow.single_block_delete_after <= $7
+ ORDER BY retention.height ASC
+ LIMIT $8`
+ rows, err := r.db.QueryContext(
+ ctx,
+ query,
+ tag,
+ startHeight,
+ endHeight,
+ RetirementStateEligible,
+ RetirementStateDeleting,
+ RetirementStateDeletedPendingVerification,
+ eligibilityCutoff,
+ limit,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to list pending retirements: %w", err)
+ }
+ defer func() { _ = rows.Close() }()
+ result := make([]RetirementManifest, 0)
+ for rows.Next() {
+ manifest, err := scanManifest(rows)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to scan pending retirement: %w", err)
+ }
+ result = append(result, manifest)
+ }
+ if err := rows.Err(); err != nil {
+ return nil, xerrors.Errorf("failed to iterate pending retirements: %w", err)
+ }
+ return result, nil
+}
+
+type scanner interface {
+ Scan(dest ...any) error
+}
+
+func scanMetadataRow(source scanner) (MetadataRow, error) {
+ var row MetadataRow
+ var primaryObjectFormat int64
+ var primaryByteOffset sql.NullInt64
+ var primaryByteLength sql.NullInt64
+ var primaryUncompressedLength sql.NullInt64
+ var primaryRetirementFencedAt sql.NullTime
+ var shadowBlockMetadataID sql.NullInt64
+ var shadowTag sql.NullInt64
+ var shadowHeight sql.NullInt64
+ var shadowHash sql.NullString
+ var shadowSingleBlockKey sql.NullString
+ var shadowConsolidatedKey sql.NullString
+ var shadowObjectFormat sql.NullInt64
+ var shadowByteOffset sql.NullInt64
+ var shadowByteLength sql.NullInt64
+ var shadowUncompressedLength sql.NullInt64
+ var shadowValidatedAt sql.NullTime
+ var shadowSingleBlockRetentionStartedAt sql.NullTime
+ var shadowSingleBlockDeleteAfter sql.NullTime
+ var shadowSingleBlockObjectDeletedAt sql.NullTime
+ var shadowFormatVersion sql.NullInt64
+ var retirementBlockMetadataID sql.NullInt64
+ var retirementState sql.NullString
+ var retirementBucket sql.NullString
+ var retirementSingleBlockKey sql.NullString
+ var retirementSingleBlockKeySHA256 sql.NullString
+ var retirementVersionIDs pq.StringArray
+ var retirementDeleteMarkerVersionIDs pq.StringArray
+ var retirementSingleBlockETag sql.NullString
+ var retirementSingleBlockBytes sql.NullInt64
+ var retirementConsolidatedKey sql.NullString
+ var retirementConsolidatedVersionID sql.NullString
+ var retirementConsolidatedETag sql.NullString
+ var retirementByteOffset sql.NullInt64
+ var retirementByteLength sql.NullInt64
+ var retirementUncompressedLength sql.NullInt64
+ var retirementPayloadSHA256 sql.NullString
+ var retirementOutcome sql.NullString
+ var retirementAttemptCount sql.NullInt64
+ var retirementClaimToken sql.NullString
+ var retirementClaimExpiresAt sql.NullTime
+ var retirementPreparedAt sql.NullTime
+ var retirementDeleteStartedAt sql.NullTime
+ var retirementLastAttemptAt sql.NullTime
+ var retirementDeletedAt sql.NullTime
+ var retirementVerifiedAt sql.NullTime
+ if err := source.Scan(
+ &row.BlockMetadataID,
+ &row.Canonical,
+ &row.Tag,
+ &row.Height,
+ &row.Hash,
+ &row.Skipped,
+ &row.PrimaryObjectKey,
+ &primaryObjectFormat,
+ &primaryByteOffset,
+ &primaryByteLength,
+ &primaryUncompressedLength,
+ &primaryRetirementFencedAt,
+ &shadowBlockMetadataID,
+ &shadowTag,
+ &shadowHeight,
+ &shadowHash,
+ &shadowSingleBlockKey,
+ &shadowConsolidatedKey,
+ &shadowObjectFormat,
+ &shadowByteOffset,
+ &shadowByteLength,
+ &shadowUncompressedLength,
+ &shadowValidatedAt,
+ &shadowSingleBlockRetentionStartedAt,
+ &shadowSingleBlockDeleteAfter,
+ &shadowSingleBlockObjectDeletedAt,
+ &shadowFormatVersion,
+ &retirementBlockMetadataID,
+ &retirementState,
+ &retirementBucket,
+ &retirementSingleBlockKey,
+ &retirementSingleBlockKeySHA256,
+ &retirementVersionIDs,
+ &retirementDeleteMarkerVersionIDs,
+ &retirementSingleBlockETag,
+ &retirementSingleBlockBytes,
+ &retirementConsolidatedKey,
+ &retirementConsolidatedVersionID,
+ &retirementConsolidatedETag,
+ &retirementByteOffset,
+ &retirementByteLength,
+ &retirementUncompressedLength,
+ &retirementPayloadSHA256,
+ &retirementOutcome,
+ &retirementAttemptCount,
+ &retirementClaimToken,
+ &retirementClaimExpiresAt,
+ &retirementPreparedAt,
+ &retirementDeleteStartedAt,
+ &retirementLastAttemptAt,
+ &retirementDeletedAt,
+ &retirementVerifiedAt,
+ ); err != nil {
+ return MetadataRow{}, err
+ }
+ row.PrimaryObjectFormat = api.BlockObjectFormat(primaryObjectFormat)
+ row.PrimaryByteOffset = nullableUint64(primaryByteOffset)
+ row.PrimaryByteLength = nullableUint64(primaryByteLength)
+ row.PrimaryUncompressedLength = nullableUint64(primaryUncompressedLength)
+ row.RetirementFencedAt = nullableTime(primaryRetirementFencedAt)
+ if shadowBlockMetadataID.Valid {
+ row.SingleBlockObjectKey = shadowSingleBlockKey.String
+ row.Shadow = &ConsolidationShadow{
+ Tag: uint32(shadowTag.Int64),
+ Height: uint64(shadowHeight.Int64),
+ Hash: shadowHash.String,
+ SingleBlockObjectKey: shadowSingleBlockKey.String,
+ ConsolidatedObjectKey: shadowConsolidatedKey.String,
+ ObjectFormat: api.BlockObjectFormat(shadowObjectFormat.Int64),
+ ByteOffset: nullableUint64(shadowByteOffset),
+ ByteLength: nullableUint64(shadowByteLength),
+ UncompressedLength: nullableUint64(shadowUncompressedLength),
+ FormatVersion: int(shadowFormatVersion.Int64),
+ }
+ row.Shadow.ValidatedAt = nullableTime(shadowValidatedAt)
+ row.Shadow.SingleBlockRetentionStartedAt = nullableTime(shadowSingleBlockRetentionStartedAt)
+ row.Shadow.SingleBlockDeleteAfter = nullableTime(shadowSingleBlockDeleteAfter)
+ row.Shadow.SingleBlockObjectDeletedAt = nullableTime(shadowSingleBlockObjectDeletedAt)
+ }
+ if retirementBlockMetadataID.Valid {
+ row.Retirement = &RetirementManifest{
+ BlockMetadataID: retirementBlockMetadataID.Int64,
+ Tag: row.Tag,
+ Height: row.Height,
+ Hash: row.Hash,
+ State: retirementState.String,
+ Bucket: retirementBucket.String,
+ SingleBlockObjectKey: retirementSingleBlockKey.String,
+ SingleBlockObjectKeySHA256: retirementSingleBlockKeySHA256.String,
+ SingleBlockObjectVersionIDs: append([]string(nil), retirementVersionIDs...),
+ SingleBlockDeleteMarkerVersionIDs: append([]string(nil), retirementDeleteMarkerVersionIDs...),
+ SingleBlockObjectETag: retirementSingleBlockETag.String,
+ SingleBlockObjectBytes: nullableUint64(retirementSingleBlockBytes),
+ ConsolidatedObjectKey: retirementConsolidatedKey.String,
+ ConsolidatedObjectVersionID: retirementConsolidatedVersionID.String,
+ ConsolidatedObjectETag: retirementConsolidatedETag.String,
+ ConsolidatedByteOffset: nullableUint64(retirementByteOffset),
+ ConsolidatedByteLength: nullableUint64(retirementByteLength),
+ ConsolidatedUncompressedLength: nullableUint64(retirementUncompressedLength),
+ PayloadSHA256: retirementPayloadSHA256.String,
+ Outcome: retirementOutcome.String,
+ AttemptCount: int(retirementAttemptCount.Int64),
+ ClaimToken: retirementClaimToken.String,
+ ClaimExpiresAt: nullableTime(retirementClaimExpiresAt),
+ PreparedAt: retirementPreparedAt.Time,
+ DeleteStartedAt: nullableTime(retirementDeleteStartedAt),
+ LastAttemptAt: nullableTime(retirementLastAttemptAt),
+ DeletedAt: nullableTime(retirementDeletedAt),
+ VerifiedAt: nullableTime(retirementVerifiedAt),
+ }
+ }
+ return row, nil
+}
+
+func getManifestForUpdate(ctx context.Context, tx *sql.Tx, blockMetadataID int64) (RetirementManifest, error) {
+ const query = `
+ SELECT
+ block_metadata_id, tag, height, COALESCE(hash, ''), state, bucket,
+ single_block_object_key_main, single_block_object_key_sha256, single_block_object_version_ids,
+ single_block_delete_marker_version_ids,
+ single_block_object_etag, single_block_object_bytes,
+ consolidated_object_key_main, consolidated_object_version_id, consolidated_object_etag,
+ consolidated_byte_offset, consolidated_byte_length, consolidated_uncompressed_length,
+ payload_sha256, outcome, attempt_count, claim_token, claim_expires_at,
+ prepared_at, delete_started_at, last_attempt_at, deleted_at, verified_at
+ FROM block_single_block_retention
+ WHERE block_metadata_id = $1
+ FOR UPDATE`
+ manifest, err := scanManifest(tx.QueryRowContext(ctx, query, blockMetadataID))
+ if err != nil {
+ return RetirementManifest{}, xerrors.Errorf("failed to lock retirement manifest: %w", err)
+ }
+ return manifest, nil
+}
+
+func scanManifest(source scanner) (RetirementManifest, error) {
+ var manifest RetirementManifest
+ var hash sql.NullString
+ var singleBlockKey sql.NullString
+ var versionIDs pq.StringArray
+ var deleteMarkerVersionIDs pq.StringArray
+ var singleBlockBytes int64
+ var byteOffset int64
+ var byteLength int64
+ var uncompressedLength int64
+ var attemptCount int
+ var claimToken sql.NullString
+ var claimExpiresAt sql.NullTime
+ var deleteStartedAt sql.NullTime
+ var lastAttemptAt sql.NullTime
+ var deletedAt sql.NullTime
+ var verifiedAt sql.NullTime
+ if err := source.Scan(
+ &manifest.BlockMetadataID,
+ &manifest.Tag,
+ &manifest.Height,
+ &hash,
+ &manifest.State,
+ &manifest.Bucket,
+ &singleBlockKey,
+ &manifest.SingleBlockObjectKeySHA256,
+ &versionIDs,
+ &deleteMarkerVersionIDs,
+ &manifest.SingleBlockObjectETag,
+ &singleBlockBytes,
+ &manifest.ConsolidatedObjectKey,
+ &manifest.ConsolidatedObjectVersionID,
+ &manifest.ConsolidatedObjectETag,
+ &byteOffset,
+ &byteLength,
+ &uncompressedLength,
+ &manifest.PayloadSHA256,
+ &manifest.Outcome,
+ &attemptCount,
+ &claimToken,
+ &claimExpiresAt,
+ &manifest.PreparedAt,
+ &deleteStartedAt,
+ &lastAttemptAt,
+ &deletedAt,
+ &verifiedAt,
+ ); err != nil {
+ return RetirementManifest{}, err
+ }
+ if singleBlockBytes < 0 || byteOffset < 0 || byteLength < 0 || uncompressedLength < 0 || attemptCount < 0 {
+ return RetirementManifest{}, xerrors.New("retirement manifest contains negative byte fields")
+ }
+ manifest.Hash = hash.String
+ manifest.SingleBlockObjectKey = singleBlockKey.String
+ manifest.SingleBlockObjectVersionIDs = append([]string(nil), versionIDs...)
+ manifest.SingleBlockDeleteMarkerVersionIDs = append([]string(nil), deleteMarkerVersionIDs...)
+ manifest.SingleBlockObjectBytes = uint64(singleBlockBytes)
+ manifest.ConsolidatedByteOffset = uint64(byteOffset)
+ manifest.ConsolidatedByteLength = uint64(byteLength)
+ manifest.ConsolidatedUncompressedLength = uint64(uncompressedLength)
+ manifest.AttemptCount = attemptCount
+ manifest.ClaimToken = claimToken.String
+ manifest.ClaimExpiresAt = nullableTime(claimExpiresAt)
+ manifest.DeleteStartedAt = nullableTime(deleteStartedAt)
+ manifest.LastAttemptAt = nullableTime(lastAttemptAt)
+ manifest.DeletedAt = nullableTime(deletedAt)
+ manifest.VerifiedAt = nullableTime(verifiedAt)
+ return manifest, nil
+}
+
+func validateManifestMetadata(row MetadataRow, manifest RetirementManifest) error {
+ if !row.Canonical || row.BlockMetadataID != manifest.BlockMetadataID || row.Tag != manifest.Tag || row.Height != manifest.Height || row.Hash != manifest.Hash {
+ return xerrors.Errorf("canonical metadata changed before retirement: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ if row.Skipped || row.Shadow == nil || !validShadowReference(row, row.Shadow) || row.SingleBlockObjectKey != manifest.SingleBlockObjectKey {
+ return xerrors.Errorf("single-block shadow metadata changed before retirement: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ if row.Shadow.ValidatedAt == nil || row.Shadow.SingleBlockRetentionStartedAt == nil || row.Shadow.SingleBlockDeleteAfter == nil ||
+ row.Shadow.SingleBlockObjectDeletedAt != nil {
+ return xerrors.Errorf("retirement eligibility changed before manifest persistence: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ if row.PrimaryObjectKey != manifest.ConsolidatedObjectKey ||
+ row.PrimaryObjectFormat != api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH ||
+ row.PrimaryByteOffset != manifest.ConsolidatedByteOffset ||
+ row.PrimaryByteLength != manifest.ConsolidatedByteLength ||
+ row.PrimaryUncompressedLength != manifest.ConsolidatedUncompressedLength {
+ return xerrors.Errorf("consolidated metadata changed before retirement: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ return nil
+}
+
+func validatePreparedManifest(manifest RetirementManifest) error {
+ if manifest.BlockMetadataID <= 0 || manifest.Bucket == "" || manifest.SingleBlockObjectKey == "" ||
+ manifest.SingleBlockObjectKeySHA256 != keySHA256(manifest.SingleBlockObjectKey) ||
+ !validPinnedVersionIDs(
+ firstString(manifest.SingleBlockObjectVersionIDs),
+ manifest.SingleBlockObjectVersionIDs,
+ manifest.SingleBlockDeleteMarkerVersionIDs,
+ ) ||
+ manifest.SingleBlockObjectETag == "" || manifest.SingleBlockObjectBytes == 0 ||
+ manifest.ConsolidatedObjectKey == "" || !isImmutableVersionID(manifest.ConsolidatedObjectVersionID) || manifest.ConsolidatedObjectETag == "" ||
+ manifest.ConsolidatedByteLength == 0 || manifest.ConsolidatedUncompressedLength == 0 ||
+ !isSHA256Hex(manifest.PayloadSHA256) || manifest.PreparedAt.IsZero() {
+ return xerrors.Errorf("retirement manifest is incomplete: block_metadata_id=%d", manifest.BlockMetadataID)
+ }
+ return nil
+}
+
+func samePreparedManifest(existing RetirementManifest, expected RetirementManifest) bool {
+ if existing.State != RetirementStateEligible && existing.State != RetirementStateDeleting {
+ return false
+ }
+ return existing.BlockMetadataID == expected.BlockMetadataID &&
+ existing.Tag == expected.Tag &&
+ existing.Height == expected.Height &&
+ existing.Hash == expected.Hash &&
+ existing.Bucket == expected.Bucket &&
+ existing.SingleBlockObjectKey == expected.SingleBlockObjectKey &&
+ existing.SingleBlockObjectKeySHA256 == expected.SingleBlockObjectKeySHA256 &&
+ sameStrings(existing.SingleBlockObjectVersionIDs, expected.SingleBlockObjectVersionIDs) &&
+ sameStrings(existing.SingleBlockDeleteMarkerVersionIDs, expected.SingleBlockDeleteMarkerVersionIDs) &&
+ existing.SingleBlockObjectETag == expected.SingleBlockObjectETag &&
+ existing.SingleBlockObjectBytes == expected.SingleBlockObjectBytes &&
+ existing.ConsolidatedObjectKey == expected.ConsolidatedObjectKey &&
+ existing.ConsolidatedObjectVersionID == expected.ConsolidatedObjectVersionID &&
+ existing.ConsolidatedObjectETag == expected.ConsolidatedObjectETag &&
+ existing.ConsolidatedByteOffset == expected.ConsolidatedByteOffset &&
+ existing.ConsolidatedByteLength == expected.ConsolidatedByteLength &&
+ existing.ConsolidatedUncompressedLength == expected.ConsolidatedUncompressedLength &&
+ existing.PayloadSHA256 == expected.PayloadSHA256
+}
+
+func sameStrings(left []string, right []string) bool {
+ if len(left) != len(right) {
+ return false
+ }
+ for i := range left {
+ if left[i] != right[i] {
+ return false
+ }
+ }
+ return true
+}
+
+func nullableTime(value sql.NullTime) *time.Time {
+ if !value.Valid {
+ return nil
+ }
+ copy := value.Time
+ return ©
+}
+
+func nullableUint64(value sql.NullInt64) uint64 {
+ if !value.Valid || value.Int64 < 0 {
+ return 0
+ }
+ return uint64(value.Int64)
+}
diff --git a/internal/storage/retirement/repository_postgres_integration_test.go b/internal/storage/retirement/repository_postgres_integration_test.go
new file mode 100644
index 00000000..8253c967
--- /dev/null
+++ b/internal/storage/retirement/repository_postgres_integration_test.go
@@ -0,0 +1,692 @@
+package retirement
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "strings"
+ "testing"
+ "time"
+
+ "github.com/lib/pq"
+ "github.com/pressly/goose/v3"
+ "github.com/stretchr/testify/require"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ metapostgres "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestIntegrationPostgresRepositoryRetirementStateMachine(t *testing.T) {
+ require := require.New(t)
+ cfg, err := config.New()
+ require.NoError(err)
+ if cfg.AWS.Postgres == nil {
+ t.Skip("Postgres is not configured")
+ }
+ if cfg.Env() == config.EnvProduction {
+ t.Skip("retirement integration tests never write to production")
+ }
+
+ ctx := context.Background()
+ db, err := openRetirementIntegrationDB(ctx, cfg.AWS.Postgres)
+ if err != nil {
+ t.Skipf("Postgres integration database is unavailable: %v", err)
+ }
+ defer func() { _ = db.Close() }()
+ goose.SetBaseFS(metapostgres.GetEmbeddedMigrations())
+ require.NoError(goose.SetDialect("postgres"))
+ require.NoError(goose.UpContext(ctx, db, "db/migrations"))
+
+ unique := time.Now().UTC().UnixNano()
+ tag := uint32(1_000_000_000 + unique%100_000_000)
+ height := uint64(8_000_000_000 + unique%100_000_000)
+ // Exercise the complete lifecycle for SQL NULL hashes. Repository reads
+ // normalize NULL to an empty string, while writes use NULLIF consistently.
+ hash := ""
+ singleBlockKey := fmt.Sprintf("single-block/%d.gzip", height)
+ cscbKey := fmt.Sprintf("consolidated/%d.cscb.gzip", height)
+ validatedAt := time.Now().UTC().Add(-96 * time.Hour)
+ retiredAt := validatedAt
+ retireAfter := retiredAt.Add(72 * time.Hour)
+ var blockMetadataID int64
+ err = db.QueryRowContext(ctx, `
+ INSERT INTO block_metadata (
+ height, tag, hash, parent_height, object_key_main, timestamp, skipped,
+ object_format, byte_offset, byte_length, uncompressed_length
+ ) VALUES ($1, $2, NULLIF($3, ''), $4, $5, $6, FALSE, $7, $8, $9, $10)
+ RETURNING id`,
+ height,
+ tag,
+ hash,
+ height-1,
+ cscbKey,
+ time.Now().UTC().Unix(),
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 0,
+ 128,
+ 128,
+ ).Scan(&blockMetadataID)
+ require.NoError(err)
+ defer func() {
+ _, _ = db.ExecContext(ctx, `ALTER TABLE block_single_block_retention DISABLE TRIGGER block_single_block_retention_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_single_block_retention WHERE block_metadata_id = $1`, blockMetadataID)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE block_single_block_retention ENABLE TRIGGER block_single_block_retention_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_consolidation_shadow WHERE block_metadata_id = $1`, blockMetadataID)
+ _, _ = db.ExecContext(ctx, `DELETE FROM canonical_blocks WHERE block_metadata_id = $1`, blockMetadataID)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_metadata WHERE id = $1`, blockMetadataID)
+ }()
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO canonical_blocks (height, block_metadata_id, tag)
+ VALUES ($1, $2, $3)`, height, blockMetadataID, tag)
+ require.NoError(err)
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO block_consolidation_shadow (
+ block_metadata_id, tag, height, hash, single_block_object_key_main,
+ consolidated_object_key_main, object_format, byte_offset, byte_length,
+ uncompressed_length, validated_at, single_block_retention_started_at, single_block_delete_after
+ ) VALUES ($1, $2, $3, NULLIF($4, ''), $5, $6, $7, $8, $9, $10, $11, $12, $13)`,
+ blockMetadataID,
+ tag,
+ height,
+ hash,
+ singleBlockKey,
+ cscbKey,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 0,
+ 128,
+ 128,
+ validatedAt,
+ retiredAt,
+ retireAfter,
+ )
+ require.NoError(err)
+
+ repo := NewPostgresRepository(db)
+ preparedAt := time.Now().UTC().Add(7 * 24 * time.Hour)
+ manifest := RetirementManifest{
+ BlockMetadataID: blockMetadataID,
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ State: RetirementStateEligible,
+ Bucket: "integration-bucket",
+ SingleBlockObjectKey: singleBlockKey,
+ SingleBlockObjectKeySHA256: keySHA256(singleBlockKey),
+ SingleBlockObjectVersionIDs: []string{"single-block-v2", "single-block-v1"},
+ SingleBlockDeleteMarkerVersionIDs: []string{"delete-marker-v1"},
+ SingleBlockObjectETag: "single-block-etag",
+ SingleBlockObjectBytes: 256,
+ ConsolidatedObjectKey: cscbKey,
+ ConsolidatedObjectVersionID: "cscb-v1",
+ ConsolidatedObjectETag: "cscb-etag",
+ ConsolidatedByteOffset: 0,
+ ConsolidatedByteLength: 128,
+ ConsolidatedUncompressedLength: 128,
+ PayloadSHA256: keySHA256("payload"),
+ PreparedAt: preparedAt,
+ }
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO block_single_block_retention (
+ block_metadata_id, tag, height, hash, state, bucket,
+ single_block_object_key_main, single_block_object_key_sha256, single_block_object_version_ids,
+ single_block_object_etag, single_block_object_bytes,
+ consolidated_object_key_main, consolidated_object_version_id, consolidated_object_etag,
+ consolidated_byte_offset, consolidated_byte_length, consolidated_uncompressed_length,
+ payload_sha256, prepared_at
+ ) VALUES ($1, $2, $3, NULLIF($4, ''), 'deleted_verified', $5, $6, $7, $8, $9, $10,
+ $11, $12, $13, $14, $15, $16, $17, CURRENT_TIMESTAMP)`,
+ blockMetadataID, tag, height, hash, manifest.Bucket, singleBlockKey, manifest.SingleBlockObjectKeySHA256,
+ pq.Array(manifest.SingleBlockObjectVersionIDs), manifest.SingleBlockObjectETag, manifest.SingleBlockObjectBytes,
+ cscbKey, manifest.ConsolidatedObjectVersionID, manifest.ConsolidatedObjectETag,
+ manifest.ConsolidatedByteOffset, manifest.ConsolidatedByteLength, manifest.ConsolidatedUncompressedLength,
+ manifest.PayloadSHA256,
+ )
+ require.Error(err)
+ require.Contains(err.Error(), "must be inserted in eligible state")
+ _, err = db.ExecContext(ctx, `UPDATE block_consolidation_shadow SET single_block_delete_after = CURRENT_TIMESTAMP + INTERVAL '1 hour' WHERE block_metadata_id = $1`, blockMetadataID)
+ require.NoError(err)
+ err = repo.PrepareRetirement(ctx, manifest)
+ require.Error(err)
+ require.Contains(err.Error(), "failed to lock canonical retirement metadata")
+ _, err = db.ExecContext(ctx, `UPDATE block_consolidation_shadow SET single_block_delete_after = $2 WHERE block_metadata_id = $1`, blockMetadataID, retireAfter)
+ require.NoError(err)
+ require.NoError(repo.PrepareRetirement(ctx, manifest))
+ _, err = db.ExecContext(ctx, `DELETE FROM block_single_block_retention WHERE block_metadata_id = $1`, blockMetadataID)
+ require.Error(err)
+ require.Contains(err.Error(), "audit manifests cannot be deleted")
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO block_single_block_retention (
+ block_metadata_id, tag, height, hash, state, bucket,
+ single_block_object_key_main, single_block_object_key_sha256, single_block_object_version_ids,
+ single_block_object_etag, single_block_object_bytes,
+ consolidated_object_key_main, consolidated_object_version_id, consolidated_object_etag,
+ consolidated_byte_offset, consolidated_byte_length, consolidated_uncompressed_length,
+ payload_sha256, prepared_at
+ ) VALUES ($1, $2, $3, NULLIF($4, ''), 'eligible', $5, $6, $7, ARRAY['null'], $8, $9,
+ $10, $11, $12, $13, $14, $15, $16, CURRENT_TIMESTAMP)
+ ON CONFLICT (block_metadata_id) DO NOTHING`,
+ blockMetadataID, tag, height, hash, manifest.Bucket, singleBlockKey, manifest.SingleBlockObjectKeySHA256,
+ manifest.SingleBlockObjectETag, manifest.SingleBlockObjectBytes, cscbKey, manifest.ConsolidatedObjectVersionID,
+ manifest.ConsolidatedObjectETag, manifest.ConsolidatedByteOffset, manifest.ConsolidatedByteLength,
+ manifest.ConsolidatedUncompressedLength, manifest.PayloadSHA256,
+ )
+ require.Error(err)
+ require.Contains(err.Error(), "block_single_block_retention_immutable_version_ids_check")
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO block_single_block_retention (
+ block_metadata_id, tag, height, hash, state, bucket,
+ single_block_object_key_main, single_block_object_key_sha256, single_block_object_version_ids,
+ single_block_object_etag, single_block_object_bytes,
+ consolidated_object_key_main, consolidated_object_version_id, consolidated_object_etag,
+ consolidated_byte_offset, consolidated_byte_length, consolidated_uncompressed_length,
+ payload_sha256, prepared_at
+ ) VALUES ($1, $2, $3, NULLIF($4, ''), 'eligible', $5, $6, $7, ARRAY[NULL]::TEXT[], $8, $9,
+ $10, $11, $12, $13, $14, $15, $16, CURRENT_TIMESTAMP)
+ ON CONFLICT (block_metadata_id) DO NOTHING`,
+ blockMetadataID, tag, height, hash, manifest.Bucket, singleBlockKey, manifest.SingleBlockObjectKeySHA256,
+ manifest.SingleBlockObjectETag, manifest.SingleBlockObjectBytes, cscbKey, manifest.ConsolidatedObjectVersionID,
+ manifest.ConsolidatedObjectETag, manifest.ConsolidatedByteOffset, manifest.ConsolidatedByteLength,
+ manifest.ConsolidatedUncompressedLength, manifest.PayloadSHA256,
+ )
+ require.Error(err)
+ require.Contains(err.Error(), "block_single_block_retention_immutable_version_ids_check")
+ firstObservedAt, observedAt, err := repo.ObserveRetentionSafety(ctx, manifest.Bucket, cscbKey, keySHA256("safe-configuration-v1"))
+ require.NoError(err)
+ require.Equal(firstObservedAt, observedAt)
+ sameFirstObservedAt, laterObservedAt, err := repo.ObserveRetentionSafety(ctx, manifest.Bucket, cscbKey, keySHA256("safe-configuration-v1"))
+ require.NoError(err)
+ require.Equal(firstObservedAt, sameFirstObservedAt)
+ require.False(laterObservedAt.Before(observedAt))
+ resetFirstObservedAt, resetObservedAt, err := repo.ObserveRetentionSafety(ctx, manifest.Bucket, cscbKey, keySHA256("safe-configuration-v2"))
+ require.NoError(err)
+ require.Equal(resetFirstObservedAt, resetObservedAt)
+ require.False(resetFirstObservedAt.Before(laterObservedAt))
+ _, err = db.ExecContext(ctx, `UPDATE block_single_block_retention SET state = 'deleted_verified' WHERE block_metadata_id = $1`, blockMetadataID)
+ require.Error(err)
+ require.Contains(err.Error(), "invalid single-block retirement transition")
+ _, err = db.ExecContext(ctx, `UPDATE block_single_block_retention SET consolidated_object_etag = 'mutated' WHERE block_metadata_id = $1`, blockMetadataID)
+ require.Error(err)
+ require.Contains(err.Error(), "cannot change pinned retirement manifest fields")
+ _, err = db.ExecContext(ctx, `UPDATE block_single_block_retention SET single_block_delete_marker_version_ids = ARRAY['mutated-marker'] WHERE block_metadata_id = $1`, blockMetadataID)
+ require.Error(err)
+ require.Contains(err.Error(), "cannot change pinned retirement delete-marker versions")
+
+ startedAt := preparedAt.Add(24 * time.Hour)
+ claimToken := "integration-claim"
+ require.NoError(repo.ClaimRetirement(ctx, blockMetadataID, claimToken, startedAt, startedAt.Add(time.Hour)))
+ var databaseClaimExpiresAt time.Time
+ require.NoError(db.QueryRowContext(ctx, `SELECT claim_expires_at FROM block_single_block_retention WHERE block_metadata_id = $1`, blockMetadataID).Scan(&databaseClaimExpiresAt))
+ require.WithinDuration(time.Now().UTC().Add(time.Hour), databaseClaimExpiresAt, 5*time.Second)
+ _, err = db.ExecContext(ctx, `UPDATE block_single_block_retention SET claim_token = 'stolen-claim' WHERE block_metadata_id = $1`, blockMetadataID)
+ require.Error(err)
+ require.Contains(err.Error(), "cannot replace an active single-block retirement claim")
+ _, err = db.ExecContext(ctx, `UPDATE block_single_block_retention SET state = 'deleted_pending_verification', claim_token = 'stolen-claim' WHERE block_metadata_id = $1`, blockMetadataID)
+ require.Error(err)
+ require.Contains(err.Error(), "cannot change claim owner while recording single-block object deletion")
+ err = repo.ClaimRetirement(ctx, blockMetadataID, "competing-claim", startedAt.Add(48*time.Hour), startedAt.Add(49*time.Hour))
+ require.ErrorIs(err, ErrRetirementClaimUnavailable)
+ err = repo.RenewRetirementClaim(ctx, blockMetadataID, "competing-claim", startedAt.Add(time.Second), startedAt.Add(2*time.Hour))
+ require.ErrorIs(err, ErrRetirementClaimUnavailable)
+ require.NoError(repo.RenewRetirementClaim(ctx, blockMetadataID, claimToken, startedAt.Add(time.Second), startedAt.Add(2*time.Hour)))
+ _, err = db.ExecContext(ctx, `UPDATE block_single_block_retention SET claim_expires_at = clock_timestamp() - INTERVAL '1 second' WHERE block_metadata_id = $1`, blockMetadataID)
+ require.NoError(err)
+ _, err = repo.RecordRetirementObjectDeleted(ctx, blockMetadataID, claimToken, ActionDeletedObjectVersion)
+ require.ErrorIs(err, ErrRetirementClaimUnavailable)
+ claimToken = "replacement-claim"
+ require.NoError(repo.ClaimRetirement(ctx, blockMetadataID, claimToken, time.Now().UTC(), time.Now().UTC().Add(time.Hour)))
+ _, err = db.ExecContext(ctx, `DELETE FROM canonical_blocks WHERE block_metadata_id = $1`, blockMetadataID)
+ require.NoError(err)
+ postReorgRow, err := repo.GetMetadataRow(ctx, blockMetadataID)
+ require.NoError(err)
+ require.False(postReorgRow.Canonical)
+ require.Equal(singleBlockKey, postReorgRow.SingleBlockObjectKey)
+ _, err = repo.RecordRetirementObjectDeleted(ctx, blockMetadataID, "competing-claim", ActionDeletedObjectVersion)
+ require.ErrorIs(err, ErrRetirementClaimUnavailable)
+ _, err = db.ExecContext(ctx, `UPDATE block_consolidation_shadow SET byte_length = 127 WHERE block_metadata_id = $1`, blockMetadataID)
+ require.NoError(err)
+ _, err = repo.RecordRetirementObjectDeleted(ctx, blockMetadataID, claimToken, ActionDeletedObjectVersion)
+ require.Error(err)
+ require.Contains(err.Error(), "CSCB metadata changed")
+ failedRow, err := repo.GetMetadataRow(ctx, blockMetadataID)
+ require.NoError(err)
+ require.Equal(singleBlockKey, failedRow.SingleBlockObjectKey)
+ require.Equal(RetirementStateDeleting, failedRow.Retirement.State)
+ _, err = db.ExecContext(ctx, `UPDATE block_consolidation_shadow SET byte_length = 128 WHERE block_metadata_id = $1`, blockMetadataID)
+ require.NoError(err)
+ deletedAt, err := repo.RecordRetirementObjectDeleted(ctx, blockMetadataID, claimToken, ActionDeletedObjectVersion)
+ require.NoError(err)
+ require.WithinDuration(time.Now().UTC(), deletedAt, 5*time.Second)
+
+ pendingRow, err := repo.GetMetadataRow(ctx, blockMetadataID)
+ require.NoError(err)
+ require.Empty(pendingRow.SingleBlockObjectKey)
+ require.Empty(pendingRow.Shadow.SingleBlockObjectKey)
+ require.NotNil(pendingRow.Shadow.SingleBlockObjectDeletedAt)
+ require.Equal(RetirementStateDeletedPendingVerification, pendingRow.Retirement.State)
+ require.Empty(pendingRow.Retirement.SingleBlockObjectKey)
+ require.Equal(ActionDeletedObjectVersion, pendingRow.Retirement.Outcome)
+ require.NotNil(pendingRow.Retirement.DeletedAt)
+ require.Nil(pendingRow.Retirement.VerifiedAt)
+ require.Equal(claimToken, pendingRow.Retirement.ClaimToken)
+
+ _, err = db.ExecContext(ctx, `UPDATE block_single_block_retention SET consolidated_object_version_id = 'mutated' WHERE block_metadata_id = $1`, blockMetadataID)
+ require.Error(err)
+ require.Contains(err.Error(), "cannot change pinned retirement manifest fields")
+ _, err = db.ExecContext(ctx, `UPDATE block_single_block_retention SET state = 'eligible' WHERE block_metadata_id = $1`, blockMetadataID)
+ require.Error(err)
+ require.Contains(err.Error(), "invalid single-block retirement transition")
+
+ _, err = repo.FinalizeRetirement(ctx, blockMetadataID, "competing-claim", ActionDeletedVerified)
+ require.ErrorIs(err, ErrRetirementClaimUnavailable)
+ verifiedAt, err := repo.FinalizeRetirement(ctx, blockMetadataID, claimToken, ActionDeletedVerified)
+ require.NoError(err)
+ idempotentVerifiedAt, err := repo.FinalizeRetirement(ctx, blockMetadataID, "idempotent-claim", ActionDeletedVerified)
+ require.NoError(err)
+ require.Equal(verifiedAt, idempotentVerifiedAt)
+
+ row, err := repo.GetMetadataRow(ctx, blockMetadataID)
+ require.NoError(err)
+ require.False(row.Canonical)
+ require.Empty(row.Hash)
+ require.Empty(row.SingleBlockObjectKey)
+ require.NotNil(row.Shadow)
+ require.Empty(row.Shadow.Hash)
+ require.Empty(row.Shadow.SingleBlockObjectKey)
+ require.NotNil(row.Shadow.SingleBlockObjectDeletedAt)
+ require.WithinDuration(deletedAt, *row.Shadow.SingleBlockObjectDeletedAt, time.Microsecond)
+ require.Equal(cscbKey, row.Shadow.ConsolidatedObjectKey)
+ require.Equal(cscbKey, row.PrimaryObjectKey)
+ require.NotNil(row.Retirement)
+ require.WithinDuration(time.Now().UTC(), row.Retirement.PreparedAt, 5*time.Second)
+ require.Equal(RetirementStateDeletedVerified, row.Retirement.State)
+ require.Empty(row.Retirement.SingleBlockObjectKey)
+ require.Equal(keySHA256(singleBlockKey), row.Retirement.SingleBlockObjectKeySHA256)
+ require.Equal([]string{"single-block-v2", "single-block-v1"}, row.Retirement.SingleBlockObjectVersionIDs)
+ require.Equal([]string{"delete-marker-v1"}, row.Retirement.SingleBlockDeleteMarkerVersionIDs)
+ require.Empty(row.Retirement.SingleBlockObjectETag)
+ require.Empty(row.Retirement.ClaimToken)
+ require.Nil(row.Retirement.ClaimExpiresAt)
+ require.NotNil(row.Retirement.DeleteStartedAt)
+ require.Equal(2, row.Retirement.AttemptCount)
+ require.NotNil(row.Retirement.LastAttemptAt)
+ require.NotNil(row.Retirement.DeletedAt)
+ require.NotNil(row.Retirement.VerifiedAt)
+ require.Equal(ActionDeletedVerified, row.Retirement.Outcome)
+ require.WithinDuration(deletedAt, *row.Retirement.DeletedAt, time.Microsecond)
+ require.WithinDuration(verifiedAt, *row.Retirement.VerifiedAt, time.Microsecond)
+ _, err = db.ExecContext(ctx, `UPDATE block_single_block_retention SET outcome = 'mutated' WHERE block_metadata_id = $1`, blockMetadataID)
+ require.Error(err)
+ require.Contains(err.Error(), "cannot change a verified single-block retirement")
+
+ pending, err := repo.ListPendingRetirements(ctx, tag, height, height+1, time.Now().UTC(), 0)
+ require.NoError(err)
+ require.Empty(pending)
+}
+
+func TestIntegrationPostgresRepositorySelectsDueRetentionCohorts(t *testing.T) {
+ require := require.New(t)
+ cfg, err := config.New()
+ require.NoError(err)
+ if cfg.AWS.Postgres == nil {
+ t.Skip("Postgres is not configured")
+ }
+ if cfg.Env() == config.EnvProduction {
+ t.Skip("retention integration tests never write to production")
+ }
+
+ ctx := context.Background()
+ db, err := openRetirementIntegrationDB(ctx, cfg.AWS.Postgres)
+ if err != nil {
+ t.Skipf("Postgres integration database is unavailable: %v", err)
+ }
+ defer func() { _ = db.Close() }()
+ goose.SetBaseFS(metapostgres.GetEmbeddedMigrations())
+ require.NoError(goose.SetDialect("postgres"))
+ require.NoError(goose.UpContext(ctx, db, "db/migrations"))
+
+ unique := time.Now().UTC().UnixNano()
+ tag := uint32(1_100_000_000 + unique%100_000_000)
+ startHeight := uint64(8_100_000_000 + unique%100_000_000)
+ dueKey := fmt.Sprintf("consolidated/due-%d.cscb.gzip", unique)
+ futureKey := fmt.Sprintf("consolidated/future-%d.cscb.gzip", unique)
+ blockMetadataIDs := make([]int64, 0, 3)
+ defer func() {
+ _, _ = db.ExecContext(ctx, `ALTER TABLE cscb_repair_manifest DISABLE TRIGGER cscb_repair_manifest_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM cscb_repair_manifest WHERE tag = $1`, tag)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE cscb_repair_manifest ENABLE TRIGGER cscb_repair_manifest_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE block_single_block_retention DISABLE TRIGGER block_single_block_retention_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_single_block_retention WHERE tag = $1`, tag)
+ _, _ = db.ExecContext(ctx, `ALTER TABLE block_single_block_retention ENABLE TRIGGER block_single_block_retention_delete_trigger`)
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_consolidation_shadow WHERE tag = $1`, tag)
+ _, _ = db.ExecContext(ctx, `DELETE FROM canonical_blocks WHERE tag = $1`, tag)
+ for _, blockMetadataID := range blockMetadataIDs {
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_metadata WHERE id = $1`, blockMetadataID)
+ }
+ }()
+
+ now := time.Now().UTC()
+ for index := 0; index < 3; index++ {
+ height := startHeight + uint64(index)
+ consolidatedKey := dueKey
+ deleteAfter := now.Add(-time.Hour)
+ if index == 2 {
+ consolidatedKey = futureKey
+ deleteAfter = now.Add(time.Hour)
+ }
+ var blockMetadataID int64
+ err = db.QueryRowContext(ctx, `
+ INSERT INTO block_metadata (
+ height, tag, hash, parent_height, object_key_main, timestamp, skipped,
+ object_format, byte_offset, byte_length, uncompressed_length
+ ) VALUES ($1, $2, NULL, $3, $4, $5, FALSE, $6, $7, $8, $9)
+ RETURNING id`,
+ height,
+ tag,
+ height-1,
+ consolidatedKey,
+ now.Unix(),
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ uint64(index*128),
+ 128,
+ 128,
+ ).Scan(&blockMetadataID)
+ require.NoError(err)
+ blockMetadataIDs = append(blockMetadataIDs, blockMetadataID)
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO canonical_blocks (height, block_metadata_id, tag)
+ VALUES ($1, $2, $3)`,
+ height,
+ blockMetadataID,
+ tag,
+ )
+ require.NoError(err)
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO block_consolidation_shadow (
+ block_metadata_id, tag, height, hash, single_block_object_key_main,
+ consolidated_object_key_main, object_format, byte_offset, byte_length,
+ uncompressed_length, validated_at, single_block_retention_started_at,
+ single_block_delete_after
+ ) VALUES ($1, $2, $3, NULL, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
+ blockMetadataID,
+ tag,
+ height,
+ fmt.Sprintf("single-block/%d.gzip", height),
+ consolidatedKey,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ uint64(index*128),
+ 128,
+ 128,
+ now.Add(-96*time.Hour),
+ now.Add(-96*time.Hour),
+ deleteAfter,
+ )
+ require.NoError(err)
+ }
+
+ repo := NewPostgresRepository(db)
+ cohorts, err := repo.ListDueRetentionCohorts(ctx, tag, 0, 0, now, 10)
+ require.NoError(err)
+ require.Len(cohorts, 1)
+ require.Equal([]RetentionCohort{{
+ ConsolidatedObjectKey: dueKey,
+ StartHeight: startHeight,
+ EndHeight: startHeight + 2,
+ RowCount: 2,
+ EligibleAt: cohorts[0].EligibleAt,
+ }}, cohorts)
+ require.WithinDuration(now.Add(-time.Hour), cohorts[0].EligibleAt, time.Second)
+
+ pendingManifest := RetirementManifest{
+ BlockMetadataID: blockMetadataIDs[0],
+ Tag: tag,
+ Height: startHeight,
+ State: RetirementStateEligible,
+ Bucket: "integration-bucket",
+ SingleBlockObjectKey: fmt.Sprintf("single-block/%d.gzip", startHeight),
+ SingleBlockObjectKeySHA256: keySHA256(fmt.Sprintf("single-block/%d.gzip", startHeight)),
+ SingleBlockObjectVersionIDs: []string{"single-block-v1"},
+ SingleBlockObjectETag: "single-block-etag",
+ SingleBlockObjectBytes: 128,
+ ConsolidatedObjectKey: dueKey,
+ ConsolidatedObjectVersionID: "cscb-v1",
+ ConsolidatedObjectETag: "cscb-etag",
+ ConsolidatedByteOffset: 0,
+ ConsolidatedByteLength: 128,
+ ConsolidatedUncompressedLength: 128,
+ PayloadSHA256: keySHA256("payload"),
+ PreparedAt: now.Add(-30 * time.Minute),
+ }
+ require.NoError(repo.PrepareRetirement(ctx, pendingManifest))
+
+ pending, err := repo.ListPendingRetentionCohorts(ctx, tag, 0, 0, now, 10)
+ require.NoError(err)
+ require.Len(pending, 1)
+ require.Equal(uint64(1), pending[0].RowCount)
+
+ futureAtCutoff, err := repo.ListPendingRetirements(
+ ctx,
+ tag,
+ startHeight,
+ startHeight+1,
+ now.Add(-2*time.Hour),
+ 10,
+ )
+ require.NoError(err)
+ require.Empty(futureAtCutoff)
+
+ due, err := repo.ListDueRetentionCohorts(ctx, tag, 0, 0, now, 10)
+ require.NoError(err)
+ require.Len(due, 1)
+ require.Equal(startHeight+1, due[0].StartHeight)
+ require.Equal(startHeight+2, due[0].EndHeight)
+ require.Equal(uint64(1), due[0].RowCount)
+
+ snapshotPending, snapshotDue, err := repo.ListRetentionCohorts(ctx, tag, 0, 0, now, 10)
+ require.NoError(err)
+ require.Equal(pending, snapshotPending)
+ require.Equal(due, snapshotDue)
+
+ merged, hasMore, err := NewSelector(repo).Select(ctx, tag, 0, 0, now, 10)
+ require.NoError(err)
+ require.False(hasMore)
+ require.Equal([]RetentionCohort{{
+ ConsolidatedObjectKey: dueKey,
+ StartHeight: startHeight,
+ EndHeight: startHeight + 2,
+ RowCount: 2,
+ EligibleAt: merged[0].EligibleAt,
+ Pending: true,
+ }}, merged)
+
+ bounded, err := repo.ListDueRetentionCohorts(
+ ctx,
+ tag,
+ startHeight+1,
+ startHeight+2,
+ now,
+ 10,
+ )
+ require.NoError(err)
+ require.Len(bounded, 1)
+ require.Equal(startHeight+1, bounded[0].StartHeight)
+ require.Equal(startHeight+2, bounded[0].EndHeight)
+ require.Equal(uint64(1), bounded[0].RowCount)
+
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO cscb_repair_manifest (
+ tag, state, bucket, old_consolidated_object_key_main,
+ start_height, end_height, canonical_block_count, total_block_count,
+ row_set_sha256
+ ) VALUES ($1, 'preparing', $2, $3, $4, $5, 2, 2, $6)`,
+ tag,
+ "integration-bucket",
+ dueKey,
+ startHeight,
+ startHeight+2,
+ strings.Repeat("a", 64),
+ )
+ require.NoError(err)
+ cohorts, err = repo.ListDueRetentionCohorts(ctx, tag, 0, 0, now, 10)
+ require.NoError(err)
+ require.Empty(cohorts, "an object with an active CSCB repair must not be selected for retention")
+}
+
+// TestIntegrationPostgresRepositoryOrdersBoundedSelectionByHeight seeds three
+// cohorts whose due-time order is the exact reverse of their height order, so
+// the two orderings cannot be confused for one another.
+//
+// A bounded selection must walk the supplied range by height, and when the limit
+// truncates it must keep the *lowest* cohorts, so that repeated runs advance
+// monotonically and an approved range maps to a predictable set of cohorts. An
+// unbounded selection must still drain the most-overdue work first.
+func TestIntegrationPostgresRepositoryOrdersBoundedSelectionByHeight(t *testing.T) {
+ require := require.New(t)
+ cfg, err := config.New()
+ require.NoError(err)
+ if cfg.AWS.Postgres == nil {
+ t.Skip("Postgres is not configured")
+ }
+ if cfg.Env() == config.EnvProduction {
+ t.Skip("retention integration tests never write to production")
+ }
+
+ ctx := context.Background()
+ db, err := openRetirementIntegrationDB(ctx, cfg.AWS.Postgres)
+ if err != nil {
+ t.Skipf("Postgres integration database is unavailable: %v", err)
+ }
+ defer func() { _ = db.Close() }()
+ goose.SetBaseFS(metapostgres.GetEmbeddedMigrations())
+ require.NoError(goose.SetDialect("postgres"))
+ require.NoError(goose.UpContext(ctx, db, "db/migrations"))
+
+ const cohortCount = 3
+ unique := time.Now().UTC().UnixNano()
+ tag := uint32(1_100_000_000 + unique%100_000_000)
+ startHeight := uint64(8_100_000_000 + unique%100_000_000)
+ blockMetadataIDs := make([]int64, 0, cohortCount)
+ cohortKeys := make([]string, 0, cohortCount)
+ defer func() {
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_consolidation_shadow WHERE tag = $1`, tag)
+ _, _ = db.ExecContext(ctx, `DELETE FROM canonical_blocks WHERE tag = $1`, tag)
+ for _, blockMetadataID := range blockMetadataIDs {
+ _, _ = db.ExecContext(ctx, `DELETE FROM block_metadata WHERE id = $1`, blockMetadataID)
+ }
+ }()
+
+ now := time.Now().UTC()
+ for index := 0; index < cohortCount; index++ {
+ height := startHeight + uint64(index)
+ consolidatedKey := fmt.Sprintf("consolidated/ordering-%d-%d.cscb.gzip", unique, index)
+ cohortKeys = append(cohortKeys, consolidatedKey)
+ // Later heights are more overdue, so due-time order reverses height order.
+ deleteAfter := now.Add(-time.Duration(index+1) * time.Hour)
+
+ var blockMetadataID int64
+ err = db.QueryRowContext(ctx, `
+ INSERT INTO block_metadata (
+ height, tag, hash, parent_height, object_key_main, timestamp, skipped,
+ object_format, byte_offset, byte_length, uncompressed_length
+ ) VALUES ($1, $2, NULL, $3, $4, $5, FALSE, $6, $7, $8, $9)
+ RETURNING id`,
+ height,
+ tag,
+ height-1,
+ consolidatedKey,
+ now.Unix(),
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 0,
+ 128,
+ 128,
+ ).Scan(&blockMetadataID)
+ require.NoError(err)
+ blockMetadataIDs = append(blockMetadataIDs, blockMetadataID)
+
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO canonical_blocks (height, block_metadata_id, tag)
+ VALUES ($1, $2, $3)`,
+ height,
+ blockMetadataID,
+ tag,
+ )
+ require.NoError(err)
+
+ _, err = db.ExecContext(ctx, `
+ INSERT INTO block_consolidation_shadow (
+ block_metadata_id, tag, height, hash, single_block_object_key_main,
+ consolidated_object_key_main, object_format, byte_offset, byte_length,
+ uncompressed_length, validated_at, single_block_retention_started_at,
+ single_block_delete_after
+ ) VALUES ($1, $2, $3, NULL, $4, $5, $6, $7, $8, $9, $10, $11, $12)`,
+ blockMetadataID,
+ tag,
+ height,
+ fmt.Sprintf("single-block/%d.gzip", height),
+ consolidatedKey,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ 0,
+ 128,
+ 128,
+ now.Add(-96*time.Hour),
+ now.Add(-96*time.Hour),
+ deleteAfter,
+ )
+ require.NoError(err)
+ }
+
+ endHeight := startHeight + cohortCount
+
+ repo := NewPostgresRepository(db)
+ bounded, err := repo.ListDueRetentionCohorts(ctx, tag, startHeight, endHeight, now, 10)
+ require.NoError(err)
+ require.Len(bounded, cohortCount)
+ boundedHeights := make([]uint64, 0, len(bounded))
+ for _, cohort := range bounded {
+ boundedHeights = append(boundedHeights, cohort.StartHeight)
+ }
+ require.Equal(
+ []uint64{startHeight, startHeight + 1, startHeight + 2},
+ boundedHeights,
+ "a bounded selection must be ordered by height, not by due time",
+ )
+
+ truncated, err := repo.ListDueRetentionCohorts(ctx, tag, startHeight, endHeight, now, 2)
+ require.NoError(err)
+ require.Len(truncated, 2)
+ require.Equal(
+ []string{cohortKeys[0], cohortKeys[1]},
+ []string{truncated[0].ConsolidatedObjectKey, truncated[1].ConsolidatedObjectKey},
+ "a truncated bounded selection must keep the lowest cohorts so repeated runs advance monotonically",
+ )
+
+ unbounded, err := repo.ListDueRetentionCohorts(ctx, tag, 0, 0, now, 10)
+ require.NoError(err)
+ require.Len(unbounded, cohortCount)
+ unboundedHeights := make([]uint64, 0, len(unbounded))
+ for _, cohort := range unbounded {
+ unboundedHeights = append(unboundedHeights, cohort.StartHeight)
+ }
+ require.Equal(
+ []uint64{startHeight + 2, startHeight + 1, startHeight},
+ unboundedHeights,
+ "an unbounded selection must still drain the most overdue cohorts first",
+ )
+}
+
+func openRetirementIntegrationDB(ctx context.Context, cfg *config.PostgresConfig) (*sql.DB, error) {
+ dsn := fmt.Sprintf("host=%s port=%d dbname=%s user=%s password=%s sslmode=%s",
+ cfg.Host, cfg.Port, cfg.Database, cfg.User, cfg.Password, cfg.SSLMode)
+ if cfg.ConnectTimeout > 0 {
+ dsn += fmt.Sprintf(" connect_timeout=%d", int(cfg.ConnectTimeout.Seconds()))
+ }
+ db, err := sql.Open("postgres", dsn)
+ if err != nil {
+ return nil, err
+ }
+ if err := db.PingContext(ctx); err != nil {
+ _ = db.Close()
+ return nil, err
+ }
+ return db, nil
+}
diff --git a/internal/storage/retirement/s3_store.go b/internal/storage/retirement/s3_store.go
new file mode 100644
index 00000000..91fbdfb3
--- /dev/null
+++ b/internal/storage/retirement/s3_store.go
@@ -0,0 +1,200 @@
+package retirement
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "io"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ awss3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/aws/smithy-go"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/s3"
+)
+
+type S3ObjectStore struct {
+ client s3.Client
+}
+
+func NewS3ObjectStore(client s3.Client) *S3ObjectStore {
+ return &S3ObjectStore{client: client}
+}
+
+func (s *S3ObjectStore) HeadObject(ctx context.Context, bucket string, key string) (ObjectHead, error) {
+ return s.headObject(ctx, bucket, key, "")
+}
+
+func (s *S3ObjectStore) HeadObjectVersion(ctx context.Context, bucket string, key string, versionID string) (ObjectHead, error) {
+ if !isImmutableVersionID(versionID) {
+ return ObjectHead{}, xerrors.New("an immutable non-null version id is required to head an object version")
+ }
+ return s.headObject(ctx, bucket, key, versionID)
+}
+
+func (s *S3ObjectStore) headObject(ctx context.Context, bucket string, key string, versionID string) (ObjectHead, error) {
+ input := &awss3.HeadObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ }
+ if versionID != "" {
+ input.VersionId = aws.String(versionID)
+ }
+ out, err := s.client.HeadObject(ctx, input)
+ if err != nil {
+ if isObjectNotFound(err) {
+ return ObjectHead{}, nil
+ }
+ return ObjectHead{}, xerrors.Errorf("failed to head object (bucket=%s key=%s): %w", bucket, key, err)
+ }
+ head := ObjectHead{
+ Exists: true,
+ VersionID: aws.ToString(out.VersionId),
+ ETag: aws.ToString(out.ETag),
+ Metadata: out.Metadata,
+ DeleteMark: aws.ToBool(out.DeleteMarker),
+ Expiration: aws.ToString(out.Expiration),
+ }
+ if out.ContentLength != nil && *out.ContentLength > 0 {
+ head.Bytes = uint64(*out.ContentLength)
+ }
+ return head, nil
+}
+
+func (s *S3ObjectStore) ListObjectVersions(ctx context.Context, bucket string, key string) (ObjectVersionTopology, error) {
+ input := &awss3.ListObjectVersionsInput{
+ Bucket: aws.String(bucket),
+ Prefix: aws.String(key),
+ MaxKeys: aws.Int32(1000),
+ }
+ var topology ObjectVersionTopology
+ for {
+ out, err := s.client.ListObjectVersions(ctx, input)
+ if err != nil {
+ return ObjectVersionTopology{}, xerrors.Errorf("failed to list object versions (bucket=%s key=%s): %w", bucket, key, err)
+ }
+ for _, version := range out.Versions {
+ if aws.ToString(version.Key) != key {
+ continue
+ }
+ item := ObjectVersion{
+ VersionID: aws.ToString(version.VersionId),
+ ETag: aws.ToString(version.ETag),
+ IsLatest: aws.ToBool(version.IsLatest),
+ LastModified: cloneTime(version.LastModified),
+ }
+ if version.Size != nil && *version.Size > 0 {
+ item.Bytes = uint64(*version.Size)
+ }
+ topology.Versions = append(topology.Versions, item)
+ }
+ for _, marker := range out.DeleteMarkers {
+ if aws.ToString(marker.Key) != key {
+ continue
+ }
+ topology.DeleteMarkers = append(topology.DeleteMarkers, ObjectDeleteMarker{
+ VersionID: aws.ToString(marker.VersionId),
+ IsLatest: aws.ToBool(marker.IsLatest),
+ LastModified: cloneTime(marker.LastModified),
+ })
+ }
+ if !aws.ToBool(out.IsTruncated) {
+ break
+ }
+ nextKeyMarker := aws.ToString(out.NextKeyMarker)
+ nextVersionIDMarker := aws.ToString(out.NextVersionIdMarker)
+ if nextKeyMarker == "" || (nextKeyMarker == aws.ToString(input.KeyMarker) && nextVersionIDMarker == aws.ToString(input.VersionIdMarker)) {
+ return ObjectVersionTopology{}, xerrors.Errorf("invalid object-version pagination cursor (bucket=%s key=%s)", bucket, key)
+ }
+ input.KeyMarker = aws.String(nextKeyMarker)
+ input.VersionIdMarker = aws.String(nextVersionIDMarker)
+ }
+ return topology, nil
+}
+
+func (s *S3ObjectStore) ReadObjectVersion(ctx context.Context, bucket string, key string, versionID string) ([]byte, error) {
+ return s.readObject(ctx, bucket, key, versionID, "")
+}
+
+func (s *S3ObjectStore) ReadObjectVersionRange(ctx context.Context, bucket string, key string, versionID string, offset uint64, length uint64) ([]byte, error) {
+ if length == 0 {
+ return nil, xerrors.New("object range length must be positive")
+ }
+ if offset > ^uint64(0)-(length-1) {
+ return nil, xerrors.Errorf("object range overflow: offset=%d length=%d", offset, length)
+ }
+ rangeHeader := fmt.Sprintf("bytes=%d-%d", offset, offset+length-1)
+ return s.readObject(ctx, bucket, key, versionID, rangeHeader)
+}
+
+func (s *S3ObjectStore) readObject(ctx context.Context, bucket string, key string, versionID string, rangeHeader string) ([]byte, error) {
+ if !isImmutableVersionID(versionID) {
+ return nil, xerrors.New("an immutable non-null version id is required to read an object version")
+ }
+ input := &awss3.GetObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ VersionId: aws.String(versionID),
+ }
+ if rangeHeader != "" {
+ input.Range = aws.String(rangeHeader)
+ }
+ out, err := s.client.GetObject(ctx, input)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to read object version (bucket=%s key=%s version_id=%s range=%s): %w", bucket, key, versionID, rangeHeader, err)
+ }
+ if out.Body == nil {
+ return nil, xerrors.Errorf("object version returned an empty body (bucket=%s key=%s version_id=%s)", bucket, key, versionID)
+ }
+ defer func() { _ = out.Body.Close() }()
+ body, err := io.ReadAll(out.Body)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to read object-version body (bucket=%s key=%s version_id=%s): %w", bucket, key, versionID, err)
+ }
+ return body, nil
+}
+
+func (s *S3ObjectStore) DeleteObjectVersion(ctx context.Context, bucket string, key string, versionID string) error {
+ if !isImmutableVersionID(versionID) {
+ return xerrors.New("an immutable non-null version id is required for retirement delete")
+ }
+ _, err := s.client.DeleteObject(ctx, &awss3.DeleteObjectInput{
+ Bucket: aws.String(bucket),
+ Key: aws.String(key),
+ VersionId: aws.String(versionID),
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to delete object version (bucket=%s key=%s version_id=%s): %w", bucket, key, versionID, err)
+ }
+ return nil
+}
+
+func cloneTime(value *time.Time) *time.Time {
+ if value == nil {
+ return nil
+ }
+ clone := *value
+ return &clone
+}
+
+func isObjectNotFound(err error) bool {
+ var notFound *awss3types.NotFound
+ if errors.As(err, ¬Found) {
+ return true
+ }
+ var noSuchKey *awss3types.NoSuchKey
+ if errors.As(err, &noSuchKey) {
+ return true
+ }
+ var apiErr smithy.APIError
+ if errors.As(err, &apiErr) {
+ switch apiErr.ErrorCode() {
+ case "NotFound", "NoSuchKey", "404":
+ return true
+ }
+ }
+ return false
+}
diff --git a/internal/storage/retirement/s3_store_test.go b/internal/storage/retirement/s3_store_test.go
new file mode 100644
index 00000000..96ea2424
--- /dev/null
+++ b/internal/storage/retirement/s3_store_test.go
@@ -0,0 +1,150 @@
+package retirement
+
+import (
+ "bytes"
+ "context"
+ "io"
+ "testing"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ awss3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+
+ s3mocks "github.com/coinbase/chainstorage/internal/s3/mocks"
+)
+
+func TestS3ObjectStore_ListObjectVersionsPaginatesAndFiltersExactKey(t *testing.T) {
+ require := require.New(t)
+ controller := gomock.NewController(t)
+ client := s3mocks.NewMockClient(controller)
+ store := NewS3ObjectStore(client)
+ modified := time.Date(2026, 7, 13, 1, 2, 3, 0, time.UTC)
+
+ first := client.EXPECT().ListObjectVersions(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, input *awss3.ListObjectVersionsInput, options ...func(*awss3.Options)) (*awss3.ListObjectVersionsOutput, error) {
+ require.Equal("bucket", aws.ToString(input.Bucket))
+ require.Equal("single-block/key", aws.ToString(input.Prefix))
+ require.Equal(int32(1000), aws.ToInt32(input.MaxKeys))
+ require.Empty(aws.ToString(input.KeyMarker))
+ require.Empty(aws.ToString(input.VersionIdMarker))
+ return &awss3.ListObjectVersionsOutput{
+ Versions: []awss3types.ObjectVersion{
+ {Key: aws.String("single-block/key"), VersionId: aws.String("v2"), ETag: aws.String("etag-v2"), Size: aws.Int64(22), IsLatest: aws.Bool(true), LastModified: &modified},
+ {Key: aws.String("single-block/key-sibling"), VersionId: aws.String("ignored"), ETag: aws.String("ignored"), Size: aws.Int64(999)},
+ },
+ DeleteMarkers: []awss3types.DeleteMarkerEntry{
+ {Key: aws.String("single-block/key-sibling"), VersionId: aws.String("ignored-marker")},
+ },
+ IsTruncated: aws.Bool(true),
+ NextKeyMarker: aws.String("single-block/key"),
+ NextVersionIdMarker: aws.String("v2"),
+ }, nil
+ },
+ )
+ client.EXPECT().ListObjectVersions(gomock.Any(), gomock.Any()).After(first).DoAndReturn(
+ func(ctx context.Context, input *awss3.ListObjectVersionsInput, options ...func(*awss3.Options)) (*awss3.ListObjectVersionsOutput, error) {
+ require.Equal("single-block/key", aws.ToString(input.KeyMarker))
+ require.Equal("v2", aws.ToString(input.VersionIdMarker))
+ return &awss3.ListObjectVersionsOutput{
+ Versions: []awss3types.ObjectVersion{
+ {Key: aws.String("single-block/key"), VersionId: aws.String("v1"), ETag: aws.String("etag-v1"), Size: aws.Int64(11), IsLatest: aws.Bool(false)},
+ },
+ DeleteMarkers: []awss3types.DeleteMarkerEntry{
+ {Key: aws.String("single-block/key"), VersionId: aws.String("marker-v3"), IsLatest: aws.Bool(false)},
+ },
+ IsTruncated: aws.Bool(false),
+ }, nil
+ },
+ )
+
+ topology, err := store.ListObjectVersions(context.Background(), "bucket", "single-block/key")
+ require.NoError(err)
+ require.Len(topology.Versions, 2)
+ require.Equal("v2", topology.Versions[0].VersionID)
+ require.Equal("etag-v2", topology.Versions[0].ETag)
+ require.Equal(uint64(22), topology.Versions[0].Bytes)
+ require.True(topology.Versions[0].IsLatest)
+ require.Equal(modified, *topology.Versions[0].LastModified)
+ require.Equal("v1", topology.Versions[1].VersionID)
+ require.Len(topology.DeleteMarkers, 1)
+ require.Equal("marker-v3", topology.DeleteMarkers[0].VersionID)
+}
+
+func TestS3ObjectStore_UsesPinnedVersionForHeadReadRangeAndDelete(t *testing.T) {
+ require := require.New(t)
+ controller := gomock.NewController(t)
+ client := s3mocks.NewMockClient(controller)
+ store := NewS3ObjectStore(client)
+
+ client.EXPECT().HeadObject(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, input *awss3.HeadObjectInput, options ...func(*awss3.Options)) (*awss3.HeadObjectOutput, error) {
+ require.Equal("bucket", aws.ToString(input.Bucket))
+ require.Equal("consolidated/key", aws.ToString(input.Key))
+ require.Equal("cscb-v1", aws.ToString(input.VersionId))
+ return &awss3.HeadObjectOutput{
+ ContentLength: aws.Int64(123),
+ VersionId: aws.String("cscb-v1"),
+ ETag: aws.String("cscb-etag"),
+ Metadata: map[string]string{"chainstorage-format": "cscb"},
+ Expiration: aws.String(`expiry-date="Mon, 20 Jul 2026 00:00:00 GMT", rule-id="expire-current"`),
+ }, nil
+ },
+ )
+ head, err := store.HeadObjectVersion(context.Background(), "bucket", "consolidated/key", "cscb-v1")
+ require.NoError(err)
+ require.True(head.Exists)
+ require.Equal(uint64(123), head.Bytes)
+ require.Equal("cscb-v1", head.VersionID)
+ require.Equal("cscb-etag", head.ETag)
+ require.NotEmpty(head.Expiration)
+
+ client.EXPECT().GetObject(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, input *awss3.GetObjectInput, options ...func(*awss3.Options)) (*awss3.GetObjectOutput, error) {
+ require.Equal("single-block-v1", aws.ToString(input.VersionId))
+ require.Empty(aws.ToString(input.Range))
+ return &awss3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader([]byte("single-block")))}, nil
+ },
+ )
+ data, err := store.ReadObjectVersion(context.Background(), "bucket", "single-block/key", "single-block-v1")
+ require.NoError(err)
+ require.Equal([]byte("single-block"), data)
+
+ client.EXPECT().GetObject(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, input *awss3.GetObjectInput, options ...func(*awss3.Options)) (*awss3.GetObjectOutput, error) {
+ require.Equal("cscb-v1", aws.ToString(input.VersionId))
+ require.Equal("bytes=96-127", aws.ToString(input.Range))
+ return &awss3.GetObjectOutput{Body: io.NopCloser(bytes.NewReader([]byte("range")))}, nil
+ },
+ )
+ data, err = store.ReadObjectVersionRange(context.Background(), "bucket", "consolidated/key", "cscb-v1", 96, 32)
+ require.NoError(err)
+ require.Equal([]byte("range"), data)
+
+ client.EXPECT().DeleteObject(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, input *awss3.DeleteObjectInput, options ...func(*awss3.Options)) (*awss3.DeleteObjectOutput, error) {
+ require.Equal("bucket", aws.ToString(input.Bucket))
+ require.Equal("single-block/key", aws.ToString(input.Key))
+ require.Equal("single-block-v1", aws.ToString(input.VersionId))
+ return &awss3.DeleteObjectOutput{}, nil
+ },
+ )
+ require.NoError(store.DeleteObjectVersion(context.Background(), "bucket", "single-block/key", "single-block-v1"))
+}
+
+func TestS3ObjectStore_RejectsMutableOrEmptyVersionIDs(t *testing.T) {
+ store := NewS3ObjectStore(nil)
+ for _, versionID := range []string{"", "null"} {
+ require.Error(t, store.DeleteObjectVersion(context.Background(), "bucket", "key", versionID))
+ require.Error(t, func() error {
+ _, err := store.HeadObjectVersion(context.Background(), "bucket", "key", versionID)
+ return err
+ }())
+ require.Error(t, func() error {
+ _, err := store.ReadObjectVersion(context.Background(), "bucket", "key", versionID)
+ return err
+ }())
+ }
+}
diff --git a/internal/storage/retirement/selector.go b/internal/storage/retirement/selector.go
new file mode 100644
index 00000000..918afc95
--- /dev/null
+++ b/internal/storage/retirement/selector.go
@@ -0,0 +1,138 @@
+package retirement
+
+import (
+ "context"
+ "math"
+ "time"
+
+ "golang.org/x/xerrors"
+)
+
+const MaxRetentionCohortsPerWorkflow = 250
+
+type (
+ RetentionCohort struct {
+ ConsolidatedObjectKey string `json:"consolidated_object_key"`
+ StartHeight uint64 `json:"start_height"`
+ EndHeight uint64 `json:"end_height"`
+ RowCount uint64 `json:"row_count"`
+ EligibleAt time.Time `json:"eligible_at"`
+ Pending bool `json:"pending"`
+ }
+
+ // CohortRepository must return row-disjoint pending and due aggregates.
+ // Both slices must come from one database snapshot. Selector merges
+ // aggregates for the same consolidated object by summing their row counts.
+ CohortRepository interface {
+ ListRetentionCohorts(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ eligibilityCutoff time.Time,
+ limit int,
+ ) ([]RetentionCohort, []RetentionCohort, error)
+ }
+
+ Selector struct {
+ repo CohortRepository
+ }
+)
+
+func NewSelector(repo CohortRepository) *Selector {
+ return &Selector{repo: repo}
+}
+
+func (s *Selector) Select(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ eligibilityCutoff time.Time,
+ limit int,
+) ([]RetentionCohort, bool, error) {
+ if s == nil || s.repo == nil {
+ return nil, false, xerrors.New("retention cohort repository is required")
+ }
+ if limit <= 0 || limit > MaxRetentionCohortsPerWorkflow {
+ return nil, false, xerrors.Errorf(
+ "retention cohort limit must be between 1 and %d: %d",
+ MaxRetentionCohortsPerWorkflow,
+ limit,
+ )
+ }
+ if endHeight == 0 && startHeight != 0 {
+ return nil, false, xerrors.New("retention selection end height is required when start height is set")
+ }
+ if endHeight != 0 && endHeight <= startHeight {
+ return nil, false, xerrors.Errorf("invalid retention selection range [%d, %d)", startHeight, endHeight)
+ }
+ if eligibilityCutoff.IsZero() {
+ return nil, false, xerrors.New("retention selection eligibility cutoff is required")
+ }
+
+ queryLimit := limit + 1
+ pending, due, err := s.repo.ListRetentionCohorts(
+ ctx,
+ tag,
+ startHeight,
+ endHeight,
+ eligibilityCutoff,
+ queryLimit,
+ )
+ if err != nil {
+ return nil, false, xerrors.Errorf("failed to list retention cohorts: %w", err)
+ }
+
+ result := make([]RetentionCohort, 0, queryLimit)
+ positions := make(map[string]int, queryLimit)
+ appendOrMerge := func(cohort RetentionCohort) error {
+ if cohort.ConsolidatedObjectKey == "" || cohort.EndHeight <= cohort.StartHeight || cohort.RowCount == 0 || cohort.EligibleAt.IsZero() {
+ return xerrors.Errorf("invalid retention cohort: %+v", cohort)
+ }
+ if position, ok := positions[cohort.ConsolidatedObjectKey]; ok {
+ existing := &result[position]
+ if cohort.StartHeight < existing.StartHeight {
+ existing.StartHeight = cohort.StartHeight
+ }
+ if cohort.EndHeight > existing.EndHeight {
+ existing.EndHeight = cohort.EndHeight
+ }
+ if cohort.RowCount > math.MaxUint64-existing.RowCount {
+ return xerrors.Errorf(
+ "retention cohort row count overflow for object %q",
+ cohort.ConsolidatedObjectKey,
+ )
+ }
+ existing.RowCount += cohort.RowCount
+ if cohort.EligibleAt.After(existing.EligibleAt) {
+ existing.EligibleAt = cohort.EligibleAt
+ }
+ existing.Pending = existing.Pending || cohort.Pending
+ return nil
+ }
+ if len(result) >= queryLimit {
+ return nil
+ }
+ positions[cohort.ConsolidatedObjectKey] = len(result)
+ result = append(result, cohort)
+ return nil
+ }
+
+ for _, cohort := range pending {
+ cohort.Pending = true
+ if err := appendOrMerge(cohort); err != nil {
+ return nil, false, err
+ }
+ }
+ for _, cohort := range due {
+ if err := appendOrMerge(cohort); err != nil {
+ return nil, false, err
+ }
+ }
+ hasMore := len(result) > limit
+ if hasMore {
+ result = result[:limit]
+ }
+ return result, hasMore, nil
+}
diff --git a/internal/storage/retirement/selector_postgres.go b/internal/storage/retirement/selector_postgres.go
new file mode 100644
index 00000000..e4ccd225
--- /dev/null
+++ b/internal/storage/retirement/selector_postgres.go
@@ -0,0 +1,340 @@
+package retirement
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "time"
+
+ "golang.org/x/xerrors"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type retentionCohortQuerier interface {
+ QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error)
+}
+
+// ListRetentionCohorts reads pending and newly due work from one repeatable-read
+// snapshot. Without the shared snapshot, a manifest inserted between the two
+// reads can disappear from both result sets and falsely signal sweep completion.
+func (r *PostgresRepository) ListRetentionCohorts(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ eligibilityCutoff time.Time,
+ limit int,
+) ([]RetentionCohort, []RetentionCohort, error) {
+ if r == nil || r.db == nil {
+ return nil, nil, xerrors.New("postgres db is required")
+ }
+ if limit <= 0 {
+ return nil, nil, nil
+ }
+ tx, err := r.db.BeginTx(ctx, &sql.TxOptions{
+ Isolation: sql.LevelRepeatableRead,
+ ReadOnly: true,
+ })
+ if err != nil {
+ return nil, nil, xerrors.Errorf("failed to begin retention cohort snapshot: %w", err)
+ }
+ defer func() { _ = tx.Rollback() }()
+
+ pending, err := listPendingRetentionCohorts(
+ ctx,
+ tx,
+ tag,
+ startHeight,
+ endHeight,
+ eligibilityCutoff,
+ limit,
+ )
+ if err != nil {
+ return nil, nil, err
+ }
+ due, err := listDueRetentionCohorts(
+ ctx,
+ tx,
+ tag,
+ startHeight,
+ endHeight,
+ eligibilityCutoff,
+ limit,
+ )
+ if err != nil {
+ return nil, nil, err
+ }
+ if err := tx.Commit(); err != nil {
+ return nil, nil, xerrors.Errorf("failed to commit retention cohort snapshot: %w", err)
+ }
+ return pending, due, nil
+}
+
+func (r *PostgresRepository) ListPendingRetentionCohorts(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ eligibilityCutoff time.Time,
+ limit int,
+) ([]RetentionCohort, error) {
+ if r == nil || r.db == nil {
+ return nil, xerrors.New("postgres db is required")
+ }
+ return listPendingRetentionCohorts(
+ ctx,
+ r.db,
+ tag,
+ startHeight,
+ endHeight,
+ eligibilityCutoff,
+ limit,
+ )
+}
+
+func listPendingRetentionCohorts(
+ ctx context.Context,
+ db retentionCohortQuerier,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ eligibilityCutoff time.Time,
+ limit int,
+) ([]RetentionCohort, error) {
+ if limit <= 0 {
+ return nil, nil
+ }
+ const query = `
+ SELECT
+ retention.consolidated_object_key_main,
+ MIN(retention.height),
+ MAX(retention.height) + 1,
+ COUNT(*),
+ MAX(shadow.single_block_delete_after)
+ FROM block_single_block_retention retention
+ JOIN block_consolidation_shadow shadow
+ ON shadow.block_metadata_id = retention.block_metadata_id
+ AND shadow.tag = retention.tag
+ AND shadow.height = retention.height
+ WHERE retention.tag = $1
+ AND retention.state IN ($2, $3, $4)
+ AND ($6::BIGINT = 0 OR (retention.height >= $5 AND retention.height < $6))
+ AND shadow.single_block_delete_after <= $7
+ GROUP BY retention.consolidated_object_key_main
+ ORDER BY MIN(retention.prepared_at), MIN(retention.height), retention.consolidated_object_key_main
+ LIMIT $8`
+ rows, err := db.QueryContext(
+ ctx,
+ query,
+ tag,
+ RetirementStateEligible,
+ RetirementStateDeleting,
+ RetirementStateDeletedPendingVerification,
+ startHeight,
+ endHeight,
+ eligibilityCutoff,
+ limit,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to query pending retention cohorts: %w", err)
+ }
+ defer func() { _ = rows.Close() }()
+ return scanRetentionCohorts(rows, true)
+}
+
+const (
+ // dueRetentionCohortOrderingByDueTime retires whatever has been due longest
+ // first. It is the right policy for an unbounded selection, where the caller
+ // is asking "find the most overdue work anywhere".
+ dueRetentionCohortOrderingByDueTime = "due.eligible_at, MIN(shadow.height), due.consolidated_object_key_main"
+ // dueRetentionCohortOrderingByHeight walks a caller-supplied range in height
+ // order so the selection is deterministic and prefix-shaped.
+ dueRetentionCohortOrderingByHeight = "MIN(shadow.height), due.consolidated_object_key_main"
+)
+
+// dueRetentionCohortOrdering picks the cohort ordering for a selection. Both
+// return values are compile-time constants and never caller input.
+//
+// When an explicit height range is supplied, selection must be deterministic:
+// a run takes the lowest eligible cohorts in the range and repeated runs walk it
+// monotonically until MoreEligibleRanges reports false. Ordering by eligible_at
+// there would instead return a due-time-ordered *subset* of the range, which
+// breaks both retention gates that operators rely on. First, an operator
+// approving [start, end) cannot predict which cohorts a run will touch, which
+// reintroduces the "approval does not match the selected work" problem. Second,
+// eligible_at advances as retention clocks tick and as repairs re-stamp
+// single_block_delete_after, so the same approved range can resolve to a
+// different subset later — meaning a clean dry run would no longer predict what
+// an execute run over that range deletes.
+func dueRetentionCohortOrdering(endHeight uint64) string {
+ if endHeight > 0 {
+ return dueRetentionCohortOrderingByHeight
+ }
+ return dueRetentionCohortOrderingByDueTime
+}
+
+func (r *PostgresRepository) ListDueRetentionCohorts(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ eligibilityCutoff time.Time,
+ limit int,
+) ([]RetentionCohort, error) {
+ if r == nil || r.db == nil {
+ return nil, xerrors.New("postgres db is required")
+ }
+ return listDueRetentionCohorts(
+ ctx,
+ r.db,
+ tag,
+ startHeight,
+ endHeight,
+ eligibilityCutoff,
+ limit,
+ )
+}
+
+func listDueRetentionCohorts(
+ ctx context.Context,
+ db retentionCohortQuerier,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ eligibilityCutoff time.Time,
+ limit int,
+) ([]RetentionCohort, error) {
+ if limit <= 0 {
+ return nil, nil
+ }
+ const queryTemplate = `
+ WITH due_keys AS (
+ SELECT
+ shadow.consolidated_object_key_main,
+ MIN(shadow.single_block_delete_after) AS eligible_at
+ FROM block_consolidation_shadow shadow
+ WHERE shadow.tag = $1
+ AND shadow.validated_at IS NOT NULL
+ AND shadow.single_block_delete_after IS NOT NULL
+ AND shadow.single_block_delete_after <= $6
+ AND shadow.single_block_object_deleted_at IS NULL
+ AND shadow.single_block_object_key_main IS NOT NULL
+ AND shadow.single_block_object_key_main <> ''
+ AND shadow.consolidated_object_key_main IS NOT NULL
+ AND shadow.consolidated_object_key_main <> ''
+ AND ($3::BIGINT = 0 OR (shadow.height >= $2 AND shadow.height < $3))
+ AND NOT EXISTS (
+ SELECT 1
+ FROM block_single_block_retention retention
+ WHERE retention.block_metadata_id = shadow.block_metadata_id
+ AND retention.tag = shadow.tag
+ AND retention.state IN ($7, $8, $9)
+ )
+ GROUP BY shadow.consolidated_object_key_main
+ )
+ SELECT
+ due.consolidated_object_key_main,
+ MIN(shadow.height),
+ MAX(shadow.height) + 1,
+ COUNT(*),
+ MAX(shadow.single_block_delete_after)
+ FROM due_keys due
+ JOIN block_consolidation_shadow shadow
+ ON shadow.tag = $1
+ AND shadow.consolidated_object_key_main = due.consolidated_object_key_main
+ JOIN canonical_blocks canonical
+ ON canonical.tag = shadow.tag
+ AND canonical.height = shadow.height
+ AND canonical.block_metadata_id = shadow.block_metadata_id
+ JOIN block_metadata metadata
+ ON metadata.id = canonical.block_metadata_id
+ AND metadata.tag = canonical.tag
+ AND metadata.height = canonical.height
+ WHERE shadow.validated_at IS NOT NULL
+ AND shadow.single_block_delete_after IS NOT NULL
+ AND shadow.single_block_object_deleted_at IS NULL
+ AND shadow.single_block_object_key_main IS NOT NULL
+ AND shadow.single_block_object_key_main <> ''
+ AND metadata.skipped = FALSE
+ AND metadata.object_format = $4
+ AND ($3::BIGINT = 0 OR (shadow.height >= $2 AND shadow.height < $3))
+ AND metadata.object_key_main = shadow.consolidated_object_key_main
+ AND NOT EXISTS (
+ SELECT 1
+ FROM block_single_block_retention retention
+ WHERE retention.block_metadata_id = shadow.block_metadata_id
+ AND retention.tag = shadow.tag
+ AND retention.state IN ($7, $8, $9)
+ )
+ AND NOT EXISTS (
+ SELECT 1
+ FROM cscb_repair_manifest repair
+ WHERE repair.tag = shadow.tag
+ AND repair.state <> 'completed'
+ AND (
+ repair.old_consolidated_object_key_main = shadow.consolidated_object_key_main
+ OR repair.new_consolidated_object_key_main = shadow.consolidated_object_key_main
+ )
+ )
+ GROUP BY due.consolidated_object_key_main, due.eligible_at
+ HAVING MAX(shadow.single_block_delete_after) <= $6
+ ORDER BY %s
+ LIMIT $5`
+ query := fmt.Sprintf(queryTemplate, dueRetentionCohortOrdering(endHeight))
+ rows, err := db.QueryContext(
+ ctx,
+ query,
+ tag,
+ startHeight,
+ endHeight,
+ api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ limit,
+ eligibilityCutoff,
+ RetirementStateEligible,
+ RetirementStateDeleting,
+ RetirementStateDeletedPendingVerification,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to query due retention cohorts: %w", err)
+ }
+ defer func() { _ = rows.Close() }()
+ return scanRetentionCohorts(rows, false)
+}
+
+func scanRetentionCohorts(rows *sql.Rows, pending bool) ([]RetentionCohort, error) {
+ result := make([]RetentionCohort, 0)
+ for rows.Next() {
+ var (
+ objectKey string
+ start int64
+ end int64
+ count int64
+ eligibleAt time.Time
+ )
+ if err := rows.Scan(&objectKey, &start, &end, &count, &eligibleAt); err != nil {
+ return nil, xerrors.Errorf("failed to scan retention cohort: %w", err)
+ }
+ if start < 0 || end <= start || count <= 0 {
+ return nil, xerrors.Errorf(
+ "invalid retention cohort bounds: key=%q start=%d end=%d rows=%d",
+ objectKey,
+ start,
+ end,
+ count,
+ )
+ }
+ result = append(result, RetentionCohort{
+ ConsolidatedObjectKey: objectKey,
+ StartHeight: uint64(start),
+ EndHeight: uint64(end),
+ RowCount: uint64(count),
+ EligibleAt: eligibleAt,
+ Pending: pending,
+ })
+ }
+ if err := rows.Err(); err != nil {
+ return nil, xerrors.Errorf("failed to iterate retention cohorts: %w", err)
+ }
+ return result, nil
+}
diff --git a/internal/storage/retirement/selector_test.go b/internal/storage/retirement/selector_test.go
new file mode 100644
index 00000000..0a880ff0
--- /dev/null
+++ b/internal/storage/retirement/selector_test.go
@@ -0,0 +1,164 @@
+package retirement
+
+import (
+ "context"
+ "errors"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+type fakeCohortRepository struct {
+ pending []RetentionCohort
+ due []RetentionCohort
+ pendingErr error
+ dueErr error
+ pendingCutoff time.Time
+ dueCutoff time.Time
+}
+
+func (r *fakeCohortRepository) ListRetentionCohorts(
+ _ context.Context,
+ _ uint32,
+ _ uint64,
+ _ uint64,
+ eligibilityCutoff time.Time,
+ _ int,
+) ([]RetentionCohort, []RetentionCohort, error) {
+ r.pendingCutoff = eligibilityCutoff
+ r.dueCutoff = eligibilityCutoff
+ if r.pendingErr != nil {
+ return nil, nil, r.pendingErr
+ }
+ if r.dueErr != nil {
+ return nil, nil, r.dueErr
+ }
+ return r.pending, r.due, nil
+}
+
+func TestSelectorPrioritizesPendingAndMergesDueRange(t *testing.T) {
+ now := time.Date(2026, 7, 23, 0, 0, 0, 0, time.UTC)
+ repo := &fakeCohortRepository{
+ pending: []RetentionCohort{{
+ ConsolidatedObjectKey: "consolidated/a.cscb.zstd",
+ StartHeight: 102,
+ EndHeight: 104,
+ RowCount: 2,
+ EligibleAt: now.Add(-time.Hour),
+ }},
+ due: []RetentionCohort{
+ {
+ ConsolidatedObjectKey: "consolidated/a.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 110,
+ RowCount: 8,
+ EligibleAt: now.Add(-2 * time.Hour),
+ },
+ {
+ ConsolidatedObjectKey: "consolidated/b.cscb.zstd",
+ StartHeight: 200,
+ EndHeight: 210,
+ RowCount: 10,
+ EligibleAt: now,
+ },
+ },
+ }
+
+ cohorts, hasMore, err := NewSelector(repo).Select(context.Background(), 2, 0, 0, now, 2)
+ require.NoError(t, err)
+ require.False(t, hasMore)
+ require.Equal(t, now, repo.pendingCutoff)
+ require.Equal(t, now, repo.dueCutoff)
+ require.Equal(t, []RetentionCohort{
+ {
+ ConsolidatedObjectKey: "consolidated/a.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 110,
+ RowCount: 10,
+ EligibleAt: now.Add(-time.Hour),
+ Pending: true,
+ },
+ {
+ ConsolidatedObjectKey: "consolidated/b.cscb.zstd",
+ StartHeight: 200,
+ EndHeight: 210,
+ RowCount: 10,
+ EligibleAt: now,
+ },
+ }, cohorts)
+}
+
+func TestSelectorRejectsInvalidLimitAndCohort(t *testing.T) {
+ now := time.Now().UTC()
+ _, _, err := NewSelector(&fakeCohortRepository{}).Select(context.Background(), 2, 0, 0, now, 0)
+ require.ErrorContains(t, err, "between 1 and")
+
+ repo := &fakeCohortRepository{
+ due: []RetentionCohort{{
+ ConsolidatedObjectKey: "consolidated/a.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 100,
+ RowCount: 1,
+ EligibleAt: time.Now(),
+ }},
+ }
+ _, _, err = NewSelector(repo).Select(context.Background(), 2, 0, 0, now, 1)
+ require.ErrorContains(t, err, "invalid retention cohort")
+}
+
+func TestSelectorPropagatesRepositoryErrors(t *testing.T) {
+ repo := &fakeCohortRepository{pendingErr: errors.New("database unavailable")}
+ _, _, err := NewSelector(repo).Select(context.Background(), 2, 0, 0, time.Now().UTC(), 1)
+ require.ErrorContains(t, err, "database unavailable")
+}
+
+func TestSelectorValidatesOptionalHeightRange(t *testing.T) {
+ selector := NewSelector(&fakeCohortRepository{})
+ now := time.Now().UTC()
+
+ _, _, err := selector.Select(context.Background(), 2, 100, 0, now, 1)
+ require.ErrorContains(t, err, "end height is required")
+
+ _, _, err = selector.Select(context.Background(), 2, 100, 100, now, 1)
+ require.ErrorContains(t, err, "invalid retention selection range")
+
+ _, _, err = selector.Select(context.Background(), 2, 100, 200, now, 1)
+ require.NoError(t, err)
+
+ _, _, err = selector.Select(context.Background(), 2, 100, 200, time.Time{}, 1)
+ require.ErrorContains(t, err, "eligibility cutoff is required")
+}
+
+func TestSelectorReportsRemainingBacklog(t *testing.T) {
+ now := time.Now().UTC()
+ repo := &fakeCohortRepository{
+ due: []RetentionCohort{
+ {
+ ConsolidatedObjectKey: "consolidated/a.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 110,
+ RowCount: 10,
+ EligibleAt: now,
+ },
+ {
+ ConsolidatedObjectKey: "consolidated/b.cscb.zstd",
+ StartHeight: 200,
+ EndHeight: 210,
+ RowCount: 10,
+ EligibleAt: now,
+ },
+ },
+ }
+
+ cohorts, hasMore, err := NewSelector(repo).Select(context.Background(), 2, 0, 0, now, 1)
+ require.NoError(t, err)
+ require.True(t, hasMore)
+ require.Len(t, cohorts, 1)
+ require.Equal(t, "consolidated/a.cscb.zstd", cohorts[0].ConsolidatedObjectKey)
+}
+
+func TestDueRetentionCohortOrdering(t *testing.T) {
+ require.Equal(t, dueRetentionCohortOrderingByHeight, dueRetentionCohortOrdering(100))
+ require.Equal(t, dueRetentionCohortOrderingByDueTime, dueRetentionCohortOrdering(0))
+}
diff --git a/internal/storage/retirement/types.go b/internal/storage/retirement/types.go
new file mode 100644
index 00000000..6f303289
--- /dev/null
+++ b/internal/storage/retirement/types.go
@@ -0,0 +1,288 @@
+package retirement
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ Repository interface {
+ ListMetadataRows(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, limit uint64) ([]MetadataRow, error)
+ GetMetadataRow(ctx context.Context, blockMetadataID int64) (MetadataRow, error)
+ PrepareRetirement(ctx context.Context, manifest RetirementManifest) error
+ ObserveRetentionSafety(ctx context.Context, bucket string, consolidatedObjectKey string, configurationSHA256 string) (time.Time, time.Time, error)
+ ClaimRetirement(ctx context.Context, blockMetadataID int64, claimToken string, claimedAt time.Time, claimExpiresAt time.Time) error
+ RenewRetirementClaim(ctx context.Context, blockMetadataID int64, claimToken string, renewedAt time.Time, claimExpiresAt time.Time) error
+ RecordRetirementOutcome(ctx context.Context, blockMetadataID int64, claimToken string, outcome string, attemptedAt time.Time) error
+ RecordRetirementObjectDeleted(ctx context.Context, blockMetadataID int64, claimToken string, outcome string) (time.Time, error)
+ FinalizeRetirement(ctx context.Context, blockMetadataID int64, claimToken string, outcome string) (time.Time, error)
+ ListPendingRetirements(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ eligibilityCutoff time.Time,
+ limit uint64,
+ ) ([]RetirementManifest, error)
+ }
+
+ ObjectStore interface {
+ InspectObjectRetentionSafety(ctx context.Context, bucket string, key string) (RetentionSafetySnapshot, error)
+ HeadObject(ctx context.Context, bucket string, key string) (ObjectHead, error)
+ HeadObjectVersion(ctx context.Context, bucket string, key string, versionID string) (ObjectHead, error)
+ ListObjectVersions(ctx context.Context, bucket string, key string) (ObjectVersionTopology, error)
+ ReadObjectVersion(ctx context.Context, bucket string, key string, versionID string) ([]byte, error)
+ ReadObjectVersionRange(ctx context.Context, bucket string, key string, versionID string, offset uint64, length uint64) ([]byte, error)
+ DeleteObjectVersion(ctx context.Context, bucket string, key string, versionID string) error
+ }
+
+ MetadataRow struct {
+ BlockMetadataID int64
+ Canonical bool
+ Tag uint32
+ Height uint64
+ Hash string
+ Skipped bool
+ PrimaryObjectKey string
+ SingleBlockObjectKey string
+ PrimaryObjectFormat api.BlockObjectFormat
+ PrimaryByteOffset uint64
+ PrimaryByteLength uint64
+ PrimaryUncompressedLength uint64
+ RetirementFencedAt *time.Time
+ Shadow *ConsolidationShadow
+ Retirement *RetirementManifest
+ }
+
+ ConsolidationShadow struct {
+ Tag uint32
+ Height uint64
+ Hash string
+ SingleBlockObjectKey string
+ ConsolidatedObjectKey string
+ ObjectFormat api.BlockObjectFormat
+ ByteOffset uint64
+ ByteLength uint64
+ UncompressedLength uint64
+ ValidatedAt *time.Time
+ SingleBlockRetentionStartedAt *time.Time
+ SingleBlockDeleteAfter *time.Time
+ SingleBlockObjectDeletedAt *time.Time
+ FormatVersion int
+ }
+
+ ObjectHead struct {
+ Exists bool
+ Bytes uint64
+ VersionID string
+ ETag string
+ Metadata map[string]string
+ DeleteMark bool
+ Expiration string
+ }
+
+ ObjectVersion struct {
+ VersionID string
+ ETag string
+ Bytes uint64
+ IsLatest bool
+ LastModified *time.Time
+ }
+
+ ObjectDeleteMarker struct {
+ VersionID string
+ IsLatest bool
+ LastModified *time.Time
+ }
+
+ ObjectVersionTopology struct {
+ Versions []ObjectVersion
+ DeleteMarkers []ObjectDeleteMarker
+ }
+
+ RetentionSafetySnapshot struct {
+ ConfigurationSHA256 string
+ }
+
+ RetirementManifest struct {
+ BlockMetadataID int64
+ Tag uint32
+ Height uint64
+ Hash string
+ State string
+ Bucket string
+ SingleBlockObjectKey string
+ SingleBlockObjectKeySHA256 string
+ SingleBlockObjectVersionIDs []string
+ SingleBlockDeleteMarkerVersionIDs []string
+ SingleBlockObjectETag string
+ SingleBlockObjectBytes uint64
+ ConsolidatedObjectKey string
+ ConsolidatedObjectVersionID string
+ ConsolidatedObjectETag string
+ ConsolidatedByteOffset uint64
+ ConsolidatedByteLength uint64
+ ConsolidatedUncompressedLength uint64
+ PayloadSHA256 string
+ Outcome string
+ AttemptCount int
+ ClaimToken string
+ ClaimExpiresAt *time.Time
+ PreparedAt time.Time
+ DeleteStartedAt *time.Time
+ LastAttemptAt *time.Time
+ DeletedAt *time.Time
+ VerifiedAt *time.Time
+ }
+
+ PlanRequest struct {
+ Environment string
+ Blockchain string
+ Network string
+ Sidechain string
+ Bucket string
+ Tag uint32
+ StartHeight uint64
+ EndHeight uint64
+ Limit uint64
+ Now time.Time
+ EligibilityCutoff time.Time
+ Execute bool
+ ProductionDeleteEnabled bool
+ DirectStorageClientsGuarded bool
+ SingleBlockWritersGuarded bool
+ FallbackErrorCount uint64
+ Approval Approval
+ }
+
+ Approval struct {
+ Chain string `json:"chain"`
+ StartHeight uint64 `json:"start_height"`
+ EndHeight uint64 `json:"end_height"`
+ AllowContainingRange bool `json:"allow_containing_range,omitempty"`
+ }
+
+ Report struct {
+ GeneratedAt time.Time `json:"generated_at"`
+ DryRun bool `json:"dry_run"`
+ Environment string `json:"environment"`
+ Blockchain string `json:"blockchain"`
+ Network string `json:"network"`
+ Sidechain string `json:"sidechain,omitempty"`
+ Bucket string `json:"bucket"`
+ Tag uint32 `json:"tag"`
+ StartHeight uint64 `json:"start_height"`
+ EndHeight uint64 `json:"end_height"`
+ Approval Approval `json:"approval"`
+ SafetyGates SafetyGates `json:"safety_gates"`
+ Summary Summary `json:"summary"`
+ Items []Candidate `json:"items"`
+ }
+
+ SafetyGates struct {
+ DirectStorageClientsGuarded bool `json:"direct_storage_clients_guarded"`
+ SingleBlockWritersGuarded bool `json:"single_block_writers_guarded"`
+ FallbackReadErrors uint64 `json:"fallback_read_errors"`
+ VersionedDeleteMode string `json:"versioned_delete_mode"`
+ CSCBWriteOncePolicyMode string `json:"cscb_write_once_policy_mode"`
+ CSCBWriteOncePolicyVerified bool `json:"cscb_write_once_policy_verified"`
+ ProductionDeleteEnabled bool `json:"production_delete_enabled"`
+ }
+
+ Summary struct {
+ TotalRows int `json:"total_rows"`
+ EligibleRows int `json:"eligible_rows"`
+ SkippedRows int `json:"skipped_rows"`
+ DeletedRows int `json:"deleted_rows"`
+ SingleBlockBytes uint64 `json:"single_block_bytes"`
+ EligibleBytes uint64 `json:"eligible_bytes"`
+ DeleteMarkerRows int `json:"delete_marker_rows"`
+ }
+
+ Candidate struct {
+ Bucket string `json:"bucket"`
+ Key string `json:"key"`
+ VersionID string `json:"version_id,omitempty"`
+ VersionIDs []string `json:"single_block_version_ids,omitempty"`
+ DeleteMarkerVersionIDs []string `json:"single_block_delete_marker_version_ids,omitempty"`
+ Height uint64 `json:"height"`
+ Hash string `json:"hash"`
+ SingleBlockBytes uint64 `json:"single_block_bytes"`
+ ConsolidatedKey string `json:"consolidated_key"`
+ BlockMetadataID int64 `json:"block_metadata_id"`
+ Tag uint32 `json:"tag"`
+ SingleBlockETag string `json:"single_block_etag,omitempty"`
+ SingleBlockKeySHA256 string `json:"single_block_key_sha256,omitempty"`
+ SingleBlockVersions int `json:"single_block_version_count"`
+ DeleteMarkers int `json:"delete_marker_count"`
+ CSCBVersionID string `json:"cscb_version_id,omitempty"`
+ CSCBETag string `json:"cscb_etag,omitempty"`
+ CSCBWriteOncePolicy bool `json:"cscb_write_once_policy_verified"`
+ PayloadSHA256 string `json:"payload_sha256,omitempty"`
+ ByteOffset uint64 `json:"byte_offset,omitempty"`
+ ByteLength uint64 `json:"byte_length,omitempty"`
+ UncompressedLength uint64 `json:"uncompressed_length,omitempty"`
+ RetirementState string `json:"retirement_state,omitempty"`
+ RetirementAttempts int `json:"retirement_attempts,omitempty"`
+ RetirementOutcome string `json:"retirement_outcome,omitempty"`
+ ValidatedAt *time.Time `json:"validated_at"`
+ RetiredAt *time.Time `json:"retired_at"`
+ EligibleAt *time.Time `json:"eligible_at"`
+ SingleBlockDeletedAt *time.Time `json:"single_block_deleted_at,omitempty"`
+ RetirementVerifiedAt *time.Time `json:"retirement_verified_at,omitempty"`
+ Action string `json:"action"`
+ SkipReason string `json:"skip_reason"`
+ }
+)
+
+const (
+ ActionSkip = "skip"
+ ActionReportOnly = "report_only"
+ ActionDeleteObjectVersion = "delete_object_version"
+ ActionDeletedObjectVersion = "deleted_object_version"
+ ActionDeletedVerified = "deleted_verified"
+ ActionAlreadyDeleted = "already_deleted"
+
+ SkipSkippedBlock = "skipped_block"
+ SkipMissingSingleBlockKey = "missing_single_block_key"
+ SkipMissingConsolidationShadow = "missing_consolidation_shadow"
+ SkipValidationNotPassed = "validation_not_passed"
+ SkipActiveMetadataStillSingleBlock = "active_metadata_still_single_block"
+ SkipMissingRetirementMarker = "missing_retirement_marker"
+ SkipInvalidMetadataReference = "invalid_metadata_reference"
+ SkipRetentionPeriodActive = "retention_period_active"
+ SkipChainRangeNotApproved = "chain_range_not_approved"
+ SkipActiveFallbackOrReadErrors = "active_fallback_or_read_errors"
+ SkipDirectStorageClientsNotGuarded = "direct_storage_clients_not_guarded"
+ SkipSingleBlockWritersNotGuarded = "single_block_writers_not_guarded"
+ SkipMissingCSCBObject = "missing_cscb_object"
+ SkipSingleBlockObjectMissing = "single_block_object_missing"
+ SkipSingleBlockCurrentDeleteMarker = "single_block_current_delete_marker"
+ SkipSingleBlockVersionIDUnavailable = "single_block_version_id_unavailable"
+ SkipUnsafeSingleBlockVersionTopology = "unsafe_single_block_version_topology"
+ SkipSingleBlockPayloadMismatch = "single_block_payload_mismatch"
+ SkipMetadataChanged = "metadata_changed"
+ SkipCSCBObjectChanged = "cscb_object_changed"
+ SkipCSCBLifecycleExpirationActive = "cscb_lifecycle_expiration_active"
+ SkipCSCBWriteOncePolicyUnverified = "cscb_write_once_policy_not_verified"
+ SkipCSCBSafetyQuiescenceActive = "cscb_safety_quiescence_active"
+ SkipPostDeleteVerificationFailed = "post_delete_verification_failed"
+ SkipRetirementVerificationPending = "retirement_verification_pending"
+ SkipRetirementAlreadyFinalized = "retirement_already_finalized"
+ SkipRetirementClaimActive = "retirement_claim_active"
+ SkipProductionDeletionDisabled = "production_deletion_disabled"
+ SkipNotAttemptedAfterFailure = "not_attempted_after_failure"
+ SkipRetentionRunCanceled = "retention_run_canceled"
+ SkipObjectInspectionFailed = "object_inspection_failed"
+ SkipVersionedDeleteFailed = "versioned_delete_failed"
+
+ RetirementStateEligible = "eligible"
+ RetirementStateDeleting = "deleting"
+ RetirementStateDeletedPendingVerification = "deleted_pending_verification"
+ RetirementStateDeletedVerified = "deleted_verified"
+)
+
+var ErrRetirementClaimUnavailable = errors.New("retirement claim unavailable")
diff --git a/internal/storage/retirement/write_once_policy.go b/internal/storage/retirement/write_once_policy.go
new file mode 100644
index 00000000..19e26811
--- /dev/null
+++ b/internal/storage/retirement/write_once_policy.go
@@ -0,0 +1,312 @@
+package retirement
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "encoding/json"
+ "errors"
+ "net/url"
+ "regexp"
+ "strings"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ awss3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/aws/smithy-go"
+ "golang.org/x/xerrors"
+)
+
+const cscbWriteOncePolicyMode = "bucket_deny_unconditional_put_delete_replication_and_lifecycle_mutation_no_matching_lifecycle_actions"
+
+type (
+ policyStringList []string
+
+ bucketPolicyDocument struct {
+ Statements bucketPolicyStatements `json:"Statement"`
+ }
+
+ bucketPolicyStatements []bucketPolicyStatement
+
+ bucketPolicyStatement struct {
+ Effect string `json:"Effect"`
+ Principal json.RawMessage `json:"Principal"`
+ NotPrincipal json.RawMessage `json:"NotPrincipal"`
+ Action policyStringList `json:"Action"`
+ NotAction policyStringList `json:"NotAction"`
+ Resource policyStringList `json:"Resource"`
+ NotResource policyStringList `json:"NotResource"`
+ Condition map[string]map[string]policyStringList `json:"Condition"`
+ }
+)
+
+func (s *S3ObjectStore) InspectObjectRetentionSafety(ctx context.Context, bucket string, key string) (RetentionSafetySnapshot, error) {
+ if s == nil || s.client == nil {
+ return RetentionSafetySnapshot{}, xerrors.New("s3 client is required to verify the CSCB write-once policy")
+ }
+ if bucket == "" || key == "" || !strings.Contains(key, "/consolidated/") {
+ return RetentionSafetySnapshot{}, xerrors.Errorf("valid CSCB bucket and consolidated object key are required: bucket=%q key=%q", bucket, key)
+ }
+ out, err := s.client.GetBucketPolicy(ctx, &awss3.GetBucketPolicyInput{Bucket: aws.String(bucket)})
+ if err != nil {
+ return RetentionSafetySnapshot{}, xerrors.Errorf("failed to get CSCB bucket write-once policy (bucket=%s): %w", bucket, err)
+ }
+ if out == nil || out.Policy == nil || strings.TrimSpace(aws.ToString(out.Policy)) == "" {
+ return RetentionSafetySnapshot{}, xerrors.Errorf("CSCB bucket has no readable write-once policy: bucket=%s", bucket)
+ }
+ policy, err := decodeBucketPolicy(aws.ToString(out.Policy))
+ if err != nil {
+ return RetentionSafetySnapshot{}, xerrors.Errorf("failed to decode CSCB bucket policy (bucket=%s): %w", bucket, err)
+ }
+ lifecycleRules, lifecycleErr := s.getLifecycleRules(ctx, bucket)
+ snapshot, snapshotErr := retentionSafetySnapshot(policy, lifecycleRules)
+ if snapshotErr != nil {
+ return RetentionSafetySnapshot{}, snapshotErr
+ }
+ if lifecycleErr != nil {
+ return snapshot, lifecycleErr
+ }
+ objectARN := "arn:aws:s3:::" + bucket + "/" + key
+ bucketARN := "arn:aws:s3:::" + bucket
+ writeProtected := false
+ mutationProtected := false
+ lifecycleConfigurationProtected := false
+ for _, statement := range policy.Statements {
+ if statementEnforcesCSCBWriteOnce(statement, objectARN) {
+ writeProtected = true
+ }
+ if statementEnforcesCSCBMutationProtection(statement, objectARN) {
+ mutationProtected = true
+ }
+ if statementEnforcesLifecycleConfigurationProtection(statement, bucketARN) {
+ lifecycleConfigurationProtected = true
+ }
+ }
+ if !writeProtected || !mutationProtected || !lifecycleConfigurationProtected {
+ return snapshot, xerrors.Errorf(
+ "bucket policy does not make CSCB object retention-safe (conditional_put=%t mutation_protected=%t lifecycle_configuration_protected=%t): bucket=%s key=%s",
+ writeProtected,
+ mutationProtected,
+ lifecycleConfigurationProtected,
+ bucket,
+ key,
+ )
+ }
+ if err := verifyNoMatchingLifecycleActions(bucket, key, lifecycleRules); err != nil {
+ return snapshot, err
+ }
+ return snapshot, nil
+}
+
+func (s *S3ObjectStore) getLifecycleRules(ctx context.Context, bucket string) ([]awss3types.LifecycleRule, error) {
+ out, err := s.client.GetBucketLifecycleConfiguration(
+ ctx,
+ &awss3.GetBucketLifecycleConfigurationInput{Bucket: aws.String(bucket)},
+ )
+ if err != nil {
+ if isNoSuchLifecycleConfiguration(err) {
+ return nil, nil
+ }
+ return nil, xerrors.Errorf("failed to get CSCB bucket lifecycle configuration (bucket=%s): %w", bucket, err)
+ }
+ if out == nil {
+ return nil, xerrors.Errorf("CSCB bucket lifecycle configuration returned an empty response: bucket=%s", bucket)
+ }
+ return out.Rules, nil
+}
+
+func verifyNoMatchingLifecycleActions(bucket string, key string, rules []awss3types.LifecycleRule) error {
+ for _, rule := range rules {
+ if rule.Status != awss3types.ExpirationStatusEnabled || !lifecycleRuleHasObjectDataAction(rule) || !lifecycleRuleMayMatchKey(rule, key) {
+ continue
+ }
+ return xerrors.Errorf(
+ "enabled S3 lifecycle rule can expire or transition CSCB object data: bucket=%s key=%s rule_id=%q",
+ bucket,
+ key,
+ aws.ToString(rule.ID),
+ )
+ }
+ return nil
+}
+
+func retentionSafetySnapshot(policy bucketPolicyDocument, rules []awss3types.LifecycleRule) (RetentionSafetySnapshot, error) {
+ encoded, err := json.Marshal(struct {
+ Policy bucketPolicyDocument `json:"policy"`
+ Rules []awss3types.LifecycleRule `json:"lifecycle_rules"`
+ }{
+ Policy: policy,
+ Rules: rules,
+ })
+ if err != nil {
+ return RetentionSafetySnapshot{}, xerrors.Errorf("failed to encode CSCB retention safety configuration: %w", err)
+ }
+ digest := sha256.Sum256(encoded)
+ return RetentionSafetySnapshot{ConfigurationSHA256: hex.EncodeToString(digest[:])}, nil
+}
+
+func lifecycleRuleHasObjectDataAction(rule awss3types.LifecycleRule) bool {
+ return rule.Expiration != nil ||
+ rule.NoncurrentVersionExpiration != nil ||
+ len(rule.Transitions) > 0 ||
+ len(rule.NoncurrentVersionTransitions) > 0
+}
+
+func lifecycleRuleMayMatchKey(rule awss3types.LifecycleRule, key string) bool {
+ prefix, hasPrefix := lifecycleRulePrefix(rule)
+ return !hasPrefix || strings.HasPrefix(key, prefix)
+}
+
+func lifecycleRulePrefix(rule awss3types.LifecycleRule) (string, bool) {
+ if rule.Filter != nil {
+ if rule.Filter.And != nil && rule.Filter.And.Prefix != nil {
+ return aws.ToString(rule.Filter.And.Prefix), true
+ }
+ if rule.Filter.Prefix != nil {
+ return aws.ToString(rule.Filter.Prefix), true
+ }
+ return "", false
+ }
+ if rule.Prefix != nil {
+ return aws.ToString(rule.Prefix), true
+ }
+ return "", false
+}
+
+func isNoSuchLifecycleConfiguration(err error) bool {
+ var apiErr smithy.APIError
+ return errors.As(err, &apiErr) && apiErr.ErrorCode() == "NoSuchLifecycleConfiguration"
+}
+
+func decodeBucketPolicy(value string) (bucketPolicyDocument, error) {
+ var policy bucketPolicyDocument
+ if err := json.Unmarshal([]byte(value), &policy); err == nil {
+ return policy, nil
+ }
+ decoded, err := url.QueryUnescape(value)
+ if err != nil {
+ return bucketPolicyDocument{}, err
+ }
+ if err := json.Unmarshal([]byte(decoded), &policy); err != nil {
+ return bucketPolicyDocument{}, err
+ }
+ return policy, nil
+}
+
+func statementEnforcesCSCBWriteOnce(statement bucketPolicyStatement, objectARN string) bool {
+ if statement.Effect != "Deny" || len(statement.NotPrincipal) != 0 || len(statement.NotAction) != 0 || len(statement.NotResource) != 0 {
+ return false
+ }
+ if !principalIncludesEveryone(statement.Principal) || !policyPatternsInclude(statement.Action, "s3:PutObject") ||
+ !policyPatternsInclude(statement.Resource, objectARN) {
+ return false
+ }
+ if len(statement.Condition) < 1 || len(statement.Condition) > 2 {
+ return false
+ }
+ nullConditions, ok := statement.Condition["Null"]
+ if !ok || len(nullConditions) != 1 {
+ return false
+ }
+ values, ok := nullConditions["s3:if-none-match"]
+ if !ok || len(values) != 1 || values[0] != "true" {
+ return false
+ }
+ if len(statement.Condition) == 1 {
+ return true
+ }
+ boolConditions, ok := statement.Condition["Bool"]
+ if !ok || len(boolConditions) != 1 {
+ return false
+ }
+ values, ok = boolConditions["s3:ObjectCreationOperation"]
+ return ok && len(values) == 1 && values[0] == "true"
+}
+
+func statementEnforcesCSCBMutationProtection(statement bucketPolicyStatement, objectARN string) bool {
+ if statement.Effect != "Deny" || len(statement.NotPrincipal) != 0 || len(statement.NotAction) != 0 || len(statement.NotResource) != 0 || len(statement.Condition) != 0 {
+ return false
+ }
+ return principalIncludesEveryone(statement.Principal) &&
+ policyPatternsInclude(statement.Action, "s3:DeleteObject") &&
+ policyPatternsInclude(statement.Action, "s3:DeleteObjectVersion") &&
+ policyPatternsInclude(statement.Action, "s3:ReplicateObject") &&
+ policyPatternsInclude(statement.Action, "s3:ReplicateDelete") &&
+ policyPatternsInclude(statement.Resource, objectARN)
+}
+
+func statementEnforcesLifecycleConfigurationProtection(statement bucketPolicyStatement, bucketARN string) bool {
+ if statement.Effect != "Deny" || len(statement.NotPrincipal) != 0 || len(statement.NotAction) != 0 || len(statement.NotResource) != 0 || len(statement.Condition) != 0 {
+ return false
+ }
+ return principalIncludesEveryone(statement.Principal) &&
+ policyPatternsInclude(statement.Action, "s3:PutLifecycleConfiguration") &&
+ policyPatternsInclude(statement.Resource, bucketARN)
+}
+
+func principalIncludesEveryone(value json.RawMessage) bool {
+ if len(value) == 0 {
+ return false
+ }
+ var principal string
+ if err := json.Unmarshal(value, &principal); err == nil {
+ return principal == "*"
+ }
+ var principals map[string]policyStringList
+ if err := json.Unmarshal(value, &principals); err != nil {
+ return false
+ }
+ for _, key := range []string{"AWS", "*"} {
+ for _, candidate := range principals[key] {
+ if candidate == "*" {
+ return true
+ }
+ }
+ }
+ return false
+}
+
+func policyPatternsInclude(patterns policyStringList, value string) bool {
+ for _, pattern := range patterns {
+ if policyPatternMatches(pattern, value) {
+ return true
+ }
+ }
+ return false
+}
+
+func policyPatternMatches(pattern string, value string) bool {
+ expression := regexp.QuoteMeta(pattern)
+ expression = strings.ReplaceAll(expression, `\*`, `.*`)
+ expression = strings.ReplaceAll(expression, `\?`, `.`)
+ matched, err := regexp.MatchString("^"+expression+"$", value)
+ return err == nil && matched
+}
+
+func (values *policyStringList) UnmarshalJSON(data []byte) error {
+ var one string
+ if err := json.Unmarshal(data, &one); err == nil {
+ *values = []string{one}
+ return nil
+ }
+ var many []string
+ if err := json.Unmarshal(data, &many); err != nil {
+ return err
+ }
+ *values = many
+ return nil
+}
+
+func (statements *bucketPolicyStatements) UnmarshalJSON(data []byte) error {
+ var many []bucketPolicyStatement
+ if err := json.Unmarshal(data, &many); err == nil {
+ *statements = many
+ return nil
+ }
+ var one bucketPolicyStatement
+ if err := json.Unmarshal(data, &one); err != nil {
+ return err
+ }
+ *statements = []bucketPolicyStatement{one}
+ return nil
+}
diff --git a/internal/storage/retirement/write_once_policy_test.go b/internal/storage/retirement/write_once_policy_test.go
new file mode 100644
index 00000000..c00a3822
--- /dev/null
+++ b/internal/storage/retirement/write_once_policy_test.go
@@ -0,0 +1,309 @@
+package retirement
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
+ awss3types "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/aws/smithy-go"
+ "github.com/stretchr/testify/require"
+ "go.uber.org/mock/gomock"
+
+ s3mocks "github.com/coinbase/chainstorage/internal/s3/mocks"
+)
+
+const (
+ testWriteOnceBucket = "co-chainstorage-solana-mainnet-prod"
+ testWriteOnceKey = "BLOCKCHAIN_SOLANA/NETWORK_SOLANA_MAINNET/consolidated/v=7/object.cscb.zstd"
+)
+
+func TestS3ObjectStore_InspectObjectRetentionSafety(t *testing.T) {
+ controller := gomock.NewController(t)
+ client := s3mocks.NewMockClient(controller)
+ store := NewS3ObjectStore(client)
+ policy := validWriteOncePolicy(`{"AWS":"*"}`)
+ client.EXPECT().GetBucketPolicy(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, input *awss3.GetBucketPolicyInput, options ...func(*awss3.Options)) (*awss3.GetBucketPolicyOutput, error) {
+ require.Equal(t, testWriteOnceBucket, aws.ToString(input.Bucket))
+ return &awss3.GetBucketPolicyOutput{Policy: aws.String(url.QueryEscape(policy))}, nil
+ },
+ )
+ client.EXPECT().GetBucketLifecycleConfiguration(gomock.Any(), gomock.Any()).DoAndReturn(
+ func(ctx context.Context, input *awss3.GetBucketLifecycleConfigurationInput, options ...func(*awss3.Options)) (*awss3.GetBucketLifecycleConfigurationOutput, error) {
+ require.Equal(t, testWriteOnceBucket, aws.ToString(input.Bucket))
+ return nil, &smithy.GenericAPIError{Code: "NoSuchLifecycleConfiguration"}
+ },
+ )
+ snapshot, err := store.InspectObjectRetentionSafety(context.Background(), testWriteOnceBucket, testWriteOnceKey)
+ require.NoError(t, err)
+ require.True(t, isSHA256Hex(snapshot.ConfigurationSHA256))
+}
+
+func TestS3ObjectStore_InspectObjectRetentionSafetyRequiresAllControlPlaneProtection(t *testing.T) {
+ tests := []struct {
+ name string
+ statements []string
+ }{
+ {
+ name: "missing delete protection",
+ statements: []string{validConditionalWriteStatement(`"*"`), validLifecycleConfigurationProtectionStatement(`"*"`)},
+ },
+ {
+ name: "missing conditional write protection",
+ statements: []string{validMutationProtectionStatement(`"*"`), validLifecycleConfigurationProtectionStatement(`"*"`)},
+ },
+ {
+ name: "missing lifecycle configuration protection",
+ statements: []string{validConditionalWriteStatement(`"*"`), validMutationProtectionStatement(`"*"`)},
+ },
+ }
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ controller := gomock.NewController(t)
+ client := s3mocks.NewMockClient(controller)
+ store := NewS3ObjectStore(client)
+ policy := fmt.Sprintf(`{"Version":"2012-10-17","Statement":[%s]}`, strings.Join(test.statements, ","))
+ client.EXPECT().GetBucketPolicy(gomock.Any(), gomock.Any()).Return(
+ &awss3.GetBucketPolicyOutput{Policy: aws.String(policy)},
+ nil,
+ )
+ client.EXPECT().GetBucketLifecycleConfiguration(gomock.Any(), gomock.Any()).Return(
+ nil,
+ &smithy.GenericAPIError{Code: "NoSuchLifecycleConfiguration"},
+ )
+ _, err := store.InspectObjectRetentionSafety(context.Background(), testWriteOnceBucket, testWriteOnceKey)
+ require.Error(t, err)
+ })
+ }
+}
+
+func TestS3ObjectStore_InspectObjectRetentionSafetyRejectsMatchingLifecycleActions(t *testing.T) {
+ tests := []struct {
+ name string
+ rules []awss3types.LifecycleRule
+ valid bool
+ }{
+ {
+ name: "abort incomplete multipart upload only",
+ rules: []awss3types.LifecycleRule{{
+ ID: aws.String("abort-multipart"), Status: awss3types.ExpirationStatusEnabled,
+ AbortIncompleteMultipartUpload: &awss3types.AbortIncompleteMultipartUpload{DaysAfterInitiation: aws.Int32(7)},
+ }},
+ valid: true,
+ },
+ {
+ name: "disabled expiration",
+ rules: []awss3types.LifecycleRule{{
+ ID: aws.String("disabled"), Status: awss3types.ExpirationStatusDisabled,
+ Expiration: &awss3types.LifecycleExpiration{Days: aws.Int32(1)},
+ }},
+ valid: true,
+ },
+ {
+ name: "expiration for unrelated prefix",
+ rules: []awss3types.LifecycleRule{{
+ ID: aws.String("single-block-only"), Status: awss3types.ExpirationStatusEnabled,
+ Filter: &awss3types.LifecycleRuleFilter{Prefix: aws.String("single-block/")},
+ Expiration: &awss3types.LifecycleExpiration{Days: aws.Int32(1)},
+ }},
+ valid: true,
+ },
+ {
+ name: "current expiration",
+ rules: []awss3types.LifecycleRule{{
+ ID: aws.String("expire-all"), Status: awss3types.ExpirationStatusEnabled,
+ Expiration: &awss3types.LifecycleExpiration{Days: aws.Int32(1)},
+ }},
+ },
+ {
+ name: "current transition",
+ rules: []awss3types.LifecycleRule{{
+ ID: aws.String("archive-consolidated"), Status: awss3types.ExpirationStatusEnabled,
+ Filter: &awss3types.LifecycleRuleFilter{And: &awss3types.LifecycleRuleAndOperator{Prefix: aws.String("BLOCKCHAIN_SOLANA/")}},
+ Transitions: []awss3types.Transition{{Days: aws.Int32(1), StorageClass: awss3types.TransitionStorageClassGlacier}},
+ }},
+ },
+ {
+ name: "noncurrent expiration",
+ rules: []awss3types.LifecycleRule{{
+ ID: aws.String("expire-noncurrent"), Status: awss3types.ExpirationStatusEnabled,
+ NoncurrentVersionExpiration: &awss3types.NoncurrentVersionExpiration{NoncurrentDays: aws.Int32(1)},
+ }},
+ },
+ {
+ name: "noncurrent transition with tag-only filter is conservatively unsafe",
+ rules: []awss3types.LifecycleRule{{
+ ID: aws.String("tag-filtered"), Status: awss3types.ExpirationStatusEnabled,
+ Filter: &awss3types.LifecycleRuleFilter{Tag: &awss3types.Tag{Key: aws.String("archive"), Value: aws.String("true")}},
+ NoncurrentVersionTransitions: []awss3types.NoncurrentVersionTransition{{NoncurrentDays: aws.Int32(1), StorageClass: awss3types.TransitionStorageClassDeepArchive}},
+ }},
+ },
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ controller := gomock.NewController(t)
+ client := s3mocks.NewMockClient(controller)
+ store := NewS3ObjectStore(client)
+ client.EXPECT().GetBucketPolicy(gomock.Any(), gomock.Any()).Return(
+ &awss3.GetBucketPolicyOutput{Policy: aws.String(validWriteOncePolicy(`"*"`))}, nil,
+ )
+ client.EXPECT().GetBucketLifecycleConfiguration(gomock.Any(), gomock.Any()).Return(
+ &awss3.GetBucketLifecycleConfigurationOutput{Rules: test.rules}, nil,
+ )
+
+ _, err := store.InspectObjectRetentionSafety(context.Background(), testWriteOnceBucket, testWriteOnceKey)
+ if test.valid {
+ require.NoError(t, err)
+ } else {
+ require.Error(t, err)
+ }
+ })
+ }
+}
+
+func TestRetentionSafetySnapshotTracksControlPlaneConfiguration(t *testing.T) {
+ policy, err := decodeBucketPolicy(validWriteOncePolicy(`"*"`))
+ require.NoError(t, err)
+ firstRules := []awss3types.LifecycleRule{{
+ ID: aws.String("abort-multipart"),
+ Status: awss3types.ExpirationStatusEnabled,
+ AbortIncompleteMultipartUpload: &awss3types.AbortIncompleteMultipartUpload{
+ DaysAfterInitiation: aws.Int32(7),
+ },
+ }}
+
+ first, err := retentionSafetySnapshot(policy, firstRules)
+ require.NoError(t, err)
+ repeated, err := retentionSafetySnapshot(policy, firstRules)
+ require.NoError(t, err)
+ require.Equal(t, first.ConfigurationSHA256, repeated.ConfigurationSHA256)
+
+ changedRules := append([]awss3types.LifecycleRule(nil), firstRules...)
+ changedRules[0].AbortIncompleteMultipartUpload = &awss3types.AbortIncompleteMultipartUpload{
+ DaysAfterInitiation: aws.Int32(14),
+ }
+ changed, err := retentionSafetySnapshot(policy, changedRules)
+ require.NoError(t, err)
+ require.NotEqual(t, first.ConfigurationSHA256, changed.ConfigurationSHA256)
+}
+
+func TestStatementEnforcesCSCBWriteOnce(t *testing.T) {
+ objectARN := "arn:aws:s3:::" + testWriteOnceBucket + "/" + testWriteOnceKey
+ valid := validConditionalWriteStatement(`"*"`)
+ tests := []struct {
+ name string
+ policy string
+ valid bool
+ }{
+ {name: "valid all principal", policy: valid, valid: true},
+ {name: "valid terraform principal", policy: validConditionalWriteStatement(`{"AWS":["*"]}`), valid: true},
+ {name: "valid multipart exemption", policy: replacePolicy(valid, `"Null":{"s3:if-none-match":"true"}`, `"Null":{"s3:if-none-match":"true"},"Bool":{"s3:ObjectCreationOperation":"true"}`), valid: true},
+ {name: "allow is insufficient", policy: replacePolicy(valid, `"Effect":"Deny"`, `"Effect":"Allow"`)},
+ {name: "restricted principal", policy: replacePolicy(valid, `"Principal":"*"`, `"Principal":{"AWS":"arn:aws:iam::992382740726:role/writer"}`)},
+ {name: "wrong action", policy: replacePolicy(valid, `"Action":"s3:PutObject"`, `"Action":"s3:GetObject"`)},
+ {name: "wrong resource", policy: replacePolicy(valid, `/*/consolidated/*`, `/single-block/*`)},
+ {name: "missing condition", policy: replacePolicy(valid, `,"Condition":{"Null":{"s3:if-none-match":"true"}}`, ``)},
+ {name: "condition false", policy: replacePolicy(valid, `"s3:if-none-match":"true"`, `"s3:if-none-match":"false"`)},
+ {name: "narrowing extra condition", policy: replacePolicy(valid, `"Null":{"s3:if-none-match":"true"}`, `"Null":{"s3:if-none-match":"true"},"Bool":{"aws:SecureTransport":"true"}`)},
+ {name: "not principal", policy: replacePolicy(valid, `"Principal":"*"`, `"Principal":"*","NotPrincipal":{"AWS":"arn:aws:iam::992382740726:role/writer"}`)},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var statement bucketPolicyStatement
+ err := json.Unmarshal([]byte(test.policy), &statement)
+ require.NoError(t, err)
+ require.Equal(t, test.valid, statementEnforcesCSCBWriteOnce(statement, objectARN))
+ })
+ }
+}
+
+func TestStatementEnforcesCSCBMutationProtection(t *testing.T) {
+ objectARN := "arn:aws:s3:::" + testWriteOnceBucket + "/" + testWriteOnceKey
+ valid := validMutationProtectionStatement(`"*"`)
+ tests := []struct {
+ name string
+ policy string
+ valid bool
+ }{
+ {name: "valid all principal", policy: valid, valid: true},
+ {name: "valid terraform principal", policy: validMutationProtectionStatement(`{"AWS":["*"]}`), valid: true},
+ {name: "allow is insufficient", policy: replacePolicy(valid, `"Effect":"Deny"`, `"Effect":"Allow"`)},
+ {name: "restricted principal", policy: replacePolicy(valid, `"Principal":"*"`, `"Principal":{"AWS":"arn:aws:iam::992382740726:role/writer"}`)},
+ {name: "missing delete object", policy: replacePolicy(valid, `"s3:DeleteObject",`, ``)},
+ {name: "missing delete version", policy: replacePolicy(valid, `,"s3:DeleteObjectVersion"`, ``)},
+ {name: "missing replicate object", policy: replacePolicy(valid, `,"s3:ReplicateObject"`, ``)},
+ {name: "missing replicate delete", policy: replacePolicy(valid, `,"s3:ReplicateDelete"`, ``)},
+ {name: "wrong resource", policy: replacePolicy(valid, `/*/consolidated/*`, `/single-block/*`)},
+ {name: "narrowing condition", policy: replacePolicy(valid, `}`, `,"Condition":{"Bool":{"aws:SecureTransport":"true"}}}`)},
+ {name: "not action", policy: replacePolicy(valid, `"Action"`, `"NotAction"`)},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var statement bucketPolicyStatement
+ err := json.Unmarshal([]byte(test.policy), &statement)
+ require.NoError(t, err)
+ require.Equal(t, test.valid, statementEnforcesCSCBMutationProtection(statement, objectARN))
+ })
+ }
+}
+
+func TestStatementEnforcesLifecycleConfigurationProtection(t *testing.T) {
+ bucketARN := "arn:aws:s3:::" + testWriteOnceBucket
+ valid := validLifecycleConfigurationProtectionStatement(`"*"`)
+ tests := []struct {
+ name string
+ policy string
+ valid bool
+ }{
+ {name: "valid all principal", policy: valid, valid: true},
+ {name: "valid terraform principal", policy: validLifecycleConfigurationProtectionStatement(`{"AWS":["*"]}`), valid: true},
+ {name: "allow is insufficient", policy: replacePolicy(valid, `"Effect":"Deny"`, `"Effect":"Allow"`)},
+ {name: "restricted principal", policy: replacePolicy(valid, `"Principal":"*"`, `"Principal":{"AWS":"arn:aws:iam::992382740726:role/admin"}`)},
+ {name: "wrong action", policy: replacePolicy(valid, `s3:PutLifecycleConfiguration`, `s3:GetLifecycleConfiguration`)},
+ {name: "object resource is insufficient", policy: replacePolicy(valid, testWriteOnceBucket+`"`, testWriteOnceBucket+`/*"`)},
+ {name: "narrowing condition", policy: replacePolicy(valid, `}`, `,"Condition":{"Bool":{"aws:SecureTransport":"true"}}}`)},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ var statement bucketPolicyStatement
+ err := json.Unmarshal([]byte(test.policy), &statement)
+ require.NoError(t, err)
+ require.Equal(t, test.valid, statementEnforcesLifecycleConfigurationProtection(statement, bucketARN))
+ })
+ }
+}
+
+func validWriteOncePolicy(principal string) string {
+ return fmt.Sprintf(
+ `{"Version":"2012-10-17","Statement":[%s,%s,%s]}`,
+ validConditionalWriteStatement(principal),
+ validMutationProtectionStatement(principal),
+ validLifecycleConfigurationProtectionStatement(principal),
+ )
+}
+
+func validConditionalWriteStatement(principal string) string {
+ return fmt.Sprintf(`{"Effect":"Deny","Principal":%s,"Action":"s3:PutObject","Resource":"arn:aws:s3:::%s/*/consolidated/*","Condition":{"Null":{"s3:if-none-match":"true"}}}`, principal, testWriteOnceBucket)
+}
+
+func validMutationProtectionStatement(principal string) string {
+ return fmt.Sprintf(`{"Effect":"Deny","Principal":%s,"Action":["s3:DeleteObject","s3:DeleteObjectVersion","s3:ReplicateObject","s3:ReplicateDelete"],"Resource":"arn:aws:s3:::%s/*/consolidated/*"}`, principal, testWriteOnceBucket)
+}
+
+func validLifecycleConfigurationProtectionStatement(principal string) string {
+ return fmt.Sprintf(`{"Effect":"Deny","Principal":%s,"Action":"s3:PutLifecycleConfiguration","Resource":"arn:aws:s3:::%s"}`, principal, testWriteOnceBucket)
+}
+
+func replacePolicy(policy string, old string, replacement string) string {
+ return strings.Replace(policy, old, replacement, 1)
+}
diff --git a/internal/storage/retirementlock/lock.go b/internal/storage/retirementlock/lock.go
new file mode 100644
index 00000000..f3c7d5de
--- /dev/null
+++ b/internal/storage/retirementlock/lock.go
@@ -0,0 +1,17 @@
+package retirementlock
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+)
+
+// Acquire serializes single-block object uploads with retirement preparation even
+// before block metadata exists. Hash collisions only cause safe over-serialization.
+func Acquire(ctx context.Context, tx *sql.Tx, tag uint32, height uint64, hash string) error {
+ key := fmt.Sprintf("%d\x1f%d\x1f%s", tag, height, hash)
+ if _, err := tx.ExecContext(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1, 0))`, key); err != nil {
+ return fmt.Errorf("failed to acquire single-block object retirement lock: %w", err)
+ }
+ return nil
+}
diff --git a/internal/storage/utils/compress.go b/internal/storage/utils/compress.go
new file mode 100644
index 00000000..318a0c34
--- /dev/null
+++ b/internal/storage/utils/compress.go
@@ -0,0 +1,164 @@
+package utils
+
+import (
+ "bytes"
+ "compress/gzip"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "path/filepath"
+
+ "github.com/klauspost/compress/zstd"
+ "golang.org/x/xerrors"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+const (
+ GzipFileSuffix = ".gzip"
+ ZstdFileSuffix = ".zstd"
+)
+
+type Compressor interface {
+ Compress(data []byte) ([]byte, error)
+ Decompress(data []byte) ([]byte, error)
+ GetObjectKey(key string) string
+}
+
+func GetCompressionType(fileURL string) api.Compression {
+ ext := filepath.Ext(fileURL)
+ switch ext {
+ case GzipFileSuffix:
+ return api.Compression_GZIP
+ case ZstdFileSuffix:
+ return api.Compression_ZSTD
+ }
+ return api.Compression_NONE
+}
+
+func CompressorFactory(compressionType api.Compression) (Compressor, error) {
+ switch compressionType {
+ case api.Compression_GZIP:
+ return &GzipCompressor{}, nil
+ case api.Compression_ZSTD:
+ return &ZstdCompressor{}, nil
+ default:
+ return nil, errors.New("unsupported compression type")
+ }
+}
+
+// ------ GZIP ------
+type GzipCompressor struct{}
+
+func (g *GzipCompressor) Compress(data []byte) ([]byte, error) {
+ var buf bytes.Buffer
+ writer := gzip.NewWriter(&buf)
+
+ if _, err := writer.Write(data); err != nil {
+ return nil, xerrors.Errorf("failed to write compressed data with gzip: %w", err)
+ }
+ if err := writer.Close(); err != nil {
+ return nil, xerrors.Errorf("failed to close gzip writer: %w", err)
+ }
+
+ return buf.Bytes(), nil
+}
+
+func (g *GzipCompressor) Decompress(data []byte) ([]byte, error) {
+ reader, err := gzip.NewReader(bytes.NewBuffer(data))
+ if err != nil {
+ return nil, xerrors.Errorf("failed to initiate gzip reader: %w", err)
+ }
+ decoded, err := ioutil.ReadAll(reader)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to read data: %w", err)
+ }
+ if err := reader.Close(); err != nil {
+ return nil, xerrors.Errorf("failed to close gzip reader: %w", err)
+ }
+ return decoded, nil
+}
+
+func (g *GzipCompressor) GetObjectKey(key string) string {
+ return fmt.Sprintf("%s%s", key, GzipFileSuffix)
+}
+
+// ------ ZSTD ------
+type ZstdCompressor struct{}
+
+func (z *ZstdCompressor) Compress(data []byte) ([]byte, error) {
+ writer, err := zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedDefault))
+ if err != nil {
+ return nil, xerrors.Errorf("failed to write compressed data with zstd: %w", err)
+ }
+ if err := writer.Close(); err != nil {
+ return nil, xerrors.Errorf("failed to close zstd writer: %w", err)
+ }
+ return writer.EncodeAll(data, nil), nil
+}
+
+func (z *ZstdCompressor) Decompress(data []byte) ([]byte, error) {
+ decoder, err := zstd.NewReader(nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to initiate zstd reader: %w", err)
+ }
+ defer decoder.Close()
+ decoded, err := decoder.DecodeAll(data, nil)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to read data with zstd: %w", err)
+ }
+ return decoded, nil
+}
+
+func (z *ZstdCompressor) GetObjectKey(key string) string {
+ return fmt.Sprintf("%s%s", key, ZstdFileSuffix)
+}
+
+// DecompressReader wraps r in a streaming decompressor for the given
+// compression type. The returned reader must be closed.
+//
+// For api.Compression_NONE, returns an io.NopCloser around r so callers
+// can uniformly defer Close().
+func DecompressReader(r io.Reader, compression api.Compression) (io.ReadCloser, error) {
+ switch compression {
+ case api.Compression_NONE:
+ return io.NopCloser(r), nil
+ case api.Compression_GZIP:
+ gr, err := gzip.NewReader(r)
+ if err != nil {
+ return nil, xerrors.Errorf("gzip reader: %w", err)
+ }
+ return gr, nil
+ case api.Compression_ZSTD:
+ // WithDecoderConcurrency(1): sequential decoding only.
+ // WithDecoderLowmem(true): enable the low-memory decoder path;
+ // the concurrent decoder otherwise pre-decodes blocks ahead of
+ // the reader and retains them in an internal channel, which on
+ // a multi-GB zcash block balloons to several GB of transient
+ // heap even though the caller never asks for that much at
+ // once.
+ zr, err := zstd.NewReader(r,
+ zstd.WithDecoderConcurrency(1),
+ zstd.WithDecoderLowmem(true),
+ zstd.WithDecoderMaxMemory(256*1024*1024),
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("zstd reader: %w", err)
+ }
+ return &zstdReadCloser{zr}, nil
+ default:
+ return nil, xerrors.Errorf("unsupported compression type: %v", compression)
+ }
+}
+
+// zstdReadCloser adapts *zstd.Decoder (whose Close returns nothing) to
+// io.ReadCloser.
+type zstdReadCloser struct {
+ *zstd.Decoder
+}
+
+func (z *zstdReadCloser) Close() error {
+ z.Decoder.Close()
+ return nil
+}
diff --git a/internal/storage/utils/storage_placement.go b/internal/storage/utils/storage_placement.go
new file mode 100644
index 00000000..aeb351dd
--- /dev/null
+++ b/internal/storage/utils/storage_placement.go
@@ -0,0 +1,40 @@
+package utils
+
+import (
+ "google.golang.org/protobuf/proto"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// CloneBlockWithoutStoragePlacement returns a block payload that does not retain
+// the object location from which it was read. Storage placement is authoritative
+// in meta storage and must not be embedded in an immutable consolidated object.
+func CloneBlockWithoutStoragePlacement(block *api.Block) *api.Block {
+ if block == nil {
+ return nil
+ }
+ clone := proto.Clone(block).(*api.Block)
+ if clone.Metadata == nil {
+ return clone
+ }
+ clone.Metadata.ObjectKeyMain = ""
+ clone.Metadata.ByteOffset = 0
+ clone.Metadata.ByteLength = 0
+ clone.Metadata.UncompressedLength = 0
+ clone.Metadata.ObjectFormat = api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK
+ return clone
+}
+
+// HasBlockStoragePlacement reports whether a serialized block payload retains
+// an object key or byte placement that belongs in meta storage.
+func HasBlockStoragePlacement(block *api.Block) bool {
+ if block == nil || block.Metadata == nil {
+ return false
+ }
+ metadata := block.Metadata
+ return metadata.ObjectKeyMain != "" ||
+ metadata.ByteOffset != 0 ||
+ metadata.ByteLength != 0 ||
+ metadata.UncompressedLength != 0 ||
+ metadata.ObjectFormat != api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK
+}
diff --git a/internal/storage/utils/storage_placement_test.go b/internal/storage/utils/storage_placement_test.go
new file mode 100644
index 00000000..5a0f24a9
--- /dev/null
+++ b/internal/storage/utils/storage_placement_test.go
@@ -0,0 +1,26 @@
+package utils
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestCloneBlockWithoutStoragePlacement(t *testing.T) {
+ block := &api.Block{Metadata: &api.BlockMetadata{
+ ObjectKeyMain: "single-block/block.gzip",
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 10,
+ ByteLength: 20,
+ UncompressedLength: 30,
+ Hash: "hash",
+ }}
+
+ require.True(t, HasBlockStoragePlacement(block))
+ clone := CloneBlockWithoutStoragePlacement(block)
+ require.False(t, HasBlockStoragePlacement(clone))
+ require.Equal(t, "hash", clone.Metadata.Hash)
+ require.Equal(t, "single-block/block.gzip", block.Metadata.ObjectKeyMain)
+}
diff --git a/internal/storage/utils/utils.go b/internal/storage/utils/utils.go
index fd1a765e..e14fe2b7 100644
--- a/internal/storage/utils/utils.go
+++ b/internal/storage/utils/utils.go
@@ -1,81 +1,49 @@
package utils
import (
- "bytes"
- "compress/gzip"
- "fmt"
- "io/ioutil"
- "strings"
-
"golang.org/x/xerrors"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
)
-const (
- GzipFileSuffix = ".gzip"
-)
-
-func GetCompressionType(fileURL string) api.Compression {
- if strings.HasSuffix(fileURL, GzipFileSuffix) {
- return api.Compression_GZIP
- }
- return api.Compression_NONE
-}
-
func Compress(data []byte, compression api.Compression) ([]byte, error) {
if compression == api.Compression_NONE {
return data, nil
}
- if compression == api.Compression_GZIP {
- var buf bytes.Buffer
- zw := gzip.NewWriter(&buf)
- if _, err := zw.Write(data); err != nil {
- return nil, xerrors.Errorf("failed to write compressed data: %w", err)
- }
- if err := zw.Close(); err != nil {
- return nil, xerrors.Errorf("failed to close writer: %w", err)
- }
-
- return buf.Bytes(), nil
+ compressor, err := CompressorFactory(compression)
+ if err != nil {
+ return nil, err
}
-
- return nil, xerrors.Errorf("failed to compress with unsupported type %v", compression.String())
+ coded, err := compressor.Compress(data)
+ if err != nil {
+ return nil, err
+ }
+ return coded, nil
}
func Decompress(data []byte, compression api.Compression) ([]byte, error) {
if compression == api.Compression_NONE {
return data, nil
}
-
- if compression == api.Compression_GZIP {
- zr, err := gzip.NewReader(bytes.NewBuffer(data))
- if err != nil {
- return nil, xerrors.Errorf("failed to initiate reader: %w", err)
- }
- decoded, err := ioutil.ReadAll(zr)
- if err != nil {
- return nil, xerrors.Errorf("failed to read data: %w", err)
- }
- if err := zr.Close(); err != nil {
- return nil, xerrors.Errorf("failed to close reader: %w", err)
- }
- return decoded, nil
+ compressor, err := CompressorFactory(compression)
+ if err != nil {
+ return nil, err
}
-
- return nil, xerrors.Errorf("failed to decompress with unsupported type %v", compression.String())
+ decoded, err := compressor.Decompress(data)
+ if err != nil {
+ return nil, err
+ }
+ return decoded, nil
}
func GetObjectKey(key string, compression api.Compression) (string, error) {
if compression == api.Compression_NONE {
return key, nil
}
-
- if compression == api.Compression_GZIP {
- key = fmt.Sprintf("%s%s", key, GzipFileSuffix)
- return key, nil
+ compressor, err := CompressorFactory(compression)
+ if err != nil {
+ return "", xerrors.Errorf("failed to Get Object Key with: %w", err)
}
-
- return "", xerrors.Errorf("failed to get object key with unsupported type %v", compression.String())
+ return compressor.GetObjectKey(key), nil
}
diff --git a/internal/storage/utils/utils_test.go b/internal/storage/utils/utils_test.go
index e626d114..0ada79e2 100644
--- a/internal/storage/utils/utils_test.go
+++ b/internal/storage/utils/utils_test.go
@@ -1,6 +1,7 @@
package utils
import (
+ "bytes"
"testing"
"github.com/coinbase/chainstorage/internal/utils/testutil"
@@ -24,6 +25,14 @@ func TestGetCompressionType(t *testing.T) {
fileURL: "a.gzip",
compression: api.Compression_GZIP,
},
+ {
+ fileURL: "bzstd",
+ compression: api.Compression_NONE,
+ },
+ {
+ fileURL: "b.zstd",
+ compression: api.Compression_ZSTD,
+ },
}
for _, test := range tests {
t.Run(test.fileURL, func(t *testing.T) {
@@ -54,6 +63,20 @@ func TestCompress(t *testing.T) {
}`),
api.Compression_GZIP,
},
+ {
+ "emptyData",
+ []byte{},
+ api.Compression_ZSTD,
+ },
+ {
+ "blockDataCompression",
+ []byte(`
+ {
+ "hash": "0xbaa42c",
+ "number": "0xacc290",
+ }`),
+ api.Compression_ZSTD,
+ },
{
"blockData",
[]byte(`
@@ -73,7 +96,7 @@ func TestCompress(t *testing.T) {
decompressed, err := Decompress(compressed, test.compression)
require.NoError(err)
- require.Equal(decompressed, test.data)
+ require.True(bytes.Equal(decompressed, test.data))
})
}
}
@@ -94,6 +117,11 @@ func TestGetObjectKey(t *testing.T) {
api.Compression_NONE,
"key2",
},
+ {
+ "key3",
+ api.Compression_ZSTD,
+ "key3.zstd",
+ },
}
for _, test := range tests {
t.Run(test.key, func(t *testing.T) {
diff --git a/internal/tally/prometheus_reporter.go b/internal/tally/prometheus_reporter.go
new file mode 100644
index 00000000..4cb78a4e
--- /dev/null
+++ b/internal/tally/prometheus_reporter.go
@@ -0,0 +1,470 @@
+package tally
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "maps"
+ "math"
+ "net/http"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/prometheus/client_golang/prometheus"
+ "github.com/prometheus/client_golang/prometheus/collectors"
+ "github.com/prometheus/client_golang/prometheus/promhttp"
+ "github.com/uber-go/tally/v4"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+
+ "github.com/coinbase/chainstorage/internal/config"
+)
+
+var defaultBuckets = []float64{
+ 0.1, 0.2, 0.3, 0.5, 0.7, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10,
+ 12, 15, 20, 25, 30, 40, 50, 75, 100, 200, 300, 500, 750,
+ 1000, 2000, 3000, 5000, 7000, 10000,
+}
+
+type prometheusReporter struct {
+ stats *prometheusStats
+}
+
+func newPrometheusReporter(
+ cfg *config.PrometheusConfig,
+ lifecycle fx.Lifecycle,
+ logger *zap.Logger,
+) tally.StatsReporter {
+ opts := []prometheusStatsOption{}
+ if cfg.Namespace != "" {
+ opts = append(opts, withPrometheusNamespace(cfg.Namespace))
+ }
+ if len(cfg.GlobalLabels) > 0 {
+ opts = append(opts, withPrometheusLabels(cfg.GlobalLabels))
+ }
+ if len(cfg.DefaultHistogramBuckets) > 0 {
+ opts = append(opts, withDefaultPrometheusHistogramBuckets(defaultBuckets))
+ }
+ if len(cfg.HistogramBuckets) > 0 {
+ opts = append(opts, withPrometheusHistogramBuckets(cfg.HistogramBuckets))
+ }
+
+ s := newPrometheusStats(logger, opts...)
+
+ mux := http.NewServeMux()
+
+ metricsPath := "/metrics"
+ if cfg.MetricsPath != "" {
+ metricsPath = cfg.MetricsPath
+ }
+ mux.Handle(metricsPath, s.MetricsHandler())
+
+ addr := fmt.Sprintf(":%d", cfg.Port)
+ srv := &http.Server{
+ Addr: addr,
+ Handler: mux,
+ }
+
+ lifecycle.Append(fx.Hook{
+ OnStart: func(ctx context.Context) error {
+ logger.Info("prometheus metrics server starting", zap.String("address", addr))
+
+ go func() {
+ if err := srv.ListenAndServe(); err != nil {
+ if err != http.ErrServerClosed {
+ logger.Error("prometheus metrics server failed to start", zap.Error(err))
+ }
+ }
+ }()
+
+ return nil
+ },
+ OnStop: func(ctx context.Context) error {
+ logger.Info("prometheus metrics server stopping", zap.String("address", addr))
+ return srv.Shutdown(ctx)
+ },
+ })
+
+ return &prometheusReporter{
+ stats: s,
+ }
+}
+
+func (p *prometheusReporter) labels() prometheus.Labels {
+ return p.stats.Labels()
+}
+
+func (p *prometheusReporter) Capabilities() tally.Capabilities {
+ return p
+}
+
+func (p *prometheusReporter) Reporting() bool {
+ return true
+}
+
+func (p *prometheusReporter) Tagging() bool {
+ return true
+}
+
+func (p *prometheusReporter) Flush() {
+ // no-op
+}
+
+func (p *prometheusReporter) ReportCounter(name string, tags map[string]string, value int64) {
+ p.stats.Count(name, value, p.tags(tags))
+}
+
+func (p *prometheusReporter) ReportGauge(name string, tags map[string]string, value float64) {
+ p.stats.Gauge(name, value, p.tags(tags))
+}
+
+func (p *prometheusReporter) ReportHistogramDurationSamples(
+ name string,
+ tags map[string]string,
+ buckets tally.Buckets,
+ bucketLowerBound time.Duration,
+ bucketUpperBound time.Duration,
+ samples int64,
+) {
+ panic("unimplemented")
+}
+
+func (p *prometheusReporter) ReportHistogramValueSamples(
+ name string,
+ tags map[string]string,
+ buckets tally.Buckets,
+ bucketLowerBound float64,
+ bucketUpperBound float64,
+ samples int64,
+) {
+ panic("unimplemented")
+}
+
+func (p *prometheusReporter) ReportTimer(
+ name string,
+ tags map[string]string,
+ interval time.Duration,
+) {
+ p.stats.Timing(name, interval, p.tags(tags))
+}
+
+func (p *prometheusReporter) tags(tags map[string]string) map[string]string {
+ if len(tags) == 0 {
+ return p.labels()
+ }
+
+ m := make(map[string]string)
+ maps.Copy(m, p.labels())
+ maps.Copy(m, tags)
+
+ return m
+}
+
+type prometheusStats struct {
+ mux sync.RWMutex
+ logger *zap.Logger
+
+ counters map[string]*prometheus.CounterVec
+ gauges map[string]*prometheus.GaugeVec
+ histograms map[string]*prometheus.HistogramVec
+ histogramBuckets map[string][]float64
+ defaultBuckets []float64
+ reg *prometheus.Registry
+
+ globalLabels prometheus.Labels
+ namespace string
+}
+
+type prometheusStatsOption func(*prometheusStats)
+
+func withPrometheusNamespace(namespace string) prometheusStatsOption {
+ return func(s *prometheusStats) {
+ s.namespace = namespace
+ }
+}
+
+func withPrometheusLabels(labels map[string]string) prometheusStatsOption {
+ return func(s *prometheusStats) {
+ for k, v := range labels {
+ s.globalLabels[k] = v
+ }
+ }
+}
+
+func withPrometheusHistogramBuckets(buckets map[string][]float64) prometheusStatsOption {
+ return func(s *prometheusStats) {
+ for k, v := range buckets {
+ s.histogramBuckets[k] = v
+ }
+ }
+}
+
+func withDefaultPrometheusHistogramBuckets(buckets []float64) prometheusStatsOption {
+ return func(s *prometheusStats) {
+ s.defaultBuckets = buckets
+ }
+}
+
+func newPrometheusStats(logger *zap.Logger, opts ...prometheusStatsOption) *prometheusStats {
+ s := &prometheusStats{
+ logger: logger,
+ counters: make(map[string]*prometheus.CounterVec),
+ gauges: make(map[string]*prometheus.GaugeVec),
+ histograms: make(map[string]*prometheus.HistogramVec),
+ histogramBuckets: make(map[string][]float64),
+ defaultBuckets: defaultBuckets,
+ globalLabels: make(prometheus.Labels),
+ namespace: "",
+ reg: prometheus.NewRegistry(),
+ }
+
+ // Add go runtime metrics and process collectors.
+ s.reg.MustRegister(
+ collectors.NewGoCollector(),
+ collectors.NewProcessCollector(collectors.ProcessCollectorOpts{}),
+ )
+
+ for _, opt := range opts {
+ opt(s)
+ }
+
+ return s
+}
+
+func (c *prometheusStats) Labels() prometheus.Labels {
+ return c.globalLabels
+}
+
+func (c *prometheusStats) MetricsHandler() http.Handler {
+ return promhttp.HandlerFor(c.reg, promhttp.HandlerOpts{Registry: c.reg})
+}
+
+func (c *prometheusStats) Count(key string, n interface{}, tags map[string]string) {
+ v, err := toFloat64(n)
+ if err != nil {
+ return
+ }
+
+ op, err := c.loadCount(key, tags)
+ if err != nil {
+ c.logger.Warn("prometheus.count.error", zap.Error(err))
+ return
+ }
+ op.With(labels(tags)).Add(v)
+}
+
+func (c *prometheusStats) Inc(key string, tags map[string]string) {
+ op, err := c.loadGauge(key, tags)
+ if err != nil {
+ c.logger.Warn("prometheus.inc.error", zap.Error(err))
+ return
+ }
+ op.With(labels(tags)).Inc()
+}
+
+func (c *prometheusStats) Dec(key string, tags map[string]string) {
+ op, err := c.loadGauge(key, tags)
+ if err != nil {
+ c.logger.Warn("prometheus.dec.error", zap.Error(err))
+ return
+ }
+ op.With(labels(tags)).Dec()
+}
+
+func (c *prometheusStats) Gauge(key string, n interface{}, tags map[string]string) {
+ v, err := toFloat64(n)
+ if err != nil {
+ return
+ }
+
+ op, err := c.loadGauge(key, tags)
+ if err != nil {
+ c.logger.Warn("prometheus.gauge.error", zap.Error(err))
+ return
+ }
+ op.With(labels(tags)).Set(v)
+}
+
+func (c *prometheusStats) Histogram(key string, n interface{}, tags map[string]string) {
+ v, err := toFloat64(n)
+ if err != nil {
+ return
+ }
+
+ op, err := c.loadHistogram(key, tags)
+ if err != nil {
+ c.logger.Warn("prometheus.histogram.error", zap.Error(err))
+ return
+ }
+ op.With(labels(tags)).Observe(v)
+}
+
+func (c *prometheusStats) Timing(key string, t time.Duration, tags map[string]string) {
+ op, err := c.loadHistogram(key, tags)
+ if err != nil {
+ c.logger.Warn("prometheus.timing.error", zap.Error(err))
+ return
+ }
+
+ op.With(labels(tags)).Observe(float64(t) / float64(time.Millisecond))
+}
+
+func (c *prometheusStats) loadGauge(key string, tags map[string]string) (*prometheus.GaugeVec, error) {
+ key = c.key(key)
+ id, labelNames := labelKey(key, tags)
+
+ c.mux.RLock()
+ gauge, ok := c.gauges[id]
+ c.mux.RUnlock()
+ if ok {
+ return gauge, nil
+ }
+
+ c.mux.Lock()
+ gauge, err := registerMetric(c.reg, prometheus.NewGaugeVec(prometheus.GaugeOpts{
+ Namespace: c.namespace,
+ Name: key,
+ ConstLabels: c.globalLabels,
+ }, labelNames))
+ if err != nil {
+ c.mux.Unlock()
+ return nil, err
+ }
+ c.gauges[id] = gauge
+ c.mux.Unlock()
+
+ return gauge, nil
+}
+
+func (c *prometheusStats) loadCount(key string, tags map[string]string) (*prometheus.CounterVec, error) {
+ key = c.key(key)
+ id, labelNames := labelKey(key, tags)
+
+ c.mux.RLock()
+ counter, ok := c.counters[id]
+ c.mux.RUnlock()
+ if ok {
+ return counter, nil
+ }
+
+ c.mux.Lock()
+ counter, err := registerMetric(c.reg, prometheus.NewCounterVec(prometheus.CounterOpts{
+ Namespace: c.namespace,
+ Name: key,
+ ConstLabels: c.globalLabels,
+ }, labelNames))
+ if err != nil {
+ c.mux.Unlock()
+ return nil, err
+ }
+ c.counters[id] = counter
+ c.mux.Unlock()
+
+ return counter, nil
+}
+
+func labelKey(key string, tags map[string]string) (id string, labelNames []string) {
+ for k := range labels(tags) {
+ labelNames = append(labelNames, k)
+ }
+
+ sort.Strings(labelNames)
+ newKey := strings.Join(append([]string{key}, labelNames...), ".")
+
+ return newKey, labelNames
+}
+
+func (c *prometheusStats) loadHistogram(key string, tags map[string]string) (*prometheus.HistogramVec, error) {
+ key = c.key(key)
+ id, labelNames := labelKey(key, tags)
+
+ c.mux.RLock()
+ histogram, registered := c.histograms[id]
+ histogramBuckets, hasBuckets := c.histogramBuckets[key]
+ c.mux.RUnlock()
+
+ if registered {
+ return histogram, nil
+ }
+
+ if !hasBuckets {
+ histogramBuckets = defaultBuckets
+ }
+
+ c.mux.Lock()
+ histogram, err := registerMetric(c.reg, prometheus.NewHistogramVec(prometheus.HistogramOpts{
+ Namespace: c.namespace,
+ Name: key,
+ ConstLabels: c.globalLabels,
+ Buckets: histogramBuckets,
+ }, labelNames))
+ if err != nil {
+ c.mux.Unlock()
+ return nil, err
+ }
+ c.histograms[id] = histogram
+ c.mux.Unlock()
+
+ return histogram, nil
+}
+
+func (c *prometheusStats) key(key string) string {
+ return strings.ReplaceAll(key, ".", "_")
+}
+
+func labels(tags map[string]string) prometheus.Labels {
+ if len(tags) > 0 {
+ return prometheus.Labels(tags)
+ }
+ return prometheus.Labels{}
+}
+
+func registerMetric[T prometheus.Collector](
+ reg prometheus.Registerer,
+ metric T,
+) (T, error) {
+ var err error
+ if reg != nil {
+ err = reg.Register(metric)
+ } else {
+ err = prometheus.Register(metric)
+ }
+ if err != nil {
+ if are, ok := err.(prometheus.AlreadyRegisteredError); ok {
+ existing, ok := are.ExistingCollector.(T)
+ if !ok {
+ return metric, fmt.Errorf("metric with different type already exists")
+ }
+
+ return existing, nil
+ }
+ }
+
+ return metric, err
+}
+
+func toFloat64(n interface{}) (float64, error) {
+ var v float64
+ switch n := n.(type) {
+ case float64:
+ v = n
+ case float32:
+ v = float64(n)
+ case int:
+ v = float64(n)
+ case int8:
+ v = float64(n)
+ case int16:
+ v = float64(n)
+ case int32:
+ v = float64(n)
+ case int64:
+ v = float64(n)
+ default:
+ // NaN
+ return math.NaN(), errors.New("failed to convert value to float64")
+ }
+ return v, nil
+}
diff --git a/internal/tally/stats_reporter.go b/internal/tally/stats_reporter.go
index 734d2a21..73bc3f2b 100644
--- a/internal/tally/stats_reporter.go
+++ b/internal/tally/stats_reporter.go
@@ -1,10 +1,6 @@
package tally
import (
- "context"
- "time"
-
- smirastatsd "github.com/smira/go-statsd"
"github.com/uber-go/tally/v4"
"go.uber.org/fx"
"go.uber.org/zap"
@@ -19,97 +15,15 @@ type (
Logger *zap.Logger
Config *config.Config
}
-
- reporter struct {
- client *smirastatsd.Client
- }
-)
-
-const (
- reportingInterval = time.Second
-)
-
-var (
- // hardcoding this to be datadog format
- // we need think about whats the best way to set it up in config such that
- // when we switch reporter impl, config will still be backward compatible
- tagFormat = smirastatsd.TagFormatDatadog
)
func NewStatsReporter(params StatsReporterParams) tally.StatsReporter {
- if params.Config.StatsD == nil {
+ switch {
+ case params.Config.StatsD != nil:
+ return newStatsDReporter(params.Config.StatsD, params.Lifecycle, params.Logger)
+ case params.Config.Prometheus != nil:
+ return newPrometheusReporter(params.Config.Prometheus, params.Lifecycle, params.Logger)
+ default:
return tally.NullStatsReporter
}
- cfg := params.Config.StatsD
- client := smirastatsd.NewClient(
- cfg.Address,
- smirastatsd.MetricPrefix(cfg.Prefix),
- smirastatsd.TagStyle(tagFormat),
- smirastatsd.ReportInterval(reportingInterval),
- )
- params.Logger.Info("initialized statsd client")
- params.Lifecycle.Append(fx.Hook{
- OnStop: func(ctx context.Context) error {
- return client.Close()
- },
- })
- return &reporter{
- client: client,
- }
-}
-
-func convertTags(tagsMap map[string]string) []smirastatsd.Tag {
- tags := make([]smirastatsd.Tag, 0, len(tagsMap))
- for key, value := range tagsMap {
- tags = append(tags, smirastatsd.StringTag(key, value))
- }
- return tags
-}
-
-func (r *reporter) ReportCounter(name string, tags map[string]string, value int64) {
- r.client.Incr(name, value, convertTags(tags)...)
-}
-
-func (r *reporter) ReportGauge(name string, tags map[string]string, value float64) {
- r.client.FGauge(name, value, convertTags(tags)...)
-}
-
-func (r *reporter) ReportTimer(name string, tags map[string]string, value time.Duration) {
- r.client.PrecisionTiming(name, value, convertTags(tags)...)
-}
-
-func (r *reporter) ReportHistogramValueSamples(
- name string,
- tags map[string]string,
- buckets tally.Buckets,
- bucketLowerBound,
- bucketUpperBound float64,
- samples int64) {
- panic("no implemented")
-}
-
-func (r *reporter) ReportHistogramDurationSamples(
- name string,
- tags map[string]string,
- buckets tally.Buckets,
- bucketLowerBound,
- bucketUpperBound time.Duration,
- samples int64) {
- panic("no implemented")
-}
-
-func (r *reporter) Capabilities() tally.Capabilities {
- return r
-}
-
-func (r *reporter) Reporting() bool {
- return true
-}
-
-func (r *reporter) Tagging() bool {
- return true
-}
-
-func (r *reporter) Flush() {
- // no-op
}
diff --git a/internal/tally/stats_reporter_test.go b/internal/tally/stats_reporter_test.go
index 834a1cfa..cd263200 100644
--- a/internal/tally/stats_reporter_test.go
+++ b/internal/tally/stats_reporter_test.go
@@ -47,3 +47,28 @@ func TestNewReporterDefaultWithStatsD(t *testing.T) {
require.Equal(true, reporter.Capabilities().Tagging())
})
}
+
+func TestNewReporterDefaultWithPrometheus(t *testing.T) {
+ testapp.TestAllConfigs(t, func(t *testing.T, cfg *config.Config) {
+ require := testutil.Require(t)
+ cfg.Prometheus = &config.PrometheusConfig{
+ // use any available port
+ Port: 0,
+ }
+
+ var reporter tally.StatsReporter
+ app := testapp.New(
+ t,
+ testapp.WithConfig(cfg),
+ fx.Provide(NewStatsReporter),
+ fx.Populate(&reporter),
+ )
+
+ // close app after the test so that the port is released
+ t.Cleanup(app.Close)
+
+ require.NotEqual(tally.NullStatsReporter, reporter)
+ require.Equal(true, reporter.Capabilities().Reporting())
+ require.Equal(true, reporter.Capabilities().Tagging())
+ })
+}
diff --git a/internal/tally/statsd_reporter.go b/internal/tally/statsd_reporter.go
new file mode 100644
index 00000000..787d90d8
--- /dev/null
+++ b/internal/tally/statsd_reporter.go
@@ -0,0 +1,103 @@
+package tally
+
+import (
+ "context"
+ "time"
+
+ smirastatsd "github.com/smira/go-statsd"
+ "github.com/uber-go/tally/v4"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+
+ "github.com/coinbase/chainstorage/internal/config"
+)
+
+type statsDReporter struct {
+ client *smirastatsd.Client
+}
+
+func newStatsDReporter(
+ cfg *config.StatsDConfig,
+ lifecycle fx.Lifecycle,
+ logger *zap.Logger,
+) tally.StatsReporter {
+ // hardcoding this to be datadog format
+ // we need think about whats the best way to set it up in config such that
+ // when we switch reporter impl, config will still be backward compatible
+ tagFormat := smirastatsd.TagFormatDatadog
+
+ client := smirastatsd.NewClient(
+ cfg.Address,
+ smirastatsd.MetricPrefix(cfg.Prefix),
+ smirastatsd.TagStyle(tagFormat),
+ smirastatsd.ReportInterval(reportingInterval),
+ )
+ logger.Info("initialized statsd client")
+ lifecycle.Append(fx.Hook{
+ OnStop: func(ctx context.Context) error {
+ return client.Close()
+ },
+ })
+
+ return &statsDReporter{
+ client: client,
+ }
+}
+
+func (r *statsDReporter) convertTags(tagsMap map[string]string) []smirastatsd.Tag {
+ tags := make([]smirastatsd.Tag, 0, len(tagsMap))
+ for key, value := range tagsMap {
+ tags = append(tags, smirastatsd.StringTag(key, value))
+ }
+ return tags
+}
+
+func (r *statsDReporter) ReportCounter(name string, tags map[string]string, value int64) {
+ r.client.Incr(name, value, r.convertTags(tags)...)
+}
+
+func (r *statsDReporter) ReportGauge(name string, tags map[string]string, value float64) {
+ r.client.FGauge(name, value, r.convertTags(tags)...)
+}
+
+func (r *statsDReporter) ReportTimer(name string, tags map[string]string, value time.Duration) {
+ r.client.PrecisionTiming(name, value, r.convertTags(tags)...)
+}
+
+func (r *statsDReporter) ReportHistogramValueSamples(
+ name string,
+ tags map[string]string,
+ buckets tally.Buckets,
+ bucketLowerBound,
+ bucketUpperBound float64,
+ samples int64,
+) {
+ panic("no implemented")
+}
+
+func (r *statsDReporter) ReportHistogramDurationSamples(
+ name string,
+ tags map[string]string,
+ buckets tally.Buckets,
+ bucketLowerBound,
+ bucketUpperBound time.Duration,
+ samples int64,
+) {
+ panic("no implemented")
+}
+
+func (r *statsDReporter) Capabilities() tally.Capabilities {
+ return r
+}
+
+func (r *statsDReporter) Reporting() bool {
+ return true
+}
+
+func (r *statsDReporter) Tagging() bool {
+ return true
+}
+
+func (r *statsDReporter) Flush() {
+ // no-op
+}
diff --git a/internal/tally/tally.go b/internal/tally/tally.go
index 9d065cb1..44090b49 100644
--- a/internal/tally/tally.go
+++ b/internal/tally/tally.go
@@ -2,6 +2,7 @@ package tally
import (
"context"
+ "time"
"github.com/uber-go/tally/v4"
"go.uber.org/fx"
@@ -10,6 +11,10 @@ import (
"github.com/coinbase/chainstorage/internal/utils/consts"
)
+const (
+ reportingInterval = time.Second
+)
+
type (
MetricParams struct {
fx.In
@@ -25,8 +30,8 @@ func NewRootScope(params MetricParams) tally.Scope {
Reporter: params.Reporter,
Tags: params.Config.GetCommonTags(),
}
- //report interval will be set on reporter
- scope, closer := tally.NewRootScope(opts, 0)
+ // report interval will be set on reporter
+ scope, closer := tally.NewRootScope(opts, reportingInterval)
params.Lifecycle.Append(fx.Hook{
OnStop: func(ctx context.Context) error {
return closer.Close()
diff --git a/internal/utils/fixtures/parser/bitcoin/bch_raw_block_940884.json b/internal/utils/fixtures/parser/bitcoin/bch_raw_block_940884.json
new file mode 100644
index 00000000..56fb5533
--- /dev/null
+++ b/internal/utils/fixtures/parser/bitcoin/bch_raw_block_940884.json
@@ -0,0 +1,1073 @@
+{
+ "blockchain": "BLOCKCHAIN_BITCOINCASH",
+ "network": "NETWORK_BITCOINCASH_MAINNET",
+ "metadata": {
+ "tag": 2,
+ "hash": "0000000000000000012a0586c619e52b73e4ba47a4d1c2b8750807c06ec9d51f",
+ "parentHash": "0000000000000000004feebcd6d18ac19a8fe46f2dff601f8a2c6eed3bd4104c",
+ "height": "940884",
+ "objectKeyMain": "BLOCKCHAIN_BITCOINCASH/NETWORK_BITCOINCASH_MAINNET/2/940884/0000000000000000012a0586c619e52b73e4ba47a4d1c2b8750807c06ec9d51f.gzip",
+ "parentHeight": "940883"
+ },
+ "bitcoin": {
+ "header": "eyJoYXNoIjoiMDAwMDAwMDAwMDAwMDAwMDAxMmEwNTg2YzYxOWU1MmI3M2U0YmE0N2E0ZDFjMmI4NzUwODA3YzA2ZWM5ZDUxZiIsImNvbmZpcm1hdGlvbnMiOjE4LCJzaXplIjoxMDUzNTUsImhlaWdodCI6OTQwODg0LCJ2ZXJzaW9uIjo1NzE1Mzk0NTYsInZlcnNpb25IZXgiOiIyMjExMDAwMCIsIm1lcmtsZXJvb3QiOiIwNjc3N2U1ZjQ4MGQ4OTFlZDlmMTI5NGVlNmU5ZDQ5OTI4OTAwMGUwZjNkMWJjZjJlOWM2MTM1NDI4N2JiYTM3IiwidHgiOlt7InR4aWQiOiI2ZmM4OGJlMmZjNTY0ZDUxYTlkOTI1NWZiMzQwZDc5ZGMyODBhYjY4NTY3YWY2ZmQ0ZGZmNjYwNTYxZDU5N2MwIiwiaGFzaCI6IjZmYzg4YmUyZmM1NjRkNTFhOWQ5MjU1ZmIzNDBkNzlkYzI4MGFiNjg1NjdhZjZmZDRkZmY2NjA1NjFkNTk3YzAiLCJ2ZXJzaW9uIjoyLCJzaXplIjo4MCwibG9ja3RpbWUiOjAsInZpbiI6W3siY29pbmJhc2UiOiIwMzU0NWIwZTQyNDM0ODJkNTM2ZjZjNmY5NThkOTE2MmZjYTUwMTAwIiwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjMuMTI4MTc0NDUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiIiLCJoZXgiOiIiLCJ0eXBlIjoic2NyaXB0In19XSwiaGV4IjoiMDIwMDAwMDAwMTAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDBmZmZmZmZmZjE0MDM1NDViMGU0MjQzNDgyZDUzNmY2YzZmOTU4ZDkxNjJmY2E1MDEwMGZmZmZmZmZmMDEyNTM3YTUxMjAwMDAwMDAwMDAwMDAwMDAwMCJ9LHsidHhpZCI6IjAwNWZkNTQyZDQyZGIwYzBkMTE1YmQwNDc0MWFlMmQyMGU3MDUxMWUzOGI3MTY1ZTQ1MjkwNzk2ZThkNjIzNzEiLCJoYXNoIjoiMDA1ZmQ1NDJkNDJkYjBjMGQxMTViZDA0NzQxYWUyZDIwZTcwNTExZTM4YjcxNjVlNDUyOTA3OTZlOGQ2MjM3MSIsInZlcnNpb24iOjIsInNpemUiOjE2NCwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjJiMmYyOTYxY2JhODIzZTc3MWZiMjUzNmVhMmNkMTA3YjY4ZjVhMWIzNTViNjBkYTZiNjFiNzliZmQ0ZWQyZTkiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiNTFiMjc1YzM1MTlkYzQ1MTlkMDI0MDAyMDJhNDAwYzBjNjdiNTI3OTkzYTA2M2MwYzdjMGNkODhjMGM2MDIyODExOTUwNGM2ODU3NDRmOTY2ZWEwNjM3ODc3NjhjMGM2YzBjYzk0NmVhMjY5NmQ2N2MwY2MwMDlkYzBjZDAxNmE4ODY4NzU1MSIsImhleCI6IjQ0NTFiMjc1YzM1MTlkYzQ1MTlkMDI0MDAyMDJhNDAwYzBjNjdiNTI3OTkzYTA2M2MwYzdjMGNkODhjMGM2MDIyODExOTUwNGM2ODU3NDRmOTY2ZWEwNjM3ODc3NjhjMGM2YzBjYzk0NmVhMjY5NmQ2N2MwY2MwMDlkYzBjZDAxNmE4ODY4NzU1MSJ9LCJzZXF1ZW5jZSI6MX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMDU3MTc1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XSwiZmVlIjoxLjY0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMWU5ZDI0ZWZkOWJiNzYxNmJkYTYwNWIzNTFiNWE4ZmI2MDdkMTJjZWEzNjI1ZmI3MWU3MjNhOGNiNjEyOTJmMmIwMDAwMDAwMDQ1NDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxMDEwMDAwMDAwMTY2YjkwODAwMDAwMDAwMDAyM2FhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODcwMDAwMDAwMCJ9LHsidHhpZCI6IjAzMDViYTYxN2I4ZWM0MmE0ZDllOTM4OThjOWU0ODNlNDkwOTEyZmZmOGU5ODhjZGY2MGFhNzQzZGNkMjkwNDQiLCJoYXNoIjoiMDMwNWJhNjE3YjhlYzQyYTRkOWU5Mzg5OGM5ZTQ4M2U0OTA5MTJmZmY4ZTk4OGNkZjYwYWE3NDNkY2QyOTA0NCIsInZlcnNpb24iOjEsInNpemUiOjIyNiwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImVkYTdmOTEwY2RjYjliMmZmZDA3NmU0YmZlOTg5YjAyYjJiYjJkZjhkZmY2MzZhM2FmNjUwMjQ2NWQ2MGI5ZjYiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMGVhMmFmZTI0ZGZiMWM0YjVjZjgxZjBmNWE1Y2YwMTgxNjI3MjVhM2E5Nzc5MjUwMjRkYmU1YTY0ZjVjY2VmMDgwMjIwMDI1NTM4Y2RhMzA5YmE4N2I5NGNjZjY1NTc3OTZmZmQwMzg5MTI4YjU4YmY2Njg3NmZlNTcyYzkzOWQxZTg2NVtBTEx8Rk9SS0lEXSAwMzAyOGZmOTY5YTczZDQyZDAwMGViOGRhZTJmMWY0NGU2MTgwMTBlYzU4YTk0NzE5NWE3MDc4MzliZThkOTQzYWEiLCJoZXgiOiI0ODMwNDUwMjIxMDBlYTJhZmUyNGRmYjFjNGI1Y2Y4MWYwZjVhNWNmMDE4MTYyNzI1YTNhOTc3OTI1MDI0ZGJlNWE2NGY1Y2NlZjA4MDIyMDAyNTUzOGNkYTMwOWJhODdiOTRjY2Y2NTU3Nzk2ZmZkMDM4OTEyOGI1OGJmNjY4NzZmZTU3MmM5MzlkMWU4NjU0MTIxMDMwMjhmZjk2OWE3M2Q0MmQwMDBlYjhkYWUyZjFmNDRlNjE4MDEwZWM1OGE5NDcxOTVhNzA3ODM5YmU4ZDk0M2FhIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjozLjAwMjk5NDQ4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTZkZjdlNjA3YzNiY2UyOTc0N2Y0ZDYxMzEzOTg0MThlMDlmMGU4MSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGU2ZGY3ZTYwN2MzYmNlMjk3NDdmNGQ2MTMxMzk4NDE4ZTA5ZjBlODE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJuZDdsbnEwc2F1dTJ0NTBheGt6dmZlc3N2d3A4Y3dzeXh3ODY3enYzIl19fSx7InZhbHVlIjowLjAyMDYyNTc4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNDUyOTM4MzdkMWNmOTE1MTdmMzk3YWUxZTI4MzVkNWMwZjY4ODg4NyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDQ1MjkzODM3ZDFjZjkxNTE3ZjM5N2FlMWUyODM1ZDVjMGY2ODg4ODc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB6amp3cGg2ODhlejV0bDg5YXdyYzVydDR3cTc2eWdzdTc0ajQ3bHBuIl19fV0sImZlZSI6Mi4yNmUtMDYsImhleCI6IjAxMDAwMDAwMDFmNmI5NjA1ZDQ2MDI2NWFmYTMzNmY2ZGZmODJkYmJiMjAyOWI5OGZlNGI2ZTA3ZmQyZjliY2JjZDEwZjlhN2VkMDEwMDAwMDA2YjQ4MzA0NTAyMjEwMGVhMmFmZTI0ZGZiMWM0YjVjZjgxZjBmNWE1Y2YwMTgxNjI3MjVhM2E5Nzc5MjUwMjRkYmU1YTY0ZjVjY2VmMDgwMjIwMDI1NTM4Y2RhMzA5YmE4N2I5NGNjZjY1NTc3OTZmZmQwMzg5MTI4YjU4YmY2Njg3NmZlNTcyYzkzOWQxZTg2NTQxMjEwMzAyOGZmOTY5YTczZDQyZDAwMGViOGRhZTJmMWY0NGU2MTgwMTBlYzU4YTk0NzE5NWE3MDc4MzliZThkOTQzYWFmZmZmZmZmZjAyYjgzNGU2MTEwMDAwMDAwMDE5NzZhOTE0ZTZkZjdlNjA3YzNiY2UyOTc0N2Y0ZDYxMzEzOTg0MThlMDlmMGU4MTg4YWNmMjc4MWYwMDAwMDAwMDAwMTk3NmE5MTQ0NTI5MzgzN2QxY2Y5MTUxN2YzOTdhZTFlMjgzNWQ1YzBmNjg4ODg3ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiMDM4MzAzOTFiYTc1YzM1NzMyNTNmMTI4NTQxY2U2MDE4MmY3MDMyNzg5MGRlYjlkNjNmN2U3MzNlMGJjZDcxOSIsImhhc2giOiIwMzgzMDM5MWJhNzVjMzU3MzI1M2YxMjg1NDFjZTYwMTgyZjcwMzI3ODkwZGViOWQ2M2Y3ZTczM2UwYmNkNzE5IiwidmVyc2lvbiI6MSwic2l6ZSI6MjI2LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiNTI0Mzg2OTdlZjdiNDBjYWQ1NDZiOTNkMDc4ZTFmNzQxNzc4M2IwZjM4MDg1MmYxZjFmOGE5YWNkMDc5Y2MxNiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwOGFkYjAwZTBkZjJlNTBkNWEzODQ3ZWNlYWIyMjc0MDBiYWRlMzg0MGZlZWQ5YmEwNDQ3N2Q0NTY4YThmNzBkOTAyMjAyNzBmMjFiNDVlMmY4MDY5ODI3MTYzMTFkNzRhZTBiZmU2MDEzMGJlZDZkMGMzNTU0ZDNjYmIzYzc4NTJlMjdlW0FMTHxGT1JLSURdIDAyMDhjNmI2ZDk3Y2EzZDYyNWZiYTU2YjY0NjBiZGNjZWIzZTA2ZjU1NTVjMTE4NDU5MGIzMzM5YzBmNTg3OWJlMSIsImhleCI6IjQ4MzA0NTAyMjEwMDhhZGIwMGUwZGYyZTUwZDVhMzg0N2VjZWFiMjI3NDAwYmFkZTM4NDBmZWVkOWJhMDQ0NzdkNDU2OGE4ZjcwZDkwMjIwMjcwZjIxYjQ1ZTJmODA2OTgyNzE2MzExZDc0YWUwYmZlNjAxMzBiZWQ2ZDBjMzU1NGQzY2JiM2M3ODUyZTI3ZTQxMjEwMjA4YzZiNmQ5N2NhM2Q2MjVmYmE1NmI2NDYwYmRjY2ViM2UwNmY1NTU1YzExODQ1OTBiMzMzOWMwZjU4NzliZTEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjEuODY4MjMwNSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5MGYyMjg1Mzk1MTk5MzE1NDBmMGUxZGUyNWRiYzAwOTM0OThjN2E1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZzB5MnpuajV2ZXg5MnE3cnNhdWZ3bWNxeW5meHg4NTVkODY2Y3k2dCJdfX0seyJ2YWx1ZSI6MC4yMDg5MzczMiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDVhMTk1ODhjY2NkMzFkOWM2MDYxMmJiOWNhMDQzZTNkNGJkNDdkYjUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1YTE5NTg4Y2NjZDMxZDljNjA2MTJiYjljYTA0M2UzZDRiZDQ3ZGI1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwZHBqa3l2ZW5mM204cnF2eTRtbmpzeThjNzVoNHJhazV6emdocXE3YyJdfX1dLCJmZWUiOjQuNTRlLTA2LCJoZXgiOiIwMTAwMDAwMDAxMTZjYzc5ZDBhY2E5ZjhmMWYxNTIwODM4MGYzYjc4MTc3NDFmOGUwNzNkYjk0NmQ1Y2E0MDdiZWY5Nzg2NDM1MjAwMDAwMDAwNmI0ODMwNDUwMjIxMDA4YWRiMDBlMGRmMmU1MGQ1YTM4NDdlY2VhYjIyNzQwMGJhZGUzODQwZmVlZDliYTA0NDc3ZDQ1NjhhOGY3MGQ5MDIyMDI3MGYyMWI0NWUyZjgwNjk4MjcxNjMxMWQ3NGFlMGJmZTYwMTMwYmVkNmQwYzM1NTRkM2NiYjNjNzg1MmUyN2U0MTIxMDIwOGM2YjZkOTdjYTNkNjI1ZmJhNTZiNjQ2MGJkY2NlYjNlMDZmNTU1NWMxMTg0NTkwYjMzMzljMGY1ODc5YmUxZmZmZmZmZmYwMjhhYjEyMjBiMDAwMDAwMDAxOTc2YTkxNDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTU4OGFjMjRkMDNlMDEwMDAwMDAwMDE5NzZhOTE0NWExOTU4OGNjY2QzMWQ5YzYwNjEyYmI5Y2EwNDNlM2Q0YmQ0N2RiNTg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjAzOTYwZTUxN2Y5MTk5NmQ5YWUzMmY2N2FjZTJlYTMzOTc1ZTNmYjczMzNiNzUzODEyZTQ3MGZlMmRkNmY3ZDMiLCJoYXNoIjoiMDM5NjBlNTE3ZjkxOTk2ZDlhZTMyZjY3YWNlMmVhMzM5NzVlM2ZiNzMzM2I3NTM4MTJlNDcwZmUyZGQ2ZjdkMyIsInZlcnNpb24iOjIsInNpemUiOjIyNSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjUzYzg1YjNjN2QxN2JjNGZiZWQxOTY3ZTk4Yzk2MWQzNzMyMWE4YzE3OGNiOTY2Nzg4YmM3Nzg2ZDZiNjk0YmUiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAzZDYwYmEyNWQxZmFmYjM3YTE1NzJkMjA4OGI3Y2RlMTgzZmJmNzk0YjBiZGU3NWMwMzUyNTJlMjIyMDhlMGMzMDIyMDc3YWVmMWNlZDRkZjRmMmM5Nzk3NTYxNjI3YTM2MGFjZmZiMjdhMzVmOWM5ZWY4MTIzODkyYzNlNDIzYjhlNGNbQUxMfEZPUktJRF0gMDMyMzM4ODEwN2VjYWI1OTEwZDc0OWE4MTA2ZTQyZDIzZGE1NjExM2E4M2Y2ZTdjZTFhMTNkZmY4ZWUzYThlMDk2IiwiaGV4IjoiNDczMDQ0MDIyMDNkNjBiYTI1ZDFmYWZiMzdhMTU3MmQyMDg4YjdjZGUxODNmYmY3OTRiMGJkZTc1YzAzNTI1MmUyMjIwOGUwYzMwMjIwNzdhZWYxY2VkNGRmNGYyYzk3OTc1NjE2MjdhMzYwYWNmZmIyN2EzNWY5YzllZjgxMjM4OTJjM2U0MjNiOGU0YzQxMjEwMzIzMzg4MTA3ZWNhYjU5MTBkNzQ5YTgxMDZlNDJkMjNkYTU2MTEzYTgzZjZlN2NlMWExM2RmZjhlZTNhOGUwOTYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9XSwidm91dCI6W3sidmFsdWUiOjIuMTQxMDAwMTgsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhYzE2Y2ZjNDE3MzU1YWUwZGMxNjk2YjBlMmIxYzA5NWU0YzdhNjhjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YWMxNmNmYzQxNzM1NWFlMGRjMTY5NmIwZTJiMWMwOTVlNGM3YTY4Yzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemtwZG43eXp1NjQ0Y3h1ejZ0dHBjNDNjejI3ZjNheDNzM3FjOGdlbGEiXX19LHsidmFsdWUiOjEuNzM5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTczNjI3MjU4NzAzOTJiZDZhYWM2OTg4MmJmZTcwYzdkOGQ3YTg3YiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGE3MzYyNzI1ODcwMzkyYmQ2YWFjNjk4ODJiZmU3MGM3ZDhkN2E4N2I4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpubnZmZTlzdXBlOTB0MjQzNWNzMmw3d3JyYTM0YWcwdnVydDRmN2VhIl19fV0sImZlZSI6Mi4yNmUtMDUsImhleCI6IjAyMDAwMDAwMDFiZTk0YjZkNjg2NzdiYzg4Njc5NmNiNzhjMWE4MjE3M2QzNjFjOTk4N2U5NmQxYmU0ZmJjMTc3ZDNjNWJjODUzMDAwMDAwMDA2YTQ3MzA0NDAyMjAzZDYwYmEyNWQxZmFmYjM3YTE1NzJkMjA4OGI3Y2RlMTgzZmJmNzk0YjBiZGU3NWMwMzUyNTJlMjIyMDhlMGMzMDIyMDc3YWVmMWNlZDRkZjRmMmM5Nzk3NTYxNjI3YTM2MGFjZmZiMjdhMzVmOWM5ZWY4MTIzODkyYzNlNDIzYjhlNGM0MTIxMDMyMzM4ODEwN2VjYWI1OTEwZDc0OWE4MTA2ZTQyZDIzZGE1NjExM2E4M2Y2ZTdjZTFhMTNkZmY4ZWUzYThlMDk2ZmVmZmZmZmYwMjMyZThjMjBjMDAwMDAwMDAxOTc2YTkxNGFjMTZjZmM0MTczNTVhZTBkYzE2OTZiMGUyYjFjMDk1ZTRjN2E2OGM4OGFjZTA4MDVkMGEwMDAwMDAwMDE5NzZhOTE0YTczNjI3MjU4NzAzOTJiZDZhYWM2OTg4MmJmZTcwYzdkOGQ3YTg3Yjg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjA1NzQ2NzU2NGRkYjRiYjdiMTY1ZTJhYzEwNmY1OTk5NmU1YjcxYjllNGU3OTNhN2I5NjM2ZDU1OTE3NmU2NDgiLCJoYXNoIjoiMDU3NDY3NTY0ZGRiNGJiN2IxNjVlMmFjMTA2ZjU5OTk2ZTViNzFiOWU0ZTc5M2E3Yjk2MzZkNTU5MTc2ZTY0OCIsInZlcnNpb24iOjEsInNpemUiOjIyNiwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjVjNGY5MzM3Yzk2ZTMxZmFkYjIwNzhhMGE2Mjg4YWVhODljMTVjNGJjMWFkOWEyZTQ2ZmFkMDliZjEyNjVlYzAiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMGFlZWM2NWZjNzMzM2VkNWMwNWE0NmJjN2M3ZmQxNzdhNzJkZGFhYmU0MWExZmMxNmE2NzMwM2FhNTFiMjRkMTQwMjIwNzA1MjFjMDc0MDllZjE5ZGI0OWJiNDlkMDQ2NDU3NjEzNGQ4NzU4MzExYmUzM2QwOWM2YTllZDgyMjIwZTdkY1tBTEx8Rk9SS0lEXSAwMzMzMjE1ZmUyOTdkYzhkMzIxODQxN2E0MjEzNTQ5YmEyNDJlOWJlYjljZTgxMjg4YTAzY2JjMDlmNDhhNDAyZDgiLCJoZXgiOiI0ODMwNDUwMjIxMDBhZWVjNjVmYzczMzNlZDVjMDVhNDZiYzdjN2ZkMTc3YTcyZGRhYWJlNDFhMWZjMTZhNjczMDNhYTUxYjI0ZDE0MDIyMDcwNTIxYzA3NDA5ZWYxOWRiNDliYjQ5ZDA0NjQ1NzYxMzRkODc1ODMxMWJlMzNkMDljNmE5ZWQ4MjIyMGU3ZGM0MTIxMDMzMzIxNWZlMjk3ZGM4ZDMyMTg0MTdhNDIxMzU0OWJhMjQyZTliZWI5Y2U4MTI4OGEwM2NiYzA5ZjQ4YTQwMmQ4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjAwMTM2MTE5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTY4MTYzN2ZmNWJhZWFkMjhkNzFlNDU5Yjg3YmM3Y2Y4NWVlMTQ3ZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU2ODE2MzdmZjViYWVhZDI4ZDcxZTQ1OWI4N2JjN2NmODVlZTE0N2Q4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB0Z3pjbWw3a2F3NDU1ZHc4ajlud3JtY2w4Y3RtczUwNWN0bTV3NW5oIl19fSx7InZhbHVlIjowLjAzODcxNDYxLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNmI0MTU3MDliYzQyNjMwZmJjYmIyNzQxOWFmODZjYjg4YTk1YWMzOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDZiNDE1NzA5YmM0MjYzMGZiY2JiMjc0MTlhZjg2Y2I4OGE5NWFjMzg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA0NXo0Y2ZoM3B4eHJhdWh2bjVyeGhjZGp1ZzQ5ZHY4cWtmeDBsbmNjIl19fV0sImZlZSI6Ny4yZS0wNiwiaGV4IjoiMDEwMDAwMDAwMWMwNWUyNmYxOWJkMGZhNDYyZTlhYWRjMTRiNWNjMTg5ZWE4YTI4YTZhMDc4MjBkYmZhMzE2ZWM5Mzc5MzRmNWMwMTAwMDAwMDZiNDgzMDQ1MDIyMTAwYWVlYzY1ZmM3MzMzZWQ1YzA1YTQ2YmM3YzdmZDE3N2E3MmRkYWFiZTQxYTFmYzE2YTY3MzAzYWE1MWIyNGQxNDAyMjA3MDUyMWMwNzQwOWVmMTlkYjQ5YmI0OWQwNDY0NTc2MTM0ZDg3NTgzMTFiZTMzZDA5YzZhOWVkODIyMjBlN2RjNDEyMTAzMzMyMTVmZTI5N2RjOGQzMjE4NDE3YTQyMTM1NDliYTI0MmU5YmViOWNlODEyODhhMDNjYmMwOWY0OGE0MDJkOGZmZmZmZmZmMDJiNzEzMDIwMDAwMDAwMDAwMTk3NmE5MTQ1NjgxNjM3ZmY1YmFlYWQyOGQ3MWU0NTliODdiYzdjZjg1ZWUxNDdkODhhY2U1MTIzYjAwMDAwMDAwMDAxOTc2YTkxNDZiNDE1NzA5YmM0MjYzMGZiY2JiMjc0MTlhZjg2Y2I4OGE5NWFjMzg4OGFjMDAwMDAwMDAifSx7InR4aWQiOiIwNWQ2NWYwM2M2NThjOThmNzVhOTRmZDM3ZTQ5ZTM3MjEwYTM3N2UxMWM1NzVhNDhiYjM5ZDIwNTE5N2M1ZGViIiwiaGFzaCI6IjA1ZDY1ZjAzYzY1OGM5OGY3NWE5NGZkMzdlNDllMzcyMTBhMzc3ZTExYzU3NWE0OGJiMzlkMjA1MTk3YzVkZWIiLCJ2ZXJzaW9uIjoyLCJzaXplIjoxNjQsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI3MjZjMzBmMmY0YTdlZmU4MmY3ZTkxMDUzYjM5YWVhMjAxZDAyZDE0ZGEzMGJkNzFlZDJhMGE0MjJhMGFlOGE5Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEiLCJoZXgiOiI0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEifSwic2VxdWVuY2UiOjF9XSwidm91dCI6W3sidmFsdWUiOjAuMDAwNDE0MjksIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dLCJmZWUiOjEuNjRlLTA2LCJoZXgiOiIwMjAwMDAwMDAxYTllODBhMmE0MjBhMmFlZDcxYmQzMGRhMTQyZGQwMDFhMmFlMzkzYjA1OTE3ZTJmZThlZmE3ZjRmMjMwNmM3MjAwMDAwMDAwNDU0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEwMTAwMDAwMDAxZDVhMTAwMDAwMDAwMDAwMDIzYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NzAwMDAwMDAwIn0seyJ0eGlkIjoiMDYxMDc2ODAxYjM2YzM1NDc4MjZhZWVjNWM5ZGY5NTk3NDVjMDMyNTJiYmJiMzIwNGE5YWE0OTE2YmEzNWIyMSIsImhhc2giOiIwNjEwNzY4MDFiMzZjMzU0NzgyNmFlZWM1YzlkZjk1OTc0NWMwMzI1MmJiYmIzMjA0YTlhYTQ5MTZiYTM1YjIxIiwidmVyc2lvbiI6Miwic2l6ZSI6MjE5LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiNzM4MjI0MTY5YTZjYzk4OWM3NWEzYjFjOTU5OGZmMzYwYjZkMDlmYzk4MDliMTYzZWI0NmJhZmRhODg4NTBmZCIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiI5ZDQ4NjExMTI5OTgxYTk2MmM4Y2M4NGU2ZmNjY2JmNjBhOWUzYzY3N2RiY2JmYjIxYWIzMjQxMmYxZjZlOGE5ODI3ZDJhYWM5NDI2ZDc1MzE2MWFhNDQ3YzIzNDU0YThhYWZjYzA1NDc0YzlkMDRjNmI0MmE2YmE2ZTc1OGZjMltBTEx8Rk9SS0lEXSAwMjQyMzVjZTRlODdiOWE0ZmVjYTZmMWViMjJlYTcxMWE2ODczZjEyMzA3MTgwZGM0ZjM2MDM1ODBmYzkxODZkZTgiLCJoZXgiOiI0MTlkNDg2MTExMjk5ODFhOTYyYzhjYzg0ZTZmY2NjYmY2MGE5ZTNjNjc3ZGJjYmZiMjFhYjMyNDEyZjFmNmU4YTk4MjdkMmFhYzk0MjZkNzUzMTYxYWE0NDdjMjM0NTRhOGFhZmNjMDU0NzRjOWQwNGM2YjQyYTZiYTZlNzU4ZmMyNDEyMTAyNDIzNWNlNGU4N2I5YTRmZWNhNmYxZWIyMmVhNzExYTY4NzNmMTIzMDcxODBkYzRmMzYwMzU4MGZjOTE4NmRlOCJ9LCJzZXF1ZW5jZSI6MH1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MTAuNjcxMjg4NzcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBlNTQxZTdjZjNmMDNiZTk3NjNlODhjOTFjOGZiYzM4NTk2ZDc2OTkxIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZTU0MWU3Y2YzZjAzYmU5NzYzZTg4YzkxYzhmYmMzODU5NmQ3Njk5MTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcmo1cmU3MDh1cG1hOW1yYXp4ZnJqOG1jd3plZDRtZmp5OGV6anZjNzUiXX19LHsidmFsdWUiOjAuMDM3NzM2NzMsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBlNzE0NzczMDdjNTdiYmIyZDg0Yzc0ODY4MTJhNjRlMzliZTk4N2Y3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZTcxNDc3MzA3YzU3YmJiMmQ4NGM3NDg2ODEyYTY0ZTM5YmU5ODdmNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcm4zZ2FlczAzdG1odmtjZjM2Z2RxZjJ2bjNlaDZ2ODd1YWc3MGd6ZGQiXX19XSwiZmVlIjoyLjE5ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMWZkNTA4OGE4ZmRiYTQ2ZWI2M2IxMDk5OGZjMDk2ZDBiMzZmZjk4OTUxYzNiNWFjNzg5Yzk2YzlhMTYyNDgyNzMwMTAwMDAwMDY0NDE5ZDQ4NjExMTI5OTgxYTk2MmM4Y2M4NGU2ZmNjY2JmNjBhOWUzYzY3N2RiY2JmYjIxYWIzMjQxMmYxZjZlOGE5ODI3ZDJhYWM5NDI2ZDc1MzE2MWFhNDQ3YzIzNDU0YThhYWZjYzA1NDc0YzlkMDRjNmI0MmE2YmE2ZTc1OGZjMjQxMjEwMjQyMzVjZTRlODdiOWE0ZmVjYTZmMWViMjJlYTcxMWE2ODczZjEyMzA3MTgwZGM0ZjM2MDM1ODBmYzkxODZkZTgwMDAwMDAwMDAyMmQxODliM2YwMDAwMDAwMDE5NzZhOTE0ZTU0MWU3Y2YzZjAzYmU5NzYzZTg4YzkxYzhmYmMzODU5NmQ3Njk5MTg4YWNlOTk0MzkwMDAwMDAwMDAwMTk3NmE5MTRlNzE0NzczMDdjNTdiYmIyZDg0Yzc0ODY4MTJhNjRlMzliZTk4N2Y3ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiMDY1OTNlZmI5MWVmZWIxNGRhYjU3OWE4NzI1Mzk3ZWY1NWFhMDVjMjYzMWQ0MTdmOTIwMTQ0YThlNDNjMTg4NSIsImhhc2giOiIwNjU5M2VmYjkxZWZlYjE0ZGFiNTc5YTg3MjUzOTdlZjU1YWEwNWMyNjMxZDQxN2Y5MjAxNDRhOGU0M2MxODg1IiwidmVyc2lvbiI6Miwic2l6ZSI6NDM0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiYzA5M2FkODQzNzRlOTcyOTc3MWY2MzYwMjE4ZDEyZjY0NmQyZDlkYjY4MzI5ZjAwYmM5MDRiYTFkNjRjNmYzZSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwZTFmMDMwNzVmMjUxZTQyMjNiMjkzOGI4ZWExMjU3MDA2YTU0MWQxMzg5MmI2ZjkwZTdmZmZlNWM3M2M2NmYxODAyMjAwZmVkMDhhMDE1Njc5MzdkZmI3ZGEyNzM4MTVkYjc1ZjY3MDNkMWU4ZDA2NTM4MjNhZDEyNWMyMTc3MjlkZDJjW0FMTHxGT1JLSURdIDAyZjFhZmRlODUxZTM5MGM5NWMxZGRhNTU4ZGRjOTRlYjlhZGE5NmNiZjE1NGUxNmY4MzhiOTkxYjgzMjM3MWJmYSIsImhleCI6IjQ4MzA0NTAyMjEwMGUxZjAzMDc1ZjI1MWU0MjIzYjI5MzhiOGVhMTI1NzAwNmE1NDFkMTM4OTJiNmY5MGU3ZmZmZTVjNzNjNjZmMTgwMjIwMGZlZDA4YTAxNTY3OTM3ZGZiN2RhMjczODE1ZGI3NWY2NzAzZDFlOGQwNjUzODIzYWQxMjVjMjE3NzI5ZGQyYzQxMjEwMmYxYWZkZTg1MWUzOTBjOTVjMWRkYTU1OGRkYzk0ZWI5YWRhOTZjYmYxNTRlMTZmODM4Yjk5MWI4MzIzNzFiZmEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjA3ZWJjMDU4NmFiNDVhNjdhN2Q1MjRmMTNhMmQ5YmRlZTg4NjljYTM4NjZjY2I3ZDcxYTg1YWFhNzgwNDliMmQiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA3N2M2MWIwY2RhYjEzNjhkMjM5ZmM3MzMwZGJlMzlhZTFjNjBiYzA5YzNmNjliYjhhNzVjNTk2M2E5NDg3MmY4MDIyMDY0NTY4NmQ0OTBhNzY4MzUwOWY4YmUzNDFiZWYxMmQ5ZGYzMDE1MWZiMTc5NTc0YTYwYTkyM2E3NDNiYTJiYWZbQUxMfEZPUktJRF0gMDJmMWFmZGU4NTFlMzkwYzk1YzFkZGE1NThkZGM5NGViOWFkYTk2Y2JmMTU0ZTE2ZjgzOGI5OTFiODMyMzcxYmZhIiwiaGV4IjoiNDczMDQ0MDIyMDc3YzYxYjBjZGFiMTM2OGQyMzlmYzczMzBkYmUzOWFlMWM2MGJjMDljM2Y2OWJiOGE3NWM1OTYzYTk0ODcyZjgwMjIwNjQ1Njg2ZDQ5MGE3NjgzNTA5ZjhiZTM0MWJlZjEyZDlkZjMwMTUxZmIxNzk1NzRhNjBhOTIzYTc0M2JhMmJhZjQxMjEwMmYxYWZkZTg1MWUzOTBjOTVjMWRkYTU1OGRkYzk0ZWI5YWRhOTZjYmYxNTRlMTZmODM4Yjk5MWI4MzIzNzFiZmEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjYuMTI3MjcwMDEsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAwMzY2MmM5ZGQ3YTMxNTUwOGMzYTc2YWQxN2VmNWIyZTIwZTM3ZjRkIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MDM2NjJjOWRkN2EzMTU1MDhjM2E3NmFkMTdlZjViMmUyMGUzN2Y0ZDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXBrdnR5YTY3MzMyNXl2OGZtMjY5bDB0dmh6cGNtbGY1dWprdjh4dHgiXX19LHsidmFsdWUiOjAuMDk5OTgsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA0ZjY5NzhmZmU4NmY0YjM0MGEwNjkxYjg3Y2I0MTJmZjNiZmQ1NzE1IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NGY2OTc4ZmZlODZmNGIzNDBhMDY5MWI4N2NiNDEyZmYzYmZkNTcxNTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDhrajc4bGFwaDVrZHEycTZnbXNsOTV6dGxuaGwyaHo1M20zN3d0ZnEiXX19LHsidmFsdWUiOjAuMCwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX1JFVFVSTiA1NDUyNDE0NDQ1MmIzYTc0Njg2ZjcyMzEzNjM0NmI2Nzc0NzYzNzdhNzY2YzczNmQ2ODc5Nzc2MzYzNzk3OTdhMzAzNzMzNzA3OTM0NzMzMzY4MzA3ODZjNzEzMzdhNzA3ODc3IiwiaGV4IjoiNmEzMjU0NTI0MTQ0NDUyYjNhNzQ2ODZmNzIzMTM2MzQ2YjY3NzQ3NjM3N2E3NjZjNzM2ZDY4Nzk3NzYzNjM3OTc5N2EzMDM3MzM3MDc5MzQ3MzMzNjgzMDc4NmM3MTMzN2E3MDc4NzciLCJ0eXBlIjoibnVsbGRhdGEifX1dLCJmZWUiOjJlLTA1LCJoZXgiOiIwMjAwMDAwMDAyM2U2ZjRjZDZhMTRiOTBiYzAwOWYzMjY4ZGJkOWQyNDZmNjEyOGQyMTYwNjMxZjc3Mjk5NzRlMzc4NGFkOTNjMDAwMDAwMDAwNmI0ODMwNDUwMjIxMDBlMWYwMzA3NWYyNTFlNDIyM2IyOTM4YjhlYTEyNTcwMDZhNTQxZDEzODkyYjZmOTBlN2ZmZmU1YzczYzY2ZjE4MDIyMDBmZWQwOGEwMTU2NzkzN2RmYjdkYTI3MzgxNWRiNzVmNjcwM2QxZThkMDY1MzgyM2FkMTI1YzIxNzcyOWRkMmM0MTIxMDJmMWFmZGU4NTFlMzkwYzk1YzFkZGE1NThkZGM5NGViOWFkYTk2Y2JmMTU0ZTE2ZjgzOGI5OTFiODMyMzcxYmZhZmZmZmZmZmYyZDliMDQ3OGFhNWFhODcxN2RjYjZjODZhMzljODZlOGRlOWIyZDNhZjEyNGQ1YTc2NzVhYjQ2YTU4YzBlYjA3MDEwMDAwMDA2YTQ3MzA0NDAyMjA3N2M2MWIwY2RhYjEzNjhkMjM5ZmM3MzMwZGJlMzlhZTFjNjBiYzA5YzNmNjliYjhhNzVjNTk2M2E5NDg3MmY4MDIyMDY0NTY4NmQ0OTBhNzY4MzUwOWY4YmUzNDFiZWYxMmQ5ZGYzMDE1MWZiMTc5NTc0YTYwYTkyM2E3NDNiYTJiYWY0MTIxMDJmMWFmZGU4NTFlMzkwYzk1YzFkZGE1NThkZGM5NGViOWFkYTk2Y2JmMTU0ZTE2ZjgzOGI5OTFiODMyMzcxYmZhZmZmZmZmZmYwM2Q5Nzg4NTI0MDAwMDAwMDAxOTc2YTkxNDAzNjYyYzlkZDdhMzE1NTA4YzNhNzZhZDE3ZWY1YjJlMjBlMzdmNGQ4OGFjYjA4ZTk4MDAwMDAwMDAwMDE5NzZhOTE0NGY2OTc4ZmZlODZmNGIzNDBhMDY5MWI4N2NiNDEyZmYzYmZkNTcxNTg4YWMwMDAwMDAwMDAwMDAwMDAwMzQ2YTMyNTQ1MjQxNDQ0NTJiM2E3NDY4NmY3MjMxMzYzNDZiNjc3NDc2Mzc3YTc2NmM3MzZkNjg3OTc3NjM2Mzc5Nzk3YTMwMzczMzcwNzkzNDczMzM2ODMwNzg2YzcxMzM3YTcwNzg3NzAwMDAwMDAwIn0seyJ0eGlkIjoiMDhjOWY3Mzc4ODA1MGY5MTRjNTA3MjJmZGJjMTg2Njk4MzQ1YTI4NGM3NTMzNzFhMDA3ZTE3ZTRiOTVjZWRiMSIsImhhc2giOiIwOGM5ZjczNzg4MDUwZjkxNGM1MDcyMmZkYmMxODY2OTgzNDVhMjg0Yzc1MzM3MWEwMDdlMTdlNGI5NWNlZGIxIiwidmVyc2lvbiI6MSwic2l6ZSI6NTIyLCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiNjFlZjNkOTA4ZmQyYWIxOWVkZDZlYzQ1NDlhZDkzMzk0ZjM2Y2E2NTZiYzBkYTViMTA5NWEyMWZmYzFmOTExZCIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwYmQyNWQyMDEwYTNkNmFmMWM4ZDc0MzFjM2Q5YmY3MjBiMzMyYmEzNmNmZDVmZGNhNWY5MTdmZjAzZTkwM2ZlOTAyMjAwOTkxYjc4NTEwZTZjYjZiMjJiYzc1OGE1M2Y4MzIzZTVhM2E5MTE5NjA1YTgxNDIzNzM1NzQwY2VkOGUzZjRlW0FMTHxGT1JLSURdIDAzYTFhZjRlYzNiNDdiYmZjNzE0N2RiMzcyNWVlMjRjNDNlN2RjYmY5NjJjZGFlZWFlNmZjNTcyNWY0NzBkNjZkZCIsImhleCI6IjQ4MzA0NTAyMjEwMGJkMjVkMjAxMGEzZDZhZjFjOGQ3NDMxYzNkOWJmNzIwYjMzMmJhMzZjZmQ1ZmRjYTVmOTE3ZmYwM2U5MDNmZTkwMjIwMDk5MWI3ODUxMGU2Y2I2YjIyYmM3NThhNTNmODMyM2U1YTNhOTExOTYwNWE4MTQyMzczNTc0MGNlZDhlM2Y0ZTQxMjEwM2ExYWY0ZWMzYjQ3YmJmYzcxNDdkYjM3MjVlZTI0YzQzZTdkY2JmOTYyY2RhZWVhZTZmYzU3MjVmNDcwZDY2ZGQifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjAzZGIzZjlhOGVkMjJkYzZjODMwMmViYjEyNDVkMzBkYTJjODg2ODM3MTZkZDk0YTcwYmYzODdhNzE5YzQ0ZTgiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMGIzOWQ3ZjBkMmU5ZWJiZDY2OTBjYzliNDQ5NTY5ODlhYWY0MGIwZDZkMjJhNDVlMThhYjVjNjU2MDY0ZTViMmEwMjIwMWM2MzQ2M2QxYTVmYWMzNjc2NDk3MjI2NDdkZDgyNTFkZGI3ZGMyNzllYTk5NjBiYmY2ZTc5ZjE3NmJmMjY5MVtBTEx8Rk9SS0lEXSAwMmZiYzdhNDExZTUxNWFiYjBiMDZhNDM5N2I4ZjY3NjQxNGQ1NGZlODk5NGI1NDk3MDA5MzNiZTlkY2QxZGM5ZTciLCJoZXgiOiI0ODMwNDUwMjIxMDBiMzlkN2YwZDJlOWViYmQ2NjkwY2M5YjQ0OTU2OTg5YWFmNDBiMGQ2ZDIyYTQ1ZTE4YWI1YzY1NjA2NGU1YjJhMDIyMDFjNjM0NjNkMWE1ZmFjMzY3NjQ5NzIyNjQ3ZGQ4MjUxZGRiN2RjMjc5ZWE5OTYwYmJmNmU3OWYxNzZiZjI2OTE0MTIxMDJmYmM3YTQxMWU1MTVhYmIwYjA2YTQzOTdiOGY2NzY0MTRkNTRmZTg5OTRiNTQ5NzAwOTMzYmU5ZGNkMWRjOWU3In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWE3MWZmYmY1N2M4YmFlNWRlMjljNzQxN2FjOWVmMTdiNjFlZjkwODg5ZDg0M2FmOWEyMTI1YTYyMGFjMTNlIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBiZmZmYjcwMWUxYWE1Zjk5N2RiMWIyYThjNDhjNjk4MThhNmVlYWFmMGZiNjM1YjBhNTZhOGY3ZjE4MmYzZTJmMDIyMDExZWE0NjkyOWEzYWZmYTk3YTkwMDVhNTUyZGM3NTgzZDAxNDM5MGE2MzQ5MzJlNDNiMTM0OGYzNzRjMTI0NzdbQUxMfEZPUktJRF0gMDM2NDZmYmNhNjc0NjdkNjkxMDRmNTU4Y2I4MTMwOWFiNDhkZjcwZjUwMjk4ZDVjZDIzNDEzNGVmNDYxMDNlZDM4IiwiaGV4IjoiNDgzMDQ1MDIyMTAwYmZmZmI3MDFlMWFhNWY5OTdkYjFiMmE4YzQ4YzY5ODE4YTZlZWFhZjBmYjYzNWIwYTU2YThmN2YxODJmM2UyZjAyMjAxMWVhNDY5MjlhM2FmZmE5N2E5MDA1YTU1MmRjNzU4M2QwMTQzOTBhNjM0OTMyZTQzYjEzNDhmMzc0YzEyNDc3NDEyMTAzNjQ2ZmJjYTY3NDY3ZDY5MTA0ZjU1OGNiODEzMDlhYjQ4ZGY3MGY1MDI5OGQ1Y2QyMzQxMzRlZjQ2MTAzZWQzOCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDFiZGFhZDI0Y2Y1OGZiNDEwZDBiYTg0YTE3ZjlhZTMwNmYwM2JhZmIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxYmRhYWQyNGNmNThmYjQxMGQwYmE4NGExN2Y5YWUzMDZmMDNiYWZiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZGE0dGZ5ZWF2MGtzZ2RwdzV5NTlsZTRjY3g3cWE2bHZ3NWt4ZG1rOCJdfX0seyJ2YWx1ZSI6MC4wMDM5Mzc2NSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDk1MDIwMTBjMDc0NjM4YTg1ZGMwMGZjNGUyMzJiYTJmNjA0NGNiMTkgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5NTAyMDEwYzA3NDYzOGE4NWRjMDBmYzRlMjMyYmEyZjYwNDRjYjE5ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6MnN5cWd2cWFycjMyemFjcTh1ZmMzamhnaGtxM3h0cnkycDg1azVxZiJdfX1dLCJmZWUiOjYuMjY0ZS0wNSwiaGV4IjoiMDEwMDAwMDAwMzFkOTExZmZjMWZhMjk1MTA1YmRhYzA2YjY1Y2EzNjRmMzk5M2FkNDk0NWVjZDZlZDE5YWJkMjhmOTAzZGVmNjEwMTAwMDAwMDZiNDgzMDQ1MDIyMTAwYmQyNWQyMDEwYTNkNmFmMWM4ZDc0MzFjM2Q5YmY3MjBiMzMyYmEzNmNmZDVmZGNhNWY5MTdmZjAzZTkwM2ZlOTAyMjAwOTkxYjc4NTEwZTZjYjZiMjJiYzc1OGE1M2Y4MzIzZTVhM2E5MTE5NjA1YTgxNDIzNzM1NzQwY2VkOGUzZjRlNDEyMTAzYTFhZjRlYzNiNDdiYmZjNzE0N2RiMzcyNWVlMjRjNDNlN2RjYmY5NjJjZGFlZWFlNmZjNTcyNWY0NzBkNjZkZGZmZmZmZmZmZTg0NDljNzE3YTM4YmY3MDRhZDk2ZDcxODM4NmM4YTIwZGQzNDUxMmJiMmUzMGM4YzYyZGQyOGU5YTNmZGIwMzAzMDAwMDAwNmI0ODMwNDUwMjIxMDBiMzlkN2YwZDJlOWViYmQ2NjkwY2M5YjQ0OTU2OTg5YWFmNDBiMGQ2ZDIyYTQ1ZTE4YWI1YzY1NjA2NGU1YjJhMDIyMDFjNjM0NjNkMWE1ZmFjMzY3NjQ5NzIyNjQ3ZGQ4MjUxZGRiN2RjMjc5ZWE5OTYwYmJmNmU3OWYxNzZiZjI2OTE0MTIxMDJmYmM3YTQxMWU1MTVhYmIwYjA2YTQzOTdiOGY2NzY0MTRkNTRmZTg5OTRiNTQ5NzAwOTMzYmU5ZGNkMWRjOWU3ZmZmZmZmZmYzZWMxMGE2MjVhMTJhMmY5M2E4NDlkODg5MGVmNjE3YmYxOWVhYzE3NzQ5Y2UyNWRhZThiN2NmNWZiMWZhNzc1MDEwMDAwMDA2YjQ4MzA0NTAyMjEwMGJmZmZiNzAxZTFhYTVmOTk3ZGIxYjJhOGM0OGM2OTgxOGE2ZWVhYWYwZmI2MzViMGE1NmE4ZjdmMTgyZjNlMmYwMjIwMTFlYTQ2OTI5YTNhZmZhOTdhOTAwNWE1NTJkYzc1ODNkMDE0MzkwYTYzNDkzMmU0M2IxMzQ4ZjM3NGMxMjQ3NzQxMjEwMzY0NmZiY2E2NzQ2N2Q2OTEwNGY1NThjYjgxMzA5YWI0OGRmNzBmNTAyOThkNWNkMjM0MTM0ZWY0NjEwM2VkMzhmZmZmZmZmZjAyODA4NDFlMDAwMDAwMDAwMDE5NzZhOTE0MWJkYWFkMjRjZjU4ZmI0MTBkMGJhODRhMTdmOWFlMzA2ZjAzYmFmYjg4YWMyNTAyMDYwMDAwMDAwMDAwMTk3NmE5MTQ5NTAyMDEwYzA3NDYzOGE4NWRjMDBmYzRlMjMyYmEyZjYwNDRjYjE5ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiMDliNmRmOWQzZTgwNjYzYWIyNDY1ZjIyMWMwMzc1ODUxYzdmMDMwYjVhYjNjMWVkNzg3ZjlmY2RkM2Y0NTJkMyIsImhhc2giOiIwOWI2ZGY5ZDNlODA2NjNhYjI0NjVmMjIxYzAzNzU4NTFjN2YwMzBiNWFiM2MxZWQ3ODdmOWZjZGQzZjQ1MmQzIiwidmVyc2lvbiI6Miwic2l6ZSI6MTY0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiZTg4MmI2NjRiZjExM2EzYmU3YjRhMWY5MTZlOGVkYzlkNDdjMDFlZGUxYjg2NGRhZjUxODk3NjYzZmY2ZGI3MCIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIiwiaGV4IjoiNDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIn0sInNlcXVlbmNlIjoxfV0sInZvdXQiOlt7InZhbHVlIjowLjAwNTY1MDI2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XSwiZmVlIjoxLjY0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTcwZGJmNjNmNjY5NzE4ZjVkYTY0YjhlMWVkMDE3Y2Q0YzllZGU4MTZmOWExYjRlNzNiM2ExMWJmNjRiNjgyZTgwMDAwMDAwMDQ1NDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxMDEwMDAwMDAwMTIyOWYwODAwMDAwMDAwMDAyM2FhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODcwMDAwMDAwMCJ9LHsidHhpZCI6IjBhZDVhOGE4OTFiYmU3OWM2MGI4ODI4ZGEwOGI4ZjIwZTMwMzVmMDQyN2RkZmFkZTNmNTVlMDMwMDA5NWEyNzYiLCJoYXNoIjoiMGFkNWE4YTg5MWJiZTc5YzYwYjg4MjhkYTA4YjhmMjBlMzAzNWYwNDI3ZGRmYWRlM2Y1NWUwMzAwMDk1YTI3NiIsInZlcnNpb24iOjIsInNpemUiOjEzNjIsImxvY2t0aW1lIjo0MiwidmluIjpbeyJ0eGlkIjoiZGJhZjg1Y2Y5ODFmYjYzOTlmMGVmOGY5ODE1YmZkOTg3MGEwODY0OGQxMTUyMzg3MjNjMTE4MzBjNmIyODhlZSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZGU3YWU3YjEwZDZkNmQ5YmJjMDE3MGFiMmMyZWQ0NDUwZWQ0Y2IxODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZGU3YWU3YjEwZDZkNmQ5YmJjMDE3MGFiMmMyZWQ0NDUwZWQ0Y2IxODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJkYmFmODVjZjk4MWZiNjM5OWYwZWY4Zjk4MTViZmQ5ODcwYTA4NjQ4ZDExNTIzODcyM2MxMTgzMGM2YjI4OGVlIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ3MWE3MTQ5ZWUwYjRlMTQ4ZDc3YWFkYjRjMGU3MWExMmYwZjM4MmE5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ3MWE3MTQ5ZWUwYjRlMTQ4ZDc3YWFkYjRjMGU3MWExMmYwZjM4MmE5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImRiYWY4NWNmOTgxZmI2Mzk5ZjBlZjhmOTgxNWJmZDk4NzBhMDg2NDhkMTE1MjM4NzIzYzExODMwYzZiMjg4ZWUiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNGZlODI3YzY1MzgzMjA3Mzk3OGI3YmQ1OGVlYzg0MGVlNDRiYzY3MDg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNGZlODI3YzY1MzgzMjA3Mzk3OGI3YmQ1OGVlYzg0MGVlNDRiYzY3MDg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZGJhZjg1Y2Y5ODFmYjYzOTlmMGVmOGY5ODE1YmZkOTg3MGEwODY0OGQxMTUyMzg3MjNjMTE4MzBjNmIyODhlZSIsInZvdXQiOjMsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0MWIyMTM0OWI4MDE5ZWM3MzhhOGRjNmYyZmM0MmRhY2JiMDY0NDFkMDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0MWIyMTM0OWI4MDE5ZWM3MzhhOGRjNmYyZmM0MmRhY2JiMDY0NDFkMDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJkYmFmODVjZjk4MWZiNjM5OWYwZWY4Zjk4MTViZmQ5ODcwYTA4NjQ4ZDExNTIzODcyM2MxMTgzMGM2YjI4OGVlIiwidm91dCI6NCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjY3ZTA0ZTcwNTE3YjU1NTBlNWE2ZWRiNDhhN2VlYzhjYmU0MGQ0NjgzMDFlM2JkZTFhZTliOTQwNDBkZGZjNDgiLCJ2b3V0IjoxMCwic2NyaXB0U2lnIjp7ImFzbSI6ImQxZTUyNDVlMTg2OGM4MGZiNmIwZmE4MWU4NTA1YTBkYTZjYmI1NDdlNjA1YmQwYmM4ODE5MDAxNGY0Njg1OTUyMDE4YjNhOGI0MTlhOTJkNDRkMzQ4YjJlY2VkNmNlNTc4NTdiOWQ4NzBiMTQ1NjI1OWFkYWUzYTdjY2Q1Y2IyW0FMTHxGT1JLSUR8VVRYT1NdIDAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2MyIsImhleCI6IjQxZDFlNTI0NWUxODY4YzgwZmI2YjBmYTgxZTg1MDVhMGRhNmNiYjU0N2U2MDViZDBiYzg4MTkwMDE0ZjQ2ODU5NTIwMThiM2E4YjQxOWE5MmQ0NGQzNDhiMmVjZWQ2Y2U1Nzg1N2I5ZDg3MGIxNDU2MjU5YWRhZTNhN2NjZDVjYjI2MTIxMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJkYmFmODVjZjk4MWZiNjM5OWYwZWY4Zjk4MTViZmQ5ODcwYTA4NjQ4ZDExNTIzODcyM2MxMTgzMGM2YjI4OGVlIiwidm91dCI6Niwic2NyaXB0U2lnIjp7ImFzbSI6IjRhYmIzNjY1MGRjMDIwNjZlNWVlYWQwOWY3OTBhNjZlODg4Yjc2YTc1N2YwZTZkNWFkYWNhZGNiZjA1NGM0YjdkYTAyMjk0ZmRhYjA5ZmIwMGM3OGE3NGFiNThlNTM1NjQzZjcwMmNjNjgzYjkwYTI3OWU1YWQyNWM4MmUyMTM2W0FMTHxGT1JLSUR8VVRYT1NdIDAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2MyIsImhleCI6IjQxNGFiYjM2NjUwZGMwMjA2NmU1ZWVhZDA5Zjc5MGE2NmU4ODhiNzZhNzU3ZjBlNmQ1YWRhY2FkY2JmMDU0YzRiN2RhMDIyOTRmZGFiMDlmYjAwYzc4YTc0YWI1OGU1MzU2NDNmNzAyY2M2ODNiOTBhMjc5ZTVhZDI1YzgyZTIxMzY2MTIxMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjoxMDMuNjU3MzIxMDIsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnMGM5aGxrZG11Nm5zYTNkeDdsZ200ZDVmenJ3azB1dmN5dWFjOW1lcWswNG11cncza2d5aGY1bW43eCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ4NjE1NjAifX0seyJ2YWx1ZSI6OTIuMzc1MzY0MDcsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDFlZTQxNDJmZjkyOGI0YWE2OTJiM2RlOGRlMjkyOWJlMzkwNWM2MDI3ZDNiYThiMGEyY2YxYzBiNTQ4N2FjYjAgT1BfRVFVQUwiLCJoZXgiOiJhYTIwMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHYwd2c5cDBseTV0ZjJuZjl2NzczaDNmOXhscmpwd3hxZjduaDI5czV0ODNjejY1czdrdHFhaHI5eWFnZCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQzMzI0MzMifX0seyJ2YWx1ZSI6NzEuMjczMjAxMiwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNiBPUF9FUVVBTCIsImhleCI6ImFhMjAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmswbjA2ZDR2ejZ5ZGZwbXNuemp3cHN5Y2hsYTNtcGdlNWdweDlteHlqMmMwYWh2c3FydmRmeGN2N3lrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzM0MjczNiJ9fSx7InZhbHVlIjozNi4xMTg5OTY0MSwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTY5Mzk5MiJ9fSx7InZhbHVlIjoxNy41MjgxMjA4NSwibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZCBPUF9FUVVBTCIsImhleCI6ImFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnN0cWttNTRkdHZucHlxeHQ1bTVuN3Nqc240ZW5ybHhjNTI2eHl4bG5qa2F5Y2R6ZmV1NjlyZXl6bXF4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODIyMDc1In19LHsidmFsdWUiOjhlLTA2LCJuIjo1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFmMGVkY3hsZm56a3RtNzdlcWc4NzRqdXAyOHRhNmE2dTVyeTVzZnU5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzM0ODEifX0seyJ2YWx1ZSI6MzYuNDI0MzExOTgsIm4iOjYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX19XSwiZmVlIjoxLjM2MmUtMDUsImhleCI6IjAyMDAwMDAwMDdlZTg4YjJjNjMwMThjMTIzODcyMzE1ZDE0ODg2YTA3MDk4ZmQ1YjgxZjlmODBlOWYzOWI2MWY5OGNmODVhZmRiMDAwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGRlN2FlN2IxMGQ2ZDZkOWJiYzAxNzBhYjJjMmVkNDQ1MGVkNGNiMTg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmZWU4OGIyYzYzMDE4YzEyMzg3MjMxNWQxNDg4NmEwNzA5OGZkNWI4MWY5ZjgwZTlmMzliNjFmOThjZjg1YWZkYjAxMDAwMDAwNDU0NDc0NjM3NmE5MTQ3MWE3MTQ5ZWUwYjRlMTQ4ZDc3YWFkYjRjMGU3MWExMmYwZjM4MmE5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZmVlODhiMmM2MzAxOGMxMjM4NzIzMTVkMTQ4ODZhMDcwOThmZDViODFmOWY4MGU5ZjM5YjYxZjk4Y2Y4NWFmZGIwMjAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZlZTg4YjJjNjMwMThjMTIzODcyMzE1ZDE0ODg2YTA3MDk4ZmQ1YjgxZjlmODBlOWYzOWI2MWY5OGNmODVhZmRiMDMwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDFiMjEzNDliODAxOWVjNzM4YThkYzZmMmZjNDJkYWNiYjA2NDQxZDA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmZWU4OGIyYzYzMDE4YzEyMzg3MjMxNWQxNDg4NmEwNzA5OGZkNWI4MWY5ZjgwZTlmMzliNjFmOThjZjg1YWZkYjA0MDAwMDAwNDU0NDc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjQ4ZmNkZDQwNDBiOWU5MWFkZTNiMWUzMDY4ZDQ0MGJlOGNlYzdlOGFiNGVkYTZlNTUwNTU3YjUxNzA0ZWUwNjcwYTAwMDAwMDY0NDFkMWU1MjQ1ZTE4NjhjODBmYjZiMGZhODFlODUwNWEwZGE2Y2JiNTQ3ZTYwNWJkMGJjODgxOTAwMTRmNDY4NTk1MjAxOGIzYThiNDE5YTkyZDQ0ZDM0OGIyZWNlZDZjZTU3ODU3YjlkODcwYjE0NTYyNTlhZGFlM2E3Y2NkNWNiMjYxMjEwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjNmZmZmZmZmZmVlODhiMmM2MzAxOGMxMjM4NzIzMTVkMTQ4ODZhMDcwOThmZDViODFmOWY4MGU5ZjM5YjYxZjk4Y2Y4NWFmZGIwNjAwMDAwMDY0NDE0YWJiMzY2NTBkYzAyMDY2ZTVlZWFkMDlmNzkwYTY2ZTg4OGI3NmE3NTdmMGU2ZDVhZGFjYWRjYmYwNTRjNGI3ZGEwMjI5NGZkYWIwOWZiMDBjNzhhNzRhYjU4ZTUzNTY0M2Y3MDJjYzY4M2I5MGEyNzllNWFkMjVjODJlMjEzNjYxMjEwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjNmZmZmZmZmZjA3MDY4NWQ4NjkwMjAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTc4MmU0YTAwYWEyMDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQ4Nzk3OWU5OTI2MDIwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmU5MTFiNDIwMGFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODczODQ2ZDJhODAxMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlOTAwMTMzMDBhYTIwMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNjg3ZjkzNjQ5ZDcwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTI4ZDkxOTAwYWEyMDY3YWQxYzY0OWIwOWJhNThiYTE5ZWIzYWQzNWY0MjQxYmEwNjBlOGJiODk3YWRjZjZiZGRkMzZhODBkOGU4YTY4NzM1Y2E3OTY4MDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmUzYjhiMGMwMGFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODcyMDAzMDAwMDAwMDAwMDAwM2VlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkYzk4Mjc2YTkxNDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDc4OGFjZGUxNjFiZDkwMDAwMDAwMDE5NzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMyYTAwMDAwMCJ9LHsidHhpZCI6IjBiMTJlMWZiZjdkNDNmZGU4NTQ2YWMwNzJiYTg1NjE1NzQ2NDg4ZGI5ZmIyNDc4NjQxZmQ5Mzk4Y2RmNzgyMTgiLCJoYXNoIjoiMGIxMmUxZmJmN2Q0M2ZkZTg1NDZhYzA3MmJhODU2MTU3NDY0ODhkYjlmYjI0Nzg2NDFmZDkzOThjZGY3ODIxOCIsInZlcnNpb24iOjEsInNpemUiOjI5MywibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImUzYzUxZmQ1Nzc2ZmQ5NGI1ZDkyNmQwYzlmZGVhNjRkYjNjNmE5NTc1ZDFmMTQxNWE5ZDlhM2NmMDE0MWEyMTMiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAxYTQ0YTQyNGQ2NWQxNWU5MzA0NTAxZTU0NTg1OTI4NTg0ZjljODkwOWFlNjVkYzVkNWY1NTcxYzZjZjZjZWE5MDIyMDFkMGJmMmYzYzI3ZDIyOTQ4YmIxZDk4ZGEwMDNkYTE3MGFlYzBmMDUxNjhlOGVhMzY0YzdmOTcyMjk4OTUzMTVbQUxMfEZPUktJRF0gMDIwOGM2YjZkOTdjYTNkNjI1ZmJhNTZiNjQ2MGJkY2NlYjNlMDZmNTU1NWMxMTg0NTkwYjMzMzljMGY1ODc5YmUxIiwiaGV4IjoiNDczMDQ0MDIyMDFhNDRhNDI0ZDY1ZDE1ZTkzMDQ1MDFlNTQ1ODU5Mjg1ODRmOWM4OTA5YWU2NWRjNWQ1ZjU1NzFjNmNmNmNlYTkwMjIwMWQwYmYyZjNjMjdkMjI5NDhiYjFkOThkYTAwM2RhMTcwYWVjMGYwNTE2OGU4ZWEzNjRjN2Y5NzIyOTg5NTMxNTQxMjEwMjA4YzZiNmQ5N2NhM2Q2MjVmYmE1NmI2NDYwYmRjY2ViM2UwNmY1NTU1YzExODQ1OTBiMzMzOWMwZjU4NzliZTEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuODQ5ODM4NTgsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5MGYyMjg1Mzk1MTk5MzE1NDBmMGUxZGUyNWRiYzAwOTM0OThjN2E1IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemcweTJ6bmo1dmV4OTJxN3JzYXVmd21jcXluZnh4ODU1ZDg2NmN5NnQiXX19LHsidmFsdWUiOjAuMTIzMDMzNzUsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3NTE5YjU2Njk5Njc5N2ZlOWNjMTFhYWNiMzI1ZGY4MGEzMTM3ODIwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NzUxOWI1NjY5OTY3OTdmZTljYzExYWFjYjMyNWRmODBhMzEzNzgyMDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDYzbmR0eG45bmUwbDV1Y3lkMmV2ZTltN3EyeHltY3lxamdsemVtZDYiXX19LHsidmFsdWUiOjAuMDgwNDc2NDUsIm4iOjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBiMTBkZTRlODk3ZGUwMGYzNTFkN2M4NzljZGYwNGZlMDc1NzA3NDkzIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YjEwZGU0ZTg5N2RlMDBmMzUxZDdjODc5Y2RmMDRmZTA3NTcwNzQ5Mzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemNzbWU4Z2psMHFwdTYzNmx5OG5uMHNmbHM4MnVyNWp2azJ4Y3k3dDkiXX19LHsidmFsdWUiOjAuMDQ4MTY2MjUsIm4iOjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3MGFjYTZhZDQyYzllYjRjNWJiMjBiZTkxYmU0NTQ4ODk1NDNjNjk2IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NzBhY2E2YWQ0MmM5ZWI0YzViYjIwYmU5MWJlNDU0ODg5NTQzYzY5Njg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGMyZWY0ZGd0eTdrbnpta2c5N2p4bHkyanlmMnM3eGpjNDZqeWdhbGwiXX19XSwiZmVlIjo1LjllLTA2LCJoZXgiOiIwMTAwMDAwMDAxMTNhMjQxMDFjZmEzZDlhOTE1MTQxZjVkNTdhOWM2YjM0ZGE2ZGU5ZjBjNmQ5MjVkNGJkOTZmNzdkNTFmYzVlMzAwMDAwMDAwNmE0NzMwNDQwMjIwMWE0NGE0MjRkNjVkMTVlOTMwNDUwMWU1NDU4NTkyODU4NGY5Yzg5MDlhZTY1ZGM1ZDVmNTU3MWM2Y2Y2Y2VhOTAyMjAxZDBiZjJmM2MyN2QyMjk0OGJiMWQ5OGRhMDAzZGExNzBhZWMwZjA1MTY4ZThlYTM2NGM3Zjk3MjI5ODk1MzE1NDEyMTAyMDhjNmI2ZDk3Y2EzZDYyNWZiYTU2YjY0NjBiZGNjZWIzZTA2ZjU1NTVjMTE4NDU5MGIzMzM5YzBmNTg3OWJlMWZmZmZmZmZmMDQzMmMwMTAwNTAwMDAwMDAwMTk3NmE5MTQ5MGYyMjg1Mzk1MTk5MzE1NDBmMGUxZGUyNWRiYzAwOTM0OThjN2E1ODhhYzBmYmNiYjAwMDAwMDAwMDAxOTc2YTkxNDc1MTliNTY2OTk2Nzk3ZmU5Y2MxMWFhY2IzMjVkZjgwYTMxMzc4MjA4OGFjMWRjYzdhMDAwMDAwMDAwMDE5NzZhOTE0YjEwZGU0ZTg5N2RlMDBmMzUxZDdjODc5Y2RmMDRmZTA3NTcwNzQ5Mzg4YWNmMTdlNDkwMDAwMDAwMDAwMTk3NmE5MTQ3MGFjYTZhZDQyYzllYjRjNWJiMjBiZTkxYmU0NTQ4ODk1NDNjNjk2ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiMGQxYzlkMDQyYWI2YmYzMzIyYmY2MzUyMTI2ZmVlOWM1YjljNmU5ZTA5ZGY4N2FkNTgxYjA0ZDc5ZGNmODBkZCIsImhhc2giOiIwZDFjOWQwNDJhYjZiZjMzMjJiZjYzNTIxMjZmZWU5YzViOWM2ZTllMDlkZjg3YWQ1ODFiMDRkNzlkY2Y4MGRkIiwidmVyc2lvbiI6Miwic2l6ZSI6MTY0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiZDZmNjRmMDRjM2VjNTM0MjVjOGIyODk0ZGVlY2ZmNTYwMmVhOTY2YmYxOTMwMDRiMjIxZTkxMjRmNWQ2OTBhZCIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIiwiaGV4IjoiNDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIn0sInNlcXVlbmNlIjoxfV0sInZvdXQiOlt7InZhbHVlIjowLjAwMDQyMjQ5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XSwiZmVlIjoxLjY0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMWFkOTBkNmY1MjQ5MTFlMjI0YjAwOTNmMTZiOTZlYTAyNTZmZmVjZGU5NDI4OGI1YzQyNTNlY2MzMDQ0ZmY2ZDYwMDAwMDAwMDQ1NDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxMDEwMDAwMDAwMTA5YTUwMDAwMDAwMDAwMDAyM2FhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODcwMDAwMDAwMCJ9LHsidHhpZCI6IjBlMDQxZWM1ODMwY2I3MmFhZTkzNzYyMThiMzNmYWVhMTg4MzU5OWFlZmE1N2Q5NTljZmE4ZjBkN2VkM2FhNjMiLCJoYXNoIjoiMGUwNDFlYzU4MzBjYjcyYWFlOTM3NjIxOGIzM2ZhZWExODgzNTk5YWVmYTU3ZDk1OWNmYThmMGQ3ZWQzYWE2MyIsInZlcnNpb24iOjIsInNpemUiOjIxMzYsImxvY2t0aW1lIjo0MiwidmluIjpbeyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZGU3YWU3YjEwZDZkNmQ5YmJjMDE3MGFiMmMyZWQ0NDUwZWQ0Y2IxODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZGU3YWU3YjEwZDZkNmQ5YmJjMDE3MGFiMmMyZWQ0NDUwZWQ0Y2IxODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjNGI3MjZmM2M4MjdiNzQ0NjVjNDFmNDFjMzBiN2FkM2UzMjkyZjViZmI3MDEyMTcxZTc0ZWNjYTUwYzU3YjJjIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ3MWE3MTQ5ZWUwYjRlMTQ4ZDc3YWFkYjRjMGU3MWExMmYwZjM4MmE5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ3MWE3MTQ5ZWUwYjRlMTQ4ZDc3YWFkYjRjMGU3MWExMmYwZjM4MmE5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImM0YjcyNmYzYzgyN2I3NDQ2NWM0MWY0MWMzMGI3YWQzZTMyOTJmNWJmYjcwMTIxNzFlNzRlY2NhNTBjNTdiMmMiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNGZlODI3YzY1MzgzMjA3Mzk3OGI3YmQ1OGVlYzg0MGVlNDRiYzY3MDg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNGZlODI3YzY1MzgzMjA3Mzk3OGI3YmQ1OGVlYzg0MGVlNDRiYzY3MDg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOjMsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0MWIyMTM0OWI4MDE5ZWM3MzhhOGRjNmYyZmM0MmRhY2JiMDY0NDFkMDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0MWIyMTM0OWI4MDE5ZWM3MzhhOGRjNmYyZmM0MmRhY2JiMDY0NDFkMDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjNGI3MjZmM2M4MjdiNzQ0NjVjNDFmNDFjMzBiN2FkM2UzMjkyZjViZmI3MDEyMTcxZTc0ZWNjYTUwYzU3YjJjIiwidm91dCI6NCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImM0YjcyNmYzYzgyN2I3NDQ2NWM0MWY0MWMzMGI3YWQzZTMyOTJmNWJmYjcwMTIxNzFlNzRlY2NhNTBjNTdiMmMiLCJ2b3V0Ijo1LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNGQwNzM2ZWU5MDExN2Y0ZWQ3ZTliYWQ1YWUwOWIwYWIwYzg5Mjk2YjQ4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNGQwNzM2ZWU5MDExN2Y0ZWQ3ZTliYWQ1YWUwOWIwYWIwYzg5Mjk2YjQ4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOjYsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0Y2Q2ZDU2N2RjNjM0YWM4ZmE4ODU2MWU4N2IyOGY5MTE4YThhZGE3YTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0Y2Q2ZDU2N2RjNjM0YWM4ZmE4ODU2MWU4N2IyOGY5MTE4YThhZGE3YTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjNGI3MjZmM2M4MjdiNzQ0NjVjNDFmNDFjMzBiN2FkM2UzMjkyZjViZmI3MDEyMTcxZTc0ZWNjYTUwYzU3YjJjIiwidm91dCI6Nywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRhNzNkNzA4OWU0OGFiZjM3YjRkYjE1Y2ZlYjRiZDQ1N2I1YTEwODFmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRhNzNkNzA4OWU0OGFiZjM3YjRkYjE1Y2ZlYjRiZDQ1N2I1YTEwODFmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImM0YjcyNmYzYzgyN2I3NDQ2NWM0MWY0MWMzMGI3YWQzZTMyOTJmNWJmYjcwMTIxNzFlNzRlY2NhNTBjNTdiMmMiLCJ2b3V0Ijo4LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNGE3ODdhMjc3NGNiMWMzYjU4N2Y5MDQ4ZjY2ZTdkYzlmNTc0MzQ0NzM4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNGE3ODdhMjc3NGNiMWMzYjU4N2Y5MDQ4ZjY2ZTdkYzlmNTc0MzQ0NzM4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzk5Mjk5OTRiZmRjMjY3YThlYjYxYWQ0NmI5Njc2NjYwNDc2M2U1NTZhMTQ5MTM3NTM4NjM0NDllOGM1MDE2MyIsInZvdXQiOjIyLCJzY3JpcHRTaWciOnsiYXNtIjoiNmJkNTZjMjlhN2YyMTkxMWVmYTM5ZjFmMjJmOWNlOWY5ZGMzZGMzY2MwNjFmMmM4OTE3N2YwYzc4Zjk1MDc5ZDgwNDJiNjk0MTc4ZDg1NjJjMGVmNjY3YzY5NTY5MjQ2MDQwYWNmZTEwYzIwMmM4ZjE3ODRkOTk4OWE3NWVhYzVbQUxMfEZPUktJRHxVVFhPU10gMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzIiwiaGV4IjoiNDE2YmQ1NmMyOWE3ZjIxOTExZWZhMzlmMWYyMmY5Y2U5ZjlkYzNkYzNjYzA2MWYyYzg5MTc3ZjBjNzhmOTUwNzlkODA0MmI2OTQxNzhkODU2MmMwZWY2NjdjNjk1NjkyNDYwNDBhY2ZlMTBjMjAyYzhmMTc4NGQ5OTg5YTc1ZWFjNTYxMjEwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImM0YjcyNmYzYzgyN2I3NDQ2NWM0MWY0MWMzMGI3YWQzZTMyOTJmNWJmYjcwMTIxNzFlNzRlY2NhNTBjNTdiMmMiLCJ2b3V0IjoxMCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwMmQ4M2NmOTQ3NGE1NjdlNDU4ZjM4OGUxN2QyN2Y4ZDNjYTBhYTJkMDc2MGE5NzYwMDI5YTJkOTg1ZDg4YzhhMDM3MzMxYzg1MWMxNGUzYThkNTU2NzM3ODFkYzM0YzlhZjQ4NGU2ODZiODYxZmJhNWM1NDAyZTYwYjA3NDZhW0FMTHxGT1JLSUR8VVRYT1NdIDAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2MyIsImhleCI6IjQxMzAyZDgzY2Y5NDc0YTU2N2U0NThmMzg4ZTE3ZDI3ZjhkM2NhMGFhMmQwNzYwYTk3NjAwMjlhMmQ5ODVkODhjOGEwMzczMzFjODUxYzE0ZTNhOGQ1NTY3Mzc4MWRjMzRjOWFmNDg0ZTY4NmI4NjFmYmE1YzU0MDJlNjBiMDc0NmE2MTIxMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjoxMDMuMTkzOTg5NywibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NCBPUF9FUVVBTCIsImhleCI6ImFhMjA1MGZjMTZmZmIzNzdjZDRlMWQ4YjRkZWZhMzc1NmQxMjIxYmFjZmUzMzA0ZTc3MDVkZTQxNjdkNzdjMWJhMzY0ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGcwYzlobGtkbXU2bnNhM2R4N2xnbTRkNWZ6cndrMHV2Y3l1YWM5bWVxazA0bXVydzNrZ3loZjVtbjd4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDg4MzQ1NCJ9fSx7InZhbHVlIjo5MS45NjI0NjI5NiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMCBPUF9FUVVBTCIsImhleCI6ImFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjB3ZzlwMGx5NXRmMm5mOXY3NzNoM2Y5eGxyanB3eHFmN25oMjlzNXQ4M2N6NjVzN2t0cWFocjl5YWdkIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDM1MTk0NCJ9fSx7InZhbHVlIjo3MC45NTQ2NDI0MSwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNiBPUF9FUVVBTCIsImhleCI6ImFhMjAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmswbjA2ZDR2ejZ5ZGZwbXNuemp3cHN5Y2hsYTNtcGdlNWdweDlteHlqMmMwYWh2c3FydmRmeGN2N3lrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzM1Nzc4OSJ9fSx7InZhbHVlIjozNS45NTc1NDc4OCwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTcwMTYyMSJ9fSx7InZhbHVlIjoxNy40NDk3Nzc0LCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDIwYjA1Yjc0YWI1NmM5ODQ4MDMyZTliYTRmZDA5NDI3NWNjYzdmMzYyOGFkMTg4NmZjZTU2ZTkzMGQxMjczY2Q4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2c3Rxa201NGR0dm5weXF4dDVtNW43c2pzbjRlbnJseGM1MjZ4eXhsbmprYXljZHpmZXU2OXJleXptcXgiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI4MjU3NzcifX0seyJ2YWx1ZSI6MTEuMzE2MDI3NSwibiI6NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYiBPUF9FUVVBTCIsImhleCI6ImFhMjBiMzQ3ZDY4YzhkMmNkYjMyMDY2YmU5YWY4MDRjNmZmYjhhYmJiNDQxYmRmMzZjYWYzODM0MzZiY2ZjYWEzNWNiODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd2U1MDQ1djM1a2RrdnN4ZDA1NmxxenZkbGFjNHdhNWd4N2x4bTkwOHE2cmQwOHU0ZzZ1a24zNzJnZ3dsIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTM1NTEwIn19LHsidmFsdWUiOjEwLjUxNDEzMDY3LCJuIjo2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDdjMmJjY2EyMDg5YTlmMGY4ZGFlYzBkM2I0NWE2YzUyNTgxYzU2NzY2NWNjNTFlYmRhOWEyMDJkOGEzM2RjZmM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkN3pobjl6cHpkZjdydWQ0bXFkOGR6NmQzZjlzOHprd2VqdWM1MHRtMmR6cXR2MngwdzBjNTR5MzlocDYiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0OTc1NjEifX0seyJ2YWx1ZSI6NS4yMDIwNjI5MiwibiI6Nywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgZjFjMzNiYzdkYTA5OTFkMjZmOGJiZjE1MGM5MDM1NjhiMmMzZGM4NzI3MzcwODYzZjRjM2Q5Zjk2NTViMzc5MSBPUF9FUVVBTCIsImhleCI6ImFhMjBmMWMzM2JjN2RhMDk5MWQyNmY4YmJmMTUwYzkwMzU2OGIyYzNkYzg3MjczNzA4NjNmNGMzZDlmOTY1NWIzNzkxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMGN1eHc3OG1neWVyNW4wM3dsMzJyeXN4NDV0OXM3dXN1bm53enJyN25wYW43dDl0dm1lenJhdTNqdXI5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjQ2MTc4In19LHsidmFsdWUiOjQuMzkwMDcwMDgsIm4iOjgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwM2JiMjRmZjkwYjIyMTlhMjYwZDBlZTBhMTY4NjdmZDMzNDY5Y2Y3MDRmNDkwMjIzMWIxNGU5NjNlY2JhZTNjNDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHZhbXlubGVwdjNwbmducTZyaHE1OTV4MGxmbmc2dzB3cDg1anEzcnJ2MndqY2x2aHQzdWd4NGthajRodSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjIwNzc1MiJ9fSx7InZhbHVlIjo4ZS0wNiwibiI6OSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZjBlZGN4bGZuemt0bTc3ZXFnODc0anVwMjh0YTZhNnU1cnk1c2Z1OSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjEwMjc2MiJ9fSx7InZhbHVlIjozOC4yMjU0NjY2LCJuIjoxMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZjBlZGN4bGZuemt0bTc3ZXFnODc0anVwMjh0YTZhNnU1cnk1c2Z1OSJdfX1dLCJmZWUiOjIuMTM2ZS0wNSwiaGV4IjoiMDIwMDAwMDAwYjJjN2JjNTUwY2FlYzc0MWUxNzEyNzBmYjViMmYyOWUzZDM3YTBiYzM0MTFmYzQ2NTQ0YjcyN2M4ZjMyNmI3YzQwMDAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZGU3YWU3YjEwZDZkNmQ5YmJjMDE3MGFiMmMyZWQ0NDUwZWQ0Y2IxODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYyYzdiYzU1MGNhZWM3NDFlMTcxMjcwZmI1YjJmMjllM2QzN2EwYmMzNDExZmM0NjU0NGI3MjdjOGYzMjZiN2M0MDEwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDcxYTcxNDllZTBiNGUxNDhkNzdhYWRiNGMwZTcxYTEyZjBmMzgyYTk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmMmM3YmM1NTBjYWVjNzQxZTE3MTI3MGZiNWIyZjI5ZTNkMzdhMGJjMzQxMWZjNDY1NDRiNzI3YzhmMzI2YjdjNDAyMDAwMDAwNDU0NDc0NjM3NmE5MTRmZTgyN2M2NTM4MzIwNzM5NzhiN2JkNThlZWM4NDBlZTQ0YmM2NzA4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjJjN2JjNTUwY2FlYzc0MWUxNzEyNzBmYjViMmYyOWUzZDM3YTBiYzM0MTFmYzQ2NTQ0YjcyN2M4ZjMyNmI3YzQwMzAwMDAwMDQ1NDQ3NDYzNzZhOTE0MWIyMTM0OWI4MDE5ZWM3MzhhOGRjNmYyZmM0MmRhY2JiMDY0NDFkMDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYyYzdiYzU1MGNhZWM3NDFlMTcxMjcwZmI1YjJmMjllM2QzN2EwYmMzNDExZmM0NjU0NGI3MjdjOGYzMjZiN2M0MDQwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDJhMThlZjA2YjFiNzFiMmUzNzk3YTk2YWNlOTBhMDRiOWIyOGMxMmI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmMmM3YmM1NTBjYWVjNzQxZTE3MTI3MGZiNWIyZjI5ZTNkMzdhMGJjMzQxMWZjNDY1NDRiNzI3YzhmMzI2YjdjNDA1MDAwMDAwNDU0NDc0NjM3NmE5MTRkMDczNmVlOTAxMTdmNGVkN2U5YmFkNWFlMDliMGFiMGM4OTI5NmI0ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjJjN2JjNTUwY2FlYzc0MWUxNzEyNzBmYjViMmYyOWUzZDM3YTBiYzM0MTFmYzQ2NTQ0YjcyN2M4ZjMyNmI3YzQwNjAwMDAwMDQ1NDQ3NDYzNzZhOTE0Y2Q2ZDU2N2RjNjM0YWM4ZmE4ODU2MWU4N2IyOGY5MTE4YThhZGE3YTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYyYzdiYzU1MGNhZWM3NDFlMTcxMjcwZmI1YjJmMjllM2QzN2EwYmMzNDExZmM0NjU0NGI3MjdjOGYzMjZiN2M0MDcwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGE3M2Q3MDg5ZTQ4YWJmMzdiNGRiMTVjZmViNGJkNDU3YjVhMTA4MWY4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmMmM3YmM1NTBjYWVjNzQxZTE3MTI3MGZiNWIyZjI5ZTNkMzdhMGJjMzQxMWZjNDY1NDRiNzI3YzhmMzI2YjdjNDA4MDAwMDAwNDU0NDc0NjM3NmE5MTRhNzg3YTI3NzRjYjFjM2I1ODdmOTA0OGY2NmU3ZGM5ZjU3NDM0NDczODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjYzMDFjNWU4NDkzNDg2NTMzNzkxMTQ2YTU1M2U3NjA0NjY3Njk2NmJkNDFhYjY4ZTdhMjZkY2JmOTQ5OTkyNzkxNjAwMDAwMDY0NDE2YmQ1NmMyOWE3ZjIxOTExZWZhMzlmMWYyMmY5Y2U5ZjlkYzNkYzNjYzA2MWYyYzg5MTc3ZjBjNzhmOTUwNzlkODA0MmI2OTQxNzhkODU2MmMwZWY2NjdjNjk1NjkyNDYwNDBhY2ZlMTBjMjAyYzhmMTc4NGQ5OTg5YTc1ZWFjNTYxMjEwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjNmZmZmZmZmZjJjN2JjNTUwY2FlYzc0MWUxNzEyNzBmYjViMmYyOWUzZDM3YTBiYzM0MTFmYzQ2NTQ0YjcyN2M4ZjMyNmI3YzQwYTAwMDAwMDY0NDEzMDJkODNjZjk0NzRhNTY3ZTQ1OGYzODhlMTdkMjdmOGQzY2EwYWEyZDA3NjBhOTc2MDAyOWEyZDk4NWQ4OGM4YTAzNzMzMWM4NTFjMTRlM2E4ZDU1NjczNzgxZGMzNGM5YWY0ODRlNjg2Yjg2MWZiYTVjNTQwMmU2MGIwNzQ2YTYxMjEwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjNmZmZmZmZmZjBiM2E4ODE1NjcwMjAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZWZlODM0YTAwYWEyMDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQ4NzE4OTUyMzI0MDIwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVjODY3NDIwMGFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODczMTMxZWNhNjAxMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlNWQzYzMzMDBhYTIwMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNjg3MjRkZDUyZDYwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZWY1ZjYxOTAwYWEyMDY3YWQxYzY0OWIwOWJhNThiYTE5ZWIzYWQzNWY0MjQxYmEwNjBlOGJiODk3YWRjZjZiZGRkMzZhODBkOGU4YTY4NzRjM2YwMjY4MDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmViMTk5MGMwMGFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODczZWUzNzI0MzAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlZDYyYjA4MDBhYTIwYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYjg3NGI0YWFiM2UwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTk5OTcwNzAwYWEyMDdjMmJjY2EyMDg5YTlmMGY4ZGFlYzBkM2I0NWE2YzUyNTgxYzU2NzY2NWNjNTFlYmRhOWEyMDJkOGEzM2RjZmM4N2Q0YjcwMTFmMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVhMmMxMDMwMGFhMjBmMWMzM2JjN2RhMDk5MWQyNmY4YmJmMTUwYzkwMzU2OGIyYzNkYzg3MjczNzA4NjNmNGMzZDlmOTY1NWIzNzkxODcyMGI3MmExYTAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlODgyYjAzMDBhYTIwM2JiMjRmZjkwYjIyMTlhMjYwZDBlZTBhMTY4NjdmZDMzNDY5Y2Y3MDRmNDkwMjIzMWIxNGU5NjNlY2JhZTNjNDg3MjAwMzAwMDAwMDAwMDAwMDQwZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTZhOTEwMTAwNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWNlNDZlZDdlMzAwMDAwMDAwMTk3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYzJhMDAwMDAwIn0seyJ0eGlkIjoiMGZlNjJlYTY2MjYyYjZkMTExZDU0NDc3NDE2MTlhOGIzYWI3MGVmMWZlMzQ1OTU3MTM0NDk1ZWMzMzNkMGQ5MSIsImhhc2giOiIwZmU2MmVhNjYyNjJiNmQxMTFkNTQ0Nzc0MTYxOWE4YjNhYjcwZWYxZmUzNDU5NTcxMzQ0OTVlYzMzM2QwZDkxIiwidmVyc2lvbiI6MSwic2l6ZSI6MjI2LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiNGQwYTQ4NTQ5NWMxN2NhYzk0OWYwZGJiNmYzOTMwOTRmYTRiMTM2YjQ1MGRkOTQ4YTE3YzU4ZTYxZTc4NGY2MCIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwOWE3NDkzMjc2OGVmMGViMTI4MmVlNDc1OWY5NTliNmVmMTViZjNiYmYzNTU0MzMyMzkwNWIwNTk5MzQ3NDY5ODAyMjAwN2JlZDA5Y2U0YTE1MjlmY2YyNDYwMWNjNTRkMTdiNTk5ZDA1ZjdlYjE0ZGFlYWYyOTQwNjEzMmU3OTM4MTJkW0FMTHxGT1JLSURdIDAyZDhkNGY5NzgzNWU2ZWYxY2NjOGNlNDQwNjhmMDBkYzAzOTcyODU0NzMxZjgzMTAxY2ZhNzQzOGI0NjE3OWE1YiIsImhleCI6IjQ4MzA0NTAyMjEwMDlhNzQ5MzI3NjhlZjBlYjEyODJlZTQ3NTlmOTU5YjZlZjE1YmYzYmJmMzU1NDMzMjM5MDViMDU5OTM0NzQ2OTgwMjIwMDdiZWQwOWNlNGExNTI5ZmNmMjQ2MDFjYzU0ZDE3YjU5OWQwNWY3ZWIxNGRhZWFmMjk0MDYxMzJlNzkzODEyZDQxMjEwMmQ4ZDRmOTc4MzVlNmVmMWNjYzhjZTQ0MDY4ZjAwZGMwMzk3Mjg1NDczMWY4MzEwMWNmYTc0MzhiNDYxNzlhNWIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMTUyMjgxNjcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5NWJlZDkwMTgxYzQwYzE3MzBhYTRiNjdhMTdkOWJkNTQ4NDkwODU4IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OTViZWQ5MDE4MWM0MGMxNzMwYWE0YjY3YTE3ZDliZDU0ODQ5MDg1ODg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxejJtYWtncHM4enFjOWVzNGY5azBndGFuMDI1c2pnZ3RxaGw0cGx5eGEiXX19LHsidmFsdWUiOjEuNjk2ODg4NTEsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA0NzQ0OWVlOGUzMGJiYmE1OGU5MjYyZTQ4MjM2NGRiNjc0ZTVhYzE5IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NDc0NDllZThlMzBiYmJhNThlOTI2MmU0ODIzNjRkYjY3NGU1YWMxOTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcHI1ZjhoZ3V2OW1oZnZ3amYzd2ZxM2tma204ZmVkdnJ5a3d6d2EwNjkiXX19XSwiZmVlIjoyLjI2ZS0wNiwiaGV4IjoiMDEwMDAwMDAwMTYwNGY3ODFlZTY1ODdjYTE0OGQ5MGQ0NTZiMTM0YmZhOTQzMDM5NmZiYjBkOWY5NGFjN2NjMTk1NTQ0ODBhNGQwMTAwMDAwMDZiNDgzMDQ1MDIyMTAwOWE3NDkzMjc2OGVmMGViMTI4MmVlNDc1OWY5NTliNmVmMTViZjNiYmYzNTU0MzMyMzkwNWIwNTk5MzQ3NDY5ODAyMjAwN2JlZDA5Y2U0YTE1MjlmY2YyNDYwMWNjNTRkMTdiNTk5ZDA1ZjdlYjE0ZGFlYWYyOTQwNjEzMmU3OTM4MTJkNDEyMTAyZDhkNGY5NzgzNWU2ZWYxY2NjOGNlNDQwNjhmMDBkYzAzOTcyODU0NzMxZjgzMTAxY2ZhNzQzOGI0NjE3OWE1YmZmZmZmZmZmMDIwNzVkZTgwMDAwMDAwMDAwMTk3NmE5MTQ5NWJlZDkwMTgxYzQwYzE3MzBhYTRiNjdhMTdkOWJkNTQ4NDkwODU4ODhhYzEzM2YxZDBhMDAwMDAwMDAxOTc2YTkxNDQ3NDQ5ZWU4ZTMwYmJiYTU4ZTkyNjJlNDgyMzY0ZGI2NzRlNWFjMTk4OGFjMDAwMDAwMDAifSx7InR4aWQiOiIxMDk5ZDQ2NGM5OWYzNGQyMjY2NjA5MDYyOTc3ODEzODAzMzg5NzA4ZDk2M2RhNTAxMzgxMDRlOTE2MGJmZTQ0IiwiaGFzaCI6IjEwOTlkNDY0Yzk5ZjM0ZDIyNjY2MDkwNjI5Nzc4MTM4MDMzODk3MDhkOTYzZGE1MDEzODEwNGU5MTYwYmZlNDQiLCJ2ZXJzaW9uIjoxLCJzaXplIjoxOTIsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiJlZGFmMGViMjQ2NDdhNjc5ZWU4OGQ3YTA0MDk3ZTZmODlhMWY5MWIzZWNlMGUyYjBlYjlmNTZkOTkxZDU4NzdkIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBkMGNhZDUzYTQ2NGYxNTVhNmM3NDg0YTNjMzQyNmY5ZjkwNDZlOGQ2NTRiODY0ZjY0ZDU0YmRlY2VlMjVhYThkMDIyMDIwMmE1YTI2OWFiMjBmYmM4OGNkYWFkZWJkYWQxMzE3ZmMyYzI1ZDQxOGU4NDEzOTUzYWVmNWQ1MjkzYWY2MDdbQUxMfEZPUktJRF0gMDI1ZGI4OWExOTQ3NzM1ZDMyZjE1MTBmZGRjODMyNTgwOWNiOTY4ZTk4NGM0YTY0ZjBhNjEwM2YyMTY3MjcwN2FhIiwiaGV4IjoiNDgzMDQ1MDIyMTAwZDBjYWQ1M2E0NjRmMTU1YTZjNzQ4NGEzYzM0MjZmOWY5MDQ2ZThkNjU0Yjg2NGY2NGQ1NGJkZWNlZTI1YWE4ZDAyMjAyMDJhNWEyNjlhYjIwZmJjODhjZGFhZGViZGFkMTMxN2ZjMmMyNWQ0MThlODQxMzk1M2FlZjVkNTI5M2FmNjA3NDEyMTAyNWRiODlhMTk0NzczNWQzMmYxNTEwZmRkYzgzMjU4MDljYjk2OGU5ODRjNGE2NGYwYTYxMDNmMjE2NzI3MDdhYSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4zNjk5MTczOSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDNmNjJjOGYzYTVhMTkwMTZhNzhjNTU1ZTFlN2ZhMjFlMzlmOGQ1NmIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQzZjYyYzhmM2E1YTE5MDE2YTc4YzU1NWUxZTdmYTIxZTM5ZjhkNTZiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxbGs5ajhuNWtzZXE5NDgzMzI0dThubDVnMHJuN3g0ZHZxZjlrZGF6ayJdfX1dLCJmZWUiOjAuMDAwMTQ0LCJoZXgiOiIwMTAwMDAwMDAxN2Q4N2Q1OTFkOTU2OWZlYmIwZTJlMGVjYjM5MTFmOWFmOGU2OTc0MGEwZDc4OGVlNzlhNjQ3NDZiMjBlYWZlZDAwMDAwMDAwNmI0ODMwNDUwMjIxMDBkMGNhZDUzYTQ2NGYxNTVhNmM3NDg0YTNjMzQyNmY5ZjkwNDZlOGQ2NTRiODY0ZjY0ZDU0YmRlY2VlMjVhYThkMDIyMDIwMmE1YTI2OWFiMjBmYmM4OGNkYWFkZWJkYWQxMzE3ZmMyYzI1ZDQxOGU4NDEzOTUzYWVmNWQ1MjkzYWY2MDc0MTIxMDI1ZGI4OWExOTQ3NzM1ZDMyZjE1MTBmZGRjODMyNTgwOWNiOTY4ZTk4NGM0YTY0ZjBhNjEwM2YyMTY3MjcwN2FhZmZmZmZmZmYwMWZiNzIzNDAyMDAwMDAwMDAxOTc2YTkxNDNmNjJjOGYzYTVhMTkwMTZhNzhjNTU1ZTFlN2ZhMjFlMzlmOGQ1NmI4OGFjMDAwMDAwMDAifSx7InR4aWQiOiIxMzU2M2FhODdkMDgwMzhlZTcyMzg3ZGI5N2ZlNTI5YTEwMWJkMTQ3NTQxY2JkZGU5MmI5ZjY0Y2FlODc5MmQxIiwiaGFzaCI6IjEzNTYzYWE4N2QwODAzOGVlNzIzODdkYjk3ZmU1MjlhMTAxYmQxNDc1NDFjYmRkZTkyYjlmNjRjYWU4NzkyZDEiLCJ2ZXJzaW9uIjoyLCJzaXplIjoxNjQsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiJkZDI1OTA2YmE0MjQwZDkxNjg0YzQxNmRiNjViNTU3NDQ5MGY4Y2Q3MzY1YTdkZGRlODgzZGRlMzllY2NiOGYxIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEiLCJoZXgiOiI0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEifSwic2VxdWVuY2UiOjF9XSwidm91dCI6W3sidmFsdWUiOjAuMDA1NjczMjIsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dLCJmZWUiOjEuNjRlLTA2LCJoZXgiOiIwMjAwMDAwMDAxZjFiOGNjOWVlM2RkODNlOGRkN2Q1YTM2ZDc4YzBmNDk3NDU1NWJiNjZkNDE0YzY4OTEwZDI0YTQ2YjkwMjVkZDAwMDAwMDAwNDU0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEwMTAwMDAwMDAxMWFhODA4MDAwMDAwMDAwMDIzYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NzAwMDAwMDAwIn0seyJ0eGlkIjoiMTQ1ODk1N2M3OTQ0NTU0ZDM0ZDZiMGFlZDVmYWJjMWEwMDUxMWVlOWNhOTFmMjFhOWNhNzk0M2ZmMGJlODAxMSIsImhhc2giOiIxNDU4OTU3Yzc5NDQ1NTRkMzRkNmIwYWVkNWZhYmMxYTAwNTExZWU5Y2E5MWYyMWE5Y2E3OTQzZmYwYmU4MDExIiwidmVyc2lvbiI6Miwic2l6ZSI6MTkxLCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiNjRiOTVkZTZiYzc4YjZhNjk1YmUyN2E1ODU1ZDMxNGEwZDBlOTM5NjdiMmJjZmU3MjNlYmZiNzYwZGU4MjNlYiIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDAzOGNlZDU0NmFkNDkyMGI3MGYxMGY2NzhiNjEzMzUzMjQ4OTJkOTFiYTgwMDA2YjliMzZlZTMzNWVjMGFjNjQwMjIwNzI1OGFkY2MyY2FkMWI4YjEyNDU4M2U3YjI2Njg3ODNhOTUwMDk5MGY5MmI0MTUwMDhhNjAyZWM4YjFhOGNjOVtBTEx8Rk9SS0lEXSAwMjg5ZjY1MWNjZTY4MDFhMGU0ZTRiYmM1ZGI0ODIxODU1YzY5OTM1NGVjZWFjYTBjMjg4MWYzYTYxMDM0M2ZmNjciLCJoZXgiOiI0NzMwNDQwMjIwMDM4Y2VkNTQ2YWQ0OTIwYjcwZjEwZjY3OGI2MTMzNTMyNDg5MmQ5MWJhODAwMDZiOWIzNmVlMzM1ZWMwYWM2NDAyMjA3MjU4YWRjYzJjYWQxYjhiMTI0NTgzZTdiMjY2ODc4M2E5NTAwOTkwZjkyYjQxNTAwOGE2MDJlYzhiMWE4Y2M5NDEyMTAyODlmNjUxY2NlNjgwMWEwZTRlNGJiYzVkYjQ4MjE4NTVjNjk5MzU0ZWNlYWNhMGMyODgxZjNhNjEwMzQzZmY2NyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MzkuOTk5NTkyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNjc2NGM5ZGJkY2I1YTM5YWFiMTI1Nzc1MmU3YThlYWEyNDZmNTkxOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDY3NjRjOWRiZGNiNWEzOWFhYjEyNTc3NTJlN2E4ZWFhMjQ2ZjU5MTg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBua2Zqd21tajY2OHg0dHpmdGgydG42MzY0emdtNmVycTl3amNyM253Il19fV0sImZlZSI6MS45NGUtMDYsImhleCI6IjAyMDAwMDAwMDFlYjIzZTgwZDc2ZmJlYjIzZTdjZjJiN2I5NjkzMGUwZDRhMzE1ZDg1YTUyN2JlOTVhNmI2NzhiY2U2NWRiOTY0MDEwMDAwMDA2YTQ3MzA0NDAyMjAwMzhjZWQ1NDZhZDQ5MjBiNzBmMTBmNjc4YjYxMzM1MzI0ODkyZDkxYmE4MDAwNmI5YjM2ZWUzMzVlYzBhYzY0MDIyMDcyNThhZGNjMmNhZDFiOGIxMjQ1ODNlN2IyNjY4NzgzYTk1MDA5OTBmOTJiNDE1MDA4YTYwMmVjOGIxYThjYzk0MTIxMDI4OWY2NTFjY2U2ODAxYTBlNGU0YmJjNWRiNDgyMTg1NWM2OTkzNTRlY2VhY2EwYzI4ODFmM2E2MTAzNDNmZjY3ZmZmZmZmZmYwMWEwODg2YWVlMDAwMDAwMDAxOTc2YTkxNDY3NjRjOWRiZGNiNWEzOWFhYjEyNTc3NTJlN2E4ZWFhMjQ2ZjU5MTg4OGFjMDAwMDAwMDAifSx7InR4aWQiOiIxNWQ1NjQxNTE5Yjc2ZWMyOTE0YjAwZDU5NDJhZDE2ZjhmNDU0OTBiMTQ3ODExMGM4OGE5YWIwMjI3ZWFiZWNkIiwiaGFzaCI6IjE1ZDU2NDE1MTliNzZlYzI5MTRiMDBkNTk0MmFkMTZmOGY0NTQ5MGIxNDc4MTEwYzg4YTlhYjAyMjdlYWJlY2QiLCJ2ZXJzaW9uIjoyLCJzaXplIjo3MjYsImxvY2t0aW1lIjo5MDAxLCJ2aW4iOlt7InR4aWQiOiJhZTIwZjczOTQxYjc2YjAyYmI0NjFjYWJmNDQ0ZTkzOWU1ZjczYmNkZTMwZDM0NTdiOTE2NDllZGQyMWM1ZjE2Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjA5N2M2YjExOWU3YjQ2NGE2ZDQwMmMzNGUxOWQwYWZkNTA3M2U4MzIzNDc5OTYzYjIxMDkwYzBmZTk4OTg3N2FmODM4MmVlMDEzMjNjMTNjN2ViYThjYTNhYjk0NWZkODEzMDJkZTUwNTlhN2FiODFmNGRmMTM0M2RlZDlkYzNhW0FMTHxGT1JLSURdIDAyMDU4YzNlNmY1ODk2YjViMWQyNTc2YWVkNDFlZDA2OTQwOGVhNzY1YmIxZTBjZjgzYjJhY2NhMGE3N2ExMGExMiIsImhleCI6IjQxMDk3YzZiMTE5ZTdiNDY0YTZkNDAyYzM0ZTE5ZDBhZmQ1MDczZTgzMjM0Nzk5NjNiMjEwOTBjMGZlOTg5ODc3YWY4MzgyZWUwMTMyM2MxM2M3ZWJhOGNhM2FiOTQ1ZmQ4MTMwMmRlNTA1OWE3YWI4MWY0ZGYxMzQzZGVkOWRjM2E0MTIxMDIwNThjM2U2ZjU4OTZiNWIxZDI1NzZhZWQ0MWVkMDY5NDA4ZWE3NjViYjFlMGNmODNiMmFjY2EwYTc3YTEwYTEyIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJhZTIwZjczOTQxYjc2YjAyYmI0NjFjYWJmNDQ0ZTkzOWU1ZjczYmNkZTMwZDM0NTdiOTE2NDllZGQyMWM1ZjE2Iiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjAgMjAyYTFjNzliZjI1OWZlODBlZDQyMTYyMzFiZDFkN2E2MzhhMWYzZTI4YmM0NzdlYzM2YjEwYWQ0NTQ1YWQzNDY2MjBmNmZmM2NhMDdhZDk2ZjI5NDRjOTM3ZTdjNDg3MDdmNWE1MTJiNjAwYzIyYjY4MDlhMjAzYjQwMWMzMWQzNjA5NTI3OTAwOWM2MzAwY2UwMTIwN2Y3NTg4YzBkMjc2ODI3NzVhYTI2OWMwY2Y3ODU2N2Y3NTgxN2M1NjdmNzU4MWEwNjk1ODdmNzc1MjdmNzU4MTAwYTA2OWMwY2NjMGM2YTI2OWMwY2RjMGM3ODhjMGQxYzBjZTg3Nzc3NzY3NTI3OTUxOWM2M2MwY2Y1NjdmNzc1MjdmNzU4MWMwY2NjMGM2N2I5M2EyNjljMGNkYzBjNzg4YzBkMWMwY2U4OGMwZDJjMGNmODc3Nzc3Nzc2NzdiNTI5ZDAwY2UwMTIwN2Y3NTdiODhjMGNkYzBjNzg4YzBkMWMwY2U4OGMwZDJjMGNmODc3NzY4NjgiLCJoZXgiOiIwMDRjYzcyMDJhMWM3OWJmMjU5ZmU4MGVkNDIxNjIzMWJkMWQ3YTYzOGExZjNlMjhiYzQ3N2VjMzZiMTBhZDQ1NDVhZDM0NjYyMGY2ZmYzY2EwN2FkOTZmMjk0NGM5MzdlN2M0ODcwN2Y1YTUxMmI2MDBjMjJiNjgwOWEyMDNiNDAxYzMxZDM2MDk1Mjc5MDA5YzYzMDBjZTAxMjA3Zjc1ODhjMGQyNzY4Mjc3NWFhMjY5YzBjZjc4NTY3Zjc1ODE3YzU2N2Y3NTgxYTA2OTU4N2Y3NzUyN2Y3NTgxMDBhMDY5YzBjY2MwYzZhMjY5YzBjZGMwYzc4OGMwZDFjMGNlODc3Nzc3Njc1Mjc5NTE5YzYzYzBjZjU2N2Y3NzUyN2Y3NTgxYzBjY2MwYzY3YjkzYTI2OWMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Nzc3NzY3N2I1MjlkMDBjZTAxMjA3Zjc1N2I4OGMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Njg2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYWUyMGY3Mzk0MWI3NmIwMmJiNDYxY2FiZjQ0NGU5MzllNWY3M2JjZGUzMGQzNDU3YjkxNjQ5ZWRkMjFjNWYxNiIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiJmMjAyMjljMWEzMmI1NWM5M2QxY2UzOTcyZTQ2ZTY5NDQ3NWYxYjVkN2IxNmUwYjM5ZTcxMGJlMzBiYWVjOTMwM2RkMWY2YTU0OTcyMjNmYmFkOGJmYTMzZDEwMmU5ZGRiZWQ2OTUwN2Y3YzI2MGE1MGE3NzA5MWRhM2VkNzdkZFtBTEx8Rk9SS0lEXSAwMjA1OGMzZTZmNTg5NmI1YjFkMjU3NmFlZDQxZWQwNjk0MDhlYTc2NWJiMWUwY2Y4M2IyYWNjYTBhNzdhMTBhMTIiLCJoZXgiOiI0MWYyMDIyOWMxYTMyYjU1YzkzZDFjZTM5NzJlNDZlNjk0NDc1ZjFiNWQ3YjE2ZTBiMzllNzEwYmUzMGJhZWM5MzAzZGQxZjZhNTQ5NzIyM2ZiYWQ4YmZhMzNkMTAyZTlkZGJlZDY5NTA3ZjdjMjYwYTUwYTc3MDkxZGEzZWQ3N2RkNDEyMTAyMDU4YzNlNmY1ODk2YjViMWQyNTc2YWVkNDFlZDA2OTQwOGVhNzY1YmIxZTBjZjgzYjJhY2NhMGE3N2ExMGExMiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6OGUtMDYsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3MmMwM2UzZWI3MDc2YTE0ODc3ZjYzOTlmMDJjZTMyYTUyZDNlMzdkIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NzJjMDNlM2ViNzA3NmExNDg3N2Y2Mzk5ZjAyY2UzMmE1MmQzZTM3ZDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGV2cTAzN2t1cms1OXk4MGEzZW51cHZ1djQ5OTVscjA1djY3YXV6aHoiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6IjA5MzYxZGMzMDFiNDAzYTIwOTY4MmJjMjAwYjYxMmE1ZjUwNzg3YzRlNzM3Yzk0NDI5NmZkOTdhYTAzY2ZmZjYiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJub25lIiwiY29tbWl0bWVudCI6IiJ9fX0seyJ2YWx1ZSI6MC4wMTI4NCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgODM3NTE2NDg2ZWM4NjI1Zjc2Mzc4Zjg4NDA1MjhjZjIxZWIxMjVkNmRhOTkyNDdiOTQ4ZjhmNTBiY2Y5NDRiMSBPUF9FUVVBTCIsImhleCI6ImFhMjA4Mzc1MTY0ODZlYzg2MjVmNzYzNzhmODg0MDUyOGNmMjFlYjEyNWQ2ZGE5OTI0N2I5NDhmOGY1MGJjZjk0NGIxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3BoMjlqZ2RteXh5aG1reDc4Y3NzemozbmVwYXZmOTZtZGZqZnJtamo4Yzc1OXVsOXp0ejR4c2MwbmU0Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiIwMTcxMWUzOWU3YmYzYjhjYTBkOWE2ZmM2ZWEzMmUzNDBjYWExZDY0ZGM3ZDFkYzUxZmFlMjBmZDY2NzU1NTU4IiwiYW1vdW50IjoiMCIsIm5mdCI6eyJjYXBhYmlsaXR5IjoibXV0YWJsZSIsImNvbW1pdG1lbnQiOiI0OTg2YTg2OTAwMDBlODAzNjMwMCJ9fX0seyJ2YWx1ZSI6MC44NjMzMjMyNCwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDcyYzAzZTNlYjcwNzZhMTQ4NzdmNjM5OWYwMmNlMzJhNTJkM2UzN2QgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3MmMwM2UzZWI3MDc2YTE0ODc3ZjYzOTlmMDJjZTMyYTUyZDNlMzdkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwZXZxMDM3a3VyazU5eTgwYTNlbnVwdnV2NDk5NWxyMDV2NjdhdXpoeiJdfX1dLCJmZWUiOjcuMjZlLTA2LCJoZXgiOiIwMjAwMDAwMDAzMTY1ZjFjZDJlZDQ5MTZiOTU3MzQwZGUzY2QzYmY3ZTUzOWU5NDRmNGFiMWM0NmJiMDI2YmI3NDEzOWY3MjBhZTAwMDAwMDAwNjQ0MTA5N2M2YjExOWU3YjQ2NGE2ZDQwMmMzNGUxOWQwYWZkNTA3M2U4MzIzNDc5OTYzYjIxMDkwYzBmZTk4OTg3N2FmODM4MmVlMDEzMjNjMTNjN2ViYThjYTNhYjk0NWZkODEzMDJkZTUwNTlhN2FiODFmNGRmMTM0M2RlZDlkYzNhNDEyMTAyMDU4YzNlNmY1ODk2YjViMWQyNTc2YWVkNDFlZDA2OTQwOGVhNzY1YmIxZTBjZjgzYjJhY2NhMGE3N2ExMGExMmZmZmZmZmZmMTY1ZjFjZDJlZDQ5MTZiOTU3MzQwZGUzY2QzYmY3ZTUzOWU5NDRmNGFiMWM0NmJiMDI2YmI3NDEzOWY3MjBhZTAxMDAwMDAwY2EwMDRjYzcyMDJhMWM3OWJmMjU5ZmU4MGVkNDIxNjIzMWJkMWQ3YTYzOGExZjNlMjhiYzQ3N2VjMzZiMTBhZDQ1NDVhZDM0NjYyMGY2ZmYzY2EwN2FkOTZmMjk0NGM5MzdlN2M0ODcwN2Y1YTUxMmI2MDBjMjJiNjgwOWEyMDNiNDAxYzMxZDM2MDk1Mjc5MDA5YzYzMDBjZTAxMjA3Zjc1ODhjMGQyNzY4Mjc3NWFhMjY5YzBjZjc4NTY3Zjc1ODE3YzU2N2Y3NTgxYTA2OTU4N2Y3NzUyN2Y3NTgxMDBhMDY5YzBjY2MwYzZhMjY5YzBjZGMwYzc4OGMwZDFjMGNlODc3Nzc3Njc1Mjc5NTE5YzYzYzBjZjU2N2Y3NzUyN2Y3NTgxYzBjY2MwYzY3YjkzYTI2OWMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Nzc3NzY3N2I1MjlkMDBjZTAxMjA3Zjc1N2I4OGMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Njg2OGZmZmZmZmZmMTY1ZjFjZDJlZDQ5MTZiOTU3MzQwZGUzY2QzYmY3ZTUzOWU5NDRmNGFiMWM0NmJiMDI2YmI3NDEzOWY3MjBhZTAyMDAwMDAwNjQ0MWYyMDIyOWMxYTMyYjU1YzkzZDFjZTM5NzJlNDZlNjk0NDc1ZjFiNWQ3YjE2ZTBiMzllNzEwYmUzMGJhZWM5MzAzZGQxZjZhNTQ5NzIyM2ZiYWQ4YmZhMzNkMTAyZTlkZGJlZDY5NTA3ZjdjMjYwYTUwYTc3MDkxZGEzZWQ3N2RkNDEyMTAyMDU4YzNlNmY1ODk2YjViMWQyNTc2YWVkNDFlZDA2OTQwOGVhNzY1YmIxZTBjZjgzYjJhY2NhMGE3N2ExMGExMmZmZmZmZmZmMDMyMDAzMDAwMDAwMDAwMDAwM2JlZmY2ZmYzY2EwN2FkOTZmMjk0NGM5MzdlN2M0ODcwN2Y1YTUxMmI2MDBjMjJiNjgwOWEyMDNiNDAxYzMxZDM2MDkyMDc2YTkxNDcyYzAzZTNlYjcwNzZhMTQ4NzdmNjM5OWYwMmNlMzJhNTJkM2UzN2Q4OGFjYTA5NzEzMDAwMDAwMDAwMDUwZWY1ODU1NzU2NmZkMjBhZTFmYzUxZDdkZGM2NDFkYWEwYzM0MmVhMzZlZmNhNmQ5YTA4YzNiYmZlNzM5MWU3MTAxNjEwYTQ5ODZhODY5MDAwMGU4MDM2MzAwYWEyMDgzNzUxNjQ4NmVjODYyNWY3NjM3OGY4ODQwNTI4Y2YyMWViMTI1ZDZkYTk5MjQ3Yjk0OGY4ZjUwYmNmOTQ0YjE4N2E0NTMyNTA1MDAwMDAwMDAxOTc2YTkxNDcyYzAzZTNlYjcwNzZhMTQ4NzdmNjM5OWYwMmNlMzJhNTJkM2UzN2Q4OGFjMjkyMzAwMDAifSx7InR4aWQiOiIxODU3NTE0MTlhN2VjN2IxN2U0ZGZlYmYwYzZmNTVmYThlNGJhOTc1YjQwZWQ0NGE1OTUyMTk0MjYxYWNmMTJiIiwiaGFzaCI6IjE4NTc1MTQxOWE3ZWM3YjE3ZTRkZmViZjBjNmY1NWZhOGU0YmE5NzViNDBlZDQ0YTU5NTIxOTQyNjFhY2YxMmIiLCJ2ZXJzaW9uIjoyLCJzaXplIjoyMTcsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI2YTAwYzUxZWJkMTlmYmRiODZhMWFjZDYyOTUzMTg2NGFiNDcyYjQ0MDkxZTQ4ZGJiNmJhZmZjZWU5YjhlODcwIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjRiNTg1YjVkNjBiZmUyODlmZGMzYWQ5ZDMzZThmMjk5YjUwMGViZjNlNzZhM2U1MWM3NGZiOWM5YjY5OGEyZjE0OTc2NjUzNmUyYjAxNjBlYzdjYmM0Zjg1ZmMzMTc1YTE0YzQxNzdhMjc3NzkxNzY2NzBjNjJkMDU3NmI3OTk1W0FMTHxGT1JLSURdIDAyMGUyNDAyNTE4MGYwMmRjNjc5YzA2NWQwMjJmYzdkM2UwZDE0MTc3YjRhYmZmNGY1NTdhOTJlNzY4ODI5Y2VlOCIsImhleCI6IjQxNGI1ODViNWQ2MGJmZTI4OWZkYzNhZDlkMzNlOGYyOTliNTAwZWJmM2U3NmEzZTUxYzc0ZmI5YzliNjk4YTJmMTQ5NzY2NTM2ZTJiMDE2MGVjN2NiYzRmODVmYzMxNzVhMTRjNDE3N2EyNzc3OTE3NjY3MGM2MmQwNTc2Yjc5OTU0MTIxMDIwZTI0MDI1MTgwZjAyZGM2NzljMDY1ZDAyMmZjN2QzZTBkMTQxNzdiNGFiZmY0ZjU1N2E5MmU3Njg4MjljZWU4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjAwMzg5Nzc0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCA3NGU4ZmI5MDVhNjhkYzk0YzQ1NjRhZTQxNTQ4M2FiNDY1NzFjYzdlIE9QX0VRVUFMIiwiaGV4IjoiYTkxNDc0ZThmYjkwNWE2OGRjOTRjNDU2NGFlNDE1NDgzYWI0NjU3MWNjN2U4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBwNnczN3VzdGY1ZGU5eHkyZTl3ZzkyZzgyNngydXd2MGN5ZHAyN3ltdyJdfX0seyJ2YWx1ZSI6MC4wMjAwMDUwMiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDBkMzdjY2Q3NmZkMmNjZGYxMDhkYTJkZWE3YWM3ZjRhMzZjMjk2MTAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwZDM3Y2NkNzZmZDJjY2RmMTA4ZGEyZGVhN2FjN2Y0YTM2YzI5NjEwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxeG4wbnhoZGxmdmVoY3MzazNkYWZhdjBhOXJkczVrenFuczd3Y3k3MCJdfX1dLCJmZWUiOjIuMjZlLTA2LCJoZXgiOiIwMjAwMDAwMDAxNzBlOGI4ZTljZWZmYmFiNmRiNDgxZTA5NDQyYjQ3YWI2NDE4NTMyOWQ2YWNhMTg2ZGJmYjE5YmQxZWM1MDA2YTAxMDAwMDAwNjQ0MTRiNTg1YjVkNjBiZmUyODlmZGMzYWQ5ZDMzZThmMjk5YjUwMGViZjNlNzZhM2U1MWM3NGZiOWM5YjY5OGEyZjE0OTc2NjUzNmUyYjAxNjBlYzdjYmM0Zjg1ZmMzMTc1YTE0YzQxNzdhMjc3NzkxNzY2NzBjNjJkMDU3NmI3OTk1NDEyMTAyMGUyNDAyNTE4MGYwMmRjNjc5YzA2NWQwMjJmYzdkM2UwZDE0MTc3YjRhYmZmNGY1NTdhOTJlNzY4ODI5Y2VlOGZmZmZmZmZmMDI4ZWYyMDUwMDAwMDAwMDAwMTdhOTE0NzRlOGZiOTA1YTY4ZGM5NGM0NTY0YWU0MTU0ODNhYjQ2NTcxY2M3ZTg3NzY4NjFlMDAwMDAwMDAwMDE5NzZhOTE0MGQzN2NjZDc2ZmQyY2NkZjEwOGRhMmRlYTdhYzdmNGEzNmMyOTYxMDg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjFiNmJkNTM4YmRlMDY5NTdkZjk0NjI5MDAwZGM3YjcxMzUyMzAyY2FhMjJiZDZlNWE5MzMxYjVhMDcyNWMyMGIiLCJoYXNoIjoiMWI2YmQ1MzhiZGUwNjk1N2RmOTQ2MjkwMDBkYzdiNzEzNTIzMDJjYWEyMmJkNmU1YTkzMzFiNWEwNzI1YzIwYiIsInZlcnNpb24iOjEsInNpemUiOjIyNSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImM4MzA5ZTczMTczYjVlNmZjOTFiZDg4MzdlMzQyZTE2M2IzNTZlZTE5ODkxNmViODk5NTJhZjZiMjRhMWEyMzMiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAyZmJlOTkyOWQ5MWUxZWMzOTExNDQwOThiZmJhMTRlOGJhNzE2ZTZmYzQ0NDhkYzJlYjNkNGQ5NmUzMGI3N2IyMDIyMDI3NTRhN2JkMzU5NTZjZWI2ZTgwY2ZjZmY4MTBlNjA0YTQ1M2FhODVlNjE1ZGY3MWUyZjMxM2YwYjQ2Nzc0OWZbQUxMfEZPUktJRF0gMDI1MGYxMmQ5MTk0NGVjNjdjNWM5MTA5M2U2ZTg2NjBkMDcyNzc1ODE0ZjdjZGY3NzAzYzVmY2EzNTFkNjY0NTZlIiwiaGV4IjoiNDczMDQ0MDIyMDJmYmU5OTI5ZDkxZTFlYzM5MTE0NDA5OGJmYmExNGU4YmE3MTZlNmZjNDQ0OGRjMmViM2Q0ZDk2ZTMwYjc3YjIwMjIwMjc1NGE3YmQzNTk1NmNlYjZlODBjZmNmZjgxMGU2MDRhNDUzYWE4NWU2MTVkZjcxZTJmMzEzZjBiNDY3NzQ5ZjQxMjEwMjUwZjEyZDkxOTQ0ZWM2N2M1YzkxMDkzZTZlODY2MGQwNzI3NzU4MTRmN2NkZjc3MDNjNWZjYTM1MWQ2NjQ1NmUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMDA2OTQ0MzUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhMTY5MzQ4ZDU4ZThlY2FkZmViZDg3MmRlOWJhN2VkOThmNGZlNTBjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YTE2OTM0OGQ1OGU4ZWNhZGZlYmQ4NzJkZTliYTdlZDk4ZjRmZTUwYzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenNramR5ZHRyNXdldDA3aGtyam02ZDYwbXZjN25sOXBzdm1yMjg4ajYiXX19LHsidmFsdWUiOjE2NDQyMC42MTQ4Mzk2MywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDVlYTNjZmZmOWM2OTM3NGQ5NGU5NWIwODM4YWVhNzg1NDlmYjU0MWEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1ZWEzY2ZmZjljNjkzNzRkOTRlOTViMDgzOGFlYTc4NTQ5ZmI1NDFhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwMDI4bmxsbjM1bndudjVhOWRzc3c5dzU3ejVuNzY1cmdlbnIzc3V3NiJdfX1dLCJmZWUiOjIuMjdlLTA2LCJoZXgiOiIwMTAwMDAwMDAxMzNhMmExMjQ2YmFmNTI5OWI4NmU5MTk4ZTE2ZTM1M2IxNjJlMzQ3ZTgzZDgxYmM5NmY1ZTNiMTc3MzllMzBjODAxMDAwMDAwNmE0NzMwNDQwMjIwMmZiZTk5MjlkOTFlMWVjMzkxMTQ0MDk4YmZiYTE0ZThiYTcxNmU2ZmM0NDQ4ZGMyZWIzZDRkOTZlMzBiNzdiMjAyMjAyNzU0YTdiZDM1OTU2Y2ViNmU4MGNmY2ZmODEwZTYwNGE0NTNhYTg1ZTYxNWRmNzFlMmYzMTNmMGI0Njc3NDlmNDEyMTAyNTBmMTJkOTE5NDRlYzY3YzVjOTEwOTNlNmU4NjYwZDA3Mjc3NTgxNGY3Y2RmNzcwM2M1ZmNhMzUxZDY2NDU2ZWZmZmZmZmZmMDJhMzk4MGEwMDAwMDAwMDAwMTk3NmE5MTRhMTY5MzQ4ZDU4ZThlY2FkZmViZDg3MmRlOWJhN2VkOThmNGZlNTBjODhhY2JiZWYzYjM3ZjQwZTAwMDAxOTc2YTkxNDVlYTNjZmZmOWM2OTM3NGQ5NGU5NWIwODM4YWVhNzg1NDlmYjU0MWE4OGFjMDAwMDAwMDAifSx7InR4aWQiOiIxY2NiNDE3YjVkYTk1OGQ4ZDQxOTBhMzYyMTg0MjZkOTEzOTAyZTRlMzRiYjA4MTE4OGJmMWZiYWU4MTZlZDk2IiwiaGFzaCI6IjFjY2I0MTdiNWRhOTU4ZDhkNDE5MGEzNjIxODQyNmQ5MTM5MDJlNGUzNGJiMDgxMTg4YmYxZmJhZTgxNmVkOTYiLCJ2ZXJzaW9uIjoyLCJzaXplIjoyMjYsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiIzN2Y1YzRmMjhjYzZjM2FiMjk3MGYwMDY4YzUxZDVjNmUxYjNlMDE5ZDdiNGZkMTA4NWMzN2RkOWZlYTRhMjQ3Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBmMTVkYWU5MzE5Mzc2NGJmOGQ1YjA0YThiYzk0YjM1MGVjZDNlYTQ3NDllYjhjNDlkMWQ4MDAzYjI2MWViZmViMDIyMDQ3OTRlNGU5YTA0YWI2NzliMWE2OTNmNGM1NjgwZWQ5N2IyMjVjNGU1MjQ1ZjEwZDU2YjRlNzdiZDRjOGQ4ZjVbQUxMfEZPUktJRF0gMDM1MTZlNGVlZWE4ZjFjYWIzZmUyNzEwMmM2OTRhZWRkOTBiMDIxOTUzYTc4ZGQzYmY2ZTkxZDc1Y2NlZDZlYmIyIiwiaGV4IjoiNDgzMDQ1MDIyMTAwZjE1ZGFlOTMxOTM3NjRiZjhkNWIwNGE4YmM5NGIzNTBlY2QzZWE0NzQ5ZWI4YzQ5ZDFkODAwM2IyNjFlYmZlYjAyMjA0Nzk0ZTRlOWEwNGFiNjc5YjFhNjkzZjRjNTY4MGVkOTdiMjI1YzRlNTI0NWYxMGQ1NmI0ZTc3YmQ0YzhkOGY1NDEyMTAzNTE2ZTRlZWVhOGYxY2FiM2ZlMjcxMDJjNjk0YWVkZDkwYjAyMTk1M2E3OGRkM2JmNmU5MWQ3NWNjZWQ2ZWJiMiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC41OTU3MTcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBkNjkzMTViMGEzYjgxYWFlZWVkZmYxN2Y3NmMxZDgxMjBiMjFiNWRlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZDY5MzE1YjBhM2I4MWFhZWVlZGZmMTdmNzZjMWQ4MTIwYjIxYjVkZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcnRmeDlkczV3dXA0dGh3bWxjaDdha3BtcWZxa2dkNG1jNWNrODVqbGUiXX19LHsidmFsdWUiOjAuMDE1Mjc1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYmU3YmVmYTI0ZDE5NDI3YWM5NmE5ZDk1MzExNTM4MmNiOTQ2NzM4ZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGJlN2JlZmEyNGQxOTQyN2FjOTZhOWQ5NTMxMTUzODJjYjk0NjczOGY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpsOGhtYXpmNXY1eTdrZmQyd2Uydmc0OHFrdGozbm4zdTc5eDRzazRwIl19fV0sImZlZSI6OGUtMDYsImhleCI6IjAyMDAwMDAwMDE0N2EyYTRmZWQ5N2RjMzg1MTBmZGI0ZDcxOWUwYjNlMWM2ZDU1MThjMDZmMDcwMjlhYmMzYzY4Y2YyYzRmNTM3MDAwMDAwMDA2YjQ4MzA0NTAyMjEwMGYxNWRhZTkzMTkzNzY0YmY4ZDViMDRhOGJjOTRiMzUwZWNkM2VhNDc0OWViOGM0OWQxZDgwMDNiMjYxZWJmZWIwMjIwNDc5NGU0ZTlhMDRhYjY3OWIxYTY5M2Y0YzU2ODBlZDk3YjIyNWM0ZTUyNDVmMTBkNTZiNGU3N2JkNGM4ZDhmNTQxMjEwMzUxNmU0ZWVlYThmMWNhYjNmZTI3MTAyYzY5NGFlZGQ5MGIwMjE5NTNhNzhkZDNiZjZlOTFkNzVjY2VkNmViYjJmZmZmZmZmZjAyZjRmZDhjMDMwMDAwMDAwMDE5NzZhOTE0ZDY5MzE1YjBhM2I4MWFhZWVlZGZmMTdmNzZjMWQ4MTIwYjIxYjVkZTg4YWNjYzRlMTcwMDAwMDAwMDAwMTk3NmE5MTRiZTdiZWZhMjRkMTk0MjdhYzk2YTlkOTUzMTE1MzgyY2I5NDY3MzhmODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiMWUyYzk2Y2UyN2ZjNzk2Mzk2MDUyMmFjNjEwODNmNzdkNDFhN2VlYjA5OGY5MTk3MTY3MTU5ZDY4MDRiNGE4ZCIsImhhc2giOiIxZTJjOTZjZTI3ZmM3OTYzOTYwNTIyYWM2MTA4M2Y3N2Q0MWE3ZWViMDk4ZjkxOTcxNjcxNTlkNjgwNGI0YThkIiwidmVyc2lvbiI6MSwic2l6ZSI6NjcwLCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiM2EyMDAyMDdkNDFkZmE1Y2YzYzA1ZDMyYTNkNTU4ZDBlZTVlMmEyZDkxYmI5MmU0MzkyYzQ0MzRkZjQ1NDc4MCIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwYzNlNDIyMWQ0NzEzMWM2YWRhNGUxYzgzYmNmMzQxODViZTBkODAyMGYzZmI4NjVlZTk1YzA2ZDAyM2MyYjE4MjAyMjAzYjExZWVkN2UwYjFkZjBiZTg1OWU2Yjk5OWNjMjE0YzEyZGQ2MWQ4YjkwZDIwMTUxYzVmNmNkMDQ2MDg3YjE0W0FMTHxGT1JLSURdIDAzYmRkNjQzMGUxZDQxMTQ2Njk4ZjRjZWM3N2YyZDZkOTY0MTBjYjJkZWM5YjhkMWRiNjIzOTBmNjg4N2YyMTI2MCIsImhleCI6IjQ4MzA0NTAyMjEwMGMzZTQyMjFkNDcxMzFjNmFkYTRlMWM4M2JjZjM0MTg1YmUwZDgwMjBmM2ZiODY1ZWU5NWMwNmQwMjNjMmIxODIwMjIwM2IxMWVlZDdlMGIxZGYwYmU4NTllNmI5OTljYzIxNGMxMmRkNjFkOGI5MGQyMDE1MWM1ZjZjZDA0NjA4N2IxNDQxMjEwM2JkZDY0MzBlMWQ0MTE0NjY5OGY0Y2VjNzdmMmQ2ZDk2NDEwY2IyZGVjOWI4ZDFkYjYyMzkwZjY4ODdmMjEyNjAifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImMwYjVhMjg2MjZiYzk2OTdhODM1YTM4NDVjOWJmMTJmYzU0YTUwYTA3ODEyNmViOGMwNDdjNTFkNzA5NmRjNjYiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMGZhMzIyZjUxYTNkNDk5MGMxNWRjYzQ2NzM3YTkxNTlhYWY5ZmQxZmJlMDNiZmI0NTQwN2VmYTY3MGIzYjFjYTEwMjIwNWNlYjM2YWQ5ZTViZTZmNmU4YTU2ZWRiZDNhMWE2MjNkMzYwZjZiYmM0NTQ3OWNkOTA0MTY0MDg5YTE2YTNjOVtBTEx8Rk9SS0lEXSAwM2JkZDY0MzBlMWQ0MTE0NjY5OGY0Y2VjNzdmMmQ2ZDk2NDEwY2IyZGVjOWI4ZDFkYjYyMzkwZjY4ODdmMjEyNjAiLCJoZXgiOiI0ODMwNDUwMjIxMDBmYTMyMmY1MWEzZDQ5OTBjMTVkY2M0NjczN2E5MTU5YWFmOWZkMWZiZTAzYmZiNDU0MDdlZmE2NzBiM2IxY2ExMDIyMDVjZWIzNmFkOWU1YmU2ZjZlOGE1NmVkYmQzYTFhNjIzZDM2MGY2YmJjNDU0NzljZDkwNDE2NDA4OWExNmEzYzk0MTIxMDNiZGQ2NDMwZTFkNDExNDY2OThmNGNlYzc3ZjJkNmQ5NjQxMGNiMmRlYzliOGQxZGI2MjM5MGY2ODg3ZjIxMjYwIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIyZDI3MTAwNjk5M2RiMDgxNmM4ZmYyZDI2NDg2NDE5ODIwMjc3YmI4NGU2MGJhNGMyZDA3MDkxZDU0MjI3NTljIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBmOTFhNTliMjM1NTVhYjU5MTYzNjQ5MDBhY2U3ZjBiNzgxMzRlYjFjNGY4ZGVmMmI2MWRlZDAzMmMwZTg5YzA1MDIyMDY4ZDlmOTE0MDg3NTBkNGNjZjU4NGE2MDQ3ODFiM2UxYTE0YTliZmU1MWVlOGMyMDgzNWQxNjk5NGUyY2MyYWNbQUxMfEZPUktJRF0gMDNiZGQ2NDMwZTFkNDExNDY2OThmNGNlYzc3ZjJkNmQ5NjQxMGNiMmRlYzliOGQxZGI2MjM5MGY2ODg3ZjIxMjYwIiwiaGV4IjoiNDgzMDQ1MDIyMTAwZjkxYTU5YjIzNTU1YWI1OTE2MzY0OTAwYWNlN2YwYjc4MTM0ZWIxYzRmOGRlZjJiNjFkZWQwMzJjMGU4OWMwNTAyMjA2OGQ5ZjkxNDA4NzUwZDRjY2Y1ODRhNjA0NzgxYjNlMWExNGE5YmZlNTFlZThjMjA4MzVkMTY5OTRlMmNjMmFjNDEyMTAzYmRkNjQzMGUxZDQxMTQ2Njk4ZjRjZWM3N2YyZDZkOTY0MTBjYjJkZWM5YjhkMWRiNjIzOTBmNjg4N2YyMTI2MCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNWFiNzBiN2RhNGE1YTQzOGM2OWI0ZmJiNTVjMTJmMTM2ZjNjZGFiYTY0NTNmZGNlMmU3OGU3OGMyNzdiNGVlMSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwYTVjZDQ1ZGFkNjFjN2YzZDhmZTQxOTRjYTYwODE4MzRlMTEwNzZjMzIxOTUzNDRlOWYwNWEzODQ1OWFlYTRhMTAyMjAxYTZiZjg0MDJkYTM0ZGEwYTNiNjcyNTY5M2E3NmZkNGNiOTNlNzJjMDI0M2FlMGRhNGU1MWU1MDU5Zjk5ZDg3W0FMTHxGT1JLSURdIDAzYmRkNjQzMGUxZDQxMTQ2Njk4ZjRjZWM3N2YyZDZkOTY0MTBjYjJkZWM5YjhkMWRiNjIzOTBmNjg4N2YyMTI2MCIsImhleCI6IjQ4MzA0NTAyMjEwMGE1Y2Q0NWRhZDYxYzdmM2Q4ZmU0MTk0Y2E2MDgxODM0ZTExMDc2YzMyMTk1MzQ0ZTlmMDVhMzg0NTlhZWE0YTEwMjIwMWE2YmY4NDAyZGEzNGRhMGEzYjY3MjU2OTNhNzZmZDRjYjkzZTcyYzAyNDNhZTBkYTRlNTFlNTA1OWY5OWQ4NzQxMjEwM2JkZDY0MzBlMWQ0MTE0NjY5OGY0Y2VjNzdmMmQ2ZDk2NDEwY2IyZGVjOWI4ZDFkYjYyMzkwZjY4ODdmMjEyNjAifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjQ5LjAsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAyYzUxYWMwNThkOTVkZDg3ZjdjZDY4N2FhZGE1NGY3MjMyYzE2YmEwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MmM1MWFjMDU4ZDk1ZGQ4N2Y3Y2Q2ODdhYWRhNTRmNzIzMmMxNmJhMDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWs5cnRxOTNrMmFtcGxoZTQ1ODR0ZDlmYWVyOXN0dDVxeTV6dmpqcGgiXX19LHsidmFsdWUiOjEuMzYzODA1MDEsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3YzhkZTNhMDYwOGIxZWIyOGU4OGNiNGM1NGQ2ZWQ1NDgxZjRlZGI4IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0N2M4ZGUzYTA2MDhiMWViMjhlODhjYjRjNTRkNmVkNTQ4MWY0ZWRiODg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDdnbWNhcXZ6OTNhdjV3M3I5NWM0eGthNDJncmE4ZGhxZmo3dWR1Y3giXX19XSwiZmVlIjoxLjM0ZS0wNSwiaGV4IjoiMDEwMDAwMDAwNDgwNDc0NWRmMzQ0NDJjMzllNDkyYmI5MTJkMmE1ZWVlZDA1OGQ1YTMzMjVkYzBmMzVjZmExZGQ0MDcwMjIwM2EwMTAwMDAwMDZiNDgzMDQ1MDIyMTAwYzNlNDIyMWQ0NzEzMWM2YWRhNGUxYzgzYmNmMzQxODViZTBkODAyMGYzZmI4NjVlZTk1YzA2ZDAyM2MyYjE4MjAyMjAzYjExZWVkN2UwYjFkZjBiZTg1OWU2Yjk5OWNjMjE0YzEyZGQ2MWQ4YjkwZDIwMTUxYzVmNmNkMDQ2MDg3YjE0NDEyMTAzYmRkNjQzMGUxZDQxMTQ2Njk4ZjRjZWM3N2YyZDZkOTY0MTBjYjJkZWM5YjhkMWRiNjIzOTBmNjg4N2YyMTI2MGZmZmZmZmZmNjZkYzk2NzAxZGM1NDdjMGI4NmUxMjc4YTA1MDRhYzUyZmYxOWI1Yzg0YTMzNWE4OTc5NmJjMjY4NmEyYjVjMDAxMDAwMDAwNmI0ODMwNDUwMjIxMDBmYTMyMmY1MWEzZDQ5OTBjMTVkY2M0NjczN2E5MTU5YWFmOWZkMWZiZTAzYmZiNDU0MDdlZmE2NzBiM2IxY2ExMDIyMDVjZWIzNmFkOWU1YmU2ZjZlOGE1NmVkYmQzYTFhNjIzZDM2MGY2YmJjNDU0NzljZDkwNDE2NDA4OWExNmEzYzk0MTIxMDNiZGQ2NDMwZTFkNDExNDY2OThmNGNlYzc3ZjJkNmQ5NjQxMGNiMmRlYzliOGQxZGI2MjM5MGY2ODg3ZjIxMjYwZmZmZmZmZmY5Yzc1MjI1NDFkMDkwNzJkNGNiYTYwNGViODdiMjcyMDk4NDE4NjY0ZDJmMjhmNmM4MWIwM2Q5OTA2MTAyNzJkMDAwMDAwMDA2YjQ4MzA0NTAyMjEwMGY5MWE1OWIyMzU1NWFiNTkxNjM2NDkwMGFjZTdmMGI3ODEzNGViMWM0ZjhkZWYyYjYxZGVkMDMyYzBlODljMDUwMjIwNjhkOWY5MTQwODc1MGQ0Y2NmNTg0YTYwNDc4MWIzZTFhMTRhOWJmZTUxZWU4YzIwODM1ZDE2OTk0ZTJjYzJhYzQxMjEwM2JkZDY0MzBlMWQ0MTE0NjY5OGY0Y2VjNzdmMmQ2ZDk2NDEwY2IyZGVjOWI4ZDFkYjYyMzkwZjY4ODdmMjEyNjBmZmZmZmZmZmUxNGU3YjI3OGNlNzc4MmVjZWZkNTM2NGJhZGEzYzZmMTMyZmMxNTViYjRmOWJjNjM4YTRhNWE0N2QwYmI3NWEwMDAwMDAwMDZiNDgzMDQ1MDIyMTAwYTVjZDQ1ZGFkNjFjN2YzZDhmZTQxOTRjYTYwODE4MzRlMTEwNzZjMzIxOTUzNDRlOWYwNWEzODQ1OWFlYTRhMTAyMjAxYTZiZjg0MDJkYTM0ZGEwYTNiNjcyNTY5M2E3NmZkNGNiOTNlNzJjMDI0M2FlMGRhNGU1MWU1MDU5Zjk5ZDg3NDEyMTAzYmRkNjQzMGUxZDQxMTQ2Njk4ZjRjZWM3N2YyZDZkOTY0MTBjYjJkZWM5YjhkMWRiNjIzOTBmNjg4N2YyMTI2MGZmZmZmZmZmMDIwMDExMTAyNDAxMDAwMDAwMTk3NmE5MTQyYzUxYWMwNThkOTVkZDg3ZjdjZDY4N2FhZGE1NGY3MjMyYzE2YmEwODhhYzU1MDAyMTA4MDAwMDAwMDAxOTc2YTkxNDdjOGRlM2EwNjA4YjFlYjI4ZTg4Y2I0YzU0ZDZlZDU0ODFmNGVkYjg4OGFjMDAwMDAwMDAifSx7InR4aWQiOiIyMDY4YTg4MzIxMzQ3ZjNlYzEwOWZkMTYyNmM3ODdhYmQ3NTQwNGIxNmZjZDI0NWVhMzg2NDBkNmNhYzc3YjM5IiwiaGFzaCI6IjIwNjhhODgzMjEzNDdmM2VjMTA5ZmQxNjI2Yzc4N2FiZDc1NDA0YjE2ZmNkMjQ1ZWEzODY0MGQ2Y2FjNzdiMzkiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyMjUsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI4ZDc2MTMwNjQ5MzZhZTM2Mzc0YWI5ZjM3ZDViNTM4MmYxMzQ3ZjNmMGFkZmY4YTUzMmQ3OTZhYmJjMTIzMDI1Iiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwMzczZDUxNzdjZmFmYzc2MzBjMGFlOWRmMzc5MzNjYzM0YzkxNTQ5YzM1ZGRhZTAzNDcxNmQ4NjQwNTNhMTM3NjAyMjA3M2UyNGIwYzZhNTMwMmRhYmNjYmNmYzdiZmNmNzc4ZTIyZjE2MDAxNmExODNjNzJkYmU4Y2ZmZGEyYWEyMzExW0FMTHxGT1JLSURdIDAzOGFjNjA4YzNjNGQwYjc1YzlkYWU3ODc5Y2VmMTMxZjMxNTc5NGEwZjk4N2ZiOGQ0MjA3NjUwMWYwZjhmNWM4NyIsImhleCI6IjQ3MzA0NDAyMjAzNzNkNTE3N2NmYWZjNzYzMGMwYWU5ZGYzNzkzM2NjMzRjOTE1NDljMzVkZGFlMDM0NzE2ZDg2NDA1M2ExMzc2MDIyMDczZTI0YjBjNmE1MzAyZGFiY2NiY2ZjN2JmY2Y3NzhlMjJmMTYwMDE2YTE4M2M3MmRiZThjZmZkYTJhYTIzMTE0MTIxMDM4YWM2MDhjM2M0ZDBiNzVjOWRhZTc4NzljZWYxMzFmMzE1Nzk0YTBmOTg3ZmI4ZDQyMDc2NTAxZjBmOGY1Yzg3In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjAwMDIzNzU5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMGMwY2IzZDIwOWU2ODAxOTMxYmE2OGJkODUwMDYxNzdmODQ1NWRlMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDBjMGNiM2QyMDllNjgwMTkzMWJhNjhiZDg1MDA2MTc3Zjg0NTVkZTE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF4cWV2N2pwOG5ncXhmM2hmNXRtcGdxdjltbHMzMmF1eWU2NmhyZ3NoIl19fSx7InZhbHVlIjowLjAwODEzOTUsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA1NWI5NTU5ZjQ2YTkyOWZiNzViY2U3ZWRjOWNiNGE4ZGE0NGI5ZGM0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NTViOTU1OWY0NmE5MjlmYjc1YmNlN2VkYzljYjRhOGRhNDRiOWRjNDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDJtajR2bGc2NWpuN200aG5uN21qd3RmMng2Z2p1YWNzcnRobTVzYXAiXX19XSwiZmVlIjo1Ljk1ZS0wNiwiaGV4IjoiMDEwMDAwMDAwMTI1MzAxMmJjYWI5NmQ3MzJhNWY4ZGYwYTNmN2YzNGYxODI1MzViN2RmM2I5NGEzNzM2YWUzNjQ5MDYxMzc2OGQwMTAwMDAwMDZhNDczMDQ0MDIyMDM3M2Q1MTc3Y2ZhZmM3NjMwYzBhZTlkZjM3OTMzY2MzNGM5MTU0OWMzNWRkYWUwMzQ3MTZkODY0MDUzYTEzNzYwMjIwNzNlMjRiMGM2YTUzMDJkYWJjY2JjZmM3YmZjZjc3OGUyMmYxNjAwMTZhMTgzYzcyZGJlOGNmZmRhMmFhMjMxMTQxMjEwMzhhYzYwOGMzYzRkMGI3NWM5ZGFlNzg3OWNlZjEzMWYzMTU3OTRhMGY5ODdmYjhkNDIwNzY1MDFmMGY4ZjVjODdmZmZmZmZmZjAyY2Y1YzAwMDAwMDAwMDAwMDE5NzZhOTE0MGMwY2IzZDIwOWU2ODAxOTMxYmE2OGJkODUwMDYxNzdmODQ1NWRlMTg4YWM3ZTZiMGMwMDAwMDAwMDAwMTk3NmE5MTQ1NWI5NTU5ZjQ2YTkyOWZiNzViY2U3ZWRjOWNiNGE4ZGE0NGI5ZGM0ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiMjA5ZDA1NWQ0MTJkZDk3MDkxMjliOWM5MzVmYzZlOTNlNDM3YTc0MWVkNDQ2MjkyYmMzNjZiNDhhNmIzOTZjMyIsImhhc2giOiIyMDlkMDU1ZDQxMmRkOTcwOTEyOWI5YzkzNWZjNmU5M2U0MzdhNzQxZWQ0NDYyOTJiYzM2NmI0OGE2YjM5NmMzIiwidmVyc2lvbiI6Miwic2l6ZSI6MTY0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiOGI4NDU5MjkxOGU3MTYwYjZiOTQxMGIxOGQ4MWNmYTljNjlhNzliOTVmNGFiNGJmZDNiOTk1NjliYWYyODk4NiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIiwiaGV4IjoiNDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIn0sInNlcXVlbmNlIjoxfV0sInZvdXQiOlt7InZhbHVlIjowLjAwMTY3NjE3LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XSwiZmVlIjoxLjY0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTg2ODlmMmJhNjk5NWI5ZDNiZmI0NGE1ZmI5Nzk5YWM2YTljZjgxOGRiMTEwOTQ2YjBiMTZlNzE4Mjk1OTg0OGIwMDAwMDAwMDQ1NDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxMDEwMDAwMDAwMWMxOGUwMjAwMDAwMDAwMDAyM2FhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODcwMDAwMDAwMCJ9LHsidHhpZCI6IjIxZGQwNDE0OTI1NzU3ZTRjNDlkYTQyMTgyZDc3NDFhMDgyNzY3MjBlMWU3MDRlMzdlOGYwODgwNTJjZDIyM2YiLCJoYXNoIjoiMjFkZDA0MTQ5MjU3NTdlNGM0OWRhNDIxODJkNzc0MWEwODI3NjcyMGUxZTcwNGUzN2U4ZjA4ODA1MmNkMjIzZiIsInZlcnNpb24iOjEsInNpemUiOjQ2NywibG9ja3RpbWUiOjk0MDg4MywidmluIjpbeyJ0eGlkIjoiMmZjM2QwNTI0YWY2NzAyMjk4YzU0MDViMTNmMzYwMzFhMWEwYmEyY2E4ZTc2Y2EyOTJiNTdkMDA2Y2VlMjJiYiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiJmOTAyMThiYzM3YzhlYjg0OTE1MWVkYjdiOTMzYzE2Y2ExMTMwNDUzYjM0MmYxMjY1Y2ZhZGZmZjg4MWUzMTQxODljZDZmNzdlOWJiMWE5MjNhM2ZmZmY4NzcwMDFhNWFiN2E5Yzk3ODE3Y2YyNzdkMjBhMDBjZDQyOGNlNjkwOFtBTEx8Rk9SS0lEXSAwMjA5NzRmZjY3ODJhNDUxNjgxZGI0YzdkMjY3MWMxMDUzYTI1MzAyZTY1MmYwNWFlMTM1ODY1MGU3NTc5MzNiNmMiLCJoZXgiOiI0MWY5MDIxOGJjMzdjOGViODQ5MTUxZWRiN2I5MzNjMTZjYTExMzA0NTNiMzQyZjEyNjVjZmFkZmZmODgxZTMxNDE4OWNkNmY3N2U5YmIxYTkyM2EzZmZmZjg3NzAwMWE1YWI3YTljOTc4MTdjZjI3N2QyMGEwMGNkNDI4Y2U2OTA4NDEyMTAyMDk3NGZmNjc4MmE0NTE2ODFkYjRjN2QyNjcxYzEwNTNhMjUzMDJlNjUyZjA1YWUxMzU4NjUwZTc1NzkzM2I2YyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH0seyJ0eGlkIjoiMzIxM2E0OTBmYzFiZTlkNDYzYjZkZjlkZWNmODcyMDU2YzIzMzAzYTk5OGEwYThlZTViMzdkNTc4ZTIxYzYxMCIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiJiODQyMTEwYTI1NDViMThiNDk5ZGIyYmQ2NjM0MDQ3M2VmNDNjMTJjZTg5OWY0ZDA3OWI2OTFiODhmODljNWNlNDY1ZTI5NzFjZTAxMWNiMzIxOGIyMTlkNTU3N2MxMmVjMzI4NGM1YTQ5Y2Q2ZmRlMjc5YzRlNjY4NjBmNjcxYltBTEx8Rk9SS0lEXSAwMjA5NzRmZjY3ODJhNDUxNjgxZGI0YzdkMjY3MWMxMDUzYTI1MzAyZTY1MmYwNWFlMTM1ODY1MGU3NTc5MzNiNmMiLCJoZXgiOiI0MWI4NDIxMTBhMjU0NWIxOGI0OTlkYjJiZDY2MzQwNDczZWY0M2MxMmNlODk5ZjRkMDc5YjY5MWI4OGY4OWM1Y2U0NjVlMjk3MWNlMDExY2IzMjE4YjIxOWQ1NTc3YzEyZWMzMjg0YzVhNDljZDZmZGUyNzljNGU2Njg2MGY2NzFiNDEyMTAyMDk3NGZmNjc4MmE0NTE2ODFkYjRjN2QyNjcxYzEwNTNhMjUzMDJlNjUyZjA1YWUxMzU4NjUwZTc1NzkzM2I2YyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH0seyJ0eGlkIjoiZDE1ZWQ1ZjM1NDc5N2FiMjA2N2IwZDkxZjUwYzg2MjFiMzAyYzUyZWVjOWVhMjQ1NzdmNjU2Yzc2MzAyNmZkMSIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiJlZWFiNjJhNWU3Nzk4NGNmMDBlNTk1MjcyYWM4ODM1MjE2Y2VjNGY4MTEwM2Q0ZGY4Y2I2Y2YxOTUwMmZmMjQ1YzlhN2RjZTQ4YjAxODZhMDM2NTU2ZjFlMTBlNzI3NjU5Yjk1OTc2YjQ1YWY4MWNmZTcxMzdjMmZlYWNmNWFjNVtBTEx8Rk9SS0lEXSAwMjA5NzRmZjY3ODJhNDUxNjgxZGI0YzdkMjY3MWMxMDUzYTI1MzAyZTY1MmYwNWFlMTM1ODY1MGU3NTc5MzNiNmMiLCJoZXgiOiI0MWVlYWI2MmE1ZTc3OTg0Y2YwMGU1OTUyNzJhYzg4MzUyMTZjZWM0ZjgxMTAzZDRkZjhjYjZjZjE5NTAyZmYyNDVjOWE3ZGNlNDhiMDE4NmEwMzY1NTZmMWUxMGU3Mjc2NTliOTU5NzZiNDVhZjgxY2ZlNzEzN2MyZmVhY2Y1YWM1NDEyMTAyMDk3NGZmNjc4MmE0NTE2ODFkYjRjN2QyNjcxYzEwNTNhMjUzMDJlNjUyZjA1YWUxMzU4NjUwZTc1NzkzM2I2YyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMDE5MDUzMywibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGRkMDA1ODE1YTkxZTkyNGU3ZTBkYmEzYzk5YjAyMGNiMWRhMGE5ZGMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkZDAwNTgxNWE5MWU5MjRlN2UwZGJhM2M5OWIwMjBjYjFkYTBhOWRjODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyd3Nxa3E0NHkwZnlubjdwa2FyZXhkc3lyOTNtZzlmbXN0OW44MnB2OSJdfX1dLCJmZWUiOjQuNjdlLTA2LCJoZXgiOiIwMTAwMDAwMDAzYmIyMmVlNmMwMDdkYjU5MmEyNmNlN2E4MmNiYWEwYTEzMTYwZjMxMzViNDBjNTk4MjI3MGY2NGE1MmQwYzMyZjAwMDAwMDAwNjQ0MWY5MDIxOGJjMzdjOGViODQ5MTUxZWRiN2I5MzNjMTZjYTExMzA0NTNiMzQyZjEyNjVjZmFkZmZmODgxZTMxNDE4OWNkNmY3N2U5YmIxYTkyM2EzZmZmZjg3NzAwMWE1YWI3YTljOTc4MTdjZjI3N2QyMGEwMGNkNDI4Y2U2OTA4NDEyMTAyMDk3NGZmNjc4MmE0NTE2ODFkYjRjN2QyNjcxYzEwNTNhMjUzMDJlNjUyZjA1YWUxMzU4NjUwZTc1NzkzM2I2Y2ZlZmZmZmZmMTBjNjIxOGU1NzdkYjNlNThlMGE4YTk5M2EzMDIzNmMwNTcyZjhlYzlkZGZiNjYzZDRlOTFiZmM5MGE0MTMzMjAxMDAwMDAwNjQ0MWI4NDIxMTBhMjU0NWIxOGI0OTlkYjJiZDY2MzQwNDczZWY0M2MxMmNlODk5ZjRkMDc5YjY5MWI4OGY4OWM1Y2U0NjVlMjk3MWNlMDExY2IzMjE4YjIxOWQ1NTc3YzEyZWMzMjg0YzVhNDljZDZmZGUyNzljNGU2Njg2MGY2NzFiNDEyMTAyMDk3NGZmNjc4MmE0NTE2ODFkYjRjN2QyNjcxYzEwNTNhMjUzMDJlNjUyZjA1YWUxMzU4NjUwZTc1NzkzM2I2Y2ZlZmZmZmZmZDE2ZjAyNjNjNzU2ZjY3NzQ1YTI5ZWVjMmVjNTAyYjMyMTg2MGNmNTkxMGQ3YjA2YjI3YTc5NTRmM2Q1NWVkMTAyMDAwMDAwNjQ0MWVlYWI2MmE1ZTc3OTg0Y2YwMGU1OTUyNzJhYzg4MzUyMTZjZWM0ZjgxMTAzZDRkZjhjYjZjZjE5NTAyZmYyNDVjOWE3ZGNlNDhiMDE4NmEwMzY1NTZmMWUxMGU3Mjc2NTliOTU5NzZiNDVhZjgxY2ZlNzEzN2MyZmVhY2Y1YWM1NDEyMTAyMDk3NGZmNjc4MmE0NTE2ODFkYjRjN2QyNjcxYzEwNTNhMjUzMDJlNjUyZjA1YWUxMzU4NjUwZTc1NzkzM2I2Y2ZlZmZmZmZmMDE0NWU4MDIwMDAwMDAwMDAwMTk3NmE5MTRkZDAwNTgxNWE5MWU5MjRlN2UwZGJhM2M5OWIwMjBjYjFkYTBhOWRjODhhYzUzNWIwZTAwIn0seyJ0eGlkIjoiMjJkYmU0ZGZkNTUwNTIzNjlhYzJkNGJlNzVmNDcwMjFkYTg2ZGUyOTA2YWQ3OTdmNWY5ZmZkOTU3ZmYxM2M5OCIsImhhc2giOiIyMmRiZTRkZmQ1NTA1MjM2OWFjMmQ0YmU3NWY0NzAyMWRhODZkZTI5MDZhZDc5N2Y1ZjlmZmQ5NTdmZjEzYzk4IiwidmVyc2lvbiI6Miwic2l6ZSI6OTQ2LCJsb2NrdGltZSI6OTAwMSwidmluIjpbeyJ0eGlkIjoiY2UxOGM0ODBmZTk4MTI1MWI4MzRlOTI3Mzk5MzA3N2Y1ZDllOGFkNzRlYWRjZDcwMmM5ZWI4OWFmMTAwMDc4ZCIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIyNTg1YTg2OTBmY2MxNzAwZjVjYjE3MDAzMGI4MDAwMCA5YWQ3YTAzZDgwOGEwMjY1MTg0YmVlZDcyYzMyYTMyOTYzYzEzNDRmMzYwNzk3NzI0MmEzNTI4ODFlYzc0ZWQ2ZDI3ZjY5NjU3ZWE1ZTVkZTY0NWFkNjM3OTI5MDY0NDYzNTU0MjRmODcyYzQ3MjJhNDhjMjlhYzY5NDZiZDg5MyAwIDIwNTYxZmQ0YTRkYzdjZGUxYjA2ZjIyNGVjNzAzODBlOGIyNzE4YTA0YWEwZWYyNDllMjdiMjg3NTMxZTY0ZWMzOTIxMDJkMDlkYjA4YWYxZmY0ZTg0NTM5MTljYzg2NmE0YmU0MjdkN2JmZTE4ZjJjMDVlNTQ0NGMxOTZmY2Y2ZmQyODE4NTI3OTAwOWM2MzUzNzk4Mjc3MDE0MDlkNTM3YTU0Nzk3YmJiYzAwMDlkNTFjZTAwODc5MTY5NTJjZTAwODhjMzUzOWQwMGNjMDBjNmEyNjkwMGNkMDBjNzg4MDBkMTAwY2U4ODUyNzk1NDdmNzU4MTUzNzk1ODdmNzc1NDdmNzU4MTU0N2E1YzdmNzc1NDdmNzU4MTUxY2Y1NjdmNzc1MjdmNzU4MTUzN2E1NjgwN2M1MjgwN2U3YjU0ODA3ZTdjNTQ4MDdlMDBjYzAwYzZhMjY5MDBjZDAwYzc4ODAwZDEwMGNlODg1MWQyODg1MWQxNTFjZTg4NTJkMTAwODhjNDUzOWM3Nzc3Njc3YjUxOWQwMGNlN2I4Nzc3NjgiLCJoZXgiOiIxMDI1ODVhODY5MGZjYzE3MDBmNWNiMTcwMDMwYjgwMDAwNDA5YWQ3YTAzZDgwOGEwMjY1MTg0YmVlZDcyYzMyYTMyOTYzYzEzNDRmMzYwNzk3NzI0MmEzNTI4ODFlYzc0ZWQ2ZDI3ZjY5NjU3ZWE1ZTVkZTY0NWFkNjM3OTI5MDY0NDYzNTU0MjRmODcyYzQ3MjJhNDhjMjlhYzY5NDZiZDg5MzAwNGNkMTIwNTYxZmQ0YTRkYzdjZGUxYjA2ZjIyNGVjNzAzODBlOGIyNzE4YTA0YWEwZWYyNDllMjdiMjg3NTMxZTY0ZWMzOTIxMDJkMDlkYjA4YWYxZmY0ZTg0NTM5MTljYzg2NmE0YmU0MjdkN2JmZTE4ZjJjMDVlNTQ0NGMxOTZmY2Y2ZmQyODE4NTI3OTAwOWM2MzUzNzk4Mjc3MDE0MDlkNTM3YTU0Nzk3YmJiYzAwMDlkNTFjZTAwODc5MTY5NTJjZTAwODhjMzUzOWQwMGNjMDBjNmEyNjkwMGNkMDBjNzg4MDBkMTAwY2U4ODUyNzk1NDdmNzU4MTUzNzk1ODdmNzc1NDdmNzU4MTU0N2E1YzdmNzc1NDdmNzU4MTUxY2Y1NjdmNzc1MjdmNzU4MTUzN2E1NjgwN2M1MjgwN2U3YjU0ODA3ZTdjNTQ4MDdlMDBjYzAwYzZhMjY5MDBjZDAwYzc4ODAwZDEwMGNlODg1MWQyODg1MWQxNTFjZTg4NTJkMTAwODhjNDUzOWM3Nzc3Njc3YjUxOWQwMGNlN2I4Nzc3NjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImNlMThjNDgwZmU5ODEyNTFiODM0ZTkyNzM5OTMwNzdmNWQ5ZThhZDc0ZWFkY2Q3MDJjOWViODlhZjEwMDA3OGQiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMCAyMDE3N2RiNThiZTQ4MjY0Y2IxZjFhY2VmMGVmMzMyNTQxZjQ1YzNlNDk2OGIwNmRkOWNiNWQyYzE0ZjEzN2JhZDkyMGMxYzNmMWIwMDA2MzAxMzYwNjllODZjNjE3YWI2ZWVlY2Y2NWZkMjI1OTNmZGE1NTc4YmE5YTQ0MjY3MzQzNWU1Mjc5MDA5YzYzMDBjZTAxMjA3Zjc1ODhjMGQyNzY4Mjc3NjBhMjY5YzBjZjc4NTg3Zjc3NTQ3Zjc1ODE3ODU4N2Y3NzU0N2Y3NTgxYTA2OTdjNTY3Zjc1ODE3YzU2N2Y3NTgxYTA2OWMwY2NjMGM2YTI2OWMwY2RjMGM3ODhjMGQxYzBjZTg3Nzc3NzY3NTI3OTUxOWM2M2MwY2Y1NjdmNzc1MjdmNzU4MWMwY2NjMGM2N2I5M2EyNjljMGNkYzBjNzg4YzBkMWMwY2U4OGMwZDJjMGNmODc3Nzc3Nzc2NzdiNTI5ZDAwY2UwMTIwN2Y3NTdiODhjMGNkYzBjNzg4YzBkMWMwY2U4OGMwZDJjMGNmODc3NzY4NjgiLCJoZXgiOiIwMDRjY2YyMDE3N2RiNThiZTQ4MjY0Y2IxZjFhY2VmMGVmMzMyNTQxZjQ1YzNlNDk2OGIwNmRkOWNiNWQyYzE0ZjEzN2JhZDkyMGMxYzNmMWIwMDA2MzAxMzYwNjllODZjNjE3YWI2ZWVlY2Y2NWZkMjI1OTNmZGE1NTc4YmE5YTQ0MjY3MzQzNWU1Mjc5MDA5YzYzMDBjZTAxMjA3Zjc1ODhjMGQyNzY4Mjc3NjBhMjY5YzBjZjc4NTg3Zjc3NTQ3Zjc1ODE3ODU4N2Y3NzU0N2Y3NTgxYTA2OTdjNTY3Zjc1ODE3YzU2N2Y3NTgxYTA2OWMwY2NjMGM2YTI2OWMwY2RjMGM3ODhjMGQxYzBjZTg3Nzc3NzY3NTI3OTUxOWM2M2MwY2Y1NjdmNzc1MjdmNzU4MWMwY2NjMGM2N2I5M2EyNjljMGNkYzBjNzg4YzBkMWMwY2U4OGMwZDJjMGNmODc3Nzc3Nzc2NzdiNTI5ZDAwY2UwMTIwN2Y3NTdiODhjMGNkYzBjNzg4YzBkMWMwY2U4OGMwZDJjMGNmODc3NzY4NjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImNlMThjNDgwZmU5ODEyNTFiODM0ZTkyNzM5OTMwNzdmNWQ5ZThhZDc0ZWFkY2Q3MDJjOWViODlhZjEwMDA3OGQiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiZmNmMWZiNTk4YjE5ZDhjYWQ5NmM2MDZkOTljMzVhMzM3NjQ4ZTcwMGVhY2MwOGRhZTg2N2Q5YzhiOTUzYjhjNjUwMjhlOWE2ZWJlODE5MDRjZjkxYzgzYzQwMjBlZGU0MWQ0NTcyZThiNGJlZjlkZWUxZTFmZWI4NTQyYjE5NTlbQUxMfEZPUktJRHxVVFhPU10gMDI0NTQ5NmViYmYzZDkwZmE5NGEzOTVhM2I2Y2M2ODUyZmJjNjIxZWU0MTQ3YmJhYzA1ODBhNTlkODAzY2FlOGY4IiwiaGV4IjoiNDFmY2YxZmI1OThiMTlkOGNhZDk2YzYwNmQ5OWMzNWEzMzc2NDhlNzAwZWFjYzA4ZGFlODY3ZDljOGI5NTNiOGM2NTAyOGU5YTZlYmU4MTkwNGNmOTFjODNjNDAyMGVkZTQxZDQ1NzJlOGI0YmVmOWRlZTFlMWZlYjg1NDJiMTk1OTYxMjEwMjQ1NDk2ZWJiZjNkOTBmYTk0YTM5NWEzYjZjYzY4NTJmYmM2MjFlZTQxNDdiYmFjMDU4MGE1OWQ4MDNjYWU4ZjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjFlLTA1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYjcxYmVkMjNiZjdlNjA2ZDJmOTE3OGQ4NWUxNTE3MGQ2OGM0NWExMGE5ZTg2ZGI1MjBhYzA4ZTNlZjQxZjZhIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiNzFiZWQyM2JmN2U2MDZkMmY5MTc4ZDg1ZTE1MTcwZDY4YzQ1YTEwYTllODZkYjUyMGFjMDhlM2VmNDFmNmE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YWhyMGtqODBtN3Zwa2pseXRjbXAwcDI5Y2Rkcno5NXk5ZmFwa20yZzl2cHIzNzdzMGs1NTc0ZHIyd2MiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6IjVlNDM3MzI2NDQ5YWJhNzg1NWRhM2Y1OTIyZmQ2NWNmZWU2ZWFiMTdjNjg2OWUwNjM2MDE2MzAwYjBmMWMzYzEiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJub25lIiwiY29tbWl0bWVudCI6IiJ9fX0seyJ2YWx1ZSI6MC4wNjAyNSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgZWQ2NzMwOWIzNjQyNGMxYjNmMjVlMDhmMTlkOGVhZDRlYWRlOTE3YzQ3NmU1YjE2NTIwZGVhYzYzZDY1ZDRhOSBPUF9FUVVBTCIsImhleCI6ImFhMjBlZDY3MzA5YjM2NDI0YzFiM2YyNWUwOGYxOWQ4ZWFkNGVhZGU5MTdjNDc2ZTViMTY1MjBkZWFjNjNkNjVkNGE5ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMGtrd3Z5bXhlcHljeGVseWhzZzd4d2NhdDJ3NGg1MzAzcmt1a2NrMmd4NzQzM2F2aDIyamx2MzlwZHp0Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJkMGQ0NmY1Y2JkODIxODhhY2VkZTBkM2U0OWM3NTcwMGMxOWNiODMzMWEzMDEwMWYwYmI2YTI2MDA2NmFjOTcyIiwiYW1vdW50IjoiMCIsIm5mdCI6eyJjYXBhYmlsaXR5IjoibXV0YWJsZSIsImNvbW1pdG1lbnQiOiIyNTg1YTg2OTAwMDBlODAzZjVjYjE3MDAzMGI4MDAwMCJ9fX0seyJ2YWx1ZSI6MC4wMzE4NDI3NiwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkyNWU5YmNlY2VhMGU5YTEyOWVkZTM5MmEwNzVkYTJlZjAzYTIyYzEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5MjVlOWJjZWNlYTBlOWExMjllZGUzOTJhMDc1ZGEyZWYwM2EyMmMxODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZjlheDd3ZTZzd25nZmZhaDNlOWdyNG1naDBxdzN6Y3k2a3E1YWZhdSJdfX1dLCJmZWUiOjkuNDZlLTA2LCJoZXgiOiIwMjAwMDAwMDAzOGQwNzAwZjE5YWI4OWUyYzcwY2RhZDRlZDc4YTllNWQ3ZjA3OTMzOTI3ZTkzNGI4NTExMjk4ZmU4MGM0MThjZTAwMDAwMDAwZmQyNjAxMTAyNTg1YTg2OTBmY2MxNzAwZjVjYjE3MDAzMGI4MDAwMDQwOWFkN2EwM2Q4MDhhMDI2NTE4NGJlZWQ3MmMzMmEzMjk2M2MxMzQ0ZjM2MDc5NzcyNDJhMzUyODgxZWM3NGVkNmQyN2Y2OTY1N2VhNWU1ZGU2NDVhZDYzNzkyOTA2NDQ2MzU1NDI0Zjg3MmM0NzIyYTQ4YzI5YWM2OTQ2YmQ4OTMwMDRjZDEyMDU2MWZkNGE0ZGM3Y2RlMWIwNmYyMjRlYzcwMzgwZThiMjcxOGEwNGFhMGVmMjQ5ZTI3YjI4NzUzMWU2NGVjMzkyMTAyZDA5ZGIwOGFmMWZmNGU4NDUzOTE5Y2M4NjZhNGJlNDI3ZDdiZmUxOGYyYzA1ZTU0NDRjMTk2ZmNmNmZkMjgxODUyNzkwMDljNjM1Mzc5ODI3NzAxNDA5ZDUzN2E1NDc5N2JiYmMwMDA5ZDUxY2UwMDg3OTE2OTUyY2UwMDg4YzM1MzlkMDBjYzAwYzZhMjY5MDBjZDAwYzc4ODAwZDEwMGNlODg1Mjc5NTQ3Zjc1ODE1Mzc5NTg3Zjc3NTQ3Zjc1ODE1NDdhNWM3Zjc3NTQ3Zjc1ODE1MWNmNTY3Zjc3NTI3Zjc1ODE1MzdhNTY4MDdjNTI4MDdlN2I1NDgwN2U3YzU0ODA3ZTAwY2MwMGM2YTI2OTAwY2QwMGM3ODgwMGQxMDBjZTg4NTFkMjg4NTFkMTUxY2U4ODUyZDEwMDg4YzQ1MzljNzc3NzY3N2I1MTlkMDBjZTdiODc3NzY4ZmZmZmZmZmY4ZDA3MDBmMTlhYjg5ZTJjNzBjZGFkNGVkNzhhOWU1ZDdmMDc5MzM5MjdlOTM0Yjg1MTEyOThmZTgwYzQxOGNlMDEwMDAwMDBkMjAwNGNjZjIwMTc3ZGI1OGJlNDgyNjRjYjFmMWFjZWYwZWYzMzI1NDFmNDVjM2U0OTY4YjA2ZGQ5Y2I1ZDJjMTRmMTM3YmFkOTIwYzFjM2YxYjAwMDYzMDEzNjA2OWU4NmM2MTdhYjZlZWVjZjY1ZmQyMjU5M2ZkYTU1NzhiYTlhNDQyNjczNDM1ZTUyNzkwMDljNjMwMGNlMDEyMDdmNzU4OGMwZDI3NjgyNzc2MGEyNjljMGNmNzg1ODdmNzc1NDdmNzU4MTc4NTg3Zjc3NTQ3Zjc1ODFhMDY5N2M1NjdmNzU4MTdjNTY3Zjc1ODFhMDY5YzBjY2MwYzZhMjY5YzBjZGMwYzc4OGMwZDFjMGNlODc3Nzc3Njc1Mjc5NTE5YzYzYzBjZjU2N2Y3NzUyN2Y3NTgxYzBjY2MwYzY3YjkzYTI2OWMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Nzc3NzY3N2I1MjlkMDBjZTAxMjA3Zjc1N2I4OGMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Njg2OGZmZmZmZmZmOGQwNzAwZjE5YWI4OWUyYzcwY2RhZDRlZDc4YTllNWQ3ZjA3OTMzOTI3ZTkzNGI4NTExMjk4ZmU4MGM0MThjZTAyMDAwMDAwNjQ0MWZjZjFmYjU5OGIxOWQ4Y2FkOTZjNjA2ZDk5YzM1YTMzNzY0OGU3MDBlYWNjMDhkYWU4NjdkOWM4Yjk1M2I4YzY1MDI4ZTlhNmViZTgxOTA0Y2Y5MWM4M2M0MDIwZWRlNDFkNDU3MmU4YjRiZWY5ZGVlMWUxZmViODU0MmIxOTU5NjEyMTAyNDU0OTZlYmJmM2Q5MGZhOTRhMzk1YTNiNmNjNjg1MmZiYzYyMWVlNDE0N2JiYWMwNTgwYTU5ZDgwM2NhZThmOGZmZmZmZmZmMDNlODAzMDAwMDAwMDAwMDAwNDVlZmMxYzNmMWIwMDA2MzAxMzYwNjllODZjNjE3YWI2ZWVlY2Y2NWZkMjI1OTNmZGE1NTc4YmE5YTQ0MjY3MzQzNWUyMGFhMjAzYjcxYmVkMjNiZjdlNjA2ZDJmOTE3OGQ4NWUxNTE3MGQ2OGM0NWExMGE5ZTg2ZGI1MjBhYzA4ZTNlZjQxZjZhODcyOGVmNWIwMDAwMDAwMDAwNTZlZjcyYzk2YTA2NjBhMmI2MGIxZjEwMzAxYTMzYjg5Y2MxMDA1N2M3NDkzZTBkZGVjZThhMTg4MmJkNWM2ZmQ0ZDA2MTEwMjU4NWE4NjkwMDAwZTgwM2Y1Y2IxNzAwMzBiODAwMDBhYTIwZWQ2NzMwOWIzNjQyNGMxYjNmMjVlMDhmMTlkOGVhZDRlYWRlOTE3YzQ3NmU1YjE2NTIwZGVhYzYzZDY1ZDRhOTg3OTQ5NjMwMDAwMDAwMDAwMDE5NzZhOTE0OTI1ZTliY2VjZWEwZTlhMTI5ZWRlMzkyYTA3NWRhMmVmMDNhMjJjMTg4YWMyOTIzMDAwMCJ9LHsidHhpZCI6IjIzN2Y0ZGY3M2FkYjZhZDhmNjNhZjk3NTg4MWNkODc5YTAwMGU4Y2IwZjRhNTIyZDM0OTAyMGNlYWVkYzgxNDEiLCJoYXNoIjoiMjM3ZjRkZjczYWRiNmFkOGY2M2FmOTc1ODgxY2Q4NzlhMDAwZThjYjBmNGE1MjJkMzQ5MDIwY2VhZWRjODE0MSIsInZlcnNpb24iOjIsInNpemUiOjIxOSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImM1MmRkYTMzMDIxZGQ2YzI2YzJjNjhiY2FiMzFjNzJmYzVlMTZjZDQ5MTA0M2NhMDk0OTU3NDM2ZDA4NDNmNTUiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiZmY1NDhkNGUyN2E3NzA3NTI3NWNlM2NlNjlkODBlNGY5Y2YyYmI5MDhmZjY3MDYyM2IzYzJmODJmZDc3MTYzYmM2ZmIxMDE3MzgwMjkxNjNmZDFjMzY0OWNlOWM0ZTA4ZTEwN2NhYmY1YmI2MWIyNzYyNjBlYzZiMmRmOTNkOGRbQUxMfEZPUktJRF0gMDNlMTRkNDY0N2FhYWMzM2M2MWE3ZjEzNWE4ZDQ2ZjU3OWNjMWI3MjBlZDAyOTZkZTA2ZmJlMTlkYjA2MzkxYWFiIiwiaGV4IjoiNDFmZjU0OGQ0ZTI3YTc3MDc1Mjc1Y2UzY2U2OWQ4MGU0ZjljZjJiYjkwOGZmNjcwNjIzYjNjMmY4MmZkNzcxNjNiYzZmYjEwMTczODAyOTE2M2ZkMWMzNjQ5Y2U5YzRlMDhlMTA3Y2FiZjViYjYxYjI3NjI2MGVjNmIyZGY5M2Q4ZDQxMjEwM2UxNGQ0NjQ3YWFhYzMzYzYxYTdmMTM1YThkNDZmNTc5Y2MxYjcyMGVkMDI5NmRlMDZmYmUxOWRiMDYzOTFhYWIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMDA0MzE3MDQsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBjMjlmYjFlYzUxOWZmNjI3ZDBiNjA1ODZhNDAxZmE5ZDRiMjIyMDNiIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YzI5ZmIxZWM1MTlmZjYyN2QwYjYwNTg2YTQwMWZhOWQ0YjIyMjAzYjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcnBmbHYwdjJ4MGx2Zjdza2N6Y2RmcXBsMnc1a2czcTh2OXhwYzBkZzciXX19LHsidmFsdWUiOjAuMTExNTkwNjksIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMDIxNjJlZWJiNzliYzQ2ZWQzNGZjY2Y2OTNmM2UxOThmNWJhYmNjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTAyMTYyZWViYjc5YmM0NmVkMzRmY2NmNjkzZjNlMTk4ZjViYWJjYzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWd6emNod2hkdW1jM2hkeG43djc2Zmw4Y3ZjN2thdGVzaGEzZzhlbGUiXX19XSwiZmVlIjoyLjI3ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTU1M2Y4NGQwMzY3NDk1OTRhMDNjMDQ5MWQ0NmNlMWM1MmZjNzMxYWJiYzY4MmM2Y2MyZDYxZDAyMzNkYTJkYzUwMDAwMDAwMDY0NDFmZjU0OGQ0ZTI3YTc3MDc1Mjc1Y2UzY2U2OWQ4MGU0ZjljZjJiYjkwOGZmNjcwNjIzYjNjMmY4MmZkNzcxNjNiYzZmYjEwMTczODAyOTE2M2ZkMWMzNjQ5Y2U5YzRlMDhlMTA3Y2FiZjViYjYxYjI3NjI2MGVjNmIyZGY5M2Q4ZDQxMjEwM2UxNGQ0NjQ3YWFhYzMzYzYxYTdmMTM1YThkNDZmNTc5Y2MxYjcyMGVkMDI5NmRlMDZmYmUxOWRiMDYzOTFhYWJmZmZmZmZmZjAyNTg5NjA2MDAwMDAwMDAwMDE5NzZhOTE0YzI5ZmIxZWM1MTlmZjYyN2QwYjYwNTg2YTQwMWZhOWQ0YjIyMjAzYjg4YWMxZDQ2YWEwMDAwMDAwMDAwMTk3NmE5MTQxMDIxNjJlZWJiNzliYzQ2ZWQzNGZjY2Y2OTNmM2UxOThmNWJhYmNjODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiMjVjY2E0NTUzZGEzZTU0YjY5OGNlODY4N2VhNGQxOGM1ODE2NTcxNzdhMzdlOGFlNTg2ZDVhYjQ5ZmViMmM4OCIsImhhc2giOiIyNWNjYTQ1NTNkYTNlNTRiNjk4Y2U4Njg3ZWE0ZDE4YzU4MTY1NzE3N2EzN2U4YWU1ODZkNWFiNDlmZWIyYzg4IiwidmVyc2lvbiI6MSwic2l6ZSI6MjI1LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiYzljYWUzOWEyMTc5YmZjZmI1OGMwYmRmOTYyODljNjgyYzUzZmNmYmMzMTAzNTlkMzExYzA1OTI3YWRmNjhmNiIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDYyMWQzYzA1NmRkZmMzY2RhYWE5NzYwOWEzODIwYjIyZGZlNzM4YmZiNDdiNDFhOGY1ZTJiZWYzMTYzOTFlMDkwMjIwNzkzMzk3OTBjNTMwNjk4OTI1Yjk1MTUyNjNjODdkMTAwYmVmM2ZmYjlhMWYwYTYwMmQ5ZDBkMWFjN2JjMmYxZltBTEx8Rk9SS0lEXSAwMzE2ZDMwMTliYmUyYTU5NGFjZjFhN2UwMTUzYjBjZjQ4NjhlMDgwYTViZTkzMjZhOGE5ZjQwMTY1Y2QwYjFmZjQiLCJoZXgiOiI0NzMwNDQwMjIwNjIxZDNjMDU2ZGRmYzNjZGFhYTk3NjA5YTM4MjBiMjJkZmU3MzhiZmI0N2I0MWE4ZjVlMmJlZjMxNjM5MWUwOTAyMjA3OTMzOTc5MGM1MzA2OTg5MjViOTUxNTI2M2M4N2QxMDBiZWYzZmZiOWExZjBhNjAyZDlkMGQxYWM3YmMyZjFmNDEyMTAzMTZkMzAxOWJiZTJhNTk0YWNmMWE3ZTAxNTNiMGNmNDg2OGUwODBhNWJlOTMyNmE4YTlmNDAxNjVjZDBiMWZmNCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMTExMiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDFhNzYwOGU2YjQ3YzJiYmUyYWU2Yzk1YzFmZTZkNWI4ZWE1M2E2NDIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxYTc2MDhlNmI0N2MyYmJlMmFlNmM5NWMxZmU2ZDViOGVhNTNhNjQyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZDh2ejh4azM3emgwMzJ1bXk0YzhseDZrdXc1NWF4Z2d5bXZoNWxzdiJdfX0seyJ2YWx1ZSI6MC44Mjg2ODE3MywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDBkNGNjYmZmNTM5MTZiMzliMGUxMDcyOTA2ZTcyMjBlYzk2ZWE5MTUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwZDRjY2JmZjUzOTE2YjM5YjBlMTA3MjkwNmU3MjIwZWM5NmVhOTE1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxeDVlamxsMndna2t3ZHN1eXJqanBoOHlnOHZqbTRmejUwbmRscDZoZSJdfX1dLCJmZWUiOjIuMjZlLTA2LCJoZXgiOiIwMTAwMDAwMDAxZjY2OGRmN2E5MjA1MWMzMTlkMzUxMGMzZmJmYzUzMmM2ODljMjg5NmRmMGI4Y2I1Y2ZiZjc5MjE5YWUzY2FjOTAxMDAwMDAwNmE0NzMwNDQwMjIwNjIxZDNjMDU2ZGRmYzNjZGFhYTk3NjA5YTM4MjBiMjJkZmU3MzhiZmI0N2I0MWE4ZjVlMmJlZjMxNjM5MWUwOTAyMjA3OTMzOTc5MGM1MzA2OTg5MjViOTUxNTI2M2M4N2QxMDBiZWYzZmZiOWExZjBhNjAyZDlkMGQxYWM3YmMyZjFmNDEyMTAzMTZkMzAxOWJiZTJhNTk0YWNmMWE3ZTAxNTNiMGNmNDg2OGUwODBhNWJlOTMyNmE4YTlmNDAxNjVjZDBiMWZmNGZmZmZmZmZmMDJjMGY3MTAwMDAwMDAwMDAwMTk3NmE5MTQxYTc2MDhlNmI0N2MyYmJlMmFlNmM5NWMxZmU2ZDViOGVhNTNhNjQyODhhY2NkNzdmMDA0MDAwMDAwMDAxOTc2YTkxNDBkNGNjYmZmNTM5MTZiMzliMGUxMDcyOTA2ZTcyMjBlYzk2ZWE5MTU4OGFjMDAwMDAwMDAifSx7InR4aWQiOiIyNzYzNTBmNDZjMTdkN2U1Y2EzNzE0NTBkMWRmZjhkZWE2YmEwNzU1NjVjZDRkYjdjN2NkNzIyYzA5YjhjOGYwIiwiaGFzaCI6IjI3NjM1MGY0NmMxN2Q3ZTVjYTM3MTQ1MGQxZGZmOGRlYTZiYTA3NTU2NWNkNGRiN2M3Y2Q3MjJjMDliOGM4ZjAiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyMjUsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI2YTVmZTUxZmI3YTMwMTg3NTE1YTdjZWZlYTNjN2U3ZTQ5ZGFkMThlZGIzZjI1MjNlYzE1ZjBmYjA2ZTA0ZGJhIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwMmU4NGJiZGVkYWZhYjc2ODM5YjFmYTM4YjZjYjFlMmRkNWRmN2M2ZmIzNDY5MDFmNTU5M2ZkMDc5NzM3NjMzYTAyMjA3YTM2YmM2ZGI0YWEyMmQ0OWEyOGY1MWViYTFkNjZjNDI4Y2QzZDhkZWM5Y2U2YTY2ZjQ2MGI1ZjNlMDFmZjM2W0FMTHxGT1JLSURdIDAzOGFjNjA4YzNjNGQwYjc1YzlkYWU3ODc5Y2VmMTMxZjMxNTc5NGEwZjk4N2ZiOGQ0MjA3NjUwMWYwZjhmNWM4NyIsImhleCI6IjQ3MzA0NDAyMjAyZTg0YmJkZWRhZmFiNzY4MzliMWZhMzhiNmNiMWUyZGQ1ZGY3YzZmYjM0NjkwMWY1NTkzZmQwNzk3Mzc2MzNhMDIyMDdhMzZiYzZkYjRhYTIyZDQ5YTI4ZjUxZWJhMWQ2NmM0MjhjZDNkOGRlYzljZTZhNjZmNDYwYjVmM2UwMWZmMzY0MTIxMDM4YWM2MDhjM2M0ZDBiNzVjOWRhZTc4NzljZWYxMzFmMzE1Nzk0YTBmOTg3ZmI4ZDQyMDc2NTAxZjBmOGY1Yzg3In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjAwMDIzNzU5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMWU3ODIyNTE2MTFkNWRkYmNmNTczMzgzM2MwNjkwM2FhYTRmYTgxZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDFlNzgyMjUxNjExZDVkZGJjZjU3MzM4MzNjMDY5MDNhYWE0ZmE4MWQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXEwOHNnajN2eXc0bWs3MDJ1ZWN4MHF4anFhMjVuYWdyNWR2bmVuYXN4Il19fSx7InZhbHVlIjowLjAwODM0NTcyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTViOTU1OWY0NmE5MjlmYjc1YmNlN2VkYzljYjRhOGRhNDRiOWRjNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU1Yjk1NTlmNDZhOTI5ZmI3NWJjZTdlZGM5Y2I0YThkYTQ0YjlkYzQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXAybWo0dmxnNjVqbjdtNGhubjdtand0ZjJ4NmdqdWFjc3J0aG01c2FwIl19fV0sImZlZSI6NS45NWUtMDYsImhleCI6IjAxMDAwMDAwMDFiYTRkZTAwNmZiZjAxNWVjMjMyNTNmZGI4ZWQxZGE0OTdlN2UzY2VhZWY3YzVhNTE4NzAxYTNiNzFmZTU1ZjZhMDAwMDAwMDA2YTQ3MzA0NDAyMjAyZTg0YmJkZWRhZmFiNzY4MzliMWZhMzhiNmNiMWUyZGQ1ZGY3YzZmYjM0NjkwMWY1NTkzZmQwNzk3Mzc2MzNhMDIyMDdhMzZiYzZkYjRhYTIyZDQ5YTI4ZjUxZWJhMWQ2NmM0MjhjZDNkOGRlYzljZTZhNjZmNDYwYjVmM2UwMWZmMzY0MTIxMDM4YWM2MDhjM2M0ZDBiNzVjOWRhZTc4NzljZWYxMzFmMzE1Nzk0YTBmOTg3ZmI4ZDQyMDc2NTAxZjBmOGY1Yzg3ZmZmZmZmZmYwMmNmNWMwMDAwMDAwMDAwMDAxOTc2YTkxNDFlNzgyMjUxNjExZDVkZGJjZjU3MzM4MzNjMDY5MDNhYWE0ZmE4MWQ4OGFjMGNiYzBjMDAwMDAwMDAwMDE5NzZhOTE0NTViOTU1OWY0NmE5MjlmYjc1YmNlN2VkYzljYjRhOGRhNDRiOWRjNDg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjJiYzNiNmQwZDE1MTIxYTBlYWVmMjczNjQ3MzBiMTZhNWRhMzQ5ZTgzYzIzYWJiNmQwMDIzNzcyZDFhYWJhOTIiLCJoYXNoIjoiMmJjM2I2ZDBkMTUxMjFhMGVhZWYyNzM2NDczMGIxNmE1ZGEzNDllODNjMjNhYmI2ZDAwMjM3NzJkMWFhYmE5MiIsInZlcnNpb24iOjIsInNpemUiOjIxOSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjJkZTFkZTIxYzg3NWRmMDM0MTYzOWFlNmU0MDk2YWVlMWNmZWEzNTJjZDFmMzk3MzUxNmJlMjdmNWQ1ZTc2YWEiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMGU3YTM4ZmJkYzBmN2UzYTk2MTk3ZDkxMjZjY2ViZDc0YWJiNTE2ZWJhZDAwNTdhMTg1ZTAyOTUwYmM5MzE2ODlmZTNmMGY3Nzg4NDBmNjk4NmQ4MWI2ZDUzZGM1MGUyN2VmNWFkNDVjNWIzMjY4YTVlMGY2ODM0NWU5ZTVmNmRbQUxMfEZPUktJRF0gMDJjMzE2N2MzYzEzZWQ5NTJjNGNkZDljODk0ZDE2MmFhNDI4ZDdkMDFjNDYxZDVlODUyZWIwMTIxM2E5MTBjYjM1IiwiaGV4IjoiNDEwZTdhMzhmYmRjMGY3ZTNhOTYxOTdkOTEyNmNjZWJkNzRhYmI1MTZlYmFkMDA1N2ExODVlMDI5NTBiYzkzMTY4OWZlM2YwZjc3ODg0MGY2OTg2ZDgxYjZkNTNkYzUwZTI3ZWY1YWQ0NWM1YjMyNjhhNWUwZjY4MzQ1ZTllNWY2ZDQxMjEwMmMzMTY3YzNjMTNlZDk1MmM0Y2RkOWM4OTRkMTYyYWE0MjhkN2QwMWM0NjFkNWU4NTJlYjAxMjEzYTkxMGNiMzUifSwic2VxdWVuY2UiOjB9XSwidm91dCI6W3sidmFsdWUiOjAuMDYwODM4NjYsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2OGNlOWU2ZjNiOGE3MmNiNDMwNmU3M2NkMzhjMTVhMzYyNzU3YWI0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NjhjZTllNmYzYjhhNzJjYjQzMDZlNzNjZDM4YzE1YTM2Mjc1N2FiNDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDV2YThuMDh3OTg5ajZycW1ubmU1dXZ6azNreWF0NmtzN3U1eHltNTgiXX19LHsidmFsdWUiOjEwLjU4MTQxNzQ4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjZjZDk3M2U4OTRkOTU3ZGI5NmVhODQ2Njc0ZjA2MGY2MDBjZWVhZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGI2Y2Q5NzNlODk0ZDk1N2RiOTZlYTg0NjY3NGYwNjBmNjAwY2VlYWU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXptdm05ZTczOXhlMmxkZWQ2NXl2ZTYwcWM4a3FyOHc0Y2cydnd0ZW01Il19fV0sImZlZSI6Mi4xOWUtMDYsImhleCI6IjAyMDAwMDAwMDFhYTc2NWU1ZDdmZTI2YjUxNzMzOTFmY2Q1MmEzZmUxY2VlNmEwOWU0ZTY5YTYzNDEwM2RmNzVjODIxZGVlMTJkMDAwMDAwMDA2NDQxMGU3YTM4ZmJkYzBmN2UzYTk2MTk3ZDkxMjZjY2ViZDc0YWJiNTE2ZWJhZDAwNTdhMTg1ZTAyOTUwYmM5MzE2ODlmZTNmMGY3Nzg4NDBmNjk4NmQ4MWI2ZDUzZGM1MGUyN2VmNWFkNDVjNWIzMjY4YTVlMGY2ODM0NWU5ZTVmNmQ0MTIxMDJjMzE2N2MzYzEzZWQ5NTJjNGNkZDljODk0ZDE2MmFhNDI4ZDdkMDFjNDYxZDVlODUyZWIwMTIxM2E5MTBjYjM1MDAwMDAwMDAwMjFhZDU1YzAwMDAwMDAwMDAxOTc2YTkxNDY4Y2U5ZTZmM2I4YTcyY2I0MzA2ZTczY2QzOGMxNWEzNjI3NTdhYjQ4OGFjMzRmNjExM2YwMDAwMDAwMDE5NzZhOTE0YjZjZDk3M2U4OTRkOTU3ZGI5NmVhODQ2Njc0ZjA2MGY2MDBjZWVhZTg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjJkZTFkZTIxYzg3NWRmMDM0MTYzOWFlNmU0MDk2YWVlMWNmZWEzNTJjZDFmMzk3MzUxNmJlMjdmNWQ1ZTc2YWEiLCJoYXNoIjoiMmRlMWRlMjFjODc1ZGYwMzQxNjM5YWU2ZTQwOTZhZWUxY2ZlYTM1MmNkMWYzOTczNTE2YmUyN2Y1ZDVlNzZhYSIsInZlcnNpb24iOjIsInNpemUiOjIxOSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjA2MTA3NjgwMWIzNmMzNTQ3ODI2YWVlYzVjOWRmOTU5NzQ1YzAzMjUyYmJiYjMyMDRhOWFhNDkxNmJhMzViMjEiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiODRiZGIyZjg2ZDc3OWIxNjlkODExOWNkNGNjYjkzODEyZDlkZWNjMTEwZTkxNWYzY2YyYTNhOTFiNGI3OTRjMWRhNTczYTE3MDBlZmI1YjVjZGVkNTlkZmFlZTRiZGE2NmI2ODY2ZjFiMjQzNWY1ZmFjMjkxNWY3YTc0OTU1ZWFbQUxMfEZPUktJRF0gMDIyM2ZmMmNkZTBjOGY3ZmM0OTFhN2I3ZGNiZTMxZmU1ZjQwYjI3M2Q1OWU4MGQ5MzY1MzdmNzM5ZTY2MzYzZDE4IiwiaGV4IjoiNDE4NGJkYjJmODZkNzc5YjE2OWQ4MTE5Y2Q0Y2NiOTM4MTJkOWRlY2MxMTBlOTE1ZjNjZjJhM2E5MWI0Yjc5NGMxZGE1NzNhMTcwMGVmYjViNWNkZWQ1OWRmYWVlNGJkYTY2YjY4NjZmMWIyNDM1ZjVmYWMyOTE1ZjdhNzQ5NTVlYTQxMjEwMjIzZmYyY2RlMGM4ZjdmYzQ5MWE3YjdkY2JlMzFmZTVmNDBiMjczZDU5ZTgwZDkzNjUzN2Y3MzllNjYzNjNkMTgifSwic2VxdWVuY2UiOjB9XSwidm91dCI6W3sidmFsdWUiOjEwLjY0MjI1ODMzLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTE3NDViOTZiN2NlOGRkYzJlMzUzYzcwMzA4YzA4OGIyYTIyZWExNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkxNzQ1Yjk2YjdjZThkZGMyZTM1M2M3MDMwOGMwODhiMmEyMmVhMTc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpnaGdrdWtrbDhnbWhwd3g1Nzhxdnl2cHo5ajVnaDJ6dXd5ZWxqNzU3Il19fSx7InZhbHVlIjowLjAyOTAyODI1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNGFjZDIxYTJjNTE3MGRjOTEyZmU5NzI0N2ZhM2NhYWEzNGJlZDQ2MCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDRhY2QyMWEyYzUxNzBkYzkxMmZlOTcyNDdmYTNjYWFhMzRiZWQ0NjA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA5djZnZHpjNXRzbWpnamw2dGpnbGFyZTI0cmYwazV2cXc1cDlmdjlrIl19fV0sImZlZSI6Mi4xOWUtMDYsImhleCI6IjAyMDAwMDAwMDEyMTViYTM2YjkxYTQ5YTRhMjBiM2JiMmIyNTAzNWM3NDU5Zjk5ZDVjZWNhZTI2Nzg1NGMzMzYxYjgwNzYxMDA2MDAwMDAwMDA2NDQxODRiZGIyZjg2ZDc3OWIxNjlkODExOWNkNGNjYjkzODEyZDlkZWNjMTEwZTkxNWYzY2YyYTNhOTFiNGI3OTRjMWRhNTczYTE3MDBlZmI1YjVjZGVkNTlkZmFlZTRiZGE2NmI2ODY2ZjFiMjQzNWY1ZmFjMjkxNWY3YTc0OTU1ZWE0MTIxMDIyM2ZmMmNkZTBjOGY3ZmM0OTFhN2I3ZGNiZTMxZmU1ZjQwYjI3M2Q1OWU4MGQ5MzY1MzdmNzM5ZTY2MzYzZDE4MDAwMDAwMDAwMjI5Y2M2ZTNmMDAwMDAwMDAxOTc2YTkxNDkxNzQ1Yjk2YjdjZThkZGMyZTM1M2M3MDMwOGMwODhiMmEyMmVhMTc4OGFjMjk0YjJjMDAwMDAwMDAwMDE5NzZhOTE0NGFjZDIxYTJjNTE3MGRjOTEyZmU5NzI0N2ZhM2NhYWEzNGJlZDQ2MDg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjJmYzUxMTFjZWUxYmRiMGE2ODM0MmFjYTc5ZTNiNWE5YTdhZTM1Y2U5Yzg3YjhhMmQ2NTNiNmI0NDMzMmVjZDkiLCJoYXNoIjoiMmZjNTExMWNlZTFiZGIwYTY4MzQyYWNhNzllM2I1YTlhN2FlMzVjZTljODdiOGEyZDY1M2I2YjQ0MzMyZWNkOSIsInZlcnNpb24iOjIsInNpemUiOjMzOSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjM5ZGFlZDBmOThmNzg3OWM3OTFmOTcyZDgzOGQxMTVmNWMyMzgzMWM4N2QyM2ExNmQ1MmExOThmODFlNmRkYzEiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAwOTEyY2YxOTZiMzZlMWZmMDdlY2Y1MjMyN2UwNDA4ZTUyMDgwNjVlZjlmOTA4MTQxNDI5Y2NkNjc0OWU2Y2JjMDIyMDFmZWNmMzI4ZjY2MTUwOWZkY2JmMmUxZTRkN2QyMzk0Y2RiMDkwN2IzOWEwMWFhZmE3YTdhYmM5MTIwMDZlMjVbQUxMfEZPUktJRF0gMDJmNzZkNDcxY2E1YzcwNGEwMGM3Y2JiMzczYjhjNjhlMWNmNTJmZjFhZTRhYjEzN2U2NWRjMDA1MWExMGJlZWIyIiwiaGV4IjoiNDczMDQ0MDIyMDA5MTJjZjE5NmIzNmUxZmYwN2VjZjUyMzI3ZTA0MDhlNTIwODA2NWVmOWY5MDgxNDE0MjljY2Q2NzQ5ZTZjYmMwMjIwMWZlY2YzMjhmNjYxNTA5ZmRjYmYyZTFlNGQ3ZDIzOTRjZGIwOTA3YjM5YTAxYWFmYTdhN2FiYzkxMjAwNmUyNTQxMjEwMmY3NmQ0NzFjYTVjNzA0YTAwYzdjYmIzNzNiOGM2OGUxY2Y1MmZmMWFlNGFiMTM3ZTY1ZGMwMDUxYTEwYmVlYjIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYzODNiZDg1YTc2YWE0NjI0NzEyYWU1MmM1MmIxYWM3Zjg0MjhhZTVmZjE3ZmNlMTI3MzAzNDQwODZmN2EyMTEiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMGUwNGZhOGIzZjhmMmNiNjFlMzdkNzZjYjhkMzEyZGI0YjllMGQyNGQ4NzBjMWI1NGQyYWE2MGJlOTk4YzE1NTMwMjIwMTBmMjE1YjI1MDZkMWNlN2U0MmFjZTFkNzIyNWE4ZjFjY2I0M2JlMjRkZjg5ODE4MTFjYjNhMDczMGM4OTdiN1tBTEx8Rk9SS0lEXSAwM2ZhOWNmNTkwYjhmOTE0NjI5ZjJlZDQwYWUxMTM2YmNlNDEyMTliYTU1OTIzNTU5M2UxZWM0YzFkODM3NmYyOTUiLCJoZXgiOiI0ODMwNDUwMjIxMDBlMDRmYThiM2Y4ZjJjYjYxZTM3ZDc2Y2I4ZDMxMmRiNGI5ZTBkMjRkODcwYzFiNTRkMmFhNjBiZTk5OGMxNTUzMDIyMDEwZjIxNWIyNTA2ZDFjZTdlNDJhY2UxZDcyMjVhOGYxY2NiNDNiZTI0ZGY4OTgxODExY2IzYTA3MzBjODk3Yjc0MTIxMDNmYTljZjU5MGI4ZjkxNDYyOWYyZWQ0MGFlMTEzNmJjZTQxMjE5YmE1NTkyMzU1OTNlMWVjNGMxZDgzNzZmMjk1In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjE5NDM1NTg5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjBmZmJiODljZWFjNzAzYjJjNWY1ZjUzOTJmNGFkZTYwMmViM2FkOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGIwZmZiYjg5Y2VhYzcwM2IyYzVmNWY1MzkyZjRhZGU2MDJlYjNhZDg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpjMGx3dWZlNms4cXdldnRhMDQ4eWg1NGhucTk2ZTZtcWMyM3pnN3YyIl19fV0sImZlZSI6Ni4wM2UtMDYsImhleCI6IjAyMDAwMDAwMDJjMWRkZTY4MThmMTkyYWQ1MTYzYWQyODcxYzgzMjM1YzVmMTE4ZDgzMmQ5NzFmNzk5Yzg3Zjc5ODBmZWRkYTM5MDEwMDAwMDA2YTQ3MzA0NDAyMjAwOTEyY2YxOTZiMzZlMWZmMDdlY2Y1MjMyN2UwNDA4ZTUyMDgwNjVlZjlmOTA4MTQxNDI5Y2NkNjc0OWU2Y2JjMDIyMDFmZWNmMzI4ZjY2MTUwOWZkY2JmMmUxZTRkN2QyMzk0Y2RiMDkwN2IzOWEwMWFhZmE3YTdhYmM5MTIwMDZlMjU0MTIxMDJmNzZkNDcxY2E1YzcwNGEwMGM3Y2JiMzczYjhjNjhlMWNmNTJmZjFhZTRhYjEzN2U2NWRjMDA1MWExMGJlZWIyZmZmZmZmZmYxMWEyZjc4NjQwMzQzMDI3ZTFmYzE3ZmZlNThhNDJmOGM3MWEyYmM1NTJhZTEyNDc2MmE0NmFhNzg1YmQ4MzYzMDAwMDAwMDA2YjQ4MzA0NTAyMjEwMGUwNGZhOGIzZjhmMmNiNjFlMzdkNzZjYjhkMzEyZGI0YjllMGQyNGQ4NzBjMWI1NGQyYWE2MGJlOTk4YzE1NTMwMjIwMTBmMjE1YjI1MDZkMWNlN2U0MmFjZTFkNzIyNWE4ZjFjY2I0M2JlMjRkZjg5ODE4MTFjYjNhMDczMGM4OTdiNzQxMjEwM2ZhOWNmNTkwYjhmOTE0NjI5ZjJlZDQwYWUxMTM2YmNlNDEyMTliYTU1OTIzNTU5M2UxZWM0YzFkODM3NmYyOTVmZmZmZmZmZjAxNDU5MDI4MDEwMDAwMDAwMDE5NzZhOTE0YjBmZmJiODljZWFjNzAzYjJjNWY1ZjUzOTJmNGFkZTYwMmViM2FkODg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjMyMTNhNDkwZmMxYmU5ZDQ2M2I2ZGY5ZGVjZjg3MjA1NmMyMzMwM2E5OThhMGE4ZWU1YjM3ZDU3OGUyMWM2MTAiLCJoYXNoIjoiMzIxM2E0OTBmYzFiZTlkNDYzYjZkZjlkZWNmODcyMDU2YzIzMzAzYTk5OGEwYThlZTViMzdkNTc4ZTIxYzYxMCIsInZlcnNpb24iOjIsInNpemUiOjI1MywibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjFkYTMzY2QxNWYwYjRiNmE0MDBjMzk0M2IzZjRlM2MzOGQ4YjNmZDgzMjEwYjY4YTFiNGQ4MGFlMTlkNGU2ZmIiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiMzFjNjc2MTVlY2QzNDEzMjI4ZDViNWQ4ZjkwYzc3ZmU1NGJmNzJjMzFiOGYyMjM4ZjU4YzA2OWZhZWUwM2I2ZGQyNzZiZjIyYTJkZGM5N2IzNTBjZDY1Y2ZkZmJiNDcwNDRlMzBlYTY5YzI3M2U2ZTYyOTFkOWM2MjYyMTYxY2VbQUxMfEZPUktJRF0gMDNjMjgwYTkzMzZkNWFjMWJiZThkMTZhNDIxMDk0YTc2NWEyYzRkMjFhMWRjMjAwZmYzMzkyZTA1MmY1NTg2MGVkIiwiaGV4IjoiNDEzMWM2NzYxNWVjZDM0MTMyMjhkNWI1ZDhmOTBjNzdmZTU0YmY3MmMzMWI4ZjIyMzhmNThjMDY5ZmFlZTAzYjZkZDI3NmJmMjJhMmRkYzk3YjM1MGNkNjVjZmRmYmI0NzA0NGUzMGVhNjljMjczZTZlNjI5MWQ5YzYyNjIxNjFjZTQxMjEwM2MyODBhOTMzNmQ1YWMxYmJlOGQxNmE0MjEwOTRhNzY1YTJjNGQyMWExZGMyMDBmZjMzOTJlMDUyZjU1ODYwZWQifSwic2VxdWVuY2UiOjB9XSwidm91dCI6W3sidmFsdWUiOjAuMDAxMzQsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBiZDJhOTQ1ZGQ4NGE4NWI3ZDQyMDdjYjk2N2Y4MDFlZjY4MzdjYzU2IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YmQyYTk0NWRkODRhODViN2Q0MjA3Y2I5NjdmODAxZWY2ODM3Y2M1Njg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxejdqNDl6YW1wOWd0ZDc1eXA3dGplbGNxOGhrc2Q3djJjN3A1NzZxcWgiXX19LHsidmFsdWUiOjAuMDAxNDIsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAzYjU4MDFhM2YzOTg2ZGE4NjhhZWYzOTdmYWEyMzQ4MWFjNTM3MDNjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0M2I1ODAxYTNmMzk4NmRhODY4YWVmMzk3ZmFhMjM0ODFhYzUzNzAzYzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWE0c3Fkcjd3dnhtMnJnNG1lZTA3NHp4anE2YzVtczhzcDVrbDN6OWgiXX19LHsidmFsdWUiOjAuMDQ2ODA5NDYsIm4iOjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2Zjc1MmRlZGY5OWY3YmE0Nzg4M2EyNTMwYzZkMjkxZGI4Mzg4NzRhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NmY3NTJkZWRmOTlmN2JhNDc4ODNhMjUzMGM2ZDI5MWRiODM4ODc0YTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGhoMnQwZGx4MGhoZnJjc3czOXhycmQ5eXdtc3d5OGZnbDV6dGw0emYiXX19XSwiZmVlIjoyLjU0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMWZiZTZkNDE5YWU4MDRkMWI4YWI2MTAzMmQ4M2Y4YjhkYzNlM2Y0YjM0MzM5MGM0MDZhNGIwYjVmZDEzY2EzMWQwMjAwMDAwMDY0NDEzMWM2NzYxNWVjZDM0MTMyMjhkNWI1ZDhmOTBjNzdmZTU0YmY3MmMzMWI4ZjIyMzhmNThjMDY5ZmFlZTAzYjZkZDI3NmJmMjJhMmRkYzk3YjM1MGNkNjVjZmRmYmI0NzA0NGUzMGVhNjljMjczZTZlNjI5MWQ5YzYyNjIxNjFjZTQxMjEwM2MyODBhOTMzNmQ1YWMxYmJlOGQxNmE0MjEwOTRhNzY1YTJjNGQyMWExZGMyMDBmZjMzOTJlMDUyZjU1ODYwZWQwMDAwMDAwMDAzNzAwYjAyMDAwMDAwMDAwMDE5NzZhOTE0YmQyYTk0NWRkODRhODViN2Q0MjA3Y2I5NjdmODAxZWY2ODM3Y2M1Njg4YWNiMDJhMDIwMDAwMDAwMDAwMTk3NmE5MTQzYjU4MDFhM2YzOTg2ZGE4NjhhZWYzOTdmYWEyMzQ4MWFjNTM3MDNjODhhY2YyNmM0NzAwMDAwMDAwMDAxOTc2YTkxNDZmNzUyZGVkZjk5ZjdiYTQ3ODgzYTI1MzBjNmQyOTFkYjgzODg3NGE4OGFjMDAwMDAwMDAifSx7InR4aWQiOiIzM2FmNGZiODYyNDA2YjFlZGEwMjEwNjhkYjMyZjViNWQ0YWIwYzVjZTIyMTA3MmU2OTgyNDI2MWNhYzY2NmI5IiwiaGFzaCI6IjMzYWY0ZmI4NjI0MDZiMWVkYTAyMTA2OGRiMzJmNWI1ZDRhYjBjNWNlMjIxMDcyZTY5ODI0MjYxY2FjNjY2YjkiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyMjYsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI0ZTg3Y2UzYjVlODA5MjFjMzc0OTBjNGYxZGE1Nzk2OGZlNTJiYzE2MTdmZjljZTMxNzQxY2EzN2ZkMDg4NTA4Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBlYjA0NjU1MGVhZmViNTY5MjRjNTI4ZTUzYWY1ZWY2ODBmZjMyMWI0NDYzZjgyYWRiNDMwYWJjNWQ0MmNkNmZkMDIyMDNiMjViYWM4ODMxNjZlNGIyZTQwMWJlZGQzZmYyYTk5NTFlNzZjODBmYWJmNmNlMDFkMDY0NGQ3ODVkMTAzYzhbQUxMfEZPUktJRF0gMDI5Zjg2NDJlOGRmYzJmNGVkNGFjYzk5OWNkODY2ZGE3N2JlN2I1ZTE0ZGUyMDI0NjhlMzFiZTBjMGRmYjNhZThiIiwiaGV4IjoiNDgzMDQ1MDIyMTAwZWIwNDY1NTBlYWZlYjU2OTI0YzUyOGU1M2FmNWVmNjgwZmYzMjFiNDQ2M2Y4MmFkYjQzMGFiYzVkNDJjZDZmZDAyMjAzYjI1YmFjODgzMTY2ZTRiMmU0MDFiZWRkM2ZmMmE5OTUxZTc2YzgwZmFiZjZjZTAxZDA2NDRkNzg1ZDEwM2M4NDEyMTAyOWY4NjQyZThkZmMyZjRlZDRhY2M5OTljZDg2NmRhNzdiZTdiNWUxNGRlMjAyNDY4ZTMxYmUwYzBkZmIzYWU4YiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4yNTc5MjU5MSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDA4OTc1N2QwYzYyMjI2N2U1Y2M5NmRkMGQ4Nzc0ZTdkODQ1Mzk0YjggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwODk3NTdkMGM2MjIyNjdlNWNjOTZkZDBkODc3NGU3ZDg0NTM5NGI4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxeWZ3NDdzY2MzenZsanVlOWthcGtyaGZlN2NnNXU1aHEyaHc1YXQ5diJdfX0seyJ2YWx1ZSI6MC4wMDMzMDI0MywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDcxODNjZmZmYTEwMmQ2NDFmYjE1MmRlMzI0OTZmMmI1MGE0M2ExNGYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3MTgzY2ZmZmExMDJkNjQxZmIxNTJkZTMyNDk2ZjJiNTBhNDNhMTRmODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwY2M4bmxsNXlwZHZzMG16NWs3eGZ5azcyNnM1c2FwZnVyOTd6Y2R1ayJdfX1dLCJmZWUiOjIuMjZlLTA2LCJoZXgiOiIwMTAwMDAwMDAxMDg4NTA4ZmQzN2NhNDExN2UzOWNmZjE3MTZiYzUyZmU2ODc5YTUxZDRmMGM0OTM3MWM5MjgwNWUzYmNlODc0ZTAwMDAwMDAwNmI0ODMwNDUwMjIxMDBlYjA0NjU1MGVhZmViNTY5MjRjNTI4ZTUzYWY1ZWY2ODBmZjMyMWI0NDYzZjgyYWRiNDMwYWJjNWQ0MmNkNmZkMDIyMDNiMjViYWM4ODMxNjZlNGIyZTQwMWJlZGQzZmYyYTk5NTFlNzZjODBmYWJmNmNlMDFkMDY0NGQ3ODVkMTAzYzg0MTIxMDI5Zjg2NDJlOGRmYzJmNGVkNGFjYzk5OWNkODY2ZGE3N2JlN2I1ZTE0ZGUyMDI0NjhlMzFiZTBjMGRmYjNhZThiZmZmZmZmZmYwMjRmOTA4OTAxMDAwMDAwMDAxOTc2YTkxNDA4OTc1N2QwYzYyMjI2N2U1Y2M5NmRkMGQ4Nzc0ZTdkODQ1Mzk0Yjg4OGFjMDMwYTA1MDAwMDAwMDAwMDE5NzZhOTE0NzE4M2NmZmZhMTAyZDY0MWZiMTUyZGUzMjQ5NmYyYjUwYTQzYTE0Zjg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjNhZmRjZDUxY2JmYTQyNjkxZGQxMDNhMjkxMDJlZDI2YWU5NTFkOTk0Yjc0NDQ4MGU1YjIzYjAyYzEzODZiYmQiLCJoYXNoIjoiM2FmZGNkNTFjYmZhNDI2OTFkZDEwM2EyOTEwMmVkMjZhZTk1MWQ5OTRiNzQ0NDgwZTViMjNiMDJjMTM4NmJiZCIsInZlcnNpb24iOjIsInNpemUiOjE2NCwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6Ijg5YzdmZmQxY2M5Mjg4NzY0ZGU3MzhlMWE2NmJkYTQyZWJhMDFjZjU2ZjMwMDA1MTg5OTdkNTE2YWFhNjBiMmEiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiNTFiMjc1YzM1MTlkYzQ1MTlkMDI0MDAyMDJhNDAwYzBjNjdiNTI3OTkzYTA2M2MwYzdjMGNkODhjMGM2MDIyODExOTUwNGM2ODU3NDRmOTY2ZWEwNjM3ODc3NjhjMGM2YzBjYzk0NmVhMjY5NmQ2N2MwY2MwMDlkYzBjZDAxNmE4ODY4NzU1MSIsImhleCI6IjQ0NTFiMjc1YzM1MTlkYzQ1MTlkMDI0MDAyMDJhNDAwYzBjNjdiNTI3OTkzYTA2M2MwYzdjMGNkODhjMGM2MDIyODExOTUwNGM2ODU3NDRmOTY2ZWEwNjM3ODc3NjhjMGM2YzBjYzk0NmVhMjY5NmQ2N2MwY2MwMDlkYzBjZDAxNmE4ODY4NzU1MSJ9LCJzZXF1ZW5jZSI6MX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMDU2NjUwMiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0YyBPUF9FUVVBTCIsImhleCI6ImFhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3N1OGY0ZnRuc3VndW56eTh3cnVodWF5dnRwejltdDg4ZXV3dnd0cDVqdnY1OHduZDk1YzB0ZDN3cGRwIl19fV0sImZlZSI6MS42NGUtMDYsImhleCI6IjAyMDAwMDAwMDEyYTBiYTZhYTE2ZDU5Nzg5NTEwMDMwNmZmNTFjYTBlYjQyZGE2YmE2ZTEzOGU3NGQ3Njg4OTJjY2QxZmZjNzg5MDAwMDAwMDA0NTQ0NTFiMjc1YzM1MTlkYzQ1MTlkMDI0MDAyMDJhNDAwYzBjNjdiNTI3OTkzYTA2M2MwYzdjMGNkODhjMGM2MDIyODExOTUwNGM2ODU3NDRmOTY2ZWEwNjM3ODc3NjhjMGM2YzBjYzk0NmVhMjY5NmQ2N2MwY2MwMDlkYzBjZDAxNmE4ODY4NzU1MTAxMDAwMDAwMDFlNmE0MDgwMDAwMDAwMDAwMjNhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3MDAwMDAwMDAifSx7InR4aWQiOiIzZjU3MzU4Mjg0OTEyYTJmMmVhODQzNTRlOTNhNDM0MTYxMDIxY2JmOTk4YjNjZmE2M2I0NjE4M2QyYjI1Zjg3IiwiaGFzaCI6IjNmNTczNTgyODQ5MTJhMmYyZWE4NDM1NGU5M2E0MzQxNjEwMjFjYmY5OThiM2NmYTYzYjQ2MTgzZDJiMjVmODciLCJ2ZXJzaW9uIjoyLCJzaXplIjoxNjQsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI2Yzc5OTM3NGYxNmEzYTRmYmVlNTYwZWQxOWMzZWZiZmYzZDBhYzg1MTg5ZWY4ZGIxZTU2ZDg3Yzk1OGNiMzlmIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEiLCJoZXgiOiI0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEifSwic2VxdWVuY2UiOjF9XSwidm91dCI6W3sidmFsdWUiOjAuMDA1Njc4MTQsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dLCJmZWUiOjEuNjRlLTA2LCJoZXgiOiIwMjAwMDAwMDAxOWZiMzhjOTU3Y2Q4NTYxZWRiZjg5ZTE4ODVhY2QwZjNiZmVmYzMxOWVkNjBlNWJlNGYzYTZhZjE3NDkzNzk2YzAwMDAwMDAwNDU0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEwMTAwMDAwMDAxMDZhYTA4MDAwMDAwMDAwMDIzYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NzAwMDAwMDAwIn0seyJ0eGlkIjoiNDA5N2IwMTAzYTliYjdhOGRiYWE2MjMzMzkxODBlODRlM2I1YzRkMzcyOTIwMzNiMTA2MTYyMjhmYjllYTg2YSIsImhhc2giOiI0MDk3YjAxMDNhOWJiN2E4ZGJhYTYyMzMzOTE4MGU4NGUzYjVjNGQzNzI5MjAzM2IxMDYxNjIyOGZiOWVhODZhIiwidmVyc2lvbiI6MSwic2l6ZSI6MzcyLCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiZmY1MWNiY2E1OGNkZDJlMmQyYWRiZGJhYTc5MmNmNGMwODhkOWZiZWMxNzIyYWNlNjE5MmU3YWQ0YzdiMWMyYiIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDc1YjhmOTU2ZWVjMWQxYzQxMDI0NTY0YTUwODUxNGQzNGQ5MDhjMTQ5MWRlOWRkMmYyYzlkNGJhOGQyYzQ5MDkwMjIwNzFkNDQ5ZGRhYTRiYjQ4ODBhMmVjMTg3MmJmZGI5OWE3OGRhOTZhNmJiYzNjMTc4YTdlMmU3Y2E5ODU3N2I2ZltBTEx8Rk9SS0lEXSAwMzhhYzYwOGMzYzRkMGI3NWM5ZGFlNzg3OWNlZjEzMWYzMTU3OTRhMGY5ODdmYjhkNDIwNzY1MDFmMGY4ZjVjODciLCJoZXgiOiI0NzMwNDQwMjIwNzViOGY5NTZlZWMxZDFjNDEwMjQ1NjRhNTA4NTE0ZDM0ZDkwOGMxNDkxZGU5ZGQyZjJjOWQ0YmE4ZDJjNDkwOTAyMjA3MWQ0NDlkZGFhNGJiNDg4MGEyZWMxODcyYmZkYjk5YTc4ZGE5NmE2YmJjM2MxNzhhN2UyZTdjYTk4NTc3YjZmNDEyMTAzOGFjNjA4YzNjNGQwYjc1YzlkYWU3ODc5Y2VmMTMxZjMxNTc5NGEwZjk4N2ZiOGQ0MjA3NjUwMWYwZjhmNWM4NyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZGNhMWJjMzM2YWFmNDdkNTQ1ZWU3MDQyOGJhN2YxYmQyOTZmNzc0YjU1ZDgwMmI5NjY1YWRmZWMyYjc3N2FhNSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDc3ZWYwYTA3MDljN2ExMDQzYTg2MzRiNjg5YjlkMzVlYmY5NzQzZWMzMjU1ZmU3NmFjZjMwMjk4YjFiZGJlNGYwMjIwN2NhYjFhZWJjMDlmMTBmMTlkMGEyNjkzZTFiYTkzNDRmOGZiZjNjM2E3NGVhNDUwNjFmODE4NDQ4NzE0NmU4MFtBTEx8Rk9SS0lEXSAwMzhhYzYwOGMzYzRkMGI3NWM5ZGFlNzg3OWNlZjEzMWYzMTU3OTRhMGY5ODdmYjhkNDIwNzY1MDFmMGY4ZjVjODciLCJoZXgiOiI0NzMwNDQwMjIwNzdlZjBhMDcwOWM3YTEwNDNhODYzNGI2ODliOWQzNWViZjk3NDNlYzMyNTVmZTc2YWNmMzAyOThiMWJkYmU0ZjAyMjA3Y2FiMWFlYmMwOWYxMGYxOWQwYTI2OTNlMWJhOTM0NGY4ZmJmM2MzYTc0ZWE0NTA2MWY4MTg0NDg3MTQ2ZTgwNDEyMTAzOGFjNjA4YzNjNGQwYjc1YzlkYWU3ODc5Y2VmMTMxZjMxNTc5NGEwZjk4N2ZiOGQ0MjA3NjUwMWYwZjhmNWM4NyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMDAzNDM1OSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDJhNzFjODhiMmE1YzJkMmYyMmRhNmExYTAyNTNmNzk0NzViNWY1YzUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQyYTcxYzg4YjJhNWMyZDJmMjJkYTZhMWEwMjUzZjc5NDc1YjVmNWM1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxNDhyanl0OWZ3ejZ0ZXptZjRwNXFqbjc3Mjh0ZDA0YzV0M3h2czMzaCJdfX0seyJ2YWx1ZSI6MC4wMDc3Nzg5NiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDU1Yjk1NTlmNDZhOTI5ZmI3NWJjZTdlZGM5Y2I0YThkYTQ0YjlkYzQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1NWI5NTU5ZjQ2YTkyOWZiNzViY2U3ZWRjOWNiNGE4ZGE0NGI5ZGM0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwMm1qNHZsZzY1am43bTRobm43bWp3dGYyeDZnanVhY3NydGhtNXNhcCJdfX1dLCJmZWUiOjhlLTA2LCJoZXgiOiIwMTAwMDAwMDAyMmIxYzdiNGNhZGU3OTI2MWNlMmE3MmMxYmU5ZjhkMDg0Y2NmOTJhN2JhYmRhZGQyZTJkMmNkNThjYWNiNTFmZjAxMDAwMDAwNmE0NzMwNDQwMjIwNzViOGY5NTZlZWMxZDFjNDEwMjQ1NjRhNTA4NTE0ZDM0ZDkwOGMxNDkxZGU5ZGQyZjJjOWQ0YmE4ZDJjNDkwOTAyMjA3MWQ0NDlkZGFhNGJiNDg4MGEyZWMxODcyYmZkYjk5YTc4ZGE5NmE2YmJjM2MxNzhhN2UyZTdjYTk4NTc3YjZmNDEyMTAzOGFjNjA4YzNjNGQwYjc1YzlkYWU3ODc5Y2VmMTMxZjMxNTc5NGEwZjk4N2ZiOGQ0MjA3NjUwMWYwZjhmNWM4N2ZmZmZmZmZmYTU3YTc3MmJlY2RmNWE2NmI5MDJkODU1NGI3NzZmMjliZGYxYTc4YjQyNzBlZTQ1ZDU0N2FmNmEzM2JjYTFkYzAxMDAwMDAwNmE0NzMwNDQwMjIwNzdlZjBhMDcwOWM3YTEwNDNhODYzNGI2ODliOWQzNWViZjk3NDNlYzMyNTVmZTc2YWNmMzAyOThiMWJkYmU0ZjAyMjA3Y2FiMWFlYmMwOWYxMGYxOWQwYTI2OTNlMWJhOTM0NGY4ZmJmM2MzYTc0ZWE0NTA2MWY4MTg0NDg3MTQ2ZTgwNDEyMTAzOGFjNjA4YzNjNGQwYjc1YzlkYWU3ODc5Y2VmMTMxZjMxNTc5NGEwZjk4N2ZiOGQ0MjA3NjUwMWYwZjhmNWM4N2ZmZmZmZmZmMDIzNzg2MDAwMDAwMDAwMDAwMTk3NmE5MTQyYTcxYzg4YjJhNWMyZDJmMjJkYTZhMWEwMjUzZjc5NDc1YjVmNWM1ODhhY2E4ZGUwYjAwMDAwMDAwMDAxOTc2YTkxNDU1Yjk1NTlmNDZhOTI5ZmI3NWJjZTdlZGM5Y2I0YThkYTQ0YjlkYzQ4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI0MTc0NzM5YWU1YzJiNTRkZTI0MDAzZDE2NjE0ZDMwNGEyOWJmYzAzOWU3NTg4YjBlMzY4ZjcwYWRmY2FlZDdkIiwiaGFzaCI6IjQxNzQ3MzlhZTVjMmI1NGRlMjQwMDNkMTY2MTRkMzA0YTI5YmZjMDM5ZTc1ODhiMGUzNjhmNzBhZGZjYWVkN2QiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyMjYsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI5MzM0OTUwNGI0YzRmZDhhZjc2NmMzOGNlZDc4ZmE5ZjhlYTI5MzRmNzk0N2QxZDEyM2UzNjY0MTU2YjhjNzQ0Iiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBmYzFiNTVjMzU4MTc2MDk2ZGJlMDNhMWY0YzBiZGMxMWFlMGU3MTYyNGJkYzZkNDdkMTFkZDdhMWNmODY4MzI1MDIyMDA1NjgyYTdiYmY1ZjE2ZmU1Y2Q5NzdlYWMwOTRhNTRjYjA4ZTNjYzc5Y2Y0YTI2YjllOWUzOWIwNWFjMDdmNmRbQUxMfEZPUktJRF0gMDIzOGM2ZTY3ZDJhMTE3NDQ4MjViODJjMTZjYTYxMGY4MjkyNDdlZWE2ZWFhODhmN2E5NjdlZThmN2MzNmM1YzZkIiwiaGV4IjoiNDgzMDQ1MDIyMTAwZmMxYjU1YzM1ODE3NjA5NmRiZTAzYTFmNGMwYmRjMTFhZTBlNzE2MjRiZGM2ZDQ3ZDExZGQ3YTFjZjg2ODMyNTAyMjAwNTY4MmE3YmJmNWYxNmZlNWNkOTc3ZWFjMDk0YTU0Y2IwOGUzY2M3OWNmNGEyNmI5ZTllMzliMDVhYzA3ZjZkNDEyMTAyMzhjNmU2N2QyYTExNzQ0ODI1YjgyYzE2Y2E2MTBmODI5MjQ3ZWVhNmVhYTg4ZjdhOTY3ZWU4ZjdjMzZjNWM2ZCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MS4wLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTAyODhmNzMyNmI1NGQyMmZlNjk1OTI1YTc1NTY5NGFmNmY4MGY1YyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDEwMjg4ZjczMjZiNTRkMjJmZTY5NTkyNWE3NTU2OTRhZjZmODBmNWM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFnejNybW55NjY1NmdoN2Q5dmp0ZjY0ZDk5MGQ3cTB0c25yeXc0eXc1Il19fSx7InZhbHVlIjowLjA3Mjc3MjY4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODUyMzY4NmI0YzVmM2U0YTg0ZGY2YjQzMmVmMDk1ZTU0ODVlNjhhMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg1MjM2ODZiNGM1ZjNlNGE4NGRmNmI0MzJlZjA5NWU1NDg1ZTY4YTA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp6ang2cnRmMzBudWo1eW1hNDV4dGhzamhqNXNobmc1cTY5Nm4ycHIzIl19fV0sImZlZSI6NC41MmUtMDYsImhleCI6IjAxMDAwMDAwMDE0NGM3Yjg1NjQxNjZlMzIzZDFkMTQ3Nzk0ZjkzYTI4ZTlmZmE3OGVkOGNjMzY2Zjc4YWZkYzRiNDA0OTUzNDkzMDEwMDAwMDA2YjQ4MzA0NTAyMjEwMGZjMWI1NWMzNTgxNzYwOTZkYmUwM2ExZjRjMGJkYzExYWUwZTcxNjI0YmRjNmQ0N2QxMWRkN2ExY2Y4NjgzMjUwMjIwMDU2ODJhN2JiZjVmMTZmZTVjZDk3N2VhYzA5NGE1NGNiMDhlM2NjNzljZjRhMjZiOWU5ZTM5YjA1YWMwN2Y2ZDQxMjEwMjM4YzZlNjdkMmExMTc0NDgyNWI4MmMxNmNhNjEwZjgyOTI0N2VlYTZlYWE4OGY3YTk2N2VlOGY3YzM2YzVjNmRmZmZmZmZmZjAyMDBlMWY1MDUwMDAwMDAwMDE5NzZhOTE0MTAyODhmNzMyNmI1NGQyMmZlNjk1OTI1YTc1NTY5NGFmNmY4MGY1Yzg4YWNkNDBhNmYwMDAwMDAwMDAwMTk3NmE5MTQ4NTIzNjg2YjRjNWYzZTRhODRkZjZiNDMyZWYwOTVlNTQ4NWU2OGEwODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiNDU2ZGZiOGI4ZjFjZTBhYWIzOGUzZmNmNThjZDVmNTEyY2UwZmQ4NjU0YzNiY2JlOTk4NTE3MGRmYmM3YWVmYSIsImhhc2giOiI0NTZkZmI4YjhmMWNlMGFhYjM4ZTNmY2Y1OGNkNWY1MTJjZTBmZDg2NTRjM2JjYmU5OTg1MTcwZGZiYzdhZWZhIiwidmVyc2lvbiI6Miwic2l6ZSI6MjI1LCJsb2NrdGltZSI6OTQwODgzLCJ2aW4iOlt7InR4aWQiOiIyZDI3YzlmNTZiNTVjNjNjMjRjYjlmZTM5MDYxZGNiMDBhZDA0YjNmYzM3ZWU1MjM2MjVmNWQ1MGQ1MGNmM2U3Iiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwNjI5NGRkNDFmNmE5MjdhZmE0OWVjNjY3MDg3NGYwYTU1Zjc4ODFlOGJiMDExOWFjZGNlY2FiZjQ4OWM4NzZmNTAyMjAxMTRmNjc5OGFmMmExZjg4MzEwNmVlMTZlNDhiMDg1NWNjOTE0NGM4MzFhYmViMWVmNjQ4YmI0MWQ4YWIyNGE5W0FMTHxGT1JLSURdIDAzYjk4OGJjZWM1OWM5ZGJlNWI2MjhiOTJjZDUwY2RhNThhZGY2NDVhYjk4NTQ0ZjIwM2E5OTNiM2RiOTQzNjZhMyIsImhleCI6IjQ3MzA0NDAyMjA2Mjk0ZGQ0MWY2YTkyN2FmYTQ5ZWM2NjcwODc0ZjBhNTVmNzg4MWU4YmIwMTE5YWNkY2VjYWJmNDg5Yzg3NmY1MDIyMDExNGY2Nzk4YWYyYTFmODgzMTA2ZWUxNmU0OGIwODU1Y2M5MTQ0YzgzMWFiZWIxZWY2NDhiYjQxZDhhYjI0YTk0MTIxMDNiOTg4YmNlYzU5YzlkYmU1YjYyOGI5MmNkNTBjZGE1OGFkZjY0NWFiOTg1NDRmMjAzYTk5M2IzZGI5NDM2NmEzIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk0fV0sInZvdXQiOlt7InZhbHVlIjowLjAzOTcwNjg4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDg2N2QxNTYwYmUzNmIzZjI1ZGNkYmRiYWNkNGVjZDA1MjM4YWE4NyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDA4NjdkMTU2MGJlMzZiM2YyNWRjZGJkYmFjZDRlY2QwNTIzOGFhODc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF5eDA1MmtwMDNrazBlOW1uZGFodHg1YW5nOXl3OTJzdXh6OXJwNWRzIl19fSx7InZhbHVlIjowLjM3Mzc0NjkyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTgzMzY0ZjYzMDEzYjU2YzNmM2ZjMDdjYmFjYzhiZDA2MjU4OTc0NSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDk4MzM2NGY2MzAxM2I1NmMzZjNmYzA3Y2JhY2M4YmQwNjI1ODk3NDU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp2cnhlOGt4cWZtMm1wbDhscThld2t2MzBneHlreWhnNW5qOXM3YXJxIl19fV0sImZlZSI6Mi4yNWUtMDYsImhleCI6IjAyMDAwMDAwMDFlN2YzMGNkNTUwNWQ1ZjYyMjNlNTdlYzMzZjRiZDAwYWIwZGM2MTkwZTM5ZmNiMjQzY2M2NTU2YmY1YzkyNzJkMDEwMDAwMDA2YTQ3MzA0NDAyMjA2Mjk0ZGQ0MWY2YTkyN2FmYTQ5ZWM2NjcwODc0ZjBhNTVmNzg4MWU4YmIwMTE5YWNkY2VjYWJmNDg5Yzg3NmY1MDIyMDExNGY2Nzk4YWYyYTFmODgzMTA2ZWUxNmU0OGIwODU1Y2M5MTQ0YzgzMWFiZWIxZWY2NDhiYjQxZDhhYjI0YTk0MTIxMDNiOTg4YmNlYzU5YzlkYmU1YjYyOGI5MmNkNTBjZGE1OGFkZjY0NWFiOTg1NDRmMjAzYTk5M2IzZGI5NDM2NmEzZmVmZmZmZmYwMjgwOTYzYzAwMDAwMDAwMDAxOTc2YTkxNDA4NjdkMTU2MGJlMzZiM2YyNWRjZGJkYmFjZDRlY2QwNTIzOGFhODc4OGFjZTQ0YTNhMDIwMDAwMDAwMDE5NzZhOTE0OTgzMzY0ZjYzMDEzYjU2YzNmM2ZjMDdjYmFjYzhiZDA2MjU4OTc0NTg4YWM1MzViMGUwMCJ9LHsidHhpZCI6IjRiNDhiZjI3ZWU2MTkyZGI0NGJkYWE5MjhiOTIxMTQ0MTMyN2JhZWMwNDBjMGJhM2QzNWNlMjI3YjI0YjZhMDUiLCJoYXNoIjoiNGI0OGJmMjdlZTYxOTJkYjQ0YmRhYTkyOGI5MjExNDQxMzI3YmFlYzA0MGMwYmEzZDM1Y2UyMjdiMjRiNmEwNSIsInZlcnNpb24iOjIsInNpemUiOjYzOCwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImM1NDFmZWM3ZjJjZmJmYTY5NWQ2OTJjODE0OWMyODI1MTliMmQ2ODkwYTY0NWJhMTExNzIxMDI4NjgyM2FlZjYiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiODEzZDcwZGUyNGFiZDJkMjk4ZTJjMGY5ZjIwNDE5ZTcwMTkxZTQ0NGQ1MWQ3ZjQ1ZjVjMGVhOTBmOTYwZGNmOTQ2MGJkZDllMGQ4NzZlYTA5MmViMzk4MDE5YTc5Y2NmYWRmODU4MjVjZTU0OGY2Mzk3ZTM0MTMyZTgxNDI3NDAgYzk4NmE4NjkxNmNjMTcwMGZjY2IxNzAwZDBiODAwMDAgMWY1MjZmOTI5M2ZjOTdkZjRjYTc0MjNhOThkMzczNjAzZjg0ODVmNzBkNjA1ZDhmMmViMjVlNjU2Y2JmNzY2ZDE5Mzg4OWIxZGIzODc2MjgxMDI0YjQxYzFjZWYzNThlMzYwMjg5MzFjMDkzYzc2ODQwNjZkNzk1ZDJhMDBmMWUgMDU4N2E4NjkxN2NjMTcwMGZkY2IxNzAwNjdiODAwMDAgMSAwNDAxODdhODY5MDQzZDBhYWU2ODAzMjZlNzA0MDM3NjhiMDAwMzAwM2U0OTAzYzAyNzA5MDU4MDI2M2FlMzJjMjEwMmQwOWRiMDhhZjFmZjRlODQ1MzkxOWNjODY2YTRiZTQyN2Q3YmZlMThmMmMwNWU1NDQ0YzE5NmZjZjZmZDI4MTgxOTc2YTkxNDEzODEzODU3MWI4Y2NkMzRlZjgzOWFkYzZiNzNhY2E4ZTc2NmI1ZTg4OGFjMTk3NmE5MTQxNDA5NjYwMGI5M2M3YjUwYzI5ZmUyNTU4MmM0OTA2Zjk0NmU3NGJmODhhYzAwMjEwM2ZkZjQ5ZTcyNDkwYmRhNGU5ZjNmMTYwOTQxNmM4ODZkNDRiYWRiN2FkNGUxNDc5NjQwYjYxOWI5ZWI4YTNmYWIyMTAzNTUxZGQyYTZlNjAxNDNlYzMxMjY3ZjkxMGVjMzRhYzY0YzJhYjZjOWJiZGYwOWY3ZDg2ODVmNTAwZDhlZGE1MDVkNzkwMDljNjM3YjY5NWQ3YTdjYWQ1YzdhN2NhZDZkNmQ2ZDZkNmQ3NTUxNjc1ZDdhNTE5ZGMzNTE5ZDYwN2E2MDc5NTc3OWJiNWU3YTVlNzk1NzdhYmI1ZDc5NTg3Zjc3NTQ3Zjc1ODE3NjAwYTA2OTVkNzk1ODdmNzc1NDdmNzU4MThjOWQ1ZDdhNTQ3Zjc1ODE1Yzc5OWY2OTVjNzk1YzdmNzc4MTc2MDBhMDY5NWE3OWEzNTk3OWE0NWQ3YTU0N2Y3NTgxNzY1ZDdhYTI2OTVjN2FhMjc4NWI3YThiNWM3YWE1OTE5YjY5MDIzNDA1NzY1ODdhNTM3YTk2NTg3YTk0YTQ3YzU3N2E1Mjc5OTRhNGM0NTI5ZDAwY2M3YjlkMDBjZDU1N2E4ODUxY2M5ZDUxY2Q1NDdhODc3Nzc3Nzc2OCIsImhleCI6IjQwODEzZDcwZGUyNGFiZDJkMjk4ZTJjMGY5ZjIwNDE5ZTcwMTkxZTQ0NGQ1MWQ3ZjQ1ZjVjMGVhOTBmOTYwZGNmOTQ2MGJkZDllMGQ4NzZlYTA5MmViMzk4MDE5YTc5Y2NmYWRmODU4MjVjZTU0OGY2Mzk3ZTM0MTMyZTgxNDI3NDAxMGM5ODZhODY5MTZjYzE3MDBmY2NiMTcwMGQwYjgwMDAwNDAxZjUyNmY5MjkzZmM5N2RmNGNhNzQyM2E5OGQzNzM2MDNmODQ4NWY3MGQ2MDVkOGYyZWIyNWU2NTZjYmY3NjZkMTkzODg5YjFkYjM4NzYyODEwMjRiNDFjMWNlZjM1OGUzNjAyODkzMWMwOTNjNzY4NDA2NmQ3OTVkMmEwMGYxZTEwMDU4N2E4NjkxN2NjMTcwMGZkY2IxNzAwNjdiODAwMDA1MTRkNWQwMTA0MDE4N2E4NjkwNDNkMGFhZTY4MDMyNmU3MDQwMzc2OGIwMDAzMDAzZTQ5MDNjMDI3MDkwNTgwMjYzYWUzMmMyMTAyZDA5ZGIwOGFmMWZmNGU4NDUzOTE5Y2M4NjZhNGJlNDI3ZDdiZmUxOGYyYzA1ZTU0NDRjMTk2ZmNmNmZkMjgxODE5NzZhOTE0MTM4MTM4NTcxYjhjY2QzNGVmODM5YWRjNmI3M2FjYThlNzY2YjVlODg4YWMxOTc2YTkxNDE0MDk2NjAwYjkzYzdiNTBjMjlmZTI1NTgyYzQ5MDZmOTQ2ZTc0YmY4OGFjMDAyMTAzZmRmNDllNzI0OTBiZGE0ZTlmM2YxNjA5NDE2Yzg4NmQ0NGJhZGI3YWQ0ZTE0Nzk2NDBiNjE5YjllYjhhM2ZhYjIxMDM1NTFkZDJhNmU2MDE0M2VjMzEyNjdmOTEwZWMzNGFjNjRjMmFiNmM5YmJkZjA5ZjdkODY4NWY1MDBkOGVkYTUwNWQ3OTAwOWM2MzdiNjk1ZDdhN2NhZDVjN2E3Y2FkNmQ2ZDZkNmQ2ZDc1NTE2NzVkN2E1MTlkYzM1MTlkNjA3YTYwNzk1Nzc5YmI1ZTdhNWU3OTU3N2FiYjVkNzk1ODdmNzc1NDdmNzU4MTc2MDBhMDY5NWQ3OTU4N2Y3NzU0N2Y3NTgxOGM5ZDVkN2E1NDdmNzU4MTVjNzk5ZjY5NWM3OTVjN2Y3NzgxNzYwMGEwNjk1YTc5YTM1OTc5YTQ1ZDdhNTQ3Zjc1ODE3NjVkN2FhMjY5NWM3YWEyNzg1YjdhOGI1YzdhYTU5MTliNjkwMjM0MDU3NjU4N2E1MzdhOTY1ODdhOTRhNDdjNTc3YTUyNzk5NGE0YzQ1MjlkMDBjYzdiOWQwMGNkNTU3YTg4NTFjYzlkNTFjZDU0N2E4Nzc3Nzc3NzY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk0fV0sInZvdXQiOlt7InZhbHVlIjowLjAzNDgzOTQ1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTQwOTY2MDBiOTNjN2I1MGMyOWZlMjU1ODJjNDkwNmY5NDZlNzRiZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDE0MDk2NjAwYjkzYzdiNTBjMjlmZTI1NTgyYzQ5MDZmOTQ2ZTc0YmY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXEycWplc3FoeTc4azV4em5sMzl0cWt5anBoZWdtbjVodW00c2NrdmhwIl19fSx7InZhbHVlIjowLjAxMzE2MDU1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTM4MTM4NTcxYjhjY2QzNGVmODM5YWRjNmI3M2FjYThlNzY2YjVlOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDEzODEzODU3MWI4Y2NkMzRlZjgzOWFkYzZiNzNhY2E4ZTc2NmI1ZTg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFmY3p3emhyd3h2NmQ4MHN3ZGRjNm1uNGo1d3dlNDRhcXY0cHR4NDV3Il19fV0sImZlZSI6MS45N2UtMDUsImhleCI6IjAyMDAwMDAwMDFmNmFlMjM2ODI4MTA3MjExYTE1YjY0MGE4OWQ2YjIxOTI1Mjg5YzE0Yzg5MmQ2OTVhNmJmY2ZmMmM3ZmU0MWM1MDAwMDAwMDBmZDA1MDI0MDgxM2Q3MGRlMjRhYmQyZDI5OGUyYzBmOWYyMDQxOWU3MDE5MWU0NDRkNTFkN2Y0NWY1YzBlYTkwZjk2MGRjZjk0NjBiZGQ5ZTBkODc2ZWEwOTJlYjM5ODAxOWE3OWNjZmFkZjg1ODI1Y2U1NDhmNjM5N2UzNDEzMmU4MTQyNzQwMTBjOTg2YTg2OTE2Y2MxNzAwZmNjYjE3MDBkMGI4MDAwMDQwMWY1MjZmOTI5M2ZjOTdkZjRjYTc0MjNhOThkMzczNjAzZjg0ODVmNzBkNjA1ZDhmMmViMjVlNjU2Y2JmNzY2ZDE5Mzg4OWIxZGIzODc2MjgxMDI0YjQxYzFjZWYzNThlMzYwMjg5MzFjMDkzYzc2ODQwNjZkNzk1ZDJhMDBmMWUxMDA1ODdhODY5MTdjYzE3MDBmZGNiMTcwMDY3YjgwMDAwNTE0ZDVkMDEwNDAxODdhODY5MDQzZDBhYWU2ODAzMjZlNzA0MDM3NjhiMDAwMzAwM2U0OTAzYzAyNzA5MDU4MDI2M2FlMzJjMjEwMmQwOWRiMDhhZjFmZjRlODQ1MzkxOWNjODY2YTRiZTQyN2Q3YmZlMThmMmMwNWU1NDQ0YzE5NmZjZjZmZDI4MTgxOTc2YTkxNDEzODEzODU3MWI4Y2NkMzRlZjgzOWFkYzZiNzNhY2E4ZTc2NmI1ZTg4OGFjMTk3NmE5MTQxNDA5NjYwMGI5M2M3YjUwYzI5ZmUyNTU4MmM0OTA2Zjk0NmU3NGJmODhhYzAwMjEwM2ZkZjQ5ZTcyNDkwYmRhNGU5ZjNmMTYwOTQxNmM4ODZkNDRiYWRiN2FkNGUxNDc5NjQwYjYxOWI5ZWI4YTNmYWIyMTAzNTUxZGQyYTZlNjAxNDNlYzMxMjY3ZjkxMGVjMzRhYzY0YzJhYjZjOWJiZGYwOWY3ZDg2ODVmNTAwZDhlZGE1MDVkNzkwMDljNjM3YjY5NWQ3YTdjYWQ1YzdhN2NhZDZkNmQ2ZDZkNmQ3NTUxNjc1ZDdhNTE5ZGMzNTE5ZDYwN2E2MDc5NTc3OWJiNWU3YTVlNzk1NzdhYmI1ZDc5NTg3Zjc3NTQ3Zjc1ODE3NjAwYTA2OTVkNzk1ODdmNzc1NDdmNzU4MThjOWQ1ZDdhNTQ3Zjc1ODE1Yzc5OWY2OTVjNzk1YzdmNzc4MTc2MDBhMDY5NWE3OWEzNTk3OWE0NWQ3YTU0N2Y3NTgxNzY1ZDdhYTI2OTVjN2FhMjc4NWI3YThiNWM3YWE1OTE5YjY5MDIzNDA1NzY1ODdhNTM3YTk2NTg3YTk0YTQ3YzU3N2E1Mjc5OTRhNGM0NTI5ZDAwY2M3YjlkMDBjZDU1N2E4ODUxY2M5ZDUxY2Q1NDdhODc3Nzc3Nzc2OGZlZmZmZmZmMDIyOTI5MzUwMDAwMDAwMDAwMTk3NmE5MTQxNDA5NjYwMGI5M2M3YjUwYzI5ZmUyNTU4MmM0OTA2Zjk0NmU3NGJmODhhY2Q3MTQxNDAwMDAwMDAwMDAxOTc2YTkxNDEzODEzODU3MWI4Y2NkMzRlZjgzOWFkYzZiNzNhY2E4ZTc2NmI1ZTg4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI0YjU1ZTFkNWU1MGJlMzU1MjlmMDlmYzVhNTljZDBkOTgwZWFlMDhiZjc3Mzc2NWUwMDgzYzgyMjU0ZTQ3MTliIiwiaGFzaCI6IjRiNTVlMWQ1ZTUwYmUzNTUyOWYwOWZjNWE1OWNkMGQ5ODBlYWUwOGJmNzczNzY1ZTAwODNjODIyNTRlNDcxOWIiLCJ2ZXJzaW9uIjoyLCJzaXplIjoxNjQsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiJjOTViYjcwZTcyZDdlMWRkODQxNWJiZjZiN2FiY2MwYjU5ZTdiNmRhOTFiODAzNzZhMWUyMWQzYjQzMTgxYjZjIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEiLCJoZXgiOiI0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEifSwic2VxdWVuY2UiOjF9XSwidm91dCI6W3sidmFsdWUiOjAuMDA1NjY1MDIsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dLCJmZWUiOjEuNjRlLTA2LCJoZXgiOiIwMjAwMDAwMDAxNmMxYjE4NDMzYjFkZTJhMTc2MDNiODkxZGFiNmU3NTkwYmNjYWJiN2Y2YmIxNTg0ZGRlMWQ3NzIwZWI3NWJjOTAwMDAwMDAwNDU0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEwMTAwMDAwMDAxZTZhNDA4MDAwMDAwMDAwMDIzYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NzAwMDAwMDAwIn0seyJ0eGlkIjoiNGJhMmFmYzM4Y2E1OGE4NzU4ODlkNTk0NDlkNzc3ZmZjOGE5MTllMTMwMGFiODA1MjZiODIxNDBhZTdhNGZiMiIsImhhc2giOiI0YmEyYWZjMzhjYTU4YTg3NTg4OWQ1OTQ0OWQ3NzdmZmM4YTkxOWUxMzAwYWI4MDUyNmI4MjE0MGFlN2E0ZmIyIiwidmVyc2lvbiI6Miwic2l6ZSI6MjI1LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiMjMzZGEwYTk3Njg0N2FlM2ZiYTJmMmZjYmI5NDYxYjI0YzViMDgyNDVlYTlmOGQ0NTYzOTE0MTIzMmQ4NzQ5MyIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDJjNzI4ZjYzNTExNjk1MmU0YzhjZDhlMWIzMDVlZTZmNWM2Zjk2NDJiMDUzYmRlNDZiYWZkYmYzYmJiMDM1ODIwMjIwNzg0OTY3ZGJlNGRmZmZkOWY1NDM5NmM3OWU2ODRlYjgyODJhNWRhMGQ5ZDlmM2IyOTI3YzJmYWZkOGE4Njg3Y1tBTEx8Rk9SS0lEXSAwMmZhMDg4Yjk2ZmNmMjQwZGFjNThjZmQ1ODkxZDNhNGQxODRiMDc1ZGZjZTlmNjc1MjVhNTI3NzE3YWZmNzdkMjciLCJoZXgiOiI0NzMwNDQwMjIwMmM3MjhmNjM1MTE2OTUyZTRjOGNkOGUxYjMwNWVlNmY1YzZmOTY0MmIwNTNiZGU0NmJhZmRiZjNiYmIwMzU4MjAyMjA3ODQ5NjdkYmU0ZGZmZmQ5ZjU0Mzk2Yzc5ZTY4NGViODI4MmE1ZGEwZDlkOWYzYjI5MjdjMmZhZmQ4YTg2ODdjNDEyMTAyZmEwODhiOTZmY2YyNDBkYWM1OGNmZDU4OTFkM2E0ZDE4NGIwNzVkZmNlOWY2NzUyNWE1Mjc3MTdhZmY3N2QyNyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMDI3ODE0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODEwZjk0ZGJiZjYyZTZkNGRiNGYyMzMxYjVkOGNhMGI2MDMyNDQyMyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDgxMGY5NGRiYmY2MmU2ZDRkYjRmMjMzMWI1ZDhjYTBiNjAzMjQ0MjM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpxc2w5eG1oYTN3ZDR4bWZ1M25yZHdjZWc5a3F2anl5djNtMnU5bXJsIl19fSx7InZhbHVlIjoxLjYwOGUtMDUsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3ZGU1YmQ3ZDU2OGQwOTliZjg1NzJkN2I4YTVmNmFiNzRkNTdjYmZlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0N2RlNWJkN2Q1NjhkMDk5YmY4NTcyZDdiOGE1ZjZhYjc0ZDU3Y2JmZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDc3dDB0YTI2eHNueGxjMnVraGh6amxkMm01NjQ3dGxjZzhwdTl1cWUiXX19XSwiZmVlIjoyLjc3ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTkzNzRkODMyMTIxNDM5NTZkNGY4YTk1ZTI0MDg1YjRjYjI2MTk0YmJmY2YyYTJmYmUzN2E4NDc2YTlhMDNkMjMwMTAwMDAwMDZhNDczMDQ0MDIyMDJjNzI4ZjYzNTExNjk1MmU0YzhjZDhlMWIzMDVlZTZmNWM2Zjk2NDJiMDUzYmRlNDZiYWZkYmYzYmJiMDM1ODIwMjIwNzg0OTY3ZGJlNGRmZmZkOWY1NDM5NmM3OWU2ODRlYjgyODJhNWRhMGQ5ZDlmM2IyOTI3YzJmYWZkOGE4Njg3YzQxMjEwMmZhMDg4Yjk2ZmNmMjQwZGFjNThjZmQ1ODkxZDNhNGQxODRiMDc1ZGZjZTlmNjc1MjVhNTI3NzE3YWZmNzdkMjdmZmZmZmZmZjAyN2MzZTA0MDAwMDAwMDAwMDE5NzZhOTE0ODEwZjk0ZGJiZjYyZTZkNGRiNGYyMzMxYjVkOGNhMGI2MDMyNDQyMzg4YWM0ODA2MDAwMDAwMDAwMDAwMTk3NmE5MTQ3ZGU1YmQ3ZDU2OGQwOTliZjg1NzJkN2I4YTVmNmFiNzRkNTdjYmZlODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiNGQ2MWU4OWFlYmI2MzAyN2Y4OTg1ZmI5ODdkYzhlZWU2Y2QzZTZjZTQ4Mjc0MWE0ZjQ2NWIxOWQ0MGNmZmJkNyIsImhhc2giOiI0ZDYxZTg5YWViYjYzMDI3Zjg5ODVmYjk4N2RjOGVlZTZjZDNlNmNlNDgyNzQxYTRmNDY1YjE5ZDQwY2ZmYmQ3IiwidmVyc2lvbiI6MSwic2l6ZSI6MjI1LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiMjEzMmZjMGIyZTE3ZWEzMTU0NjI0ZWI2ZTliMjY0MGI1NTBiYzgzOWRmYWE2Y2NhMGRlMWI1OWQ0ZGUxNjMyZCIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDQ1MzcyMjc2MTdjMzQ1MjdiZDY5M2Q1Mjk3MTM5ZjlhNmZkY2JjMGRhNmNiZmFhNTJhYWZkYTkwMGVhYWE5MTYwMjIwMmYxNjQ3YWZjNDFjZGM3ZjRhNWQzZDk2N2Y1NTlhY2ZmOWM5ZGIzZDY4OWEzMTZlZGZlNzNlNGI4N2UwMTI0OFtBTEx8Rk9SS0lEXSAwMmFhYWY3YjVmNWI3MTgzZWVlNTVkNjk4ZjNkNGEwMTBiMTRmNDVjYjBhOWQ3MGIwNGIxOTY5OWI0NjhjMmYzYTIiLCJoZXgiOiI0NzMwNDQwMjIwNDUzNzIyNzYxN2MzNDUyN2JkNjkzZDUyOTcxMzlmOWE2ZmRjYmMwZGE2Y2JmYWE1MmFhZmRhOTAwZWFhYTkxNjAyMjAyZjE2NDdhZmM0MWNkYzdmNGE1ZDNkOTY3ZjU1OWFjZmY5YzlkYjNkNjg5YTMxNmVkZmU3M2U0Yjg3ZTAxMjQ4NDEyMTAyYWFhZjdiNWY1YjcxODNlZWU1NWQ2OThmM2Q0YTAxMGIxNGY0NWNiMGE5ZDcwYjA0YjE5Njk5YjQ2OGMyZjNhMiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC40MDM3OTQwNCwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDRlNjIwMDVmZjExODFhMmNkNDY1NWMxOWIzYzYzMWYxZTNiYzYzNWQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0ZTYyMDA1ZmYxMTgxYTJjZDQ2NTVjMTliM2M2MzFmMWUzYmM2MzVkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwOHh5cXpsN3l2cDV0eDV2NHdwbnY3eHg4Yzc4MHJydDUybjJ0YWE4cSJdfX0seyJ2YWx1ZSI6MC40ODE3NDQ1NiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDRiYjlmMDdjNTI3Y2JkYzhiZWI0M2U1MGI5ZDQ5ZTVjOGIxMDIxMmUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0YmI5ZjA3YzUyN2NiZGM4YmViNDNlNTBiOWQ0OWU1YzhiMTAyMTJlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwOW1udXJ1MmY3dG1qOTdrc2w5cHd3NW5ld2dreXBwOWMzMzY3am14aCJdfX1dLCJmZWUiOjIuMjZlLTA2LCJoZXgiOiIwMTAwMDAwMDAxMmQ2M2UxNGQ5ZGI1ZTEwZGNhNmNhYWRmMzljODBiNTUwYjY0YjJlOWI2NGU2MjU0MzFlYTE3MmUwYmZjMzIyMTAwMDAwMDAwNmE0NzMwNDQwMjIwNDUzNzIyNzYxN2MzNDUyN2JkNjkzZDUyOTcxMzlmOWE2ZmRjYmMwZGE2Y2JmYWE1MmFhZmRhOTAwZWFhYTkxNjAyMjAyZjE2NDdhZmM0MWNkYzdmNGE1ZDNkOTY3ZjU1OWFjZmY5YzlkYjNkNjg5YTMxNmVkZmU3M2U0Yjg3ZTAxMjQ4NDEyMTAyYWFhZjdiNWY1YjcxODNlZWU1NWQ2OThmM2Q0YTAxMGIxNGY0NWNiMGE5ZDcwYjA0YjE5Njk5YjQ2OGMyZjNhMmZmZmZmZmZmMDIwYzI0NjgwMjAwMDAwMDAwMTk3NmE5MTQ0ZTYyMDA1ZmYxMTgxYTJjZDQ2NTVjMTliM2M2MzFmMWUzYmM2MzVkODhhYzc4MTVkZjAyMDAwMDAwMDAxOTc2YTkxNDRiYjlmMDdjNTI3Y2JkYzhiZWI0M2U1MGI5ZDQ5ZTVjOGIxMDIxMmU4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI1MThjYzJlZDY1MTNhMmVmNDQ3Yzk2ZDdiZjQzMGQ1NWUyNmM3OThmN2M1NWU4NDk4NTU4NmUyYWY1MTI5Zjk0IiwiaGFzaCI6IjUxOGNjMmVkNjUxM2EyZWY0NDdjOTZkN2JmNDMwZDU1ZTI2Yzc5OGY3YzU1ZTg0OTg1NTg2ZTJhZjUxMjlmOTQiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyMjYsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiJlZGU4YmUwMzIzNDg4Y2JiYWM4ZWJjNDQxYzc5YzI4YjczYmFlYmI5MWU2OTUzMmU1M2IwMTAyN2MxNWQ5ZWVmIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBlMzY5YmM1ZTIwMDcwMzNmZmZkYTY2ZTVkNGJjYWEyMjBiNDBjNDcxZTc0NGJjNzc5OTA1MGZjOGU2OTM5OTU2MDIyMDFlYjUwN2U3N2VmM2RlYTg0ZjIzNDBiYzI4Mzk4MWYzYzYzODVlY2UxMTZlZTc1YjUxMDljYjVmY2ViYjJkOThbQUxMfEZPUktJRF0gMDNjMTFhNDJlMjhiNzM5OWVmMzM3ZDcxMTc5ZDAzYWQwZTM4N2MyN2Q3NWFmYjI1MTc2OGJhNTQ1NDU1ODMzMjIwIiwiaGV4IjoiNDgzMDQ1MDIyMTAwZTM2OWJjNWUyMDA3MDMzZmZmZGE2NmU1ZDRiY2FhMjIwYjQwYzQ3MWU3NDRiYzc3OTkwNTBmYzhlNjkzOTk1NjAyMjAxZWI1MDdlNzdlZjNkZWE4NGYyMzQwYmMyODM5ODFmM2M2Mzg1ZWNlMTE2ZWU3NWI1MTA5Y2I1ZmNlYmIyZDk4NDEyMTAzYzExYTQyZTI4YjczOTllZjMzN2Q3MTE3OWQwM2FkMGUzODdjMjdkNzVhZmIyNTE3NjhiYTU0NTQ1NTgzMzIyMCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMTExMiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDUwZGI0YTZmNThiYTAwMDcwMWI2YTBiZWI4OWU1MzI3MDE0Yzk1MmIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1MGRiNGE2ZjU4YmEwMDA3MDFiNmEwYmViODllNTMyNzAxNGM5NTJiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwZ2Rram4wdHphcXFwY3BrNnN0YXd5NzJ2bnN6bnk0OXZsNGpncW1zNyJdfX0seyJ2YWx1ZSI6MC4wMDUwNjQ0OCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGRhNmY3Yjg4MDFkNDRhZjQ5MDZlMzUwNjc5MTJkNTk1YTQ5NDc3MzIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkYTZmN2I4ODAxZDQ0YWY0OTA2ZTM1MDY3OTEyZDU5NWE0OTQ3NzMyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyZHg3N3VncTgyeTRheXNkYzZzdjdnajZrMjZmOXJoeGc2ZXBtd2c0YSJdfX1dLCJmZWUiOjQuNTJlLTA2LCJoZXgiOiIwMTAwMDAwMDAxZWY5ZTVkYzEyNzEwYjA1MzJlNTM2OTFlYjllYmJhNzM4YmMyNzkxYzQ0YmM4ZWFjYmI4YzQ4MjMwM2JlZThlZDAxMDAwMDAwNmI0ODMwNDUwMjIxMDBlMzY5YmM1ZTIwMDcwMzNmZmZkYTY2ZTVkNGJjYWEyMjBiNDBjNDcxZTc0NGJjNzc5OTA1MGZjOGU2OTM5OTU2MDIyMDFlYjUwN2U3N2VmM2RlYTg0ZjIzNDBiYzI4Mzk4MWYzYzYzODVlY2UxMTZlZTc1YjUxMDljYjVmY2ViYjJkOTg0MTIxMDNjMTFhNDJlMjhiNzM5OWVmMzM3ZDcxMTc5ZDAzYWQwZTM4N2MyN2Q3NWFmYjI1MTc2OGJhNTQ1NDU1ODMzMjIwZmZmZmZmZmYwMmMwZjcxMDAwMDAwMDAwMDAxOTc2YTkxNDUwZGI0YTZmNThiYTAwMDcwMWI2YTBiZWI4OWU1MzI3MDE0Yzk1MmI4OGFjNTBiYTA3MDAwMDAwMDAwMDE5NzZhOTE0ZGE2ZjdiODgwMWQ0NGFmNDkwNmUzNTA2NzkxMmQ1OTVhNDk0NzczMjg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjU3NDk0MmNjYzU4M2M4OTI5MDE2MmYzMTczMzNhNzhkN2YzZmNjYjM2NTNiYmU0MmU5ZTU5MjYzYzE5MjljMTYiLCJoYXNoIjoiNTc0OTQyY2NjNTgzYzg5MjkwMTYyZjMxNzMzM2E3OGQ3ZjNmY2NiMzY1M2JiZTQyZTllNTkyNjNjMTkyOWMxNiIsInZlcnNpb24iOjEsInNpemUiOjM0MCwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImY0MjVlMDAyNzBiNDM0Mjc0ODEzYzdmMjRhNTIyZDFlMDc4YjU3OWVkODQ5MDhiZjJkYTMzYmY0MGRjODg2NWMiLCJ2b3V0Ijo0LCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ1MDIyMTAwYzEzOTY5MDc1ZTZlM2FhOTUzNDM0YmI0YTRhNmU5ODk5MGNmZjdlNmExMzJjMjVlYzBkZTY1OWZhYTllNzM5MjAyMjAyMjc3OGVkMGJiMGFhYTRlMDBhYTNlODkwNGJmYzg5NjBmZTUxYzU4ZmZlMzk2ZWQyZTI5OTVkYjczMmY1ZDZjW0FMTHxGT1JLSURdIDMwNDQwMjIwNmQwNTIwMjJlN2FhNzRhZjY3ZDk3MjU0MTYwMmQ2NDdiZThmMmY4NDhiYjM0MDgzZWQyOTc2ZDk4ZTM2YjM5MjAyMjAxMGViN2Y5ZGFjYTJiOTMzNzZmNmRlMDhlMjRkNjdjMTBlMzNlZDI0MDY3MmQ0NzdhNWU5ZDk0Yzg4ZDMzMzgxW0FMTHxGT1JLSURdIDUyMjEwMjY1YjAwZTkyZWIyZDdjZTEyMjE0NmMzY2M4MjhiMGQ4YjEzNDBhYWU1YzllOTIyZjU0MTI2N2U3YmRlM2NhMTEyMTAyNzQwNGVkYTM4ODI1ZjQ5ODg1NWMxNDc5NzVlYzZjMWE0OTJjMTZhMzA2NDQ3ZTFiYjYzZTI0YTY2N2E5YjExYjIxMDJmMzhlODdlNjBkYTExZjRkZTBiZWNkMGE5NTU0OWY0NDhlYmJmMWU3YzJkYWFjYTBmZDhiZTgzNmQ1ODE5MjU4NTNhZSIsImhleCI6IjAwNDgzMDQ1MDIyMTAwYzEzOTY5MDc1ZTZlM2FhOTUzNDM0YmI0YTRhNmU5ODk5MGNmZjdlNmExMzJjMjVlYzBkZTY1OWZhYTllNzM5MjAyMjAyMjc3OGVkMGJiMGFhYTRlMDBhYTNlODkwNGJmYzg5NjBmZTUxYzU4ZmZlMzk2ZWQyZTI5OTVkYjczMmY1ZDZjNDE0NzMwNDQwMjIwNmQwNTIwMjJlN2FhNzRhZjY3ZDk3MjU0MTYwMmQ2NDdiZThmMmY4NDhiYjM0MDgzZWQyOTc2ZDk4ZTM2YjM5MjAyMjAxMGViN2Y5ZGFjYTJiOTMzNzZmNmRlMDhlMjRkNjdjMTBlMzNlZDI0MDY3MmQ0NzdhNWU5ZDk0Yzg4ZDMzMzgxNDE0YzY5NTIyMTAyNjViMDBlOTJlYjJkN2NlMTIyMTQ2YzNjYzgyOGIwZDhiMTM0MGFhZTVjOWU5MjJmNTQxMjY3ZTdiZGUzY2ExMTIxMDI3NDA0ZWRhMzg4MjVmNDk4ODU1YzE0Nzk3NWVjNmMxYTQ5MmMxNmEzMDY0NDdlMWJiNjNlMjRhNjY3YTliMTFiMjEwMmYzOGU4N2U2MGRhMTFmNGRlMGJlY2QwYTk1NTQ5ZjQ0OGViYmYxZTdjMmRhYWNhMGZkOGJlODM2ZDU4MTkyNTg1M2FlIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjIwMjUzMzQ1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTNjM2MwNGU2ZmViOWZkMzgzYWE4ZWI4ODIxMWM0Y2Y2MDEyZmZjYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGUzYzNjMDRlNmZlYjlmZDM4M2FhOGViODgyMTFjNGNmNjAxMmZmY2M4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXIzdThzendkbDRlbDV1cjQyOHQzcXMzY244a3F5aGxlc2pxZmptcDdrIl19fV0sImZlZSI6Ni44NGUtMDYsImhleCI6IjAxMDAwMDAwMDE1Yzg2YzgwZGY0M2JhMzJkYmYwODQ5ZDg5ZTU3OGIwNzFlMmQ1MjRhZjJjNzEzNDgyNzM0YjQ3MDAyZTAyNWY0MDQwMDAwMDBmZGZkMDAwMDQ4MzA0NTAyMjEwMGMxMzk2OTA3NWU2ZTNhYTk1MzQzNGJiNGE0YTZlOTg5OTBjZmY3ZTZhMTMyYzI1ZWMwZGU2NTlmYWE5ZTczOTIwMjIwMjI3NzhlZDBiYjBhYWE0ZTAwYWEzZTg5MDRiZmM4OTYwZmU1MWM1OGZmZTM5NmVkMmUyOTk1ZGI3MzJmNWQ2YzQxNDczMDQ0MDIyMDZkMDUyMDIyZTdhYTc0YWY2N2Q5NzI1NDE2MDJkNjQ3YmU4ZjJmODQ4YmIzNDA4M2VkMjk3NmQ5OGUzNmIzOTIwMjIwMTBlYjdmOWRhY2EyYjkzMzc2ZjZkZTA4ZTI0ZDY3YzEwZTMzZWQyNDA2NzJkNDc3YTVlOWQ5NGM4OGQzMzM4MTQxNGM2OTUyMjEwMjY1YjAwZTkyZWIyZDdjZTEyMjE0NmMzY2M4MjhiMGQ4YjEzNDBhYWU1YzllOTIyZjU0MTI2N2U3YmRlM2NhMTEyMTAyNzQwNGVkYTM4ODI1ZjQ5ODg1NWMxNDc5NzVlYzZjMWE0OTJjMTZhMzA2NDQ3ZTFiYjYzZTI0YTY2N2E5YjExYjIxMDJmMzhlODdlNjBkYTExZjRkZTBiZWNkMGE5NTU0OWY0NDhlYmJmMWU3YzJkYWFjYTBmZDhiZTgzNmQ1ODE5MjU4NTNhZWZmZmZmZmZmMDFhMTBhMzUwMTAwMDAwMDAwMTk3NmE5MTRlM2MzYzA0ZTZmZWI5ZmQzODNhYThlYjg4MjExYzRjZjYwMTJmZmNjODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiNTg0MjdmMDcxYTJhNzE4YTM3YmU3OWYxZTQ4N2M0ZTVhYWZkYzY5OTNmMzg4OWViMTYyNzI2YTM5YmE3NDQxOSIsImhhc2giOiI1ODQyN2YwNzFhMmE3MThhMzdiZTc5ZjFlNDg3YzRlNWFhZmRjNjk5M2YzODg5ZWIxNjI3MjZhMzliYTc0NDE5IiwidmVyc2lvbiI6Miwic2l6ZSI6MjI2LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiNGY0NTljYTA3YTE0ZTc3Y2UyMDEyZWQ4ZDVjMTA4MTE5M2ZhNjg1YzFlNzk5NTRmMmJjZmQ3ZDI4NjQ3MmMzYiIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwZDE3ZTU2YzYzMWYwNzg4ZTI2MmI2MWI2MWU2MWFlN2MxOTM1ZDBiNzgyYWJiNzgxNjZhMWFhNzhjODI3MWYyODAyMjAyMDMxNWNjM2ZmMzliZWE1OTIxN2VhYWIzOTE5OWY2NzZiMzkyYjBkOWRhYjc4ZTEyMWQ2MjI2NDc5MDcyMTI4W0FMTHxGT1JLSURdIDAzM2I1ZTBjMDcwOWQ1YzZiNjE3MTk5NGQ3M2UxMmNhZDU0NGNkMTc1MGIxYmRiMGFjZWYyZWQ0NWYzYzc2ZTU3OCIsImhleCI6IjQ4MzA0NTAyMjEwMGQxN2U1NmM2MzFmMDc4OGUyNjJiNjFiNjFlNjFhZTdjMTkzNWQwYjc4MmFiYjc4MTY2YTFhYTc4YzgyNzFmMjgwMjIwMjAzMTVjYzNmZjM5YmVhNTkyMTdlYWFiMzkxOTlmNjc2YjM5MmIwZDlkYWI3OGUxMjFkNjIyNjQ3OTA3MjEyODQxMjEwMzNiNWUwYzA3MDlkNWM2YjYxNzE5OTRkNzNlMTJjYWQ1NDRjZDE3NTBiMWJkYjBhY2VmMmVkNDVmM2M3NmU1NzgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMjA1MjU4MDEsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA4YzllY2Q4NDk2YjJkNjliMTY3ZWE2Y2M5MWFjMTgxNTg2MzJlOTRlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OGM5ZWNkODQ5NmIyZDY5YjE2N2VhNmNjOTFhYzE4MTU4NjMyZTk0ZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenhmYW52eWo2ZWRkeGNrMDZudmV5ZHZycTJjdnZoZmZjbmdhZzVoc3IiXX19LHsidmFsdWUiOjE3LjEwOTI5NDc3LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTZlZTA0Y2I3NzI4NzU5YThkZWU2YmVkM2E3ODEzMTdhM2NlM2QyZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpud3VweHR3dTU4dHg1ZGFlNDc2d25jenZ0NjhuM2E5Y200bXI5cHRtIl19fV0sImZlZSI6Mi43ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTNiMmM0Nzg2ZDJkN2NmMmI0Zjk1NzkxZTVjNjhmYTkzMTEwOGMxZDVkODJlMDFlMjdjZTcxNDdhYTA5YzQ1NGYwMjAwMDAwMDZiNDgzMDQ1MDIyMTAwZDE3ZTU2YzYzMWYwNzg4ZTI2MmI2MWI2MWU2MWFlN2MxOTM1ZDBiNzgyYWJiNzgxNjZhMWFhNzhjODI3MWYyODAyMjAyMDMxNWNjM2ZmMzliZWE1OTIxN2VhYWIzOTE5OWY2NzZiMzkyYjBkOWRhYjc4ZTEyMWQ2MjI2NDc5MDcyMTI4NDEyMTAzM2I1ZTBjMDcwOWQ1YzZiNjE3MTk5NGQ3M2UxMmNhZDU0NGNkMTc1MGIxYmRiMGFjZWYyZWQ0NWYzYzc2ZTU3OGZmZmZmZmZmMDJlOTMyMzkwMTAwMDAwMDAwMTk3NmE5MTQ4YzllY2Q4NDk2YjJkNjliMTY3ZWE2Y2M5MWFjMTgxNTg2MzJlOTRlODhhYzQ1YjZmYTY1MDAwMDAwMDAxOTc2YTkxNGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmU4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI2MWU5ZDhjZGE4NDNiYWY0NTA3Y2YzZDFmNzEzY2EzNDcwYWRiNjA0MDFlOGM3ODMyNDcxY2EwZWM5OWFjNDQ0IiwiaGFzaCI6IjYxZTlkOGNkYTg0M2JhZjQ1MDdjZjNkMWY3MTNjYTM0NzBhZGI2MDQwMWU4Yzc4MzI0NzFjYTBlYzk5YWM0NDQiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyMjYsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI4MDA2ZTBiZjM0NGZjNjRkMjJlMTM1ZDM4ZWNhZTA3NDZlNTEzMzAyMWI0MDkwMzFjYTNmMDUxMDdmYjYyYWZiIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBiNDhkYzlmM2MxNGQ5YmNlYTg5MGU0NDkwNDQ5ODY4NDE3YjFhMDJlMzVkZjg3OGQyNTJmY2Q1YjdjNDk0ZGRjMDIyMDdjZDE3ZDViNmUyY2RjYzM0OTNkYWEyZTE5NWQ2OGQxZWE4MDE1YzBkYWNlMTE4ZDY1NWI4YzZhZjUxNjA2OWJbQUxMfEZPUktJRF0gMDM4YWM2MDhjM2M0ZDBiNzVjOWRhZTc4NzljZWYxMzFmMzE1Nzk0YTBmOTg3ZmI4ZDQyMDc2NTAxZjBmOGY1Yzg3IiwiaGV4IjoiNDgzMDQ1MDIyMTAwYjQ4ZGM5ZjNjMTRkOWJjZWE4OTBlNDQ5MDQ0OTg2ODQxN2IxYTAyZTM1ZGY4NzhkMjUyZmNkNWI3YzQ5NGRkYzAyMjA3Y2QxN2Q1YjZlMmNkY2MzNDkzZGFhMmUxOTVkNjhkMWVhODAxNWMwZGFjZTExOGQ2NTViOGM2YWY1MTYwNjliNDEyMTAzOGFjNjA4YzNjNGQwYjc1YzlkYWU3ODc5Y2VmMTMxZjMxNTc5NGEwZjk4N2ZiOGQ0MjA3NjUwMWYwZjhmNWM4NyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMDA1NzY1OSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGEzNmU1NGI0YmRlNDhlMGNkZDAyMTAwYjE4ZmNkZTM2OTZkZWM5YTYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhMzZlNTRiNGJkZTQ4ZTBjZGQwMjEwMGIxOGZjZGUzNjk2ZGVjOWE2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6M2t1NDk1aGhqZ3VyeGFxZ2dxa3g4dW1jbWZkaGtmNWM0c25yOHE2dCJdfX0seyJ2YWx1ZSI6NC41NDJlLTA1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTViOTU1OWY0NmE5MjlmYjc1YmNlN2VkYzljYjRhOGRhNDRiOWRjNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU1Yjk1NTlmNDZhOTI5ZmI3NWJjZTdlZGM5Y2I0YThkYTQ0YjlkYzQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXAybWo0dmxnNjVqbjdtNGhubjdtand0ZjJ4NmdqdWFjc3J0aG01c2FwIl19fV0sImZlZSI6NS45NWUtMDYsImhleCI6IjAxMDAwMDAwMDFmYjJhYjY3ZjEwMDUzZmNhMzE5MDQwMWIwMjMzNTE2ZTc0ZTBjYThlZDMzNWUxMjI0ZGM2NGYzNGJmZTAwNjgwMDEwMDAwMDA2YjQ4MzA0NTAyMjEwMGI0OGRjOWYzYzE0ZDliY2VhODkwZTQ0OTA0NDk4Njg0MTdiMWEwMmUzNWRmODc4ZDI1MmZjZDViN2M0OTRkZGMwMjIwN2NkMTdkNWI2ZTJjZGNjMzQ5M2RhYTJlMTk1ZDY4ZDFlYTgwMTVjMGRhY2UxMThkNjU1YjhjNmFmNTE2MDY5YjQxMjEwMzhhYzYwOGMzYzRkMGI3NWM5ZGFlNzg3OWNlZjEzMWYzMTU3OTRhMGY5ODdmYjhkNDIwNzY1MDFmMGY4ZjVjODdmZmZmZmZmZjAyM2JlMTAwMDAwMDAwMDAwMDE5NzZhOTE0YTM2ZTU0YjRiZGU0OGUwY2RkMDIxMDBiMThmY2RlMzY5NmRlYzlhNjg4YWNiZTExMDAwMDAwMDAwMDAwMTk3NmE5MTQ1NWI5NTU5ZjQ2YTkyOWZiNzViY2U3ZWRjOWNiNGE4ZGE0NGI5ZGM0ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiNjM4M2JkODVhNzZhYTQ2MjQ3MTJhZTUyYzUyYjFhYzdmODQyOGFlNWZmMTdmY2UxMjczMDM0NDA4NmY3YTIxMSIsImhhc2giOiI2MzgzYmQ4NWE3NmFhNDYyNDcxMmFlNTJjNTJiMWFjN2Y4NDI4YWU1ZmYxN2ZjZTEyNzMwMzQ0MDg2ZjdhMjExIiwidmVyc2lvbiI6Miwic2l6ZSI6MjI1LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiMzIzMGQyNTBlMGQxZTkxYWM2MTJlMGE5YTZkNDc4MjVhZmY5M2ZjNWQ4NWQ4MTMwMDFiMWZjZWIyYzJjNzY1MiIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDRiNzJmYWE5Nzg4NzM3YzFkMzMyYjYxNDcyOWY0MWZmNWMwNTliMDFhOTc2MjlmZmM2MmYyY2VmNTFmN2JjZGIwMjIwMTZiNmVjYmZiMjMzNmRkNzU0N2I0MjRjNGI4ZmFmNmQwOTYwZDVmNTlhOWIyMzU5NmI1NzVlZjg1ODE3OTRlYVtBTEx8Rk9SS0lEXSAwMzc0NjlkZWFhZjBjMjA5YjIyYzU4ZGVmOWU1NzlmZGJkNDhkMzE3YWE4MmZiMmIxYzBhMDc5ZDhiNzk2ZWY1MTkiLCJoZXgiOiI0NzMwNDQwMjIwNGI3MmZhYTk3ODg3MzdjMWQzMzJiNjE0NzI5ZjQxZmY1YzA1OWIwMWE5NzYyOWZmYzYyZjJjZWY1MWY3YmNkYjAyMjAxNmI2ZWNiZmIyMzM2ZGQ3NTQ3YjQyNGM0YjhmYWY2ZDA5NjBkNWY1OWE5YjIzNTk2YjU3NWVmODU4MTc5NGVhNDEyMTAzNzQ2OWRlYWFmMGMyMDliMjJjNThkZWY5ZTU3OWZkYmQ0OGQzMTdhYTgyZmIyYjFjMGEwNzlkOGI3OTZlZjUxOSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4xOTQxMjYsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxNDIwNDM5OGUzYmE4MzM1OTZmM2UwNzBhNjQzNTA1NDk2ZjFiOWU0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTQyMDQzOThlM2JhODMzNTk2ZjNlMDcwYTY0MzUwNTQ5NmYxYjllNDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcTJ6cXN1Y3V3YWd4ZHZrNzBzOHBmanIycDJmZHVkZXVzcXN0ajdxZTIiXX19LHsidmFsdWUiOjAuOTk0ODU3MDksIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5ZDE1YmE4NzBiYWI0ZGJiZmZiZmRmYjI5NTkxNDEyN2Q3MDJiOTA3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OWQxNWJhODcwYmFiNGRiYmZmYmZkZmIyOTU5MTQxMjdkNzAyYjkwNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenczdHc1OHB3NDVtd2xsaGwwbTk5djNneW5hd3E0ZXF1d24zbnl3OGYiXX19XSwiZmVlIjoyLjI4ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTUyNzYyYzJjZWJmY2IxMDEzMDgxNWRkOGM1M2ZmOWFmMjU3OGQ0YTZhOWUwMTJjNjFhZTlkMWUwNTBkMjMwMzIwMTAwMDAwMDZhNDczMDQ0MDIyMDRiNzJmYWE5Nzg4NzM3YzFkMzMyYjYxNDcyOWY0MWZmNWMwNTliMDFhOTc2MjlmZmM2MmYyY2VmNTFmN2JjZGIwMjIwMTZiNmVjYmZiMjMzNmRkNzU0N2I0MjRjNGI4ZmFmNmQwOTYwZDVmNTlhOWIyMzU5NmI1NzVlZjg1ODE3OTRlYTQxMjEwMzc0NjlkZWFhZjBjMjA5YjIyYzU4ZGVmOWU1NzlmZGJkNDhkMzE3YWE4MmZiMmIxYzBhMDc5ZDhiNzk2ZWY1MTlmZmZmZmZmZjAyNzgzNjI4MDEwMDAwMDAwMDE5NzZhOTE0MTQyMDQzOThlM2JhODMzNTk2ZjNlMDcwYTY0MzUwNTQ5NmYxYjllNDg4YWMwZDA4ZWUwNTAwMDAwMDAwMTk3NmE5MTQ5ZDE1YmE4NzBiYWI0ZGJiZmZiZmRmYjI5NTkxNDEyN2Q3MDJiOTA3ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiNjY0MGFjN2Y5NmExMGRiZjYyOGVjMjI0NjgzOWZiYTllNWRkYmZhNTg5N2FiNjA0NDZmODhmZjM4YTRjMmQ5NiIsImhhc2giOiI2NjQwYWM3Zjk2YTEwZGJmNjI4ZWMyMjQ2ODM5ZmJhOWU1ZGRiZmE1ODk3YWI2MDQ0NmY4OGZmMzhhNGMyZDk2IiwidmVyc2lvbiI6Miwic2l6ZSI6Nzc5OSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6Ijk3MTQwMDcxYjg2MTI5Y2MzYjI0ZDliYzk3ZDkxNTQxNmY4Y2VjZTE0NTU2MTAyYzk0OWIyZTI5MzdkYWQyMDUiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNmEyYTEwOTZmMjYzNzQ4OWUwOGViNGY0MjE2YzY3NTMwNDM1NWNlNTUzMGRkYzNiNjIzMTlhMzhiMWYwNjRmZDY5M2Q2Y2E0NjJmZjEyN2Y3NGQwNDk1NzM0MmZiZDNjOGNjMzQyODI5MmMxMjZhMzU1ZmI1MTE1NWViYTQ2MDVbQUxMfEZPUktJRF0gMDM0YjM2NGMyOTYyOTZlOTExNTJkNzJiZDQxOGI2M2E0ZmZjYjYzYWI0OTZjMWIyYTE5MmZkOGZhM2ZiNmM2ZTgyIiwiaGV4IjoiNDE2YTJhMTA5NmYyNjM3NDg5ZTA4ZWI0ZjQyMTZjNjc1MzA0MzU1Y2U1NTMwZGRjM2I2MjMxOWEzOGIxZjA2NGZkNjkzZDZjYTQ2MmZmMTI3Zjc0ZDA0OTU3MzQyZmJkM2M4Y2MzNDI4MjkyYzEyNmEzNTVmYjUxMTU1ZWJhNDYwNTQxMjEwMzRiMzY0YzI5NjI5NmU5MTE1MmQ3MmJkNDE4YjYzYTRmZmNiNjNhYjQ5NmMxYjJhMTkyZmQ4ZmEzZmI2YzZlODIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjdlNTQ0MmYwN2NhNWJiM2JlZGViNTExYWUzMGY0MmJiMWM0MzY4OWZjNDUyMTRkMGZmNDE4YzRlNWExMTVlYmEiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiNDg2YzAwN2ViMmQ4NDQxZWMxMTdjMGIwMThlYjU0ODY3YTg2MGQ3MDQxYTEzMTZiYTcwZTE1ZWNkNWI0YzhhMDEzNzE0YjhkZjMyZDFkMDYwNDE0MWMxZGRkMDc3YTJhMWYzMTc3NTk4NDc0MWMyZWMxZjNlOTliMzU3ZDVjNGFbQUxMfEZPUktJRF0gMDJiNTczZjg2YzUxN2ZkY2NhM2JiN2MwYWUyN2E1ZTg2MTBkMGU3ODFiN2Q0Njk0NTk4NmRhNDNiZGRkOWJjN2NlIiwiaGV4IjoiNDE0ODZjMDA3ZWIyZDg0NDFlYzExN2MwYjAxOGViNTQ4NjdhODYwZDcwNDFhMTMxNmJhNzBlMTVlY2Q1YjRjOGEwMTM3MTRiOGRmMzJkMWQwNjA0MTQxYzFkZGQwNzdhMmExZjMxNzc1OTg0NzQxYzJlYzFmM2U5OWIzNTdkNWM0YTQxMjEwMmI1NzNmODZjNTE3ZmRjY2EzYmI3YzBhZTI3YTVlODYxMGQwZTc4MWI3ZDQ2OTQ1OTg2ZGE0M2JkZGQ5YmM3Y2UifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjBhM2IyZWQwZDA3OTY5YzA5MTc3YjJkMTE0NzRmNDAwZDdjZDE2MjIzYmRkZGViMmI4NDY5MmM0NTVhMTNkMWEiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiZGI5NDRiMWJmNDM2OGY5Yzc1MmEzN2ZmMjkxMWFhMDQ4YjU5NmYzODA5MjE1Y2RjOGU5YWFiNzU4ZjAyNWQyMWM5YTAxYjZkYmJiZTUxNTgxYzhkM2YzOTlmZjY4ZDY2YTBlYWZmNWZlMWQ1YTZkMTg2MjJkMWYwYWEyOGVkYWNbQUxMfEZPUktJRF0gMDM0NGRmODI1ZWFkZjhmY2YyZDI3ZTA0ZGEwYTU4MGNkOWJjMDA3YzlmNDNmZDcwYWEzMWM5ZjY5NjIwOWFkODU2IiwiaGV4IjoiNDFkYjk0NGIxYmY0MzY4ZjljNzUyYTM3ZmYyOTExYWEwNDhiNTk2ZjM4MDkyMTVjZGM4ZTlhYWI3NThmMDI1ZDIxYzlhMDFiNmRiYmJlNTE1ODFjOGQzZjM5OWZmNjhkNjZhMGVhZmY1ZmUxZDVhNmQxODYyMmQxZjBhYTI4ZWRhYzQxMjEwMzQ0ZGY4MjVlYWRmOGZjZjJkMjdlMDRkYTBhNTgwY2Q5YmMwMDdjOWY0M2ZkNzBhYTMxYzlmNjk2MjA5YWQ4NTYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImI3MjE2YTIzNjY0ZjJjYjFiYTdmOGE1MWI1YmFjYmY0YzY2Njc1MTViODQ4OTlhNWFiOTA4YTAxMjM4ODE1OTYiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiZWJiMWQ5YWM4ZThiNzc1ODMwNjc1ZWM2ZGM2MzBlMWEzMmQ0MDkwYzFkODIwMGUyZWM2ZThiYmQ5MDdjN2UzYWJiMGE3MWZkMjY3NGQyMGEzNGRlNTJlZWUyNTIzMmU5OTY0MTJiMGM1MGJiN2MwMzIxZTY3OGQ2MWJjODg1YjZbQUxMfEZPUktJRF0gMDI3Y2QxZWViN2JlOTE2ZDRhZTUyNWMwZmMyZGIyNTJhOWRlZDRmY2YyYzVjZWUzMmUyNGYyYTQzZTRjMWUwYTNlIiwiaGV4IjoiNDFlYmIxZDlhYzhlOGI3NzU4MzA2NzVlYzZkYzYzMGUxYTMyZDQwOTBjMWQ4MjAwZTJlYzZlOGJiZDkwN2M3ZTNhYmIwYTcxZmQyNjc0ZDIwYTM0ZGU1MmVlZTI1MjMyZTk5NjQxMmIwYzUwYmI3YzAzMjFlNjc4ZDYxYmM4ODViNjQxMjEwMjdjZDFlZWI3YmU5MTZkNGFlNTI1YzBmYzJkYjI1MmE5ZGVkNGZjZjJjNWNlZTMyZTI0ZjJhNDNlNGMxZTBhM2UifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImE1ZTZlMjhiYzk0NDEwNTA2NjU3MTI1NmQ2MzZiNmY1YzIyZDE1Y2YxNjQzOThjNTJhYWJkNWYxYWEwYjUwM2MiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMjUzMmNmZGRjMzcwODU5MDYwZDIyZTk2Njg3ZWNiMmNkMTY2M2Y2M2UzODVkOWY0NjFmYWQ3MWI3YTY2ODRjNTRlZjQ4NDk4MGE4ZDQ5NmU1NWI5NmMzNGYyMzY0NzJhZGExZWNkNmU2ZTlhZDhiOTY3NTg4M2E2MWVjNmM2MWJbQUxMfEZPUktJRF0gMDJlODdjNWNmNGNlZGYyMWYxNzNmMTY1YzQ4YzM5ZmRhNzA0NTY3OTQ1OGExYjBjYTRlY2IyNGM0MzhkOWI4NTBmIiwiaGV4IjoiNDEyNTMyY2ZkZGMzNzA4NTkwNjBkMjJlOTY2ODdlY2IyY2QxNjYzZjYzZTM4NWQ5ZjQ2MWZhZDcxYjdhNjY4NGM1NGVmNDg0OTgwYThkNDk2ZTU1Yjk2YzM0ZjIzNjQ3MmFkYTFlY2Q2ZTZlOWFkOGI5Njc1ODgzYTYxZWM2YzYxYjQxMjEwMmU4N2M1Y2Y0Y2VkZjIxZjE3M2YxNjVjNDhjMzlmZGE3MDQ1Njc5NDU4YTFiMGNhNGVjYjI0YzQzOGQ5Yjg1MGYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjVjZDZkYzJiN2JlYWYwMTNmNTc2ZDViYmJlM2M1YmMwMWUyNDBmMjM2ZTZlYTI4NzVjZjFmZDM5MjMzYzU3M2YiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiM2Q3NmE5NDdkNmM0NTAyZWJiYTE5N2Y1ZjM5NWIyYTNkZmU0YjRkYzk3NjYwMzg2MWRmYjg4OTU5NTA1ZDYzMWE0MTk2NzA5MzY3MjYxOGMzM2I0ZGU2NGIwYjk1YTk2M2IzMjUxZDBlMjc2NDBkZTRlZGRiODNlOWViMWZiOWRbQUxMfEZPUktJRF0gMDNiZjhjNjlmM2IxNGE1ZmMyNmJmZjY1NjBhNzk0ODQwMDYyZWRlYjNkMzliYjFhMTgwODExZTFlNDM3MGI4MmZmIiwiaGV4IjoiNDEzZDc2YTk0N2Q2YzQ1MDJlYmJhMTk3ZjVmMzk1YjJhM2RmZTRiNGRjOTc2NjAzODYxZGZiODg5NTk1MDVkNjMxYTQxOTY3MDkzNjcyNjE4YzMzYjRkZTY0YjBiOTVhOTYzYjMyNTFkMGUyNzY0MGRlNGVkZGI4M2U5ZWIxZmI5ZDQxMjEwM2JmOGM2OWYzYjE0YTVmYzI2YmZmNjU2MGE3OTQ4NDAwNjJlZGViM2QzOWJiMWExODA4MTFlMWU0MzcwYjgyZmYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjEyNWFmOWYwZGJhNzM2NDNmNDQwZWQ1NjNjMWMwNDBlN2I4YWFkYjFlMjE0ODJiYzJlODk3NjBmODNmMzE0NGIiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiYTE2NGNmNmFkOWM5NjhmMzFkMmE3NWRjNzRhYTBkMzcxYTljZmMzNzlkZDc4ODRkYzFkMzlhN2FhN2NhOTBjNmYyZGZjNWJhNTI3YWRjZWQyODVlOGY2YWUxYWYxMDYxNDhkOGZiZWIzMWRjYjE3YTViNzVjYjJjNzk3NmRlNzhbQUxMfEZPUktJRF0gMDIzMTMwNGM1YzMzZmU3MTFhMDRkMTU0NmY4ODFmNmNhYTFlZDk1MmNhNTkwN2JkZTExZjk3YmRiMWJkMTg3M2RhIiwiaGV4IjoiNDFhMTY0Y2Y2YWQ5Yzk2OGYzMWQyYTc1ZGM3NGFhMGQzNzFhOWNmYzM3OWRkNzg4NGRjMWQzOWE3YWE3Y2E5MGM2ZjJkZmM1YmE1MjdhZGNlZDI4NWU4ZjZhZTFhZjEwNjE0OGQ4ZmJlYjMxZGNiMTdhNWI3NWNiMmM3OTc2ZGU3ODQxMjEwMjMxMzA0YzVjMzNmZTcxMWEwNGQxNTQ2Zjg4MWY2Y2FhMWVkOTUyY2E1OTA3YmRlMTFmOTdiZGIxYmQxODczZGEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImIwZTdmY2VjMTY2YjRlNDc3ZTllYjE3NWY5MGNlMDFkYTFiYjg0OWY5Y2ZkNDEyZWY0Yjc1NTkxOTAxODkyZGEiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiMGQ4YjQ0N2RkNmFmMTlmNjE4MzRmNzk5NWNmMmY1OWVmZTIyNTg0MmI1YzdiYzg4MDUxZjIwNGNhNzFmZDk5MDRlNDFjMTVlNGU4NjcxMmRmZDdiYzRjMzgwMGYzOTQ4YjY4MGE3NmRjNjA5ZTJkYjE2ZTY3YTY4NjE4ZWE5ZTZbQUxMfEZPUktJRF0gMDNjNGI3MzVhZjhlMDQ4OTdmZTA1MTg3ZjI0ZDNkMmI3NDM1NDk0MWEzNDJjYzI2NzYyYmZkNTk1MjM5ZGJhNWJiIiwiaGV4IjoiNDEwZDhiNDQ3ZGQ2YWYxOWY2MTgzNGY3OTk1Y2YyZjU5ZWZlMjI1ODQyYjVjN2JjODgwNTFmMjA0Y2E3MWZkOTkwNGU0MWMxNWU0ZTg2NzEyZGZkN2JjNGMzODAwZjM5NDhiNjgwYTc2ZGM2MDllMmRiMTZlNjdhNjg2MThlYTllNjQxMjEwM2M0YjczNWFmOGUwNDg5N2ZlMDUxODdmMjRkM2QyYjc0MzU0OTQxYTM0MmNjMjY3NjJiZmQ1OTUyMzlkYmE1YmIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjIzZWUyOWM1ODc2Nzg0YzE3NTkzMTVjYTcxNmRkNWI5MjY1YzUyYTExMzc1ZTZiMWYyMmM1OGM3OGRlOWM4MzIiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMTJmNmU1YmNmMDJiMTJiNGEwYmNkZGNiMWZkNmVjMjE1ZTBkNTdmMTJlMWIzNTYzODEzNDM5MDAwYTNjZGQ5N2VhNjVmNGYyMzNlM2ZiYzA4OTQ5ZGQ1NzQxNjA5OGQ0MzcwYWQzZDQ1OGY2NGE3ZDczMmUzNDc3YzNhNjVkMThbQUxMfEZPUktJRF0gMDI4N2E1ZWU3NmExYTc2MGMxOTZhYTYxMGI3ZGRlODA1YmMyZWNhMGM0YjRmMzA1MjRiYjFjYTEwMDQ3OGJkY2NkIiwiaGV4IjoiNDExMmY2ZTViY2YwMmIxMmI0YTBiY2RkY2IxZmQ2ZWMyMTVlMGQ1N2YxMmUxYjM1NjM4MTM0MzkwMDBhM2NkZDk3ZWE2NWY0ZjIzM2UzZmJjMDg5NDlkZDU3NDE2MDk4ZDQzNzBhZDNkNDU4ZjY0YTdkNzMyZTM0NzdjM2E2NWQxODQxMjEwMjg3YTVlZTc2YTFhNzYwYzE5NmFhNjEwYjdkZGU4MDViYzJlY2EwYzRiNGYzMDUyNGJiMWNhMTAwNDc4YmRjY2QifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImM2MTMyYWQxZDUzYjhjZWI2MTY0ZjkzMDFlM2ZjYjRkNTg2MjY4MzcxYWY2NGYxODQzZTNhMTY2ZWU0M2VlYjMiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiOWI1YTM4MDVlN2I3YmM1MGFmYzMyMzgzZTA2ZTk5MmM0YjFiMGYxNjdiYjNlM2ExZGNlNmIzZmJjMmM3ZmRjMTJkYmZiMjcwYjUyY2NjMDAxODQwZjQ4YjAzY2Q5Y2YzNjgwMmM0NTkxMzBjMzZlZGFkMWFkYmYyNDY2OGYwMzRbQUxMfEZPUktJRF0gMDIyODMxMjNiYzRmNDMyZGExMDIxOGQ2NzVlNTg2MmUxMjljZDAwMzI1YTNhOTY4Yzk0ZDc0M2U0Zjc0NTg2ODc1IiwiaGV4IjoiNDE5YjVhMzgwNWU3YjdiYzUwYWZjMzIzODNlMDZlOTkyYzRiMWIwZjE2N2JiM2UzYTFkY2U2YjNmYmMyYzdmZGMxMmRiZmIyNzBiNTJjY2MwMDE4NDBmNDhiMDNjZDljZjM2ODAyYzQ1OTEzMGMzNmVkYWQxYWRiZjI0NjY4ZjAzNDQxMjEwMjI4MzEyM2JjNGY0MzJkYTEwMjE4ZDY3NWU1ODYyZTEyOWNkMDAzMjVhM2E5NjhjOTRkNzQzZTRmNzQ1ODY4NzUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImU4YTVlOGM2ODE2NjFhMzk2ZmU2ZTJiYjQyNDQzZDRkMDUxNDI3YjMxYmRmZTE0ZjQwZmYzYjcwNWRkNDUzZjAiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiMTY2N2ZmMmFlODUxOWFlNTU4ZDhmNTI1NjkxMmEwNWRiMTA0ZGZkMDI3NmVmZTgxN2Q4M2NkNTRkOWFiOTM2NDgzOWQxODhmMDUzNzEwYmQyNjI5NGRhNGVkNDE5NGQ3OTg1OTNmOWU3ZmQ0NjFiOTgzN2U4ZGViOTAwNmZmNGNbQUxMfEZPUktJRF0gMDNjMjczOGMyMDk4NTcyZjU0NmQxM2M1YjUwMmJlZTA5NGQyNjhhZmRiNTBlODFlOTQ3Y2ZmMTg1N2M0ZWU2NWQ4IiwiaGV4IjoiNDExNjY3ZmYyYWU4NTE5YWU1NThkOGY1MjU2OTEyYTA1ZGIxMDRkZmQwMjc2ZWZlODE3ZDgzY2Q1NGQ5YWI5MzY0ODM5ZDE4OGYwNTM3MTBiZDI2Mjk0ZGE0ZWQ0MTk0ZDc5ODU5M2Y5ZTdmZDQ2MWI5ODM3ZThkZWI5MDA2ZmY0YzQxMjEwM2MyNzM4YzIwOTg1NzJmNTQ2ZDEzYzViNTAyYmVlMDk0ZDI2OGFmZGI1MGU4MWU5NDdjZmYxODU3YzRlZTY1ZDgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjUwMjFiOGE4MmQ1ZjAzZDE3NDgxMzkxZjc5Yjk5MjJkYjcxOWU2NmM5NDM0MTc1YTQ4MzllMDNiM2ViYTM5NjgiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzNhODlhMDIyOGVkNjQ3OWM3ZmE4ZGVhYTJkMjBkMzE4YWMwNGYyZWIwNzJlM2Q2ZTBkOWRiZTg0Y2M1MmZiZjEyZmQxZjVkNWNhYWFhZWJhZjliYWU3NjBiYzdlMDAwNTg4YzllMjhiNGJjMzkyMDU1ZWE0MjE2OWZhMjg2MTVbQUxMfEZPUktJRF0gMDJmYTllZDBlMjg0ZWU0OTI4YmZhOGIyYmMyMDFkZjI5N2E3NGNhMzZmZmMwZDMwNjliNWI2Y2NiYTQyMWU1MDJmIiwiaGV4IjoiNDEzM2E4OWEwMjI4ZWQ2NDc5YzdmYThkZWFhMmQyMGQzMThhYzA0ZjJlYjA3MmUzZDZlMGQ5ZGJlODRjYzUyZmJmMTJmZDFmNWQ1Y2FhYWFlYmFmOWJhZTc2MGJjN2UwMDA1ODhjOWUyOGI0YmMzOTIwNTVlYTQyMTY5ZmEyODYxNTQxMjEwMmZhOWVkMGUyODRlZTQ5MjhiZmE4YjJiYzIwMWRmMjk3YTc0Y2EzNmZmYzBkMzA2OWI1YjZjY2JhNDIxZTUwMmYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijg0NDQyY2UwOWQwNTU2ZmNkMDE1MGU4MGE5MjA2MDM5YjAzOWM2NGZjMGM4MjQwZDUxZTRmODRmZmIwZjk3ZjIiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiOGQwYjcwOTc1Nzg1NTVkYjU2YmYzN2E5NzhlNDU3YzMyNGNkYTQ1MjBkOGFjOWIyZDBhMWU3OGY1ZWYxNTc1YTJkNzc1YTEzZjk1Y2M4OTE5M2ZlZDFjNjY2OWIwY2VmYjc4MTUwNDA2ZWM1YTA0YzE0ZTA3OTE5OGNlMmI3ZDdbQUxMfEZPUktJRF0gMDM2ODdkMWFhZTk1ZjNjZDA5YzQyOGJkODFiNzYyOGE5NjIwMjRjN2NkNjZlMWQ2NzRlZTFmZTJlMjFlZTZhYWQyIiwiaGV4IjoiNDE4ZDBiNzA5NzU3ODU1NWRiNTZiZjM3YTk3OGU0NTdjMzI0Y2RhNDUyMGQ4YWM5YjJkMGExZTc4ZjVlZjE1NzVhMmQ3NzVhMTNmOTVjYzg5MTkzZmVkMWM2NjY5YjBjZWZiNzgxNTA0MDZlYzVhMDRjMTRlMDc5MTk4Y2UyYjdkNzQxMjEwMzY4N2QxYWFlOTVmM2NkMDljNDI4YmQ4MWI3NjI4YTk2MjAyNGM3Y2Q2NmUxZDY3NGVlMWZlMmUyMWVlNmFhZDIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjRmNDU5Y2EwN2ExNGU3N2NlMjAxMmVkOGQ1YzEwODExOTNmYTY4NWMxZTc5OTU0ZjJiY2ZkN2QyODY0NzJjM2IiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiYzQ3ZGNmMDA1MjhkYTAxMmYwMWM1NTAwNWQ0YTM2ZTY0YjdjZWE5ZWMzN2M4ZmQwMWIzZGZmMGYwN2UyODRkYzNjOGQ3OWIwZGU2M2UwZjFkZmRjOTc3ODExZDM5MTY3ZDZkNzk2ZGMwZTZlNGFiZjExNjExZjRhOTI2Njk3NDVbQUxMfEZPUktJRF0gMDMyODIxZGIwMGM3MzllODNlN2QwYjVhMDlhNzUyNzg0OTUwZDlkYjU0ZjFiMjk4NjNlMWQwN2NjMjE0ZTI4ZTU4IiwiaGV4IjoiNDFjNDdkY2YwMDUyOGRhMDEyZjAxYzU1MDA1ZDRhMzZlNjRiN2NlYTllYzM3YzhmZDAxYjNkZmYwZjA3ZTI4NGRjM2M4ZDc5YjBkZTYzZTBmMWRmZGM5Nzc4MTFkMzkxNjdkNmQ3OTZkYzBlNmU0YWJmMTE2MTFmNGE5MjY2OTc0NTQxMjEwMzI4MjFkYjAwYzczOWU4M2U3ZDBiNWEwOWE3NTI3ODQ5NTBkOWRiNTRmMWIyOTg2M2UxZDA3Y2MyMTRlMjhlNTgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjY4YTRhZDQ5YzYyNjYzODQwNTA4ZWVlMjk4Mzk5NTc4N2I0OWMxNDQ5MDkyZjg5MDY5Y2FmZDBkZDRkNWI0ZGEiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiODg1MzVhOTVhMTIzOWU4ZGQzOGJiMTgwNjNiYjUyYjBmMTkxNDYyYzc4ZjY5YzVlNzgyZThkY2VjMTdkYzQ1NGVhNmUxZGI1NDBjMTg0MjQ4NzM1ZDRkODQ1YWQyY2M1NjEyMDEzYzE0NjgxNDU1NzJhZDUzZDIxZDAyZjljNzhbQUxMfEZPUktJRF0gMDJjZWZiYTNlOTkwZDY5ZDRiMTJjOTkwOGRiMjhkY2JiODdjZTRjN2Y2MmQxYTRmOWZhYzk5ODY4Y2U2ZDdkMTViIiwiaGV4IjoiNDE4ODUzNWE5NWExMjM5ZThkZDM4YmIxODA2M2JiNTJiMGYxOTE0NjJjNzhmNjljNWU3ODJlOGRjZWMxN2RjNDU0ZWE2ZTFkYjU0MGMxODQyNDg3MzVkNGQ4NDVhZDJjYzU2MTIwMTNjMTQ2ODE0NTU3MmFkNTNkMjFkMDJmOWM3ODQxMjEwMmNlZmJhM2U5OTBkNjlkNGIxMmM5OTA4ZGIyOGRjYmI4N2NlNGM3ZjYyZDFhNGY5ZmFjOTk4NjhjZTZkN2QxNWIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijk5ODEzYTI5MGZjZWJlZTgzMDdiNDJkYjA2MGZiNzcxNTdhMTc3YWQ1NWY3ZmFkMTAzNDkxMjAwNTRlMGRlMTUiLCJ2b3V0Ijo0LCJzY3JpcHRTaWciOnsiYXNtIjoiMzMwNWFiOTQ3Nzk0NGZhMDQ0MDliNmE5ZDAxYjI2NDdmMWM0MGZiMWEzNmZiYTI0NDNkMTMwZTRhYTRjY2VlNmYzNWU3MmY5YzBiMTU1ZDRjYTc3ODk5YTZmMGQ4MmQ3NjE4N2ZlMGU4Zjk2NzA0NGNlOTEyYzc0MGViMmE3MTNbQUxMfEZPUktJRF0gMDNmNjY1NDk3ODhlNWIzZGY5MTQwMWY4NDM1ZWQ2NjY0MzNlMjM5Nzg0NWQ0ZjJmMGNmNWZhMWZmMDFlOTcwZGNkIiwiaGV4IjoiNDEzMzA1YWI5NDc3OTQ0ZmEwNDQwOWI2YTlkMDFiMjY0N2YxYzQwZmIxYTM2ZmJhMjQ0M2QxMzBlNGFhNGNjZWU2ZjM1ZTcyZjljMGIxNTVkNGNhNzc4OTlhNmYwZDgyZDc2MTg3ZmUwZThmOTY3MDQ0Y2U5MTJjNzQwZWIyYTcxMzQxMjEwM2Y2NjU0OTc4OGU1YjNkZjkxNDAxZjg0MzVlZDY2NjQzM2UyMzk3ODQ1ZDRmMmYwY2Y1ZmExZmYwMWU5NzBkY2QifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImY2NTNkN2I3NzNhNmZjNWM5NWI4ZmU1OWJmYzUxYzMzY2M3ZWQxYjM2NTQ5MDlkYTVjMzBkNjJkMzc0ODM0ZGUiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiNjZiZTBiZWY2OGI2NDM1OTJlMzIyZjJiN2VmNWI5M2E0ZWU0NmE5ODI2N2Q3ZWNkN2U0NmNlOTk3MzE5NDkzYTYwMWIxZDJhNWM2NDgxMzI0ZTNmNjQ2YzUwYTZiNzI4MTRiNTNlZTliM2Y5MDk0ZGY2NmQyNzE2Y2E2YWVlYTFbQUxMfEZPUktJRF0gMDIxMjhhZGYzNWJlNjcwNjM5NmRhODlhN2I2NGFmODlmZmVjMGY5YmNjN2RmOTc5ZWVlMzU2ZDE1OTczOGVlMzE3IiwiaGV4IjoiNDE2NmJlMGJlZjY4YjY0MzU5MmUzMjJmMmI3ZWY1YjkzYTRlZTQ2YTk4MjY3ZDdlY2Q3ZTQ2Y2U5OTczMTk0OTNhNjAxYjFkMmE1YzY0ODEzMjRlM2Y2NDZjNTBhNmI3MjgxNGI1M2VlOWIzZjkwOTRkZjY2ZDI3MTZjYTZhZWVhMTQxMjEwMjEyOGFkZjM1YmU2NzA2Mzk2ZGE4OWE3YjY0YWY4OWZmZWMwZjliY2M3ZGY5NzllZWUzNTZkMTU5NzM4ZWUzMTcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjQ3NDVhYjdjODlkNTcwZTRkYjEwZjBmMmM5MjEyZDQ0OGQyNjg5MDA3OGY1YzdkMDQ3ODFmYzJiNmU1OTllZmMiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiY2IyYzdkOTQ4ODFjOThjZDJhYTJiOTVhOWQyZjE3N2RhMjIzZmNlMWFlN2QyZTU0NTM1YTUzZWM5NjQwNDMzZDQ2OGEyNjM1MzE2MWQ1MGMwZDRhNTdiYTcyZDJlNTBiN2M5MDA2ZTllZTAxODU1Y2MzMzdhNDc5MmRkNjAyNjdbQUxMfEZPUktJRF0gMDIyMWQ0NzVlYjIyMzg1OWEzZWEwYTliMTNiY2I4NzQ4NDM5ODNkMTI2ZDc4ZDNmZWMwNmIyNmFmNDQ5NmE2MTRhIiwiaGV4IjoiNDFjYjJjN2Q5NDg4MWM5OGNkMmFhMmI5NWE5ZDJmMTc3ZGEyMjNmY2UxYWU3ZDJlNTQ1MzVhNTNlYzk2NDA0MzNkNDY4YTI2MzUzMTYxZDUwYzBkNGE1N2JhNzJkMmU1MGI3YzkwMDZlOWVlMDE4NTVjYzMzN2E0NzkyZGQ2MDI2NzQxMjEwMjIxZDQ3NWViMjIzODU5YTNlYTBhOWIxM2JjYjg3NDg0Mzk4M2QxMjZkNzhkM2ZlYzA2YjI2YWY0NDk2YTYxNGEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjJkOTljNWJkNjNlZWEwNzhiYTEyZDg5MWNhNjQ1NjcwYmU0YThmYzQ5OTkxMTJhODMwYTZmNDgyYTM4NWM4ZGMiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ0NWEyZGRlZjk3MzA3YTRiMGI5MzU3ZTg0NWI0MGNmYzAxOTliODc2NjI5ZmYxZDA4YWIzOWVkNDVhZDRlNzA4MzdmOTY4NjNmODUwZjQwYjk5OTI5NjVjNmMwM2ZjZjVkNTMxMTZjNDUzMTA2YzQwNWI1NzU5ZDUwY2Q1MDVbQUxMfEZPUktJRF0gMDMzMjRjZWFlODdkZjM3YmIzZjEwMTkwZWM2OTMzMjZhMTRjOTFkYjdiNTQzNzViNzQ5MmY0MTBmYjM1MmIwOGE4IiwiaGV4IjoiNDE3NDQ1YTJkZGVmOTczMDdhNGIwYjkzNTdlODQ1YjQwY2ZjMDE5OWI4NzY2MjlmZjFkMDhhYjM5ZWQ0NWFkNGU3MDgzN2Y5Njg2M2Y4NTBmNDBiOTk5Mjk2NWM2YzAzZmNmNWQ1MzExNmM0NTMxMDZjNDA1YjU3NTlkNTBjZDUwNTQxMjEwMzMyNGNlYWU4N2RmMzdiYjNmMTAxOTBlYzY5MzMyNmExNGM5MWRiN2I1NDM3NWI3NDkyZjQxMGZiMzUyYjA4YTgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImU4NWE0N2Y1NmJkNWFmYTExMzY5OGQ1MGI1NjM4NmFmNzViMzMwMDdiNWUwNDI4ODIxN2I1NjRkODI3NzhmOGIiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNmVhNDc1ODRiNzdhMmFkMTM5ZjliNDBlYWQ0ZGM1MDQwNDU2YTg4NzNiYWM2YzFhYjRhNmQ2YTA3NWRiYTM0MGMzMTZjZmViZTFkNmFhZmU4NjIwNzI3ZDA1NDQ1OWM2MWY1NDVjZmY5OTQ5NzQ5NzVkZDc0MmVmMWM1MTU4YjJbQUxMfEZPUktJRF0gMDMzOGVkMmQ0NjcwMzU3MGY3NjQ0MTdlMmE3ODE0ZTQwZmUyMTQ1YWI0NDMzYjIxYzAwNzE3ZGY2YjZhMDk5YWExIiwiaGV4IjoiNDE2ZWE0NzU4NGI3N2EyYWQxMzlmOWI0MGVhZDRkYzUwNDA0NTZhODg3M2JhYzZjMWFiNGE2ZDZhMDc1ZGJhMzQwYzMxNmNmZWJlMWQ2YWFmZTg2MjA3MjdkMDU0NDU5YzYxZjU0NWNmZjk5NDk3NDk3NWRkNzQyZWYxYzUxNThiMjQxMjEwMzM4ZWQyZDQ2NzAzNTcwZjc2NDQxN2UyYTc4MTRlNDBmZTIxNDVhYjQ0MzNiMjFjMDA3MTdkZjZiNmEwOTlhYTEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc2MzNiNDgwMTE3OTRjNTAxOTYyNWY3OWU0ZjQ2MWUxN2Q2N2RiZGFmODAxNGMwNmFiNDc2YmRhY2U5NTk3ZDciLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiOGU2YWQ3MzFiYzJjYTNiMjUzMGU2MDUxYTZiOTFjZGM4NGU5ZmFiYjFlN2Q4NTEwZjFhMWM2MTMwYzA2MmE1MmQzNGRkMmZmYjIxMjJlZGJlYmRiZTE0OGM1MmI3YmI2YmNjZTEzODBkYjY3ZjdmNjcxNTA0ZjhhNzJkNmM3ODhbQUxMfEZPUktJRF0gMDMzMjRjZWFlODdkZjM3YmIzZjEwMTkwZWM2OTMzMjZhMTRjOTFkYjdiNTQzNzViNzQ5MmY0MTBmYjM1MmIwOGE4IiwiaGV4IjoiNDE4ZTZhZDczMWJjMmNhM2IyNTMwZTYwNTFhNmI5MWNkYzg0ZTlmYWJiMWU3ZDg1MTBmMWExYzYxMzBjMDYyYTUyZDM0ZGQyZmZiMjEyMmVkYmViZGJlMTQ4YzUyYjdiYjZiY2NlMTM4MGRiNjdmN2Y2NzE1MDRmOGE3MmQ2Yzc4ODQxMjEwMzMyNGNlYWU4N2RmMzdiYjNmMTAxOTBlYzY5MzMyNmExNGM5MWRiN2I1NDM3NWI3NDkyZjQxMGZiMzUyYjA4YTgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjRjMWZiYzQ2Y2VlMGI5NjFmODc5YTI1YWI5Y2NkZTIxZTQzNjAyNDZjMTA2N2RjYmFlNWVmMGY5MTgwNjQzYzIiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiZWMwY2JkM2E5ODdlYzI3ZmQyZjY4ZjIyNjk3ZGE4YWZkYzhhMmI2ZTFjYTBmMmM5NTI2M2M2YmUxMWE5ODI4ZmUxODRiMjg2OWM4MGJkZTUyNWQ1MWExNDE3ZTU5ZmQxMzUyYzY4M2M1OTU1MDBmYzI3MTNmNDZhMjI2NzUzYTFbQUxMfEZPUktJRF0gMDM2Y2UwNTdkNzcyNDg4YzFlYmQyMDY4ODRjODg0NDMzZGEwNzllOGNmOWNhOTA5NThkYzhlMTBkZjM2YTY5NDM5IiwiaGV4IjoiNDFlYzBjYmQzYTk4N2VjMjdmZDJmNjhmMjI2OTdkYThhZmRjOGEyYjZlMWNhMGYyYzk1MjYzYzZiZTExYTk4MjhmZTE4NGIyODY5YzgwYmRlNTI1ZDUxYTE0MTdlNTlmZDEzNTJjNjgzYzU5NTUwMGZjMjcxM2Y0NmEyMjY3NTNhMTQxMjEwMzZjZTA1N2Q3NzI0ODhjMWViZDIwNjg4NGM4ODQ0MzNkYTA3OWU4Y2Y5Y2E5MDk1OGRjOGUxMGRmMzZhNjk0MzkifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImFjYzFjZjY2Y2Q0ZTVkMTEzNTcyYWY2YzA0Y2QxYWM1M2I5ZmE4ZmE5MzViODdmZjFhNjYyNDM1OTNjYjM5YjYiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiNzBkYzA3YzdjZTJiZjRiMGU3YWY5YWU2NmZkZDk3ZWQ3Yjc2MDhmZGQxZmVhM2I2N2E0ODhlMGU0MDQ3NmM0OTU2MDIxNTFlM2ViODgwNTJkMjIxMWYwYzNkYzFjMTA2NWNjZGI3ZjlmNjdiMWUwMjUyM2VlZDY5Zjk1YmIzOTZbQUxMfEZPUktJRF0gMDM2ZGMxMWI4MjMwNjM2YTA3OTE2OWFhN2Q5M2Y2MDA2NzdkYTA0MDgzZWQzMDk0YmU1YzQxNjVmNGMxMDVmZjgzIiwiaGV4IjoiNDE3MGRjMDdjN2NlMmJmNGIwZTdhZjlhZTY2ZmRkOTdlZDdiNzYwOGZkZDFmZWEzYjY3YTQ4OGUwZTQwNDc2YzQ5NTYwMjE1MWUzZWI4ODA1MmQyMjExZjBjM2RjMWMxMDY1Y2NkYjdmOWY2N2IxZTAyNTIzZWVkNjlmOTViYjM5NjQxMjEwMzZkYzExYjgyMzA2MzZhMDc5MTY5YWE3ZDkzZjYwMDY3N2RhMDQwODNlZDMwOTRiZTVjNDE2NWY0YzEwNWZmODMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImY5OGIyMDMwMWQ2MmNmMmQ3YTQxOTU1MzQxOWMxMzQxZmE4Y2U4ZDc3NjYxZDFhNzVmNTI5MmFiNjA2NmNlMzkiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiY2FlODgzNTUwNWZkN2Q5YzBhYWI3NjFmNDM5YjBjZGY4NGZhYzczMzc0ZWFiOGUzNmI1M2U4NjY1YmM4YTQ2NWY2MTNhMDBlOThhOTZmOGIyMGVjOWU5ZTYxNzUwODkwOGRjMWEwMGUwYzAzNzZlOGY0MGNhYjQwOWFmNzFmYTJbQUxMfEZPUktJRF0gMDI1ODVlZmY5YjVjNTU5ODliNzU2NzBlNGRmMDEyNjU0ZTQzMzI5ZmQyZmU5NjE1MWRjNDMwNmQ1ODBmNDQ1NmIxIiwiaGV4IjoiNDFjYWU4ODM1NTA1ZmQ3ZDljMGFhYjc2MWY0MzliMGNkZjg0ZmFjNzMzNzRlYWI4ZTM2YjUzZTg2NjViYzhhNDY1ZjYxM2EwMGU5OGE5NmY4YjIwZWM5ZTllNjE3NTA4OTA4ZGMxYTAwZTBjMDM3NmU4ZjQwY2FiNDA5YWY3MWZhMjQxMjEwMjU4NWVmZjliNWM1NTk4OWI3NTY3MGU0ZGYwMTI2NTRlNDMzMjlmZDJmZTk2MTUxZGM0MzA2ZDU4MGY0NDU2YjEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjE2OWZmOTA1MjgxZmUyM2ZhZTI3ZTk0ZWI2YjY0OWJjYTg2YzI5NWEzMTJlNmU0MTUyNDgyN2E3Y2Y3NGRmOGMiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiZjI5ZWFmODgzYjcxMTYzODc4MDQ4NjA0MDBhNWRlYjYwZjA2ZGQ5MmYxOGM3ZDI3NDNiNTNjZWMyNjM2ODlmYzA1OTdlYTA3MDkyMWM3NDFiMzM4MmI0N2FiYzA4ZTA2YzZmMzdhZmIwOTYzZGQ3OThiNmM4MThlZGNlZmIxNzdbQUxMfEZPUktJRF0gMDM0MTE2MjM2N2JhMDA4YmY5M2RjZjJhMGU3M2MzYjZmNzljMjRiNmNhMDE2YjYyOTM5MzAxN2NhNjBjOWU2NzY3IiwiaGV4IjoiNDFmMjllYWY4ODNiNzExNjM4NzgwNDg2MDQwMGE1ZGViNjBmMDZkZDkyZjE4YzdkMjc0M2I1M2NlYzI2MzY4OWZjMDU5N2VhMDcwOTIxYzc0MWIzMzgyYjQ3YWJjMDhlMDZjNmYzN2FmYjA5NjNkZDc5OGI2YzgxOGVkY2VmYjE3NzQxMjEwMzQxMTYyMzY3YmEwMDhiZjkzZGNmMmEwZTczYzNiNmY3OWMyNGI2Y2EwMTZiNjI5MzkzMDE3Y2E2MGM5ZTY3NjcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImIwZTdmY2VjMTY2YjRlNDc3ZTllYjE3NWY5MGNlMDFkYTFiYjg0OWY5Y2ZkNDEyZWY0Yjc1NTkxOTAxODkyZGEiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiZGJjYTc1OTVhYmZhMzY5MTA4YTgzMjc4NWZjMTY4OTYxMWY3MTllMGJmZDQ1NzNlYTNjZmQ4MjI2NzQ0ZmYwYjk1ZmZmMTY1MWNiYjMyNDI0MmE4Y2E1YzliZWYyMzM0OWQ1MzhjYWFkNjdlMmYwNGRmNjI5Yzg0M2FjODIxZDdbQUxMfEZPUktJRF0gMDI3YTg3NDMzMTNmN2MyZjRhODlmMGFkNTBlZjU0YjEwODcxOWUzN2NmZDRkYWY2N2JlZjM4NGRjNjZmMGI2ZDJhIiwiaGV4IjoiNDFkYmNhNzU5NWFiZmEzNjkxMDhhODMyNzg1ZmMxNjg5NjExZjcxOWUwYmZkNDU3M2VhM2NmZDgyMjY3NDRmZjBiOTVmZmYxNjUxY2JiMzI0MjQyYThjYTVjOWJlZjIzMzQ5ZDUzOGNhYWQ2N2UyZjA0ZGY2MjljODQzYWM4MjFkNzQxMjEwMjdhODc0MzMxM2Y3YzJmNGE4OWYwYWQ1MGVmNTRiMTA4NzE5ZTM3Y2ZkNGRhZjY3YmVmMzg0ZGM2NmYwYjZkMmEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijk5ODEzYTI5MGZjZWJlZTgzMDdiNDJkYjA2MGZiNzcxNTdhMTc3YWQ1NWY3ZmFkMTAzNDkxMjAwNTRlMGRlMTUiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiODFkN2JiMmFiM2E5OTBhZDU3MmZkZDVhNzVmZDg0ZmFlMjYwMWZhNzQwMWU0MTU1NGZiNGJjZTFlZDYwNzFhZjljN2MwMmI4NTcwZWFiMmIyN2Q5ZjI1MjU2ZDI0MjQ3NGJkNWQ4NjNiODc0NmQ1YjkzMjRmMjRiZmQ0NzRlNjBbQUxMfEZPUktJRF0gMDMwOTYwNTAxY2QzYjc2NzVhMzI4ZDI2ZTVmZTdmZTMxM2E0N2YzMmRiY2I5MDJkZTgzMjNjMTIwY2E1MmZmMGIyIiwiaGV4IjoiNDE4MWQ3YmIyYWIzYTk5MGFkNTcyZmRkNWE3NWZkODRmYWUyNjAxZmE3NDAxZTQxNTU0ZmI0YmNlMWVkNjA3MWFmOWM3YzAyYjg1NzBlYWIyYjI3ZDlmMjUyNTZkMjQyNDc0YmQ1ZDg2M2I4NzQ2ZDViOTMyNGYyNGJmZDQ3NGU2MDQxMjEwMzA5NjA1MDFjZDNiNzY3NWEzMjhkMjZlNWZlN2ZlMzEzYTQ3ZjMyZGJjYjkwMmRlODMyM2MxMjBjYTUyZmYwYjIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjliZjQ4YTE5MDFhYWYwZGI2MjNhYjQ0ODI4MTRhNTEzZmQ0NTBiM2I0NTBkOTdjOTMwYTk3M2MzNjgyMTdmMDYiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiZWZiMzYxMWFmZGUyZjllZWVkYTczOGUzMDFiMTYyMGY1NDYxYThkMmQzODRjMzZmNTAzYWEwNGU1YjIzM2I5ZmU1ZjRiMmQzM2JhM2ZmZTEwNjYwYzVhMmU2ZmEzMTM2NmRkYzRmODMzOWJkMGVmZDk1NDJjYmJkODZlYjBmY2NbQUxMfEZPUktJRF0gMDNhOTNiNDYxNWM1ZGU2N2VjZTA4MTRkNzRmMTI5ZWU1Mzk1ZGY3MWVkNWJkMjIxNDc1NzQ0YjljMjM3ZmUyY2UyIiwiaGV4IjoiNDFlZmIzNjExYWZkZTJmOWVlZWRhNzM4ZTMwMWIxNjIwZjU0NjFhOGQyZDM4NGMzNmY1MDNhYTA0ZTViMjMzYjlmZTVmNGIyZDMzYmEzZmZlMTA2NjBjNWEyZTZmYTMxMzY2ZGRjNGY4MzM5YmQwZWZkOTU0MmNiYmQ4NmViMGZjYzQxMjEwM2E5M2I0NjE1YzVkZTY3ZWNlMDgxNGQ3NGYxMjllZTUzOTVkZjcxZWQ1YmQyMjE0NzU3NDRiOWMyMzdmZTJjZTIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjNkMDk5YjBmNmVlOGMwZjY4Zjg2NmExNmIxMTI3NTcyODRlMjdlNzI2NDg2MmJmYWNhNWQ1ZmE1OWY5MTVmMWEiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMGI5MDZiZjA3Y2FjMDU0Y2I2ZmQ1OGZmMmE0MzE0ZDA5Nzg3NDViNmRkMWQwMzA5ZDM5Y2Y2OGM2MmY2ZDU2NGY5Njg3ZTFmYjY5YjljOWM2M2IxZTU1NzI1MjlkNjg1YmI5MmI1ZmI0OTc0NDFlNmFjOTJiMzkwYWQ2MGMxNTBbQUxMfEZPUktJRF0gMDM3MjllZWRlMThmMzU2ZTc3YmYxODIxYTUxZmUxNTEzZDU3YTMxYTA4ZWM0OTMzYjlmZjhkZGUwOThhOWZlODJiIiwiaGV4IjoiNDEwYjkwNmJmMDdjYWMwNTRjYjZmZDU4ZmYyYTQzMTRkMDk3ODc0NWI2ZGQxZDAzMDlkMzljZjY4YzYyZjZkNTY0Zjk2ODdlMWZiNjliOWM5YzYzYjFlNTU3MjUyOWQ2ODViYjkyYjVmYjQ5NzQ0MWU2YWM5MmIzOTBhZDYwYzE1MDQxMjEwMzcyOWVlZGUxOGYzNTZlNzdiZjE4MjFhNTFmZTE1MTNkNTdhMzFhMDhlYzQ5MzNiOWZmOGRkZTA5OGE5ZmU4MmIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImY0M2I1NDRmNWU4ZmFmNjUxOGUzNzI0MzBkMDI1MGZlYmI4OTAyNjcxNjQyN2M4Y2U5MGM1YTRlYzk3MjYwM2QiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiYTNhNDZmMDAxMGRhOTMzODcxYTdmMmRiMjgzNjUxNzc5NWQzMWVlMWQwOWJjZDMwNTlkYTE0NTJjMjI4NDAwYWIyNzA1OGRjYTVkNjI3ZDVjNmVhOTJiY2E1ZmI0MjhiMjQxMjJhNjQ1NmU1MjllMTQwNzRjYWEzNjEwNzU4NGZbQUxMfEZPUktJRF0gMDI0YTUwMzM1MGI5NjJhNjA2NzAxYzFjNGRmMDgzNTM0M2Y4ODIwNmVlZjU0NGE4MDdjNjQ4MjUxYjc0N2YyOWY2IiwiaGV4IjoiNDFhM2E0NmYwMDEwZGE5MzM4NzFhN2YyZGIyODM2NTE3Nzk1ZDMxZWUxZDA5YmNkMzA1OWRhMTQ1MmMyMjg0MDBhYjI3MDU4ZGNhNWQ2MjdkNWM2ZWE5MmJjYTVmYjQyOGIyNDEyMmE2NDU2ZTUyOWUxNDA3NGNhYTM2MTA3NTg0ZjQxMjEwMjRhNTAzMzUwYjk2MmE2MDY3MDFjMWM0ZGYwODM1MzQzZjg4MjA2ZWVmNTQ0YTgwN2M2NDgyNTFiNzQ3ZjI5ZjYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjY1NTdiYmM3Yzg2N2IyMjhhNDZjMzJlNWRiMDYyZmRhYTkxNjQyZmI0NmRjMTVjYjJkMjhlZGVkZmQ3YWE1MDUiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiM2MwNGI1YWM5NzgyODY2OTI2MzY2MzZmZGViODJjYTFhYWYxYTkwODA0Mjg2YTEwZDAwZjZlM2ZmMTM1ODUzNDlhMDQzOWU2NTY4N2IxNDcxYjQzYmYyNzhkYjZmYjljNzI4ZmNjOGI4M2QxM2Y1YjEwN2IzOWE0MjQ1NmY2MGFbQUxMfEZPUktJRF0gMDNkNDcwOWJlZWRkNDYzMDdhMTllZTE5ZTZmNGI3M2IxZmRhZGE3NzA4MWMyNDg5ODc5NDY2MWM0MjdjMzUwNDRjIiwiaGV4IjoiNDEzYzA0YjVhYzk3ODI4NjY5MjYzNjYzNmZkZWI4MmNhMWFhZjFhOTA4MDQyODZhMTBkMDBmNmUzZmYxMzU4NTM0OWEwNDM5ZTY1Njg3YjE0NzFiNDNiZjI3OGRiNmZiOWM3MjhmY2M4YjgzZDEzZjViMTA3YjM5YTQyNDU2ZjYwYTQxMjEwM2Q0NzA5YmVlZGQ0NjMwN2ExOWVlMTllNmY0YjczYjFmZGFkYTc3MDgxYzI0ODk4Nzk0NjYxYzQyN2MzNTA0NGMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjcyNTMxYzQ3YTYzZGM3NzkxOWMxZWU0YTE1NzliNWM0NmE0M2ZhMTU0MzM0N2MwYzA0YjZiYWI2MDlkNTg3YjgiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiZDdhNDFmOTllNWNjYmM2MDY5YjQwYTAyYTI5YmI3ODBlZWY2Yjg4NTgwYzU2ZTI3YzcwNjU2NmUwNjVmMDk2YWVjZDY4OWQ5NGJlNTgzNDc0NDM2ZjM5YjRlNWE5NzI5ODhlMmM0MzQ3NGNjZTQ1NmViNGEzMzE5OTE4Y2ZkZTZbQUxMfEZPUktJRF0gMDMyZmI0NjliN2QxN2NhYzg0ZTI3ZDgxM2EwMDg3MDgwZjc3MTc0MWVlZjUzOTRhNGE2ZTUzNjViYzAyNmYxNWIyIiwiaGV4IjoiNDFkN2E0MWY5OWU1Y2NiYzYwNjliNDBhMDJhMjliYjc4MGVlZjZiODg1ODBjNTZlMjdjNzA2NTY2ZTA2NWYwOTZhZWNkNjg5ZDk0YmU1ODM0NzQ0MzZmMzliNGU1YTk3Mjk4OGUyYzQzNDc0Y2NlNDU2ZWI0YTMzMTk5MThjZmRlNjQxMjEwMzJmYjQ2OWI3ZDE3Y2FjODRlMjdkODEzYTAwODcwODBmNzcxNzQxZWVmNTM5NGE0YTZlNTM2NWJjMDI2ZjE1YjIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjliZjQ4YTE5MDFhYWYwZGI2MjNhYjQ0ODI4MTRhNTEzZmQ0NTBiM2I0NTBkOTdjOTMwYTk3M2MzNjgyMTdmMDYiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiYjVhOTBjNzUxOTM0ZWU4MGEyMzg1YTE1MGMyMDVlYzg0MTZkMGI1NTJkOGQzMjQzNGYwNmZjMDY0MzY4YmNmYjhhZTFiODY2YWE4ODA3NzRmZWQ1NGQwYTc5MDA3OGJhMGQzOGUzZWYzNmZlOTMyNTI5MjEzMDkzODA5YTA5ZWFbQUxMfEZPUktJRF0gMDNkNTFjNTIwNTkwYjhlNGM2ZmMzYzNmZGEwYzQ4YzkwYzllZjE4YTliYTk3OTkxNzE5OTc1NTRlNGIyNmE5OGYwIiwiaGV4IjoiNDFiNWE5MGM3NTE5MzRlZTgwYTIzODVhMTUwYzIwNWVjODQxNmQwYjU1MmQ4ZDMyNDM0ZjA2ZmMwNjQzNjhiY2ZiOGFlMWI4NjZhYTg4MDc3NGZlZDU0ZDBhNzkwMDc4YmEwZDM4ZTNlZjM2ZmU5MzI1MjkyMTMwOTM4MDlhMDllYTQxMjEwM2Q1MWM1MjA1OTBiOGU0YzZmYzNjM2ZkYTBjNDhjOTBjOWVmMThhOWJhOTc5OTE3MTk5NzU1NGU0YjI2YTk4ZjAifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjFiMzgxMmE2YzYxMTAxZTkyYjFlNmQ0YzEzNTE0ZWFmZjc4ZDVhYzAzY2VjMDUxM2ZkNjY5MzMyYzBiMzI0ODkiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMDZhYjllNzZjYmEzZmM2ZjA3ODAyY2JkYzVhMmEyNWI2NTAzZjY4ZTM5NDUwNWZlYTJkY2NkZGQwYWZkZjYxNDcwYmI1MGYzYzM0YmY4Yzc2NGJlZGNmMjU5YTJmMjJjMjQ4ZjFlMjM3MGU2M2MyNmMzNzUwM2NlZDY0ZGJkMzZbQUxMfEZPUktJRF0gMDIyY2ZlNGVlOGU1OWU5MDc1NWY5MjNkZTkxYThiYTU2NmJkYTFhMDk2MTNkZjIzNTlhNTRjZmM1NDBlNzlmZTEzIiwiaGV4IjoiNDEwNmFiOWU3NmNiYTNmYzZmMDc4MDJjYmRjNWEyYTI1YjY1MDNmNjhlMzk0NTA1ZmVhMmRjY2RkZDBhZmRmNjE0NzBiYjUwZjNjMzRiZjhjNzY0YmVkY2YyNTlhMmYyMmMyNDhmMWUyMzcwZTYzYzI2YzM3NTAzY2VkNjRkYmQzNjQxMjEwMjJjZmU0ZWU4ZTU5ZTkwNzU1ZjkyM2RlOTFhOGJhNTY2YmRhMWEwOTYxM2RmMjM1OWE1NGNmYzU0MGU3OWZlMTMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImM4N2RjZDlhMmQwNzhiNGE2MGRjNmE0YzkzMzY5ZjQ3M2NiNzA0ZTM5NDlkMjU3ZTU0MGUzOTVhOTg0OGYxZDMiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMmUwMzYzYWRiM2VhYzI5YWI5NDQ1OTM0MDU1ZmFlZWE1ZDM5ZGVjNjk5MDczN2FkMjdlYjE4NWYyODQ1ZTk1ZDg5NmEzYzA5Y2UwMDFkNjkzZGIxNGVlNzQ5Mjk4ZDNmOTNjOGQ2MzZlY2ZlMTg0MGZlODEwY2M4ODdmNjFlYmNbQUxMfEZPUktJRF0gMDM2YWFlNzFjMTY5NDllN2RlMjQ3MDNiZWM1NTE4NDQzNGI4Nzc1YTVlYWEwZGJkODMyYmYyZmU5MWQwZDc3NmUyIiwiaGV4IjoiNDEyZTAzNjNhZGIzZWFjMjlhYjk0NDU5MzQwNTVmYWVlYTVkMzlkZWM2OTkwNzM3YWQyN2ViMTg1ZjI4NDVlOTVkODk2YTNjMDljZTAwMWQ2OTNkYjE0ZWU3NDkyOThkM2Y5M2M4ZDYzNmVjZmUxODQwZmU4MTBjYzg4N2Y2MWViYzQxMjEwMzZhYWU3MWMxNjk0OWU3ZGUyNDcwM2JlYzU1MTg0NDM0Yjg3NzVhNWVhYTBkYmQ4MzJiZjJmZTkxZDBkNzc2ZTIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImVlNGFmNjMwMTBjOWYxMThlZGU2ZmQ3OThhNjZjYWE2MGJiZTFlNzgxZGY5ODVhNDU4OTJkY2I4OWIzMDM3MmEiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiNTM5MTlmMTMzNWFjOTk5YjM4ZGYyN2EwY2EyMWIyYjIyYmJjYjI5NzY5YTMxYmM5YTNlMTY4ODhiYmQ5M2E2YmMwNzIwYzVmMjMyY2U2OTk2ZjM4OGE1MDE4NGUzYzFjMDgyNDY0YzcxMjZkMzVkMGQyMjgxY2QxOWYzMzdlMDFbQUxMfEZPUktJRF0gMDMwZDkyMGExNGJiMzc3OGJkYjQwNDE4MzllNWNjNWNhMTc5YWU3OWVkYjg4ZGNiNTcyNDhlNjAzODYxZjg1NGZjIiwiaGV4IjoiNDE1MzkxOWYxMzM1YWM5OTliMzhkZjI3YTBjYTIxYjJiMjJiYmNiMjk3NjlhMzFiYzlhM2UxNjg4OGJiZDkzYTZiYzA3MjBjNWYyMzJjZTY5OTZmMzg4YTUwMTg0ZTNjMWMwODI0NjRjNzEyNmQzNWQwZDIyODFjZDE5ZjMzN2UwMTQxMjEwMzBkOTIwYTE0YmIzNzc4YmRiNDA0MTgzOWU1Y2M1Y2ExNzlhZTc5ZWRiODhkY2I1NzI0OGU2MDM4NjFmODU0ZmMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImZjODBlOWNhYTdlMDBlNzEwOWUxYzkxZDkxOTA0ZTE2NDFlNzI1MzczM2FhNjhjYzNhZDYwOGFmN2YwMjU1OWQiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiYTFmZDc2YzhmODNmYjU3ZTAzYTA3ZDk2YTM5Y2M1ZGYzYTExMjM2MjJhNzA2YjAyNDRmOTVjNzE0NDQ5NDg3YTQyMjRlM2YzNDZjNTU1ZTczOGY0ZDFjNmZjNTlhYWMyMTZmZDEyYjlkYmUwNTljY2M3YTFjNTM4MDEwNDRiOWRbQUxMfEZPUktJRF0gMDI1MjA5MGQ4ZTE0Yjc0YTk3NTFmYWRkY2JjMjMxY2IwMTMxZTA4Mzc5YjdiOTkzOThkMGNlNDY4MmUwNzAzNWZjIiwiaGV4IjoiNDFhMWZkNzZjOGY4M2ZiNTdlMDNhMDdkOTZhMzljYzVkZjNhMTEyMzYyMmE3MDZiMDI0NGY5NWM3MTQ0NDk0ODdhNDIyNGUzZjM0NmM1NTVlNzM4ZjRkMWM2ZmM1OWFhYzIxNmZkMTJiOWRiZTA1OWNjYzdhMWM1MzgwMTA0NGI5ZDQxMjEwMjUyMDkwZDhlMTRiNzRhOTc1MWZhZGRjYmMyMzFjYjAxMzFlMDgzNzliN2I5OTM5OGQwY2U0NjgyZTA3MDM1ZmMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijg2ZTNiZDI2ZWJlMDgxMWU0OGNjNDI5NTY2MmQyZDhiMTUwZjc3ODZhNjAxYzg4ZWZkZGRjNmZhNmVmZmJjMDEiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiY2ZmNmFhN2Q1MzMwZGRlOGY3MjgzZDNhMTIzZjY5OGQwZTA3ZTIxYTY2NTJjYTU2NjVlNmQ3ZTk3YTc5OWM5MWU3YWYxOTkxYTcxMTczYjYxYjIyY2U5ODkwMThlNmIzZDU4OGQ1MTFhYzY5ZTg2M2NhZjMxYzI0YzE2MGJjZDNbQUxMfEZPUktJRF0gMDNhMDQ1ZWI0ZWQ5ZmJmZGY3NzA1Mzc2MWUyZmNkYjk2NjczMGQwZGJmMTBkYTRlYTY3NzQwNDZhYjMxOWVjYWYwIiwiaGV4IjoiNDFjZmY2YWE3ZDUzMzBkZGU4ZjcyODNkM2ExMjNmNjk4ZDBlMDdlMjFhNjY1MmNhNTY2NWU2ZDdlOTdhNzk5YzkxZTdhZjE5OTFhNzExNzNiNjFiMjJjZTk4OTAxOGU2YjNkNTg4ZDUxMWFjNjllODYzY2FmMzFjMjRjMTYwYmNkMzQxMjEwM2EwNDVlYjRlZDlmYmZkZjc3MDUzNzYxZTJmY2RiOTY2NzMwZDBkYmYxMGRhNGVhNjc3NDA0NmFiMzE5ZWNhZjAifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImYyNmNjNjcwZmRjNWM5NzA3ZGM5NmQyNjUwMGI5ZWVlYWQzYzljODU4MjQxOTY2Y2E4NzZmNTI1ZWFhYTcyMWQiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMTVkZjljZDRhYjYyMDg5Mzc2MTgyZjUyODYxMjhlMjMzNmMxYjMzNzNjNzcyYTY4MTViN2M5ZGUzNTQzOGUwYzYzMTBlNTI3YzIzNmM4NmZlZThlOTRmMWIwZGJjNTVjNDc3YThiZWU0Y2FhOWYxODUxZDI2MzFjYzM1NzVjZjZbQUxMfEZPUktJRF0gMDI5MmU2MzNkYWQ3ODNjYTFjYzk4Njk0NzQ4ZjZkYTcxZmQwNjc1NDU1YTJkNGJkMGY4YTRlZjI2ZGMzMTdlNGM4IiwiaGV4IjoiNDExNWRmOWNkNGFiNjIwODkzNzYxODJmNTI4NjEyOGUyMzM2YzFiMzM3M2M3NzJhNjgxNWI3YzlkZTM1NDM4ZTBjNjMxMGU1MjdjMjM2Yzg2ZmVlOGU5NGYxYjBkYmM1NWM0NzdhOGJlZTRjYWE5ZjE4NTFkMjYzMWNjMzU3NWNmNjQxMjEwMjkyZTYzM2RhZDc4M2NhMWNjOTg2OTQ3NDhmNmRhNzFmZDA2NzU0NTVhMmQ0YmQwZjhhNGVmMjZkYzMxN2U0YzgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImFjYzFjZjY2Y2Q0ZTVkMTEzNTcyYWY2YzA0Y2QxYWM1M2I5ZmE4ZmE5MzViODdmZjFhNjYyNDM1OTNjYjM5YjYiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiMmY1YWZjZGI1MzVmNDhiZmViNmM3NmEyZmNmNzFlOWFmMDA1ZTVlYTcxMmYwMTFiZDUzZmZiZWY3Yzg0YTRjNzNhNGEwNDhkODUyNmY5ODBhMDRiMDIyZGZiMzc1ZDc3NzBmMjAxNjdlMDA5ZTcxMjUzZWYzZWUwYmEwNmJjMDJbQUxMfEZPUktJRF0gMDIzNmExNWZlNzljZGU5NjgyMWIyYjMxMTE4YjgzN2E3MzY3N2E4ODA1OGEwN2U3Mjk0ZmVmODZiNDNmZmVjMDlkIiwiaGV4IjoiNDEyZjVhZmNkYjUzNWY0OGJmZWI2Yzc2YTJmY2Y3MWU5YWYwMDVlNWVhNzEyZjAxMWJkNTNmZmJlZjdjODRhNGM3M2E0YTA0OGQ4NTI2Zjk4MGEwNGIwMjJkZmIzNzVkNzc3MGYyMDE2N2UwMDllNzEyNTNlZjNlZTBiYTA2YmMwMjQxMjEwMjM2YTE1ZmU3OWNkZTk2ODIxYjJiMzExMThiODM3YTczNjc3YTg4MDU4YTA3ZTcyOTRmZWY4NmI0M2ZmZWMwOWQifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjQyNzZiOGE3MmJlNzUwMDNjYmU1ODhlYThlODcwNjc2NjBkZWViZGNlZTcyZjg5YzU4YWZkZDA4NGRjYzcyNjYiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiYzA2NzZiNGY1MmM4OWZmOWM3OGU4NDg1MjdkYmQyNmQyYzA0ZmE5MzNiN2Y1OTkyM2Q4YjNjMDMwODcyNWY3MTg3YTk0OTc0MGVjNzMwOTNhNjZkZDVhY2M4YTMwNGY3ZDQ4ZDI1OWZmYzU4ZTI5Y2UzMmMzYTc0NWVlYmZhZTZbQUxMfEZPUktJRF0gMDMwMDEzYzMxYzMzYTRhNDY0Zjg4OTIwZDQ5ODIxNDJhNmMyYjkyNWMyY2MxMDcxYWFjNjc0MjQ1ZjZkZDU5N2UyIiwiaGV4IjoiNDFjMDY3NmI0ZjUyYzg5ZmY5Yzc4ZTg0ODUyN2RiZDI2ZDJjMDRmYTkzM2I3ZjU5OTIzZDhiM2MwMzA4NzI1ZjcxODdhOTQ5NzQwZWM3MzA5M2E2NmRkNWFjYzhhMzA0ZjdkNDhkMjU5ZmZjNThlMjljZTMyYzNhNzQ1ZWViZmFlNjQxMjEwMzAwMTNjMzFjMzNhNGE0NjRmODg5MjBkNDk4MjE0MmE2YzJiOTI1YzJjYzEwNzFhYWM2NzQyNDVmNmRkNTk3ZTIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjQyNzZiOGE3MmJlNzUwMDNjYmU1ODhlYThlODcwNjc2NjBkZWViZGNlZTcyZjg5YzU4YWZkZDA4NGRjYzcyNjYiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzY2MTQ5MmQxOTQxY2QxMTUzNDE2NTM0NmZhNGY4OGY4ZWZiZDEwMjU0NzAyOTkyOWE2ZGFjOTIwOWI1ZTMwYWVlZmM4ZDAzYTMwNWIwNTZkZDU4M2Y0ZGQ2MTFjMzAwYTg3YTQyOTUwNTQ2MmZiZWQ0ZDc5MzM3MDI3NzQ5MjRbQUxMfEZPUktJRF0gMDNmZWFjNzRjODU2NzZjMjdmN2MwMWVmMDcwNTY2ZGNhYjRhMjUyOTE3YWE1Y2UwMzVjZGRjNmExMmNkNzc3MmMzIiwiaGV4IjoiNDEzNjYxNDkyZDE5NDFjZDExNTM0MTY1MzQ2ZmE0Zjg4ZjhlZmJkMTAyNTQ3MDI5OTI5YTZkYWM5MjA5YjVlMzBhZWVmYzhkMDNhMzA1YjA1NmRkNTgzZjRkZDYxMWMzMDBhODdhNDI5NTA1NDYyZmJlZDRkNzkzMzcwMjc3NDkyNDQxMjEwM2ZlYWM3NGM4NTY3NmMyN2Y3YzAxZWYwNzA1NjZkY2FiNGEyNTI5MTdhYTVjZTAzNWNkZGM2YTEyY2Q3NzcyYzMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjA0ZDg3ODEzMTU4NDc0ZmQxNDYzNzRmNDI1MjMyNWMzNzMyZTQwN2NmNzBlZTlkNTdlMDAyM2EyMzkwYzViY2IiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiNjE4OWFmYzMwNDU0ZjIyNWM2NTViNTQ0YTc2YzIxNzlhODg4YjU3NDRkZTllMThmZGQ1MmVkZTVhMjkwZTRlYjMyZDFjZDgxZTZlNGM3OGRiMTAzOTJkOWNiZjllMzJmNzVlMmM0NzYzYmU4OWY3YTkzNDMxNjI0NDJmZmY0NDlbQUxMfEZPUktJRF0gMDIyMzdmYTJmZDkxODQzYTY4YTA0MDliM2IxZDMyZjJhODFjZWI0YTNkOWZkYTE2ZTM5MjNkMzQ1MDA5ZDdhZGQ1IiwiaGV4IjoiNDE2MTg5YWZjMzA0NTRmMjI1YzY1NWI1NDRhNzZjMjE3OWE4ODhiNTc0NGRlOWUxOGZkZDUyZWRlNWEyOTBlNGViMzJkMWNkODFlNmU0Yzc4ZGIxMDM5MmQ5Y2JmOWUzMmY3NWUyYzQ3NjNiZTg5ZjdhOTM0MzE2MjQ0MmZmZjQ0OTQxMjEwMjIzN2ZhMmZkOTE4NDNhNjhhMDQwOWIzYjFkMzJmMmE4MWNlYjRhM2Q5ZmRhMTZlMzkyM2QzNDUwMDlkN2FkZDUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjU2YTdkMjgxOTkwNTgwN2JiMTdmZGQxZTYwNTk2MTVhYzQ5OWM2OGYzMmE3MjZiZjNkMzA3NjBhYTgxNjRlNzciLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiNjM1NWUyNGYwZmI0MDMwNDM5ZmY1YzBiNzNmNDMxNDg5ZDZiYjZhZDY5NjhlMTk5Zjc3ODRiMzU1ZWFkNDU1NDJkMDY4M2VlMDIyZmY5M2JlODRkYjE0NjFlZDRiNDBmYWYzNmI1MjA2NzJiYTFjMDEzNjAyYTliODc2ZmY2ZmFbQUxMfEZPUktJRF0gMDM4NzUwYzA5N2RjZGU5M2EzMjY1NGMxZDAzZGRmZDFjZGQzMzVjZTZhYWE4ZWZiMzIzODQwM2U1MzcwMDcwMmNlIiwiaGV4IjoiNDE2MzU1ZTI0ZjBmYjQwMzA0MzlmZjVjMGI3M2Y0MzE0ODlkNmJiNmFkNjk2OGUxOTlmNzc4NGIzNTVlYWQ0NTU0MmQwNjgzZWUwMjJmZjkzYmU4NGRiMTQ2MWVkNGI0MGZhZjM2YjUyMDY3MmJhMWMwMTM2MDJhOWI4NzZmZjZmYTQxMjEwMzg3NTBjMDk3ZGNkZTkzYTMyNjU0YzFkMDNkZGZkMWNkZDMzNWNlNmFhYThlZmIzMjM4NDAzZTUzNzAwNzAyY2UifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImY0ODM2M2ZhZTYxMTAyNTg0MTVmMjNiNjhjNDUxY2ZhMzU3ZjI0MTgxNWI0NDM0Yzg3MzY3N2E3NzRjNjEzNzQiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMjUzZGU2MTQyOWJkYTU1ZGEzZmNlZmExNjYzMWY1MzczZTQyZjFhYTczNGFkY2NhNWE1Yjk4M2MxNWYxNmE5NDkyZWI4OGRiYjkzNjg2NWE3NzA2OTVkZmIwOGFjNTA2OWJhZTViMjExMDYyNzk4NmQzOGY2YzcyOTkyN2RmODFbQUxMfEZPUktJRF0gMDM4OTU4MjIwMDMwNTBkZjJmN2Y2ZTZkNjIxMTVhY2U3ZDM0ZTE3MDg2ZmU1ZmExMGZmZTAwMzVmMjUxMTExMDM3IiwiaGV4IjoiNDEyNTNkZTYxNDI5YmRhNTVkYTNmY2VmYTE2NjMxZjUzNzNlNDJmMWFhNzM0YWRjY2E1YTViOTgzYzE1ZjE2YTk0OTJlYjg4ZGJiOTM2ODY1YTc3MDY5NWRmYjA4YWM1MDY5YmFlNWIyMTEwNjI3OTg2ZDM4ZjZjNzI5OTI3ZGY4MTQxMjEwMzg5NTgyMjAwMzA1MGRmMmY3ZjZlNmQ2MjExNWFjZTdkMzRlMTcwODZmZTVmYTEwZmZlMDAzNWYyNTExMTEwMzcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImU4MDY1MjdhMjkyNzhlZTkzMTUxYjMwOTc5NzE3MDYwYzU2MjFlMGYxZDU2ZDFjOTI4YjcwY2IyYzM3NjkyNmMiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMjQ3MmE0MGU5ZmU0NzY1ZmE5MWYyZTJlY2YwZDQzNmIyMmQ0ZGNlMWEwZTU2OTRjZTcxOGUyMTMwNWM0ZThkMjM0YzM2MjQ2YTA4MDY0MzJjODI2NmQyYjM0NzRiZDMwYzExMTU2YjJhMzQ5ODA3ZGFmMjgyZjVlZWUxNDRkZjhbQUxMfEZPUktJRF0gMDI2NzA5MjdmZjYwMDFiYjA3ZjZjZDEyNTRmZmNiZGJkMTNiMWU1OGVhN2E2NGJjZGU3MTc0YWMwY2NlMzIzNDc3IiwiaGV4IjoiNDEyNDcyYTQwZTlmZTQ3NjVmYTkxZjJlMmVjZjBkNDM2YjIyZDRkY2UxYTBlNTY5NGNlNzE4ZTIxMzA1YzRlOGQyMzRjMzYyNDZhMDgwNjQzMmM4MjY2ZDJiMzQ3NGJkMzBjMTExNTZiMmEzNDk4MDdkYWYyODJmNWVlZTE0NGRmODQxMjEwMjY3MDkyN2ZmNjAwMWJiMDdmNmNkMTI1NGZmY2JkYmQxM2IxZTU4ZWE3YTY0YmNkZTcxNzRhYzBjY2UzMjM0NzcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjljODNhNzdjNmFmNDJkNzdiNDkyNmIyMTA2OTUyY2M4ZmI2ZTE2NzkzMjc0MmI5ODFhMmZlNTM2NGU2M2JlMjAiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzdmOTg4MTAxZDZmODE3ZjQxZDE5M2JkM2QzN2JmZmU4MDBiZGZiOGYyZTQzYzg5ZGRmYzczYzRjNTMxM2Q3OGJiNzVkNmRmMDhjYTliZjAwOWQ1YWEyZmNkZmExZTdkMzVlZTIyMzMyZWY3YjEyODdmMDcxZjM0YjkzMDIxMzZbQUxMfEZPUktJRF0gMDNmODdhNzRiNjRmMTljNWEzMzFjZjAyOTM2ODA2YTRlYmU2NzNjZGRhMzcxMmNhNjMxNjQ0NDRmZjVkZjMyN2FhIiwiaGV4IjoiNDEzN2Y5ODgxMDFkNmY4MTdmNDFkMTkzYmQzZDM3YmZmZTgwMGJkZmI4ZjJlNDNjODlkZGZjNzNjNGM1MzEzZDc4YmI3NWQ2ZGYwOGNhOWJmMDA5ZDVhYTJmY2RmYTFlN2QzNWVlMjIzMzJlZjdiMTI4N2YwNzFmMzRiOTMwMjEzNjQxMjEwM2Y4N2E3NGI2NGYxOWM1YTMzMWNmMDI5MzY4MDZhNGViZTY3M2NkZGEzNzEyY2E2MzE2NDQ0NGZmNWRmMzI3YWEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImNhOWZlY2EyZDUxZjY4NTFmZTFjZGI5NzBkNWEwNmMxNmVjM2IzODY4MThmMTFkMWU1MTlhOTZhNDBkZGMyMzQiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiNWM5YTQ0NTdmZGRmMzEzODExNTQ0ZWFjYjFiZmQ5ZTBmZWEzYzIwMzViNzE5Mzk3MGIwOTY3NzUzZWEyMWIzN2E3NDgwMjY1MjY2ZWM2MDNiNzFiODJjZWJjZTlkZTNkNTQxMjc0OTVmOTY4MWQxOTE0MzU4ZGQyNDA3NDRmOGZbQUxMfEZPUktJRF0gMDM4ZGE1MDY3OTY1ZDdkMzhhZmY3YTMyYWYxYjg4MjhjYjMyZDQ4YzFhOTE5NTg1MWIxNzdhYjc0NTlmZTU5MGVjIiwiaGV4IjoiNDE1YzlhNDQ1N2ZkZGYzMTM4MTE1NDRlYWNiMWJmZDllMGZlYTNjMjAzNWI3MTkzOTcwYjA5Njc3NTNlYTIxYjM3YTc0ODAyNjUyNjZlYzYwM2I3MWI4MmNlYmNlOWRlM2Q1NDEyNzQ5NWY5NjgxZDE5MTQzNThkZDI0MDc0NGY4ZjQxMjEwMzhkYTUwNjc5NjVkN2QzOGFmZjdhMzJhZjFiODgyOGNiMzJkNDhjMWE5MTk1ODUxYjE3N2FiNzQ1OWZlNTkwZWMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjVhNGVkMGI4NzI4YTc1NDcxNTU4M2M3MjAzZDZiOTE0MjZlZmM0ZjI4YmZhOTUyNDljOGFiNTUxMjA1YWY2MTciLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMGNmNDBiNTM0YjNkMWQ0NWZlOWM5ZjNkOWZiYzM2OTcyYzY0NWY1NmVlMWU3NmJkNDU4NGIzMDUyYWVjYzBkOTczZDA4YzE0ZTBmOGJjMWZkM2FmNzMzM2E3NDI2NDkxOGRiYzE4ZDRlMDNiNjBhNDlmZjBhNmY3YjZjNjI2YmRbQUxMfEZPUktJRF0gMDI3MWUwNzZlZWMzOTUzYTAyYzBlNzcxNWQ4NjhjZTI5OTRiNGJjNDc3NGZjNTE0ZGU2ZTZiNTRiMWQ0MTM1NjkyIiwiaGV4IjoiNDEwY2Y0MGI1MzRiM2QxZDQ1ZmU5YzlmM2Q5ZmJjMzY5NzJjNjQ1ZjU2ZWUxZTc2YmQ0NTg0YjMwNTJhZWNjMGQ5NzNkMDhjMTRlMGY4YmMxZmQzYWY3MzMzYTc0MjY0OTE4ZGJjMThkNGUwM2I2MGE0OWZmMGE2ZjdiNmM2MjZiZDQxMjEwMjcxZTA3NmVlYzM5NTNhMDJjMGU3NzE1ZDg2OGNlMjk5NGI0YmM0Nzc0ZmM1MTRkZTZlNmI1NGIxZDQxMzU2OTIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjUzYWUxM2IzMDgyY2Y1MmMxMWNhMDAzMmNmYjVhNWRlNWU3ZDRkMDNlYmQ2YzVhMjNmOWE3MWFlYTVhZTllNGYiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMGUxMmNmZmRmYTNmY2Q4ZGY2Y2IzNjMxNDhlOGIxYjBlYjBkNmI2MTUxMWJiNTcyNzZlM2ExYjYwYzM1ZDIyODcwNTUyNTA5MjU4NGY5NzcxN2FiMWM5ZDU0YmQwM2Q1NjI5OTgyZTExOTFiYzRmNDkzMTJlOTc5MDU5NTQ5NDBbQUxMfEZPUktJRF0gMDM2NjA5NzkzOGQ3MGUzY2ViMzE5OWU5MTcwZGM4MWI1MGNhMmZkMmU0YTEzMTYxZjc5NTI2NDFmY2FhYjFjMTlmIiwiaGV4IjoiNDEwZTEyY2ZmZGZhM2ZjZDhkZjZjYjM2MzE0OGU4YjFiMGViMGQ2YjYxNTExYmI1NzI3NmUzYTFiNjBjMzVkMjI4NzA1NTI1MDkyNTg0Zjk3NzE3YWIxYzlkNTRiZDAzZDU2Mjk5ODJlMTE5MWJjNGY0OTMxMmU5NzkwNTk1NDk0MDQxMjEwMzY2MDk3OTM4ZDcwZTNjZWIzMTk5ZTkxNzBkYzgxYjUwY2EyZmQyZTRhMTMxNjFmNzk1MjY0MWZjYWFiMWMxOWYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImQ4ODI0YjJhZGUzMjVlNmQ2Yzk2NTlkMzE4NGM0MDQ0ZmY3ZWM4Zjk4YWNjYzY4YmRjZTc2YTRiMmRiMzI0NzkiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiOTE1MTBlM2RkZjdkZjljOWY1NWEzN2I3ZjYzYjIzNTk1ZDc0NWViMTU4N2UyM2I1ZTRhYzdhNmJkZGE0OTQ5MmIyMGQ0YjhlNDdiODM2NmJjNTlkNTc4NGVkN2I4YmI1NGFjMzhiMDc5ZGYyNDA2ZDRjOTM2ODE1ZDg4MjZjMjJbQUxMfEZPUktJRF0gMDNjMjNiZmQzYmU3ZWJmODFkYjdhNjQ3ZGNlNTMyMzQ4NjE2NjZkOWNjYjIzNzUyODAzYzJiODk1NmQxOTk5N2M1IiwiaGV4IjoiNDE5MTUxMGUzZGRmN2RmOWM5ZjU1YTM3YjdmNjNiMjM1OTVkNzQ1ZWIxNTg3ZTIzYjVlNGFjN2E2YmRkYTQ5NDkyYjIwZDRiOGU0N2I4MzY2YmM1OWQ1Nzg0ZWQ3YjhiYjU0YWMzOGIwNzlkZjI0MDZkNGM5MzY4MTVkODgyNmMyMjQxMjEwM2MyM2JmZDNiZTdlYmY4MWRiN2E2NDdkY2U1MzIzNDg2MTY2NmQ5Y2NiMjM3NTI4MDNjMmI4OTU2ZDE5OTk3YzUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImY4NjQ5MmNkZTY3NmM2N2JiZWJhYmFlYzM1MjI0OGU1NWM2M2Y4OWVlNmRjYjdiNDFhNTcwNzFmODRlN2Q3MzUiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiNGZmNjdmYzI2NTcyOTZiNWRkMjU2NGFlZmM0NjA5NmExNDYwNzg0MmFjMDA1YTNlYTk4OTFiNDZkY2FhMWY4MmZmODk3OGM5NzI2MjJhNTZkN2M1MDNmYTFlMGY5ZDVjZDMzZjZjNDcyNzY0NTUwODk4ODI3ZWY0YTYxMGMxMTRbQUxMfEZPUktJRF0gMDNkNzY0NWNkM2MwYzk4NTM2MzUwZDYzNDE4OTg2NDZjMDE0NWNkZTA2ZGUwZWNlNDYyMjdhYzg4MTBmZmE0NjQxIiwiaGV4IjoiNDE0ZmY2N2ZjMjY1NzI5NmI1ZGQyNTY0YWVmYzQ2MDk2YTE0NjA3ODQyYWMwMDVhM2VhOTg5MWI0NmRjYWExZjgyZmY4OTc4Yzk3MjYyMmE1NmQ3YzUwM2ZhMWUwZjlkNWNkMzNmNmM0NzI3NjQ1NTA4OTg4MjdlZjRhNjEwYzExNDQxMjEwM2Q3NjQ1Y2QzYzBjOTg1MzYzNTBkNjM0MTg5ODY0NmMwMTQ1Y2RlMDZkZTBlY2U0NjIyN2FjODgxMGZmYTQ2NDEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImJmODYwZDNkMzEyNWNmNjE1NzZlYTJlMjIyMjhiNGM0NjNhZWYxYWQ0ZmIwZmQxZGUxNTE1YzJlYTBjN2E5MjAiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNGQyNTI1ZTA4MTY0ODA3NTk2YjdiOWUzNzk4M2I1YzVkZTNlNGJiZGQzZjMyNjcyNDEyOGRkNGFmZTUwOGFjY2NlZjM0YWU1MWQwZTczY2FhMDFmOTlmZDgyODExNDE2YzFkMmQwYjQxNzc2MzNmMWJiMWRmNmZhNDAwMjZlYWJbQUxMfEZPUktJRF0gMDMzNDA2Y2QyYTZmMjhjNTQ4ODMwZTQwN2EwNzkzOTA3YTA1M2RkMmYxYjliZDNlNjdhNTc1YmVkMzc5OTUxM2U0IiwiaGV4IjoiNDE0ZDI1MjVlMDgxNjQ4MDc1OTZiN2I5ZTM3OTgzYjVjNWRlM2U0YmJkZDNmMzI2NzI0MTI4ZGQ0YWZlNTA4YWNjY2VmMzRhZTUxZDBlNzNjYWEwMWY5OWZkODI4MTE0MTZjMWQyZDBiNDE3NzYzM2YxYmIxZGY2ZmE0MDAyNmVhYjQxMjEwMzM0MDZjZDJhNmYyOGM1NDg4MzBlNDA3YTA3OTM5MDdhMDUzZGQyZjFiOWJkM2U2N2E1NzViZWQzNzk5NTEzZTQifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImU2ODBkMDczN2Y3NjM3MmU5MGFhNDBmODI4MDI3NGEyNzZlMDkzMzk5MWQ5ZDllMzY4NTE0ZjdjMmM3NmI4NmEiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiOTNhNzgwNDk4M2VlMjg2NmE5NDM5ODI4ZTcyOTBmN2U5MDNjYTMwNDQyZWFlNjZhYmY3YzI0ODVhNTU5ZTVjMGMxNDdlNjg4Yzk3YTljZDljNzM5OTQ4YjUzNDQyYjFiZGZkZjU0YzAxOTI0ZTQ2YmMxZGUzOTQ3ZWE5NzE5ZGRbQUxMfEZPUktJRF0gMDMxNzY0NTUyNDRkZWU2ZDIxMDFiMzA4NDY3MGYzYmNkMzU2MDk2ODg5YTdhYjU1MDJiNjAzOWQ5MjQwNjI4MjM3IiwiaGV4IjoiNDE5M2E3ODA0OTgzZWUyODY2YTk0Mzk4MjhlNzI5MGY3ZTkwM2NhMzA0NDJlYWU2NmFiZjdjMjQ4NWE1NTllNWMwYzE0N2U2ODhjOTdhOWNkOWM3Mzk5NDhiNTM0NDJiMWJkZmRmNTRjMDE5MjRlNDZiYzFkZTM5NDdlYTk3MTlkZDQxMjEwMzE3NjQ1NTI0NGRlZTZkMjEwMWIzMDg0NjcwZjNiY2QzNTYwOTY4ODlhN2FiNTUwMmI2MDM5ZDkyNDA2MjgyMzcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjhhZGRiMTZjNDlhM2IzMTU0ODNkMWZiNWYwOTBmZTA4MTEzNWI4Nzc0MTQ2NzQ2NDgyZTc1MDNjNDVmYjhjMzMiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzBhOTg4NDE5ZGE4MWNmMDY1Zjk0MjQxNzAxZThiYzNmMzQ4MmUyMThiMGFkNDkwNWE0NDU2YjBkNzViNzU1YTk3MTA1MTg0MWM3NTQ0ZmUxZjY1NTMzNmNjYzZiOTM4NjE3YjA3YTFjYTcxYzQzZDU5OWJlNDcwZTAxOTE0YjFbQUxMfEZPUktJRF0gMDNmMTcyNDIyOTkxOWQxZWQzMWU3MmJmZGZiZWVmN2FkN2RmMTFkMjM5YTc4YWMwNjNkODA1ZDgzNDY1NTQ1NjQwIiwiaGV4IjoiNDEzMGE5ODg0MTlkYTgxY2YwNjVmOTQyNDE3MDFlOGJjM2YzNDgyZTIxOGIwYWQ0OTA1YTQ0NTZiMGQ3NWI3NTVhOTcxMDUxODQxYzc1NDRmZTFmNjU1MzM2Y2NjNmI5Mzg2MTdiMDdhMWNhNzFjNDNkNTk5YmU0NzBlMDE5MTRiMTQxMjEwM2YxNzI0MjI5OTE5ZDFlZDMxZTcyYmZkZmJlZWY3YWQ3ZGYxMWQyMzlhNzhhYzA2M2Q4MDVkODM0NjU1NDU2NDAifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjQuOTk3NTE1MjgsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAyNmJlNzdlY2EwNDgwNDdkYjE4NmY5MmM3ODQzNDdjODc2OWI0NWRmIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MjZiZTc3ZWNhMDQ4MDQ3ZGIxODZmOTJjNzg0MzQ3Yzg3NjliNDVkZjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcW50dWFsdjVweXFnbGQzc211amM3enJnbHk4ZHg2OW11dXByejhhMDkiXX19XSwiZmVlIjo3Ljc5OWUtMDUsImhleCI6IjAyMDAwMDAwMzcwNWQyZGEzNzI5MmU5Yjk0MmMxMDU2NDVlMWVjOGM2ZjQxMTVkOTk3YmNkOTI0M2JjYzI5NjFiODcxMDAxNDk3MDEwMDAwMDA2NDQxNmEyYTEwOTZmMjYzNzQ4OWUwOGViNGY0MjE2YzY3NTMwNDM1NWNlNTUzMGRkYzNiNjIzMTlhMzhiMWYwNjRmZDY5M2Q2Y2E0NjJmZjEyN2Y3NGQwNDk1NzM0MmZiZDNjOGNjMzQyODI5MmMxMjZhMzU1ZmI1MTE1NWViYTQ2MDU0MTIxMDM0YjM2NGMyOTYyOTZlOTExNTJkNzJiZDQxOGI2M2E0ZmZjYjYzYWI0OTZjMWIyYTE5MmZkOGZhM2ZiNmM2ZTgyZmZmZmZmZmZiYTVlMTE1YTRlOGM0MWZmZDAxNDUyYzQ5ZjY4NDMxY2JiNDIwZmUzMWE1MWViZWQzYmJiYTU3Y2YwNDI1NDdlMDAwMDAwMDA2NDQxNDg2YzAwN2ViMmQ4NDQxZWMxMTdjMGIwMThlYjU0ODY3YTg2MGQ3MDQxYTEzMTZiYTcwZTE1ZWNkNWI0YzhhMDEzNzE0YjhkZjMyZDFkMDYwNDE0MWMxZGRkMDc3YTJhMWYzMTc3NTk4NDc0MWMyZWMxZjNlOTliMzU3ZDVjNGE0MTIxMDJiNTczZjg2YzUxN2ZkY2NhM2JiN2MwYWUyN2E1ZTg2MTBkMGU3ODFiN2Q0Njk0NTk4NmRhNDNiZGRkOWJjN2NlZmZmZmZmZmYxYTNkYTE1NWM0OTI0NmI4YjJkZWRkM2IyMjE2Y2RkNzAwZjQ3NDE0ZDFiMjc3OTFjMDY5NzlkMGQwMmUzYjBhMDEwMDAwMDA2NDQxZGI5NDRiMWJmNDM2OGY5Yzc1MmEzN2ZmMjkxMWFhMDQ4YjU5NmYzODA5MjE1Y2RjOGU5YWFiNzU4ZjAyNWQyMWM5YTAxYjZkYmJiZTUxNTgxYzhkM2YzOTlmZjY4ZDY2YTBlYWZmNWZlMWQ1YTZkMTg2MjJkMWYwYWEyOGVkYWM0MTIxMDM0NGRmODI1ZWFkZjhmY2YyZDI3ZTA0ZGEwYTU4MGNkOWJjMDA3YzlmNDNmZDcwYWEzMWM5ZjY5NjIwOWFkODU2ZmZmZmZmZmY5NjE1ODgyMzAxOGE5MGFiYTU5OTQ4YjgxNTc1NjZjNmY0Y2JiYWI1NTE4YTdmYmFiMTJjNGY2NjIzNmEyMWI3MDAwMDAwMDA2NDQxZWJiMWQ5YWM4ZThiNzc1ODMwNjc1ZWM2ZGM2MzBlMWEzMmQ0MDkwYzFkODIwMGUyZWM2ZThiYmQ5MDdjN2UzYWJiMGE3MWZkMjY3NGQyMGEzNGRlNTJlZWUyNTIzMmU5OTY0MTJiMGM1MGJiN2MwMzIxZTY3OGQ2MWJjODg1YjY0MTIxMDI3Y2QxZWViN2JlOTE2ZDRhZTUyNWMwZmMyZGIyNTJhOWRlZDRmY2YyYzVjZWUzMmUyNGYyYTQzZTRjMWUwYTNlZmZmZmZmZmYzYzUwMGJhYWYxZDVhYjJhYzU5ODQzMTZjZjE1MmRjMmY1YjYzNmQ2NTYxMjU3NjY1MDEwNDRjOThiZTJlNmE1MDAwMDAwMDA2NDQxMjUzMmNmZGRjMzcwODU5MDYwZDIyZTk2Njg3ZWNiMmNkMTY2M2Y2M2UzODVkOWY0NjFmYWQ3MWI3YTY2ODRjNTRlZjQ4NDk4MGE4ZDQ5NmU1NWI5NmMzNGYyMzY0NzJhZGExZWNkNmU2ZTlhZDhiOTY3NTg4M2E2MWVjNmM2MWI0MTIxMDJlODdjNWNmNGNlZGYyMWYxNzNmMTY1YzQ4YzM5ZmRhNzA0NTY3OTQ1OGExYjBjYTRlY2IyNGM0MzhkOWI4NTBmZmZmZmZmZmYzZjU3M2MyMzM5ZmRmMTVjODdhMjZlNmUyMzBmMjQxZWMwNWIzY2JlYmJkNTc2ZjUxM2YwZWE3YjJiZGNkNjVjMDEwMDAwMDA2NDQxM2Q3NmE5NDdkNmM0NTAyZWJiYTE5N2Y1ZjM5NWIyYTNkZmU0YjRkYzk3NjYwMzg2MWRmYjg4OTU5NTA1ZDYzMWE0MTk2NzA5MzY3MjYxOGMzM2I0ZGU2NGIwYjk1YTk2M2IzMjUxZDBlMjc2NDBkZTRlZGRiODNlOWViMWZiOWQ0MTIxMDNiZjhjNjlmM2IxNGE1ZmMyNmJmZjY1NjBhNzk0ODQwMDYyZWRlYjNkMzliYjFhMTgwODExZTFlNDM3MGI4MmZmZmZmZmZmZmY0YjE0ZjM4MzBmNzY4OTJlYmM4MjE0ZTJiMWFkOGE3YjBlMDQxYzNjNTZlZDQwZjQ0MzM2YTdkYmYwZjk1YTEyMDIwMDAwMDA2NDQxYTE2NGNmNmFkOWM5NjhmMzFkMmE3NWRjNzRhYTBkMzcxYTljZmMzNzlkZDc4ODRkYzFkMzlhN2FhN2NhOTBjNmYyZGZjNWJhNTI3YWRjZWQyODVlOGY2YWUxYWYxMDYxNDhkOGZiZWIzMWRjYjE3YTViNzVjYjJjNzk3NmRlNzg0MTIxMDIzMTMwNGM1YzMzZmU3MTFhMDRkMTU0NmY4ODFmNmNhYTFlZDk1MmNhNTkwN2JkZTExZjk3YmRiMWJkMTg3M2RhZmZmZmZmZmZkYTkyMTg5MDkxNTViN2Y0MmU0MWZkOWM5Zjg0YmJhMTFkZTAwY2Y5NzViMTllN2U0NzRlNmIxNmVjZmNlN2IwMDMwMDAwMDA2NDQxMGQ4YjQ0N2RkNmFmMTlmNjE4MzRmNzk5NWNmMmY1OWVmZTIyNTg0MmI1YzdiYzg4MDUxZjIwNGNhNzFmZDk5MDRlNDFjMTVlNGU4NjcxMmRmZDdiYzRjMzgwMGYzOTQ4YjY4MGE3NmRjNjA5ZTJkYjE2ZTY3YTY4NjE4ZWE5ZTY0MTIxMDNjNGI3MzVhZjhlMDQ4OTdmZTA1MTg3ZjI0ZDNkMmI3NDM1NDk0MWEzNDJjYzI2NzYyYmZkNTk1MjM5ZGJhNWJiZmZmZmZmZmYzMmM4ZTk4ZGM3NTgyY2YyYjFlNjc1MTNhMTUyNWMyNmI5ZDU2ZDcxY2ExNTkzNzVjMTg0Njc4N2M1MjllZTIzMDAwMDAwMDA2NDQxMTJmNmU1YmNmMDJiMTJiNGEwYmNkZGNiMWZkNmVjMjE1ZTBkNTdmMTJlMWIzNTYzODEzNDM5MDAwYTNjZGQ5N2VhNjVmNGYyMzNlM2ZiYzA4OTQ5ZGQ1NzQxNjA5OGQ0MzcwYWQzZDQ1OGY2NGE3ZDczMmUzNDc3YzNhNjVkMTg0MTIxMDI4N2E1ZWU3NmExYTc2MGMxOTZhYTYxMGI3ZGRlODA1YmMyZWNhMGM0YjRmMzA1MjRiYjFjYTEwMDQ3OGJkY2NkZmZmZmZmZmZiM2VlNDNlZTY2YTFlMzQzMTg0ZmY2MWEzNzY4NjI1ODRkY2IzZjFlMzBmOTY0NjFlYjhjM2JkNWQxMmExM2M2MDAwMDAwMDA2NDQxOWI1YTM4MDVlN2I3YmM1MGFmYzMyMzgzZTA2ZTk5MmM0YjFiMGYxNjdiYjNlM2ExZGNlNmIzZmJjMmM3ZmRjMTJkYmZiMjcwYjUyY2NjMDAxODQwZjQ4YjAzY2Q5Y2YzNjgwMmM0NTkxMzBjMzZlZGFkMWFkYmYyNDY2OGYwMzQ0MTIxMDIyODMxMjNiYzRmNDMyZGExMDIxOGQ2NzVlNTg2MmUxMjljZDAwMzI1YTNhOTY4Yzk0ZDc0M2U0Zjc0NTg2ODc1ZmZmZmZmZmZmMDUzZDQ1ZDcwM2JmZjQwNGZlMWRmMWJiMzI3MTQwNTRkM2Q0NDQyYmJlMmU2NmYzOTFhNjY4MWM2ZThhNWU4MDMwMDAwMDA2NDQxMTY2N2ZmMmFlODUxOWFlNTU4ZDhmNTI1NjkxMmEwNWRiMTA0ZGZkMDI3NmVmZTgxN2Q4M2NkNTRkOWFiOTM2NDgzOWQxODhmMDUzNzEwYmQyNjI5NGRhNGVkNDE5NGQ3OTg1OTNmOWU3ZmQ0NjFiOTgzN2U4ZGViOTAwNmZmNGM0MTIxMDNjMjczOGMyMDk4NTcyZjU0NmQxM2M1YjUwMmJlZTA5NGQyNjhhZmRiNTBlODFlOTQ3Y2ZmMTg1N2M0ZWU2NWQ4ZmZmZmZmZmY2ODM5YmEzZTNiZTAzOTQ4NWExNzM0OTQ2Y2U2MTliNzJkOTJiOTc5MWYzOTgxNzRkMTAzNWYyZGE4YjgyMTUwMDEwMDAwMDA2NDQxMzNhODlhMDIyOGVkNjQ3OWM3ZmE4ZGVhYTJkMjBkMzE4YWMwNGYyZWIwNzJlM2Q2ZTBkOWRiZTg0Y2M1MmZiZjEyZmQxZjVkNWNhYWFhZWJhZjliYWU3NjBiYzdlMDAwNTg4YzllMjhiNGJjMzkyMDU1ZWE0MjE2OWZhMjg2MTU0MTIxMDJmYTllZDBlMjg0ZWU0OTI4YmZhOGIyYmMyMDFkZjI5N2E3NGNhMzZmZmMwZDMwNjliNWI2Y2NiYTQyMWU1MDJmZmZmZmZmZmZmMjk3MGZmYjRmZjhlNDUxMGQyNGM4YzA0ZmM2MzliMDM5NjAyMGE5ODAwZTE1ZDBmYzU2MDU5ZGUwMmM0NDg0MDMwMDAwMDA2NDQxOGQwYjcwOTc1Nzg1NTVkYjU2YmYzN2E5NzhlNDU3YzMyNGNkYTQ1MjBkOGFjOWIyZDBhMWU3OGY1ZWYxNTc1YTJkNzc1YTEzZjk1Y2M4OTE5M2ZlZDFjNjY2OWIwY2VmYjc4MTUwNDA2ZWM1YTA0YzE0ZTA3OTE5OGNlMmI3ZDc0MTIxMDM2ODdkMWFhZTk1ZjNjZDA5YzQyOGJkODFiNzYyOGE5NjIwMjRjN2NkNjZlMWQ2NzRlZTFmZTJlMjFlZTZhYWQyZmZmZmZmZmYzYjJjNDc4NmQyZDdjZjJiNGY5NTc5MWU1YzY4ZmE5MzExMDhjMWQ1ZDgyZTAxZTI3Y2U3MTQ3YWEwOWM0NTRmMDAwMDAwMDA2NDQxYzQ3ZGNmMDA1MjhkYTAxMmYwMWM1NTAwNWQ0YTM2ZTY0YjdjZWE5ZWMzN2M4ZmQwMWIzZGZmMGYwN2UyODRkYzNjOGQ3OWIwZGU2M2UwZjFkZmRjOTc3ODExZDM5MTY3ZDZkNzk2ZGMwZTZlNGFiZjExNjExZjRhOTI2Njk3NDU0MTIxMDMyODIxZGIwMGM3MzllODNlN2QwYjVhMDlhNzUyNzg0OTUwZDlkYjU0ZjFiMjk4NjNlMWQwN2NjMjE0ZTI4ZTU4ZmZmZmZmZmZkYWI0ZDVkNDBkZmRjYTY5OTBmODkyOTA0NGMxNDk3Yjc4OTUzOTk4ZTJlZTA4MDU4NDYzMjZjNjQ5YWRhNDY4MDMwMDAwMDA2NDQxODg1MzVhOTVhMTIzOWU4ZGQzOGJiMTgwNjNiYjUyYjBmMTkxNDYyYzc4ZjY5YzVlNzgyZThkY2VjMTdkYzQ1NGVhNmUxZGI1NDBjMTg0MjQ4NzM1ZDRkODQ1YWQyY2M1NjEyMDEzYzE0NjgxNDU1NzJhZDUzZDIxZDAyZjljNzg0MTIxMDJjZWZiYTNlOTkwZDY5ZDRiMTJjOTkwOGRiMjhkY2JiODdjZTRjN2Y2MmQxYTRmOWZhYzk5ODY4Y2U2ZDdkMTViZmZmZmZmZmYxNWRlZTA1NDAwMTI0OTAzZDFmYWY3NTVhZDc3YTE1NzcxYjcwZjA2ZGI0MjdiMzBlOGJlY2UwZjI5M2E4MTk5MDQwMDAwMDA2NDQxMzMwNWFiOTQ3Nzk0NGZhMDQ0MDliNmE5ZDAxYjI2NDdmMWM0MGZiMWEzNmZiYTI0NDNkMTMwZTRhYTRjY2VlNmYzNWU3MmY5YzBiMTU1ZDRjYTc3ODk5YTZmMGQ4MmQ3NjE4N2ZlMGU4Zjk2NzA0NGNlOTEyYzc0MGViMmE3MTM0MTIxMDNmNjY1NDk3ODhlNWIzZGY5MTQwMWY4NDM1ZWQ2NjY0MzNlMjM5Nzg0NWQ0ZjJmMGNmNWZhMWZmMDFlOTcwZGNkZmZmZmZmZmZkZTM0NDgzNzJkZDYzMDVjZGEwOTQ5NjViM2QxN2VjYzMzMWNjNWJmNTlmZWI4OTU1Y2ZjYTY3M2I3ZDc1M2Y2MDIwMDAwMDA2NDQxNjZiZTBiZWY2OGI2NDM1OTJlMzIyZjJiN2VmNWI5M2E0ZWU0NmE5ODI2N2Q3ZWNkN2U0NmNlOTk3MzE5NDkzYTYwMWIxZDJhNWM2NDgxMzI0ZTNmNjQ2YzUwYTZiNzI4MTRiNTNlZTliM2Y5MDk0ZGY2NmQyNzE2Y2E2YWVlYTE0MTIxMDIxMjhhZGYzNWJlNjcwNjM5NmRhODlhN2I2NGFmODlmZmVjMGY5YmNjN2RmOTc5ZWVlMzU2ZDE1OTczOGVlMzE3ZmZmZmZmZmZmYzllNTk2ZTJiZmM4MTQ3ZDBjN2Y1NzgwMDg5MjY4ZDQ0MmQyMWM5ZjJmMDEwZGJlNDcwZDU4OTdjYWI0NTQ3MDAwMDAwMDA2NDQxY2IyYzdkOTQ4ODFjOThjZDJhYTJiOTVhOWQyZjE3N2RhMjIzZmNlMWFlN2QyZTU0NTM1YTUzZWM5NjQwNDMzZDQ2OGEyNjM1MzE2MWQ1MGMwZDRhNTdiYTcyZDJlNTBiN2M5MDA2ZTllZTAxODU1Y2MzMzdhNDc5MmRkNjAyNjc0MTIxMDIyMWQ0NzVlYjIyMzg1OWEzZWEwYTliMTNiY2I4NzQ4NDM5ODNkMTI2ZDc4ZDNmZWMwNmIyNmFmNDQ5NmE2MTRhZmZmZmZmZmZkY2M4ODVhMzgyZjRhNjMwYTgxMjkxOTljNDhmNGFiZTcwNTY2NGNhOTFkODEyYmE3OGEwZWU2M2JkYzU5OTJkMDIwMDAwMDA2NDQxNzQ0NWEyZGRlZjk3MzA3YTRiMGI5MzU3ZTg0NWI0MGNmYzAxOTliODc2NjI5ZmYxZDA4YWIzOWVkNDVhZDRlNzA4MzdmOTY4NjNmODUwZjQwYjk5OTI5NjVjNmMwM2ZjZjVkNTMxMTZjNDUzMTA2YzQwNWI1NzU5ZDUwY2Q1MDU0MTIxMDMzMjRjZWFlODdkZjM3YmIzZjEwMTkwZWM2OTMzMjZhMTRjOTFkYjdiNTQzNzViNzQ5MmY0MTBmYjM1MmIwOGE4ZmZmZmZmZmY4YjhmNzc4MjRkNTY3YjIxODg0MmUwYjUwNzMwYjM3NWFmODY2M2I1NTA4ZDY5MTNhMWFmZDU2YmY1NDc1YWU4MDEwMDAwMDA2NDQxNmVhNDc1ODRiNzdhMmFkMTM5ZjliNDBlYWQ0ZGM1MDQwNDU2YTg4NzNiYWM2YzFhYjRhNmQ2YTA3NWRiYTM0MGMzMTZjZmViZTFkNmFhZmU4NjIwNzI3ZDA1NDQ1OWM2MWY1NDVjZmY5OTQ5NzQ5NzVkZDc0MmVmMWM1MTU4YjI0MTIxMDMzOGVkMmQ0NjcwMzU3MGY3NjQ0MTdlMmE3ODE0ZTQwZmUyMTQ1YWI0NDMzYjIxYzAwNzE3ZGY2YjZhMDk5YWExZmZmZmZmZmZkNzk3OTVjZWRhNmI0N2FiMDY0YzAxZjhkYWRiNjc3ZGUxNjFmNGU0Nzk1ZjYyMTk1MDRjNzkxMTgwYjQzMzc2MDEwMDAwMDA2NDQxOGU2YWQ3MzFiYzJjYTNiMjUzMGU2MDUxYTZiOTFjZGM4NGU5ZmFiYjFlN2Q4NTEwZjFhMWM2MTMwYzA2MmE1MmQzNGRkMmZmYjIxMjJlZGJlYmRiZTE0OGM1MmI3YmI2YmNjZTEzODBkYjY3ZjdmNjcxNTA0ZjhhNzJkNmM3ODg0MTIxMDMzMjRjZWFlODdkZjM3YmIzZjEwMTkwZWM2OTMzMjZhMTRjOTFkYjdiNTQzNzViNzQ5MmY0MTBmYjM1MmIwOGE4ZmZmZmZmZmZjMjQzMDYxOGY5ZjA1ZWFlY2I3ZDA2YzE0NjAyMzZlNDIxZGVjY2I5NWFhMjc5Zjg2MWI5ZTBjZTQ2YmMxZjRjMDEwMDAwMDA2NDQxZWMwY2JkM2E5ODdlYzI3ZmQyZjY4ZjIyNjk3ZGE4YWZkYzhhMmI2ZTFjYTBmMmM5NTI2M2M2YmUxMWE5ODI4ZmUxODRiMjg2OWM4MGJkZTUyNWQ1MWExNDE3ZTU5ZmQxMzUyYzY4M2M1OTU1MDBmYzI3MTNmNDZhMjI2NzUzYTE0MTIxMDM2Y2UwNTdkNzcyNDg4YzFlYmQyMDY4ODRjODg0NDMzZGEwNzllOGNmOWNhOTA5NThkYzhlMTBkZjM2YTY5NDM5ZmZmZmZmZmZiNjM5Y2I5MzM1MjQ2NjFhZmY4NzViOTNmYWE4OWYzYmM1MWFjZDA0NmNhZjcyMzUxMTVkNGVjZDY2Y2ZjMWFjMDMwMDAwMDA2NDQxNzBkYzA3YzdjZTJiZjRiMGU3YWY5YWU2NmZkZDk3ZWQ3Yjc2MDhmZGQxZmVhM2I2N2E0ODhlMGU0MDQ3NmM0OTU2MDIxNTFlM2ViODgwNTJkMjIxMWYwYzNkYzFjMTA2NWNjZGI3ZjlmNjdiMWUwMjUyM2VlZDY5Zjk1YmIzOTY0MTIxMDM2ZGMxMWI4MjMwNjM2YTA3OTE2OWFhN2Q5M2Y2MDA2NzdkYTA0MDgzZWQzMDk0YmU1YzQxNjVmNGMxMDVmZjgzZmZmZmZmZmYzOWNlNjY2MGFiOTI1MjVmYTdkMTYxNzZkN2U4OGNmYTQxMTM5YzQxNTM5NTQxN2EyZGNmNjIxZDMwMjA4YmY5MDMwMDAwMDA2NDQxY2FlODgzNTUwNWZkN2Q5YzBhYWI3NjFmNDM5YjBjZGY4NGZhYzczMzc0ZWFiOGUzNmI1M2U4NjY1YmM4YTQ2NWY2MTNhMDBlOThhOTZmOGIyMGVjOWU5ZTYxNzUwODkwOGRjMWEwMGUwYzAzNzZlOGY0MGNhYjQwOWFmNzFmYTI0MTIxMDI1ODVlZmY5YjVjNTU5ODliNzU2NzBlNGRmMDEyNjU0ZTQzMzI5ZmQyZmU5NjE1MWRjNDMwNmQ1ODBmNDQ1NmIxZmZmZmZmZmY4Y2RmNzRjZmE3Mjc0ODUyNDE2ZTJlMzE1YTI5NmNhOGJjNDliNmI2NGVlOTI3YWUzZmUyMWYyODA1Zjk5ZjE2MDIwMDAwMDA2NDQxZjI5ZWFmODgzYjcxMTYzODc4MDQ4NjA0MDBhNWRlYjYwZjA2ZGQ5MmYxOGM3ZDI3NDNiNTNjZWMyNjM2ODlmYzA1OTdlYTA3MDkyMWM3NDFiMzM4MmI0N2FiYzA4ZTA2YzZmMzdhZmIwOTYzZGQ3OThiNmM4MThlZGNlZmIxNzc0MTIxMDM0MTE2MjM2N2JhMDA4YmY5M2RjZjJhMGU3M2MzYjZmNzljMjRiNmNhMDE2YjYyOTM5MzAxN2NhNjBjOWU2NzY3ZmZmZmZmZmZkYTkyMTg5MDkxNTViN2Y0MmU0MWZkOWM5Zjg0YmJhMTFkZTAwY2Y5NzViMTllN2U0NzRlNmIxNmVjZmNlN2IwMDIwMDAwMDA2NDQxZGJjYTc1OTVhYmZhMzY5MTA4YTgzMjc4NWZjMTY4OTYxMWY3MTllMGJmZDQ1NzNlYTNjZmQ4MjI2NzQ0ZmYwYjk1ZmZmMTY1MWNiYjMyNDI0MmE4Y2E1YzliZWYyMzM0OWQ1MzhjYWFkNjdlMmYwNGRmNjI5Yzg0M2FjODIxZDc0MTIxMDI3YTg3NDMzMTNmN2MyZjRhODlmMGFkNTBlZjU0YjEwODcxOWUzN2NmZDRkYWY2N2JlZjM4NGRjNjZmMGI2ZDJhZmZmZmZmZmYxNWRlZTA1NDAwMTI0OTAzZDFmYWY3NTVhZDc3YTE1NzcxYjcwZjA2ZGI0MjdiMzBlOGJlY2UwZjI5M2E4MTk5MDIwMDAwMDA2NDQxODFkN2JiMmFiM2E5OTBhZDU3MmZkZDVhNzVmZDg0ZmFlMjYwMWZhNzQwMWU0MTU1NGZiNGJjZTFlZDYwNzFhZjljN2MwMmI4NTcwZWFiMmIyN2Q5ZjI1MjU2ZDI0MjQ3NGJkNWQ4NjNiODc0NmQ1YjkzMjRmMjRiZmQ0NzRlNjA0MTIxMDMwOTYwNTAxY2QzYjc2NzVhMzI4ZDI2ZTVmZTdmZTMxM2E0N2YzMmRiY2I5MDJkZTgzMjNjMTIwY2E1MmZmMGIyZmZmZmZmZmYwNjdmMjE2OGMzNzNhOTMwYzk5NzBkNDUzYjBiNDVmZDEzYTUxNDI4NDhiNDNhNjJkYmYwYWEwMTE5OGFmNDliMDIwMDAwMDA2NDQxZWZiMzYxMWFmZGUyZjllZWVkYTczOGUzMDFiMTYyMGY1NDYxYThkMmQzODRjMzZmNTAzYWEwNGU1YjIzM2I5ZmU1ZjRiMmQzM2JhM2ZmZTEwNjYwYzVhMmU2ZmEzMTM2NmRkYzRmODMzOWJkMGVmZDk1NDJjYmJkODZlYjBmY2M0MTIxMDNhOTNiNDYxNWM1ZGU2N2VjZTA4MTRkNzRmMTI5ZWU1Mzk1ZGY3MWVkNWJkMjIxNDc1NzQ0YjljMjM3ZmUyY2UyZmZmZmZmZmYxYTVmOTE5ZmE1NWY1ZGNhZmEyYjg2NjQ3MjdlZTI4NDcyNzUxMmIxMTY2YTg2OGZmNmMwZTg2ZTBmOWIwOTNkMDEwMDAwMDA2NDQxMGI5MDZiZjA3Y2FjMDU0Y2I2ZmQ1OGZmMmE0MzE0ZDA5Nzg3NDViNmRkMWQwMzA5ZDM5Y2Y2OGM2MmY2ZDU2NGY5Njg3ZTFmYjY5YjljOWM2M2IxZTU1NzI1MjlkNjg1YmI5MmI1ZmI0OTc0NDFlNmFjOTJiMzkwYWQ2MGMxNTA0MTIxMDM3MjllZWRlMThmMzU2ZTc3YmYxODIxYTUxZmUxNTEzZDU3YTMxYTA4ZWM0OTMzYjlmZjhkZGUwOThhOWZlODJiZmZmZmZmZmYzZDYwNzJjOTRlNWEwY2U5OGM3YzQyMTY2NzAyODliYmZlNTAwMjBkNDM3MmUzMTg2NWFmOGY1ZTRmNTQzYmY0MDEwMDAwMDA2NDQxYTNhNDZmMDAxMGRhOTMzODcxYTdmMmRiMjgzNjUxNzc5NWQzMWVlMWQwOWJjZDMwNTlkYTE0NTJjMjI4NDAwYWIyNzA1OGRjYTVkNjI3ZDVjNmVhOTJiY2E1ZmI0MjhiMjQxMjJhNjQ1NmU1MjllMTQwNzRjYWEzNjEwNzU4NGY0MTIxMDI0YTUwMzM1MGI5NjJhNjA2NzAxYzFjNGRmMDgzNTM0M2Y4ODIwNmVlZjU0NGE4MDdjNjQ4MjUxYjc0N2YyOWY2ZmZmZmZmZmYwNWE1N2FmZGVkZWQyODJkY2IxNWRjNDZmYjQyMTZhOWRhMmYwNmRiZTUzMjZjYTQyOGIyNjdjOGM3YmI1NzY1MDAwMDAwMDA2NDQxM2MwNGI1YWM5NzgyODY2OTI2MzY2MzZmZGViODJjYTFhYWYxYTkwODA0Mjg2YTEwZDAwZjZlM2ZmMTM1ODUzNDlhMDQzOWU2NTY4N2IxNDcxYjQzYmYyNzhkYjZmYjljNzI4ZmNjOGI4M2QxM2Y1YjEwN2IzOWE0MjQ1NmY2MGE0MTIxMDNkNDcwOWJlZWRkNDYzMDdhMTllZTE5ZTZmNGI3M2IxZmRhZGE3NzA4MWMyNDg5ODc5NDY2MWM0MjdjMzUwNDRjZmZmZmZmZmZiODg3ZDUwOWI2YmFiNjA0MGM3YzM0NDMxNWZhNDM2YWM0YjU3OTE1NGFlZWMxMTk3OWM3M2RhNjQ3MWM1MzcyMDEwMDAwMDA2NDQxZDdhNDFmOTllNWNjYmM2MDY5YjQwYTAyYTI5YmI3ODBlZWY2Yjg4NTgwYzU2ZTI3YzcwNjU2NmUwNjVmMDk2YWVjZDY4OWQ5NGJlNTgzNDc0NDM2ZjM5YjRlNWE5NzI5ODhlMmM0MzQ3NGNjZTQ1NmViNGEzMzE5OTE4Y2ZkZTY0MTIxMDMyZmI0NjliN2QxN2NhYzg0ZTI3ZDgxM2EwMDg3MDgwZjc3MTc0MWVlZjUzOTRhNGE2ZTUzNjViYzAyNmYxNWIyZmZmZmZmZmYwNjdmMjE2OGMzNzNhOTMwYzk5NzBkNDUzYjBiNDVmZDEzYTUxNDI4NDhiNDNhNjJkYmYwYWEwMTE5OGFmNDliMDEwMDAwMDA2NDQxYjVhOTBjNzUxOTM0ZWU4MGEyMzg1YTE1MGMyMDVlYzg0MTZkMGI1NTJkOGQzMjQzNGYwNmZjMDY0MzY4YmNmYjhhZTFiODY2YWE4ODA3NzRmZWQ1NGQwYTc5MDA3OGJhMGQzOGUzZWYzNmZlOTMyNTI5MjEzMDkzODA5YTA5ZWE0MTIxMDNkNTFjNTIwNTkwYjhlNGM2ZmMzYzNmZGEwYzQ4YzkwYzllZjE4YTliYTk3OTkxNzE5OTc1NTRlNGIyNmE5OGYwZmZmZmZmZmY4OTI0YjNjMDMyOTM2NmZkMTMwNWVjM2NjMDVhOGRmN2FmNGU1MTEzNGM2ZDFlMmJlOTAxMTFjNmE2MTIzODFiMDAwMDAwMDA2NDQxMDZhYjllNzZjYmEzZmM2ZjA3ODAyY2JkYzVhMmEyNWI2NTAzZjY4ZTM5NDUwNWZlYTJkY2NkZGQwYWZkZjYxNDcwYmI1MGYzYzM0YmY4Yzc2NGJlZGNmMjU5YTJmMjJjMjQ4ZjFlMjM3MGU2M2MyNmMzNzUwM2NlZDY0ZGJkMzY0MTIxMDIyY2ZlNGVlOGU1OWU5MDc1NWY5MjNkZTkxYThiYTU2NmJkYTFhMDk2MTNkZjIzNTlhNTRjZmM1NDBlNzlmZTEzZmZmZmZmZmZkM2YxNDg5ODVhMzkwZTU0N2UyNTlkOTRlMzA0YjczYzQ3OWYzNjkzNGM2YWRjNjA0YThiMDcyZDlhY2Q3ZGM4MDAwMDAwMDA2NDQxMmUwMzYzYWRiM2VhYzI5YWI5NDQ1OTM0MDU1ZmFlZWE1ZDM5ZGVjNjk5MDczN2FkMjdlYjE4NWYyODQ1ZTk1ZDg5NmEzYzA5Y2UwMDFkNjkzZGIxNGVlNzQ5Mjk4ZDNmOTNjOGQ2MzZlY2ZlMTg0MGZlODEwY2M4ODdmNjFlYmM0MTIxMDM2YWFlNzFjMTY5NDllN2RlMjQ3MDNiZWM1NTE4NDQzNGI4Nzc1YTVlYWEwZGJkODMyYmYyZmU5MWQwZDc3NmUyZmZmZmZmZmYyYTM3MzA5YmI4ZGM5MjU4YTQ4NWY5MWQ3ODFlYmUwYmE2Y2E2NjhhNzlmZGU2ZWQxOGYxYzkxMDMwZjY0YWVlMDAwMDAwMDA2NDQxNTM5MTlmMTMzNWFjOTk5YjM4ZGYyN2EwY2EyMWIyYjIyYmJjYjI5NzY5YTMxYmM5YTNlMTY4ODhiYmQ5M2E2YmMwNzIwYzVmMjMyY2U2OTk2ZjM4OGE1MDE4NGUzYzFjMDgyNDY0YzcxMjZkMzVkMGQyMjgxY2QxOWYzMzdlMDE0MTIxMDMwZDkyMGExNGJiMzc3OGJkYjQwNDE4MzllNWNjNWNhMTc5YWU3OWVkYjg4ZGNiNTcyNDhlNjAzODYxZjg1NGZjZmZmZmZmZmY5ZDU1MDI3ZmFmMDhkNjNhY2M2OGFhMzMzNzI1ZTc0MTE2NGU5MDkxMWRjOWUxMDk3MTBlZTBhN2NhZTk4MGZjMDAwMDAwMDA2NDQxYTFmZDc2YzhmODNmYjU3ZTAzYTA3ZDk2YTM5Y2M1ZGYzYTExMjM2MjJhNzA2YjAyNDRmOTVjNzE0NDQ5NDg3YTQyMjRlM2YzNDZjNTU1ZTczOGY0ZDFjNmZjNTlhYWMyMTZmZDEyYjlkYmUwNTljY2M3YTFjNTM4MDEwNDRiOWQ0MTIxMDI1MjA5MGQ4ZTE0Yjc0YTk3NTFmYWRkY2JjMjMxY2IwMTMxZTA4Mzc5YjdiOTkzOThkMGNlNDY4MmUwNzAzNWZjZmZmZmZmZmYwMWJjZmY2ZWZhYzZkZGZkOGVjODAxYTY4Njc3MGYxNThiMmQyZDY2OTU0MmNjNDgxZTgxZTBlYjI2YmRlMzg2MDEwMDAwMDA2NDQxY2ZmNmFhN2Q1MzMwZGRlOGY3MjgzZDNhMTIzZjY5OGQwZTA3ZTIxYTY2NTJjYTU2NjVlNmQ3ZTk3YTc5OWM5MWU3YWYxOTkxYTcxMTczYjYxYjIyY2U5ODkwMThlNmIzZDU4OGQ1MTFhYzY5ZTg2M2NhZjMxYzI0YzE2MGJjZDM0MTIxMDNhMDQ1ZWI0ZWQ5ZmJmZGY3NzA1Mzc2MWUyZmNkYjk2NjczMGQwZGJmMTBkYTRlYTY3NzQwNDZhYjMxOWVjYWYwZmZmZmZmZmYxZDcyYWFlYTI1ZjU3NmE4NmM5NjQxODI4NTljM2NhZGVlOWUwYjUwMjY2ZGM5N2Q3MGM5YzVmZDcwYzY2Y2YyMDAwMDAwMDA2NDQxMTVkZjljZDRhYjYyMDg5Mzc2MTgyZjUyODYxMjhlMjMzNmMxYjMzNzNjNzcyYTY4MTViN2M5ZGUzNTQzOGUwYzYzMTBlNTI3YzIzNmM4NmZlZThlOTRmMWIwZGJjNTVjNDc3YThiZWU0Y2FhOWYxODUxZDI2MzFjYzM1NzVjZjY0MTIxMDI5MmU2MzNkYWQ3ODNjYTFjYzk4Njk0NzQ4ZjZkYTcxZmQwNjc1NDU1YTJkNGJkMGY4YTRlZjI2ZGMzMTdlNGM4ZmZmZmZmZmZiNjM5Y2I5MzM1MjQ2NjFhZmY4NzViOTNmYWE4OWYzYmM1MWFjZDA0NmNhZjcyMzUxMTVkNGVjZDY2Y2ZjMWFjMDIwMDAwMDA2NDQxMmY1YWZjZGI1MzVmNDhiZmViNmM3NmEyZmNmNzFlOWFmMDA1ZTVlYTcxMmYwMTFiZDUzZmZiZWY3Yzg0YTRjNzNhNGEwNDhkODUyNmY5ODBhMDRiMDIyZGZiMzc1ZDc3NzBmMjAxNjdlMDA5ZTcxMjUzZWYzZWUwYmEwNmJjMDI0MTIxMDIzNmExNWZlNzljZGU5NjgyMWIyYjMxMTE4YjgzN2E3MzY3N2E4ODA1OGEwN2U3Mjk0ZmVmODZiNDNmZmVjMDlkZmZmZmZmZmY2NjcyY2M0ZDA4ZGRhZjU4OWNmODcyZWVkY2ViZGU2MDc2MDY4NzhlZWE4OGU1Y2IwMzUwZTcyYmE3Yjg3NjQyMDIwMDAwMDA2NDQxYzA2NzZiNGY1MmM4OWZmOWM3OGU4NDg1MjdkYmQyNmQyYzA0ZmE5MzNiN2Y1OTkyM2Q4YjNjMDMwODcyNWY3MTg3YTk0OTc0MGVjNzMwOTNhNjZkZDVhY2M4YTMwNGY3ZDQ4ZDI1OWZmYzU4ZTI5Y2UzMmMzYTc0NWVlYmZhZTY0MTIxMDMwMDEzYzMxYzMzYTRhNDY0Zjg4OTIwZDQ5ODIxNDJhNmMyYjkyNWMyY2MxMDcxYWFjNjc0MjQ1ZjZkZDU5N2UyZmZmZmZmZmY2NjcyY2M0ZDA4ZGRhZjU4OWNmODcyZWVkY2ViZGU2MDc2MDY4NzhlZWE4OGU1Y2IwMzUwZTcyYmE3Yjg3NjQyMDEwMDAwMDA2NDQxMzY2MTQ5MmQxOTQxY2QxMTUzNDE2NTM0NmZhNGY4OGY4ZWZiZDEwMjU0NzAyOTkyOWE2ZGFjOTIwOWI1ZTMwYWVlZmM4ZDAzYTMwNWIwNTZkZDU4M2Y0ZGQ2MTFjMzAwYTg3YTQyOTUwNTQ2MmZiZWQ0ZDc5MzM3MDI3NzQ5MjQ0MTIxMDNmZWFjNzRjODU2NzZjMjdmN2MwMWVmMDcwNTY2ZGNhYjRhMjUyOTE3YWE1Y2UwMzVjZGRjNmExMmNkNzc3MmMzZmZmZmZmZmZjYjViMGMzOWEyMjMwMDdlZDVlOTBlZjc3YzQwMmU3M2MzMjUyMzI1ZjQ3NDYzMTRmZDc0ODQxNTEzNzhkODA0MDAwMDAwMDA2NDQxNjE4OWFmYzMwNDU0ZjIyNWM2NTViNTQ0YTc2YzIxNzlhODg4YjU3NDRkZTllMThmZGQ1MmVkZTVhMjkwZTRlYjMyZDFjZDgxZTZlNGM3OGRiMTAzOTJkOWNiZjllMzJmNzVlMmM0NzYzYmU4OWY3YTkzNDMxNjI0NDJmZmY0NDk0MTIxMDIyMzdmYTJmZDkxODQzYTY4YTA0MDliM2IxZDMyZjJhODFjZWI0YTNkOWZkYTE2ZTM5MjNkMzQ1MDA5ZDdhZGQ1ZmZmZmZmZmY3NzRlMTZhODBhNzYzMDNkYmYyNmE3MzI4ZmM2OTljNDVhNjE1OTYwMWVkZDdmYjE3YjgwMDU5OTgxZDJhNzU2MDIwMDAwMDA2NDQxNjM1NWUyNGYwZmI0MDMwNDM5ZmY1YzBiNzNmNDMxNDg5ZDZiYjZhZDY5NjhlMTk5Zjc3ODRiMzU1ZWFkNDU1NDJkMDY4M2VlMDIyZmY5M2JlODRkYjE0NjFlZDRiNDBmYWYzNmI1MjA2NzJiYTFjMDEzNjAyYTliODc2ZmY2ZmE0MTIxMDM4NzUwYzA5N2RjZGU5M2EzMjY1NGMxZDAzZGRmZDFjZGQzMzVjZTZhYWE4ZWZiMzIzODQwM2U1MzcwMDcwMmNlZmZmZmZmZmY3NDEzYzY3NGE3NzczNjg3NGM0M2I0MTUxODI0N2YzNWZhMWM0NThjYjYyMzVmNDE1ODAyMTFlNmZhNjM4M2Y0MDAwMDAwMDA2NDQxMjUzZGU2MTQyOWJkYTU1ZGEzZmNlZmExNjYzMWY1MzczZTQyZjFhYTczNGFkY2NhNWE1Yjk4M2MxNWYxNmE5NDkyZWI4OGRiYjkzNjg2NWE3NzA2OTVkZmIwOGFjNTA2OWJhZTViMjExMDYyNzk4NmQzOGY2YzcyOTkyN2RmODE0MTIxMDM4OTU4MjIwMDMwNTBkZjJmN2Y2ZTZkNjIxMTVhY2U3ZDM0ZTE3MDg2ZmU1ZmExMGZmZTAwMzVmMjUxMTExMDM3ZmZmZmZmZmY2YzkyNzZjM2IyMGNiNzI4YzlkMTU2MWQwZjFlNjJjNTYwNzA3MTc5MDliMzUxMzFlOThlMjcyOTdhNTIwNmU4MDAwMDAwMDA2NDQxMjQ3MmE0MGU5ZmU0NzY1ZmE5MWYyZTJlY2YwZDQzNmIyMmQ0ZGNlMWEwZTU2OTRjZTcxOGUyMTMwNWM0ZThkMjM0YzM2MjQ2YTA4MDY0MzJjODI2NmQyYjM0NzRiZDMwYzExMTU2YjJhMzQ5ODA3ZGFmMjgyZjVlZWUxNDRkZjg0MTIxMDI2NzA5MjdmZjYwMDFiYjA3ZjZjZDEyNTRmZmNiZGJkMTNiMWU1OGVhN2E2NGJjZGU3MTc0YWMwY2NlMzIzNDc3ZmZmZmZmZmYyMGJlNjM0ZTM2ZTUyZjFhOTgyYjc0MzI3OTE2NmVmYmM4MmM5NTA2MjE2YjkyYjQ3NzJkZjQ2YTdjYTc4MzljMDAwMDAwMDA2NDQxMzdmOTg4MTAxZDZmODE3ZjQxZDE5M2JkM2QzN2JmZmU4MDBiZGZiOGYyZTQzYzg5ZGRmYzczYzRjNTMxM2Q3OGJiNzVkNmRmMDhjYTliZjAwOWQ1YWEyZmNkZmExZTdkMzVlZTIyMzMyZWY3YjEyODdmMDcxZjM0YjkzMDIxMzY0MTIxMDNmODdhNzRiNjRmMTljNWEzMzFjZjAyOTM2ODA2YTRlYmU2NzNjZGRhMzcxMmNhNjMxNjQ0NDRmZjVkZjMyN2FhZmZmZmZmZmYzNGMyZGQ0MDZhYTkxOWU1ZDExMThmODE4NmIzYzM2ZWMxMDY1YTBkOTdkYjFjZmU1MTY4MWZkNWEyZWM5ZmNhMDAwMDAwMDA2NDQxNWM5YTQ0NTdmZGRmMzEzODExNTQ0ZWFjYjFiZmQ5ZTBmZWEzYzIwMzViNzE5Mzk3MGIwOTY3NzUzZWEyMWIzN2E3NDgwMjY1MjY2ZWM2MDNiNzFiODJjZWJjZTlkZTNkNTQxMjc0OTVmOTY4MWQxOTE0MzU4ZGQyNDA3NDRmOGY0MTIxMDM4ZGE1MDY3OTY1ZDdkMzhhZmY3YTMyYWYxYjg4MjhjYjMyZDQ4YzFhOTE5NTg1MWIxNzdhYjc0NTlmZTU5MGVjZmZmZmZmZmYxN2Y2NWEyMDUxYjU4YTljMjQ5NWZhOGJmMmM0ZWYyNjE0YjlkNjAzNzIzYzU4MTU0Nzc1OGE3MmI4ZDA0ZTVhMDEwMDAwMDA2NDQxMGNmNDBiNTM0YjNkMWQ0NWZlOWM5ZjNkOWZiYzM2OTcyYzY0NWY1NmVlMWU3NmJkNDU4NGIzMDUyYWVjYzBkOTczZDA4YzE0ZTBmOGJjMWZkM2FmNzMzM2E3NDI2NDkxOGRiYzE4ZDRlMDNiNjBhNDlmZjBhNmY3YjZjNjI2YmQ0MTIxMDI3MWUwNzZlZWMzOTUzYTAyYzBlNzcxNWQ4NjhjZTI5OTRiNGJjNDc3NGZjNTE0ZGU2ZTZiNTRiMWQ0MTM1NjkyZmZmZmZmZmY0ZjllYWVhNWFlNzE5YTNmYTJjNWQ2ZWIwMzRkN2Q1ZWRlYTViNWNmMzIwMGNhMTEyY2Y1MmMwOGIzMTNhZTUzMDAwMDAwMDA2NDQxMGUxMmNmZmRmYTNmY2Q4ZGY2Y2IzNjMxNDhlOGIxYjBlYjBkNmI2MTUxMWJiNTcyNzZlM2ExYjYwYzM1ZDIyODcwNTUyNTA5MjU4NGY5NzcxN2FiMWM5ZDU0YmQwM2Q1NjI5OTgyZTExOTFiYzRmNDkzMTJlOTc5MDU5NTQ5NDA0MTIxMDM2NjA5NzkzOGQ3MGUzY2ViMzE5OWU5MTcwZGM4MWI1MGNhMmZkMmU0YTEzMTYxZjc5NTI2NDFmY2FhYjFjMTlmZmZmZmZmZmY3OTI0YjMyZDRiNmFlN2RjOGJjNmNjOGFmOWM4N2VmZjQ0NDA0YzE4ZDM1OTk2NmM2ZDVlMzJkZTJhNGI4MmQ4MDAwMDAwMDA2NDQxOTE1MTBlM2RkZjdkZjljOWY1NWEzN2I3ZjYzYjIzNTk1ZDc0NWViMTU4N2UyM2I1ZTRhYzdhNmJkZGE0OTQ5MmIyMGQ0YjhlNDdiODM2NmJjNTlkNTc4NGVkN2I4YmI1NGFjMzhiMDc5ZGYyNDA2ZDRjOTM2ODE1ZDg4MjZjMjI0MTIxMDNjMjNiZmQzYmU3ZWJmODFkYjdhNjQ3ZGNlNTMyMzQ4NjE2NjZkOWNjYjIzNzUyODAzYzJiODk1NmQxOTk5N2M1ZmZmZmZmZmYzNWQ3ZTc4NDFmMDc1NzFhYjRiN2RjZTY5ZWY4NjM1Y2U1NDgyMjM1ZWNiYWJhYmU3YmM2NzZlNmNkOTI2NGY4MDAwMDAwMDA2NDQxNGZmNjdmYzI2NTcyOTZiNWRkMjU2NGFlZmM0NjA5NmExNDYwNzg0MmFjMDA1YTNlYTk4OTFiNDZkY2FhMWY4MmZmODk3OGM5NzI2MjJhNTZkN2M1MDNmYTFlMGY5ZDVjZDMzZjZjNDcyNzY0NTUwODk4ODI3ZWY0YTYxMGMxMTQ0MTIxMDNkNzY0NWNkM2MwYzk4NTM2MzUwZDYzNDE4OTg2NDZjMDE0NWNkZTA2ZGUwZWNlNDYyMjdhYzg4MTBmZmE0NjQxZmZmZmZmZmYyMGE5YzdhMDJlNWM1MWUxMWRmZGIwNGZhZGYxYWU2M2M0YjQyODIyZTJhMjZlNTc2MWNmMjUzMTNkMGQ4NmJmMDEwMDAwMDA2NDQxNGQyNTI1ZTA4MTY0ODA3NTk2YjdiOWUzNzk4M2I1YzVkZTNlNGJiZGQzZjMyNjcyNDEyOGRkNGFmZTUwOGFjY2NlZjM0YWU1MWQwZTczY2FhMDFmOTlmZDgyODExNDE2YzFkMmQwYjQxNzc2MzNmMWJiMWRmNmZhNDAwMjZlYWI0MTIxMDMzNDA2Y2QyYTZmMjhjNTQ4ODMwZTQwN2EwNzkzOTA3YTA1M2RkMmYxYjliZDNlNjdhNTc1YmVkMzc5OTUxM2U0ZmZmZmZmZmY2YWI4NzYyYzdjNGY1MTY4ZTNkOWQ5OTEzOTkzZTA3NmEyNzQwMjI4Zjg0MGFhOTAyZTM3NzY3ZjczZDA4MGU2MDEwMDAwMDA2NDQxOTNhNzgwNDk4M2VlMjg2NmE5NDM5ODI4ZTcyOTBmN2U5MDNjYTMwNDQyZWFlNjZhYmY3YzI0ODVhNTU5ZTVjMGMxNDdlNjg4Yzk3YTljZDljNzM5OTQ4YjUzNDQyYjFiZGZkZjU0YzAxOTI0ZTQ2YmMxZGUzOTQ3ZWE5NzE5ZGQ0MTIxMDMxNzY0NTUyNDRkZWU2ZDIxMDFiMzA4NDY3MGYzYmNkMzU2MDk2ODg5YTdhYjU1MDJiNjAzOWQ5MjQwNjI4MjM3ZmZmZmZmZmYzMzhjZmI0NTNjNTBlNzgyNjQ3NDQ2NDE3N2I4MzUxMTA4ZmU5MGYwYjUxZjNkNDgxNWIzYTM0OTZjYjFkZDhhMDAwMDAwMDA2NDQxMzBhOTg4NDE5ZGE4MWNmMDY1Zjk0MjQxNzAxZThiYzNmMzQ4MmUyMThiMGFkNDkwNWE0NDU2YjBkNzViNzU1YTk3MTA1MTg0MWM3NTQ0ZmUxZjY1NTMzNmNjYzZiOTM4NjE3YjA3YTFjYTcxYzQzZDU5OWJlNDcwZTAxOTE0YjE0MTIxMDNmMTcyNDIyOTkxOWQxZWQzMWU3MmJmZGZiZWVmN2FkN2RmMTFkMjM5YTc4YWMwNjNkODA1ZDgzNDY1NTQ1NjQwZmZmZmZmZmYwMTY4OWFjOTFkMDAwMDAwMDAxOTc2YTkxNDI2YmU3N2VjYTA0ODA0N2RiMTg2ZjkyYzc4NDM0N2M4NzY5YjQ1ZGY4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI2NmI3MjI4OWEzMDczNTRlYTVjZjBlMjVkODBmZjBiYmFhMzdmODEzNzI3MmViNzYxNDc5ODQ3MmY4N2UyOTNmIiwiaGFzaCI6IjY2YjcyMjg5YTMwNzM1NGVhNWNmMGUyNWQ4MGZmMGJiYWEzN2Y4MTM3MjcyZWI3NjE0Nzk4NDcyZjg3ZTI5M2YiLCJ2ZXJzaW9uIjoyLCJzaXplIjoxNjQsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiJhMTI4YmFkMWZmMWRlYWUwNzRmNzlkN2I2ODRjNzBhODIxZTk4M2M4Yjc4Mzc2NGI5MWZlODdiOGRhZGI2ZDljIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEiLCJoZXgiOiI0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEifSwic2VxdWVuY2UiOjF9XSwidm91dCI6W3sidmFsdWUiOjAuMDA1NjYzMzgsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dLCJmZWUiOjEuNjRlLTA2LCJoZXgiOiIwMjAwMDAwMDAxOWM2ZGRiZGFiODg3ZmU5MTRiNzY4M2I3Yzg4M2U5MjFhODcwNGM2ODdiOWRmNzc0ZTBlYTFkZmZkMWJhMjhhMTAwMDAwMDAwNDU0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEwMTAwMDAwMDAxNDJhNDA4MDAwMDAwMDAwMDIzYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NzAwMDAwMDAwIn0seyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsImhhc2giOiI2NzczZmQ4MWMyZjZlZDI1ODdiZDYxYTZlMGM2YTg3M2Y5ZjJiNjIyZmM1ZTljMWY4NTA0ZDA2MjEwYzNjNGFmIiwidmVyc2lvbiI6Miwic2l6ZSI6MjEzNCwibG9ja3RpbWUiOjQyLCJ2aW4iOlt7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkZTdhZTdiMTBkNmQ2ZDliYmMwMTcwYWIyYzJlZDQ0NTBlZDRjYjE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkZTdhZTdiMTBkNmQ2ZDliYmMwMTcwYWIyYzJlZDQ0NTBlZDRjYjE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDcxYTcxNDllZTBiNGUxNDhkNzdhYWRiNGMwZTcxYTEyZjBmMzgyYTk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDcxYTcxNDllZTBiNGUxNDhkNzdhYWRiNGMwZTcxYTEyZjBmMzgyYTk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6Mywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQxYjIxMzQ5YjgwMTllYzczOGE4ZGM2ZjJmYzQyZGFjYmIwNjQ0MWQwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQxYjIxMzQ5YjgwMTllYzczOGE4ZGM2ZjJmYzQyZGFjYmIwNjQ0MWQwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0Ijo2LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDJhMThlZjA2YjFiNzFiMmUzNzk3YTk2YWNlOTBhMDRiOWIyOGMxMmI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDJhMThlZjA2YjFiNzFiMmUzNzk3YTk2YWNlOTBhMDRiOWIyOGMxMmI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOjQsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZDA3MzZlZTkwMTE3ZjRlZDdlOWJhZDVhZTA5YjBhYjBjODkyOTZiNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZDA3MzZlZTkwMTE3ZjRlZDdlOWJhZDVhZTA5YjBhYjBjODkyOTZiNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6NSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRjZDZkNTY3ZGM2MzRhYzhmYTg4NTYxZTg3YjI4ZjkxMThhOGFkYTdhODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRjZDZkNTY3ZGM2MzRhYzhmYTg4NTYxZTg3YjI4ZjkxMThhOGFkYTdhODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0Ijo3LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNGE3M2Q3MDg5ZTQ4YWJmMzdiNGRiMTVjZmViNGJkNDU3YjVhMTA4MWY4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNGE3M2Q3MDg5ZTQ4YWJmMzdiNGRiMTVjZmViNGJkNDU3YjVhMTA4MWY4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOjgsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjZWQ2MzgwNDRjYTI3ZjQzMmRjZmFmMTMzZTA5ZjljODI4NzlmYmM2MzYzMjBiOTA2YjcwNjViNjZhZjllOGZiIiwidm91dCI6MTAsInNjcmlwdFNpZyI6eyJhc20iOiIwYTlhODM2MWI4YmZhMmUzMzEwZmNhZmJmMTY5NDAxM2VlNGQwYzhiNDA2ZTVlMGEyZTc5N2QxZmYzNTc5YjhhY2RiOTEyNWM4MDM1MTAzOTU2MWVmYzg4ZmZiZGY0NTdmMTgzNzIxNTUxNzY2OTAzZDRkNzMxZjhkNTNiMzBhMVtBTEx8Rk9SS0lEfFVUWE9TXSAwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjMiLCJoZXgiOiI0MTBhOWE4MzYxYjhiZmEyZTMzMTBmY2FmYmYxNjk0MDEzZWU0ZDBjOGI0MDZlNWUwYTJlNzk3ZDFmZjM1NzliOGFjZGI5MTI1YzgwMzUxMDM5NTYxZWZjODhmZmJkZjQ1N2YxODM3MjE1NTE3NjY5MDNkNGQ3MzFmOGQ1M2IzMGExNjEyMTAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2MyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOjMwLCJzY3JpcHRTaWciOnsiYXNtIjoiNjVhYzVmZTlmZDNlNzY2M2Q5NTU1Mzg5MmRlZDM5YjRlOGVjZjc5M2I2MzRmYmQ4NDA0NzkwZjhkNjk3MWYzN2MwNGE3Yzk1MWFhMjNmNDZjYmZmZTY1MGEwYjllYjU0MjUwNTg4MzE2MDYxMDM5OWE2ZDFmYmU0NWE2ZjNjMmZbQUxMfEZPUktJRHxVVFhPU10gMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzIiwiaGV4IjoiNDE2NWFjNWZlOWZkM2U3NjYzZDk1NTUzODkyZGVkMzliNGU4ZWNmNzkzYjYzNGZiZDg0MDQ3OTBmOGQ2OTcxZjM3YzA0YTdjOTUxYWEyM2Y0NmNiZmZlNjUwYTBiOWViNTQyNTA1ODgzMTYwNjEwMzk5YTZkMWZiZTQ1YTZmM2MyZjYxMjEwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjEwMy41MDYyMzAwMiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NCBPUF9FUVVBTCIsImhleCI6ImFhMjA1MGZjMTZmZmIzNzdjZDRlMWQ4YjRkZWZhMzc1NmQxMjIxYmFjZmUzMzA0ZTc3MDVkZTQxNjdkNzdjMWJhMzY0ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGcwYzlobGtkbXU2bnNhM2R4N2xnbTRkNWZ6cndrMHV2Y3l1YWM5bWVxazA0bXVydzNrZ3loZjVtbjd4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDg2ODY3OCJ9fSx7InZhbHVlIjo5Mi4yNDA3MjM2OSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMCBPUF9FUVVBTCIsImhleCI6ImFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjB3ZzlwMGx5NXRmMm5mOXY3NzNoM2Y5eGxyanB3eHFmN25oMjlzNXQ4M2N6NjVzN2t0cWFocjl5YWdkIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDMzODc3NiJ9fSx7InZhbHVlIjo3MS4xNjkzMTgyLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDJjZjliZjRkYWIwNWEyMzUyMWRjMjYyOTM4MzAyNjJmZmVjNzYxNDY2ODgwOThiYjMxMjRhYzNmYjc2NDAwMzY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2azBuMDZkNHZ6NnlkZnBtc256andwc3ljaGxhM21wZ2U1Z3B4OW14eWoyYzBhaHZzcXJ2ZGZ4Y3Y3eWsiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzMzQ3NjMwIn19LHsidmFsdWUiOjM2LjA2NjM1NDQyLCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDY3YWQxYzY0OWIwOWJhNThiYTE5ZWIzYWQzNWY0MjQxYmEwNjBlOGJiODk3YWRjZjZiZGRkMzZhODBkOGU4YTY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkbjY2OHJ5bnZ5bTVrOTZyODRuNDU2bGdmcW01cHN3M3d1ZjB0dzBkMHdheDY1cW1yNTJ2cGZuMjVtdGsiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxNjk2NDcyIn19LHsidmFsdWUiOjE3LjUwMjU2NDA1LCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDIwYjA1Yjc0YWI1NmM5ODQ4MDMyZTliYTRmZDA5NDI3NWNjYzdmMzYyOGFkMTg4NmZjZTU2ZTkzMGQxMjczY2Q4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2c3Rxa201NGR0dm5weXF4dDVtNW43c2pzbjRlbnJseGM1MjZ4eXhsbmprYXljZHpmZXU2OXJleXptcXgiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI4MjMyNzkifX0seyJ2YWx1ZSI6MTEuMzUwMzQ1MzUsIm4iOjUsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGIzNDdkNjhjOGQyY2RiMzIwNjZiZTlhZjgwNGM2ZmZiOGFiYmI0NDFiZGYzNmNhZjM4MzQzNmJjZmNhYTM1Y2IgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdlNTA0NXYzNWtka3ZzeGQwNTZscXp2ZGxhYzR3YTVneDdseG05MDhxNnJkMDh1NGc2dWtuMzcyZ2d3bCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjUzMzg4NiJ9fSx7InZhbHVlIjoxMC41NDYwMTg0NCwibiI6Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2MyYmNjYTIwODlhOWYwZjhkYWVjMGQzYjQ1YTZjNTI1ODFjNTY3NjY1Y2M1MWViZGE5YTIwMmQ4YTMzZGNmYyBPUF9FUVVBTCIsImhleCI6ImFhMjA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZDd6aG45enB6ZGY3cnVkNG1xZDhkejZkM2Y5czh6a3dlanVjNTB0bTJkenF0djJ4MHcwYzU0eTM5aHA2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDk2MDUyIn19LHsidmFsdWUiOjUuMjE3ODI3MTIsIm4iOjcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwZjFjMzNiYzdkYTA5OTFkMjZmOGJiZjE1MGM5MDM1NjhiMmMzZGM4NzI3MzcwODYzZjRjM2Q5Zjk2NTViMzc5MTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cDBjdXh3NzhtZ3llcjVuMDN3bDMycnlzeDQ1dDlzN3VzdW5ud3pycjducGFuN3Q5dHZtZXpyYXUzanVyOSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjI0NTQzMiJ9fSx7InZhbHVlIjo0LjQwMzM4MzAzLCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YW15bmxlcHYzcG5nbnE2cmhxNTk1eDBsZm5nNncwd3A4NWpxM3JydjJ3amNsdmh0M3VneDRrYWo0aHUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMDcxMjIifX0seyJ2YWx1ZSI6OGUtMDYsIm4iOjksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzMzE1NyJ9fSx7InZhbHVlIjozNy4wMjQ1ODQ3NiwibiI6MTAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX19XSwiZmVlIjoyLjEzNGUtMDUsImhleCI6IjAyMDAwMDAwMGI5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MDAwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGRlN2FlN2IxMGQ2ZDZkOWJiYzAxNzBhYjJjMmVkNDQ1MGVkNGNiMTg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmOTk4NzU0NmQ1ZjRlZmM0MzAyMjJiMTk2ODNlODQ3MGE3OTlkYTE5MTI0YjAzMGZiOGFhYzY0ZTkyZjc1Y2I3ODAxMDAwMDAwNDU0NDc0NjM3NmE5MTQ3MWE3MTQ5ZWUwYjRlMTQ4ZDc3YWFkYjRjMGU3MWExMmYwZjM4MmE5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjk5ODc1NDZkNWY0ZWZjNDMwMjIyYjE5NjgzZTg0NzBhNzk5ZGExOTEyNGIwMzBmYjhhYWM2NGU5MmY3NWNiNzgwMjAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MDMwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDFiMjEzNDliODAxOWVjNzM4YThkYzZmMmZjNDJkYWNiYjA2NDQxZDA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmOTk4NzU0NmQ1ZjRlZmM0MzAyMjJiMTk2ODNlODQ3MGE3OTlkYTE5MTI0YjAzMGZiOGFhYzY0ZTkyZjc1Y2I3ODA2MDAwMDAwNDU0NDc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjk5ODc1NDZkNWY0ZWZjNDMwMjIyYjE5NjgzZTg0NzBhNzk5ZGExOTEyNGIwMzBmYjhhYWM2NGU5MmY3NWNiNzgwNDAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZDA3MzZlZTkwMTE3ZjRlZDdlOWJhZDVhZTA5YjBhYjBjODkyOTZiNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MDUwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGNkNmQ1NjdkYzYzNGFjOGZhODg1NjFlODdiMjhmOTExOGE4YWRhN2E4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmOTk4NzU0NmQ1ZjRlZmM0MzAyMjJiMTk2ODNlODQ3MGE3OTlkYTE5MTI0YjAzMGZiOGFhYzY0ZTkyZjc1Y2I3ODA3MDAwMDAwNDU0NDc0NjM3NmE5MTRhNzNkNzA4OWU0OGFiZjM3YjRkYjE1Y2ZlYjRiZDQ1N2I1YTEwODFmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjk5ODc1NDZkNWY0ZWZjNDMwMjIyYjE5NjgzZTg0NzBhNzk5ZGExOTEyNGIwMzBmYjhhYWM2NGU5MmY3NWNiNzgwODAwMDAwMDQ1NDQ3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZmYmU4Zjk2YWI2NjU3MDZiOTAwYjMyMzZjNmZiNzkyOGM4ZjkwOTNlMTNhZmNmMmQ0MzdmYTI0YzA0MzhkNmNlMGEwMDAwMDA2NDQxMGE5YTgzNjFiOGJmYTJlMzMxMGZjYWZiZjE2OTQwMTNlZTRkMGM4YjQwNmU1ZTBhMmU3OTdkMWZmMzU3OWI4YWNkYjkxMjVjODAzNTEwMzk1NjFlZmM4OGZmYmRmNDU3ZjE4MzcyMTU1MTc2NjkwM2Q0ZDczMWY4ZDUzYjMwYTE2MTIxMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzZmZmZmZmZmY5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MWUwMDAwMDA2NDQxNjVhYzVmZTlmZDNlNzY2M2Q5NTU1Mzg5MmRlZDM5YjRlOGVjZjc5M2I2MzRmYmQ4NDA0NzkwZjhkNjk3MWYzN2MwNGE3Yzk1MWFhMjNmNDZjYmZmZTY1MGEwYjllYjU0MjUwNTg4MzE2MDYxMDM5OWE2ZDFmYmU0NWE2ZjNjMmY2MTIxMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzZmZmZmZmZmYwYjFhZjlmMTY4MDIwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmU0NjRhNGEwMGFhMjA1MGZjMTZmZmIzNzdjZDRlMWQ4YjRkZWZhMzc1NmQxMjIxYmFjZmUzMzA0ZTc3MDVkZTQxNjdkNzdjMWJhMzY0ODdiMTJjY2MyNTAyMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlNTgzNDQyMDBhYTIwMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMDg3ZWNjMjMzYTgwMTAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZWFlMTQzMzAwYWEyMDJjZjliZjRkYWIwNWEyMzUyMWRjMjYyOTM4MzAyNjJmZmVjNzYxNDY2ODgwOThiYjMxMjRhYzNmYjc2NDAwMzY4N2IyZTNmOGQ2MDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVkOGUyMTkwMGFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODcxNWNiNTI2ODAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlZWY4ZjBjMDBhYTIwMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZDg3YTc0MGE3NDMwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTdlMjUwODAwYWEyMGIzNDdkNjhjOGQyY2RiMzIwNjZiZTlhZjgwNGM2ZmZiOGFiYmI0NDFiZGYzNmNhZjM4MzQzNmJjZmNhYTM1Y2I4Nzc0ZjJkYjNlMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmViNDkxMDcwMGFhMjA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjODdiOGM1MTkxZjAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlYjhiZTAzMDBhYTIwZjFjMzNiYzdkYTA5OTFkMjZmOGJiZjE1MGM5MDM1NjhiMmMzZGM4NzI3MzcwODYzZjRjM2Q5Zjk2NTViMzc5MTg3N2YwNzNmMWEwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTEyMjkwMzAwYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NzIwMDMwMDAwMDAwMDAwMDAzZWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQ4NTgxNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWM2YzA4YWZkYzAwMDAwMDAwMTk3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYzJhMDAwMDAwIn0seyJ0eGlkIjoiNjdjNzRjNzllNTM2OTM2MDg4NTYzM2FmZGM1MTJkNzU5YjFjOTQwMzY5OTU2MmUwYzBiY2FkMGU3ZWMwMzllMSIsImhhc2giOiI2N2M3NGM3OWU1MzY5MzYwODg1NjMzYWZkYzUxMmQ3NTliMWM5NDAzNjk5NTYyZTBjMGJjYWQwZTdlYzAzOWUxIiwidmVyc2lvbiI6Miwic2l6ZSI6MjI2LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiNzljNTM3YjQ4MDk5ZWRlM2U0Yzg2ZjRlODUwMGZjODMwYTg3ZjM3ODI5MzFiMzQwNGY4ZmJiYTA0ZjhhMGU0MSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwYmJiOGJhN2ZjZDI2OTMwZmIwNDJmMWNjYTI5MTViMzA2YTIzYmZhMmJmYTBhNDNiMjBjOWU0OGM3YjU5OTgwYjAyMjA2M2RmOWE4OTcxNGJlMjMwZDA1MmVhMTE0NDkxYzQ2OTE5NGMwOWFlODZlZjk2MTk5NjZiMmIyMzVkOWRkNGFjW0FMTHxGT1JLSURdIDAzM2I1ZTBjMDcwOWQ1YzZiNjE3MTk5NGQ3M2UxMmNhZDU0NGNkMTc1MGIxYmRiMGFjZWYyZWQ0NWYzYzc2ZTU3OCIsImhleCI6IjQ4MzA0NTAyMjEwMGJiYjhiYTdmY2QyNjkzMGZiMDQyZjFjY2EyOTE1YjMwNmEyM2JmYTJiZmEwYTQzYjIwYzllNDhjN2I1OTk4MGIwMjIwNjNkZjlhODk3MTRiZTIzMGQwNTJlYTExNDQ5MWM0NjkxOTRjMDlhZTg2ZWY5NjE5OTY2YjJiMjM1ZDlkZDRhYzQxMjEwMzNiNWUwYzA3MDlkNWM2YjYxNzE5OTRkNzNlMTJjYWQ1NDRjZDE3NTBiMWJkYjBhY2VmMmVkNDVmM2M3NmU1NzgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMDgyMzQ1MTgsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAyNTllMDMzOWM5YzZmMmNiZWNjMWIyOTYwNzMzMDc2NzVlNDY2YWYxIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MjU5ZTAzMzljOWM2ZjJjYmVjYzFiMjk2MDczMzA3Njc1ZTQ2NmFmMTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWpldXFlZWU4cjA5amx2Y3hlZnZwZW5xYW40dTNuMjd5ZXJhM2hxNTYiXX19LHsidmFsdWUiOjE2Ljk4NTU5NTA4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTZlZTA0Y2I3NzI4NzU5YThkZWU2YmVkM2E3ODEzMTdhM2NlM2QyZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpud3VweHR3dTU4dHg1ZGFlNDc2d25jenZ0NjhuM2E5Y200bXI5cHRtIl19fV0sImZlZSI6Mi43ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTQxMGU4YTRmYTBiYjhmNGY0MGIzMzEyOTc4ZjM4NzBhODNmYzAwODU0ZTZmYzhlNGUzZWQ5OTgwYjQzN2M1NzkwMTAwMDAwMDZiNDgzMDQ1MDIyMTAwYmJiOGJhN2ZjZDI2OTMwZmIwNDJmMWNjYTI5MTViMzA2YTIzYmZhMmJmYTBhNDNiMjBjOWU0OGM3YjU5OTgwYjAyMjA2M2RmOWE4OTcxNGJlMjMwZDA1MmVhMTE0NDkxYzQ2OTE5NGMwOWFlODZlZjk2MTk5NjZiMmIyMzVkOWRkNGFjNDEyMTAzM2I1ZTBjMDcwOWQ1YzZiNjE3MTk5NGQ3M2UxMmNhZDU0NGNkMTc1MGIxYmRiMGFjZWYyZWQ0NWYzYzc2ZTU3OGZmZmZmZmZmMDIxNmE2N2QwMDAwMDAwMDAwMTk3NmE5MTQyNTllMDMzOWM5YzZmMmNiZWNjMWIyOTYwNzMzMDc2NzVlNDY2YWYxODhhYzE0ZjYzZDY1MDAwMDAwMDAxOTc2YTkxNGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmU4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI2YTAwYzUxZWJkMTlmYmRiODZhMWFjZDYyOTUzMTg2NGFiNDcyYjQ0MDkxZTQ4ZGJiNmJhZmZjZWU5YjhlODcwIiwiaGFzaCI6IjZhMDBjNTFlYmQxOWZiZGI4NmExYWNkNjI5NTMxODY0YWI0NzJiNDQwOTFlNDhkYmI2YmFmZmNlZTliOGU4NzAiLCJ2ZXJzaW9uIjoyLCJzaXplIjo3ODMsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI1NmU3YmEwOTRjZDZiNjZhYWViYmUzNWQ5MjBiODliNjkxMjUzOTcwOTVmMzYxNTQzMmYyNTM3YTM3YTNlNWRkIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjA2YThiOTNiYzE2ZjJlZmVlN2Y2YjAxZWVjOGQ0N2M2MWY5NTRiYzk0NzMzYzM4ZWI3MTYyMWU1MTdiYWZmNzU2ZTcwM2E2NWQ3YTY2YzI3NDY5YzExMWVkNjkxNDI5MTg5YWYzNTE1YmQxMzg2NzUyMjI3OGVhZjViMGEyOWQ5W0FMTHxGT1JLSURdIDAyYWRlN2IwNWJkYTkxZGE5N2I2ZGUwODJhNzZmN2VhYjRiMjYzMWNmNjg5ZjlkY2YyNDkwM2ZjNzMyZTliOTM2ZCIsImhleCI6IjQxMDZhOGI5M2JjMTZmMmVmZWU3ZjZiMDFlZWM4ZDQ3YzYxZjk1NGJjOTQ3MzNjMzhlYjcxNjIxZTUxN2JhZmY3NTZlNzAzYTY1ZDdhNjZjMjc0NjljMTExZWQ2OTE0MjkxODlhZjM1MTViZDEzODY3NTIyMjc4ZWFmNWIwYTI5ZDk0MTIxMDJhZGU3YjA1YmRhOTFkYTk3YjZkZTA4MmE3NmY3ZWFiNGIyNjMxY2Y2ODlmOWRjZjI0OTAzZmM3MzJlOWI5MzZkIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI2MDQwYzA3NDFlNWUzODM2NDRlYmVmZGMxYzU3MDVhOTM5MmUwYTY2ZjYzMWRmNDdlMGM3ZjI1ODBmNTkyOTk5Iiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6ImMwNWIyMWUzMmFjZGQzNmYwOTk1YjJlNWFkYTc4Yjc2NGVlNjRkYjM4MzU3NzRhOTI0ZDkwOThlZjViMjNjNjE2MjUzZTVlMTc2Y2YwZjJlZGU5MWFiNWZmZDg2MTM0NTIwZjQwODFhMThlZWMxMWY3ZTU1YTQ2NmI0NDJhNDUyW0FMTHxGT1JLSURdIDAyMzgwODhlZGFmZWFmNDBhNzg2NDFjY2QxMzRlMTcyZjBiYTlhMGY3YmYyNDZiYmVjN2MxNzFmNWVmNTFlOGYxNSIsImhleCI6IjQxYzA1YjIxZTMyYWNkZDM2ZjA5OTViMmU1YWRhNzhiNzY0ZWU2NGRiMzgzNTc3NGE5MjRkOTA5OGVmNWIyM2M2MTYyNTNlNWUxNzZjZjBmMmVkZTkxYWI1ZmZkODYxMzQ1MjBmNDA4MWExOGVlYzExZjdlNTVhNDY2YjQ0MmE0NTI0MTIxMDIzODA4OGVkYWZlYWY0MGE3ODY0MWNjZDEzNGUxNzJmMGJhOWEwZjdiZjI0NmJiZWM3YzE3MWY1ZWY1MWU4ZjE1In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI5ZDczNjFhOWYxN2FkZWY0ZWE2YTA3OGU0YmVkZDRlZTNkNGQ3MDgzN2IxY2YxZmIwNGUzNzNhMmM0MTE2OTdkIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjVmOGM5ZGU5OTY1Yzk1MTA1MGJlMDM4ODFlODQwNGM0NDkyYzJlZGE5N2ZhMDc0MTQwMDU2OGRjZDliYWRlMGVkMWJjMzE4ZTEyMzIxYTVkOWNmYjUyNGY4NTZlNjYyNWFlMDQ2MjM4MDk4NzY5MmE5YTg4YTQ4ZWFlZTg5NzI2W0FMTHxGT1JLSURdIDAyMzgwODhlZGFmZWFmNDBhNzg2NDFjY2QxMzRlMTcyZjBiYTlhMGY3YmYyNDZiYmVjN2MxNzFmNWVmNTFlOGYxNSIsImhleCI6IjQxNWY4YzlkZTk5NjVjOTUxMDUwYmUwMzg4MWU4NDA0YzQ0OTJjMmVkYTk3ZmEwNzQxNDAwNTY4ZGNkOWJhZGUwZWQxYmMzMThlMTIzMjFhNWQ5Y2ZiNTI0Zjg1NmU2NjI1YWUwNDYyMzgwOTg3NjkyYTlhODhhNDhlYWVlODk3MjY0MTIxMDIzODA4OGVkYWZlYWY0MGE3ODY0MWNjZDEzNGUxNzJmMGJhOWEwZjdiZjI0NmJiZWM3YzE3MWY1ZWY1MWU4ZjE1In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJkNDVlZmU1ZmM4MTA2N2YwZmM3NmJhYWNjMzAyNjkxZWI1NGE5MjYzNzIwOWMyZTQzOGZmMjM4OTc3MzBlNWFhIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6ImQyOTgxZmZiNjUxOTYxYWIxNTJjNzQwMWYxYWM0YjAxY2EwZTdiYjMxYmU3NWRjOWEyZDEzZDJjN2JhZWIyOGNkNzQ2OGIyYmViZDA1MTBhOGMxYjU3Y2Q5MmU4M2QzNzgyZTQ1YmUyMWZhNDJmNDdiODJhZjE0MmU4NWM2YWY2W0FMTHxGT1JLSURdIDAyMzgwODhlZGFmZWFmNDBhNzg2NDFjY2QxMzRlMTcyZjBiYTlhMGY3YmYyNDZiYmVjN2MxNzFmNWVmNTFlOGYxNSIsImhleCI6IjQxZDI5ODFmZmI2NTE5NjFhYjE1MmM3NDAxZjFhYzRiMDFjYTBlN2JiMzFiZTc1ZGM5YTJkMTNkMmM3YmFlYjI4Y2Q3NDY4YjJiZWJkMDUxMGE4YzFiNTdjZDkyZTgzZDM3ODJlNDViZTIxZmE0MmY0N2I4MmFmMTQyZTg1YzZhZjY0MTIxMDIzODA4OGVkYWZlYWY0MGE3ODY0MWNjZDEzNGUxNzJmMGJhOWEwZjdiZjI0NmJiZWM3YzE3MWY1ZWY1MWU4ZjE1In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJlZTI0MzMzMTJkNzMyMTY2ZWEyOThiNmM0MzJjYzBjOTkwNzEyYmZkMGRkYTI5MWQwZmJjN2NhMjc0NjhlZTMwIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6ImVkMzQxMTBiNGRlZWFhZmIyZjZlMWE1YmZjMmQxMGJmMGVjZDYzZGJkMzQyN2IxNDIwZTkxNGMwYTJlN2VjYTE3OWNjMzQ4NDM5NWJkY2Q5YzM5YmQ1NGNjZTY3Y2FkYmFhNTk1MjM2Yjc5ZDFlYjEzMTc1MjE2NjdkYjcyZTVjW0FMTHxGT1JLSURdIDAyM2M4ZTE1MjNkNjlmYzczNDk0NDY4YTJlOGVhOWQ3ZmZjNTJlMzA2NzEwMjE5NzdmNzNjNWEyNGIyNjAzMjI4ZSIsImhleCI6IjQxZWQzNDExMGI0ZGVlYWFmYjJmNmUxYTViZmMyZDEwYmYwZWNkNjNkYmQzNDI3YjE0MjBlOTE0YzBhMmU3ZWNhMTc5Y2MzNDg0Mzk1YmRjZDljMzliZDU0Y2NlNjdjYWRiYWE1OTUyMzZiNzlkMWViMTMxNzUyMTY2N2RiNzJlNWM0MTIxMDIzYzhlMTUyM2Q2OWZjNzM0OTQ0NjhhMmU4ZWE5ZDdmZmM1MmUzMDY3MTAyMTk3N2Y3M2M1YTI0YjI2MDMyMjhlIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjAxOTk5MTgyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNDIwN2Q3OWU2ZjliZjYzYTkzZDhiODFkMmI0ZGI2NmVjOWJmOWY0OCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDQyMDdkNzllNmY5YmY2M2E5M2Q4YjgxZDJiNGRiNjZlYzliZjlmNDg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBwcTA0dTdkN2Rsdnc1bm16dXA2MjZka2Vodm4wdWxmcXZuanZhMzByIl19fSx7InZhbHVlIjowLjAyMzkwNTAyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2E5NmIyYmIwYWE3YmQzMzBkNzg2ZTZiNTJlZDAwYzc3NmI5OTJmNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdhOTZiMmJiMGFhN2JkMzMwZDc4NmU2YjUyZWQwMGM3NzZiOTkyZjQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBhZmR2NG1wMm5tNnZjZDBwaHhrNWhkcXJyaGR3dmo3czRkN21hNDNtIl19fV0sImZlZSI6OC4xOGUtMDYsImhleCI6IjAyMDAwMDAwMDVkZGU1YTMzNzdhNTNmMjMyNTQ2MWYzOTU3MDM5MjU5MWI2ODkwYjkyNWRlM2JiYWU2YWI2ZDY0YzA5YmFlNzU2MDAwMDAwMDA2NDQxMDZhOGI5M2JjMTZmMmVmZWU3ZjZiMDFlZWM4ZDQ3YzYxZjk1NGJjOTQ3MzNjMzhlYjcxNjIxZTUxN2JhZmY3NTZlNzAzYTY1ZDdhNjZjMjc0NjljMTExZWQ2OTE0MjkxODlhZjM1MTViZDEzODY3NTIyMjc4ZWFmNWIwYTI5ZDk0MTIxMDJhZGU3YjA1YmRhOTFkYTk3YjZkZTA4MmE3NmY3ZWFiNGIyNjMxY2Y2ODlmOWRjZjI0OTAzZmM3MzJlOWI5MzZkZmZmZmZmZmY5OTI5NTkwZjU4ZjJjN2UwNDdkZjMxZjY2NjBhMmUzOWE5MDU1NzFjZGNlZmViNDQzNjM4NWUxZTc0YzA0MDYwMDEwMDAwMDA2NDQxYzA1YjIxZTMyYWNkZDM2ZjA5OTViMmU1YWRhNzhiNzY0ZWU2NGRiMzgzNTc3NGE5MjRkOTA5OGVmNWIyM2M2MTYyNTNlNWUxNzZjZjBmMmVkZTkxYWI1ZmZkODYxMzQ1MjBmNDA4MWExOGVlYzExZjdlNTVhNDY2YjQ0MmE0NTI0MTIxMDIzODA4OGVkYWZlYWY0MGE3ODY0MWNjZDEzNGUxNzJmMGJhOWEwZjdiZjI0NmJiZWM3YzE3MWY1ZWY1MWU4ZjE1ZmZmZmZmZmY3ZDY5MTFjNGEyNzNlMzA0ZmJmMTFjN2I4MzcwNGQzZGVlZDRlZDRiOGUwNzZhZWFmNGRlN2FmMWE5NjE3MzlkMDAwMDAwMDA2NDQxNWY4YzlkZTk5NjVjOTUxMDUwYmUwMzg4MWU4NDA0YzQ0OTJjMmVkYTk3ZmEwNzQxNDAwNTY4ZGNkOWJhZGUwZWQxYmMzMThlMTIzMjFhNWQ5Y2ZiNTI0Zjg1NmU2NjI1YWUwNDYyMzgwOTg3NjkyYTlhODhhNDhlYWVlODk3MjY0MTIxMDIzODA4OGVkYWZlYWY0MGE3ODY0MWNjZDEzNGUxNzJmMGJhOWEwZjdiZjI0NmJiZWM3YzE3MWY1ZWY1MWU4ZjE1ZmZmZmZmZmZhYWU1MzA3Nzg5MjNmZjM4ZTRjMjA5NzI2MzkyNGFiNTFlNjkwMmMzYWNiYTc2ZmNmMDY3MTBjODVmZmU1ZWQ0MDEwMDAwMDA2NDQxZDI5ODFmZmI2NTE5NjFhYjE1MmM3NDAxZjFhYzRiMDFjYTBlN2JiMzFiZTc1ZGM5YTJkMTNkMmM3YmFlYjI4Y2Q3NDY4YjJiZWJkMDUxMGE4YzFiNTdjZDkyZTgzZDM3ODJlNDViZTIxZmE0MmY0N2I4MmFmMTQyZTg1YzZhZjY0MTIxMDIzODA4OGVkYWZlYWY0MGE3ODY0MWNjZDEzNGUxNzJmMGJhOWEwZjdiZjI0NmJiZWM3YzE3MWY1ZWY1MWU4ZjE1ZmZmZmZmZmYzMGVlNjg3NGEyN2NiYzBmMWQyOWRhMGRmZDJiNzE5MGM5YzAyYzQzNmM4YjI5ZWE2NjIxNzMyZDMxMzMyNGVlMDEwMDAwMDA2NDQxZWQzNDExMGI0ZGVlYWFmYjJmNmUxYTViZmMyZDEwYmYwZWNkNjNkYmQzNDI3YjE0MjBlOTE0YzBhMmU3ZWNhMTc5Y2MzNDg0Mzk1YmRjZDljMzliZDU0Y2NlNjdjYWRiYWE1OTUyMzZiNzlkMWViMTMxNzUyMTY2N2RiNzJlNWM0MTIxMDIzYzhlMTUyM2Q2OWZjNzM0OTQ0NjhhMmU4ZWE5ZDdmZmM1MmUzMDY3MTAyMTk3N2Y3M2M1YTI0YjI2MDMyMjhlZmZmZmZmZmYwMjRlODExZTAwMDAwMDAwMDAxOTc2YTkxNDQyMDdkNzllNmY5YmY2M2E5M2Q4YjgxZDJiNGRiNjZlYzliZjlmNDg4OGFjZTY3OTI0MDAwMDAwMDAwMDE5NzZhOTE0N2E5NmIyYmIwYWE3YmQzMzBkNzg2ZTZiNTJlZDAwYzc3NmI5OTJmNDg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjZhZmJjZDA2NzdkMWU2OWQ1MjQ4OWNiZjA5ZjNlNTI3MGExYTEyMTFjZjQwNTc4ZDNkOWI4ZDYwZmI2ZjRiNTciLCJoYXNoIjoiNmFmYmNkMDY3N2QxZTY5ZDUyNDg5Y2JmMDlmM2U1MjcwYTFhMTIxMWNmNDA1NzhkM2Q5YjhkNjBmYjZmNGI1NyIsInZlcnNpb24iOjIsInNpemUiOjIxOSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImEwZmFlYTdhMDUyMjVjZjQzMDk4MzY2Nzk0MWVlYWQ1OGU5MWE5Mzc4MWE0ZmUzODc1M2RlNjEwM2VhMjZlOWMiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNzMxZmQ2YzU5NDZmOTE3MmE5NDM5MGI5NjlkZTk1ZGE2MTU5YTY0MmM5ODAyYmZmMzM4ZTUzOTAwZmJlMzJmNjU2YzI5NmJiMGVmZmNlM2ExYzQ1ZjE1NTlhODQzZjAyYjFlYTcxMDBhZjhlMWI3MmVhZjdkOTcxNTNjZjhjZTRbQUxMfEZPUktJRF0gMDNjMjgwYTkzMzZkNWFjMWJiZThkMTZhNDIxMDk0YTc2NWEyYzRkMjFhMWRjMjAwZmYzMzkyZTA1MmY1NTg2MGVkIiwiaGV4IjoiNDE3MzFmZDZjNTk0NmY5MTcyYTk0MzkwYjk2OWRlOTVkYTYxNTlhNjQyYzk4MDJiZmYzMzhlNTM5MDBmYmUzMmY2NTZjMjk2YmIwZWZmY2UzYTFjNDVmMTU1OWE4NDNmMDJiMWVhNzEwMGFmOGUxYjcyZWFmN2Q5NzE1M2NmOGNlNDQxMjEwM2MyODBhOTMzNmQ1YWMxYmJlOGQxNmE0MjEwOTRhNzY1YTJjNGQyMWExZGMyMDBmZjMzOTJlMDUyZjU1ODYwZWQifSwic2VxdWVuY2UiOjB9XSwidm91dCI6W3sidmFsdWUiOjAuMDAxMTksIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2MDAwMjUxNTQyY2JiMWY4OTM4YzBkZDBlZTJkYzBlMzQ4MDJiYmI1IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NjAwMDI1MTU0MmNiYjFmODkzOGMwZGQwZWUyZGMwZTM0ODAyYmJiNTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcHNxcWZnNGd0OW1yN3luM3N4YXBtM2RjcjM1c3E0bWs1ZnNtdXJtNGwiXX19LHsidmFsdWUiOjAuMDMyNDg1NTIsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2Zjc1MmRlZGY5OWY3YmE0Nzg4M2EyNTMwYzZkMjkxZGI4Mzg4NzRhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NmY3NTJkZWRmOTlmN2JhNDc4ODNhMjUzMGM2ZDI5MWRiODM4ODc0YTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGhoMnQwZGx4MGhoZnJjc3czOXhycmQ5eXdtc3d5OGZnbDV6dGw0emYiXX19XSwiZmVlIjoyLjJlLTA2LCJoZXgiOiIwMjAwMDAwMDAxOWM2ZWEyM2UxMGU2M2Q3NTM4ZmVhNDgxMzdhOTkxOGVkNWVhMWU5NDY3MzY5ODMwZjQ1YzIyMDU3YWVhZmFhMDAxMDAwMDAwNjQ0MTczMWZkNmM1OTQ2ZjkxNzJhOTQzOTBiOTY5ZGU5NWRhNjE1OWE2NDJjOTgwMmJmZjMzOGU1MzkwMGZiZTMyZjY1NmMyOTZiYjBlZmZjZTNhMWM0NWYxNTU5YTg0M2YwMmIxZWE3MTAwYWY4ZTFiNzJlYWY3ZDk3MTUzY2Y4Y2U0NDEyMTAzYzI4MGE5MzM2ZDVhYzFiYmU4ZDE2YTQyMTA5NGE3NjVhMmM0ZDIxYTFkYzIwMGZmMzM5MmUwNTJmNTU4NjBlZDAwMDAwMDAwMDJkOGQwMDEwMDAwMDAwMDAwMTk3NmE5MTQ2MDAwMjUxNTQyY2JiMWY4OTM4YzBkZDBlZTJkYzBlMzQ4MDJiYmI1ODhhY2E4OTEzMTAwMDAwMDAwMDAxOTc2YTkxNDZmNzUyZGVkZjk5ZjdiYTQ3ODgzYTI1MzBjNmQyOTFkYjgzODg3NGE4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI2YjNjN2NkYjNmZTg5ZWU4NjY4YmNjMmY1NDlkYTBjYzVjNjQ1YThlM2FkMDBkYTRmMGYxZTgyOGM4NWExZjI5IiwiaGFzaCI6IjZiM2M3Y2RiM2ZlODllZTg2NjhiY2MyZjU0OWRhMGNjNWM2NDVhOGUzYWQwMGRhNGYwZjFlODI4Yzg1YTFmMjkiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyOTQsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI5OTEyZTg2Mzc0MDQ2Y2Q0MjU5MTQzNzU2OGZlYzRlMDc1OGI5ZTIxZmJlYjdkOWY5ZTQ4YjhkOWFhMmIwOTA0Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBlNjIyNThkOTA4ZDI0YzQzMTBjMjVhMGJhMGUzZjk3ZDlkZTk3Mzk2NDEwNTY1ZmNiMDk1ODFjYjE0MzU1NTMwMDIyMDdmYjU5ZjRkNWY1Njc1YTY5MjRkYmM1ZjY0NTc3ZGJiNGIwYzdmODZjYzE2YWE2ZjgzYmEwZmVkOGMxMGQ0NzVbQUxMfEZPUktJRF0gMDIwOGM2YjZkOTdjYTNkNjI1ZmJhNTZiNjQ2MGJkY2NlYjNlMDZmNTU1NWMxMTg0NTkwYjMzMzljMGY1ODc5YmUxIiwiaGV4IjoiNDgzMDQ1MDIyMTAwZTYyMjU4ZDkwOGQyNGM0MzEwYzI1YTBiYTBlM2Y5N2Q5ZGU5NzM5NjQxMDU2NWZjYjA5NTgxY2IxNDM1NTUzMDAyMjA3ZmI1OWY0ZDVmNTY3NWE2OTI0ZGJjNWY2NDU3N2RiYjRiMGM3Zjg2Y2MxNmFhNmY4M2JhMGZlZDhjMTBkNDc1NDEyMTAyMDhjNmI2ZDk3Y2EzZDYyNWZiYTU2YjY0NjBiZGNjZWIzZTA2ZjU1NTVjMTE4NDU5MGIzMzM5YzBmNTg3OWJlMSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6Mi4xODk4OTU3OCwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5MGYyMjg1Mzk1MTk5MzE1NDBmMGUxZGUyNWRiYzAwOTM0OThjN2E1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZzB5MnpuajV2ZXg5MnE3cnNhdWZ3bWNxeW5meHg4NTVkODY2Y3k2dCJdfX0seyJ2YWx1ZSI6MC4wNTEyMzAzMywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDM2MjdmOGQzYzY2YTU2ZGRmMTBmZjRiOGY2ZmVmZGU1ZmFjNDM5MzkgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQzNjI3ZjhkM2M2NmE1NmRkZjEwZmY0YjhmNmZlZmRlNWZhYzQzOTM5ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxbXowN3huY2U0OWRoMDNwbDZ0M2FoN2xoamw0M3BlOHl3cHJ2MDBtdiJdfX0seyJ2YWx1ZSI6MC4wMzgyMTUwOCwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGUwMWM0YTZjOWI2NzMyZTM4OWEzY2FmMjdiMjEzZmQ0NDlmNzY2YjQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRlMDFjNGE2YzliNjczMmUzODlhM2NhZjI3YjIxM2ZkNDQ5Zjc2NmI0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyc3Bjam52bmRubjljdWY1MDkweTdlcDhsMnluYW14a3Nta3BjYTBtOSJdfX0seyJ2YWx1ZSI6MC4wMzA1NDUzNiwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDQyYjMzMWE3YjgwNzk3YjBkOTQyYTRmMjAxNTcwNDE4Y2Y3MzE2ZjAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0MmIzMzFhN2I4MDc5N2IwZDk0MmE0ZjIwMTU3MDQxOGNmNzMxNmYwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwcHR4dmQ4aHFyZTB2eGVnMmoweXEyaHFzdnY3dWNrN3E2bGpqZnF6MCJdfX1dLCJmZWUiOjUuOWUtMDYsImhleCI6IjAxMDAwMDAwMDEwNDA5MmJhYWQ5Yjg0ODllOWY3ZGViZmIyMTllOGI3NWUwYzRmZTY4NzU0MzkxMjVkNDZjMDQ3NDYzZTgxMjk5MDAwMDAwMDA2YjQ4MzA0NTAyMjEwMGU2MjI1OGQ5MDhkMjRjNDMxMGMyNWEwYmEwZTNmOTdkOWRlOTczOTY0MTA1NjVmY2IwOTU4MWNiMTQzNTU1MzAwMjIwN2ZiNTlmNGQ1ZjU2NzVhNjkyNGRiYzVmNjQ1NzdkYmI0YjBjN2Y4NmNjMTZhYTZmODNiYTBmZWQ4YzEwZDQ3NTQxMjEwMjA4YzZiNmQ5N2NhM2Q2MjVmYmE1NmI2NDYwYmRjY2ViM2UwNmY1NTU1YzExODQ1OTBiMzMzOWMwZjU4NzliZTFmZmZmZmZmZjA0MGE4NDBkMGQwMDAwMDAwMDE5NzZhOTE0OTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNTg4YWNkOTJiNGUwMDAwMDAwMDAwMTk3NmE5MTQzNjI3ZjhkM2M2NmE1NmRkZjEwZmY0YjhmNmZlZmRlNWZhYzQzOTM5ODhhY2M0NGYzYTAwMDAwMDAwMDAxOTc2YTkxNGUwMWM0YTZjOWI2NzMyZTM4OWEzY2FmMjdiMjEzZmQ0NDlmNzY2YjQ4OGFjYzg5YjJlMDAwMDAwMDAwMDE5NzZhOTE0NDJiMzMxYTdiODA3OTdiMGQ5NDJhNGYyMDE1NzA0MThjZjczMTZmMDg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjZkNzM4Y2FjNTdjYmZjYWEyZjg5YjczMTk0NThmYjRiOWRlZWEwMzc1ZDU4NTY3ZjdiMjlmZTUwNzA1MTVlZjkiLCJoYXNoIjoiNmQ3MzhjYWM1N2NiZmNhYTJmODliNzMxOTQ1OGZiNGI5ZGVlYTAzNzVkNTg1NjdmN2IyOWZlNTA3MDUxNWVmOSIsInZlcnNpb24iOjIsInNpemUiOjIyNSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjA0N2U3ZTAzN2VjODc0OWFiZTk3N2Y1OTRkYmM1YzE5OTc4Njc0YjA0YzI1MDMwZDg2MDc2Yjk5N2ViNjBjODQiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA1YTk4NzMwZTQzYzJkZmE0MGJlMDIwYzQ0ZmI4ZGMxNTBiOTRlOWYxY2MwMWIyM2E4Y2JhNmM3MjUzYjE2MDNiMDIyMDQ5MjU5ZTFiY2JiYTA3NzFiZTg1YTk2NmRmZmE2MGU5NjIxY2RkZTliOWM4NGI5NDEyN2E3YjI3YjJmNWY2OThbQUxMfEZPUktJRF0gMDI5Yjc4NzIwYWI1NDIxY2Q4N2RkOWQ3OTU4MjJhYzBmNWJjZThiY2JlNTYwYTQzMTliZGM5N2I1OWI5ODliOGM3IiwiaGV4IjoiNDczMDQ0MDIyMDVhOTg3MzBlNDNjMmRmYTQwYmUwMjBjNDRmYjhkYzE1MGI5NGU5ZjFjYzAxYjIzYThjYmE2YzcyNTNiMTYwM2IwMjIwNDkyNTllMWJjYmJhMDc3MWJlODVhOTY2ZGZmYTYwZTk2MjFjZGRlOWI5Yzg0Yjk0MTI3YTdiMjdiMmY1ZjY5ODQxMjEwMjliNzg3MjBhYjU0MjFjZDg3ZGQ5ZDc5NTgyMmFjMGY1YmNlOGJjYmU1NjBhNDMxOWJkYzk3YjU5Yjk4OWI4YzcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjYuNTE3NzMsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA0ZjY5NzhmZmU4NmY0YjM0MGEwNjkxYjg3Y2I0MTJmZjNiZmQ1NzE1IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NGY2OTc4ZmZlODZmNGIzNDBhMDY5MWI4N2NiNDEyZmYzYmZkNTcxNTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDhrajc4bGFwaDVrZHEycTZnbXNsOTV6dGxuaGwyaHo1M20zN3d0ZnEiXX19LHsidmFsdWUiOjY0LjY5Njc2OTk4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjk2MmZkN2I5NjVmZDczZDRhNGRhMjRhM2FkMzYwOWY5ZThhMGZkMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDI5NjJmZDdiOTY1ZmQ3M2Q0YTRkYTI0YTNhZDM2MDlmOWU4YTBmZDE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXE1azlsdG1qZTBhdzAyMmZrM3k1d2tudnowZWF6czA2eWpyNWRxNmRhIl19fV0sImZlZSI6MC4wMDAyMjYsImhleCI6IjAyMDAwMDAwMDE4NDBjYjY3ZTk5NmIwNzg2MGQwMzI1NGNiMDc0ODY5NzE5NWNiYzRkNTk3Zjk3YmU5YTc0Yzg3ZTAzN2U3ZTA0MDEwMDAwMDA2YTQ3MzA0NDAyMjA1YTk4NzMwZTQzYzJkZmE0MGJlMDIwYzQ0ZmI4ZGMxNTBiOTRlOWYxY2MwMWIyM2E4Y2JhNmM3MjUzYjE2MDNiMDIyMDQ5MjU5ZTFiY2JiYTA3NzFiZTg1YTk2NmRmZmE2MGU5NjIxY2RkZTliOWM4NGI5NDEyN2E3YjI3YjJmNWY2OTg0MTIxMDI5Yjc4NzIwYWI1NDIxY2Q4N2RkOWQ3OTU4MjJhYzBmNWJjZThiY2JlNTYwYTQzMTliZGM5N2I1OWI5ODliOGM3ZmZmZmZmZmYwMjQ4NDRkOTI2MDAwMDAwMDAxOTc2YTkxNDRmNjk3OGZmZTg2ZjRiMzQwYTA2OTFiODdjYjQxMmZmM2JmZDU3MTU4OGFjYzY2ZjlmODEwMTAwMDAwMDE5NzZhOTE0Mjk2MmZkN2I5NjVmZDczZDRhNGRhMjRhM2FkMzYwOWY5ZThhMGZkMTg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjZmZmNkYTUwODQwOGRjYzllN2EyZjlmODY2ZGQzMTVlMzkzYWI3MmE3ZmUzM2I5MzRjNDk3MGMwNGY2MWY1MzIiLCJoYXNoIjoiNmZmY2RhNTA4NDA4ZGNjOWU3YTJmOWY4NjZkZDMxNWUzOTNhYjcyYTdmZTMzYjkzNGM0OTcwYzA0ZjYxZjUzMiIsInZlcnNpb24iOjEsInNpemUiOjIyNSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6Ijg3ZTZmOWVmN2VlMjJjNzU3ZTU4ZWY4MzRhYWJjNDU4MDY5Yzc5M2M5ZTkwYTRlOWU2NDE0ZmY3Mjg5YjA3ZDAiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA0YjI3YTlkNzhmMGM0OGVmODA4MzFkMDk2NjI4NTY1OTNjYWEyNmQ3ZjU3ZmJhZDhjZGIxMGM1OWRiM2NmMGYwMDIyMDZjZDcwNGVlZmJhN2E5YmNkZjRmN2JiNjdmODMxZDkxOTEwZDAwMDRjOGRlNTZhYmI5NDI0NjY2YTQzMjg1NjhbQUxMfEZPUktJRF0gMDMwOWFjY2EzMGJhNDVlZjkyMTUxNDc3YjZkYTI1YzBhNDlmNjIxNThkNzAxNDk0ZWY5NTQxMDUyYjJjMjU0MjlmIiwiaGV4IjoiNDczMDQ0MDIyMDRiMjdhOWQ3OGYwYzQ4ZWY4MDgzMWQwOTY2Mjg1NjU5M2NhYTI2ZDdmNTdmYmFkOGNkYjEwYzU5ZGIzY2YwZjAwMjIwNmNkNzA0ZWVmYmE3YTliY2RmNGY3YmI2N2Y4MzFkOTE5MTBkMDAwNGM4ZGU1NmFiYjk0MjQ2NjZhNDMyODU2ODQxMjEwMzA5YWNjYTMwYmE0NWVmOTIxNTE0NzdiNmRhMjVjMGE0OWY2MjE1OGQ3MDE0OTRlZjk1NDEwNTJiMmMyNTQyOWYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9XSwidm91dCI6W3sidmFsdWUiOjAuMTgyNjc4MSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGZjMWUxZTlkMDdlNzIyMzk2YjE1MDJlZDk5MmY1NmRhMTlmY2MwOTggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmYzFlMWU5ZDA3ZTcyMjM5NmIxNTAyZWQ5OTJmNTZkYTE5ZmNjMDk4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyN3B1ODVhcWxuanl3dHR6NXB3bXhmMDJtZHBubHhxbnE0NTM1eGs5NyJdfX0seyJ2YWx1ZSI6NDkuNzY4OTc2NjIsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA4ZGIxMzUyOTlhNDNjNTM2NTA3MjZkZTY0ZTk2NjA1MzU5OTMxY2FmIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OGRiMTM1Mjk5YTQzYzUzNjUwNzI2ZGU2NGU5NjYwNTM1OTkzMWNhZjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenhtemRmZm5mcHUyZGpzd2ZrN3ZuNWt2cGY0bnljdTR1dGw5ZGswa3YiXX19XSwiZmVlIjo5ZS0wNiwiaGV4IjoiMDEwMDAwMDAwMWQwMDc5YjI4Zjc0ZjQxZTZlOWE0OTA5ZTNjNzk5YzA2NThjNGFiNGE4M2VmNTg3ZTc1MmNlMjdlZWZmOWU2ODcwMTAwMDAwMDZhNDczMDQ0MDIyMDRiMjdhOWQ3OGYwYzQ4ZWY4MDgzMWQwOTY2Mjg1NjU5M2NhYTI2ZDdmNTdmYmFkOGNkYjEwYzU5ZGIzY2YwZjAwMjIwNmNkNzA0ZWVmYmE3YTliY2RmNGY3YmI2N2Y4MzFkOTE5MTBkMDAwNGM4ZGU1NmFiYjk0MjQ2NjZhNDMyODU2ODQxMjEwMzA5YWNjYTMwYmE0NWVmOTIxNTE0NzdiNmRhMjVjMGE0OWY2MjE1OGQ3MDE0OTRlZjk1NDEwNTJiMmMyNTQyOWZmZGZmZmZmZjAyYTJiZTE2MDEwMDAwMDAwMDE5NzZhOTE0ZmMxZTFlOWQwN2U3MjIzOTZiMTUwMmVkOTkyZjU2ZGExOWZjYzA5ODg4YWM3ZTZlYTUyODAxMDAwMDAwMTk3NmE5MTQ4ZGIxMzUyOTlhNDNjNTM2NTA3MjZkZTY0ZTk2NjA1MzU5OTMxY2FmODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiNzJlYjI4YWI4MjI0MjEzZTk5YjU0NTk1ZjA3YjZhOTg5OTRhYzJkMzZkMWUxMWU3NjBiYmQ5M2M0NDRlMjMzMCIsImhhc2giOiI3MmViMjhhYjgyMjQyMTNlOTliNTQ1OTVmMDdiNmE5ODk5NGFjMmQzNmQxZTExZTc2MGJiZDkzYzQ0NGUyMzMwIiwidmVyc2lvbiI6MSwic2l6ZSI6Mjk0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiZjY1M2Q3Yjc3M2E2ZmM1Yzk1YjhmZTU5YmZjNTFjMzNjYzdlZDFiMzY1NDkwOWRhNWMzMGQ2MmQzNzQ4MzRkZSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwODNkZWIzN2ViNGVmYjdiYTE3MjI2MWUxYmVjYWY3ZDg5NTlmNDQ0ZmU2MTNjMTIwNGIzNDI3MTFhYTE3ZjI3MDAyMjAyZWQ1ZTkyMjMzYzAxNzNlNTI4NmZkZWY5ZDBlYmU3MmE3Yzk0MDIyYmM3ZjJjYmE1ODFjMGY5ZjVmZmIzMjA2W0FMTHxGT1JLSURdIDAyMDhjNmI2ZDk3Y2EzZDYyNWZiYTU2YjY0NjBiZGNjZWIzZTA2ZjU1NTVjMTE4NDU5MGIzMzM5YzBmNTg3OWJlMSIsImhleCI6IjQ4MzA0NTAyMjEwMDgzZGViMzdlYjRlZmI3YmExNzIyNjFlMWJlY2FmN2Q4OTU5ZjQ0NGZlNjEzYzEyMDRiMzQyNzExYWExN2YyNzAwMjIwMmVkNWU5MjIzM2MwMTczZTUyODZmZGVmOWQwZWJlNzJhN2M5NDAyMmJjN2YyY2JhNTgxYzBmOWY1ZmZiMzIwNjQxMjEwMjA4YzZiNmQ5N2NhM2Q2MjVmYmE1NmI2NDYwYmRjY2ViM2UwNmY1NTU1YzExODQ1OTBiMzMzOWMwZjU4NzliZTEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjEuNjQ5NzM0NjUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5MGYyMjg1Mzk1MTk5MzE1NDBmMGUxZGUyNWRiYzAwOTM0OThjN2E1IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemcweTJ6bmo1dmV4OTJxN3JzYXVmd21jcXluZnh4ODU1ZDg2NmN5NnQiXX19LHsidmFsdWUiOjAuMTA1NDQ1MTgsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBmNTE1YzdkOGU5MTQ5MDQzMjA3NWRkMGZlOWRjYmI5ODBmMmE0MWNiIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZjUxNWM3ZDhlOTE0OTA0MzIwNzVkZDBmZTlkY2JiOTgwZjJhNDFjYjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcjYzdDM3Y2F5MmZxc2Vxd2h3c2w2d3Vod3ZxNzJqcGV2OXd4ZXIyem4iXX19LHsidmFsdWUiOjAuMDc5Mzk2MTMsIm4iOjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhY2NjZGVhNDAxNGU5ZDM1YWVhMzgxMGM3Njg0MjQzOTBlODdjMWE1IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YWNjY2RlYTQwMTRlOWQzNWFlYTM4MTBjNzY4NDI0MzkwZTg3YzFhNTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemt2ZWg0eXE5OGY2ZGR3NXdxc2NhNXl5c3VzYXA3cDU1eGR0a2gyOHgiXX19LHsidmFsdWUiOjAuMDQ3MzE4MjksIm4iOjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBlOWZiNzIxN2UwMmMxZDhjNzhkNDY2ZWNhYjNjNjRhZWQ5OWQwMjJjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZTlmYjcyMTdlMDJjMWQ4Yzc4ZDQ2NmVjYWIzYzY0YWVkOTlkMDIyYzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcjVsa3VzaHVxa3BtcnJjNjNud2UyZXV2amhkbjhnejlzMzg1c3g5dDUiXX19XSwiZmVlIjo1LjllLTA2LCJoZXgiOiIwMTAwMDAwMDAxZGUzNDQ4MzcyZGQ2MzA1Y2RhMDk0OTY1YjNkMTdlY2MzMzFjYzViZjU5ZmViODk1NWNmY2E2NzNiN2Q3NTNmNjAwMDAwMDAwNmI0ODMwNDUwMjIxMDA4M2RlYjM3ZWI0ZWZiN2JhMTcyMjYxZTFiZWNhZjdkODk1OWY0NDRmZTYxM2MxMjA0YjM0MjcxMWFhMTdmMjcwMDIyMDJlZDVlOTIyMzNjMDE3M2U1Mjg2ZmRlZjlkMGViZTcyYTdjOTQwMjJiYzdmMmNiYTU4MWMwZjlmNWZmYjMyMDY0MTIxMDIwOGM2YjZkOTdjYTNkNjI1ZmJhNTZiNjQ2MGJkY2NlYjNlMDZmNTU1NWMxMTg0NTkwYjMzMzljMGY1ODc5YmUxZmZmZmZmZmYwNDk5NGJkNTA5MDAwMDAwMDAxOTc2YTkxNDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTU4OGFjODZlNWEwMDAwMDAwMDAwMDE5NzZhOTE0ZjUxNWM3ZDhlOTE0OTA0MzIwNzVkZDBmZTlkY2JiOTgwZjJhNDFjYjg4YWMxZDI2NzkwMDAwMDAwMDAwMTk3NmE5MTRhY2NjZGVhNDAxNGU5ZDM1YWVhMzgxMGM3Njg0MjQzOTBlODdjMWE1ODhhY2I1MzM0ODAwMDAwMDAwMDAxOTc2YTkxNGU5ZmI3MjE3ZTAyYzFkOGM3OGQ0NjZlY2FiM2M2NGFlZDk5ZDAyMmM4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI3MmYyMDFmNWRlOWUxZTVkMTI3OTZkZGVhYjYzMGM0YTgyYjc2OGRlZWFlZjU0ODVjOGNlODY3NGNlYmEzMjJjIiwiaGFzaCI6IjcyZjIwMWY1ZGU5ZTFlNWQxMjc5NmRkZWFiNjMwYzRhODJiNzY4ZGVlYWVmNTQ4NWM4Y2U4Njc0Y2ViYTMyMmMiLCJ2ZXJzaW9uIjoyLCJzaXplIjoyMjUsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI4YTQzNmExNWFjOGQ0MzgxNjg5NTMzOTZkMmU2ZGRiOTIyMTQ2YWNmNDk1NTBkNTc2OTM1NzVjYmI0NDQyNjhlIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwMDk1OTBjN2EyZTQzMTcxMTE1ZWNjOTM1YTY3Y2MzYjcwMzM4NDE0MDdkNDg5NjNjMDk3ZGM3ODAwMjU4MTE1MjAyMjAzZDcxMjY5MWFiNGYyOTFmYTQwMzEwZGIxZjQzMTUzZmI2MmFjMGJjYTRjMjVkNTI3MmM1MWFhNzljNWI5NjI3W0FMTHxGT1JLSURdIDAzMGY2NmViNzFjMWI3ZmIzOWU4M2EyZjczMDc2MGY3MTU4MWVlMDlkMTE5ZTRkNTA2ZGNkNTYzMDMzMGFiZTk3NSIsImhleCI6IjQ3MzA0NDAyMjAwOTU5MGM3YTJlNDMxNzExMTVlY2M5MzVhNjdjYzNiNzAzMzg0MTQwN2Q0ODk2M2MwOTdkYzc4MDAyNTgxMTUyMDIyMDNkNzEyNjkxYWI0ZjI5MWZhNDAzMTBkYjFmNDMxNTNmYjYyYWMwYmNhNGMyNWQ1MjcyYzUxYWE3OWM1Yjk2Mjc0MTIxMDMwZjY2ZWI3MWMxYjdmYjM5ZTgzYTJmNzMwNzYwZjcxNTgxZWUwOWQxMTllNGQ1MDZkY2Q1NjMwMzMwYWJlOTc1In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjA5NjQwODYxLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODk2NDNiYWM4YWMzOTc0ZGI1YjA0MzRiMmJmMmU3YTJiN2UyMzkwYiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg5NjQzYmFjOGFjMzk3NGRiNWIwNDM0YjJiZjJlN2EyYjdlMjM5MGI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp5a2d3YXYzdHBld25kNGtwcDVrMmxqdTczdDBjM2VwdnU2NDBwbXlhIl19fSx7InZhbHVlIjozLjExMjA0NTAzLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2I1ODMwODRlNTE1ODUzM2IxN2IzODFjNGY2MmY5YjQxNDIzYmQ5OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdiNTgzMDg0ZTUxNTg1MzNiMTdiMzgxYzRmNjJmOWI0MTQyM2JkOTk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBhNHN2eXl1NTJjMnZhMzB2dXBjbm16bHg2cGdnYWFueWt0ZnBwejUyIl19fV0sImZlZSI6NC41MmUtMDYsImhleCI6IjAyMDAwMDAwMDE4ZTI2NDRiNGNiNzUzNTY5NTcwZDU1NDljZjZhMTQyMmI5ZGRlNmQyOTYzMzk1Njg4MTQzOGRhYzE1NmE0MzhhMDEwMDAwMDA2YTQ3MzA0NDAyMjAwOTU5MGM3YTJlNDMxNzExMTVlY2M5MzVhNjdjYzNiNzAzMzg0MTQwN2Q0ODk2M2MwOTdkYzc4MDAyNTgxMTUyMDIyMDNkNzEyNjkxYWI0ZjI5MWZhNDAzMTBkYjFmNDMxNTNmYjYyYWMwYmNhNGMyNWQ1MjcyYzUxYWE3OWM1Yjk2Mjc0MTIxMDMwZjY2ZWI3MWMxYjdmYjM5ZTgzYTJmNzMwNzYwZjcxNTgxZWUwOWQxMTllNGQ1MDZkY2Q1NjMwMzMwYWJlOTc1ZmZmZmZmZmYwMjlkMWI5MzAwMDAwMDAwMDAxOTc2YTkxNDg5NjQzYmFjOGFjMzk3NGRiNWIwNDM0YjJiZjJlN2EyYjdlMjM5MGI4OGFjOTc5YThjMTIwMDAwMDAwMDE5NzZhOTE0N2I1ODMwODRlNTE1ODUzM2IxN2IzODFjNGY2MmY5YjQxNDIzYmQ5OTg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6Ijc3MGFiMzgyMjc1YzFiNWRjZmQ5MzE1MjhhZWQ5YjBiNDgyMGVkYmExNTVkMzU3ZDEzOWJhOWUzZGZiYzNhM2EiLCJoYXNoIjoiNzcwYWIzODIyNzVjMWI1ZGNmZDkzMTUyOGFlZDliMGI0ODIwZWRiYTE1NWQzNTdkMTM5YmE5ZTNkZmJjM2EzYSIsInZlcnNpb24iOjEsInNpemUiOjg0ODcsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiIwMzE3ZjRkMzk3ZWE4MmY5YzI0YzkwZjdhNWE5OTg0NjBiNzYzMDQ3MTk1ZTI5MjllNDZkNmIyNGI2MDk4N2M1Iiwidm91dCI6MTEsInNjcmlwdFNpZyI6eyJhc20iOiIxMGU5NGZiMzgyNjc0OTcyMDdiNmVlZDk2NWU5Mzc3YjY1YzliODc0ZmFlNTFmNDFjZjg2MjZjMGRjYjgxZWM5MTk0ZDc3ZGEzZGE4ZjBhNWJhZjI1OTdkNGFhNzkxNmYxZDkzMWQ3MzEwZjM5NzYyZGU3M2QwY2FkNTZkY2RhZltBTEx8Rk9SS0lEXSAwMmIzZmU2NWI1NzYzYWMzOWQ0YzdhNjVkMDMzMDc0MDYwM2YxNTE5MTE3ZmUzOGYyZDBhNTQwNjRiMDQyNWU3YzQiLCJoZXgiOiI0MTEwZTk0ZmIzODI2NzQ5NzIwN2I2ZWVkOTY1ZTkzNzdiNjVjOWI4NzRmYWU1MWY0MWNmODYyNmMwZGNiODFlYzkxOTRkNzdkYTNkYThmMGE1YmFmMjU5N2Q0YWE3OTE2ZjFkOTMxZDczMTBmMzk3NjJkZTczZDBjYWQ1NmRjZGFmNDEyMTAyYjNmZTY1YjU3NjNhYzM5ZDRjN2E2NWQwMzMwNzQwNjAzZjE1MTkxMTdmZTM4ZjJkMGE1NDA2NGIwNDI1ZTdjNCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMDMyMTc0Nzc4NjdkMDI0NTUyMTQ3NzBiOGI4YzE3NTkyZGFhNmZkZmViZmY1MzRiMzdiYWU1NjI5ZGE5YWIzNSIsInZvdXQiOjIxLCJzY3JpcHRTaWciOnsiYXNtIjoiOWM4Y2M0MDlhNGM4ZDAyNmExNzgwNGFlZWUyMWIwZmE1OWEzMGJiODJjZmZlOGJlNjg0Y2I4MWFlNmY2MmMzYjFjMTRhMGNmMDgzYTI2OWU2MjA4NmUyOTY5ZjcwYjJiNDM3YmJjMTU2YzljYzgwMzFiNDE0NTFlOGYzZDk3ZmJbQUxMfEZPUktJRF0gMDI2NzZiY2MzMDRiN2NkOTY4NzdjYTg3MTlkOGM5ZTZmZDJmYTcwOWRjNzliZTczZjQ5MmY5YWMzMTE1YTNkMDFlIiwiaGV4IjoiNDE5YzhjYzQwOWE0YzhkMDI2YTE3ODA0YWVlZTIxYjBmYTU5YTMwYmI4MmNmZmU4YmU2ODRjYjgxYWU2ZjYyYzNiMWMxNGEwY2YwODNhMjY5ZTYyMDg2ZTI5NjlmNzBiMmI0MzdiYmMxNTZjOWNjODAzMWI0MTQ1MWU4ZjNkOTdmYjQxMjEwMjY3NmJjYzMwNGI3Y2Q5Njg3N2NhODcxOWQ4YzllNmZkMmZhNzA5ZGM3OWJlNzNmNDkyZjlhYzMxMTVhM2QwMWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjA4N2UzNDlhYzcyNTI0MGZiYWFiY2NhNDFhZjllY2VhOTA0YTZkOGJlNWZiMGY1NzZmMzI4YmQ5ZTJmMzQxZDMiLCJ2b3V0Ijo0LCJzY3JpcHRTaWciOnsiYXNtIjoiYzcwNTVkMzBjNmE5YWJmNDVjZjM5M2YzNjU1ZmJkMjA3Yjk2YzdlNWM5NzNhZTdkNTdiNzIzYTQ4NmQzNGM3ZDI5ZmYwMGIxZjU1YmJlM2ViYTQzNDQyMWZlYTdkZDVhMDMyMDYwNmNmMzBhMThmNmQyZjYwYjFhMjAwYmEzYjBbQUxMfEZPUktJRF0gMDI2MmQwNDA2NzA4NWViZGEwMTNjMDc5ZjA0MGE3MzVkZTRkNDA1MmI2NmZhMWViNmFjMDdmNjE4N2MwNGY1NDc1IiwiaGV4IjoiNDFjNzA1NWQzMGM2YTlhYmY0NWNmMzkzZjM2NTVmYmQyMDdiOTZjN2U1Yzk3M2FlN2Q1N2I3MjNhNDg2ZDM0YzdkMjlmZjAwYjFmNTViYmUzZWJhNDM0NDIxZmVhN2RkNWEwMzIwNjA2Y2YzMGExOGY2ZDJmNjBiMWEyMDBiYTNiMDQxMjEwMjYyZDA0MDY3MDg1ZWJkYTAxM2MwNzlmMDQwYTczNWRlNGQ0MDUyYjY2ZmExZWI2YWMwN2Y2MTg3YzA0ZjU0NzUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjEwZjIxY2RhMmJhODZlNTRmYzQ3YWIzZmFmN2Q1ODcwM2IyNzFkMzU0MjUxZWE5NTVhYTU1OTc5NjllZDc5YjkiLCJ2b3V0IjoxOCwic2NyaXB0U2lnIjp7ImFzbSI6IjgxM2VkNWE3ZjJlNTZhZDg0NjVjMGM3YzA2NzUwNjVlYzYzOTk5ODUxODQ2NTY3YjJjM2UxZWRmM2M1NDgzMmIxYTZmNjQ3MzczYTI5MTYzMjFlMDNhN2NjZGZiMmM1Y2ZlNDQwMzg0ZWQ3YmVhOWQ5NTUxMjNkNmY0YWZjNzk5W0FMTHxGT1JLSURdIDAyMDI2ZTI2ZjFkYmFiOWEzNzIyZWM0OGUxOWMwMjdjYmM0ZDY5M2EzMzFkNWIwM2FkMzg0NjRiNmFhMjEzMDhlMSIsImhleCI6IjQxODEzZWQ1YTdmMmU1NmFkODQ2NWMwYzdjMDY3NTA2NWVjNjM5OTk4NTE4NDY1NjdiMmMzZTFlZGYzYzU0ODMyYjFhNmY2NDczNzNhMjkxNjMyMWUwM2E3Y2NkZmIyYzVjZmU0NDAzODRlZDdiZWE5ZDk1NTEyM2Q2ZjRhZmM3OTk0MTIxMDIwMjZlMjZmMWRiYWI5YTM3MjJlYzQ4ZTE5YzAyN2NiYzRkNjkzYTMzMWQ1YjAzYWQzODQ2NGI2YWEyMTMwOGUxIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIyZDgwODQ3MGFkZTQ3ODNkNjhmNjE0OWM4NTJhNWFlYjA2ZTRjMmNhMDBiYzk1NGVlOTA4YTNhNjVjMjI4NTM1Iiwidm91dCI6NDQsInNjcmlwdFNpZyI6eyJhc20iOiJjMjhmYmE2YWJlYjcwMTZlZTI0ZmZiYzNmZGZmMjJhY2M1NjMxOWM0NTBhNjhiZDVjMmYwMzI2MWM3MmY2YmEzMjYyMjBiZjdiMDhhNWI1MTlkYzY5ZGNiM2RiYmFhMGZhM2FiYTUwN2VmYTVmY2E1Y2JkZjExNmRmNmNlMDA3MltBTEx8Rk9SS0lEXSAwMjJjOGEzOTgyZjQ1ZmQ3MjI3NzQwOTI2YzhjYmU2NDRlYmI0OWQ0MjYwOThiZTIwNzVhNDQ3YzJjMTk3MDllZjAiLCJoZXgiOiI0MWMyOGZiYTZhYmViNzAxNmVlMjRmZmJjM2ZkZmYyMmFjYzU2MzE5YzQ1MGE2OGJkNWMyZjAzMjYxYzcyZjZiYTMyNjIyMGJmN2IwOGE1YjUxOWRjNjlkY2IzZGJiYWEwZmEzYWJhNTA3ZWZhNWZjYTVjYmRmMTE2ZGY2Y2UwMDcyNDEyMTAyMmM4YTM5ODJmNDVmZDcyMjc3NDA5MjZjOGNiZTY0NGViYjQ5ZDQyNjA5OGJlMjA3NWE0NDdjMmMxOTcwOWVmMCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMzE1MGVjZDI0MWY5NzQ4ZmZjZGNlYjJhYjk3ZjI4NzQyOGZmZjc0NTAwM2E2YjI2ZGRkNThjMzk2NDliMjE2MCIsInZvdXQiOjMyLCJzY3JpcHRTaWciOnsiYXNtIjoiYWE1MDJlZWMxYjNlOGRjMGE3MTgzMTFjY2Y4YzIxZDViM2JlMTQxZDU0YjZlOTQ5NmU2ODQwMGJmMWE4OTE3NTE5MjJmZGYzZGQ5N2Q5ODkwNDEwOWE2YTI1NGY2YTRjYWI3ZTBjZjlmYzJmMDY3NzU4ZmVjZGNlNzZkMzg5NThbQUxMfEZPUktJRF0gMDI2OTY2MDQwOWExN2I4MjFjZTNiM2JkNjI0ZjE0MzgwMDRiYzk0MTE2NTU2YzRiNjBhZjEyNWE4ZTNkOWJlODQzIiwiaGV4IjoiNDFhYTUwMmVlYzFiM2U4ZGMwYTcxODMxMWNjZjhjMjFkNWIzYmUxNDFkNTRiNmU5NDk2ZTY4NDAwYmYxYTg5MTc1MTkyMmZkZjNkZDk3ZDk4OTA0MTA5YTZhMjU0ZjZhNGNhYjdlMGNmOWZjMmYwNjc3NThmZWNkY2U3NmQzODk1ODQxMjEwMjY5NjYwNDA5YTE3YjgyMWNlM2IzYmQ2MjRmMTQzODAwNGJjOTQxMTY1NTZjNGI2MGFmMTI1YThlM2Q5YmU4NDMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjM4ZmI1ZTA4ZjhiYTE0YjVkYmJlZmYxYzQ4NTQ0NGY1YzQ5NTMyZThjMDA1MDQ3ZTIzZDI1NTEyNzY1ZjVlNzUiLCJ2b3V0Ijo0LCJzY3JpcHRTaWciOnsiYXNtIjoiZmNiNDY2NWM3NjY0OGMwMDkxNTFmNTY2OTAxYmZlZjcwMTJiNWNjMTBiYWI2NWFmMDgxZDFiNTI3MDdlNWVhMjU1MWVmNTllN2JkMDdhYmY3MDk2OTAzNmQzNmZmOGIwNDI1OTY0OWIxN2RjMmQ2ZjJjOWNlOTFkZDlmZmFmNTZbQUxMfEZPUktJRF0gMDM5ZjQ4YmYxZDVjMWVhNGY1NWUyMjQ4NTE3MTVmMTdiN2FjMGJiNzI4OTY4MGMwY2Q3ZGYwOTA5NjBkYTlhNjM2IiwiaGV4IjoiNDFmY2I0NjY1Yzc2NjQ4YzAwOTE1MWY1NjY5MDFiZmVmNzAxMmI1Y2MxMGJhYjY1YWYwODFkMWI1MjcwN2U1ZWEyNTUxZWY1OWU3YmQwN2FiZjcwOTY5MDM2ZDM2ZmY4YjA0MjU5NjQ5YjE3ZGMyZDZmMmM5Y2U5MWRkOWZmYWY1NjQxMjEwMzlmNDhiZjFkNWMxZWE0ZjU1ZTIyNDg1MTcxNWYxN2I3YWMwYmI3Mjg5NjgwYzBjZDdkZjA5MDk2MGRhOWE2MzYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjM4ZmI1ZTA4ZjhiYTE0YjVkYmJlZmYxYzQ4NTQ0NGY1YzQ5NTMyZThjMDA1MDQ3ZTIzZDI1NTEyNzY1ZjVlNzUiLCJ2b3V0IjoxMiwic2NyaXB0U2lnIjp7ImFzbSI6IjBkNDBiNWFkOWJmMDQ2YzBiNjZlNTkzNjVlOTFiM2ZiN2RlZjlkNTVkNWQ1ZjE1ODIxNTQ4Y2FmMzI0NWVlZDc5Mjg2MWIwZGE5YTAyMWYzZGUxYTIxZDljZjg1NzJiY2VjODlhYTE0ZTY4MTYzNDgyNGYyMmM1YWU2MDA3NWVlW0FMTHxGT1JLSURdIDAyMmUwNzMyYTlhZWMxZmJhZjRiZDcwMDEzYTQ0NTQzMWIyNTg1YmVhNzE4OGU4YWRiNjVmOGJiNWNkMWM1Y2Q0YSIsImhleCI6IjQxMGQ0MGI1YWQ5YmYwNDZjMGI2NmU1OTM2NWU5MWIzZmI3ZGVmOWQ1NWQ1ZDVmMTU4MjE1NDhjYWYzMjQ1ZWVkNzkyODYxYjBkYTlhMDIxZjNkZTFhMjFkOWNmODU3MmJjZWM4OWFhMTRlNjgxNjM0ODI0ZjIyYzVhZTYwMDc1ZWU0MTIxMDIyZTA3MzJhOWFlYzFmYmFmNGJkNzAwMTNhNDQ1NDMxYjI1ODViZWE3MTg4ZThhZGI2NWY4YmI1Y2QxYzVjZDRhIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIzOGZiNWUwOGY4YmExNGI1ZGJiZWZmMWM0ODU0NDRmNWM0OTUzMmU4YzAwNTA0N2UyM2QyNTUxMjc2NWY1ZTc1Iiwidm91dCI6MTUsInNjcmlwdFNpZyI6eyJhc20iOiJlZDc4MzMwZTBmZTAxNTJmNzhmNjQyZDhlZmI5NGNlZTU0ZjEzNmIzODg1MjU4ZTVkY2NhZjFiN2MzYjIwYWU5MjFkYTNlMDVmNjc1YmYwNDdjYjY2Y2QwZmM1MTgzYjQ4M2JhOTJiNDZkZmNkN2U3NDMyNGZjNThkNmVhNDJlY1tBTEx8Rk9SS0lEXSAwMjk1NDhiZWE1YjZjMTYxMDIyZDMyZTMyYmQyNjg2MDc1MDA0M2ViNmFjNmIyMzhlZTIxZTI1YTY3MDliMTA2ZmIiLCJoZXgiOiI0MWVkNzgzMzBlMGZlMDE1MmY3OGY2NDJkOGVmYjk0Y2VlNTRmMTM2YjM4ODUyNThlNWRjY2FmMWI3YzNiMjBhZTkyMWRhM2UwNWY2NzViZjA0N2NiNjZjZDBmYzUxODNiNDgzYmE5MmI0NmRmY2Q3ZTc0MzI0ZmM1OGQ2ZWE0MmVjNDEyMTAyOTU0OGJlYTViNmMxNjEwMjJkMzJlMzJiZDI2ODYwNzUwMDQzZWI2YWM2YjIzOGVlMjFlMjVhNjcwOWIxMDZmYiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOjM4LCJzY3JpcHRTaWciOnsiYXNtIjoiOWNjN2ZlMDQ4YjZkMDczMmY1OTZiNjRhMzJmOWQwZjlmNzE5Mzc3YWRhYTI0M2EzNTM3YTVkNDAxNjM1MzE1MTJiYjQ0NWM0MGM1M2UzMWFjOTI3NjhjZjkzZjIyNjhmNTUzMzRkYmViMmFmMzA4MzcwZWY5NDZmODc5OTJlZTFbQUxMfEZPUktJRF0gMDM0Y2IwNDg2ZjM0ZjM4ZDQyOTIxZTM0YmY0OGRjYzM5NmIxOGM2MzlhMzQwYmM2Mzg1ZjBjYTUwYjlhYjYxMDhkIiwiaGV4IjoiNDE5Y2M3ZmUwNDhiNmQwNzMyZjU5NmI2NGEzMmY5ZDBmOWY3MTkzNzdhZGFhMjQzYTM1MzdhNWQ0MDE2MzUzMTUxMmJiNDQ1YzQwYzUzZTMxYWM5Mjc2OGNmOTNmMjI2OGY1NTMzNGRiZWIyYWYzMDgzNzBlZjk0NmY4Nzk5MmVlMTQxMjEwMzRjYjA0ODZmMzRmMzhkNDI5MjFlMzRiZjQ4ZGNjMzk2YjE4YzYzOWEzNDBiYzYzODVmMGNhNTBiOWFiNjEwOGQifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjNmMDEyYTBhY2Y4YTZmOGQ4MjExYmI1MzFiN2I4NGZjYTdmMjk3NTk1OGI4NTQ1NGIyODE4ZDc3ZmY3Y2QwY2EiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiZDkwM2M4MjYwMjhhZGExMTZhMzc3OWY4ODQ0NjQ0N2IzZmRiYjExMTM4ODg4MGMxNjU3MTU1MDU0YjFmMTlkZWZkNGYzNDFiZDdiZGJjZDYzYTc2YTkzNzM0MDY2OTg0N2I1OTkzMjVhYmE5MjJhNTAwYTA4YWYwM2E4MDYzNTdbQUxMfEZPUktJRF0gMDI2NzlhNjQ3YjhkNzc2Yzc1MDhiZWZkZGQ4OWQwZmQ3NDhhYTgyMmNhYzM5NDMyZWEyZTFmYTMwN2IxMTQ2ODZhIiwiaGV4IjoiNDFkOTAzYzgyNjAyOGFkYTExNmEzNzc5Zjg4NDQ2NDQ3YjNmZGJiMTExMzg4ODgwYzE2NTcxNTUwNTRiMWYxOWRlZmQ0ZjM0MWJkN2JkYmNkNjNhNzZhOTM3MzQwNjY5ODQ3YjU5OTMyNWFiYTkyMmE1MDBhMDhhZjAzYTgwNjM1NzQxMjEwMjY3OWE2NDdiOGQ3NzZjNzUwOGJlZmRkZDg5ZDBmZDc0OGFhODIyY2FjMzk0MzJlYTJlMWZhMzA3YjExNDY4NmEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjNmMDEyYTBhY2Y4YTZmOGQ4MjExYmI1MzFiN2I4NGZjYTdmMjk3NTk1OGI4NTQ1NGIyODE4ZDc3ZmY3Y2QwY2EiLCJ2b3V0IjozMywic2NyaXB0U2lnIjp7ImFzbSI6IjQyOTJlNDA3MTgzYmViM2EzNmQ5MjgwZDRlYTQ3M2YxOGY3MmNiZWVmNjNlZjEyMzZjMDk3N2EzNzM1NDMzNTMxNjg3NGUwZDFjYjhhYWE0YzM0ZGYzNWU0MDBkYjZmOGIyMWVjNWQ1MDgxZjJkZWVjY2UwYjE2OGZlMjhlNmUxW0FMTHxGT1JLSURdIDAyMTUwODhmOTI2MzIxMGI5ZWMzNGMzYjI4YmVjN2U3ODcyYmM4MDk4ZDZiNGE5ZTI0MzllOTVhODcwYTUwNGNiMiIsImhleCI6IjQxNDI5MmU0MDcxODNiZWIzYTM2ZDkyODBkNGVhNDczZjE4ZjcyY2JlZWY2M2VmMTIzNmMwOTc3YTM3MzU0MzM1MzE2ODc0ZTBkMWNiOGFhYTRjMzRkZjM1ZTQwMGRiNmY4YjIxZWM1ZDUwODFmMmRlZWNjZTBiMTY4ZmUyOGU2ZTE0MTIxMDIxNTA4OGY5MjYzMjEwYjllYzM0YzNiMjhiZWM3ZTc4NzJiYzgwOThkNmI0YTllMjQzOWU5NWE4NzBhNTA0Y2IyIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIzZjAxMmEwYWNmOGE2ZjhkODIxMWJiNTMxYjdiODRmY2E3ZjI5NzU5NThiODU0NTRiMjgxOGQ3N2ZmN2NkMGNhIiwidm91dCI6MzQsInNjcmlwdFNpZyI6eyJhc20iOiIxMjgxODM5OWRmZGE2NDM2YTM1MDVjYmNlODZlNGY5ZWU2ZTlkYzc5ZGU0NWY3MjYyNmVjMzdlNjMwZjY5OTI4MzZkMTY3MjdjOWI2NGI0MzBjMDFmNDE5ZGVjZTJjZWFlZTIwNzM3NjEyNWUyMDZhMTgyMTlmN2Y0M2ExOWQxZFtBTEx8Rk9SS0lEXSAwMzhkNmJmZDEzNjU1MmE5ZWNmZjhkZmQ0YTNiODFlNTFiMGFmZTk2MTU1ODI3YTYwNDdjOGUzNGUwYTA4ODM5N2UiLCJoZXgiOiI0MTEyODE4Mzk5ZGZkYTY0MzZhMzUwNWNiY2U4NmU0ZjllZTZlOWRjNzlkZTQ1ZjcyNjI2ZWMzN2U2MzBmNjk5MjgzNmQxNjcyN2M5YjY0YjQzMGMwMWY0MTlkZWNlMmNlYWVlMjA3Mzc2MTI1ZTIwNmExODIxOWY3ZjQzYTE5ZDFkNDEyMTAzOGQ2YmZkMTM2NTUyYTllY2ZmOGRmZDRhM2I4MWU1MWIwYWZlOTYxNTU4MjdhNjA0N2M4ZTM0ZTBhMDg4Mzk3ZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOjQ5LCJzY3JpcHRTaWciOnsiYXNtIjoiODE1ZDgzNzRkMjdlZWE4M2I2YWU3ZmNkYjA1NzRhZTI5MDUzOWE0ZDI3M2MzNTAwZGVmY2JhMWZjMWY3YTI1NDIwNGE0NTA5ODg1ODc0YTI0OTNhYWRhNjQwNGU0MjZiYjk5MTQwNmQ0ZTUzYjVmMzY4MGU3ODQ3M2NkMjg0M2ZbQUxMfEZPUktJRF0gMDNiYmYxZDNiYjg0NDQ1NjgyZGE2ZjBiMjllNDliNDE1ZjU3OGUzNzVlZjJhOTlmMzZiNWQ1OWU5NjczNzQ2NzA2IiwiaGV4IjoiNDE4MTVkODM3NGQyN2VlYTgzYjZhZTdmY2RiMDU3NGFlMjkwNTM5YTRkMjczYzM1MDBkZWZjYmExZmMxZjdhMjU0MjA0YTQ1MDk4ODU4NzRhMjQ5M2FhZGE2NDA0ZTQyNmJiOTkxNDA2ZDRlNTNiNWYzNjgwZTc4NDczY2QyODQzZjQxMjEwM2JiZjFkM2JiODQ0NDU2ODJkYTZmMGIyOWU0OWI0MTVmNTc4ZTM3NWVmMmE5OWYzNmI1ZDU5ZTk2NzM3NDY3MDYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjQ1ZmI1MjA5MTgxYmEzNGViM2JhMzA1N2FiZjA0OTM5YzM5NTU5OTdmNjhiYzdlNDk2MmRlMzM5NzlkZDM4ODgiLCJ2b3V0IjoyOCwic2NyaXB0U2lnIjp7ImFzbSI6IjlhZmU4ODJhOWE3MjdkNzhmZTA5Zjk4NGMzZTExNDg5ZGRhMjBmOWMyYmIyYmEyYzk0MzczNWVhOGJkYjg3Y2NjYjlhMjk3ZDViYjA4OGI0ZmQzZDQzNjkxNzg3NzBiNWE2NDc3NWUyN2VhM2Q5MTg4MmM3MGVjODdmMzFkN2QzW0FMTHxGT1JLSURdIDAzMTJkMmYxNGRkYjY1Y2IwNTA3NGU3ZDZlMDY5ZTE4ZjNjOTQ2MTI0ZjhlMTZjMTUwYjE3ODdjN2Q1NGFiM2Q4ZiIsImhleCI6IjQxOWFmZTg4MmE5YTcyN2Q3OGZlMDlmOTg0YzNlMTE0ODlkZGEyMGY5YzJiYjJiYTJjOTQzNzM1ZWE4YmRiODdjY2NiOWEyOTdkNWJiMDg4YjRmZDNkNDM2OTE3ODc3MGI1YTY0Nzc1ZTI3ZWEzZDkxODgyYzcwZWM4N2YzMWQ3ZDM0MTIxMDMxMmQyZjE0ZGRiNjVjYjA1MDc0ZTdkNmUwNjllMThmM2M5NDYxMjRmOGUxNmMxNTBiMTc4N2M3ZDU0YWIzZDhmIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI0NWZiNTIwOTE4MWJhMzRlYjNiYTMwNTdhYmYwNDkzOWMzOTU1OTk3ZjY4YmM3ZTQ5NjJkZTMzOTc5ZGQzODg4Iiwidm91dCI6MzgsInNjcmlwdFNpZyI6eyJhc20iOiI1NGIzYjRjNzI1NWY4NmIxZTM0YzYxN2M4YzcwNTNhMzE0NDE3MzhmZDI0Mzg2MWVkYjRmNzljM2MzNTM2N2NkOTk3OGFhMzhkZDIzM2QyODcyNzhiMzVhMmM5N2Q2ZDg2NTE5YmJmM2Q3YWU1ZDM0MzkxMDEzZDNjYmMxYTVjOFtBTEx8Rk9SS0lEXSAwMjVjN2E5YTdkYzA4MTc4ZjY0YTQ3ZWI0NGZkMWZmZTQ4NjMxNjM5ODczNjk5NzYwNDMyMzIyNjYyN2U1NmM0MWIiLCJoZXgiOiI0MTU0YjNiNGM3MjU1Zjg2YjFlMzRjNjE3YzhjNzA1M2EzMTQ0MTczOGZkMjQzODYxZWRiNGY3OWMzYzM1MzY3Y2Q5OTc4YWEzOGRkMjMzZDI4NzI3OGIzNWEyYzk3ZDZkODY1MTliYmYzZDdhZTVkMzQzOTEwMTNkM2NiYzFhNWM4NDEyMTAyNWM3YTlhN2RjMDgxNzhmNjRhNDdlYjQ0ZmQxZmZlNDg2MzE2Mzk4NzM2OTk3NjA0MzIzMjI2NjI3ZTU2YzQxYiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNDVmYjUyMDkxODFiYTM0ZWIzYmEzMDU3YWJmMDQ5MzljMzk1NTk5N2Y2OGJjN2U0OTYyZGUzMzk3OWRkMzg4OCIsInZvdXQiOjQ3LCJzY3JpcHRTaWciOnsiYXNtIjoiZWQxZjkzOTY3MGRlYWM2MTAxYWE3Y2UxYjI2Zjk3M2I2Zjc5YzQ2YWEzYWYyNmQzN2ViZTk1Nzg4ZDQ2MTUwZmI0MjNmNjM4OWRjNzZmYzkyMWE4YmI4ODhiZGIwZmFmZDVmZjY2MjMxYjlkZmIzYTc1NTBjZTljY2M5ZTVjM2ZbQUxMfEZPUktJRF0gMDM4ZDU5NGI0YThlZDMxYmZkYWZmYmEwYzlhYzI4Mzk1NTNjZGJhYWI5YjYzMGVjODZkOGJhMDU2NWIwYmNmYzE4IiwiaGV4IjoiNDFlZDFmOTM5NjcwZGVhYzYxMDFhYTdjZTFiMjZmOTczYjZmNzljNDZhYTNhZjI2ZDM3ZWJlOTU3ODhkNDYxNTBmYjQyM2Y2Mzg5ZGM3NmZjOTIxYThiYjg4OGJkYjBmYWZkNWZmNjYyMzFiOWRmYjNhNzU1MGNlOWNjYzllNWMzZjQxMjEwMzhkNTk0YjRhOGVkMzFiZmRhZmZiYTBjOWFjMjgzOTU1M2NkYmFhYjliNjMwZWM4NmQ4YmEwNTY1YjBiY2ZjMTgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjQ1ZmI1MjA5MTgxYmEzNGViM2JhMzA1N2FiZjA0OTM5YzM5NTU5OTdmNjhiYzdlNDk2MmRlMzM5NzlkZDM4ODgiLCJ2b3V0Ijo1Miwic2NyaXB0U2lnIjp7ImFzbSI6IjA1M2VjZGM0MGU4YzJhOWRjZDJiNTQwZjcxYjg0NzkxOThmMjVmNzUyYmFiOTcxODM0NWE1ZDVkZGZjNWIxYTliMThkNmM4MjczOGRlMGQxNzgyMjU1Y2QzZDdlYTdlNTRlN2Q3OGU5Mjk2NmQxY2RiYjgwMDVlYzU4NDQ1YjU0W0FMTHxGT1JLSURdIDAzYzBhODNlMWRhYmRhYmI2ZDJiMjk5YjBhOWIxOWU2M2ZkY2NkMzY2NzRhYWUwYjE4MGI3MTE3MzQ3NmQ4NzNjMCIsImhleCI6IjQxMDUzZWNkYzQwZThjMmE5ZGNkMmI1NDBmNzFiODQ3OTE5OGYyNWY3NTJiYWI5NzE4MzQ1YTVkNWRkZmM1YjFhOWIxOGQ2YzgyNzM4ZGUwZDE3ODIyNTVjZDNkN2VhN2U1NGU3ZDc4ZTkyOTY2ZDFjZGJiODAwNWVjNTg0NDViNTQ0MTIxMDNjMGE4M2UxZGFiZGFiYjZkMmIyOTliMGE5YjE5ZTYzZmRjY2QzNjY3NGFhZTBiMTgwYjcxMTczNDc2ZDg3M2MwIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI1OGJkY2I2M2YzMGRmNmE3NGI1ODEzMjRiMGY0ZGJkZDllODQzMzdiNjFmYzFlYmYxOWJmOGI4OGY4MDdmNzgxIiwidm91dCI6MTgsInNjcmlwdFNpZyI6eyJhc20iOiI2ZjA3MDY3NDkzMDJhNjMwYWM5NGJjNTNjYTk1ZmUwZWU1N2E3NTdiN2YwYWI0NTc2ZDZjMmI1ZGE0NWZhODA3MjRjMDhiMTRjYTkwYjQwNmFlZWE2MGM5MDRmYmRlZDc2N2Q5YTM0ODYzZDdhZjZjMzRmNDQ2MGIyMWExYWM3NltBTEx8Rk9SS0lEXSAwMmE5MWExY2I4N2IyYWI0MDY0NzBhZjIzYzg2MWI0ZDYzYjA2YzMxNTNmZTI2MmVlODNhYjA0NTFjZmNmZmQ1YjIiLCJoZXgiOiI0MTZmMDcwNjc0OTMwMmE2MzBhYzk0YmM1M2NhOTVmZTBlZTU3YTc1N2I3ZjBhYjQ1NzZkNmMyYjVkYTQ1ZmE4MDcyNGMwOGIxNGNhOTBiNDA2YWVlYTYwYzkwNGZiZGVkNzY3ZDlhMzQ4NjNkN2FmNmMzNGY0NDYwYjIxYTFhYzc2NDEyMTAyYTkxYTFjYjg3YjJhYjQwNjQ3MGFmMjNjODYxYjRkNjNiMDZjMzE1M2ZlMjYyZWU4M2FiMDQ1MWNmY2ZmZDViMiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNWViMTUzZWViYzVlZjViYmJiOWNhOGY2YzVkNDFhN2FiOTNiM2RhMThhMGJlYWMxYzExNTVjYzhkY2E5NmYwMiIsInZvdXQiOjYsInNjcmlwdFNpZyI6eyJhc20iOiIxMzViZDM2MWFjZTEyYjJiNTg2M2YxODZhOTI4NTg5OGE0Yjg1ZmY4MGJmOWRmMzdkYzMyNjkyNDczOTY2MTAyY2QxZTE1NGEyNzY2OWE1NzNjMmRhODIxZDdjMDQwNDk3Y2FkMTdiNzNjMWYyNDg4NTkyOWVkOGE5ODAzZmU5NFtBTEx8Rk9SS0lEXSAwMzY4MzNjMGQ3MzU4NjQ1NDAwYmI0ZjZjZDM0YmZkMDIwNWZhZDI5ZjBkMDYzN2FkYmUyNTc0YTRiZjIyYTdkOGUiLCJoZXgiOiI0MTEzNWJkMzYxYWNlMTJiMmI1ODYzZjE4NmE5Mjg1ODk4YTRiODVmZjgwYmY5ZGYzN2RjMzI2OTI0NzM5NjYxMDJjZDFlMTU0YTI3NjY5YTU3M2MyZGE4MjFkN2MwNDA0OTdjYWQxN2I3M2MxZjI0ODg1OTI5ZWQ4YTk4MDNmZTk0NDEyMTAzNjgzM2MwZDczNTg2NDU0MDBiYjRmNmNkMzRiZmQwMjA1ZmFkMjlmMGQwNjM3YWRiZTI1NzRhNGJmMjJhN2Q4ZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNWViMTUzZWViYzVlZjViYmJiOWNhOGY2YzVkNDFhN2FiOTNiM2RhMThhMGJlYWMxYzExNTVjYzhkY2E5NmYwMiIsInZvdXQiOjE0LCJzY3JpcHRTaWciOnsiYXNtIjoiZjBlYjA0N2VlZTMwZjUwMTU0ODlhNDM3NzdjNjQ2OTg1NmRiM2I1MTA5MzM4ODdiNjkzMjNlMjllODllOGI1ODA2OTY3YWU4NmFiZDcxOTE5OGQ2Y2YxMDAyMGVhYzFjNmUyM2UwZDI3NTE5NDQ2NTZhZWNkMGMzYjI3ZGRlNGNbQUxMfEZPUktJRF0gMDNkZTFiNmJmZTI1MGJmNzkxYWYxOTkxMGVkZTgyNzgzYWNiM2E5NjY1ZjZmNTJiZjViYWM5MDQyY2MyZGUxNWQwIiwiaGV4IjoiNDFmMGViMDQ3ZWVlMzBmNTAxNTQ4OWE0Mzc3N2M2NDY5ODU2ZGIzYjUxMDkzMzg4N2I2OTMyM2UyOWU4OWU4YjU4MDY5NjdhZTg2YWJkNzE5MTk4ZDZjZjEwMDIwZWFjMWM2ZTIzZTBkMjc1MTk0NDY1NmFlY2QwYzNiMjdkZGU0YzQxMjEwM2RlMWI2YmZlMjUwYmY3OTFhZjE5OTEwZWRlODI3ODNhY2IzYTk2NjVmNmY1MmJmNWJhYzkwNDJjYzJkZTE1ZDAifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjZmODRjNWRmNjE0Yzk3MTBmNDUzMzFhYTgzNDgxY2M0NTM2M2YwMTUyYmQwMzMxNjdhOGNlZTAzZjNiNmIyMmIiLCJ2b3V0Ijo4LCJzY3JpcHRTaWciOnsiYXNtIjoiNGRiNzNjMmM1MDQ4ZDc2ZDY2NGNhMDQwOGMyOGI5ZGI2ZDg5ZTYzODI3YzhhYzFlOWY0M2QxNzE5YTM1OTY2OGQ3OThlMmI2NmYxNDU2NGFjMGY4MjVmMDJiMDA4MGRhYmRmODZlMjRhMWE2YWU2NDk1Y2MxODMxNTQ3NWE2MjZbQUxMfEZPUktJRF0gMDMwY2YxZTA2MWZiNmJmOTNhMTQ2YjFhM2Q2MWUzZWNhMDk1ZjlmZmRkZTljYjIxZmNmNjk2MjY2ZGNjMWQ0ZTFhIiwiaGV4IjoiNDE0ZGI3M2MyYzUwNDhkNzZkNjY0Y2EwNDA4YzI4YjlkYjZkODllNjM4MjdjOGFjMWU5ZjQzZDE3MTlhMzU5NjY4ZDc5OGUyYjY2ZjE0NTY0YWMwZjgyNWYwMmIwMDgwZGFiZGY4NmUyNGExYTZhZTY0OTVjYzE4MzE1NDc1YTYyNjQxMjEwMzBjZjFlMDYxZmI2YmY5M2ExNDZiMWEzZDYxZTNlY2EwOTVmOWZmZGRlOWNiMjFmY2Y2OTYyNjZkY2MxZDRlMWEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjZmODRjNWRmNjE0Yzk3MTBmNDUzMzFhYTgzNDgxY2M0NTM2M2YwMTUyYmQwMzMxNjdhOGNlZTAzZjNiNmIyMmIiLCJ2b3V0IjoyMywic2NyaXB0U2lnIjp7ImFzbSI6ImJhZjBmZTJkNTA2MzkzMWZjNzdjMmU4MjY0YzU4NmRmMWY1MmZmYmU3ZTA0OGRiYmJjMGMyOWRlOGEyNjY2NWFiY2MyMTE1Y2IwNWExNGRiYmY3MmU2MTVjNzA4MjJkYmViZDI2ZTA4NTY1ZjM3M2EyMWI5ODAzYWY1ODgzZWVjW0FMTHxGT1JLSURdIDAzMTg0ZDZlYmE2ZjQyZjg5MmRiNmEzYzY4OWNhNmZkMTVkMzJjZDg0NWVlNjRlMzQ0MThlZjc4NGI4YTA4MWM3MSIsImhleCI6IjQxYmFmMGZlMmQ1MDYzOTMxZmM3N2MyZTgyNjRjNTg2ZGYxZjUyZmZiZTdlMDQ4ZGJiYmMwYzI5ZGU4YTI2NjY1YWJjYzIxMTVjYjA1YTE0ZGJiZjcyZTYxNWM3MDgyMmRiZWJkMjZlMDg1NjVmMzczYTIxYjk4MDNhZjU4ODNlZWM0MTIxMDMxODRkNmViYTZmNDJmODkyZGI2YTNjNjg5Y2E2ZmQxNWQzMmNkODQ1ZWU2NGUzNDQxOGVmNzg0YjhhMDgxYzcxIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI4MWQyN2I0NmJhMzAxNTNhMTJlYzc4ZTA2NTcyODlmMThjZjgyZWZhNDQwOWU2NDRlZDQ3Y2M0MzlhN2UzYThiIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjAxN2ZkNzI3YzdmYWFmZjNkNjM0NjIzMGQzMTdlZWRhNzMyZWZlM2E5N2QxOGQ4Njg3OWFhYmU5MGM2YzY2ZTg3ZDA2ZDdhMjc4M2UzODExYTM2ZjhjMWI3Y2JiNWE0M2IxODIyMDE4MjVlYzgzMjI2ODQ2NzdlNDE2Yjg0ZmM2W0FMTHxGT1JLSURdIDAyN2NmYzI1M2E2M2Y3MjBlMzlhZmJmNGYwNGNjYjVmZTdiOGY5MDAyNmE0MWNjYjY0ZDNiMmUxMTIxYjYyODkxZCIsImhleCI6IjQxMDE3ZmQ3MjdjN2ZhYWZmM2Q2MzQ2MjMwZDMxN2VlZGE3MzJlZmUzYTk3ZDE4ZDg2ODc5YWFiZTkwYzZjNjZlODdkMDZkN2EyNzgzZTM4MTFhMzZmOGMxYjdjYmI1YTQzYjE4MjIwMTgyNWVjODMyMjY4NDY3N2U0MTZiODRmYzY0MTIxMDI3Y2ZjMjUzYTYzZjcyMGUzOWFmYmY0ZjA0Y2NiNWZlN2I4ZjkwMDI2YTQxY2NiNjRkM2IyZTExMjFiNjI4OTFkIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI4N2U5Y2M0MDM1MmExODIxYTM0N2JlOWMyM2UyYjM3MmUzNDNlMTkyZWRmYzU5NzY1MjY1YjFmNzllMWI3NWE3Iiwidm91dCI6MTIsInNjcmlwdFNpZyI6eyJhc20iOiI3MzIzMjJmMDgzMzI5ZjY4N2Q2NWUwNzNkMDIzMWY2YmY5YWJiMzBmMzMwYjgwOGY0YTVmMjMyOTRjZWRkYjgzNjgyZmM3OTIyNmZjMGU4YzMwZmIwYjM2ZjEwYzRmNDBiNGY4MjIyYTAxNWI5MWMxYjdlYzhjMDEzYTQ2MTE5YVtBTEx8Rk9SS0lEXSAwMjAwMjQ3YzUwMDFiMjQ3OWM2N2Q5OWI0OTQ2YmJjY2IwOTFmZDA5OTFiZDkxYjkyYjNjMGFlMWU3NGE1MGY4OGYiLCJoZXgiOiI0MTczMjMyMmYwODMzMjlmNjg3ZDY1ZTA3M2QwMjMxZjZiZjlhYmIzMGYzMzBiODA4ZjRhNWYyMzI5NGNlZGRiODM2ODJmYzc5MjI2ZmMwZThjMzBmYjBiMzZmMTBjNGY0MGI0ZjgyMjJhMDE1YjkxYzFiN2VjOGMwMTNhNDYxMTlhNDEyMTAyMDAyNDdjNTAwMWIyNDc5YzY3ZDk5YjQ5NDZiYmNjYjA5MWZkMDk5MWJkOTFiOTJiM2MwYWUxZTc0YTUwZjg4ZiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiOTAwYjY2M2RkZDA3NjJiMzRmZDgxNjhkM2FjZWNlMGU4OGUzODIxYmU3M2FjOWRhMDhkN2E0NDRlOGI3MzA5ZiIsInZvdXQiOjIwLCJzY3JpcHRTaWciOnsiYXNtIjoiMjNlMTUzMjIzYjJmNmYyOGRkOWMwYWFlZWM5ZjI3ZTgzYmI1ZWFjYzE5NzFlMTgyM2M0NzVhMTVhNDg1ZmE3MjMwMTkwYjU1YzlkOWU4MDMxMjg1MTY1YjgzNzk4MGUxNjdkMjM1NWJlMzY3ZTAyYTI5NWE4MTQ3ODhjMjM2OTJbQUxMfEZPUktJRF0gMDMwOWM1ZjYyN2YxMjI0NGU0NGU4Y2IzNGQ4OWE2Njg0MDkwZmUxZTFiMjRmZjcxZTVkYzNiNTFmMWY4OGI5MjJiIiwiaGV4IjoiNDEyM2UxNTMyMjNiMmY2ZjI4ZGQ5YzBhYWVlYzlmMjdlODNiYjVlYWNjMTk3MWUxODIzYzQ3NWExNWE0ODVmYTcyMzAxOTBiNTVjOWQ5ZTgwMzEyODUxNjViODM3OTgwZTE2N2QyMzU1YmUzNjdlMDJhMjk1YTgxNDc4OGMyMzY5MjQxMjEwMzA5YzVmNjI3ZjEyMjQ0ZTQ0ZThjYjM0ZDg5YTY2ODQwOTBmZTFlMWIyNGZmNzFlNWRjM2I1MWYxZjg4YjkyMmIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjlmZjgxZTRhY2QyMTc1N2RiMTMyYjc1OWY0ZTliZWUwZWM4YTk5NmI4MmVmN2E2ODRhZjExNTEyNWYxMjJmYzgiLCJ2b3V0IjoxMCwic2NyaXB0U2lnIjp7ImFzbSI6IjlmYTUxYjdkZjNiZjY5MTBjZDU1YTE0YTg3MTUyNTlkODU3ZGU1Y2VlZGU3MThhZjI5YTY2YWQ2YmVhYjE2MTI4NzFlN2RmMmYxOWNlODc3ZjA5NjgzNmI4OTllNzZiMWEzODY5NTFkZDAyM2VjYjA5OWZiZGQ2OGY4OTdjMmY0W0FMTHxGT1JLSURdIDAyNmVjZjk5MmIzNGJkNjcwMmI3NTJiODk0MGZkYmJhZGUyY2VhYjc5NTk3YWE5ZWYyNTljMjU1ODg4YmEzMjhjNiIsImhleCI6IjQxOWZhNTFiN2RmM2JmNjkxMGNkNTVhMTRhODcxNTI1OWQ4NTdkZTVjZWVkZTcxOGFmMjlhNjZhZDZiZWFiMTYxMjg3MWU3ZGYyZjE5Y2U4NzdmMDk2ODM2Yjg5OWU3NmIxYTM4Njk1MWRkMDIzZWNiMDk5ZmJkZDY4Zjg5N2MyZjQ0MTIxMDI2ZWNmOTkyYjM0YmQ2NzAyYjc1MmI4OTQwZmRiYmFkZTJjZWFiNzk1OTdhYTllZjI1OWMyNTU4ODhiYTMyOGM2In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI5ZmY4MWU0YWNkMjE3NTdkYjEzMmI3NTlmNGU5YmVlMGVjOGE5OTZiODJlZjdhNjg0YWYxMTUxMjVmMTIyZmM4Iiwidm91dCI6MTEsInNjcmlwdFNpZyI6eyJhc20iOiIwZDRhZjhlNTIzM2JmYWQ0ZjM3YWY2Y2EyZDc5ZDJkNTFhMjY4MzU0MjIxYjc3YmRiM2NlN2I4YWU4NDRmZGI3N2M4OWFlM2Y4ZjcxODJkOTY4NWQ4ZDlmNmQyMjk0MTFjYmFiYTgzNmVmMmQ5NWU0NGEwYWFhMWYwNDY5YTJmMFtBTEx8Rk9SS0lEXSAwMjk3MDRlYjA1MTFkNjFiYzRjNDg2YjkyYmY2NjJjYmQxNTQ0OGI0YjFjOTE5OWFiYTI5MTk2MWI2MzVkMDc5N2MiLCJoZXgiOiI0MTBkNGFmOGU1MjMzYmZhZDRmMzdhZjZjYTJkNzlkMmQ1MWEyNjgzNTQyMjFiNzdiZGIzY2U3YjhhZTg0NGZkYjc3Yzg5YWUzZjhmNzE4MmQ5Njg1ZDhkOWY2ZDIyOTQxMWNiYWJhODM2ZWYyZDk1ZTQ0YTBhYWExZjA0NjlhMmYwNDEyMTAyOTcwNGViMDUxMWQ2MWJjNGM0ODZiOTJiZjY2MmNiZDE1NDQ4YjRiMWM5MTk5YWJhMjkxOTYxYjYzNWQwNzk3YyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiOWZmODFlNGFjZDIxNzU3ZGIxMzJiNzU5ZjRlOWJlZTBlYzhhOTk2YjgyZWY3YTY4NGFmMTE1MTI1ZjEyMmZjOCIsInZvdXQiOjI2LCJzY3JpcHRTaWciOnsiYXNtIjoiMjczMDFjZDhlOTkwMzk4MDE4NjRkNjIwZTY1OGIzYTAwMDdiZTYyMmQxNWFjMDdmZmNkOGM5ODcxMjJlMGIyZmNkMzJlZGY5MmNkMWJiMGY4ZWU1OTBmNTMzZDRlMGE1N2U0MTA2NDE4Mzk4YTQ2ZmY2MTk5YjE2ZjMyNWQxYTBbQUxMfEZPUktJRF0gMDMzZjdmYzNlZDkyOTFlNTQ1ZTQwNjdkNDU3MjBjMWNkNzRlODc4MGMyYjMyZWRkMTExYjEyNDk1OGY0YmZhZGYxIiwiaGV4IjoiNDEyNzMwMWNkOGU5OTAzOTgwMTg2NGQ2MjBlNjU4YjNhMDAwN2JlNjIyZDE1YWMwN2ZmY2Q4Yzk4NzEyMmUwYjJmY2QzMmVkZjkyY2QxYmIwZjhlZTU5MGY1MzNkNGUwYTU3ZTQxMDY0MTgzOThhNDZmZjYxOTliMTZmMzI1ZDFhMDQxMjEwMzNmN2ZjM2VkOTI5MWU1NDVlNDA2N2Q0NTcyMGMxY2Q3NGU4NzgwYzJiMzJlZGQxMTFiMTI0OTU4ZjRiZmFkZjEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImFkZGQ4MDQwMTYzYTAwZGViMjI5NjZiM2QyOTlhMThiYTE0ZmE0NzJhOGJhOWVlZDA5MmM0MjA1MDYyM2M2OGUiLCJ2b3V0IjoyMywic2NyaXB0U2lnIjp7ImFzbSI6IjJkMDlkY2E2YjhhMzRiNzc3YzdkZjhiODkzNGMwMWEzOTZkMWFmYWJkZDI1OWI3YTg2NjExZDE0MGI3ZmUyY2Q2MWFkNDIwNzIwOGM0MzczNzI1YmYzNGM3MjRlODFiYWQ4NDIxNzU0YjMyNTAyODBhNTU5ZjRjMzdhYjYwMWJhW0FMTHxGT1JLSURdIDAzMWY4MDA3MTFjZGM3MDUyOWUzMTZmMzdlNmQwMjE0YmFlMGNkYTg4ZmRlYmRiYzUzZTMxNmI2ZWI4NzY3ZDcyMSIsImhleCI6IjQxMmQwOWRjYTZiOGEzNGI3NzdjN2RmOGI4OTM0YzAxYTM5NmQxYWZhYmRkMjU5YjdhODY2MTFkMTQwYjdmZTJjZDYxYWQ0MjA3MjA4YzQzNzM3MjViZjM0YzcyNGU4MWJhZDg0MjE3NTRiMzI1MDI4MGE1NTlmNGMzN2FiNjAxYmE0MTIxMDMxZjgwMDcxMWNkYzcwNTI5ZTMxNmYzN2U2ZDAyMTRiYWUwY2RhODhmZGViZGJjNTNlMzE2YjZlYjg3NjdkNzIxIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJiN2M5NjkwM2NlZjA4Y2VkYThmNzgyMjUwOWUyNDNiOWIxZGI4ZGY0NTA1NTM0ZjZkMjY4ZWYwYzA4YzgzMjc1Iiwidm91dCI6Nywic2NyaXB0U2lnIjp7ImFzbSI6IjI5YjlhODRmY2ZiOWE5MzMxZGMzNTg1OTBiM2IxYmUyYjJjMDY1ZmI2NTQ4ZGVlMDZmZTUwY2IwOTA3YzhmNThhNzUzMDQ5ZjE1N2JkYzQ3YTEyNGFhNjZjYzZhZGEwZDRkNWEzYWFkNzA2NjA2N2Y2M2MwNzhmM2U1ZjVjMTI4W0FMTHxGT1JLSURdIDAyMGI4M2Y2ZDhjNzhkMzNmZDEzY2Q1YTA2MjhkYzdjNjkxYzBjMzA1ZWQ2Nzg5NzdmZjYwZWU4ZTFjMjk4YzcwZSIsImhleCI6IjQxMjliOWE4NGZjZmI5YTkzMzFkYzM1ODU5MGIzYjFiZTJiMmMwNjVmYjY1NDhkZWUwNmZlNTBjYjA5MDdjOGY1OGE3NTMwNDlmMTU3YmRjNDdhMTI0YWE2NmNjNmFkYTBkNGQ1YTNhYWQ3MDY2MDY3ZjYzYzA3OGYzZTVmNWMxMjg0MTIxMDIwYjgzZjZkOGM3OGQzM2ZkMTNjZDVhMDYyOGRjN2M2OTFjMGMzMDVlZDY3ODk3N2ZmNjBlZThlMWMyOThjNzBlIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJiOGUwNzY4M2ZmMWU2YTQ1NGFjYmY3MDA4ZTg4YTEyNmVmY2YyMTRmZmFkMDM5YjlmMDgyZjk4Yzc2YTE2N2VhIiwidm91dCI6MjYsInNjcmlwdFNpZyI6eyJhc20iOiI4NmJjOTdmMDAxYzc5NWVkZjJkZWYyMjc3NjMxZGRkNzcxMjQ3ZGRiOTQzOGM2YzEwMzY4YzY0NjE2OTFjM2YxYmEwNWEwYmFmMWFhNGI3MzlmNDUzZjQ2MjQ0YzdlOWZkYTM4YjczZWUyMTgzZDRhMTg5OTM5YTAxNGVjZDE5ZVtBTEx8Rk9SS0lEXSAwMjhiNDA4M2NiYTg1NGI4MjEzODA3NzQ3YmUxZTM0ZWY2ZWM3Zjg0OTI4ZWYwZWE3MzEzNTJlZmIyNTQ4Y2EzNWIiLCJoZXgiOiI0MTg2YmM5N2YwMDFjNzk1ZWRmMmRlZjIyNzc2MzFkZGQ3NzEyNDdkZGI5NDM4YzZjMTAzNjhjNjQ2MTY5MWMzZjFiYTA1YTBiYWYxYWE0YjczOWY0NTNmNDYyNDRjN2U5ZmRhMzhiNzNlZTIxODNkNGExODk5MzlhMDE0ZWNkMTllNDEyMTAyOGI0MDgzY2JhODU0YjgyMTM4MDc3NDdiZTFlMzRlZjZlYzdmODQ5MjhlZjBlYTczMTM1MmVmYjI1NDhjYTM1YiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYmM2MDZhNTUwNDMwNjc2N2FhNDkxZWY3NTg5N2NjYjc0YTM4MTM0M2Y2ZjU0YTA0MDk1ZjU3ZDk4YjA0Mzc2OSIsInZvdXQiOjMxLCJzY3JpcHRTaWciOnsiYXNtIjoiZGY1OWU4YmNkZTc2Mzc2MjgxOTNkMDVmZjY2MDY4ZGZlMGM5YzI2ZjA2NzFhNjRiN2U0NzIzOTA4ZTFiZjZjYTMzMmJiZWMwOTI1MzBiMDE2NmZmYWYwZjUzNTE3NDgzZGJmMzBlNmQwZDc1NmZhNmQzNjQ5ODVhNGIwZmU0MzlbQUxMfEZPUktJRF0gMDI1YjNmNzU1NzRiOGI5MDQxYjAyNGZlZWE3ZjMwODMyYzdhZjAwNzk3NjE3N2QwMGM2NjM1YWZkMDVmMjdhZDJjIiwiaGV4IjoiNDFkZjU5ZThiY2RlNzYzNzYyODE5M2QwNWZmNjYwNjhkZmUwYzljMjZmMDY3MWE2NGI3ZTQ3MjM5MDhlMWJmNmNhMzMyYmJlYzA5MjUzMGIwMTY2ZmZhZjBmNTM1MTc0ODNkYmYzMGU2ZDBkNzU2ZmE2ZDM2NDk4NWE0YjBmZTQzOTQxMjEwMjViM2Y3NTU3NGI4YjkwNDFiMDI0ZmVlYTdmMzA4MzJjN2FmMDA3OTc2MTc3ZDAwYzY2MzVhZmQwNWYyN2FkMmMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImJjOGFmNDM4MTgzNGM3NzRhZTZmOGM3ODM4YTIxNjY0NTM5YzA1MDM4YmE3ZGViYzNkMGFjZjFkZWJjY2UyNDAiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiMGVlNGZjN2Q0YjJkYTI3Zjk3MmFlZjY4MDk0MmI3OTc2YzhhZGQ2OTc4MzM2NjQ3ZDMzNTRmNDg4OTBiYjI3ZmQ0OTg2MGY4ZDk0OTM3NDBiOTYwZDAxOWMwNjEwOGQwYWQxNjM2NDhkYTYxNTRlODQ3MWQ2YzBiOGQ5MGQ1NzFbQUxMfEZPUktJRF0gMDMwMjVlZDBjYjJhYjUyOTUwZWNjYjc3NjVkMDRiMWU0MDEwYjE2ODU1YTJmNGQ1ZGJiOGY0OTcwNmM5NzljMDcxIiwiaGV4IjoiNDEwZWU0ZmM3ZDRiMmRhMjdmOTcyYWVmNjgwOTQyYjc5NzZjOGFkZDY5NzgzMzY2NDdkMzM1NGY0ODg5MGJiMjdmZDQ5ODYwZjhkOTQ5Mzc0MGI5NjBkMDE5YzA2MTA4ZDBhZDE2MzY0OGRhNjE1NGU4NDcxZDZjMGI4ZDkwZDU3MTQxMjEwMzAyNWVkMGNiMmFiNTI5NTBlY2NiNzc2NWQwNGIxZTQwMTBiMTY4NTVhMmY0ZDVkYmI4ZjQ5NzA2Yzk3OWMwNzEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImJjOGFmNDM4MTgzNGM3NzRhZTZmOGM3ODM4YTIxNjY0NTM5YzA1MDM4YmE3ZGViYzNkMGFjZjFkZWJjY2UyNDAiLCJ2b3V0IjoxNCwic2NyaXB0U2lnIjp7ImFzbSI6IjgyMjYyMmMyYzljOTU5MGZlZjBiMDMyMGM5YjViODQ4ZDhmZWEwZjYxZWFhNDExNDgwZDNmYmE1ZjdkYTA4ZjgzNTcyZDU4Y2VmYjE1OTEwYTU0ZmY0OWI4YTlhMWNiZjQ5ODBiYzRiMzIxYmFjNTUxMzg3ZTA0YTlmY2ZkNjIyW0FMTHxGT1JLSURdIDAzNWY2MWFjZmJmYTdhMWI3ODViNzYyMDUzZDk2MDE4YjU5MjlhZTdlNThjMDc1ODNhNjI2MDlhMWExM2UyMjY4NCIsImhleCI6IjQxODIyNjIyYzJjOWM5NTkwZmVmMGIwMzIwYzliNWI4NDhkOGZlYTBmNjFlYWE0MTE0ODBkM2ZiYTVmN2RhMDhmODM1NzJkNThjZWZiMTU5MTBhNTRmZjQ5YjhhOWExY2JmNDk4MGJjNGIzMjFiYWM1NTEzODdlMDRhOWZjZmQ2MjI0MTIxMDM1ZjYxYWNmYmZhN2ExYjc4NWI3NjIwNTNkOTYwMThiNTkyOWFlN2U1OGMwNzU4M2E2MjYwOWExYTEzZTIyNjg0In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjNjM1NDllM2ZhNjY1NjAxYTI5OGNmNDZiY2UyNTM4OGY1ZGMxZGNiMGI5MWMyMmU4ZmQ2NWE0ZmRhNDkwZjIyIiwidm91dCI6MjQsInNjcmlwdFNpZyI6eyJhc20iOiIzMmM0ZjA1YTVlMWE1NWUwM2QxYzE4MGUwN2YxOTRmNGQ3M2ZjMjVhZjk2NTYxMzhmNWEzZmY5OTM3ZDRhNDU3NTA3OTgyMTc3MzEwNjRhNmJjYjYzNDNjYjA5OGQyNjkxZWU1MmE3MTBkYTQ2YjRjMDVmN2M1NjZkZGM3MTIzNltBTEx8Rk9SS0lEXSAwMzQwYmYxYzcyNmFhZDI5OTkzMTI2MzVlZWIxYTE2M2Q2ZjNlYmM3OGI0YjZjNzllZDVmNzZkZDdmNDA5NDQ2YTEiLCJoZXgiOiI0MTMyYzRmMDVhNWUxYTU1ZTAzZDFjMTgwZTA3ZjE5NGY0ZDczZmMyNWFmOTY1NjEzOGY1YTNmZjk5MzdkNGE0NTc1MDc5ODIxNzczMTA2NGE2YmNiNjM0M2NiMDk4ZDI2OTFlZTUyYTcxMGRhNDZiNGMwNWY3YzU2NmRkYzcxMjM2NDEyMTAzNDBiZjFjNzI2YWFkMjk5OTMxMjYzNWVlYjFhMTYzZDZmM2ViYzc4YjRiNmM3OWVkNWY3NmRkN2Y0MDk0NDZhMSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYzYzNTQ5ZTNmYTY2NTYwMWEyOThjZjQ2YmNlMjUzODhmNWRjMWRjYjBiOTFjMjJlOGZkNjVhNGZkYTQ5MGYyMiIsInZvdXQiOjMwLCJzY3JpcHRTaWciOnsiYXNtIjoiYTA0MDQ5NGJhYzBiYmY1NGZiNGM5Y2IzMjExNDc4Nzc2Y2RmYjkzNzY3ZTE1OWFhYzIzN2Q0NTk3YmU5MGIxYzE4MjczMTNiYTQ1YmU4NzY5NmU1MzgwZDA2NGJlMWJkMjRkNDE2OTA5YjdjZTcwMWZjYmJjMWYzN2JiZmNhZmRbQUxMfEZPUktJRF0gMDJkMzE5YWNmNzFiZGEwYjA1Y2IwMGY5YjFmNDU3NTk1ZGIzMTkzMzhiZDcyZDNlZWRiMGJlNmI0OWI1ZjdmNjYyIiwiaGV4IjoiNDFhMDQwNDk0YmFjMGJiZjU0ZmI0YzljYjMyMTE0Nzg3NzZjZGZiOTM3NjdlMTU5YWFjMjM3ZDQ1OTdiZTkwYjFjMTgyNzMxM2JhNDViZTg3Njk2ZTUzODBkMDY0YmUxYmQyNGQ0MTY5MDliN2NlNzAxZmNiYmMxZjM3YmJmY2FmZDQxMjEwMmQzMTlhY2Y3MWJkYTBiMDVjYjAwZjliMWY0NTc1OTVkYjMxOTMzOGJkNzJkM2VlZGIwYmU2YjQ5YjVmN2Y2NjIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImM2MzU0OWUzZmE2NjU2MDFhMjk4Y2Y0NmJjZTI1Mzg4ZjVkYzFkY2IwYjkxYzIyZThmZDY1YTRmZGE0OTBmMjIiLCJ2b3V0Ijo1Mywic2NyaXB0U2lnIjp7ImFzbSI6IjAzNzEyODAyMmNiZTA2NzEwYjc4YWEzOWM0NmEwMDg4ODUyMmYzNGQ2NGIyNjk2ZWExNWI5OTRiMTRjMzA0YzU2OTcyN2Q2NWI0ZDFjODc5YTU5MzIzMjZlMWRmY2M0MzdkMjM3MTQ0MDcxNTk1YTg1NmQ5OTQyZGRhNGQ4ZDU2W0FMTHxGT1JLSURdIDAzN2ViOWRlZjYxMTEwYzRiZTI4MTNiODYyMzAzYzljNmYyYzQyZGE3MDBiNGUyZTAxMzYwNjBjYWMyMGZjNzI2YSIsImhleCI6IjQxMDM3MTI4MDIyY2JlMDY3MTBiNzhhYTM5YzQ2YTAwODg4NTIyZjM0ZDY0YjI2OTZlYTE1Yjk5NGIxNGMzMDRjNTY5NzI3ZDY1YjRkMWM4NzlhNTkzMjMyNmUxZGZjYzQzN2QyMzcxNDQwNzE1OTVhODU2ZDk5NDJkZGE0ZDhkNTY0MTIxMDM3ZWI5ZGVmNjExMTBjNGJlMjgxM2I4NjIzMDNjOWM2ZjJjNDJkYTcwMGI0ZTJlMDEzNjA2MGNhYzIwZmM3MjZhIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjZGVmNGExMmRhMzU0YTJiNjhiOTY4ZDM5Y2MzN2IzNzVkZWI0YThlNGUwZDMxOGM2MTY3OGJjYmEwYzNhMzhhIiwidm91dCI6NTUsInNjcmlwdFNpZyI6eyJhc20iOiJiNzllN2VlN2M1MmZmNzhkNTExOWY3ZmQ2NjZiMjFiNWY2NWY3Zjg0MzNmOWMxYTA2MzRhN2YzNzBmY2FjZTA1ZmMyZDgyY2Q1OTMwOWYwMzRlMjhiZjFkMmMyMDRhMzkwMDc3MDU5YzU1MjM4Y2JjNTljMWE1NTA1YmNjZWQ0M1tBTEx8Rk9SS0lEXSAwMjM5NzRiZGIzMjYxNzZmNGYxNjg4YzhjYWEzZDE3NTM4MmE5MjQyYjU3NWZlMmVjZTBlNzM0YzBiMjMxODZmY2QiLCJoZXgiOiI0MWI3OWU3ZWU3YzUyZmY3OGQ1MTE5ZjdmZDY2NmIyMWI1ZjY1ZjdmODQzM2Y5YzFhMDYzNGE3ZjM3MGZjYWNlMDVmYzJkODJjZDU5MzA5ZjAzNGUyOGJmMWQyYzIwNGEzOTAwNzcwNTljNTUyMzhjYmM1OWMxYTU1MDViY2NlZDQzNDEyMTAyMzk3NGJkYjMyNjE3NmY0ZjE2ODhjOGNhYTNkMTc1MzgyYTkyNDJiNTc1ZmUyZWNlMGU3MzRjMGIyMzE4NmZjZCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZDA1ZDgwYzcyY2NlMWQ5NzM1ZWFjYjdhM2UwODE0MTA5M2EzMGI1ZWUwMDkxMTAzM2Y2NGNhYzU4ZTM0MTcxNiIsInZvdXQiOjEyLCJzY3JpcHRTaWciOnsiYXNtIjoiOWE0ZjNhZjkxMzI3ZjQyMzc2NmI3MDc5MjQyODc1MTEwNjU2OGVlYTM4NmM2MjIzNDUxNDhmMzQyMmI4NTZlNDljNjYzMmI1ZDE5MGI4YTY2YjNhNTU3ODRhZjM4M2FkMTllN2IwZWY0YTMxZjcxYTVjOTdkYWUxNWI1NGUxOGNbQUxMfEZPUktJRF0gMDIwZTI3YmQyYjBkYzg4MjQ0NDEyN2IxNDNhNjNiNmM0ODFjOGZmMmZiMzRmYzAwZWQwM2Q5N2QxMWYxYTdlMmViIiwiaGV4IjoiNDE5YTRmM2FmOTEzMjdmNDIzNzY2YjcwNzkyNDI4NzUxMTA2NTY4ZWVhMzg2YzYyMjM0NTE0OGYzNDIyYjg1NmU0OWM2NjMyYjVkMTkwYjhhNjZiM2E1NTc4NGFmMzgzYWQxOWU3YjBlZjRhMzFmNzFhNWM5N2RhZTE1YjU0ZTE4YzQxMjEwMjBlMjdiZDJiMGRjODgyNDQ0MTI3YjE0M2E2M2I2YzQ4MWM4ZmYyZmIzNGZjMDBlZDAzZDk3ZDExZjFhN2UyZWIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImQwNWQ4MGM3MmNjZTFkOTczNWVhY2I3YTNlMDgxNDEwOTNhMzBiNWVlMDA5MTEwMzNmNjRjYWM1OGUzNDE3MTYiLCJ2b3V0Ijo0MSwic2NyaXB0U2lnIjp7ImFzbSI6IjA5ODRmYWI2NDg2MTBhYzU5N2QzYTQ3ZGY4YzQ4NGEzZjY3NTFkZDYxNTUzNzE2Nzg4ZjM3ZDY4OGVlNmVkZGUwZWViZWY2MTVjYTUyMjQ2OGViOWU2NWYxMDcxZjE1ZDkwNTg4ODY3M2YxZjBhYWFlZTQyNDAxOGEyNjE1ZDcxW0FMTHxGT1JLSURdIDAyYjI1ZDhhMDUyMzg0MmQ5ZDVkNTNkNmIyM2JlNTBkMWYwY2FiMTI2ODQ1MGNjMDE0YjM1Y2QzMmQwZGFlYWUwOSIsImhleCI6IjQxMDk4NGZhYjY0ODYxMGFjNTk3ZDNhNDdkZjhjNDg0YTNmNjc1MWRkNjE1NTM3MTY3ODhmMzdkNjg4ZWU2ZWRkZTBlZWJlZjYxNWNhNTIyNDY4ZWI5ZTY1ZjEwNzFmMTVkOTA1ODg4NjczZjFmMGFhYWVlNDI0MDE4YTI2MTVkNzE0MTIxMDJiMjVkOGEwNTIzODQyZDlkNWQ1M2Q2YjIzYmU1MGQxZjBjYWIxMjY4NDUwY2MwMTRiMzVjZDMyZDBkYWVhZTA5In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJkNmQ1ODQ3Y2JjOTljNzA2NmZkMmVmODM3ZjU2N2RkZjY2MTQ4ZTA4YzQxMjVkYTU1NDliYThhYTdjYjdhZjYyIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjdmOTU5NjlkN2QzNmNlYzA0ZjRkZGQ5NTVkNTllNDQ5ZTkzY2I1MmRiMTY4ZDEyZjc4M2M2ZjVmY2VmYzc2NjNjYmJmNjYyNWJlMTBlMzMwYzAyOWUwOTA2NTg2NDM0NWU4ZWM1NzNiOTBmOWI4NjAwNmE1OGM0NTgzNzU0ZWI4W0FMTHxGT1JLSURdIDAzYTcxZDFkODdmYTMwYTI1N2Q3Y2RkYzE1Mjg3YjVkNWU2MjdjNGUwMjllZGI3YTdmMzI2ZDRkMWY2MzNiZmMxZiIsImhleCI6IjQxN2Y5NTk2OWQ3ZDM2Y2VjMDRmNGRkZDk1NWQ1OWU0NDllOTNjYjUyZGIxNjhkMTJmNzgzYzZmNWZjZWZjNzY2M2NiYmY2NjI1YmUxMGUzMzBjMDI5ZTA5MDY1ODY0MzQ1ZThlYzU3M2I5MGY5Yjg2MDA2YTU4YzQ1ODM3NTRlYjg0MTIxMDNhNzFkMWQ4N2ZhMzBhMjU3ZDdjZGRjMTUyODdiNWQ1ZTYyN2M0ZTAyOWVkYjdhN2YzMjZkNGQxZjYzM2JmYzFmIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJlMTIwNTc4MzJiZDU5MzAwYjZiMTVmMWUyMDRmYTU5M2FiZWM2ZWVkNmFhNTBiMzhlMzkzYmZmMGQwOGZiYzMyIiwidm91dCI6MTksInNjcmlwdFNpZyI6eyJhc20iOiI2MzllZjFmODNiMTUzNTg1YzIyMTVkYTMzZGFkZDY3MzljZWU4MWQ2ZjFjNWYyNzJhOTMxYWY3NmYxYTQ0NGI0ODM2ZWMyYzBmMzc5NGFlMzA1MmE5MDQyNDBmMjMzZWQ1N2IxN2E5ZjRkOTQ1Y2NlYzkzNDU1OWEwMjhlYWEzZFtBTEx8Rk9SS0lEXSAwMjJmNTA3YTU0ZmU3NTMxNjcxZGNhMGM4ODNhMTJlMDdiNzkyMmQwMDVlM2M2NDA0ZTdhMTQzMjRkYmUzMDQ5ZjgiLCJoZXgiOiI0MTYzOWVmMWY4M2IxNTM1ODVjMjIxNWRhMzNkYWRkNjczOWNlZTgxZDZmMWM1ZjI3MmE5MzFhZjc2ZjFhNDQ0YjQ4MzZlYzJjMGYzNzk0YWUzMDUyYTkwNDI0MGYyMzNlZDU3YjE3YTlmNGQ5NDVjY2VjOTM0NTU5YTAyOGVhYTNkNDEyMTAyMmY1MDdhNTRmZTc1MzE2NzFkY2EwYzg4M2ExMmUwN2I3OTIyZDAwNWUzYzY0MDRlN2ExNDMyNGRiZTMwNDlmOCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZTEyMDU3ODMyYmQ1OTMwMGI2YjE1ZjFlMjA0ZmE1OTNhYmVjNmVlZDZhYTUwYjM4ZTM5M2JmZjBkMDhmYmMzMiIsInZvdXQiOjQxLCJzY3JpcHRTaWciOnsiYXNtIjoiMjUwYjM5MTJjZmJiMDM4ODgzNTBkOTMyZDliY2NmNjYxY2ZmZDRkYTgxNWRlYmY0MzMyNzA4OTBhZTMzMTY2YmZlODllYzFmNTc5OGM0ZmVmZWMyYTRlN2U1MmQ2ODAwZmRjNGUwNDI3ZDA0Y2RhOGNlMDVhNGJiNzQ5NWEyOGNbQUxMfEZPUktJRF0gMDNhYmZjMDExMmYzZTAyN2Q5MjkwYmZjNmZiZmU5ZjFiMTcyZDI1MTFhMWRjZTU2NjA5OWRlMWI3ZmEzNDkzNjg2IiwiaGV4IjoiNDEyNTBiMzkxMmNmYmIwMzg4ODM1MGQ5MzJkOWJjY2Y2NjFjZmZkNGRhODE1ZGViZjQzMzI3MDg5MGFlMzMxNjZiZmU4OWVjMWY1Nzk4YzRmZWZlYzJhNGU3ZTUyZDY4MDBmZGM0ZTA0MjdkMDRjZGE4Y2UwNWE0YmI3NDk1YTI4YzQxMjEwM2FiZmMwMTEyZjNlMDI3ZDkyOTBiZmM2ZmJmZTlmMWIxNzJkMjUxMWExZGNlNTY2MDk5ZGUxYjdmYTM0OTM2ODYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImUxYzI0MzhmMjViNjUyY2Y1YTNlNDNjY2I4YWM3MzU0OTRhOTJiN2I4MDc4ZTMwOTE4NDA1YTk5N2RiMTM1YWIiLCJ2b3V0Ijo5LCJzY3JpcHRTaWciOnsiYXNtIjoiMGU0MGM1MzdjZGNmZTA5Y2FkMWVmYjk3ZTFhY2E3Mzc4NjU4YTc5M2FiNjgwMWRhOGQxYTgyOWM5MjhhOWQ5ODIwNDYwOTdkMjk2NzI4OWQ1MGUwYjAyOTMxMzhjZWYzMmY2MzIwZGVmY2IyNjcwNzRmODBmYzNjYjg2NTE0MzJbQUxMfEZPUktJRF0gMDMxMmI4YThjZTQ3NmVkMmEyZjQwZjVjN2RhMDI4OGE5N2M1ZmUxMDgxNTMyZjIyYWJlYzI1NzRhMzE0YTcwZjc2IiwiaGV4IjoiNDEwZTQwYzUzN2NkY2ZlMDljYWQxZWZiOTdlMWFjYTczNzg2NThhNzkzYWI2ODAxZGE4ZDFhODI5YzkyOGE5ZDk4MjA0NjA5N2QyOTY3Mjg5ZDUwZTBiMDI5MzEzOGNlZjMyZjYzMjBkZWZjYjI2NzA3NGY4MGZjM2NiODY1MTQzMjQxMjEwMzEyYjhhOGNlNDc2ZWQyYTJmNDBmNWM3ZGEwMjg4YTk3YzVmZTEwODE1MzJmMjJhYmVjMjU3NGEzMTRhNzBmNzYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImY4MTQ1OWM4OWE2ODE0NWRmYTk5MDU3NDllYTI0NWFiYTI5ODRmNzlmYzVjNTNjOTVhOWRiOTJlMTI0NGU2NDMiLCJ2b3V0IjoxMywic2NyaXB0U2lnIjp7ImFzbSI6IjMzMWE4ZTZjZTYzOTUwYzE1Yjg1NzVhMzE5NTFhNTIwNTI0YTkwNDI2MTY5ZGJmODY5OTY4OGE0NzM2ZWQzZDMwMjA0Y2I4ZGU1ZTBkZmY4YWQyNWE1NWJmYjI5OTMwMGZiZTk0M2VjMDk4ZTFkZWY5YjkyZmIzNDNjNGYzZmE4W0FMTHxGT1JLSURdIDAzYjExNzg4ZWNiZWI4NDczMGY3NTNlODcwNGU1YmU5NGE0YjBlNjMxZmJmNTc0ZGI2YjE3NDY5MWM3Y2ZjMzhhYiIsImhleCI6IjQxMzMxYThlNmNlNjM5NTBjMTViODU3NWEzMTk1MWE1MjA1MjRhOTA0MjYxNjlkYmY4Njk5Njg4YTQ3MzZlZDNkMzAyMDRjYjhkZTVlMGRmZjhhZDI1YTU1YmZiMjk5MzAwZmJlOTQzZWMwOThlMWRlZjliOTJmYjM0M2M0ZjNmYTg0MTIxMDNiMTE3ODhlY2JlYjg0NzMwZjc1M2U4NzA0ZTViZTk0YTRiMGU2MzFmYmY1NzRkYjZiMTc0NjkxYzdjZmMzOGFiIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJmOTdjODhjODVmMjMzMTBjOGY2MTRlOGI1YTA2NTQ5MzZiNzhmODk4YTg0NjRiYmYwMDQ5NWZlOWU4Y2NlM2NjIiwidm91dCI6MTEsInNjcmlwdFNpZyI6eyJhc20iOiIzZjhjZmNjYWJjYmQ3MTY0YTM0YjZjMDI0NTk1MWRlZTg3ZThjMzhkZDhiNzkyYzk0OTQyNmFmMWI5MDI1ODJhZTU3ODQzMjJmNzQ4NjkyZWJiMmQ1NzUzNWM3ZjAyMGRiZTlhZDI1ODgwMDc1M2Y5NDViY2RkZjVkZWYwZjg2M1tBTEx8Rk9SS0lEXSAwM2JlNjE5MWU4ZDlmMzNjYjc2ZmI3NWZhYzNmYjU4MTRhMGMyNzU5MWZlOGMxMjE3Y2VlOTdiMDA0YWE4ZTJkYTgiLCJoZXgiOiI0MTNmOGNmY2NhYmNiZDcxNjRhMzRiNmMwMjQ1OTUxZGVlODdlOGMzOGRkOGI3OTJjOTQ5NDI2YWYxYjkwMjU4MmFlNTc4NDMyMmY3NDg2OTJlYmIyZDU3NTM1YzdmMDIwZGJlOWFkMjU4ODAwNzUzZjk0NWJjZGRmNWRlZjBmODYzNDEyMTAzYmU2MTkxZThkOWYzM2NiNzZmYjc1ZmFjM2ZiNTgxNGEwYzI3NTkxZmU4YzEyMTdjZWU5N2IwMDRhYThlMmRhOCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfUkVUVVJOIDU5MjAwNzAgYWIwYzg4NGVmMmY2NWMxMjI0NzVmZGNlYmZhYzkxNjYxOTgzODhkYjBhNGQwMTEwNWVhMjM3YThiZmM4ZWI5ZCIsImhleCI6IjZhMDQ0NjU1NWEwMDIwYWIwYzg4NGVmMmY2NWMxMjI0NzVmZGNlYmZhYzkxNjYxOTgzODhkYjBhNGQwMTEwNWVhMjM3YThiZmM4ZWI5ZCIsInR5cGUiOiJudWxsZGF0YSJ9fSx7InZhbHVlIjowLjAwOTU2NzcyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYmZjODZkYWViYmIzMDk2ODc3YTdkNjY0OTNhZTM3Y2RhMWZmZjkwMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGJmYzg2ZGFlYmJiMzA5Njg3N2E3ZDY2NDkzYWUzN2NkYTFmZmY5MDE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpsdXNtZHdod2VzajZyaDVsdHhmeWF3eGx4NnJsbGVxeTNkZHAzMjc2Il19fSx7InZhbHVlIjowLjAxMjIzNjgxLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2JhNzUzNzY1M2Y5YTAzNjI3NzI0M2Q1NThiMWRhM2Y2MGZjMDFiYSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdiYTc1Mzc2NTNmOWEwMzYyNzcyNDNkNTU4YjFkYTNmNjBmYzAxYmE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBhNnc1bWsyMHU2cWQzOHdmcGEyazkzbWdsa3BscXBoZ2d0MzRqYzRnIl19fSx7InZhbHVlIjowLjAxMzYyNTAxLCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTA1ZTEwMzVmNmUxZTdkMGU2MjE2ZTk0OGY4NDBhNDc2Y2RjZmEyZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDEwNWUxMDM1ZjZlMWU3ZDBlNjIxNmU5NDhmODQwYTQ3NmNkY2ZhMmY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFnOXV5cDQ3bXM3MDU4eHk5aGZmcnV5cGZya2VoODY5dTAzM2ZrNXR5Il19fSx7InZhbHVlIjowLjAxNjI2MzQ2LCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYzRlMmUyZGQ3NDg4YTdiOGZjNzgyYzc3NGQ1MzBkMWJkZTkwNDk3YyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGM0ZTJlMmRkNzQ4OGE3YjhmYzc4MmM3NzRkNTMwZDFiZGU5MDQ5N2M4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ6dzlja2F3ankyMHc4dTBxazh3bjJucDVkYWF5emYwc2xzajNrajVsIl19fSx7InZhbHVlIjowLjAxNzI5NzU5LCJuIjo1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDA5NDZkMjJhNzY3YmVjMWVmODAzODcxYmQ2ZjRhODNjY2E2ZWYwMyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDAwOTQ2ZDIyYTc2N2JlYzFlZjgwMzg3MWJkNmY0YTgzY2NhNmVmMDM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFxZmdtZno1YW5tYXMwMHNxdThyMHQwZjJwdWVmaDBxdjlmeXdtdHR3Il19fSx7InZhbHVlIjowLjAxNzcwMjExLCJuIjo2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYzFiZDYxOTkxZWZmNWIzY2YxZjY0Njc3Zjg2NTJkYzQ4MWY2YjM5NiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGMxYmQ2MTk5MWVmZjViM2NmMWY2NDY3N2Y4NjUyZGM0ODFmNmIzOTY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJxbTZjdmVybWw0azA4MzdlcjgwN3I5OWh6Z3JhNG5qY2N1bW16ZnB2Il19fSx7InZhbHVlIjowLjAyMjEzOTg3LCJuIjo3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNmEzNTMyZmFmZTYzNWU0ZDc4ZGM5NTQ2M2ZlOGRiNzEyZTJiNDFkNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDZhMzUzMmZhZmU2MzVlNGQ3OGRjOTU0NjNmZThkYjcxMmUyYjQxZDc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA0cjJ2aDZsZTM0dW50Y21qMjV2MGxnbWRjanUyNnA2dWRodzk1NDB2Il19fSx7InZhbHVlIjowLjAzMTM5NTg1LCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZmQxMTUxYWY3MDUzYWU3YThmYzY1OTUyY2EwZmRhZmI0MjVhY2ZlYiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGZkMTE1MWFmNzA1M2FlN2E4ZmM2NTk1MmNhMGZkYWZiNDI1YWNmZWI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI3M3o1ZDB3cGY2dTc1MGNldjQ5anMwbXRhNXlrazBhdjNxZWZyaGdnIl19fSx7InZhbHVlIjowLjA1ODczMTc0LCJuIjo5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2VlZmJiOTFiNGIwMDgyNmY0MDFkOTM4ZGFlNWNlZDRmMGYwNTQ3OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdlZWZiYjkxYjRiMDA4MjZmNDAxZDkzOGRhZTVjZWQ0ZjBmMDU0Nzk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBsd2x3dTNramNxc2ZoNXE4dm4za2g5ZW0yMHB1ejUweTh3NzU1eTBhIl19fSx7InZhbHVlIjowLjA2MDIxNjkxLCJuIjoxMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGEyMzBhZjdlNDJmNDgzZGU3ZTdmNTgxNzM2ZWYyNTQ1YjkyM2NkMzYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhMjMwYWY3ZTQyZjQ4M2RlN2U3ZjU4MTczNmVmMjU0NWI5MjNjZDM2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6M3JwdG03Z3Q2ZzhobjcwYXZwd2RoMHk0em1qZzdkeGNzbGRwZmNmcyJdfX0seyJ2YWx1ZSI6MC4wNzI3ODg0OSwibiI6MTEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBiZDk0MDYyN2M3MzFlMzQ1OGM5NDQ4OTgwYzZkYmE0MDRkNjhhYjliIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YmQ5NDA2MjdjNzMxZTM0NThjOTQ0ODk4MGM2ZGJhNDA0ZDY4YWI5Yjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxejdlZ3AzOGN1Yzd4M3Z2ajN5ZnNycmRoZnF5NjY5dG52bjdoNTBjMnIiXX19LHsidmFsdWUiOjAuMDc0NTg4NjUsIm4iOjEyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODBjNzg2NGJhNDFmOWRlYTRlNTM4NDU1MmJlZWRmYjY2ZTg5ZmYwNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDgwYzc4NjRiYTQxZjlkZWE0ZTUzODQ1NTJiZWVkZmI2NmU4OWZmMDY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpxdjBwanQ1czBlbTZqdzJ3ejkyMmx3bTdteGF6MGxxYzlqN3F0ZjJmIl19fSx7InZhbHVlIjowLjA5MTQzODcxLCJuIjoxMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGFjOGY3YWIzNmNkZjZlMTFkM2FmZDE2ZGYwOWM0ZGVhZTQwMmZlOWQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhYzhmN2FiMzZjZGY2ZTExZDNhZmQxNmRmMDljNGRlYWU0MDJmZTlkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6a2c3NzRuZG4wa3V5d240bGdrbXV5dWZoNHdncWg3bjU4bmdrNTRwcyJdfX0seyJ2YWx1ZSI6MC4xMTQzMDk2NiwibiI6MTQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA4ZDg0ZGJiYmZkMzhmMzViYzcyNDhhOWQ0YjY3YTMxMTI0YzFiZTU0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OGQ4NGRiYmJmZDM4ZjM1YmM3MjQ4YTlkNGI2N2EzMTEyNGMxYmU1NDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenhjZmthbWw1dTB4azc4eWo5ZjZqbTg1dmdqZnNkNzJzNHVxcGhtOTAiXX19LHsidmFsdWUiOjAuMTMyNDU3OTYsIm4iOjE1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODQ2YmZlNDJkNjgzMTEwNDI3ZGIzNDI4NGFmNDE3YzE1ZDU0NWQwNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg0NmJmZTQyZDY4MzExMDQyN2RiMzQyODRhZjQxN2MxNWQ1NDVkMDQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp6eGhsano2NnAzenBwOG12Nnpzamg1emxxNDY0emFxc3ZlbWRxZTBwIl19fSx7InZhbHVlIjowLjEzMjQ5ODYxLCJuIjoxNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGFkZDBhNmRjYzQ2NGQ1MzhjZGU5OGY2MTVjZDBhMDMyMTdlZmJkNmQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhZGQwYTZkY2M0NjRkNTM4Y2RlOThmNjE1Y2QwYTAzMjE3ZWZiZDZkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6a2FwZmt1YzNqZDJ3eGRheDhremh4czVxZXAwbWFhZDVqYXZtNWQ0eiJdfX0seyJ2YWx1ZSI6MC4xNTkyNzU4MywibiI6MTcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAzNTFmNjkyYzVmODZlYzYyZDRiZTEyZjQxOWMzYzliMWNmZTc4MjY5IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MzUxZjY5MmM1Zjg2ZWM2MmQ0YmUxMmY0MTljM2M5YjFjZmU3ODI2OTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcTYzNzZmdnQ3cndjY2s1aGNmMGd4d3JleGN1bGV1emR5M3o4azQ4YWEiXX19LHsidmFsdWUiOjAuMTYxMjQ5NTIsIm4iOjE4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZjViY2JhMTU5YWFkZjgwNmI5MWQzYzJlMTNmZTkxMzRhZTJlYWJjNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGY1YmNiYTE1OWFhZGY4MDZiOTFkM2MyZTEzZmU5MTM0YWUyZWFiYzU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI2bWV3czRuMmtsc3A0ZXI1N3p1eWw3ank2MnV0NHRjNXN5dXBxdW5sIl19fSx7InZhbHVlIjowLjE4MjQ5MDM2LCJuIjoxOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDgxYWJlYWM4Y2Q1NjVhYTc3OWYxZDVkYWQ0ZjYyNDJiMGMzZTdlNjkgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4MWFiZWFjOGNkNTY1YWE3NzlmMWQ1ZGFkNGY2MjQyYjBjM2U3ZTY5ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6cTZoNmtnZTR0OTRmbWU3ODJhNDQ4a3lzNHNjMG43ZHlsdjk5cjZnciJdfX0seyJ2YWx1ZSI6MC4yNDM4MDU3OSwibiI6MjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBmMDU5M2Y2NDY1YTNhZTc5Yjg2YjEwZGM5NjMwZWNhMzI3NGYyYjZjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZjA1OTNmNjQ2NWEzYWU3OWI4NmIxMGRjOTYzMGVjYTMyNzRmMmI2Yzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcmM5ajBteXZrMzZ1N2RjZHZnZGU5M3NhajNqd25ldGRzd3d4cHgycmwiXX19LHsidmFsdWUiOjAuMjUyMTAwMjksIm4iOjIxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDJiM2I2ZGQ2MjAyYzgxNDhiNGUwM2UxYWU4ZWZhYWExOGFmMGU0ZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQyYjNiNmRkNjIwMmM4MTQ4YjRlMDNlMWFlOGVmYWFhMThhZjBlNGU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJmdDhka2F2Z3B2czl5dGZjcDdydDV3bDI0cDN0Y3dmYzg3cTlqbm44Il19fSx7InZhbHVlIjowLjI2MTYyNzA3LCJuIjoyMiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGFmZGFjZTgwNGVmZTZlZjljMjY1YzZiYzg1YTcyMmZmODZmNTdjM2QgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhZmRhY2U4MDRlZmU2ZWY5YzI2NWM2YmM4NWE3MjJmZjg2ZjU3YzNkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6aGE0bjVxZm1seGE3d3p2aHJ0ZXBkOHl0bGNkYXR1ODV3bWhta2w5aiJdfX0seyJ2YWx1ZSI6MC4yNjUxOTg5NCwibiI6MjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3ZTNmNDAzZDVmNzYzN2JlYTU3NzYyZjA2YTAyNDVkYTMzZmNhYjg3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0N2UzZjQwM2Q1Zjc2MzdiZWE1Nzc2MmYwNmEwMjQ1ZGEzM2ZjYWI4Nzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGxyN3NwYXRhbXIwMDQ5d2EzMHE2c3pnaGRyOGw5dHN1NTA0Y244MnQiXX19LHsidmFsdWUiOjAuMjgwNTg1NDYsIm4iOjI0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYmZkMmM3ZTg3ZmNmYmI3ZjBjN2M3N2IxZmM0ODI2YmUzMGM3ZTdlNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGJmZDJjN2U4N2ZjZmJiN2YwYzdjNzdiMWZjNDgyNmJlMzBjN2U3ZTQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpsYTkzbGcwbDhta2xjdjAzbW1ybHpneTZscnAzbDh1czg0Zzd2ZjNzIl19fSx7InZhbHVlIjowLjI5MDY4MDcxLCJuIjoyNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGZlOThhM2YzZDdkMWI4ODZhMWQzMGFmZmI0ZjU4NGE0MjMwOTcwMDIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmZTk4YTNmM2Q3ZDFiODg2YTFkMzBhZmZiNGY1ODRhNDIzMDk3MDAyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFybGYzZ2xuNmxnbTNwNHA2djkwbGQ4NHNqanp4enRzcWc4M3Bxc3RwaCJdfX0seyJ2YWx1ZSI6MC4yOTE0MzUxLCJuIjoyNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGYwMDc1NjVmM2I2OWE3NzUzNzdiYTU1ZTQ0YzZkNGQ3ZGIxYWU0ZjcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmMDA3NTY1ZjNiNjlhNzc1Mzc3YmE1NWU0NGM2ZDRkN2RiMWFlNGY3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyY3F3NGpsOGQ1NndhZmgwd2o0dTN4eDZudGFreGh5N3U0bGE5cGQ5YyJdfX0seyJ2YWx1ZSI6MC4zNTU5Mjc1NCwibiI6MjcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA1ODNkMGFkZWQ1ZDQ5OGVkYzhlYjNhNzQ0MjhiNzBkNmFmNTI0OTVlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NTgzZDBhZGVkNWQ0OThlZGM4ZWIzYTc0NDI4YjcwZDZhZjUyNDk1ZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcHZyNnprNzZoMmYzbXdnYXZhOGdzNXR3cnQyNzVqZnRjang4ZmR1dzMiXX19LHsidmFsdWUiOjAuMzc4MzMxOCwibiI6MjgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBkMTAyMjYzNTU3MGRhNTZjYWIzMzFhNzFhYTYwYzU5NjQ4NzZkMDg0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZDEwMjI2MzU1NzBkYTU2Y2FiMzMxYTcxYWE2MGM1OTY0ODc2ZDA4NDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcmdzeWYzNDJ1eDYybTl0eHZkOHIybnFja3R5c2Frc3NzODIwbGdnZzQiXX19LHsidmFsdWUiOjAuMzc4ODc1NywibiI6MjksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5Mjg2ZGE2NjVjMzZkMWE4ZTk3MzQ0ZTI2YTE0ODVjNTY5ZTE3Y2QyIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OTI4NmRhNjY1YzM2ZDFhOGU5NzM0NGUyNmExNDg1YzU2OWUxN2NkMjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemZnZGtueHRzbWRyMjhmd2R6d3k2czVzaHprbmN0dTZneHFmZTJmOXUiXX19LHsidmFsdWUiOjAuMzgyNDMxNjUsIm4iOjMwLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZjNlMjBjNzhlNGYwYTZjZjEyYzlkNzRiZTIxN2RmM2Y4MTIwYmNlMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGYzZTIwYzc4ZTRmMGE2Y2YxMmM5ZDc0YmUyMTdkZjNmODEyMGJjZTI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJlN3lycmN1bmMyZG5jamU4dDVoY3NobXVsY3pnOXV1Zzhodjl2enNmIl19fSx7InZhbHVlIjowLjM4OTk3MzQ0LCJuIjozMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGVlMGU4ZWU2Zjc3Y2JiNDc3ZTM1ODI2NzM5Yzg0NmNkNTY1ZDY1YmEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRlZTBlOGVlNmY3N2NiYjQ3N2UzNTgyNjczOWM4NDZjZDU2NWQ2NWJhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyaHFhcmh4N2E3dGszbTd4a3B4d3d3Z2dteDR2aHQ5aGdoZ2h5N2Z5YSJdfX0seyJ2YWx1ZSI6MC40MDY3MzI4MSwibiI6MzIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBiZGQ4MGRiYTZlMThhNjk2ZDM4NGY2OTU1MTM5Y2RhZDhjZDFiNjVlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YmRkODBkYmE2ZTE4YTY5NmQzODRmNjk1NTEzOWNkYWQ4Y2QxYjY1ZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxejdhc3JkNmRjdjJkOWtuc25tZjI1ZmVla2tjZTVka3RjcnA2bnQ0ZTUiXX19LHsidmFsdWUiOjAuNDI1NTM2NTEsIm4iOjMzLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjZiNGQ5Y2U1NjBiOTIzZmY2ZjE2ODk0MmU2NGE3OGNjOGUwMWQ4NCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGI2YjRkOWNlNTYwYjkyM2ZmNmYxNjg5NDJlNjRhNzhjYzhlMDFkODQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXptdGZrd3cyYzlleTBsazc5NWZndG55NTd4djNjcWFzc2RzeGdlZDc0Il19fSx7InZhbHVlIjowLjQyNzIxNTc5LCJuIjozNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDVlN2Q5YzRhNTlkMjM3NDE0OGU2ZTI3NTc0MGY1N2QzMjI4YmEzZTUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1ZTdkOWM0YTU5ZDIzNzQxNDhlNmUyNzU3NDBmNTdkMzIyOGJhM2U1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwMDhtOHoydDhmcndzMmd1bTM4MmFxMDJsZmo5emFydTVoN3Z3cWZkMiJdfX0seyJ2YWx1ZSI6MC40NTc4MTg5OSwibiI6MzUsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2YjcyNjBiNGMzMmJmYjAzYjQ5NzBhZmU0ZjNhOTNiYjM0MWVhNTVjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NmI3MjYwYjRjMzJiZmIwM2I0OTcwYWZlNGYzYTkzYmIzNDFlYTU1Yzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDRoeWM5NWN2NGxrcWE1anU5MHVuZTZqd2FuZzg0OXRzYXlheTh3d2siXX19LHsidmFsdWUiOjAuNDcyMTk1NzcsIm4iOjM2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDlmYzU1YmIwMjI3OGUwODRlYjY4NTMyNmI4NGEwOTc5OTJjYTY0OCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQ5ZmM1NWJiMDIyNzhlMDg0ZWI2ODUzMjZiODRhMDk3OTkyY2E2NDg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ2bGM0ZG1xZ25jdXp6d2s2em55NnV5NXp0ZWp0OXhmcXhlOWNhbGZqIl19fSx7InZhbHVlIjowLjQ4NDYzODQzLCJuIjozNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDhlYTMwZWM1NTIwN2UwZmExOTBkNDZmMWJmZGJiZTc2ZWM4ZDM3ZTYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4ZWEzMGVjNTUyMDdlMGZhMTkwZDQ2ZjFiZmRiYmU3NmVjOGQzN2U2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ODJ4cms5MmdyN3A3c2VwNHIwcjA3bWhlbXdlcmZodWM3dTV3bWdqZiJdfX0seyJ2YWx1ZSI6MC40ODg0MTQ0NywibiI6MzgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAyOGQ5ZTE4MDQ5NWE4ZTc1MzBkMDc3Y2RkZDVmNTZhM2M1NGY1MjdmIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MjhkOWUxODA0OTVhOGU3NTMwZDA3N2NkZGQ1ZjU2YTNjNTRmNTI3Zjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcTVkbmN2cWY5ZGd1YWZzNnBtdW1oMmwyNjN1Mm42ajB1N212MDhnNzMiXX19LHsidmFsdWUiOjAuNTE4NTUzNywibiI6MzksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAzM2Q1MjkzMzkwZWEwYzY5OTY3N2Q3YWMxNTY2NWIwZGY3NDNmN2JiIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MzNkNTI5MzM5MGVhMGM2OTk2NzdkN2FjMTU2NjViMGRmNzQzZjdiYjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWVhMjJmbmpyNHFjNnZrd2x0NmM5dHh0dnhsd3NsaGh2bDBkbDM2dzgiXX19LHsidmFsdWUiOjAuNTc0MDA4NDIsIm4iOjQwLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMzVjYjljOGI2NjMzYzMxMDg1YzE5ODc4ZDhmZTAwYjY2OTNjNTQ2MiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDM1Y2I5YzhiNjYzM2MzMTA4NWMxOTg3OGQ4ZmUwMGI2NjkzYzU0NjI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXE2dWg4eXR2Y2V1eHl5OWN4djgzazg3cXpteGowejV2ZzBua2FjcnltIl19fSx7InZhbHVlIjowLjU4NDY0MTEsIm4iOjQxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYWQ4M2Y5M2NlNGJlYjA3ODhiZWE3MDQ3MmNkMDAzZDRiMTc1ZmFhMyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGFkODNmOTNjZTRiZWIwNzg4YmVhNzA0NzJjZDAwM2Q0YjE3NWZhYTM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXprYzg3ZnV1amx0cTd5dGFmY3l3dHhzcTAydHphMDY1dndsbXl2NHJ4Il19fSx7InZhbHVlIjowLjY0MzI5ODQxLCJuIjo0Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGEzZTQ5MjNmYjk5ZmMyZWFhNWRiZjRjODlkMTVmYmQ5ZjZlM2NhNDUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhM2U0OTIzZmI5OWZjMmVhYTVkYmY0Yzg5ZDE1ZmJkOWY2ZTNjYTQ1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6MzdmeTNsaHgwdTk2NDltMDZ2MzhnNGwwdmxkYzcyZzVoNzM4YXMzeiJdfX0seyJ2YWx1ZSI6MC42NDQ1OTE2OSwibiI6NDMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA0NDUzZmNiYTJkNTk2YjZlNDA0YTM1YjIwMDBlZjcxYTcxZTU0YTFmIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NDQ1M2ZjYmEyZDU5NmI2ZTQwNGEzNWIyMDAwZWY3MWE3MWU1NGExZjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcHo5OGw5Njk0dmtrbWpxZmc2bXlxcXc3dWQ4cmUyMnJ1Nm1zZTM2cnQiXX19LHsidmFsdWUiOjAuNjU3NTM2MjgsIm4iOjQ0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTY2NDAyN2NkZDZlZDBkYjc3NzBjOGYzZjYwOTU0OTMwNDg0YTM2NiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGU2NjQwMjdjZGQ2ZWQwZGI3NzcwYzhmM2Y2MDk1NDkzMDQ4NGEzNjY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJueGdxbnVtNGhkcGttaHdyeTA4YXNmMmpmc2ZwOXJ2Y21sNjBocW00Il19fSx7InZhbHVlIjowLjc0NjE5NDQxLCJuIjo0NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDEzMjlmZWFmYWQ5M2QzY2JjYTlhN2FlNjVkNDFlNGI0MDUzODUzOGUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxMzI5ZmVhZmFkOTNkM2NiY2E5YTdhZTY1ZDQxZTRiNDA1Mzg1MzhlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZmpubDQwNGtmYThqNzJuZmF3dmgycHVqNnEyd3puM2NqNHlneXB5MyJdfX0seyJ2YWx1ZSI6MC44MjYwNDM3MiwibiI6NDYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBmOTdlNDFlYTgwNDZhNjBmMDlkMTMxMjU4N2EwOGM5ZjMyZDU1ZjIyIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0Zjk3ZTQxZWE4MDQ2YTYwZjA5ZDEzMTI1ODdhMDhjOWYzMmQ1NWYyMjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcnVodXMwMnNwcjJ2cmNmNnljanRwYXEzajBuOTQybHlnYWRhN3lqdWYiXX19LHsidmFsdWUiOjAuOTI4MzIyMTIsIm4iOjQ3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTdkODZkOGExNGVlZDliY2EwMjVhMzFmNmQ1MzYyZmQ4YjZmM2UyNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDE3ZDg2ZDhhMTRlZWQ5YmNhMDI1YTMxZjZkNTM2MmZkOGI2ZjNlMjU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF0YXNtdjJ6bmhkbjA5cXlrMzM3bTJudnQ3Y2ttZTd5NXpmaHN6OGE3Il19fSx7InZhbHVlIjowLjk5MDc2NzU4LCJuIjo0OCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDllOGU2YjdkMmNjNzdkMTc4NDY2NzZhMmYzYTE0ZTJjMjVmOTJjZjYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5ZThlNmI3ZDJjYzc3ZDE3ODQ2Njc2YTJmM2ExNGUyYzI1ZjkyY2Y2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6MGd1Nm1hOW5yaDY5dXl2ZW0yOXVhcGZja3p0N2Z2N2N5aHowY2t0eiJdfX0seyJ2YWx1ZSI6MS4wOTk4Mzg1OCwibiI6NDksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5YWU4NGIxNTZjNmU4OTJlN2VmNGNiMTZiNjM0ZDg2ODlmZWQ2YjRlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OWFlODRiMTU2YzZlODkyZTdlZjRjYjE2YjYzNGQ4Njg5ZmVkNmI0ZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemR3c2pjNGQzaGdqdG43N245M2RkMzVtcDVmbG10dGZjcWgyeTgwMjkiXX19LHsidmFsdWUiOjEuMjg3Njc0NjMsIm4iOjUwLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTMxNjZjZmExZTA5YjczZTBlOGEzOTRjNzMyNzMwMTdmOGQ5NTgzMyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDEzMTY2Y2ZhMWUwOWI3M2UwZThhMzk0YzczMjczMDE3ZjhkOTU4MzM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFmM3ZtODZyY3ltdzBzdzNndTVjdWU4eHF0bDNrMmN4dnBtdXRldWg5Il19fSx7InZhbHVlIjoxLjQ2NDYxMjg2LCJuIjo1MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGZiMmYwNGE4YjA1NGM3MmNkZWY5ZjAxMzU1YWQxMzI2YmU1YzY2YzYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmYjJmMDRhOGIwNTRjNzJjZGVmOWYwMTM1NWFkMTMyNmJlNWM2NmM2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyYWo3cDlna3Aydnd0eDdsOGNweDRkZHp2bnR1aHJ4Y2NlbWR3MDMzZyJdfX0seyJ2YWx1ZSI6MS43NjQyOTk2OSwibiI6NTIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3MzI2OTRhYjE3NmQxZDA4ZmExMGVhYWRiZDUzZjEzNGJkYmQ0OWNmIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NzMyNjk0YWIxNzZkMWQwOGZhMTBlYWFkYmQ1M2YxMzRiZGJkNDljZjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGVqZDk5dHphazM2ejg2enI0Mm0wMm43eTZ0bTAyZmV1NG1jZ2RlM24iXX19LHsidmFsdWUiOjIuNjY1NDQxNjQsIm4iOjUzLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDg5OGNjZmY0YjQ1ZjVlY2IyNTA4Y2FjZjNkYzQwOTBjYzVjMTViNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQ4OThjY2ZmNGI0NWY1ZWNiMjUwOGNhY2YzZGM0MDkwY2M1YzE1YjY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ2ZjNuOGxmZHpsdG05ajJ6eDJldTd1Z3pndmNocTRrYzdneWt1ZHFzIl19fV0sImZlZSI6OC41ODRlLTA1LCJoZXgiOiIwMTAwMDAwMDJmYzU4NzA5YjYyNDZiNmRlNDI5Mjk1ZTE5NDczMDc2MGI0Njk4YTlhNWY3OTA0Y2MyZjk4MmVhOTdkM2Y0MTcwMzBiMDAwMDAwNjQ0MTEwZTk0ZmIzODI2NzQ5NzIwN2I2ZWVkOTY1ZTkzNzdiNjVjOWI4NzRmYWU1MWY0MWNmODYyNmMwZGNiODFlYzkxOTRkNzdkYTNkYThmMGE1YmFmMjU5N2Q0YWE3OTE2ZjFkOTMxZDczMTBmMzk3NjJkZTczZDBjYWQ1NmRjZGFmNDEyMTAyYjNmZTY1YjU3NjNhYzM5ZDRjN2E2NWQwMzMwNzQwNjAzZjE1MTkxMTdmZTM4ZjJkMGE1NDA2NGIwNDI1ZTdjNGZmZmZmZmZmMzVhYmE5OWQ2MmU1YmEzNzRiNTNmZmViZGY2ZmFhMmQ1OTE3OGM4YjBiNzcxNDUyNDUwMjdkODY3Nzc0MjEwMzE1MDAwMDAwNjQ0MTljOGNjNDA5YTRjOGQwMjZhMTc4MDRhZWVlMjFiMGZhNTlhMzBiYjgyY2ZmZThiZTY4NGNiODFhZTZmNjJjM2IxYzE0YTBjZjA4M2EyNjllNjIwODZlMjk2OWY3MGIyYjQzN2JiYzE1NmM5Y2M4MDMxYjQxNDUxZThmM2Q5N2ZiNDEyMTAyNjc2YmNjMzA0YjdjZDk2ODc3Y2E4NzE5ZDhjOWU2ZmQyZmE3MDlkYzc5YmU3M2Y0OTJmOWFjMzExNWEzZDAxZWZmZmZmZmZmZDM0MWYzZTJkOThiMzI2ZjU3MGZmYmU1OGI2ZDRhOTBlYWVjZjkxYWE0Y2NhYmJhMGYyNDI1Yzc5YTM0N2UwODA0MDAwMDAwNjQ0MWM3MDU1ZDMwYzZhOWFiZjQ1Y2YzOTNmMzY1NWZiZDIwN2I5NmM3ZTVjOTczYWU3ZDU3YjcyM2E0ODZkMzRjN2QyOWZmMDBiMWY1NWJiZTNlYmE0MzQ0MjFmZWE3ZGQ1YTAzMjA2MDZjZjMwYTE4ZjZkMmY2MGIxYTIwMGJhM2IwNDEyMTAyNjJkMDQwNjcwODVlYmRhMDEzYzA3OWYwNDBhNzM1ZGU0ZDQwNTJiNjZmYTFlYjZhYzA3ZjYxODdjMDRmNTQ3NWZmZmZmZmZmYjk3OWVkNjk3OTU5YTU1YTk1ZWE1MTQyMzUxZDI3M2I3MDU4N2RhZjNmYWI0N2ZjNTQ2ZWE4MmJkYTFjZjIxMDEyMDAwMDAwNjQ0MTgxM2VkNWE3ZjJlNTZhZDg0NjVjMGM3YzA2NzUwNjVlYzYzOTk5ODUxODQ2NTY3YjJjM2UxZWRmM2M1NDgzMmIxYTZmNjQ3MzczYTI5MTYzMjFlMDNhN2NjZGZiMmM1Y2ZlNDQwMzg0ZWQ3YmVhOWQ5NTUxMjNkNmY0YWZjNzk5NDEyMTAyMDI2ZTI2ZjFkYmFiOWEzNzIyZWM0OGUxOWMwMjdjYmM0ZDY5M2EzMzFkNWIwM2FkMzg0NjRiNmFhMjEzMDhlMWZmZmZmZmZmMzU4NTIyNWNhNmEzMDhlOTRlOTViYzAwY2FjMmU0MDZlYjVhMmE4NTljMTRmNjY4M2Q3OGU0YWQ3MDg0ODAyZDJjMDAwMDAwNjQ0MWMyOGZiYTZhYmViNzAxNmVlMjRmZmJjM2ZkZmYyMmFjYzU2MzE5YzQ1MGE2OGJkNWMyZjAzMjYxYzcyZjZiYTMyNjIyMGJmN2IwOGE1YjUxOWRjNjlkY2IzZGJiYWEwZmEzYWJhNTA3ZWZhNWZjYTVjYmRmMTE2ZGY2Y2UwMDcyNDEyMTAyMmM4YTM5ODJmNDVmZDcyMjc3NDA5MjZjOGNiZTY0NGViYjQ5ZDQyNjA5OGJlMjA3NWE0NDdjMmMxOTcwOWVmMGZmZmZmZmZmNjAyMTliNjQzOThjZDVkZDI2NmIzYTAwNDVmN2ZmMjg3NDI4N2ZiOTJhZWJkY2ZjOGY3NGY5NDFkMmVjNTAzMTIwMDAwMDAwNjQ0MWFhNTAyZWVjMWIzZThkYzBhNzE4MzExY2NmOGMyMWQ1YjNiZTE0MWQ1NGI2ZTk0OTZlNjg0MDBiZjFhODkxNzUxOTIyZmRmM2RkOTdkOTg5MDQxMDlhNmEyNTRmNmE0Y2FiN2UwY2Y5ZmMyZjA2Nzc1OGZlY2RjZTc2ZDM4OTU4NDEyMTAyNjk2NjA0MDlhMTdiODIxY2UzYjNiZDYyNGYxNDM4MDA0YmM5NDExNjU1NmM0YjYwYWYxMjVhOGUzZDliZTg0M2ZmZmZmZmZmNzU1ZTVmNzYxMjU1ZDIyMzdlMDQwNWMwZTgzMjk1YzRmNTQ0NTQ0ODFjZmZiZWRiYjUxNGJhZjgwODVlZmIzODA0MDAwMDAwNjQ0MWZjYjQ2NjVjNzY2NDhjMDA5MTUxZjU2NjkwMWJmZWY3MDEyYjVjYzEwYmFiNjVhZjA4MWQxYjUyNzA3ZTVlYTI1NTFlZjU5ZTdiZDA3YWJmNzA5NjkwMzZkMzZmZjhiMDQyNTk2NDliMTdkYzJkNmYyYzljZTkxZGQ5ZmZhZjU2NDEyMTAzOWY0OGJmMWQ1YzFlYTRmNTVlMjI0ODUxNzE1ZjE3YjdhYzBiYjcyODk2ODBjMGNkN2RmMDkwOTYwZGE5YTYzNmZmZmZmZmZmNzU1ZTVmNzYxMjU1ZDIyMzdlMDQwNWMwZTgzMjk1YzRmNTQ0NTQ0ODFjZmZiZWRiYjUxNGJhZjgwODVlZmIzODBjMDAwMDAwNjQ0MTBkNDBiNWFkOWJmMDQ2YzBiNjZlNTkzNjVlOTFiM2ZiN2RlZjlkNTVkNWQ1ZjE1ODIxNTQ4Y2FmMzI0NWVlZDc5Mjg2MWIwZGE5YTAyMWYzZGUxYTIxZDljZjg1NzJiY2VjODlhYTE0ZTY4MTYzNDgyNGYyMmM1YWU2MDA3NWVlNDEyMTAyMmUwNzMyYTlhZWMxZmJhZjRiZDcwMDEzYTQ0NTQzMWIyNTg1YmVhNzE4OGU4YWRiNjVmOGJiNWNkMWM1Y2Q0YWZmZmZmZmZmNzU1ZTVmNzYxMjU1ZDIyMzdlMDQwNWMwZTgzMjk1YzRmNTQ0NTQ0ODFjZmZiZWRiYjUxNGJhZjgwODVlZmIzODBmMDAwMDAwNjQ0MWVkNzgzMzBlMGZlMDE1MmY3OGY2NDJkOGVmYjk0Y2VlNTRmMTM2YjM4ODUyNThlNWRjY2FmMWI3YzNiMjBhZTkyMWRhM2UwNWY2NzViZjA0N2NiNjZjZDBmYzUxODNiNDgzYmE5MmI0NmRmY2Q3ZTc0MzI0ZmM1OGQ2ZWE0MmVjNDEyMTAyOTU0OGJlYTViNmMxNjEwMjJkMzJlMzJiZDI2ODYwNzUwMDQzZWI2YWM2YjIzOGVlMjFlMjVhNjcwOWIxMDZmYmZmZmZmZmZmNzU1ZTVmNzYxMjU1ZDIyMzdlMDQwNWMwZTgzMjk1YzRmNTQ0NTQ0ODFjZmZiZWRiYjUxNGJhZjgwODVlZmIzODI2MDAwMDAwNjQ0MTljYzdmZTA0OGI2ZDA3MzJmNTk2YjY0YTMyZjlkMGY5ZjcxOTM3N2FkYWEyNDNhMzUzN2E1ZDQwMTYzNTMxNTEyYmI0NDVjNDBjNTNlMzFhYzkyNzY4Y2Y5M2YyMjY4ZjU1MzM0ZGJlYjJhZjMwODM3MGVmOTQ2Zjg3OTkyZWUxNDEyMTAzNGNiMDQ4NmYzNGYzOGQ0MjkyMWUzNGJmNDhkY2MzOTZiMThjNjM5YTM0MGJjNjM4NWYwY2E1MGI5YWI2MTA4ZGZmZmZmZmZmY2FkMDdjZmY3NzhkODFiMjU0NTRiODU4NTk5N2YyYTdmYzg0N2IxYjUzYmIxMTgyOGQ2ZjhhY2YwYTJhMDEzZjAxMDAwMDAwNjQ0MWQ5MDNjODI2MDI4YWRhMTE2YTM3NzlmODg0NDY0NDdiM2ZkYmIxMTEzODg4ODBjMTY1NzE1NTA1NGIxZjE5ZGVmZDRmMzQxYmQ3YmRiY2Q2M2E3NmE5MzczNDA2Njk4NDdiNTk5MzI1YWJhOTIyYTUwMGEwOGFmMDNhODA2MzU3NDEyMTAyNjc5YTY0N2I4ZDc3NmM3NTA4YmVmZGRkODlkMGZkNzQ4YWE4MjJjYWMzOTQzMmVhMmUxZmEzMDdiMTE0Njg2YWZmZmZmZmZmY2FkMDdjZmY3NzhkODFiMjU0NTRiODU4NTk5N2YyYTdmYzg0N2IxYjUzYmIxMTgyOGQ2ZjhhY2YwYTJhMDEzZjIxMDAwMDAwNjQ0MTQyOTJlNDA3MTgzYmViM2EzNmQ5MjgwZDRlYTQ3M2YxOGY3MmNiZWVmNjNlZjEyMzZjMDk3N2EzNzM1NDMzNTMxNjg3NGUwZDFjYjhhYWE0YzM0ZGYzNWU0MDBkYjZmOGIyMWVjNWQ1MDgxZjJkZWVjY2UwYjE2OGZlMjhlNmUxNDEyMTAyMTUwODhmOTI2MzIxMGI5ZWMzNGMzYjI4YmVjN2U3ODcyYmM4MDk4ZDZiNGE5ZTI0MzllOTVhODcwYTUwNGNiMmZmZmZmZmZmY2FkMDdjZmY3NzhkODFiMjU0NTRiODU4NTk5N2YyYTdmYzg0N2IxYjUzYmIxMTgyOGQ2ZjhhY2YwYTJhMDEzZjIyMDAwMDAwNjQ0MTEyODE4Mzk5ZGZkYTY0MzZhMzUwNWNiY2U4NmU0ZjllZTZlOWRjNzlkZTQ1ZjcyNjI2ZWMzN2U2MzBmNjk5MjgzNmQxNjcyN2M5YjY0YjQzMGMwMWY0MTlkZWNlMmNlYWVlMjA3Mzc2MTI1ZTIwNmExODIxOWY3ZjQzYTE5ZDFkNDEyMTAzOGQ2YmZkMTM2NTUyYTllY2ZmOGRmZDRhM2I4MWU1MWIwYWZlOTYxNTU4MjdhNjA0N2M4ZTM0ZTBhMDg4Mzk3ZWZmZmZmZmZmY2FkMDdjZmY3NzhkODFiMjU0NTRiODU4NTk5N2YyYTdmYzg0N2IxYjUzYmIxMTgyOGQ2ZjhhY2YwYTJhMDEzZjMxMDAwMDAwNjQ0MTgxNWQ4Mzc0ZDI3ZWVhODNiNmFlN2ZjZGIwNTc0YWUyOTA1MzlhNGQyNzNjMzUwMGRlZmNiYTFmYzFmN2EyNTQyMDRhNDUwOTg4NTg3NGEyNDkzYWFkYTY0MDRlNDI2YmI5OTE0MDZkNGU1M2I1ZjM2ODBlNzg0NzNjZDI4NDNmNDEyMTAzYmJmMWQzYmI4NDQ0NTY4MmRhNmYwYjI5ZTQ5YjQxNWY1NzhlMzc1ZWYyYTk5ZjM2YjVkNTllOTY3Mzc0NjcwNmZmZmZmZmZmODgzOGRkNzkzOWUzMmQ5NmU0Yzc4YmY2OTc1OTk1YzMzOTQ5ZjBhYjU3MzBiYWIzNGVhMzFiMTgwOTUyZmI0NTFjMDAwMDAwNjQ0MTlhZmU4ODJhOWE3MjdkNzhmZTA5Zjk4NGMzZTExNDg5ZGRhMjBmOWMyYmIyYmEyYzk0MzczNWVhOGJkYjg3Y2NjYjlhMjk3ZDViYjA4OGI0ZmQzZDQzNjkxNzg3NzBiNWE2NDc3NWUyN2VhM2Q5MTg4MmM3MGVjODdmMzFkN2QzNDEyMTAzMTJkMmYxNGRkYjY1Y2IwNTA3NGU3ZDZlMDY5ZTE4ZjNjOTQ2MTI0ZjhlMTZjMTUwYjE3ODdjN2Q1NGFiM2Q4ZmZmZmZmZmZmODgzOGRkNzkzOWUzMmQ5NmU0Yzc4YmY2OTc1OTk1YzMzOTQ5ZjBhYjU3MzBiYWIzNGVhMzFiMTgwOTUyZmI0NTI2MDAwMDAwNjQ0MTU0YjNiNGM3MjU1Zjg2YjFlMzRjNjE3YzhjNzA1M2EzMTQ0MTczOGZkMjQzODYxZWRiNGY3OWMzYzM1MzY3Y2Q5OTc4YWEzOGRkMjMzZDI4NzI3OGIzNWEyYzk3ZDZkODY1MTliYmYzZDdhZTVkMzQzOTEwMTNkM2NiYzFhNWM4NDEyMTAyNWM3YTlhN2RjMDgxNzhmNjRhNDdlYjQ0ZmQxZmZlNDg2MzE2Mzk4NzM2OTk3NjA0MzIzMjI2NjI3ZTU2YzQxYmZmZmZmZmZmODgzOGRkNzkzOWUzMmQ5NmU0Yzc4YmY2OTc1OTk1YzMzOTQ5ZjBhYjU3MzBiYWIzNGVhMzFiMTgwOTUyZmI0NTJmMDAwMDAwNjQ0MWVkMWY5Mzk2NzBkZWFjNjEwMWFhN2NlMWIyNmY5NzNiNmY3OWM0NmFhM2FmMjZkMzdlYmU5NTc4OGQ0NjE1MGZiNDIzZjYzODlkYzc2ZmM5MjFhOGJiODg4YmRiMGZhZmQ1ZmY2NjIzMWI5ZGZiM2E3NTUwY2U5Y2NjOWU1YzNmNDEyMTAzOGQ1OTRiNGE4ZWQzMWJmZGFmZmJhMGM5YWMyODM5NTUzY2RiYWFiOWI2MzBlYzg2ZDhiYTA1NjViMGJjZmMxOGZmZmZmZmZmODgzOGRkNzkzOWUzMmQ5NmU0Yzc4YmY2OTc1OTk1YzMzOTQ5ZjBhYjU3MzBiYWIzNGVhMzFiMTgwOTUyZmI0NTM0MDAwMDAwNjQ0MTA1M2VjZGM0MGU4YzJhOWRjZDJiNTQwZjcxYjg0NzkxOThmMjVmNzUyYmFiOTcxODM0NWE1ZDVkZGZjNWIxYTliMThkNmM4MjczOGRlMGQxNzgyMjU1Y2QzZDdlYTdlNTRlN2Q3OGU5Mjk2NmQxY2RiYjgwMDVlYzU4NDQ1YjU0NDEyMTAzYzBhODNlMWRhYmRhYmI2ZDJiMjk5YjBhOWIxOWU2M2ZkY2NkMzY2NzRhYWUwYjE4MGI3MTE3MzQ3NmQ4NzNjMGZmZmZmZmZmODFmNzA3Zjg4ODhiYmYxOWJmMWVmYzYxN2IzMzg0OWVkZGRiZjRiMDI0MTM1ODRiYTdmNjBkZjM2M2NiYmQ1ODEyMDAwMDAwNjQ0MTZmMDcwNjc0OTMwMmE2MzBhYzk0YmM1M2NhOTVmZTBlZTU3YTc1N2I3ZjBhYjQ1NzZkNmMyYjVkYTQ1ZmE4MDcyNGMwOGIxNGNhOTBiNDA2YWVlYTYwYzkwNGZiZGVkNzY3ZDlhMzQ4NjNkN2FmNmMzNGY0NDYwYjIxYTFhYzc2NDEyMTAyYTkxYTFjYjg3YjJhYjQwNjQ3MGFmMjNjODYxYjRkNjNiMDZjMzE1M2ZlMjYyZWU4M2FiMDQ1MWNmY2ZmZDViMmZmZmZmZmZmMDI2ZmE5ZGNjODVjMTVjMWMxZWEwYjhhYTEzZDNiYjk3YTFhZDRjNWY2YTg5Y2JiYmJmNTVlYmNlZTUzYjE1ZTA2MDAwMDAwNjQ0MTEzNWJkMzYxYWNlMTJiMmI1ODYzZjE4NmE5Mjg1ODk4YTRiODVmZjgwYmY5ZGYzN2RjMzI2OTI0NzM5NjYxMDJjZDFlMTU0YTI3NjY5YTU3M2MyZGE4MjFkN2MwNDA0OTdjYWQxN2I3M2MxZjI0ODg1OTI5ZWQ4YTk4MDNmZTk0NDEyMTAzNjgzM2MwZDczNTg2NDU0MDBiYjRmNmNkMzRiZmQwMjA1ZmFkMjlmMGQwNjM3YWRiZTI1NzRhNGJmMjJhN2Q4ZWZmZmZmZmZmMDI2ZmE5ZGNjODVjMTVjMWMxZWEwYjhhYTEzZDNiYjk3YTFhZDRjNWY2YTg5Y2JiYmJmNTVlYmNlZTUzYjE1ZTBlMDAwMDAwNjQ0MWYwZWIwNDdlZWUzMGY1MDE1NDg5YTQzNzc3YzY0Njk4NTZkYjNiNTEwOTMzODg3YjY5MzIzZTI5ZTg5ZThiNTgwNjk2N2FlODZhYmQ3MTkxOThkNmNmMTAwMjBlYWMxYzZlMjNlMGQyNzUxOTQ0NjU2YWVjZDBjM2IyN2RkZTRjNDEyMTAzZGUxYjZiZmUyNTBiZjc5MWFmMTk5MTBlZGU4Mjc4M2FjYjNhOTY2NWY2ZjUyYmY1YmFjOTA0MmNjMmRlMTVkMGZmZmZmZmZmMmJiMmI2ZjMwM2VlOGM3YTE2MzNkMDJiMTVmMDYzNTNjNDFjNDg4M2FhMzE1M2Y0MTA5NzRjNjFkZmM1ODQ2ZjA4MDAwMDAwNjQ0MTRkYjczYzJjNTA0OGQ3NmQ2NjRjYTA0MDhjMjhiOWRiNmQ4OWU2MzgyN2M4YWMxZTlmNDNkMTcxOWEzNTk2NjhkNzk4ZTJiNjZmMTQ1NjRhYzBmODI1ZjAyYjAwODBkYWJkZjg2ZTI0YTFhNmFlNjQ5NWNjMTgzMTU0NzVhNjI2NDEyMTAzMGNmMWUwNjFmYjZiZjkzYTE0NmIxYTNkNjFlM2VjYTA5NWY5ZmZkZGU5Y2IyMWZjZjY5NjI2NmRjYzFkNGUxYWZmZmZmZmZmMmJiMmI2ZjMwM2VlOGM3YTE2MzNkMDJiMTVmMDYzNTNjNDFjNDg4M2FhMzE1M2Y0MTA5NzRjNjFkZmM1ODQ2ZjE3MDAwMDAwNjQ0MWJhZjBmZTJkNTA2MzkzMWZjNzdjMmU4MjY0YzU4NmRmMWY1MmZmYmU3ZTA0OGRiYmJjMGMyOWRlOGEyNjY2NWFiY2MyMTE1Y2IwNWExNGRiYmY3MmU2MTVjNzA4MjJkYmViZDI2ZTA4NTY1ZjM3M2EyMWI5ODAzYWY1ODgzZWVjNDEyMTAzMTg0ZDZlYmE2ZjQyZjg5MmRiNmEzYzY4OWNhNmZkMTVkMzJjZDg0NWVlNjRlMzQ0MThlZjc4NGI4YTA4MWM3MWZmZmZmZmZmOGIzYTdlOWE0M2NjNDdlZDQ0ZTYwOTQ0ZmEyZWY4OGNmMTg5NzI2NWUwNzhlYzEyM2ExNTMwYmE0NjdiZDI4MTAwMDAwMDAwNjQ0MTAxN2ZkNzI3YzdmYWFmZjNkNjM0NjIzMGQzMTdlZWRhNzMyZWZlM2E5N2QxOGQ4Njg3OWFhYmU5MGM2YzY2ZTg3ZDA2ZDdhMjc4M2UzODExYTM2ZjhjMWI3Y2JiNWE0M2IxODIyMDE4MjVlYzgzMjI2ODQ2NzdlNDE2Yjg0ZmM2NDEyMTAyN2NmYzI1M2E2M2Y3MjBlMzlhZmJmNGYwNGNjYjVmZTdiOGY5MDAyNmE0MWNjYjY0ZDNiMmUxMTIxYjYyODkxZGZmZmZmZmZmYTc3NTFiOWVmN2IxNjU1Mjc2NTlmY2VkOTJlMTQzZTM3MmIzZTIyMzljYmU0N2EzMjExODJhMzU0MGNjZTk4NzBjMDAwMDAwNjQ0MTczMjMyMmYwODMzMjlmNjg3ZDY1ZTA3M2QwMjMxZjZiZjlhYmIzMGYzMzBiODA4ZjRhNWYyMzI5NGNlZGRiODM2ODJmYzc5MjI2ZmMwZThjMzBmYjBiMzZmMTBjNGY0MGI0ZjgyMjJhMDE1YjkxYzFiN2VjOGMwMTNhNDYxMTlhNDEyMTAyMDAyNDdjNTAwMWIyNDc5YzY3ZDk5YjQ5NDZiYmNjYjA5MWZkMDk5MWJkOTFiOTJiM2MwYWUxZTc0YTUwZjg4ZmZmZmZmZmZmOWYzMGI3ZTg0NGE0ZDcwOGRhYzkzYWU3MWI4MmUzODgwZWNlY2UzYThkMTZkODRmYjM2MjA3ZGQzZDY2MGI5MDE0MDAwMDAwNjQ0MTIzZTE1MzIyM2IyZjZmMjhkZDljMGFhZWVjOWYyN2U4M2JiNWVhY2MxOTcxZTE4MjNjNDc1YTE1YTQ4NWZhNzIzMDE5MGI1NWM5ZDllODAzMTI4NTE2NWI4Mzc5ODBlMTY3ZDIzNTViZTM2N2UwMmEyOTVhODE0Nzg4YzIzNjkyNDEyMTAzMDljNWY2MjdmMTIyNDRlNDRlOGNiMzRkODlhNjY4NDA5MGZlMWUxYjI0ZmY3MWU1ZGMzYjUxZjFmODhiOTIyYmZmZmZmZmZmYzgyZjEyNWYxMjE1ZjE0YTY4N2FlZjgyNmI5OThhZWNlMGJlZTlmNDU5YjczMmIxN2Q3NTIxY2Q0YTFlZjg5ZjBhMDAwMDAwNjQ0MTlmYTUxYjdkZjNiZjY5MTBjZDU1YTE0YTg3MTUyNTlkODU3ZGU1Y2VlZGU3MThhZjI5YTY2YWQ2YmVhYjE2MTI4NzFlN2RmMmYxOWNlODc3ZjA5NjgzNmI4OTllNzZiMWEzODY5NTFkZDAyM2VjYjA5OWZiZGQ2OGY4OTdjMmY0NDEyMTAyNmVjZjk5MmIzNGJkNjcwMmI3NTJiODk0MGZkYmJhZGUyY2VhYjc5NTk3YWE5ZWYyNTljMjU1ODg4YmEzMjhjNmZmZmZmZmZmYzgyZjEyNWYxMjE1ZjE0YTY4N2FlZjgyNmI5OThhZWNlMGJlZTlmNDU5YjczMmIxN2Q3NTIxY2Q0YTFlZjg5ZjBiMDAwMDAwNjQ0MTBkNGFmOGU1MjMzYmZhZDRmMzdhZjZjYTJkNzlkMmQ1MWEyNjgzNTQyMjFiNzdiZGIzY2U3YjhhZTg0NGZkYjc3Yzg5YWUzZjhmNzE4MmQ5Njg1ZDhkOWY2ZDIyOTQxMWNiYWJhODM2ZWYyZDk1ZTQ0YTBhYWExZjA0NjlhMmYwNDEyMTAyOTcwNGViMDUxMWQ2MWJjNGM0ODZiOTJiZjY2MmNiZDE1NDQ4YjRiMWM5MTk5YWJhMjkxOTYxYjYzNWQwNzk3Y2ZmZmZmZmZmYzgyZjEyNWYxMjE1ZjE0YTY4N2FlZjgyNmI5OThhZWNlMGJlZTlmNDU5YjczMmIxN2Q3NTIxY2Q0YTFlZjg5ZjFhMDAwMDAwNjQ0MTI3MzAxY2Q4ZTk5MDM5ODAxODY0ZDYyMGU2NThiM2EwMDA3YmU2MjJkMTVhYzA3ZmZjZDhjOTg3MTIyZTBiMmZjZDMyZWRmOTJjZDFiYjBmOGVlNTkwZjUzM2Q0ZTBhNTdlNDEwNjQxODM5OGE0NmZmNjE5OWIxNmYzMjVkMWEwNDEyMTAzM2Y3ZmMzZWQ5MjkxZTU0NWU0MDY3ZDQ1NzIwYzFjZDc0ZTg3ODBjMmIzMmVkZDExMWIxMjQ5NThmNGJmYWRmMWZmZmZmZmZmOGVjNjIzMDYwNTQyMmMwOWVkOWViYWE4NzJhNDRmYTE4YmExOTlkMmIzNjYyOWIyZGUwMDNhMTY0MDgwZGRhZDE3MDAwMDAwNjQ0MTJkMDlkY2E2YjhhMzRiNzc3YzdkZjhiODkzNGMwMWEzOTZkMWFmYWJkZDI1OWI3YTg2NjExZDE0MGI3ZmUyY2Q2MWFkNDIwNzIwOGM0MzczNzI1YmYzNGM3MjRlODFiYWQ4NDIxNzU0YjMyNTAyODBhNTU5ZjRjMzdhYjYwMWJhNDEyMTAzMWY4MDA3MTFjZGM3MDUyOWUzMTZmMzdlNmQwMjE0YmFlMGNkYTg4ZmRlYmRiYzUzZTMxNmI2ZWI4NzY3ZDcyMWZmZmZmZmZmNzUzMmM4MDgwY2VmNjhkMmY2MzQ1NTUwZjQ4ZGRiYjFiOTQzZTIwOTI1ODJmN2E4ZWQ4Y2YwY2UwMzY5YzliNzA3MDAwMDAwNjQ0MTI5YjlhODRmY2ZiOWE5MzMxZGMzNTg1OTBiM2IxYmUyYjJjMDY1ZmI2NTQ4ZGVlMDZmZTUwY2IwOTA3YzhmNThhNzUzMDQ5ZjE1N2JkYzQ3YTEyNGFhNjZjYzZhZGEwZDRkNWEzYWFkNzA2NjA2N2Y2M2MwNzhmM2U1ZjVjMTI4NDEyMTAyMGI4M2Y2ZDhjNzhkMzNmZDEzY2Q1YTA2MjhkYzdjNjkxYzBjMzA1ZWQ2Nzg5NzdmZjYwZWU4ZTFjMjk4YzcwZWZmZmZmZmZmZWE2N2ExNzY4Y2Y5ODJmMGI5MzlkMGZhNGYyMWNmZWYyNmExODg4ZTAwZjdjYjRhNDU2YTFlZmY4Mzc2ZTBiODFhMDAwMDAwNjQ0MTg2YmM5N2YwMDFjNzk1ZWRmMmRlZjIyNzc2MzFkZGQ3NzEyNDdkZGI5NDM4YzZjMTAzNjhjNjQ2MTY5MWMzZjFiYTA1YTBiYWYxYWE0YjczOWY0NTNmNDYyNDRjN2U5ZmRhMzhiNzNlZTIxODNkNGExODk5MzlhMDE0ZWNkMTllNDEyMTAyOGI0MDgzY2JhODU0YjgyMTM4MDc3NDdiZTFlMzRlZjZlYzdmODQ5MjhlZjBlYTczMTM1MmVmYjI1NDhjYTM1YmZmZmZmZmZmNjkzNzA0OGJkOTU3NWYwOTA0NGFmNWY2NDMxMzM4NGFiN2NjOTc1OGY3MWU0OWFhNjc2NzMwMDQ1NTZhNjBiYzFmMDAwMDAwNjQ0MWRmNTllOGJjZGU3NjM3NjI4MTkzZDA1ZmY2NjA2OGRmZTBjOWMyNmYwNjcxYTY0YjdlNDcyMzkwOGUxYmY2Y2EzMzJiYmVjMDkyNTMwYjAxNjZmZmFmMGY1MzUxNzQ4M2RiZjMwZTZkMGQ3NTZmYTZkMzY0OTg1YTRiMGZlNDM5NDEyMTAyNWIzZjc1NTc0YjhiOTA0MWIwMjRmZWVhN2YzMDgzMmM3YWYwMDc5NzYxNzdkMDBjNjYzNWFmZDA1ZjI3YWQyY2ZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzAzMDAwMDAwNjQ0MTBlZTRmYzdkNGIyZGEyN2Y5NzJhZWY2ODA5NDJiNzk3NmM4YWRkNjk3ODMzNjY0N2QzMzU0ZjQ4ODkwYmIyN2ZkNDk4NjBmOGQ5NDkzNzQwYjk2MGQwMTljMDYxMDhkMGFkMTYzNjQ4ZGE2MTU0ZTg0NzFkNmMwYjhkOTBkNTcxNDEyMTAzMDI1ZWQwY2IyYWI1Mjk1MGVjY2I3NzY1ZDA0YjFlNDAxMGIxNjg1NWEyZjRkNWRiYjhmNDk3MDZjOTc5YzA3MWZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzBlMDAwMDAwNjQ0MTgyMjYyMmMyYzljOTU5MGZlZjBiMDMyMGM5YjViODQ4ZDhmZWEwZjYxZWFhNDExNDgwZDNmYmE1ZjdkYTA4ZjgzNTcyZDU4Y2VmYjE1OTEwYTU0ZmY0OWI4YTlhMWNiZjQ5ODBiYzRiMzIxYmFjNTUxMzg3ZTA0YTlmY2ZkNjIyNDEyMTAzNWY2MWFjZmJmYTdhMWI3ODViNzYyMDUzZDk2MDE4YjU5MjlhZTdlNThjMDc1ODNhNjI2MDlhMWExM2UyMjY4NGZmZmZmZmZmMjIwZjQ5ZGE0ZjVhZDY4ZjJlYzI5MTBiY2IxZGRjZjU4ODUzZTJiYzQ2Y2Y5OGEyMDE1NjY2ZmFlMzQ5MzVjNjE4MDAwMDAwNjQ0MTMyYzRmMDVhNWUxYTU1ZTAzZDFjMTgwZTA3ZjE5NGY0ZDczZmMyNWFmOTY1NjEzOGY1YTNmZjk5MzdkNGE0NTc1MDc5ODIxNzczMTA2NGE2YmNiNjM0M2NiMDk4ZDI2OTFlZTUyYTcxMGRhNDZiNGMwNWY3YzU2NmRkYzcxMjM2NDEyMTAzNDBiZjFjNzI2YWFkMjk5OTMxMjYzNWVlYjFhMTYzZDZmM2ViYzc4YjRiNmM3OWVkNWY3NmRkN2Y0MDk0NDZhMWZmZmZmZmZmMjIwZjQ5ZGE0ZjVhZDY4ZjJlYzI5MTBiY2IxZGRjZjU4ODUzZTJiYzQ2Y2Y5OGEyMDE1NjY2ZmFlMzQ5MzVjNjFlMDAwMDAwNjQ0MWEwNDA0OTRiYWMwYmJmNTRmYjRjOWNiMzIxMTQ3ODc3NmNkZmI5Mzc2N2UxNTlhYWMyMzdkNDU5N2JlOTBiMWMxODI3MzEzYmE0NWJlODc2OTZlNTM4MGQwNjRiZTFiZDI0ZDQxNjkwOWI3Y2U3MDFmY2JiYzFmMzdiYmZjYWZkNDEyMTAyZDMxOWFjZjcxYmRhMGIwNWNiMDBmOWIxZjQ1NzU5NWRiMzE5MzM4YmQ3MmQzZWVkYjBiZTZiNDliNWY3ZjY2MmZmZmZmZmZmMjIwZjQ5ZGE0ZjVhZDY4ZjJlYzI5MTBiY2IxZGRjZjU4ODUzZTJiYzQ2Y2Y5OGEyMDE1NjY2ZmFlMzQ5MzVjNjM1MDAwMDAwNjQ0MTAzNzEyODAyMmNiZTA2NzEwYjc4YWEzOWM0NmEwMDg4ODUyMmYzNGQ2NGIyNjk2ZWExNWI5OTRiMTRjMzA0YzU2OTcyN2Q2NWI0ZDFjODc5YTU5MzIzMjZlMWRmY2M0MzdkMjM3MTQ0MDcxNTk1YTg1NmQ5OTQyZGRhNGQ4ZDU2NDEyMTAzN2ViOWRlZjYxMTEwYzRiZTI4MTNiODYyMzAzYzljNmYyYzQyZGE3MDBiNGUyZTAxMzYwNjBjYWMyMGZjNzI2YWZmZmZmZmZmOGFhM2MzYTBjYjhiNjc2MThjMzEwZDRlOGU0YWViNWQzNzdiYzM5Y2QzNjhiOTY4MmI0YTM1ZGExMjRhZWZjZDM3MDAwMDAwNjQ0MWI3OWU3ZWU3YzUyZmY3OGQ1MTE5ZjdmZDY2NmIyMWI1ZjY1ZjdmODQzM2Y5YzFhMDYzNGE3ZjM3MGZjYWNlMDVmYzJkODJjZDU5MzA5ZjAzNGUyOGJmMWQyYzIwNGEzOTAwNzcwNTljNTUyMzhjYmM1OWMxYTU1MDViY2NlZDQzNDEyMTAyMzk3NGJkYjMyNjE3NmY0ZjE2ODhjOGNhYTNkMTc1MzgyYTkyNDJiNTc1ZmUyZWNlMGU3MzRjMGIyMzE4NmZjZGZmZmZmZmZmMTYxNzM0OGVjNWNhNjQzZjAzMTEwOWUwNWUwYmEzOTMxMDE0MDgzZTdhY2JlYTM1OTcxZGNlMmNjNzgwNWRkMDBjMDAwMDAwNjQ0MTlhNGYzYWY5MTMyN2Y0MjM3NjZiNzA3OTI0Mjg3NTExMDY1NjhlZWEzODZjNjIyMzQ1MTQ4ZjM0MjJiODU2ZTQ5YzY2MzJiNWQxOTBiOGE2NmIzYTU1Nzg0YWYzODNhZDE5ZTdiMGVmNGEzMWY3MWE1Yzk3ZGFlMTViNTRlMThjNDEyMTAyMGUyN2JkMmIwZGM4ODI0NDQxMjdiMTQzYTYzYjZjNDgxYzhmZjJmYjM0ZmMwMGVkMDNkOTdkMTFmMWE3ZTJlYmZmZmZmZmZmMTYxNzM0OGVjNWNhNjQzZjAzMTEwOWUwNWUwYmEzOTMxMDE0MDgzZTdhY2JlYTM1OTcxZGNlMmNjNzgwNWRkMDI5MDAwMDAwNjQ0MTA5ODRmYWI2NDg2MTBhYzU5N2QzYTQ3ZGY4YzQ4NGEzZjY3NTFkZDYxNTUzNzE2Nzg4ZjM3ZDY4OGVlNmVkZGUwZWViZWY2MTVjYTUyMjQ2OGViOWU2NWYxMDcxZjE1ZDkwNTg4ODY3M2YxZjBhYWFlZTQyNDAxOGEyNjE1ZDcxNDEyMTAyYjI1ZDhhMDUyMzg0MmQ5ZDVkNTNkNmIyM2JlNTBkMWYwY2FiMTI2ODQ1MGNjMDE0YjM1Y2QzMmQwZGFlYWUwOWZmZmZmZmZmNjJhZmI3N2NhYWE4OWI1NGE1NWQxMmM0MDg4ZTE0NjZkZjdkNTY3ZjgzZWZkMjZmMDZjNzk5YmM3Yzg0ZDVkNjAwMDAwMDAwNjQ0MTdmOTU5NjlkN2QzNmNlYzA0ZjRkZGQ5NTVkNTllNDQ5ZTkzY2I1MmRiMTY4ZDEyZjc4M2M2ZjVmY2VmYzc2NjNjYmJmNjYyNWJlMTBlMzMwYzAyOWUwOTA2NTg2NDM0NWU4ZWM1NzNiOTBmOWI4NjAwNmE1OGM0NTgzNzU0ZWI4NDEyMTAzYTcxZDFkODdmYTMwYTI1N2Q3Y2RkYzE1Mjg3YjVkNWU2MjdjNGUwMjllZGI3YTdmMzI2ZDRkMWY2MzNiZmMxZmZmZmZmZmZmMzJiYzhmZDBmMGJmOTNlMzM4MGJhNTZhZWQ2ZWVjYWI5M2E1NGYyMDFlNWZiMWI2MDA5M2Q1MmI4MzU3MjBlMTEzMDAwMDAwNjQ0MTYzOWVmMWY4M2IxNTM1ODVjMjIxNWRhMzNkYWRkNjczOWNlZTgxZDZmMWM1ZjI3MmE5MzFhZjc2ZjFhNDQ0YjQ4MzZlYzJjMGYzNzk0YWUzMDUyYTkwNDI0MGYyMzNlZDU3YjE3YTlmNGQ5NDVjY2VjOTM0NTU5YTAyOGVhYTNkNDEyMTAyMmY1MDdhNTRmZTc1MzE2NzFkY2EwYzg4M2ExMmUwN2I3OTIyZDAwNWUzYzY0MDRlN2ExNDMyNGRiZTMwNDlmOGZmZmZmZmZmMzJiYzhmZDBmMGJmOTNlMzM4MGJhNTZhZWQ2ZWVjYWI5M2E1NGYyMDFlNWZiMWI2MDA5M2Q1MmI4MzU3MjBlMTI5MDAwMDAwNjQ0MTI1MGIzOTEyY2ZiYjAzODg4MzUwZDkzMmQ5YmNjZjY2MWNmZmQ0ZGE4MTVkZWJmNDMzMjcwODkwYWUzMzE2NmJmZTg5ZWMxZjU3OThjNGZlZmVjMmE0ZTdlNTJkNjgwMGZkYzRlMDQyN2QwNGNkYThjZTA1YTRiYjc0OTVhMjhjNDEyMTAzYWJmYzAxMTJmM2UwMjdkOTI5MGJmYzZmYmZlOWYxYjE3MmQyNTExYTFkY2U1NjYwOTlkZTFiN2ZhMzQ5MzY4NmZmZmZmZmZmYWIzNWIxN2Q5OTVhNDAxODA5ZTM3ODgwN2IyYmE5OTQ1NDczYWNiOGNjNDMzZTVhY2Y1MmI2MjU4ZjQzYzJlMTA5MDAwMDAwNjQ0MTBlNDBjNTM3Y2RjZmUwOWNhZDFlZmI5N2UxYWNhNzM3ODY1OGE3OTNhYjY4MDFkYThkMWE4MjljOTI4YTlkOTgyMDQ2MDk3ZDI5NjcyODlkNTBlMGIwMjkzMTM4Y2VmMzJmNjMyMGRlZmNiMjY3MDc0ZjgwZmMzY2I4NjUxNDMyNDEyMTAzMTJiOGE4Y2U0NzZlZDJhMmY0MGY1YzdkYTAyODhhOTdjNWZlMTA4MTUzMmYyMmFiZWMyNTc0YTMxNGE3MGY3NmZmZmZmZmZmNDNlNjQ0MTIyZWI5OWQ1YWM5NTM1Y2ZjNzk0Zjk4YTJhYjQ1YTI5ZTc0MDU5OWZhNWQxNDY4OWFjODU5MTRmODBkMDAwMDAwNjQ0MTMzMWE4ZTZjZTYzOTUwYzE1Yjg1NzVhMzE5NTFhNTIwNTI0YTkwNDI2MTY5ZGJmODY5OTY4OGE0NzM2ZWQzZDMwMjA0Y2I4ZGU1ZTBkZmY4YWQyNWE1NWJmYjI5OTMwMGZiZTk0M2VjMDk4ZTFkZWY5YjkyZmIzNDNjNGYzZmE4NDEyMTAzYjExNzg4ZWNiZWI4NDczMGY3NTNlODcwNGU1YmU5NGE0YjBlNjMxZmJmNTc0ZGI2YjE3NDY5MWM3Y2ZjMzhhYmZmZmZmZmZmY2NlM2NjZThlOTVmNDkwMGJmNGI0NmE4OThmODc4NmI5MzU0MDY1YThiNGU2MThmMGMzMTIzNWZjODg4N2NmOTBiMDAwMDAwNjQ0MTNmOGNmY2NhYmNiZDcxNjRhMzRiNmMwMjQ1OTUxZGVlODdlOGMzOGRkOGI3OTJjOTQ5NDI2YWYxYjkwMjU4MmFlNTc4NDMyMmY3NDg2OTJlYmIyZDU3NTM1YzdmMDIwZGJlOWFkMjU4ODAwNzUzZjk0NWJjZGRmNWRlZjBmODYzNDEyMTAzYmU2MTkxZThkOWYzM2NiNzZmYjc1ZmFjM2ZiNTgxNGEwYzI3NTkxZmU4YzEyMTdjZWU5N2IwMDRhYThlMmRhOGZmZmZmZmZmMzYwMDAwMDAwMDAwMDAwMDAwMjc2YTA0NDY1NTVhMDAyMGFiMGM4ODRlZjJmNjVjMTIyNDc1ZmRjZWJmYWM5MTY2MTk4Mzg4ZGIwYTRkMDExMDVlYTIzN2E4YmZjOGViOWQ2NDk5MGUwMDAwMDAwMDAwMTk3NmE5MTRiZmM4NmRhZWJiYjMwOTY4NzdhN2Q2NjQ5M2FlMzdjZGExZmZmOTAxODhhYzAxYWMxMjAwMDAwMDAwMDAxOTc2YTkxNDdiYTc1Mzc2NTNmOWEwMzYyNzcyNDNkNTU4YjFkYTNmNjBmYzAxYmE4OGFjNDVjYTE0MDAwMDAwMDAwMDE5NzZhOTE0MTA1ZTEwMzVmNmUxZTdkMGU2MjE2ZTk0OGY4NDBhNDc2Y2RjZmEyZjg4YWNlYWQwMTgwMDAwMDAwMDAwMTk3NmE5MTRjNGUyZTJkZDc0ODhhN2I4ZmM3ODJjNzc0ZDUzMGQxYmRlOTA0OTdjODhhY2RmNjQxYTAwMDAwMDAwMDAxOTc2YTkxNDAwOTQ2ZDIyYTc2N2JlYzFlZjgwMzg3MWJkNmY0YTgzY2NhNmVmMDM4OGFjZTMwMjFiMDAwMDAwMDAwMDE5NzZhOTE0YzFiZDYxOTkxZWZmNWIzY2YxZjY0Njc3Zjg2NTJkYzQ4MWY2YjM5Njg4YWM2M2M4MjEwMDAwMDAwMDAwMTk3NmE5MTQ2YTM1MzJmYWZlNjM1ZTRkNzhkYzk1NDYzZmU4ZGI3MTJlMmI0MWQ3ODhhYzAxZTgyZjAwMDAwMDAwMDAxOTc2YTkxNGZkMTE1MWFmNzA1M2FlN2E4ZmM2NTk1MmNhMGZkYWZiNDI1YWNmZWI4OGFjMTY5ZTU5MDAwMDAwMDAwMDE5NzZhOTE0N2VlZmJiOTFiNGIwMDgyNmY0MDFkOTM4ZGFlNWNlZDRmMGYwNTQ3OTg4YWMzYmUyNWIwMDAwMDAwMDAwMTk3NmE5MTRhMjMwYWY3ZTQyZjQ4M2RlN2U3ZjU4MTczNmVmMjU0NWI5MjNjZDM2ODhhYzAxMTE2ZjAwMDAwMDAwMDAxOTc2YTkxNGJkOTQwNjI3YzczMWUzNDU4Yzk0NDg5ODBjNmRiYTQwNGQ2OGFiOWI4OGFjMzFkMDcxMDAwMDAwMDAwMDE5NzZhOTE0ODBjNzg2NGJhNDFmOWRlYTRlNTM4NDU1MmJlZWRmYjY2ZTg5ZmYwNjg4YWMzZjg2OGIwMDAwMDAwMDAwMTk3NmE5MTRhYzhmN2FiMzZjZGY2ZTExZDNhZmQxNmRmMDljNGRlYWU0MDJmZTlkODhhYzM2NmNhZTAwMDAwMDAwMDAxOTc2YTkxNDhkODRkYmJiZmQzOGYzNWJjNzI0OGE5ZDRiNjdhMzExMjRjMWJlNTQ4OGFjNjQxZGNhMDAwMDAwMDAwMDE5NzZhOTE0ODQ2YmZlNDJkNjgzMTEwNDI3ZGIzNDI4NGFmNDE3YzE1ZDU0NWQwNDg4YWM0NTJkY2EwMDAwMDAwMDAwMTk3NmE5MTRhZGQwYTZkY2M0NjRkNTM4Y2RlOThmNjE1Y2QwYTAzMjE3ZWZiZDZkODhhYzFmMDlmMzAwMDAwMDAwMDAxOTc2YTkxNDM1MWY2OTJjNWY4NmVjNjJkNGJlMTJmNDE5YzNjOWIxY2ZlNzgyNjk4OGFjMTgwY2Y2MDAwMDAwMDAwMDE5NzZhOTE0ZjViY2JhMTU5YWFkZjgwNmI5MWQzYzJlMTNmZTkxMzRhZTJlYWJjNTg4YWM0Yzc1MTYwMTAwMDAwMDAwMTk3NmE5MTQ4MWFiZWFjOGNkNTY1YWE3NzlmMWQ1ZGFkNGY2MjQyYjBjM2U3ZTY5ODhhY2EzMDQ3NDAxMDAwMDAwMDAxOTc2YTkxNGYwNTkzZjY0NjVhM2FlNzliODZiMTBkYzk2MzBlY2EzMjc0ZjJiNmM4OGFjYWRhYzgwMDEwMDAwMDAwMDE5NzZhOTE0ZDJiM2I2ZGQ2MjAyYzgxNDhiNGUwM2UxYWU4ZWZhYWExOGFmMGU0ZTg4YWMxMzM2OGYwMTAwMDAwMDAwMTk3NmE5MTRhZmRhY2U4MDRlZmU2ZWY5YzI2NWM2YmM4NWE3MjJmZjg2ZjU3YzNkODhhYzU2YTk5NDAxMDAwMDAwMDAxOTc2YTkxNDdlM2Y0MDNkNWY3NjM3YmVhNTc3NjJmMDZhMDI0NWRhMzNmY2FiODc4OGFjYjIyM2FjMDEwMDAwMDAwMDE5NzZhOTE0YmZkMmM3ZTg3ZmNmYmI3ZjBjN2M3N2IxZmM0ODI2YmUzMGM3ZTdlNDg4YWMyNzhiYmIwMTAwMDAwMDAwMTk3NmE5MTRmZTk4YTNmM2Q3ZDFiODg2YTFkMzBhZmZiNGY1ODRhNDIzMDk3MDAyODhhY2Q2YjFiYzAxMDAwMDAwMDAxOTc2YTkxNGYwMDc1NjVmM2I2OWE3NzUzNzdiYTU1ZTQ0YzZkNGQ3ZGIxYWU0Zjc4OGFjMzIxYTFmMDIwMDAwMDAwMDE5NzZhOTE0NTgzZDBhZGVkNWQ0OThlZGM4ZWIzYTc0NDI4YjcwZDZhZjUyNDk1ZTg4YWNkYzQ5NDEwMjAwMDAwMDAwMTk3NmE5MTRkMTAyMjYzNTU3MGRhNTZjYWIzMzFhNzFhYTYwYzU5NjQ4NzZkMDg0ODhhYzUyMWU0MjAyMDAwMDAwMDAxOTc2YTkxNDkyODZkYTY2NWMzNmQxYThlOTczNDRlMjZhMTQ4NWM1NjllMTdjZDI4OGFjNWQ4YjQ3MDIwMDAwMDAwMDE5NzZhOTE0ZjNlMjBjNzhlNGYwYTZjZjEyYzlkNzRiZTIxN2RmM2Y4MTIwYmNlMjg4YWM2MDBkNTMwMjAwMDAwMDAwMTk3NmE5MTRlZTBlOGVlNmY3N2NiYjQ3N2UzNTgyNjczOWM4NDZjZDU2NWQ2NWJhODhhYzAxYTA2YzAyMDAwMDAwMDAxOTc2YTkxNGJkZDgwZGJhNmUxOGE2OTZkMzg0ZjY5NTUxMzljZGFkOGNkMWI2NWU4OGFjMzM1MTg5MDIwMDAwMDAwMDE5NzZhOTE0YjZiNGQ5Y2U1NjBiOTIzZmY2ZjE2ODk0MmU2NGE3OGNjOGUwMWQ4NDg4YWMyYmUxOGIwMjAwMDAwMDAwMTk3NmE5MTQ1ZTdkOWM0YTU5ZDIzNzQxNDhlNmUyNzU3NDBmNTdkMzIyOGJhM2U1ODhhYzhiOTNiYTAyMDAwMDAwMDAxOTc2YTkxNDZiNzI2MGI0YzMyYmZiMDNiNDk3MGFmZTRmM2E5M2JiMzQxZWE1NWM4OGFjNzk4M2QwMDIwMDAwMDAwMDE5NzZhOTE0ZDlmYzU1YmIwMjI3OGUwODRlYjY4NTMyNmI4NGEwOTc5OTJjYTY0ODg4YWNlMzdmZTMwMjAwMDAwMDAwMTk3NmE5MTQ4ZWEzMGVjNTUyMDdlMGZhMTkwZDQ2ZjFiZmRiYmU3NmVjOGQzN2U2ODhhY2U3NDJlOTAyMDAwMDAwMDAxOTc2YTkxNDI4ZDllMTgwNDk1YThlNzUzMGQwNzdjZGRkNWY1NmEzYzU0ZjUyN2Y4OGFjMGE0MDE3MDMwMDAwMDAwMDE5NzZhOTE0MzNkNTI5MzM5MGVhMGM2OTk2NzdkN2FjMTU2NjViMGRmNzQzZjdiYjg4YWMwYWRlNmIwMzAwMDAwMDAwMTk3NmE5MTQzNWNiOWM4YjY2MzNjMzEwODVjMTk4NzhkOGZlMDBiNjY5M2M1NDYyODhhYzZlMTc3YzAzMDAwMDAwMDAxOTc2YTkxNGFkODNmOTNjZTRiZWIwNzg4YmVhNzA0NzJjZDAwM2Q0YjE3NWZhYTM4OGFjNzE5OGQ1MDMwMDAwMDAwMDE5NzZhOTE0YTNlNDkyM2ZiOTlmYzJlYWE1ZGJmNGM4OWQxNWZiZDlmNmUzY2E0NTg4YWNhMTkxZDcwMzAwMDAwMDAwMTk3NmE5MTQ0NDUzZmNiYTJkNTk2YjZlNDA0YTM1YjIwMDBlZjcxYTcxZTU0YTFmODhhYzFjNTJlYjAzMDAwMDAwMDAxOTc2YTkxNGU2NjQwMjdjZGQ2ZWQwZGI3NzcwYzhmM2Y2MDk1NDkzMDQ4NGEzNjY4OGFjMzE5YTcyMDQwMDAwMDAwMDE5NzZhOTE0MTMyOWZlYWZhZDkzZDNjYmNhOWE3YWU2NWQ0MWU0YjQwNTM4NTM4ZTg4YWM1NDcxZWMwNDAwMDAwMDAwMTk3NmE5MTRmOTdlNDFlYTgwNDZhNjBmMDlkMTMxMjU4N2EwOGM5ZjMyZDU1ZjIyODhhY2Q0ODE4ODA1MDAwMDAwMDAxOTc2YTkxNDE3ZDg2ZDhhMTRlZWQ5YmNhMDI1YTMxZjZkNTM2MmZkOGI2ZjNlMjU4OGFjOTZjYWU3MDUwMDAwMDAwMDE5NzZhOTE0OWU4ZTZiN2QyY2M3N2QxNzg0NjY3NmEyZjNhMTRlMmMyNWY5MmNmNjg4YWM3MjM4OGUwNjAwMDAwMDAwMTk3NmE5MTQ5YWU4NGIxNTZjNmU4OTJlN2VmNGNiMTZiNjM0ZDg2ODlmZWQ2YjRlODhhY2U3ZDVhYzA3MDAwMDAwMDAxOTc2YTkxNDEzMTY2Y2ZhMWUwOWI3M2UwZThhMzk0YzczMjczMDE3ZjhkOTU4MzM4OGFjNjZkMmJhMDgwMDAwMDAwMDE5NzZhOTE0ZmIyZjA0YThiMDU0YzcyY2RlZjlmMDEzNTVhZDEzMjZiZTVjNjZjNjg4YWM5MTFiODQwYTAwMDAwMDAwMTk3NmE5MTQ3MzI2OTRhYjE3NmQxZDA4ZmExMGVhYWRiZDUzZjEzNGJkYmQ0OWNmODhhYzI0MjRlMzBmMDAwMDAwMDAxOTc2YTkxNGQ4OThjY2ZmNGI0NWY1ZWNiMjUwOGNhY2YzZGM0MDkwY2M1YzE1YjY4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI3NzZlOWYxNTBhMzhkOWFmNzIyNzM0NWMzMGQyNzAyYmQ3MjRiODcxYzI1Y2QzMTk4YTk4NjdlYThiNDFjNmNjIiwiaGFzaCI6Ijc3NmU5ZjE1MGEzOGQ5YWY3MjI3MzQ1YzMwZDI3MDJiZDcyNGI4NzFjMjVjZDMxOThhOTg2N2VhOGI0MWM2Y2MiLCJ2ZXJzaW9uIjoyLCJzaXplIjozMDI1LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiMWMyYWVmNDk0MDk4ZWY3Y2YzOGZlMzdmOGU0MzM5NGIwNjhhOGVmNzE2YjJkNWQ5ODQxMzkzYjU2MzNmNThjMSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDMwMjIwN2E0OTU4OTY4ZDhhNWQzZjY2MmIxMWM0MmVlZjFlMTk4NTQ0MDJlNjkyMzg3ZDFmM2FmMzBlOWQzZjM2YmU3MjAyMWY3MDBiM2U1MzExZThiOTVlYmM1NDdlOGI3OTIxMDVhZmIwYzQxZWRiY2M2Nzg4MmNlZmFmOTlhNzk3ODI3OFtBTEx8Rk9SS0lEXSAzMDQ1MDIyMTAwODBhZWNiNDU5YWMyNzMzMWRkYmM4OGQyNjAyMWEzY2Q1Mjk2MjVjMDU0NjdiODc3N2U4YTk3Nzc2YTk0Y2JhOTAyMjA2YTk5ZTJmNzZmZjVjZmExZjI4M2IwMTllODQ4NmYxOTdlNGMyNzRmM2JiMjAxMmU5YjU2ZGRjNTY0ZDRmZmRiW0FMTHxGT1JLSURdIDUyMjEwM2I3YjJhNjRlODQ2MjhlNzI1NTcxMTk0NDQ0MjI4YjVmMTZmY2JhOTZjMGM3NjQ5Zjg2OTlhM2M4N2U5NWNkNTgyMTAzY2Y5MDU2OTRkZDEyM2YxNzdiYzlmZWY5ODEwZmJhOGNiYTc4YjRlNDdmZmZlMjA2NmNiY2FiNTk3ZmM2MWZkMzIxMDI0Y2JkMTNmNjY4MzUzZDZiMjkwNTAwNWMyMTQ0N2VhMTVlZmFmNDliNjI0N2ExNjZmOWZjZDkyODI4Y2NhY2Q4NTNhZSIsImhleCI6IjAwNDYzMDQzMDIyMDdhNDk1ODk2OGQ4YTVkM2Y2NjJiMTFjNDJlZWYxZTE5ODU0NDAyZTY5MjM4N2QxZjNhZjMwZTlkM2YzNmJlNzIwMjFmNzAwYjNlNTMxMWU4Yjk1ZWJjNTQ3ZThiNzkyMTA1YWZiMGM0MWVkYmNjNjc4ODJjZWZhZjk5YTc5NzgyNzg0MTQ4MzA0NTAyMjEwMDgwYWVjYjQ1OWFjMjczMzFkZGJjODhkMjYwMjFhM2NkNTI5NjI1YzA1NDY3Yjg3NzdlOGE5Nzc3NmE5NGNiYTkwMjIwNmE5OWUyZjc2ZmY1Y2ZhMWYyODNiMDE5ZTg0ODZmMTk3ZTRjMjc0ZjNiYjIwMTJlOWI1NmRkYzU2NGQ0ZmZkYjQxNGM2OTUyMjEwM2I3YjJhNjRlODQ2MjhlNzI1NTcxMTk0NDQ0MjI4YjVmMTZmY2JhOTZjMGM3NjQ5Zjg2OTlhM2M4N2U5NWNkNTgyMTAzY2Y5MDU2OTRkZDEyM2YxNzdiYzlmZWY5ODEwZmJhOGNiYTc4YjRlNDdmZmZlMjA2NmNiY2FiNTk3ZmM2MWZkMzIxMDI0Y2JkMTNmNjY4MzUzZDZiMjkwNTAwNWMyMTQ0N2VhMTVlZmFmNDliNjI0N2ExNjZmOWZjZDkyODI4Y2NhY2Q4NTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiNDI5MTExNWQ5Yjg5MDJiNjk3N2FiNmU5MjY4MmEwNGRmMzU2Njg3OWUzNDQzZGU5NDFiMDQwZjFiMjhmZDYyMiIsInZvdXQiOjIyMDksInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDQwMjIwMjc5OWEyNDc0YTBhMWMxOGI4MWNmZWRhZDNlMzE2NzUyM2UxNGFkZTdmZjRhODZhZDU5OWEzOTZkZDk2ZDIyMTAyMjA1MTk5Yjk2MTNkNWQwMGUyZWJjOTQ5NTM5ODI2Y2Q4ZjYxN2E3ZjZhZDc5MjVkZTlmNjRhODcyZTkwMjkzYTAzW0FMTHxGT1JLSURdIDMwNDQwMjIwNmViZGEyZmViZjUyNmMzM2U4NzMwYmRhZDZhYTBkMGQ1Y2QxNGVhNjExYjAxNjJiZjM1YTBmNjY0MDQyOTg1ZjAyMjA2ZGQ4ZWI3NDVlMDA1OWU1NjMxOGU5ZGQ5MGEzYTA0ZjZiOWJhY2ZkY2VhNjcwMGVlN2FhMDY5OWFlMDkwNjk5W0FMTHxGT1JLSURdIDUyMjEwMmExOGMzNDhkZWYwYzA2ZTI5MzliYzdjY2FjNzkxOWQ0MjI2NTgxNzhkYmUzZWNmMTFiZmJlNjQyOTJlOTE5N2EyMTAyOTcyNjlkZTJhNDUzZTc2OTE4OTFjOGFlZWFmMjI2ZDlkYmI5NjIyMTdjOWZmZGRhZTYwZmIwMGE2MjEwYTMxOTIxMDMzZGI2NjY1MjYzNGQ2ODM5YzA2NjJhMGE1M2EzZjBiMTQwNTYwZDA0ZTRiZTE2ZjgwMWQzZGVlYjJmNDhjMTczNTNhZSIsImhleCI6IjAwNDczMDQ0MDIyMDI3OTlhMjQ3NGEwYTFjMThiODFjZmVkYWQzZTMxNjc1MjNlMTRhZGU3ZmY0YTg2YWQ1OTlhMzk2ZGQ5NmQyMjEwMjIwNTE5OWI5NjEzZDVkMDBlMmViYzk0OTUzOTgyNmNkOGY2MTdhN2Y2YWQ3OTI1ZGU5ZjY0YTg3MmU5MDI5M2EwMzQxNDczMDQ0MDIyMDZlYmRhMmZlYmY1MjZjMzNlODczMGJkYWQ2YWEwZDBkNWNkMTRlYTYxMWIwMTYyYmYzNWEwZjY2NDA0Mjk4NWYwMjIwNmRkOGViNzQ1ZTAwNTllNTYzMThlOWRkOTBhM2EwNGY2YjliYWNmZGNlYTY3MDBlZTdhYTA2OTlhZTA5MDY5OTQxNGM2OTUyMjEwMmExOGMzNDhkZWYwYzA2ZTI5MzliYzdjY2FjNzkxOWQ0MjI2NTgxNzhkYmUzZWNmMTFiZmJlNjQyOTJlOTE5N2EyMTAyOTcyNjlkZTJhNDUzZTc2OTE4OTFjOGFlZWFmMjI2ZDlkYmI5NjIyMTdjOWZmZGRhZTYwZmIwMGE2MjEwYTMxOTIxMDMzZGI2NjY1MjYzNGQ2ODM5YzA2NjJhMGE1M2EzZjBiMTQwNTYwZDA0ZTRiZTE2ZjgwMWQzZGVlYjJmNDhjMTczNTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiNGRmOTQ5YWJlYjgzNzQ4YzA5ZGViY2YxZDRkZWMzYjI4NmQ3OTMxZGUyZDlkNzIzNWQxNDIxOTY2YWVmY2NlZSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDQwMjIwNzM1YzhiNzFjMjAyOWE2OTFmMjA3ZjY1ZGQwOGIyYjRjYjQzNWNmNmFlMTA4ZTZjMTYxZDc5MjY0ODY3MTJhNDAyMjAyNzdjYTMzM2YyOTA0NjA2MTliODYwNjZkN2Y1OTQxNmUxODQ0ZWEyZjgyYjM4NDM3ZjNhM2E5ZGQ5NjAzNzRlW0FMTHxGT1JLSURdIDMwNDQwMjIwMDM3ZTM4NTAyMzUzNjNkNmRmNThmNTI1ZmNhNDE1Y2M0OTMzMGMwNTQ1MDU4YjM2M2NjN2YwMjFhZGFkNGE1ZjAyMjA0YmI2YWEwYzViMTgzY2UxNWZlMmJkZGNiOTk4NDg3ZTY2ZmZiZDdkMDgzNWFmNjE0NGQ0ZGRmOGI0MzQxNGNhW0FMTHxGT1JLSURdIDUyMjEwM2I3YjJhNjRlODQ2MjhlNzI1NTcxMTk0NDQ0MjI4YjVmMTZmY2JhOTZjMGM3NjQ5Zjg2OTlhM2M4N2U5NWNkNTgyMTAzY2Y5MDU2OTRkZDEyM2YxNzdiYzlmZWY5ODEwZmJhOGNiYTc4YjRlNDdmZmZlMjA2NmNiY2FiNTk3ZmM2MWZkMzIxMDI0Y2JkMTNmNjY4MzUzZDZiMjkwNTAwNWMyMTQ0N2VhMTVlZmFmNDliNjI0N2ExNjZmOWZjZDkyODI4Y2NhY2Q4NTNhZSIsImhleCI6IjAwNDczMDQ0MDIyMDczNWM4YjcxYzIwMjlhNjkxZjIwN2Y2NWRkMDhiMmI0Y2I0MzVjZjZhZTEwOGU2YzE2MWQ3OTI2NDg2NzEyYTQwMjIwMjc3Y2EzMzNmMjkwNDYwNjE5Yjg2MDY2ZDdmNTk0MTZlMTg0NGVhMmY4MmIzODQzN2YzYTNhOWRkOTYwMzc0ZTQxNDczMDQ0MDIyMDAzN2UzODUwMjM1MzYzZDZkZjU4ZjUyNWZjYTQxNWNjNDkzMzBjMDU0NTA1OGIzNjNjYzdmMDIxYWRhZDRhNWYwMjIwNGJiNmFhMGM1YjE4M2NlMTVmZTJiZGRjYjk5ODQ4N2U2NmZmYmQ3ZDA4MzVhZjYxNDRkNGRkZjhiNDM0MTRjYTQxNGM2OTUyMjEwM2I3YjJhNjRlODQ2MjhlNzI1NTcxMTk0NDQ0MjI4YjVmMTZmY2JhOTZjMGM3NjQ5Zjg2OTlhM2M4N2U5NWNkNTgyMTAzY2Y5MDU2OTRkZDEyM2YxNzdiYzlmZWY5ODEwZmJhOGNiYTc4YjRlNDdmZmZlMjA2NmNiY2FiNTk3ZmM2MWZkMzIxMDI0Y2JkMTNmNjY4MzUzZDZiMjkwNTAwNWMyMTQ0N2VhMTVlZmFmNDliNjI0N2ExNjZmOWZjZDkyODI4Y2NhY2Q4NTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiNTFmMzJiMTc5YTE1N2Y4MzI3OWQyY2JkZjMzY2NjZmFkMTY5MmY5ZjlkMGU4NDdhY2E1OGZhMjg4OTUwYmM1NiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDQwMjIwNGI0Mzk5NWU3OWFiNmNkZmJhYTU5OTBhOGM5OTJkNzU3ODBkZmY0OGI3ZjVmZTE1OTYxMGYzYmY4Y2I2MTM5MzAyMjAyNDFmOTU0NTQ1MTkxZmFiN2JmNDg3NDA0ZDMxNTliNmY5ZjI3OWZjOGFmM2JhYmM2MjI1MGZkNDJmMmE2M2EzW0FMTHxGT1JLSURdIDMwNDUwMjIxMDBiYzU3Y2E1NWI0MTg3Mzk2YjY3NDk0OTE0NDBjNTE3Mzg2ZTA5ODg0M2FmMTdiNTEzOGRjNTdhNTEwN2NjYmRmMDIyMDEzOWEyYjVjNmI5Y2U1ZWY4OWRkMWJkMDcyMjYwYjIwNDQ4NWVmOWMyMWNlNjVkMzI5N2ZlMjliMGY2M2FlOThbQUxMfEZPUktJRF0gNTIyMTAzZWFjNGEyNjYwZDk1YjU4ZThjOWE3MTRhOGE1MzdiMGZmMThjMTIxMWQ2YjFlYzllZWY5ZGFhNjAzNDQyMDc1NDIxMDM1N2M1MjE4YmMwZjk1OWQ4ZDdmMjU0ZTEzYTI0ZmJhMmY1NmQzOThmM2I1ZWMzN2M5YTY1YTYzZDBkNGE5OGJjMjEwMzkyMjRjM2Y0ZTg3YmMxMTEyYzY1Y2Y4MDg5OGYxYTU1NjYxNTIzMDAwYjRkNDJiZTRhZTdiYTY0NDU4ODJmMGU1M2FlIiwiaGV4IjoiMDA0NzMwNDQwMjIwNGI0Mzk5NWU3OWFiNmNkZmJhYTU5OTBhOGM5OTJkNzU3ODBkZmY0OGI3ZjVmZTE1OTYxMGYzYmY4Y2I2MTM5MzAyMjAyNDFmOTU0NTQ1MTkxZmFiN2JmNDg3NDA0ZDMxNTliNmY5ZjI3OWZjOGFmM2JhYmM2MjI1MGZkNDJmMmE2M2EzNDE0ODMwNDUwMjIxMDBiYzU3Y2E1NWI0MTg3Mzk2YjY3NDk0OTE0NDBjNTE3Mzg2ZTA5ODg0M2FmMTdiNTEzOGRjNTdhNTEwN2NjYmRmMDIyMDEzOWEyYjVjNmI5Y2U1ZWY4OWRkMWJkMDcyMjYwYjIwNDQ4NWVmOWMyMWNlNjVkMzI5N2ZlMjliMGY2M2FlOTg0MTRjNjk1MjIxMDNlYWM0YTI2NjBkOTViNThlOGM5YTcxNGE4YTUzN2IwZmYxOGMxMjExZDZiMWVjOWVlZjlkYWE2MDM0NDIwNzU0MjEwMzU3YzUyMThiYzBmOTU5ZDhkN2YyNTRlMTNhMjRmYmEyZjU2ZDM5OGYzYjVlYzM3YzlhNjVhNjNkMGQ0YTk4YmMyMTAzOTIyNGMzZjRlODdiYzExMTJjNjVjZjgwODk4ZjFhNTU2NjE1MjMwMDBiNGQ0MmJlNGFlN2JhNjQ0NTg4MmYwZTUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9LHsidHhpZCI6IjUyNjhhY2U0MzZmNWI0NzdkNmMzNTlkMmIxMjliYWY0MTRmMmExNTgzODdkNTA1NWI1NjBiMmQyZTM2NDI0ZmYiLCJ2b3V0IjoxOTc3LCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ0MDIyMDU0MzQyNGVmYTU5ZDY5OWFhM2Q0NmY4MWRhMTNlZTJjZGNjNGViZmVjYzJjNjNlNzg2YjBlNGE3YzBlY2VjYjgwMjIwMjIwZjEzYTIyNmYyMDkzMDVjMjUyMzNiMTY4YWIyMzgxMTllNTBkNmRiMWJmYTM0MjFhNjc4Y2M0Zjk3NzFiM1tBTEx8Rk9SS0lEXSAzMDQ1MDIyMTAwZDU3ZmUxODUxYTcwMTU2ZTMyZmEwMjVmNjYwODg4ZWE5ZTYzYmZlYTZiMDcxOGFiMzkzNDY0ZDc3ZWQzMDNlZTAyMjA3MTcxY2FkOTUxMjI4Yjg4ZjY1NTJhNDg0OGVkZjZkODNhNmE0YmUzYzE2Yzc2ZDY2ZmIwMDRlNmM0Njc3ZTAzW0FMTHxGT1JLSURdIDUyMjEwMmExOGMzNDhkZWYwYzA2ZTI5MzliYzdjY2FjNzkxOWQ0MjI2NTgxNzhkYmUzZWNmMTFiZmJlNjQyOTJlOTE5N2EyMTAyOTcyNjlkZTJhNDUzZTc2OTE4OTFjOGFlZWFmMjI2ZDlkYmI5NjIyMTdjOWZmZGRhZTYwZmIwMGE2MjEwYTMxOTIxMDMzZGI2NjY1MjYzNGQ2ODM5YzA2NjJhMGE1M2EzZjBiMTQwNTYwZDA0ZTRiZTE2ZjgwMWQzZGVlYjJmNDhjMTczNTNhZSIsImhleCI6IjAwNDczMDQ0MDIyMDU0MzQyNGVmYTU5ZDY5OWFhM2Q0NmY4MWRhMTNlZTJjZGNjNGViZmVjYzJjNjNlNzg2YjBlNGE3YzBlY2VjYjgwMjIwMjIwZjEzYTIyNmYyMDkzMDVjMjUyMzNiMTY4YWIyMzgxMTllNTBkNmRiMWJmYTM0MjFhNjc4Y2M0Zjk3NzFiMzQxNDgzMDQ1MDIyMTAwZDU3ZmUxODUxYTcwMTU2ZTMyZmEwMjVmNjYwODg4ZWE5ZTYzYmZlYTZiMDcxOGFiMzkzNDY0ZDc3ZWQzMDNlZTAyMjA3MTcxY2FkOTUxMjI4Yjg4ZjY1NTJhNDg0OGVkZjZkODNhNmE0YmUzYzE2Yzc2ZDY2ZmIwMDRlNmM0Njc3ZTAzNDE0YzY5NTIyMTAyYTE4YzM0OGRlZjBjMDZlMjkzOWJjN2NjYWM3OTE5ZDQyMjY1ODE3OGRiZTNlY2YxMWJmYmU2NDI5MmU5MTk3YTIxMDI5NzI2OWRlMmE0NTNlNzY5MTg5MWM4YWVlYWYyMjZkOWRiYjk2MjIxN2M5ZmZkZGFlNjBmYjAwYTYyMTBhMzE5MjEwMzNkYjY2NjUyNjM0ZDY4MzljMDY2MmEwYTUzYTNmMGIxNDA1NjBkMDRlNGJlMTZmODAxZDNkZWViMmY0OGMxNzM1M2FlIn0sInNlcXVlbmNlIjo0Mjk0OTY3MjkzfSx7InR4aWQiOiJjZjYxMTMyNzg2ZDc2MTIyMjEyMjBlYzg5NTBhNjFiNGVkNDcxZmQ1NGYyMTRkZjFiMDk1ODgwNTY3MTU1YTk2Iiwidm91dCI6MTI4Niwic2NyaXB0U2lnIjp7ImFzbSI6IjAgMzA0NDAyMjA2MGQzZDkzZDQwYzdlOWViOTQ5ZWM4ODcwNDBmMzZjNTkxMjliNzE3NGI5NDM2OTk1MjJiOTUwMDgxZDRmMDRkMDIyMDZjNWRlMjEzYzEzOTQ1Njk4NzJkMmUzZGM4MjIzZmUxY2EzYzVhZDZiYmE4YzYzZWU0MzMwODg3MzNhYjQyODNbQUxMfEZPUktJRF0gMzA0NTAyMjEwMDllMjljMDljYjc1YzZkYjkzOWJhMjExOWIwM2U5NjkzYzI1NjY2N2FhMGU5ODEwNTdmY2FkNjMwNjBiZDA1MTAwMjIwM2ZkYjZlYWU4OTY4NDMwY2ZkNjczNTExMzk1MWVjM2JlYTg2ZTI3N2NhMmM1YThmNzU2MTM1MGIwZTA0N2FkZFtBTEx8Rk9SS0lEXSA1MjIxMDJhMThjMzQ4ZGVmMGMwNmUyOTM5YmM3Y2NhYzc5MTlkNDIyNjU4MTc4ZGJlM2VjZjExYmZiZTY0MjkyZTkxOTdhMjEwMjk3MjY5ZGUyYTQ1M2U3NjkxODkxYzhhZWVhZjIyNmQ5ZGJiOTYyMjE3YzlmZmRkYWU2MGZiMDBhNjIxMGEzMTkyMTAzM2RiNjY2NTI2MzRkNjgzOWMwNjYyYTBhNTNhM2YwYjE0MDU2MGQwNGU0YmUxNmY4MDFkM2RlZWIyZjQ4YzE3MzUzYWUiLCJoZXgiOiIwMDQ3MzA0NDAyMjA2MGQzZDkzZDQwYzdlOWViOTQ5ZWM4ODcwNDBmMzZjNTkxMjliNzE3NGI5NDM2OTk1MjJiOTUwMDgxZDRmMDRkMDIyMDZjNWRlMjEzYzEzOTQ1Njk4NzJkMmUzZGM4MjIzZmUxY2EzYzVhZDZiYmE4YzYzZWU0MzMwODg3MzNhYjQyODM0MTQ4MzA0NTAyMjEwMDllMjljMDljYjc1YzZkYjkzOWJhMjExOWIwM2U5NjkzYzI1NjY2N2FhMGU5ODEwNTdmY2FkNjMwNjBiZDA1MTAwMjIwM2ZkYjZlYWU4OTY4NDMwY2ZkNjczNTExMzk1MWVjM2JlYTg2ZTI3N2NhMmM1YThmNzU2MTM1MGIwZTA0N2FkZDQxNGM2OTUyMjEwMmExOGMzNDhkZWYwYzA2ZTI5MzliYzdjY2FjNzkxOWQ0MjI2NTgxNzhkYmUzZWNmMTFiZmJlNjQyOTJlOTE5N2EyMTAyOTcyNjlkZTJhNDUzZTc2OTE4OTFjOGFlZWFmMjI2ZDlkYmI5NjIyMTdjOWZmZGRhZTYwZmIwMGE2MjEwYTMxOTIxMDMzZGI2NjY1MjYzNGQ2ODM5YzA2NjJhMGE1M2EzZjBiMTQwNTYwZDA0ZTRiZTE2ZjgwMWQzZGVlYjJmNDhjMTczNTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiZTA3ZGY0NzQyZWZlYmE5NDdkNmE4ZDQ2ZDBmNTg3MmRhODBjZTA0OWEyZGE0MmQ4ZjZjMjZhOTBlM2FmNmVhMiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDUwMjIxMDBlNTJhOGRlODViZTI1MWMzOGE0MjExZDBmMmQzNWM4OGE2NzdhNGYxNDU4NWViODZiZDczMDdkMDYzZDg0ZTliMDIyMDIyOGZmOGUwNjVjMDE5YTVmNzFjMmUxNzhjOGQ2YjAwNzBhMGFiMWM0MTAzNTNlYTcyZmMwYzk0YjAzOTM0YmNbQUxMfEZPUktJRF0gMzA0NDAyMjA1YjNkMmIzNjA4OWRiZjk4NWNhOTM0ZTVkM2FkZGIzNDJkODAxM2JjY2E3YTE1ZWM0MGM2MDczOGUwNjI2NzQzMDIyMDA3ZjdhZTliNjhiZTNmMjVlZjNlMDQyMGU2YmUxMDUyZmY1MGQwZTZiYmMzNmUzOTE2YzgwYmQ1ODBlNjkyNDFbQUxMfEZPUktJRF0gNTIyMTAzZWFjNGEyNjYwZDk1YjU4ZThjOWE3MTRhOGE1MzdiMGZmMThjMTIxMWQ2YjFlYzllZWY5ZGFhNjAzNDQyMDc1NDIxMDM1N2M1MjE4YmMwZjk1OWQ4ZDdmMjU0ZTEzYTI0ZmJhMmY1NmQzOThmM2I1ZWMzN2M5YTY1YTYzZDBkNGE5OGJjMjEwMzkyMjRjM2Y0ZTg3YmMxMTEyYzY1Y2Y4MDg5OGYxYTU1NjYxNTIzMDAwYjRkNDJiZTRhZTdiYTY0NDU4ODJmMGU1M2FlIiwiaGV4IjoiMDA0ODMwNDUwMjIxMDBlNTJhOGRlODViZTI1MWMzOGE0MjExZDBmMmQzNWM4OGE2NzdhNGYxNDU4NWViODZiZDczMDdkMDYzZDg0ZTliMDIyMDIyOGZmOGUwNjVjMDE5YTVmNzFjMmUxNzhjOGQ2YjAwNzBhMGFiMWM0MTAzNTNlYTcyZmMwYzk0YjAzOTM0YmM0MTQ3MzA0NDAyMjA1YjNkMmIzNjA4OWRiZjk4NWNhOTM0ZTVkM2FkZGIzNDJkODAxM2JjY2E3YTE1ZWM0MGM2MDczOGUwNjI2NzQzMDIyMDA3ZjdhZTliNjhiZTNmMjVlZjNlMDQyMGU2YmUxMDUyZmY1MGQwZTZiYmMzNmUzOTE2YzgwYmQ1ODBlNjkyNDE0MTRjNjk1MjIxMDNlYWM0YTI2NjBkOTViNThlOGM5YTcxNGE4YTUzN2IwZmYxOGMxMjExZDZiMWVjOWVlZjlkYWE2MDM0NDIwNzU0MjEwMzU3YzUyMThiYzBmOTU5ZDhkN2YyNTRlMTNhMjRmYmEyZjU2ZDM5OGYzYjVlYzM3YzlhNjVhNjNkMGQ0YTk4YmMyMTAzOTIyNGMzZjRlODdiYzExMTJjNjVjZjgwODk4ZjFhNTU2NjE1MjMwMDBiNGQ0MmJlNGFlN2JhNjQ0NTg4MmYwZTUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9LHsidHhpZCI6ImU3M2EyNTY1ZWYwOTdmODlmMjUyNTQ2NmVmNDEzMDZmNTM0ODY5MzE4OWI1MDFmYmRmMTAwNTYxZDZkZTUzOTkiLCJ2b3V0IjoyMzI5LCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ0MDIyMDUyYThkYTQwZTViOWMwZmQ0OWQ0NDYwYzhjMmMwZjc5NTA2YTBhMTdmN2U3MzhlNmMzNzgyOTU2MTc0ODQ2NDQwMjIwNTIwNzVjNWM3OTg1MDlmZTc1ZjI1MGVlYjY4MTI5ZDk2NzVkY2Q1MzFmZDBmNWJmZGI2ZWE5YzU4ODc0ZDEwNltBTEx8Rk9SS0lEXSAzMDQ0MDIyMDViZDQwMDA0ZTcyMzljODU1OTY3NThhMDQ3ODg4OTExOTJiZWQ5NDRjYTU3YmNiNDZlMTA0NzYyYjMyYWNiZGQwMjIwN2Y3ZDVmNTEzMGZlZDc0NjRlNWE4NzZlMzcyMWRlMzAxYzQ4Zjg4MzFmM2Y5N2JlYmJlZWI5YTgzMDQxZmYxYltBTEx8Rk9SS0lEXSA1MjIxMDJhMThjMzQ4ZGVmMGMwNmUyOTM5YmM3Y2NhYzc5MTlkNDIyNjU4MTc4ZGJlM2VjZjExYmZiZTY0MjkyZTkxOTdhMjEwMjk3MjY5ZGUyYTQ1M2U3NjkxODkxYzhhZWVhZjIyNmQ5ZGJiOTYyMjE3YzlmZmRkYWU2MGZiMDBhNjIxMGEzMTkyMTAzM2RiNjY2NTI2MzRkNjgzOWMwNjYyYTBhNTNhM2YwYjE0MDU2MGQwNGU0YmUxNmY4MDFkM2RlZWIyZjQ4YzE3MzUzYWUiLCJoZXgiOiIwMDQ3MzA0NDAyMjA1MmE4ZGE0MGU1YjljMGZkNDlkNDQ2MGM4YzJjMGY3OTUwNmEwYTE3ZjdlNzM4ZTZjMzc4Mjk1NjE3NDg0NjQ0MDIyMDUyMDc1YzVjNzk4NTA5ZmU3NWYyNTBlZWI2ODEyOWQ5Njc1ZGNkNTMxZmQwZjViZmRiNmVhOWM1ODg3NGQxMDY0MTQ3MzA0NDAyMjA1YmQ0MDAwNGU3MjM5Yzg1NTk2NzU4YTA0Nzg4ODkxMTkyYmVkOTQ0Y2E1N2JjYjQ2ZTEwNDc2MmIzMmFjYmRkMDIyMDdmN2Q1ZjUxMzBmZWQ3NDY0ZTVhODc2ZTM3MjFkZTMwMWM0OGY4ODMxZjNmOTdiZWJiZWViOWE4MzA0MWZmMWI0MTRjNjk1MjIxMDJhMThjMzQ4ZGVmMGMwNmUyOTM5YmM3Y2NhYzc5MTlkNDIyNjU4MTc4ZGJlM2VjZjExYmZiZTY0MjkyZTkxOTdhMjEwMjk3MjY5ZGUyYTQ1M2U3NjkxODkxYzhhZWVhZjIyNmQ5ZGJiOTYyMjE3YzlmZmRkYWU2MGZiMDBhNjIxMGEzMTkyMTAzM2RiNjY2NTI2MzRkNjgzOWMwNjYyYTBhNTNhM2YwYjE0MDU2MGQwNGU0YmUxNmY4MDFkM2RlZWIyZjQ4YzE3MzUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9LHsidHhpZCI6ImU5OGMwM2NiYWFmYWQ4NTllNDQ2ZjI2MDZlNTQ0ZGQzNmE4MTFiODg0Mzc3Njc0ZjlmY2M3ZjM4ZTM4YWQ4ODEiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ0MDIyMDI3OWYwZTVlNzIwZjEwMGZmYTMwMWMwOTBhZDRmMjg4ZjcwZjRmNzFiZDNkODQ2Y2Y3NjgzMjc0YmRhYTMzYTkwMjIwMDQ2ODYxMDgzNzRjOGI2YmI0NmM4NTBhNDJkY2NkZGM3NDBiZjY3ZGU1N2Q3NzgzYjM4Zjk2MjZjYjYyMTlmM1tBTEx8Rk9SS0lEXSAzMDQ1MDIyMTAwZjYzMTQxMWI3NTRlZjcxZTgzNTJiNGU4N2YyODM4M2NiNjJhZGIyNWRiMjEyZjNmYjVmOGEyZTM2ZjhiYjMyMjAyMjA0NDZiNGE3MzYwNDI5NDFkZWE4YjRhZWMwMmI4ZWE4Nzc0NGI1YTEwYzZiODYyZmIxMTA1MWRiMDU4OWU4Y2I3W0FMTHxGT1JLSURdIDUyMjEwMmMwZGVmOTEwOTg1NzJkMDE5YTZlOWE2ZmY0YTU1ZmRlOTNiZDc3OTI3MTdjM2Q0OGViNzk1ZTczYzkxZDJhOTgyMTAyMjU4MGQ1NDkyNGZlMGQxYmZiNjVmM2IzYTNhZTFmNjcwOTdlYzQzYjlmZTcyYTAwZDdiYWI2OTE5ZjVlOTc3NzIxMDMyZDE3MDc3NmI1Y2E2MWNmNDI4NDQ1M2I3YjM4ZmY0MTE3MGIxYWFhNjUxMDBkNDFjNmQyZGE0MmFmZWVhNzVjNTNhZSIsImhleCI6IjAwNDczMDQ0MDIyMDI3OWYwZTVlNzIwZjEwMGZmYTMwMWMwOTBhZDRmMjg4ZjcwZjRmNzFiZDNkODQ2Y2Y3NjgzMjc0YmRhYTMzYTkwMjIwMDQ2ODYxMDgzNzRjOGI2YmI0NmM4NTBhNDJkY2NkZGM3NDBiZjY3ZGU1N2Q3NzgzYjM4Zjk2MjZjYjYyMTlmMzQxNDgzMDQ1MDIyMTAwZjYzMTQxMWI3NTRlZjcxZTgzNTJiNGU4N2YyODM4M2NiNjJhZGIyNWRiMjEyZjNmYjVmOGEyZTM2ZjhiYjMyMjAyMjA0NDZiNGE3MzYwNDI5NDFkZWE4YjRhZWMwMmI4ZWE4Nzc0NGI1YTEwYzZiODYyZmIxMTA1MWRiMDU4OWU4Y2I3NDE0YzY5NTIyMTAyYzBkZWY5MTA5ODU3MmQwMTlhNmU5YTZmZjRhNTVmZGU5M2JkNzc5MjcxN2MzZDQ4ZWI3OTVlNzNjOTFkMmE5ODIxMDIyNTgwZDU0OTI0ZmUwZDFiZmI2NWYzYjNhM2FlMWY2NzA5N2VjNDNiOWZlNzJhMDBkN2JhYjY5MTlmNWU5Nzc3MjEwMzJkMTcwNzc2YjVjYTYxY2Y0Mjg0NDUzYjdiMzhmZjQxMTcwYjFhYWE2NTEwMGQ0MWM2ZDJkYTQyYWZlZWE3NWM1M2FlIn0sInNlcXVlbmNlIjo0Mjk0OTY3MjkzfSx7InR4aWQiOiJmZTgwNjQyZGEyZGZiMGY1ZGY5OWJiYjkxNzE5MjQ3MjBiYTQ5NzRlZDNkMjQzMGYyODVjMWY2YmI1NDc3ZGEwIiwidm91dCI6NzgxLCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ1MDIyMTAwYTFkZGJkY2U1YTBmNzIyNWU1YmFmMTMzYjJmMjU3M2JmZDQxYWM5MmI0ZDYyMGYxMjg0ZjVjNjEyODhiYzE2MzAyMjA1NTgxODMwZGU4ZDBjMjQzYzAwYTI4NTRiY2QyOWMyMDkzZjM4MzkxNmNlYjk3YTY4NzBiZGNkZDEwZjdmY2E2W0FMTHxGT1JLSURdIDMwNDUwMjIxMDBiNjIzZDRkM2YxODdhOTU3Y2U0NGNiNTZjYjRiNmQwZjk3YjVhMzJhZGZlMmViMWIwOWYxNDU5ZTdmMzliMGQyMDIyMDFlN2E2NjljMTlmMDc5ZjQzZGZhZDhkOTZhZDMxNDdhYTlmMDQwY2U2NjlkZDUyZjM4NDAzNjhiOTdjZTNkMDBbQUxMfEZPUktJRF0gNTIyMTAzYmI5MDMxY2ZkMTk3NzUwODZlZDA2YmY2OWI4OGFiYjNiNzdiNmI5OWUzZDM5Zjg4MTdhMDM5NTJjNTQxMjVlYzIxMDI3ODNiNmY3MzdiZTdkZThhZGM0MDE1OTI4ZGU3ZDhiZmMzMzI3NWRmODk5ZjczY2I4NGE5ZWZiZThkNGZhZGMyMjEwM2MyMDU1NTUzODFlMDExYzdlNTI3NzVkOTljYWQwNGE4Mzk1NDliOTI5OTQ3Y2VjYzlkYzJjZTZkNGRhOWFiZTg1M2FlIiwiaGV4IjoiMDA0ODMwNDUwMjIxMDBhMWRkYmRjZTVhMGY3MjI1ZTViYWYxMzNiMmYyNTczYmZkNDFhYzkyYjRkNjIwZjEyODRmNWM2MTI4OGJjMTYzMDIyMDU1ODE4MzBkZThkMGMyNDNjMDBhMjg1NGJjZDI5YzIwOTNmMzgzOTE2Y2ViOTdhNjg3MGJkY2RkMTBmN2ZjYTY0MTQ4MzA0NTAyMjEwMGI2MjNkNGQzZjE4N2E5NTdjZTQ0Y2I1NmNiNGI2ZDBmOTdiNWEzMmFkZmUyZWIxYjA5ZjE0NTllN2YzOWIwZDIwMjIwMWU3YTY2OWMxOWYwNzlmNDNkZmFkOGQ5NmFkMzE0N2FhOWYwNDBjZTY2OWRkNTJmMzg0MDM2OGI5N2NlM2QwMDQxNGM2OTUyMjEwM2JiOTAzMWNmZDE5Nzc1MDg2ZWQwNmJmNjliODhhYmIzYjc3YjZiOTllM2QzOWY4ODE3YTAzOTUyYzU0MTI1ZWMyMTAyNzgzYjZmNzM3YmU3ZGU4YWRjNDAxNTkyOGRlN2Q4YmZjMzMyNzVkZjg5OWY3M2NiODRhOWVmYmU4ZDRmYWRjMjIxMDNjMjA1NTU1MzgxZTAxMWM3ZTUyNzc1ZDk5Y2FkMDRhODM5NTQ5YjkyOTk0N2NlY2M5ZGMyY2U2ZDRkYTlhYmU4NTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M31dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDRmMDM3NTRlZjdlYmZiNTY1ZjRkYTRmZjQwNDBmZmE4NDNkZTIwZDggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0ZjAzNzU0ZWY3ZWJmYjU2NWY0ZGE0ZmY0MDQwZmZhODQzZGUyMGQ4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwOHN4YTJ3N2w0bGs0amxma2owN3N6cWw3NXk4aDNxbXF1Z25sMm5wbCJdfX0seyJ2YWx1ZSI6MC4wMTQ4NzA0NSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gxNjAgYjBmMmNmODAxM2MwMmE2ZWUwNzliYjJkZTllYjhlNmM5Y2NiOWI3NyBPUF9FUVVBTCIsImhleCI6ImE5MTRiMGYyY2Y4MDEzYzAyYTZlZTA3OWJiMmRlOWViOGU2YzljY2I5Yjc3ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwemMwOW51cXowcXo1bWhxMHhham02MHQzZWtmZWp1bXd1NGs5NWZ2ajYiXX19XSwiZmVlIjo2LjExMmUtMDUsImhleCI6IjAyMDAwMDAwMGFjMTU4M2Y2M2I1OTMxMzg0ZDlkNWIyMTZmNzhlOGEwNjRiMzk0MzhlN2ZlMzhmZjM3Y2VmOTg0MDQ5ZWYyYTFjMDAwMDAwMDBmYzAwNDYzMDQzMDIyMDdhNDk1ODk2OGQ4YTVkM2Y2NjJiMTFjNDJlZWYxZTE5ODU0NDAyZTY5MjM4N2QxZjNhZjMwZTlkM2YzNmJlNzIwMjFmNzAwYjNlNTMxMWU4Yjk1ZWJjNTQ3ZThiNzkyMTA1YWZiMGM0MWVkYmNjNjc4ODJjZWZhZjk5YTc5NzgyNzg0MTQ4MzA0NTAyMjEwMDgwYWVjYjQ1OWFjMjczMzFkZGJjODhkMjYwMjFhM2NkNTI5NjI1YzA1NDY3Yjg3NzdlOGE5Nzc3NmE5NGNiYTkwMjIwNmE5OWUyZjc2ZmY1Y2ZhMWYyODNiMDE5ZTg0ODZmMTk3ZTRjMjc0ZjNiYjIwMTJlOWI1NmRkYzU2NGQ0ZmZkYjQxNGM2OTUyMjEwM2I3YjJhNjRlODQ2MjhlNzI1NTcxMTk0NDQ0MjI4YjVmMTZmY2JhOTZjMGM3NjQ5Zjg2OTlhM2M4N2U5NWNkNTgyMTAzY2Y5MDU2OTRkZDEyM2YxNzdiYzlmZWY5ODEwZmJhOGNiYTc4YjRlNDdmZmZlMjA2NmNiY2FiNTk3ZmM2MWZkMzIxMDI0Y2JkMTNmNjY4MzUzZDZiMjkwNTAwNWMyMTQ0N2VhMTVlZmFmNDliNjI0N2ExNjZmOWZjZDkyODI4Y2NhY2Q4NTNhZWZkZmZmZmZmMjJkNjhmYjJmMTQwYjA0MWU5M2Q0NGUzNzk2ODU2ZjM0ZGEwODIyNmU5YjY3YTk3YjYwMjg5OWI1ZDExOTE0MmExMDgwMDAwZmMwMDQ3MzA0NDAyMjAyNzk5YTI0NzRhMGExYzE4YjgxY2ZlZGFkM2UzMTY3NTIzZTE0YWRlN2ZmNGE4NmFkNTk5YTM5NmRkOTZkMjIxMDIyMDUxOTliOTYxM2Q1ZDAwZTJlYmM5NDk1Mzk4MjZjZDhmNjE3YTdmNmFkNzkyNWRlOWY2NGE4NzJlOTAyOTNhMDM0MTQ3MzA0NDAyMjA2ZWJkYTJmZWJmNTI2YzMzZTg3MzBiZGFkNmFhMGQwZDVjZDE0ZWE2MTFiMDE2MmJmMzVhMGY2NjQwNDI5ODVmMDIyMDZkZDhlYjc0NWUwMDU5ZTU2MzE4ZTlkZDkwYTNhMDRmNmI5YmFjZmRjZWE2NzAwZWU3YWEwNjk5YWUwOTA2OTk0MTRjNjk1MjIxMDJhMThjMzQ4ZGVmMGMwNmUyOTM5YmM3Y2NhYzc5MTlkNDIyNjU4MTc4ZGJlM2VjZjExYmZiZTY0MjkyZTkxOTdhMjEwMjk3MjY5ZGUyYTQ1M2U3NjkxODkxYzhhZWVhZjIyNmQ5ZGJiOTYyMjE3YzlmZmRkYWU2MGZiMDBhNjIxMGEzMTkyMTAzM2RiNjY2NTI2MzRkNjgzOWMwNjYyYTBhNTNhM2YwYjE0MDU2MGQwNGU0YmUxNmY4MDFkM2RlZWIyZjQ4YzE3MzUzYWVmZGZmZmZmZmVlY2NlZjZhOTYyMTE0NWQyM2Q3ZDllMjFkOTNkNzg2YjJjM2RlZDRmMWJjZGUwOThjNzQ4M2ViYWI0OWY5NGQwMDAwMDAwMGZjMDA0NzMwNDQwMjIwNzM1YzhiNzFjMjAyOWE2OTFmMjA3ZjY1ZGQwOGIyYjRjYjQzNWNmNmFlMTA4ZTZjMTYxZDc5MjY0ODY3MTJhNDAyMjAyNzdjYTMzM2YyOTA0NjA2MTliODYwNjZkN2Y1OTQxNmUxODQ0ZWEyZjgyYjM4NDM3ZjNhM2E5ZGQ5NjAzNzRlNDE0NzMwNDQwMjIwMDM3ZTM4NTAyMzUzNjNkNmRmNThmNTI1ZmNhNDE1Y2M0OTMzMGMwNTQ1MDU4YjM2M2NjN2YwMjFhZGFkNGE1ZjAyMjA0YmI2YWEwYzViMTgzY2UxNWZlMmJkZGNiOTk4NDg3ZTY2ZmZiZDdkMDgzNWFmNjE0NGQ0ZGRmOGI0MzQxNGNhNDE0YzY5NTIyMTAzYjdiMmE2NGU4NDYyOGU3MjU1NzExOTQ0NDQyMjhiNWYxNmZjYmE5NmMwYzc2NDlmODY5OWEzYzg3ZTk1Y2Q1ODIxMDNjZjkwNTY5NGRkMTIzZjE3N2JjOWZlZjk4MTBmYmE4Y2JhNzhiNGU0N2ZmZmUyMDY2Y2JjYWI1OTdmYzYxZmQzMjEwMjRjYmQxM2Y2NjgzNTNkNmIyOTA1MDA1YzIxNDQ3ZWExNWVmYWY0OWI2MjQ3YTE2NmY5ZmNkOTI4MjhjY2FjZDg1M2FlZmRmZmZmZmY1NmJjNTA4OTI4ZmE1OGNhN2E4NDBlOWQ5ZjJmNjlkMWZhY2MzY2YzYmQyYzlkMjc4MzdmMTU5YTE3MmJmMzUxMDAwMDAwMDBmZGZkMDAwMDQ3MzA0NDAyMjA0YjQzOTk1ZTc5YWI2Y2RmYmFhNTk5MGE4Yzk5MmQ3NTc4MGRmZjQ4YjdmNWZlMTU5NjEwZjNiZjhjYjYxMzkzMDIyMDI0MWY5NTQ1NDUxOTFmYWI3YmY0ODc0MDRkMzE1OWI2ZjlmMjc5ZmM4YWYzYmFiYzYyMjUwZmQ0MmYyYTYzYTM0MTQ4MzA0NTAyMjEwMGJjNTdjYTU1YjQxODczOTZiNjc0OTQ5MTQ0MGM1MTczODZlMDk4ODQzYWYxN2I1MTM4ZGM1N2E1MTA3Y2NiZGYwMjIwMTM5YTJiNWM2YjljZTVlZjg5ZGQxYmQwNzIyNjBiMjA0NDg1ZWY5YzIxY2U2NWQzMjk3ZmUyOWIwZjYzYWU5ODQxNGM2OTUyMjEwM2VhYzRhMjY2MGQ5NWI1OGU4YzlhNzE0YThhNTM3YjBmZjE4YzEyMTFkNmIxZWM5ZWVmOWRhYTYwMzQ0MjA3NTQyMTAzNTdjNTIxOGJjMGY5NTlkOGQ3ZjI1NGUxM2EyNGZiYTJmNTZkMzk4ZjNiNWVjMzdjOWE2NWE2M2QwZDRhOThiYzIxMDM5MjI0YzNmNGU4N2JjMTExMmM2NWNmODA4OThmMWE1NTY2MTUyMzAwMGI0ZDQyYmU0YWU3YmE2NDQ1ODgyZjBlNTNhZWZkZmZmZmZmZmYyNDY0ZTNkMmIyNjBiNTU1NTA3ZDM4NThhMWYyMTRmNGJhMjliMWQyNTljM2Q2NzdiNGY1MzZlNGFjNjg1MmI5MDcwMDAwZmRmZDAwMDA0NzMwNDQwMjIwNTQzNDI0ZWZhNTlkNjk5YWEzZDQ2ZjgxZGExM2VlMmNkY2M0ZWJmZWNjMmM2M2U3ODZiMGU0YTdjMGVjZWNiODAyMjAyMjBmMTNhMjI2ZjIwOTMwNWMyNTIzM2IxNjhhYjIzODExOWU1MGQ2ZGIxYmZhMzQyMWE2NzhjYzRmOTc3MWIzNDE0ODMwNDUwMjIxMDBkNTdmZTE4NTFhNzAxNTZlMzJmYTAyNWY2NjA4ODhlYTllNjNiZmVhNmIwNzE4YWIzOTM0NjRkNzdlZDMwM2VlMDIyMDcxNzFjYWQ5NTEyMjhiODhmNjU1MmE0ODQ4ZWRmNmQ4M2E2YTRiZTNjMTZjNzZkNjZmYjAwNGU2YzQ2NzdlMDM0MTRjNjk1MjIxMDJhMThjMzQ4ZGVmMGMwNmUyOTM5YmM3Y2NhYzc5MTlkNDIyNjU4MTc4ZGJlM2VjZjExYmZiZTY0MjkyZTkxOTdhMjEwMjk3MjY5ZGUyYTQ1M2U3NjkxODkxYzhhZWVhZjIyNmQ5ZGJiOTYyMjE3YzlmZmRkYWU2MGZiMDBhNjIxMGEzMTkyMTAzM2RiNjY2NTI2MzRkNjgzOWMwNjYyYTBhNTNhM2YwYjE0MDU2MGQwNGU0YmUxNmY4MDFkM2RlZWIyZjQ4YzE3MzUzYWVmZGZmZmZmZjk2NWExNTY3MDU4ODk1YjBmMTRkMjE0ZmQ1MWY0N2VkYjQ2MTBhOTVjODBlMjIyMTIyNjFkNzg2MjcxMzYxY2YwNjA1MDAwMGZkZmQwMDAwNDczMDQ0MDIyMDYwZDNkOTNkNDBjN2U5ZWI5NDllYzg4NzA0MGYzNmM1OTEyOWI3MTc0Yjk0MzY5OTUyMmI5NTAwODFkNGYwNGQwMjIwNmM1ZGUyMTNjMTM5NDU2OTg3MmQyZTNkYzgyMjNmZTFjYTNjNWFkNmJiYThjNjNlZTQzMzA4ODczM2FiNDI4MzQxNDgzMDQ1MDIyMTAwOWUyOWMwOWNiNzVjNmRiOTM5YmEyMTE5YjAzZTk2OTNjMjU2NjY3YWEwZTk4MTA1N2ZjYWQ2MzA2MGJkMDUxMDAyMjAzZmRiNmVhZTg5Njg0MzBjZmQ2NzM1MTEzOTUxZWMzYmVhODZlMjc3Y2EyYzVhOGY3NTYxMzUwYjBlMDQ3YWRkNDE0YzY5NTIyMTAyYTE4YzM0OGRlZjBjMDZlMjkzOWJjN2NjYWM3OTE5ZDQyMjY1ODE3OGRiZTNlY2YxMWJmYmU2NDI5MmU5MTk3YTIxMDI5NzI2OWRlMmE0NTNlNzY5MTg5MWM4YWVlYWYyMjZkOWRiYjk2MjIxN2M5ZmZkZGFlNjBmYjAwYTYyMTBhMzE5MjEwMzNkYjY2NjUyNjM0ZDY4MzljMDY2MmEwYTUzYTNmMGIxNDA1NjBkMDRlNGJlMTZmODAxZDNkZWViMmY0OGMxNzM1M2FlZmRmZmZmZmZhMjZlYWZlMzkwNmFjMmY2ZDg0MmRhYTI0OWUwMGNhODJkODdmNWQwNDY4ZDZhN2Q5NGJhZmUyZTc0ZjQ3ZGUwMDAwMDAwMDBmZGZkMDAwMDQ4MzA0NTAyMjEwMGU1MmE4ZGU4NWJlMjUxYzM4YTQyMTFkMGYyZDM1Yzg4YTY3N2E0ZjE0NTg1ZWI4NmJkNzMwN2QwNjNkODRlOWIwMjIwMjI4ZmY4ZTA2NWMwMTlhNWY3MWMyZTE3OGM4ZDZiMDA3MGEwYWIxYzQxMDM1M2VhNzJmYzBjOTRiMDM5MzRiYzQxNDczMDQ0MDIyMDViM2QyYjM2MDg5ZGJmOTg1Y2E5MzRlNWQzYWRkYjM0MmQ4MDEzYmNjYTdhMTVlYzQwYzYwNzM4ZTA2MjY3NDMwMjIwMDdmN2FlOWI2OGJlM2YyNWVmM2UwNDIwZTZiZTEwNTJmZjUwZDBlNmJiYzM2ZTM5MTZjODBiZDU4MGU2OTI0MTQxNGM2OTUyMjEwM2VhYzRhMjY2MGQ5NWI1OGU4YzlhNzE0YThhNTM3YjBmZjE4YzEyMTFkNmIxZWM5ZWVmOWRhYTYwMzQ0MjA3NTQyMTAzNTdjNTIxOGJjMGY5NTlkOGQ3ZjI1NGUxM2EyNGZiYTJmNTZkMzk4ZjNiNWVjMzdjOWE2NWE2M2QwZDRhOThiYzIxMDM5MjI0YzNmNGU4N2JjMTExMmM2NWNmODA4OThmMWE1NTY2MTUyMzAwMGI0ZDQyYmU0YWU3YmE2NDQ1ODgyZjBlNTNhZWZkZmZmZmZmOTk1M2RlZDY2MTA1MTBkZmZiMDFiNTg5MzE2OTQ4NTM2ZjMwNDFlZjY2NTQ1MmYyODk3ZjA5ZWY2NTI1M2FlNzE5MDkwMDAwZmMwMDQ3MzA0NDAyMjA1MmE4ZGE0MGU1YjljMGZkNDlkNDQ2MGM4YzJjMGY3OTUwNmEwYTE3ZjdlNzM4ZTZjMzc4Mjk1NjE3NDg0NjQ0MDIyMDUyMDc1YzVjNzk4NTA5ZmU3NWYyNTBlZWI2ODEyOWQ5Njc1ZGNkNTMxZmQwZjViZmRiNmVhOWM1ODg3NGQxMDY0MTQ3MzA0NDAyMjA1YmQ0MDAwNGU3MjM5Yzg1NTk2NzU4YTA0Nzg4ODkxMTkyYmVkOTQ0Y2E1N2JjYjQ2ZTEwNDc2MmIzMmFjYmRkMDIyMDdmN2Q1ZjUxMzBmZWQ3NDY0ZTVhODc2ZTM3MjFkZTMwMWM0OGY4ODMxZjNmOTdiZWJiZWViOWE4MzA0MWZmMWI0MTRjNjk1MjIxMDJhMThjMzQ4ZGVmMGMwNmUyOTM5YmM3Y2NhYzc5MTlkNDIyNjU4MTc4ZGJlM2VjZjExYmZiZTY0MjkyZTkxOTdhMjEwMjk3MjY5ZGUyYTQ1M2U3NjkxODkxYzhhZWVhZjIyNmQ5ZGJiOTYyMjE3YzlmZmRkYWU2MGZiMDBhNjIxMGEzMTkyMTAzM2RiNjY2NTI2MzRkNjgzOWMwNjYyYTBhNTNhM2YwYjE0MDU2MGQwNGU0YmUxNmY4MDFkM2RlZWIyZjQ4YzE3MzUzYWVmZGZmZmZmZjgxZDg4YWUzMzg3ZmNjOWY0ZjY3Nzc0Mzg4MWI4MTZhZDM0ZDU0NmU2MGYyNDZlNDU5ZDhmYWFhY2IwMzhjZTkwMDAwMDAwMGZkZmQwMDAwNDczMDQ0MDIyMDI3OWYwZTVlNzIwZjEwMGZmYTMwMWMwOTBhZDRmMjg4ZjcwZjRmNzFiZDNkODQ2Y2Y3NjgzMjc0YmRhYTMzYTkwMjIwMDQ2ODYxMDgzNzRjOGI2YmI0NmM4NTBhNDJkY2NkZGM3NDBiZjY3ZGU1N2Q3NzgzYjM4Zjk2MjZjYjYyMTlmMzQxNDgzMDQ1MDIyMTAwZjYzMTQxMWI3NTRlZjcxZTgzNTJiNGU4N2YyODM4M2NiNjJhZGIyNWRiMjEyZjNmYjVmOGEyZTM2ZjhiYjMyMjAyMjA0NDZiNGE3MzYwNDI5NDFkZWE4YjRhZWMwMmI4ZWE4Nzc0NGI1YTEwYzZiODYyZmIxMTA1MWRiMDU4OWU4Y2I3NDE0YzY5NTIyMTAyYzBkZWY5MTA5ODU3MmQwMTlhNmU5YTZmZjRhNTVmZGU5M2JkNzc5MjcxN2MzZDQ4ZWI3OTVlNzNjOTFkMmE5ODIxMDIyNTgwZDU0OTI0ZmUwZDFiZmI2NWYzYjNhM2FlMWY2NzA5N2VjNDNiOWZlNzJhMDBkN2JhYjY5MTlmNWU5Nzc3MjEwMzJkMTcwNzc2YjVjYTYxY2Y0Mjg0NDUzYjdiMzhmZjQxMTcwYjFhYWE2NTEwMGQ0MWM2ZDJkYTQyYWZlZWE3NWM1M2FlZmRmZmZmZmZhMDdkNDdiNTZiMWY1YzI4MGY0M2QyZDM0ZTk3YTQwYjcyMjQxOTE3YjliYjk5ZGZmNWIwZGZhMjJkNjQ4MGZlMGQwMzAwMDBmZGZlMDAwMDQ4MzA0NTAyMjEwMGExZGRiZGNlNWEwZjcyMjVlNWJhZjEzM2IyZjI1NzNiZmQ0MWFjOTJiNGQ2MjBmMTI4NGY1YzYxMjg4YmMxNjMwMjIwNTU4MTgzMGRlOGQwYzI0M2MwMGEyODU0YmNkMjljMjA5M2YzODM5MTZjZWI5N2E2ODcwYmRjZGQxMGY3ZmNhNjQxNDgzMDQ1MDIyMTAwYjYyM2Q0ZDNmMTg3YTk1N2NlNDRjYjU2Y2I0YjZkMGY5N2I1YTMyYWRmZTJlYjFiMDlmMTQ1OWU3ZjM5YjBkMjAyMjAxZTdhNjY5YzE5ZjA3OWY0M2RmYWQ4ZDk2YWQzMTQ3YWE5ZjA0MGNlNjY5ZGQ1MmYzODQwMzY4Yjk3Y2UzZDAwNDE0YzY5NTIyMTAzYmI5MDMxY2ZkMTk3NzUwODZlZDA2YmY2OWI4OGFiYjNiNzdiNmI5OWUzZDM5Zjg4MTdhMDM5NTJjNTQxMjVlYzIxMDI3ODNiNmY3MzdiZTdkZThhZGM0MDE1OTI4ZGU3ZDhiZmMzMzI3NWRmODk5ZjczY2I4NGE5ZWZiZThkNGZhZGMyMjEwM2MyMDU1NTUzODFlMDExYzdlNTI3NzVkOTljYWQwNGE4Mzk1NDliOTI5OTQ3Y2VjYzlkYzJjZTZkNGRhOWFiZTg1M2FlZmRmZmZmZmYwMjQwNDIwZjAwMDAwMDAwMDAxOTc2YTkxNDRmMDM3NTRlZjdlYmZiNTY1ZjRkYTRmZjQwNDBmZmE4NDNkZTIwZDg4OGFjYzViMDE2MDAwMDAwMDAwMDE3YTkxNGIwZjJjZjgwMTNjMDJhNmVlMDc5YmIyZGU5ZWI4ZTZjOWNjYjliNzc4NzAwMDAwMDAwIn0seyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsImhhc2giOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5IiwidmVyc2lvbiI6Miwic2l6ZSI6NTk3MiwibG9ja3RpbWUiOjQyLCJ2aW4iOlt7InR4aWQiOiIwYWQ1YThhODkxYmJlNzljNjBiODgyOGRhMDhiOGYyMGUzMDM1ZjA0MjdkZGZhZGUzZjU1ZTAzMDAwOTVhMjc2Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkZTdhZTdiMTBkNmQ2ZDliYmMwMTcwYWIyYzJlZDQ0NTBlZDRjYjE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkZTdhZTdiMTBkNmQ2ZDliYmMwMTcwYWIyYzJlZDQ0NTBlZDRjYjE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjBhZDVhOGE4OTFiYmU3OWM2MGI4ODI4ZGEwOGI4ZjIwZTMwMzVmMDQyN2RkZmFkZTNmNTVlMDMwMDA5NWEyNzYiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDcxYTcxNDllZTBiNGUxNDhkNzdhYWRiNGMwZTcxYTEyZjBmMzgyYTk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDcxYTcxNDllZTBiNGUxNDhkNzdhYWRiNGMwZTcxYTEyZjBmMzgyYTk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMGFkNWE4YTg5MWJiZTc5YzYwYjg4MjhkYTA4YjhmMjBlMzAzNWYwNDI3ZGRmYWRlM2Y1NWUwMzAwMDk1YTI3NiIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIwYWQ1YThhODkxYmJlNzljNjBiODgyOGRhMDhiOGYyMGUzMDM1ZjA0MjdkZGZhZGUzZjU1ZTAzMDAwOTVhMjc2Iiwidm91dCI6Mywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQxYjIxMzQ5YjgwMTllYzczOGE4ZGM2ZjJmYzQyZGFjYmIwNjQ0MWQwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQxYjIxMzQ5YjgwMTllYzczOGE4ZGM2ZjJmYzQyZGFjYmIwNjQ0MWQwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImYzMzNhYjRmMTNlY2I0M2ViMTgwMTA4YTA4OTZjODRiMWIwMTNmZWQ3NTBkYWU0ZTczNDA0OTBjODAzYjRlMTQiLCJ2b3V0Ijo1LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNGQwNzM2ZWU5MDExN2Y0ZWQ3ZTliYWQ1YWUwOWIwYWIwYzg5Mjk2YjQ4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNGQwNzM2ZWU5MDExN2Y0ZWQ3ZTliYWQ1YWUwOWIwYWIwYzg5Mjk2YjQ4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZjMzM2FiNGYxM2VjYjQzZWIxODAxMDhhMDg5NmM4NGIxYjAxM2ZlZDc1MGRhZTRlNzM0MDQ5MGM4MDNiNGUxNCIsInZvdXQiOjYsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0Y2Q2ZDU2N2RjNjM0YWM4ZmE4ODU2MWU4N2IyOGY5MTE4YThhZGE3YTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0Y2Q2ZDU2N2RjNjM0YWM4ZmE4ODU2MWU4N2IyOGY5MTE4YThhZGE3YTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIwYWQ1YThhODkxYmJlNzljNjBiODgyOGRhMDhiOGYyMGUzMDM1ZjA0MjdkZGZhZGUzZjU1ZTAzMDAwOTVhMjc2Iiwidm91dCI6NCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImYzMzNhYjRmMTNlY2I0M2ViMTgwMTA4YTA4OTZjODRiMWIwMTNmZWQ3NTBkYWU0ZTczNDA0OTBjODAzYjRlMTQiLCJ2b3V0Ijo4LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNGE3M2Q3MDg5ZTQ4YWJmMzdiNGRiMTVjZmViNGJkNDU3YjVhMTA4MWY4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNGE3M2Q3MDg5ZTQ4YWJmMzdiNGRiMTVjZmViNGJkNDU3YjVhMTA4MWY4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZjMzM2FiNGYxM2VjYjQzZWIxODAxMDhhMDg5NmM4NGIxYjAxM2ZlZDc1MGRhZTRlNzM0MDQ5MGM4MDNiNGUxNCIsInZvdXQiOjksInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJmMzMzYWI0ZjEzZWNiNDNlYjE4MDEwOGEwODk2Yzg0YjFiMDEzZmVkNzUwZGFlNGU3MzQwNDkwYzgwM2I0ZTE0Iiwidm91dCI6MTAsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJmMzMzYWI0ZjEzZWNiNDNlYjE4MDEwOGEwODk2Yzg0YjFiMDEzZmVkNzUwZGFlNGU3MzQwNDkwYzgwM2I0ZTE0Iiwidm91dCI6Nywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkNmM5ZDA3YTAzNWFlNWNmMzE3ZjVmNDA0NjQ5YWZiODA4MzdiNGExODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkNmM5ZDA3YTAzNWFlNWNmMzE3ZjVmNDA0NjQ5YWZiODA4MzdiNGExODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjgxYjI2ZDAwYjFlNjE2ZjlkYWE3NmExNmMwNmM3ODViYjE4MmRhM2FmMmU2ZWJmMDY1ZjNmZDlkNjFiMTEzZmEiLCJ2b3V0Ijo1LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDQ1ZGMxNDI0ODg4YmM0OWIzNGJlZDk4MDIzMzAzY2VmMWUwOGZiNzA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDQ1ZGMxNDI0ODg4YmM0OWIzNGJlZDk4MDIzMzAzY2VmMWUwOGZiNzA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiODFiMjZkMDBiMWU2MTZmOWRhYTc2YTE2YzA2Yzc4NWJiMTgyZGEzYWYyZTZlYmYwNjVmM2ZkOWQ2MWIxMTNmYSIsInZvdXQiOjgsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI4MWIyNmQwMGIxZTYxNmY5ZGFhNzZhMTZjMDZjNzg1YmIxODJkYTNhZjJlNmViZjA2NWYzZmQ5ZDYxYjExM2ZhIiwidm91dCI6Nywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ0NWRjMTQyNDg4OGJjNDliMzRiZWQ5ODAyMzMwM2NlZjFlMDhmYjcwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ0NWRjMTQyNDg4OGJjNDliMzRiZWQ5ODAyMzMwM2NlZjFlMDhmYjcwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjgxYjI2ZDAwYjFlNjE2ZjlkYWE3NmExNmMwNmM3ODViYjE4MmRhM2FmMmU2ZWJmMDY1ZjNmZDlkNjFiMTEzZmEiLCJ2b3V0Ijo2LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDQ1ZGMxNDI0ODg4YmM0OWIzNGJlZDk4MDIzMzAzY2VmMWUwOGZiNzA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDQ1ZGMxNDI0ODg4YmM0OWIzNGJlZDk4MDIzMzAzY2VmMWUwOGZiNzA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiODFiMjZkMDBiMWU2MTZmOWRhYTc2YTE2YzA2Yzc4NWJiMTgyZGEzYWYyZTZlYmYwNjVmM2ZkOWQ2MWIxMTNmYSIsInZvdXQiOjksInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI4MWIyNmQwMGIxZTYxNmY5ZGFhNzZhMTZjMDZjNzg1YmIxODJkYTNhZjJlNmViZjA2NWYzZmQ5ZDYxYjExM2ZhIiwidm91dCI6MTAsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI4MWIyNmQwMGIxZTYxNmY5ZGFhNzZhMTZjMDZjNzg1YmIxODJkYTNhZjJlNmViZjA2NWYzZmQ5ZDYxYjExM2ZhIiwidm91dCI6MTEsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0OTU2OTljYzkwMmMyZDliZTE0MzhiMzg2ZjY3OGNjZjZhZjA2ODgyMzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0OTU2OTljYzkwMmMyZDliZTE0MzhiMzg2ZjY3OGNjZjZhZjA2ODgyMzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJhYzZlYTAyYjExZTY0NjViMDU5ODFjZWVjMTAwMDlmZTBlODg0MTJiOTNjNTcxYzY0NDZhZmUxYzdiYTY1ZDM5Iiwidm91dCI6MTMsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0OTlmOWM0MTFmODM5MTM2ODNhNmRmNWI0NTM2ZjdhYzJiOWQwZmJjNTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0OTlmOWM0MTFmODM5MTM2ODNhNmRmNWI0NTM2ZjdhYzJiOWQwZmJjNTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJhYzZlYTAyYjExZTY0NjViMDU5ODFjZWVjMTAwMDlmZTBlODg0MTJiOTNjNTcxYzY0NDZhZmUxYzdiYTY1ZDM5Iiwidm91dCI6MTUsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NGEwODUzMDY1MGU4NjFkMzRhY2VlNDMxNTJlNTRkZTI3ZDFiY2Y4Mjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NGEwODUzMDY1MGU4NjFkMzRhY2VlNDMxNTJlNTRkZTI3ZDFiY2Y4Mjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJhYzZlYTAyYjExZTY0NjViMDU5ODFjZWVjMTAwMDlmZTBlODg0MTJiOTNjNTcxYzY0NDZhZmUxYzdiYTY1ZDM5Iiwidm91dCI6MTQsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWI4Y2Y5YWI3YjgzNTRhZTQ5MGQzYTk2Y2M4Y2U0MTE4Nzg3ZmNmMjQwNTZkOGY0NGNiOGFkZWNmYWUxYjNhIiwidm91dCI6MjUsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NzQ1NzZjNzdhZWYzZmI1ODdmOGM2YjJmYWY4OGUzN2Q4ZWFhZjhiODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NzQ1NzZjNzdhZWYzZmI1ODdmOGM2YjJmYWY4OGUzN2Q4ZWFhZjhiODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJhYzZlYTAyYjExZTY0NjViMDU5ODFjZWVjMTAwMDlmZTBlODg0MTJiOTNjNTcxYzY0NDZhZmUxYzdiYTY1ZDM5Iiwidm91dCI6MTYsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ODg0MjQ5NWY5ODhkOTA3YzhlY2Y4ZjZmZjE3ZTkwYTM5ZTg5ZDJhNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ODg0MjQ5NWY5ODhkOTA3YzhlY2Y4ZjZmZjE3ZTkwYTM5ZTg5ZDJhNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWI4Y2Y5YWI3YjgzNTRhZTQ5MGQzYTk2Y2M4Y2U0MTE4Nzg3ZmNmMjQwNTZkOGY0NGNiOGFkZWNmYWUxYjNhIiwidm91dCI6MjYsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NGEwODUzMDY1MGU4NjFkMzRhY2VlNDMxNTJlNTRkZTI3ZDFiY2Y4Mjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NGEwODUzMDY1MGU4NjFkMzRhY2VlNDMxNTJlNTRkZTI3ZDFiY2Y4Mjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJhYzZlYTAyYjExZTY0NjViMDU5ODFjZWVjMTAwMDlmZTBlODg0MTJiOTNjNTcxYzY0NDZhZmUxYzdiYTY1ZDM5Iiwidm91dCI6MTcsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJhYzZlYTAyYjExZTY0NjViMDU5ODFjZWVjMTAwMDlmZTBlODg0MTJiOTNjNTcxYzY0NDZhZmUxYzdiYTY1ZDM5Iiwidm91dCI6MTksInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0MjQyYTIxOTZjZTEzNzYxNWNjNjNjZmNmYmQ1NTRjMjUwOTNkNDI4Yjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0MjQyYTIxOTZjZTEzNzYxNWNjNjNjZmNmYmQ1NTRjMjUwOTNkNDI4Yjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWI4Y2Y5YWI3YjgzNTRhZTQ5MGQzYTk2Y2M4Y2U0MTE4Nzg3ZmNmMjQwNTZkOGY0NGNiOGFkZWNmYWUxYjNhIiwidm91dCI6MTksInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZGMxYWIwYmY4ZjQ3MDM1MjU5NTAzYWUxYTE4YzQ2MjQ3YmZiMTExZDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZGMxYWIwYmY4ZjQ3MDM1MjU5NTAzYWUxYTE4YzQ2MjQ3YmZiMTExZDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWI4Y2Y5YWI3YjgzNTRhZTQ5MGQzYTk2Y2M4Y2U0MTE4Nzg3ZmNmMjQwNTZkOGY0NGNiOGFkZWNmYWUxYjNhIiwidm91dCI6MjAsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NzdiNGVjNzA2YjhmNzUzMWM4MTIxOWFjZGJhODA3ZGEwZWYzZTdiMzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NzdiNGVjNzA2YjhmNzUzMWM4MTIxOWFjZGJhODA3ZGEwZWYzZTdiMzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWI4Y2Y5YWI3YjgzNTRhZTQ5MGQzYTk2Y2M4Y2U0MTE4Nzg3ZmNmMjQwNTZkOGY0NGNiOGFkZWNmYWUxYjNhIiwidm91dCI6MjEsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NTM3ZmI4N2Q0MDczMTU4NDE2YTBkNDE3MTA2NjUxNTEzNDcyNjRhMDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NTM3ZmI4N2Q0MDczMTU4NDE2YTBkNDE3MTA2NjUxNTEzNDcyNjRhMDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJlZGJlZjBiM2YwZDg2YTIyNWQ0NWUyN2I1ZTZmNDVlYmVkYmI2NjJlMjdhNzQzY2E4N2I5NjE1NDEzOThiNTFmIiwidm91dCI6NDUsInNjcmlwdFNpZyI6eyJhc20iOiJhOTQ1NDBkZTVhMDc3OTNiMzcyNWQzYmNhZmRmN2FiYjY5MDRjMWVmNzMyYzg5MGI0MGQyMDI0ZGZlMDIxZDA2MTdmODNmOGFkN2YwY2VmZGZhYzUxNTAzYjVkZjZlODdhNDk2ODFiNmQ3MjhmZDg5YzAzMzRhYTM4ODU1OGQ1ZVtBTEx8Rk9SS0lEfFVUWE9TXSAwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjMiLCJoZXgiOiI0MWE5NDU0MGRlNWEwNzc5M2IzNzI1ZDNiY2FmZGY3YWJiNjkwNGMxZWY3MzJjODkwYjQwZDIwMjRkZmUwMjFkMDYxN2Y4M2Y4YWQ3ZjBjZWZkZmFjNTE1MDNiNWRmNmU4N2E0OTY4MWI2ZDcyOGZkODljMDMzNGFhMzg4NTU4ZDVlNjEyMTAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2MyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMGFkNWE4YTg5MWJiZTc5YzYwYjg4MjhkYTA4YjhmMjBlMzAzNWYwNDI3ZGRmYWRlM2Y1NWUwMzAwMDk1YTI3NiIsInZvdXQiOjYsInNjcmlwdFNpZyI6eyJhc20iOiI1ZTgzZDg0OTc5Y2M0MmIwMWI2MzllOTI1ZGY0OWI3OTNiYzg3NzhjNTEyN2VlMDE4M2MwZTU2NzE3Y2I4NDk3YWNmM2YzYjJlYjQyOGExN2IyYzgyYmNlNmMyNDM3MzlhOGI4NTVjYjQyZjg1MGMwZmZmNGFkNmYzMWMzZmM3NFtBTEx8Rk9SS0lEfFVUWE9TXSAwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjMiLCJoZXgiOiI0MTVlODNkODQ5NzljYzQyYjAxYjYzOWU5MjVkZjQ5Yjc5M2JjODc3OGM1MTI3ZWUwMTgzYzBlNTY3MTdjYjg0OTdhY2YzZjNiMmViNDI4YTE3YjJjODJiY2U2YzI0MzczOWE4Yjg1NWNiNDJmODUwYzBmZmY0YWQ2ZjMxYzNmYzc0NjEyMTAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2MyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MTAzLjU5NTI2NDY3LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1MGZjMTZmZmIzNzdjZDRlMWQ4YjRkZWZhMzc1NmQxMjIxYmFjZmUzMzA0ZTc3MDVkZTQxNjdkNzdjMWJhMzY0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkZzBjOWhsa2RtdTZuc2EzZHg3bGdtNGQ1Znpyd2swdXZjeXVhYzltZXFrMDRtdXJ3M2tneWhmNW1uN3giXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0ODY0NDgxIn19LHsidmFsdWUiOjkyLjMyMDA2MzYsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDFlZTQxNDJmZjkyOGI0YWE2OTJiM2RlOGRlMjkyOWJlMzkwNWM2MDI3ZDNiYThiMGEyY2YxYzBiNTQ4N2FjYjAgT1BfRVFVQUwiLCJoZXgiOiJhYTIwMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHYwd2c5cDBseTV0ZjJuZjl2NzczaDNmOXhscmpwd3hxZjduaDI5czV0ODNjejY1czdrdHFhaHI5eWFnZCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQzMzUwMzYifX0seyJ2YWx1ZSI6NzEuMjMwNTQxNDUsIm4iOjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDJjZjliZjRkYWIwNWEyMzUyMWRjMjYyOTM4MzAyNjJmZmVjNzYxNDY2ODgwOThiYjMxMjRhYzNmYjc2NDAwMzYgT1BfRVFVQUwiLCJoZXgiOiJhYTIwMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHZrMG4wNmQ0dno2eWRmcG1zbnpqd3BzeWNobGEzbXBnZTVncHg5bXh5ajJjMGFodnNxcnZkZnhjdjd5ayJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjMzNDQ3NDQifX0seyJ2YWx1ZSI6MzYuMDk3MzY5MTEsIm4iOjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDY3YWQxYzY0OWIwOWJhNThiYTE5ZWIzYWQzNWY0MjQxYmEwNjBlOGJiODk3YWRjZjZiZGRkMzZhODBkOGU4YTYgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRuNjY4cnludnltNWs5NnI4NG40NTZsZ2ZxbTVwc3czd3VmMHR3MGQwd2F4NjVxbXI1MnZwZm4yNW10ayJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjE2OTUwMTAifX0seyJ2YWx1ZSI6MTEuMzU5MTQ4NDcsIm4iOjQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGIzNDdkNjhjOGQyY2RiMzIwNjZiZTlhZjgwNGM2ZmZiOGFiYmI0NDFiZGYzNmNhZjM4MzQzNmJjZmNhYTM1Y2IgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdlNTA0NXYzNWtka3ZzeGQwNTZscXp2ZGxhYzR3YTVneDdseG05MDhxNnJkMDh1NGc2dWtuMzcyZ2d3bCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjUzMzQ3MSJ9fSx7InZhbHVlIjoxMC41NTQxODUyLCJuIjo1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDdjMmJjY2EyMDg5YTlmMGY4ZGFlYzBkM2I0NWE2YzUyNTgxYzU2NzY2NWNjNTFlYmRhOWEyMDJkOGEzM2RjZmM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkN3pobjl6cHpkZjdydWQ0bXFkOGR6NmQzZjlzOHprd2VqdWM1MHRtMmR6cXR2MngwdzBjNTR5MzlocDYiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0OTU2NjcifX0seyJ2YWx1ZSI6MTcuNTE3NjI1ODgsIm4iOjYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDIwYjA1Yjc0YWI1NmM5ODQ4MDMyZTliYTRmZDA5NDI3NWNjYzdmMzYyOGFkMTg4NmZjZTU2ZTkzMGQxMjczY2QgT1BfRVFVQUwiLCJoZXgiOiJhYTIwMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHZzdHFrbTU0ZHR2bnB5cXh0NW01bjdzanNuNGVucmx4YzUyNnh5eGxuamtheWNkemZldTY5cmV5em1xeCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjgyMjU2OSJ9fSx7InZhbHVlIjo1LjIyMTg3ODY2LCJuIjo3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBmMWMzM2JjN2RhMDk5MWQyNmY4YmJmMTUwYzkwMzU2OGIyYzNkYzg3MjczNzA4NjNmNGMzZDlmOTY1NWIzNzkxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwY3V4dzc4bWd5ZXI1bjAzd2wzMnJ5c3g0NXQ5czd1c3Vubnd6cnI3bnBhbjd0OXR2bWV6cmF1M2p1cjkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyNDUyNDEifX0seyJ2YWx1ZSI6NC40MDY3OTgyMiwibiI6OCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2JiMjRmZjkwYjIyMTlhMjYwZDBlZTBhMTY4NjdmZDMzNDY5Y2Y3MDRmNDkwMjIzMWIxNGU5NjNlY2JhZTNjNCBPUF9FUVVBTCIsImhleCI6ImFhMjAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmFteW5sZXB2M3BuZ25xNnJocTU5NXgwbGZuZzZ3MHdwODVqcTNycnYyd2pjbHZodDN1Z3g0a2FqNGh1Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjA2OTYxIn19LHsidmFsdWUiOjMuMzA3MDg2MzgsIm4iOjksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwM2JiMjRmZjkwYjIyMTlhMjYwZDBlZTBhMTY4NjdmZDMzNDY5Y2Y3MDRmNDkwMjIzMWIxNGU5NjNlY2JhZTNjNDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHZhbXlubGVwdjNwbmducTZyaHE1OTV4MGxmbmc2dzB3cDg1anEzcnJ2MndqY2x2aHQzdWd4NGthajRodSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjE1NTMxNCJ9fSx7InZhbHVlIjoyLjg3OTg2MDEzLCJuIjoxMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgZWZiYzU1NmQ1Y2ZlOTBhNDU4ZGJmNGQ1N2JkNzg3ZjMzNGJhZmIxYTkzM2ZmYzMzMjIyNjdlYmY0NGMzNGZiMCBPUF9FUVVBTCIsImhleCI6ImFhMjBlZmJjNTU2ZDVjZmU5MGE0NThkYmY0ZDU3YmQ3ODdmMzM0YmFmYjFhOTMzZmZjMzMyMjI2N2ViZjQ0YzM0ZmIwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMGhtYzR0ZHRubGZwZnpjbTA2ZDI3N2hzbGVuZndobXIyZm5sbHBueWduOGEwNnljZDhtcWRhNWV2cHMyIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTM1MjUxIn19LHsidmFsdWUiOjIuNTQyMjc4NjIsIm4iOjExLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkZ2o2OW4ydTI4dDB6dmc1MGxyM3VrbXQ5dnhwcGdkY3JmOWFxM2N1NTZsZHNweXhsNHc1aHNmeW04czciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMTk0MDEifX0seyJ2YWx1ZSI6Mi41Mzc1MTIwNiwibiI6MTIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnajY5bjJ1Mjh0MHp2ZzUwbHIzdWttdDl2eHBwZ2RjcmY5YXEzY3U1Nmxkc3B5eGw0dzVoc2Z5bThzNyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjExOTE3NiJ9fSx7InZhbHVlIjoyLjUzNzAwNjYsIm4iOjEzLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkZ2o2OW4ydTI4dDB6dmc1MGxyM3VrbXQ5dnhwcGdkY3JmOWFxM2N1NTZsZHNweXhsNHc1aHNmeW04czciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMTkxNTMifX0seyJ2YWx1ZSI6Mi41Mzc1MjkxMSwibiI6MTQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnajY5bjJ1Mjh0MHp2ZzUwbHIzdWttdDl2eHBwZ2RjcmY5YXEzY3U1Nmxkc3B5eGw0dzVoc2Z5bThzNyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjExOTE3OCJ9fSx7InZhbHVlIjoyLjQ5NTI2MTEzLCJuIjoxNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYSBPUF9FUVVBTCIsImhleCI6ImFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGdqNjluMnUyOHQwenZnNTBscjN1a210OXZ4cHBnZGNyZjlhcTNjdTU2bGRzcHl4bDR3NWhzZnltOHM3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTE3MTkzIn19LHsidmFsdWUiOjIuNDY3MjIwNDUsIm4iOjE2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkZ2o2OW4ydTI4dDB6dmc1MGxyM3VrbXQ5dnhwcGdkY3JmOWFxM2N1NTZsZHNweXhsNHc1aHNmeW04czciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMTU4NzYifX0seyJ2YWx1ZSI6Mi4zNTIxNjA4MywibiI6MTcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGM4ZGQ5NzdhZTk5MTJiNzIxMzQxYmFmMzA4Njg3YmU1YjllZmIzMmNjMGZiZjI2OTY2NjkwYjYxOThlMjAxY2YgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYzhkZDk3N2FlOTkxMmI3MjEzNDFiYWYzMDg2ODdiZTViOWVmYjMyY2MwZmJmMjY5NjY2OTBiNjE5OGUyMDFjZjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cDB5ZG05bTZheGdqa3Vzbmd4YTB4enJnMDBqbW5tYW45bnEwaHVuZnZlNXNrY3ZjdWdxdTdydWFnd3ZlbCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjExMDQ3MiJ9fSx7InZhbHVlIjoxLjA3OTg2NTk4LCJuIjoxOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2Y5OTJkMTI5MDQxMTM3NDhjNDQ0M2MxNjRlY2YxMmY0OGQxYjA3NTE4MDhlMmI1MzcxODQ5N2UxMmY5NGI0MSBPUF9FUVVBTCIsImhleCI6ImFhMjAzZjk5MmQxMjkwNDExMzc0OGM0NDQzYzE2NGVjZjEyZjQ4ZDFiMDc1MTgwOGUyYjUzNzE4NDk3ZTEyZjk0YjQxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmxlanRnampwcTN4YXl2ZzNwdXplOHY3eWg1MzVkc3c1dnEzYzQ0eHV2eWpsc2psOTk1ejdxOHcyeThrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTA3MjQifX0seyJ2YWx1ZSI6MS4wNjU2ODE1MiwibiI6MTksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGFhZWE2NDlmNzE5M2FiNDMyOTk5OTkzYmQ4ZDJjY2NlNWZhYmQzYWYwMWViZmU0MWEwZTBhMzU4MTA0NGQ0NmQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHc0dzVleWx3eGY2a3NlZm54dm5oa3hqZW44OWwyN240dXE3aGxqcDVyczJ4a3FzZ24yeDY4Z251dnplayJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjUwMDU5In19LHsidmFsdWUiOjEuMDY0OTM2NzksIm4iOjIwLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA3YWZkNGRlOGFmZmIwN2NiNjY4MzEzNGFkNTllOWM3YTc0M2E3MGU2MTRhZDYyMWFmZjVhMTEwMzdjMDExNWUyIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDdhZmQ0ZGU4YWZmYjA3Y2I2NjgzMTM0YWQ1OWU5YzdhNzQzYTcwZTYxNGFkNjIxYWZmNWExMTAzN2MwMTE1ZTI4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkYTA2bjBnNGxhczBqbXhzdmY1NDR2N24zYThnd25zdWMyMjZjczZsYWRwenFtdXF5Mjd5NmprZXU2emciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI1MDAyMyJ9fSx7InZhbHVlIjoxLjI1Njg3MTkxLCJuIjoyMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmRkNzA1NWM5MjYyNTMyNTJlYjQ0ZjdlYTQxMzlkYjI0NWU0OTJhYWRjMTllOWU1Y2ZlOTVhMmVjODdjYjA2MCBPUF9FUVVBTCIsImhleCI6ImFhMjAyZGQ3MDU1YzkyNjI1MzI1MmViNDRmN2VhNDEzOWRiMjQ1ZTQ5MmFhZGMxOWU5ZTVjZmU5NWEyZWM4N2NiMDYwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmthd3AydWpmMzl4ZmZ3azM4aGFmcW5ua2V5dGV5ajR0d3BuNjA5ZWw1NDV0a2cwamN4cW40OGM3enllIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTkwMzUifX0seyJ2YWx1ZSI6MC45OTE1Njk3OCwibiI6MjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGE2NGE2NTMwOTE2MzMyOTgwNjI3NWE1YTk5YjI3ODUyNGIyMWE0NjU1OWRlNDdlZmNjZTA5OGQzZWI1YjJhMGUgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTY0YTY1MzA5MTYzMzI5ODA2Mjc1YTVhOTliMjc4NTI0YjIxYTQ2NTU5ZGU0N2VmY2NlMDk4ZDNlYjViMmEwZTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdueTVlZnNqOTNuOXhxeHlhZDk0eGRqMHBmeWtnZHl2NHZhdTNsMGVuc2YzNWx0dHY0cXUyeHJzMzZ3aCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ2NTc3In19LHsidmFsdWUiOjEuMjM2MjA1NywibiI6MjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGFhZWE2NDlmNzE5M2FiNDMyOTk5OTkzYmQ4ZDJjY2NlNWZhYmQzYWYwMWViZmU0MWEwZTBhMzU4MTA0NGQ0NmQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHc0dzVleWx3eGY2a3NlZm54dm5oa3hqZW44OWwyN240dXE3aGxqcDVyczJ4a3FzZ24yeDY4Z251dnplayJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjU4MDYzIn19LHsidmFsdWUiOjAuOTc3MzQ0NjksIm4iOjI0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA3YWZkNGRlOGFmZmIwN2NiNjY4MzEzNGFkNTllOWM3YTc0M2E3MGU2MTRhZDYyMWFmZjVhMTEwMzdjMDExNWUyIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDdhZmQ0ZGU4YWZmYjA3Y2I2NjgzMTM0YWQ1OWU5YzdhNzQzYTcwZTYxNGFkNjIxYWZmNWExMTAzN2MwMTE1ZTI4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkYTA2bjBnNGxhczBqbXhzdmY1NDR2N24zYThnd25zdWMyMjZjczZsYWRwenFtdXF5Mjd5NmprZXU2emciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0NTkwOSJ9fSx7InZhbHVlIjowLjg2NDU4Mjk4LCJuIjoyNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYzNlN2RjY2U2M2UzN2FiMmRlM2JmOGE3ZDkzMmNmYTAyOTM3NDkwMmM2MzEwZThlZGNiMzExODEyMzQ1NzY3YSBPUF9FUVVBTCIsImhleCI6ImFhMjBjM2U3ZGNjZTYzZTM3YWIyZGUzYmY4YTdkOTMyY2ZhMDI5Mzc0OTAyYzYzMTBlOGVkY2IzMTE4MTIzNDU3NjdhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMHA3MGh4d3YwM2g0dms3ODB1MjBrZmplN3N6amQ2ZnF0cnJ6cjV3bWplM3JxZnJnNG04NXgyNzBmNTZ2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDA2MTIifX0seyJ2YWx1ZSI6MC44NDQxMzg5OCwibiI6MjYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDFkNzk3NzlkNmI4MjM2MjgxOWNjMWE1YTk1MWI5ZDdjOWY3MmU3YTFhZjQ3NTc3NmQ5NDRmMzU0ZDc2MzM5ZjMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwMWQ3OTc3OWQ2YjgyMzYyODE5Y2MxYTVhOTUxYjlkN2M5ZjcyZTdhMWFmNDc1Nzc2ZDk0NGYzNTRkNzYzMzlmMzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHZ3aGphdWFkd3BydjJxZWVzZDk0OWdtbjQ3Zjd1aDg1eGg1dzRta205ejB4NHhodnZ1bHhjNnI2MzRoNiJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjM5NjQ5In19LHsidmFsdWUiOjAuNzg4MjM0MjIsIm4iOjI3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA0NjE5ZjBiMjlkMWVlNzA1NDY3ODZiOGIzMGM3NWJiZTViMTViZGI5OGQwMWE2YWRkM2E0MTQ3MDAwZjg2ZTYxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDQ2MTlmMGIyOWQxZWU3MDU0Njc4NmI4YjMwYzc1YmJlNWIxNWJkYjk4ZDAxYTZhZGQzYTQxNDcwMDBmODZlNjE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkcnBudTlqbjUwd3dwMngwcDRja3Z4OHR3bDlrOWRhaHh4c3JmNGQ2d2pwZ3VxcWxwaHh6ZXVwdGR4Nm4iXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzNzAyMyJ9fSx7InZhbHVlIjowLjc5MDY2MDA4LCJuIjoyOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOGRhZDcxYTk3YmMxZGM5ZTJiZWQ5ZjZhYTE0YmNlMTk4OThkM2NlODE0MmY2NWQyMTg1ODFkYWY0Mjc0NTMxYyBPUF9FUVVBTCIsImhleCI6ImFhMjA4ZGFkNzFhOTdiYzFkYzllMmJlZDlmNmFhMTRiY2UxOTg5OGQzY2U4MTQyZjY1ZDIxODU4MWRhZjQyNzQ1MzFjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3g2NnVkZjAwcWFlODN0YWswazRnMnRlY3ZjbnJmdWFxMno3ZXdqcnB2cG10Nnp3M2YzYzc1cmd6azN5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzcxMzcifX0seyJ2YWx1ZSI6OGUtMDYsIm4iOjI5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFmMGVkY3hsZm56a3RtNzdlcWc4NzRqdXAyOHRhNmE2dTVyeTVzZnU5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzI3MjkifX0seyJ2YWx1ZSI6MzYuNzI0NDk1MTYsIm4iOjMwLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFmMGVkY3hsZm56a3RtNzdlcWc4NzRqdXAyOHRhNmE2dTVyeTVzZnU5Il19fV0sImZlZSI6NS45NzJlLTA1LCJoZXgiOiIwMjAwMDAwMDFmNzZhMjk1MDAzMGUwNTUzZmRlZmFkZDI3MDQ1ZjAzZTMyMDhmOGJhMDhkODJiODYwOWNlN2JiOTFhOGE4ZDUwYTAwMDAwMDAwNDU0NDc0NjM3NmE5MTRkZTdhZTdiMTBkNmQ2ZDliYmMwMTcwYWIyYzJlZDQ0NTBlZDRjYjE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjc2YTI5NTAwMzBlMDU1M2ZkZWZhZGQyNzA0NWYwM2UzMjA4ZjhiYTA4ZDgyYjg2MDljZTdiYjkxYThhOGQ1MGEwMTAwMDAwMDQ1NDQ3NDYzNzZhOTE0NzFhNzE0OWVlMGI0ZTE0OGQ3N2FhZGI0YzBlNzFhMTJmMGYzODJhOTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY3NmEyOTUwMDMwZTA1NTNmZGVmYWRkMjcwNDVmMDNlMzIwOGY4YmEwOGQ4MmI4NjA5Y2U3YmI5MWE4YThkNTBhMDIwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGZlODI3YzY1MzgzMjA3Mzk3OGI3YmQ1OGVlYzg0MGVlNDRiYzY3MDg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmNzZhMjk1MDAzMGUwNTUzZmRlZmFkZDI3MDQ1ZjAzZTMyMDhmOGJhMDhkODJiODYwOWNlN2JiOTFhOGE4ZDUwYTAzMDAwMDAwNDU0NDc0NjM3NmE5MTQxYjIxMzQ5YjgwMTllYzczOGE4ZGM2ZjJmYzQyZGFjYmIwNjQ0MWQwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjE0NGUzYjgwMGM0OTQwNzM0ZWFlMGQ3NWVkM2YwMTFiNGJjODk2MDg4YTEwODBiMTNlYjRlYzEzNGZhYjMzZjMwNTAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZDA3MzZlZTkwMTE3ZjRlZDdlOWJhZDVhZTA5YjBhYjBjODkyOTZiNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYxNDRlM2I4MDBjNDk0MDczNGVhZTBkNzVlZDNmMDExYjRiYzg5NjA4OGExMDgwYjEzZWI0ZWMxMzRmYWIzM2YzMDYwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGNkNmQ1NjdkYzYzNGFjOGZhODg1NjFlODdiMjhmOTExOGE4YWRhN2E4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmNzZhMjk1MDAzMGUwNTUzZmRlZmFkZDI3MDQ1ZjAzZTMyMDhmOGJhMDhkODJiODYwOWNlN2JiOTFhOGE4ZDUwYTA0MDAwMDAwNDU0NDc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjE0NGUzYjgwMGM0OTQwNzM0ZWFlMGQ3NWVkM2YwMTFiNGJjODk2MDg4YTEwODBiMTNlYjRlYzEzNGZhYjMzZjMwODAwMDAwMDQ1NDQ3NDYzNzZhOTE0YTczZDcwODllNDhhYmYzN2I0ZGIxNWNmZWI0YmQ0NTdiNWExMDgxZjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYxNDRlM2I4MDBjNDk0MDczNGVhZTBkNzVlZDNmMDExYjRiYzg5NjA4OGExMDgwYjEzZWI0ZWMxMzRmYWIzM2YzMDkwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGE3ODdhMjc3NGNiMWMzYjU4N2Y5MDQ4ZjY2ZTdkYzlmNTc0MzQ0NzM4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmMTQ0ZTNiODAwYzQ5NDA3MzRlYWUwZDc1ZWQzZjAxMWI0YmM4OTYwODhhMTA4MGIxM2ViNGVjMTM0ZmFiMzNmMzBhMDAwMDAwNDU0NDc0NjM3NmE5MTRhNzg3YTI3NzRjYjFjM2I1ODdmOTA0OGY2NmU3ZGM5ZjU3NDM0NDczODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjE0NGUzYjgwMGM0OTQwNzM0ZWFlMGQ3NWVkM2YwMTFiNGJjODk2MDg4YTEwODBiMTNlYjRlYzEzNGZhYjMzZjMwNzAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZDZjOWQwN2EwMzVhZTVjZjMxN2Y1ZjQwNDY0OWFmYjgwODM3YjRhMTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZmYTEzYjE2MTlkZmRmMzY1ZjBlYmU2ZjIzYWRhODJiMTViNzg2Y2MwMTY2YWE3ZGFmOTE2ZTZiMTAwNmRiMjgxMDUwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDQ1ZGMxNDI0ODg4YmM0OWIzNGJlZDk4MDIzMzAzY2VmMWUwOGZiNzA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmZmExM2IxNjE5ZGZkZjM2NWYwZWJlNmYyM2FkYTgyYjE1Yjc4NmNjMDE2NmFhN2RhZjkxNmU2YjEwMDZkYjI4MTA4MDAwMDAwNDU0NDc0NjM3NmE5MTQ0NWRjMTQyNDg4OGJjNDliMzRiZWQ5ODAyMzMwM2NlZjFlMDhmYjcwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZmZhMTNiMTYxOWRmZGYzNjVmMGViZTZmMjNhZGE4MmIxNWI3ODZjYzAxNjZhYTdkYWY5MTZlNmIxMDA2ZGIyODEwNzAwMDAwMDQ1NDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZmYTEzYjE2MTlkZmRmMzY1ZjBlYmU2ZjIzYWRhODJiMTViNzg2Y2MwMTY2YWE3ZGFmOTE2ZTZiMTAwNmRiMjgxMDYwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDQ1ZGMxNDI0ODg4YmM0OWIzNGJlZDk4MDIzMzAzY2VmMWUwOGZiNzA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmZmExM2IxNjE5ZGZkZjM2NWYwZWJlNmYyM2FkYTgyYjE1Yjc4NmNjMDE2NmFhN2RhZjkxNmU2YjEwMDZkYjI4MTA5MDAwMDAwNDU0NDc0NjM3NmE5MTQ0NWRjMTQyNDg4OGJjNDliMzRiZWQ5ODAyMzMwM2NlZjFlMDhmYjcwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZmZhMTNiMTYxOWRmZGYzNjVmMGViZTZmMjNhZGE4MmIxNWI3ODZjYzAxNjZhYTdkYWY5MTZlNmIxMDA2ZGIyODEwYTAwMDAwMDQ1NDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZmYTEzYjE2MTlkZmRmMzY1ZjBlYmU2ZjIzYWRhODJiMTViNzg2Y2MwMTY2YWE3ZGFmOTE2ZTZiMTAwNmRiMjgxMGIwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDk1Njk5Y2M5MDJjMmQ5YmUxNDM4YjM4NmY2NzhjY2Y2YWYwNjg4MjM4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmMzk1ZGE2N2IxY2ZlNmE0NGM2NzFjNTkzMmI0MTg4MGVmZTA5MDBjMWVlMWM5ODA1NWI0NmU2MTEyYmEwNmVhYzBkMDAwMDAwNDU0NDc0NjM3NmE5MTQ5OWY5YzQxMWY4MzkxMzY4M2E2ZGY1YjQ1MzZmN2FjMmI5ZDBmYmM1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjM5NWRhNjdiMWNmZTZhNDRjNjcxYzU5MzJiNDE4ODBlZmUwOTAwYzFlZTFjOTgwNTViNDZlNjExMmJhMDZlYWMwZjAwMDAwMDQ1NDQ3NDYzNzZhOTE0NGEwODUzMDY1MGU4NjFkMzRhY2VlNDMxNTJlNTRkZTI3ZDFiY2Y4Mjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzOTVkYTY3YjFjZmU2YTQ0YzY3MWM1OTMyYjQxODgwZWZlMDkwMGMxZWUxYzk4MDU1YjQ2ZTYxMTJiYTA2ZWFjMGUwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGQzZTc4NmM2YTRhZjEzNzUxMWFjODQxMjJlNzgxNWExMDY3NDkyNjk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTE5MDAwMDAwNDU0NDc0NjM3NmE5MTQ3NDU3NmM3N2FlZjNmYjU4N2Y4YzZiMmZhZjg4ZTM3ZDhlYWFmOGI4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjM5NWRhNjdiMWNmZTZhNDRjNjcxYzU5MzJiNDE4ODBlZmUwOTAwYzFlZTFjOTgwNTViNDZlNjExMmJhMDZlYWMxMDAwMDAwMDQ1NDQ3NDYzNzZhOTE0ODg0MjQ5NWY5ODhkOTA3YzhlY2Y4ZjZmZjE3ZTkwYTM5ZTg5ZDJhNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MWEwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDRhMDg1MzA2NTBlODYxZDM0YWNlZTQzMTUyZTU0ZGUyN2QxYmNmODI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmMzk1ZGE2N2IxY2ZlNmE0NGM2NzFjNTkzMmI0MTg4MGVmZTA5MDBjMWVlMWM5ODA1NWI0NmU2MTEyYmEwNmVhYzExMDAwMDAwNDU0NDc0NjM3NmE5MTRkM2U3ODZjNmE0YWYxMzc1MTFhYzg0MTIyZTc4MTVhMTA2NzQ5MjY5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjM5NWRhNjdiMWNmZTZhNDRjNjcxYzU5MzJiNDE4ODBlZmUwOTAwYzFlZTFjOTgwNTViNDZlNjExMmJhMDZlYWMxMzAwMDAwMDQ1NDQ3NDYzNzZhOTE0MjQyYTIxOTZjZTEzNzYxNWNjNjNjZmNmYmQ1NTRjMjUwOTNkNDI4Yjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MTMwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGRjMWFiMGJmOGY0NzAzNTI1OTUwM2FlMWExOGM0NjI0N2JmYjExMWQ4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTE0MDAwMDAwNDU0NDc0NjM3NmE5MTQ3N2I0ZWM3MDZiOGY3NTMxYzgxMjE5YWNkYmE4MDdkYTBlZjNlN2IzODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjNhMWJhZWNmZGU4YWNiNDQ4ZjZkMDUyNGNmN2Y3ODE4NDFjZWM4NmNhOWQzOTBlNDRhMzViOGI3OWFjZmI4NzUxNTAwMDAwMDQ1NDQ3NDYzNzZhOTE0NTM3ZmI4N2Q0MDczMTU4NDE2YTBkNDE3MTA2NjUxNTEzNDcyNjRhMDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYxZmI1OTgxMzU0NjFiOTg3Y2E0M2E3MjcyZTY2YmJlZGViNDU2ZjVlN2JlMjQ1NWQyMjZhZDhmMGIzZjBiZWVkMmQwMDAwMDA2NDQxYTk0NTQwZGU1YTA3NzkzYjM3MjVkM2JjYWZkZjdhYmI2OTA0YzFlZjczMmM4OTBiNDBkMjAyNGRmZTAyMWQwNjE3ZjgzZjhhZDdmMGNlZmRmYWM1MTUwM2I1ZGY2ZTg3YTQ5NjgxYjZkNzI4ZmQ4OWMwMzM0YWEzODg1NThkNWU2MTIxMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzZmZmZmZmZmY3NmEyOTUwMDMwZTA1NTNmZGVmYWRkMjcwNDVmMDNlMzIwOGY4YmEwOGQ4MmI4NjA5Y2U3YmI5MWE4YThkNTBhMDYwMDAwMDA2NDQxNWU4M2Q4NDk3OWNjNDJiMDFiNjM5ZTkyNWRmNDliNzkzYmM4Nzc4YzUxMjdlZTAxODNjMGU1NjcxN2NiODQ5N2FjZjNmM2IyZWI0MjhhMTdiMmM4MmJjZTZjMjQzNzM5YThiODU1Y2I0MmY4NTBjMGZmZjRhZDZmMzFjM2ZjNzQ2MTIxMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzZmZmZmZmZmYxZjQzZDQ3OTY5MDIwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVlMTM5NGEwMGFhMjA1MGZjMTZmZmIzNzdjZDRlMWQ4YjRkZWZhMzc1NmQxMjIxYmFjZmUzMzA0ZTc3MDVkZTQxNjdkNzdjMWJhMzY0ODdkODNjNDUyNjAyMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlYmMyNTQyMDBhYTIwMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMDg3NDEyZTkxYTgwMTAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTY4MDkzMzAwYWEyMDJjZjliZjRkYWIwNWEyMzUyMWRjMjYyOTM4MzAyNjJmZmVjNzYxNDY2ODgwOThiYjMxMjRhYzNmYjc2NDAwMzY4N2NmMzYyOGQ3MDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmUyMmRkMTkwMGFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODc1ZmFmYjQ0MzAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlZGYyMzA4MDBhYTIwYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYjg3OTg2OGU4M2UwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTMzOTAwNzAwYWEyMDdjMmJjY2EyMDg5YTlmMGY4ZGFlYzBkM2I0NWE2YzUyNTgxYzU2NzY2NWNjNTFlYmRhOWEyMDJkOGEzM2RjZmM4NzljYzY2OTY4MDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmUyOThkMGMwMGFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODc1YWY0MWYxZjAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlZjliZDAzMDBhYTIwZjFjMzNiYzdkYTA5OTFkMjZmOGJiZjE1MGM5MDM1NjhiMmMzZGM4NzI3MzcwODYzZjRjM2Q5Zjk2NTViMzc5MTg3OGUzZDQ0MWEwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTcxMjgwMzAwYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NzllMzZiNjEzMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmViMjVlMDIwMGFhMjAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0ODc1ZDUxMmExMTAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlNTMxMDAyMDBhYTIwZWZiYzU1NmQ1Y2ZlOTBhNDU4ZGJmNGQ1N2JkNzg3ZjMzNGJhZmIxYTkzM2ZmYzMzMjIyNjdlYmY0NGMzNGZiMDg3OTYzNTI3MGYwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTY5ZDIwMTAwYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4N2E2ZWYxZjBmMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmU4OGQxMDEwMGFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODczNDJhMWYwZjAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlNzFkMTAxMDBhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3NGZmNjFmMGYwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZThhZDEwMTAwYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4NzYxNzdkZjBlMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVjOWM5MDEwMGFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODdmZGFkYjQwZTAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlYTRjNDAxMDBhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3ZDMxYzA1MGUwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTg4YWYwMTAwYWEyMGM4ZGQ5NzdhZTk5MTJiNzIxMzQxYmFmMzA4Njg3YmU1YjllZmIzMmNjMGZiZjI2OTY2NjkwYjYxOThlMjAxY2Y4N2E2YmU2ZjA2MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQyNGM2YWEyMDNmOTkyZDEyOTA0MTEzNzQ4YzQ0NDNjMTY0ZWNmMTJmNDhkMWIwNzUxODA4ZTJiNTM3MTg0OTdlMTJmOTRiNDE4N2Q4MTk1YTA2MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQ4YmMzYWEyMGFhZWE2NDlmNzE5M2FiNDMyOTk5OTkzYmQ4ZDJjY2NlNWZhYmQzYWYwMWViZmU0MWEwZTBhMzU4MTA0NGQ0NmQ4N2VmZjY1ODA2MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQ2N2MzYWEyMDdhZmQ0ZGU4YWZmYjA3Y2I2NjgzMTM0YWQ1OWU5YzdhNzQzYTcwZTYxNGFkNjIxYWZmNWExMTAzN2MwMTE1ZTI4Nzk3ZDU3ZDA3MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQ5YmU2YWEyMDJkZDcwNTVjOTI2MjUzMjUyZWI0NGY3ZWE0MTM5ZGIyNDVlNDkyYWFkYzE5ZTllNWNmZTk1YTJlYzg3Y2IwNjA4N2YyMDNlOTA1MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmRmMWI1YWEyMGE2NGE2NTMwOTE2MzMyOTgwNjI3NWE1YTk5YjI3ODUyNGIyMWE0NjU1OWRlNDdlZmNjZTA5OGQzZWI1YjJhMGU4N2RhNGM1ZTA3MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmRjZmUyYWEyMGFhZWE2NDlmNzE5M2FiNDMyOTk5OTkzYmQ4ZDJjY2NlNWZhYmQzYWYwMWViZmU0MWEwZTBhMzU4MTA0NGQ0NmQ4NzQ1NGZkMzA1MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQ1NWIzYWEyMDdhZmQ0ZGU4YWZmYjA3Y2I2NjgzMTM0YWQ1OWU5YzdhNzQzYTcwZTYxNGFkNjIxYWZmNWExMTAzN2MwMTE1ZTI4N2JhM2YyNzA1MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmRhNDllYWEyMGMzZTdkY2NlNjNlMzdhYjJkZTNiZjhhN2Q5MzJjZmEwMjkzNzQ5MDJjNjMxMGU4ZWRjYjMxMTgxMjM0NTc2N2E4N2NhMGQwODA1MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmRlMTlhYWEyMDFkNzk3NzlkNmI4MjM2MjgxOWNjMWE1YTk1MWI5ZDdjOWY3MmU3YTFhZjQ3NTc3NmQ5NDRmMzU0ZDc2MzM5ZjM4N2ZlYmZiMjA0MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQ5ZjkwYWEyMDQ2MTlmMGIyOWQxZWU3MDU0Njc4NmI4YjMwYzc1YmJlNWIxNWJkYjk4ZDAxYTZhZGQzYTQxNDcwMDBmODZlNjE4Nzk4NzNiNjA0MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQxMTkxYWEyMDhkYWQ3MWE5N2JjMWRjOWUyYmVkOWY2YWExNGJjZTE5ODk4ZDNjZTgxNDJmNjVkMjE4NTgxZGFmNDI3NDUzMWM4NzIwMDMwMDAwMDAwMDAwMDAzZWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmRkOTdmNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWNlYzIxZTVkYTAwMDAwMDAwMTk3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYzJhMDAwMDAwIn0seyJ0eGlkIjoiN2E3ZDYwMTRiMDdhN2EzZTYxODU2N2EyNWQ1YmVhZjNkZTFhMjIxMWZkN2RiZDMyYmRmY2FlODEwNmE2N2NmYyIsImhhc2giOiI3YTdkNjAxNGIwN2E3YTNlNjE4NTY3YTI1ZDViZWFmM2RlMWEyMjExZmQ3ZGJkMzJiZGZjYWU4MTA2YTY3Y2ZjIiwidmVyc2lvbiI6Miwic2l6ZSI6MzY5LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiZTk4YzAzY2JhYWZhZDg1OWU0NDZmMjYwNmU1NDRkZDM2YTgxMWI4ODQzNzc2NzRmOWZjYzdmMzhlMzhhZDg4MSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDQwMjIwNzczZjU3ZmFjMjk4ZjEyNjA2M2FjMDMzZmVhY2ZlYmVlMTdhZTI2ODdjZDg4YmUzYWQxZGFmMDE5NGE2OTIyYTAyMjAzMzg5N2ZlZWM0MjFmYTI2ZTJkYTdhYTViNWY3NGMzMjkwNzViZGIyN2VmMDM5ZGYyODNjMmJlMGJmZmJhZjU4W0FMTHxGT1JLSURdIDMwNDQwMjIwMGI5Mzg1NzZkMDEyYWVhNDAyZDgwYTNiMDYzZWM1YjlmNTZjNjkxMTQwNDM2ZmY1ZDEzNzAxODgxMjIzMjgwNjAyMjAzZWI0MWNiODY3NGMyMmE4YzIzYTcyNzEzMTkwNWMyMjQ2ZjI5MTBjYzAzYWQ4NDU4ZWQxM2NmZWFkMGZjZDlmW0FMTHxGT1JLSURdIDUyMjEwM2U1YjZjNzM2ZjlmM2Q5MjA3MTgxZmYzZGFlMTAyMDY4NDUwZWU5NjdjMzRlMjgwMTljMjA0NzIzMDBkOGM5NTMyMTAyZDJjZmFiMzQxMTdjZGM5ZTc5OTUyMTA1NGFmZTljNjViMTg5YTg3MmEzYjFlYzI1YjQ1MjQ1MTlmNWJkM2I4MDIxMDMyNjAzOTFlZmI1MmM4Zjg4NjBlZWI4YjMzMTljYTg4ZDM2M2Y3NDYwNmIwZDc4Y2JlOWVmODZjYjE5NWQ4MGU5NTNhZSIsImhleCI6IjAwNDczMDQ0MDIyMDc3M2Y1N2ZhYzI5OGYxMjYwNjNhYzAzM2ZlYWNmZWJlZTE3YWUyNjg3Y2Q4OGJlM2FkMWRhZjAxOTRhNjkyMmEwMjIwMzM4OTdmZWVjNDIxZmEyNmUyZGE3YWE1YjVmNzRjMzI5MDc1YmRiMjdlZjAzOWRmMjgzYzJiZTBiZmZiYWY1ODQxNDczMDQ0MDIyMDBiOTM4NTc2ZDAxMmFlYTQwMmQ4MGEzYjA2M2VjNWI5ZjU2YzY5MTE0MDQzNmZmNWQxMzcwMTg4MTIyMzI4MDYwMjIwM2ViNDFjYjg2NzRjMjJhOGMyM2E3MjcxMzE5MDVjMjI0NmYyOTEwY2MwM2FkODQ1OGVkMTNjZmVhZDBmY2Q5ZjQxNGM2OTUyMjEwM2U1YjZjNzM2ZjlmM2Q5MjA3MTgxZmYzZGFlMTAyMDY4NDUwZWU5NjdjMzRlMjgwMTljMjA0NzIzMDBkOGM5NTMyMTAyZDJjZmFiMzQxMTdjZGM5ZTc5OTUyMTA1NGFmZTljNjViMTg5YTg3MmEzYjFlYzI1YjQ1MjQ1MTlmNWJkM2I4MDIxMDMyNjAzOTFlZmI1MmM4Zjg4NjBlZWI4YjMzMTljYTg4ZDM2M2Y3NDYwNmIwZDc4Y2JlOWVmODZjYjE5NWQ4MGU5NTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M31dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDRmMDM3NTRlZjdlYmZiNTY1ZjRkYTRmZjQwNDBmZmE4NDNkZTIwZDggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0ZjAzNzU0ZWY3ZWJmYjU2NWY0ZGE0ZmY0MDQwZmZhODQzZGUyMGQ4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwOHN4YTJ3N2w0bGs0amxma2owN3N6cWw3NXk4aDNxbXF1Z25sMm5wbCJdfX0seyJ2YWx1ZSI6NDI0Ljk4OTk5MjUyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBlZDgwYTkxMDFjMmZmZGUwYzEyMTIwOGFkYjhiNjVkMzdjY2M1N2M3IE9QX0VRVUFMIiwiaGV4IjoiYTkxNGVkODBhOTEwMWMyZmZkZTBjMTIxMjA4YWRiOGI2NWQzN2NjYzU3Yzc4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBya2NwMmdzcnNobG1jeHB5eXNnNGt1dHZoZmhlbnpoY3VuMGVwdTlrcyJdfX1dLCJmZWUiOjcuNDhlLTA2LCJoZXgiOiIwMjAwMDAwMDAxODFkODhhZTMzODdmY2M5ZjRmNjc3NzQzODgxYjgxNmFkMzRkNTQ2ZTYwZjI0NmU0NTlkOGZhYWFjYjAzOGNlOTAxMDAwMDAwZmMwMDQ3MzA0NDAyMjA3NzNmNTdmYWMyOThmMTI2MDYzYWMwMzNmZWFjZmViZWUxN2FlMjY4N2NkODhiZTNhZDFkYWYwMTk0YTY5MjJhMDIyMDMzODk3ZmVlYzQyMWZhMjZlMmRhN2FhNWI1Zjc0YzMyOTA3NWJkYjI3ZWYwMzlkZjI4M2MyYmUwYmZmYmFmNTg0MTQ3MzA0NDAyMjAwYjkzODU3NmQwMTJhZWE0MDJkODBhM2IwNjNlYzViOWY1NmM2OTExNDA0MzZmZjVkMTM3MDE4ODEyMjMyODA2MDIyMDNlYjQxY2I4Njc0YzIyYThjMjNhNzI3MTMxOTA1YzIyNDZmMjkxMGNjMDNhZDg0NThlZDEzY2ZlYWQwZmNkOWY0MTRjNjk1MjIxMDNlNWI2YzczNmY5ZjNkOTIwNzE4MWZmM2RhZTEwMjA2ODQ1MGVlOTY3YzM0ZTI4MDE5YzIwNDcyMzAwZDhjOTUzMjEwMmQyY2ZhYjM0MTE3Y2RjOWU3OTk1MjEwNTRhZmU5YzY1YjE4OWE4NzJhM2IxZWMyNWI0NTI0NTE5ZjViZDNiODAyMTAzMjYwMzkxZWZiNTJjOGY4ODYwZWViOGIzMzE5Y2E4OGQzNjNmNzQ2MDZiMGQ3OGNiZTllZjg2Y2IxOTVkODBlOTUzYWVmZGZmZmZmZjAyNDA0MjBmMDAwMDAwMDAwMDE5NzZhOTE0NGYwMzc1NGVmN2ViZmI1NjVmNGRhNGZmNDA0MGZmYTg0M2RlMjBkODg4YWNkNDQzMjNlNTA5MDAwMDAwMTdhOTE0ZWQ4MGE5MTAxYzJmZmRlMGMxMjEyMDhhZGI4YjY1ZDM3Y2NjNTdjNzg3MDAwMDAwMDAifSx7InR4aWQiOiI3ZWIzNGZiYzZmZWFmMjBhMmUxZDNmNzFiMDFlYzllNDI4NmFjODliYmRjNmQ4NGFlMmJjMjY4YjJkOTgxNjA2IiwiaGFzaCI6IjdlYjM0ZmJjNmZlYWYyMGEyZTFkM2Y3MWIwMWVjOWU0Mjg2YWM4OWJiZGM2ZDg0YWUyYmMyNjhiMmQ5ODE2MDYiLCJ2ZXJzaW9uIjoyLCJzaXplIjoyMjUsImxvY2t0aW1lIjo5NDA4ODMsInZpbiI6W3sidHhpZCI6ImY0ZDJhZTk5ODQxZmUxN2E0OTBmZmViMjM5NDBlYzZjODM1NjRiZGY4YWZiM2RhN2FkNTE1ODk3ZjFjMTY2NTMiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAyYzM3ZWE4YzYwMWJjZTYyZDQxZmUyMzllNjBkMWI1ZGFmMjlkYThkNzBlMjM4ZDY5NzljZjIzMWQ3NGNjOGI2MDIyMDQxNDFiNTE3N2MzOTgwNjMwYTA4OTFmYWYxZGZjMGY2MWMyZThkNWRiZjExNzcxNTA1M2FlYzQ0MDBjYTE3MmZbQUxMfEZPUktJRF0gMDIxYmI4YWE4ZTBjOWJlYzRlZTEwZmY3MzQ4Yzg4YmVlOTA0ODU4YzkxMDI3ZTc0OTg2MDRlZDFiZTMwMmExMDMxIiwiaGV4IjoiNDczMDQ0MDIyMDJjMzdlYThjNjAxYmNlNjJkNDFmZTIzOWU2MGQxYjVkYWYyOWRhOGQ3MGUyMzhkNjk3OWNmMjMxZDc0Y2M4YjYwMjIwNDE0MWI1MTc3YzM5ODA2MzBhMDg5MWZhZjFkZmMwZjYxYzJlOGQ1ZGJmMTE3NzE1MDUzYWVjNDQwMGNhMTcyZjQxMjEwMjFiYjhhYThlMGM5YmVjNGVlMTBmZjczNDhjODhiZWU5MDQ4NThjOTEwMjdlNzQ5ODYwNGVkMWJlMzAyYTEwMzEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9XSwidm91dCI6W3sidmFsdWUiOjAuMjg4NTIyMDgsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA4MTc4NmIzMDk4ZjZmZWFhZjQ5ZGFiODZhY2YzMTczMjM0ZDFkYTYzIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ODE3ODZiMzA5OGY2ZmVhYWY0OWRhYjg2YWNmMzE3MzIzNGQxZGE2Mzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenFoczZlc25ybTBhMmg1bms0Y2R0OG56dWVyZjV3NnZ2Njdjcmp6YTYiXX19LHsidmFsdWUiOjEuNzg4MjU0NywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkzMzI4M2Y1NTY5ZWY3YWY4ODg4YzZkMDI3NGYzYTFiMDZjMDc2NzAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5MzMyODNmNTU2OWVmN2FmODg4OGM2ZDAyNzRmM2ExYjA2YzA3NjcwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6Zm45cWw0MjYwMDB0dWczcnJkcWY2MDhnZHNkc3Jrd3E0cXNrendzaiJdfX1dLCJmZWUiOjIuMjVlLTA2LCJoZXgiOiIwMjAwMDAwMDAxNTM2NmMxZjE5NzU4NTFhZGE3M2RmYjhhZGY0YjU2ODM2Y2VjNDAzOWIyZmUwZjQ5N2FlMTFmODQ5OWFlZDJmNDAyMDAwMDAwNmE0NzMwNDQwMjIwMmMzN2VhOGM2MDFiY2U2MmQ0MWZlMjM5ZTYwZDFiNWRhZjI5ZGE4ZDcwZTIzOGQ2OTc5Y2YyMzFkNzRjYzhiNjAyMjA0MTQxYjUxNzdjMzk4MDYzMGEwODkxZmFmMWRmYzBmNjFjMmU4ZDVkYmYxMTc3MTUwNTNhZWM0NDAwY2ExNzJmNDEyMTAyMWJiOGFhOGUwYzliZWM0ZWUxMGZmNzM0OGM4OGJlZTkwNDg1OGM5MTAyN2U3NDk4NjA0ZWQxYmUzMDJhMTAzMWZlZmZmZmZmMDJmMDNmYjgwMTAwMDAwMDAwMTk3NmE5MTQ4MTc4NmIzMDk4ZjZmZWFhZjQ5ZGFiODZhY2YzMTczMjM0ZDFkYTYzODhhY2ZlYThhODBhMDAwMDAwMDAxOTc2YTkxNDkzMzI4M2Y1NTY5ZWY3YWY4ODg4YzZkMDI3NGYzYTFiMDZjMDc2NzA4OGFjNTM1YjBlMDAifSx7InR4aWQiOiI3ZmVjYzlkZGZlYWQ4OThjMzRiNTFkMzkwYWRiZTFlNTQwZTYxN2M5NjQ5YzRkZmNiNjRlMjJiNGRhYmIxOTE2IiwiaGFzaCI6IjdmZWNjOWRkZmVhZDg5OGMzNGI1MWQzOTBhZGJlMWU1NDBlNjE3Yzk2NDljNGRmY2I2NGUyMmI0ZGFiYjE5MTYiLCJ2ZXJzaW9uIjoyLCJzaXplIjoyMjYsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI4NjRhNTBlNDI4ZWU4YmIzNGZkYzJiOTI3NzVkMzU0MDhhZjYxZTI5YjkyYmRmZDVmOWVkMWExZDliMDg1ZGZkIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBmYWIzNTM1NTgxMjY2MTNlZTM4NWNjZjkyM2I4YjUwOWUwOTQ0NDhiNjAxY2E0ZjdmZmE5ZmVhOWMzZTdjOTgzMDIyMDRjNjJlNjdjODg4OWE5Y2ZkNDM1MGFhOWMxOTQzYTYxMWQ3YzMzMjI0ZmQwM2RhNGRhOWE2YmU2ODVmZDgxNTFbQUxMfEZPUktJRF0gMDMzYjVlMGMwNzA5ZDVjNmI2MTcxOTk0ZDczZTEyY2FkNTQ0Y2QxNzUwYjFiZGIwYWNlZjJlZDQ1ZjNjNzZlNTc4IiwiaGV4IjoiNDgzMDQ1MDIyMTAwZmFiMzUzNTU4MTI2NjEzZWUzODVjY2Y5MjNiOGI1MDllMDk0NDQ4YjYwMWNhNGY3ZmZhOWZlYTljM2U3Yzk4MzAyMjA0YzYyZTY3Yzg4ODlhOWNmZDQzNTBhYTljMTk0M2E2MTFkN2MzMzIyNGZkMDNkYTRkYTlhNmJlNjg1ZmQ4MTUxNDEyMTAzM2I1ZTBjMDcwOWQ1YzZiNjE3MTk5NGQ3M2UxMmNhZDU0NGNkMTc1MGIxYmRiMGFjZWYyZWQ0NWYzYzc2ZTU3OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4xMjAwOSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDAwZWE4YTkxYTcwY2Q0OGM1OTIxNjQzNzBjYWI1MmQwMjIyYTU4MGYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwMGVhOGE5MWE3MGNkNDhjNTkyMTY0MzcwY2FiNTJkMDIyMmE1ODBmODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxcXc0ejUzNXV4ZGZyemV5OWpyd3I5dDJ0Z3p5MmpjcHU0NHRlN3AweCJdfX0seyJ2YWx1ZSI6MTYuOTg1NjI3MTUsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhNmVlMDRjYjc3Mjg3NTlhOGRlZTZiZWQzYTc4MTMxN2EzY2UzZDJlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YTZlZTA0Y2I3NzI4NzU5YThkZWU2YmVkM2E3ODEzMTdhM2NlM2QyZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxem53dXB4dHd1NTh0eDVkYWU0NzZ3bmN6dnQ2OG4zYTljbTRtcjlwdG0iXX19XSwiZmVlIjoyLjdlLTA2LCJoZXgiOiIwMjAwMDAwMDAxZmQ1ZDA4OWIxZDFhZWRmOWQ1ZGYyYmI5MjkxZWY2OGE0MDM1NWQ3NzkyMmJkYzRmYjM4YmVlMjhlNDUwNGE4NjAxMDAwMDAwNmI0ODMwNDUwMjIxMDBmYWIzNTM1NTgxMjY2MTNlZTM4NWNjZjkyM2I4YjUwOWUwOTQ0NDhiNjAxY2E0ZjdmZmE5ZmVhOWMzZTdjOTgzMDIyMDRjNjJlNjdjODg4OWE5Y2ZkNDM1MGFhOWMxOTQzYTYxMWQ3YzMzMjI0ZmQwM2RhNGRhOWE2YmU2ODVmZDgxNTE0MTIxMDMzYjVlMGMwNzA5ZDVjNmI2MTcxOTk0ZDczZTEyY2FkNTQ0Y2QxNzUwYjFiZGIwYWNlZjJlZDQ1ZjNjNzZlNTc4ZmZmZmZmZmYwMjI4M2ViNzAwMDAwMDAwMDAxOTc2YTkxNDAwZWE4YTkxYTcwY2Q0OGM1OTIxNjQzNzBjYWI1MmQwMjIyYTU4MGY4OGFjOWIwMjNlNjUwMDAwMDAwMDE5NzZhOTE0YTZlZTA0Y2I3NzI4NzU5YThkZWU2YmVkM2E3ODEzMTdhM2NlM2QyZTg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjgwNTFkNzBlYWEwYjRiNjNiMTk3ZThkYmYwNTBkMTc4OTY1ZjY2NjhkMDM5M2MwZjg5MjhlZTFlODkzNDBjZTMiLCJoYXNoIjoiODA1MWQ3MGVhYTBiNGI2M2IxOTdlOGRiZjA1MGQxNzg5NjVmNjY2OGQwMzkzYzBmODkyOGVlMWU4OTM0MGNlMyIsInZlcnNpb24iOjEsInNpemUiOjI2MCwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjY4OWIwNDAyYWQyYTYyYWYwOGUzMGE0NjcyODYxM2YyODU2MGNiOTkxYTRhYzIxMGMyZTNiZTJhNWUyYzhiZjQiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMGVlNWYxNGNkNWNhNjQ1Y2I0N2RiOTc5NGE3YTRmZjFjNzYyY2VjMmQ4MDI5NmY0NjUzMjc2YmQxYWM2ZmNiNTkwMjIwNzkwY2NiODg3ZWY4N2I3MTIxMjc3Yzk4OTYxODJiZmQ1MTQxOGM0MGJmYzRlMGViM2I5ODJiYzNhMDk1ZGQyYVtBTEx8Rk9SS0lEXSAwMjA4YzZiNmQ5N2NhM2Q2MjVmYmE1NmI2NDYwYmRjY2ViM2UwNmY1NTU1YzExODQ1OTBiMzMzOWMwZjU4NzliZTEiLCJoZXgiOiI0ODMwNDUwMjIxMDBlZTVmMTRjZDVjYTY0NWNiNDdkYjk3OTRhN2E0ZmYxYzc2MmNlYzJkODAyOTZmNDY1MzI3NmJkMWFjNmZjYjU5MDIyMDc5MGNjYjg4N2VmODdiNzEyMTI3N2M5ODk2MTgyYmZkNTE0MThjNDBiZmM0ZTBlYjNiOTgyYmMzYTA5NWRkMmE0MTIxMDIwOGM2YjZkOTdjYTNkNjI1ZmJhNTZiNjQ2MGJkY2NlYjNlMDZmNTU1NWMxMTg0NTkwYjMzMzljMGY1ODc5YmUxIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjoyLjc0MDY4NTk4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpnMHkyem5qNXZleDkycTdyc2F1ZndtY3F5bmZ4eDg1NWQ4NjZjeTZ0Il19fSx7InZhbHVlIjowLjA1MjM3MjYzLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNmQ4MmMwMTY1ZTQyNjFkZTAwODU0OTJiM2FmYTE1YWIyODM5NjM3MyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDZkODJjMDE2NWU0MjYxZGUwMDg1NDkyYjNhZmExNWFiMjgzOTYzNzM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBrYzlzcWt0ZXB4cmhzcXM0eWprd2g2ems0anN3dHJ3djBqaGxleGVuIl19fSx7InZhbHVlIjowLjAzNjE1ODM3LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZmY0NWE0ZGViODhkZWNkNzhhN2E0NTg3OGY1MGU4N2E5ODYyNmIxOSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGZmNDVhNGRlYjg4ZGVjZDc4YTdhNDU4NzhmNTBlODdhOTg2MjZiMTk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJsNXRmeDdoeng3ZTR1MjBmemMwcjZzYXBhZnNjbnRyeTJjd3Z2ZzVtIl19fV0sImZlZSI6NS4yMmUtMDYsImhleCI6IjAxMDAwMDAwMDFmNDhiMmM1ZTJhYmVlM2MyMTBjMjRhMWE5OWNiNjA4NWYyMTM4NjcyNDYwYWUzMDhhZjYyMmFhZDAyMDQ5YjY4MDAwMDAwMDA2YjQ4MzA0NTAyMjEwMGVlNWYxNGNkNWNhNjQ1Y2I0N2RiOTc5NGE3YTRmZjFjNzYyY2VjMmQ4MDI5NmY0NjUzMjc2YmQxYWM2ZmNiNTkwMjIwNzkwY2NiODg3ZWY4N2I3MTIxMjc3Yzk4OTYxODJiZmQ1MTQxOGM0MGJmYzRlMGViM2I5ODJiYzNhMDk1ZGQyYTQxMjEwMjA4YzZiNmQ5N2NhM2Q2MjVmYmE1NmI2NDYwYmRjY2ViM2UwNmY1NTU1YzExODQ1OTBiMzMzOWMwZjU4NzliZTFmZmZmZmZmZjAzNzZmNDU1MTAwMDAwMDAwMDE5NzZhOTE0OTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNTg4YWMwZmVhNGYwMDAwMDAwMDAwMTk3NmE5MTQ2ZDgyYzAxNjVlNDI2MWRlMDA4NTQ5MmIzYWZhMTVhYjI4Mzk2MzczODhhYzVkMmMzNzAwMDAwMDAwMDAxOTc2YTkxNGZmNDVhNGRlYjg4ZGVjZDc4YTdhNDU4NzhmNTBlODdhOTg2MjZiMTk4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI4MTNjNWRhZjYyZTc1ZGRlYzQ4MDExYmVjYTk5OGUzYTdhOWEyNGMyODcyZDQ5OWVhNGFjYTRjMmM1YTY0NTJkIiwiaGFzaCI6IjgxM2M1ZGFmNjJlNzVkZGVjNDgwMTFiZWNhOTk4ZTNhN2E5YTI0YzI4NzJkNDk5ZWE0YWNhNGMyYzVhNjQ1MmQiLCJ2ZXJzaW9uIjoyLCJzaXplIjoxNjQsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiJlNWI2ZDhhYTI3ZDNjOTBkZWZhYTU3ZTk5NjZjMWRmYTk4MDhjZDZiNWY1MWE5ODcxNzNiYTAwM2Q5ZTcyYTEyIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEiLCJoZXgiOiI0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEifSwic2VxdWVuY2UiOjF9XSwidm91dCI6W3sidmFsdWUiOjAuMDAwNDE3NTcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dLCJmZWUiOjEuNjRlLTA2LCJoZXgiOiIwMjAwMDAwMDAxMTIyYWU3ZDkwM2EwM2IxNzg3YTk1MTVmNmJjZDA4OThmYTFkNmM5NmU5NTdhYWVmMGRjOWQzMjdhYWQ4YjZlNTAwMDAwMDAwNDU0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEwMTAwMDAwMDAxMWRhMzAwMDAwMDAwMDAwMDIzYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NzAwMDAwMDAwIn0seyJ0eGlkIjoiODFmODI4ZTdjYmI0YzEzMDM3ZDkxMjUwNWFhY2Q3OGYxNGEzZmZlZmUxNDdkNmJkNGU1ODQ1NjM4MzQwZWM5NyIsImhhc2giOiI4MWY4MjhlN2NiYjRjMTMwMzdkOTEyNTA1YWFjZDc4ZjE0YTNmZmVmZTE0N2Q2YmQ0ZTU4NDU2MzgzNDBlYzk3IiwidmVyc2lvbiI6Miwic2l6ZSI6MjI1LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiN2VmMTEzYTVkNjQwMGY1Yjg3ZTUyNTRmMGZlZTVhMzE0YjEwMjZiZGEzMWViYWQwY2I1ZTFjMTUwOTAwMGUyZiIsInZvdXQiOjQ1LCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAzMDc5NWM0Yzg4NTNhYTk4NGJkNGE0MGU5YzY2NmUyOGQxMjU2ODRmYTA2MTg0ODU5OWNmZWYxMTIzN2ZiMTEyMDIyMDc4YWVkZDM3OWVkZjM2ZjA5NTI4ZWE1Nzg2NGM1Yzg4OWFlYzM5OGNmMTkwMmQ4MThiMjExNzM4YjljNGZlOTdbQUxMfEZPUktJRF0gMDI4NmFjMWI3OGZmNGMxODBjMmI5NzQ3M2QzZGZmZDRmMTM0MmZiMjZkMGY5Zjg5OWVlMmRhZmNiYzMwNzA2ZGNlIiwiaGV4IjoiNDczMDQ0MDIyMDMwNzk1YzRjODg1M2FhOTg0YmQ0YTQwZTljNjY2ZTI4ZDEyNTY4NGZhMDYxODQ4NTk5Y2ZlZjExMjM3ZmIxMTIwMjIwNzhhZWRkMzc5ZWRmMzZmMDk1MjhlYTU3ODY0YzVjODg5YWVjMzk4Y2YxOTAyZDgxOGIyMTE3MzhiOWM0ZmU5NzQxMjEwMjg2YWMxYjc4ZmY0YzE4MGMyYjk3NDczZDNkZmZkNGYxMzQyZmIyNmQwZjlmODk5ZWUyZGFmY2JjMzA3MDZkY2UifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMDIzMTI0MDcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBlNWJlZGVkYTRiZjY1MDlhODQ2MGM0N2IzMDc5YzU4ZmU2N2RiNTYyIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZTViZWRlZGE0YmY2NTA5YTg0NjBjNDdiMzA3OWM1OGZlNjdkYjU2Mjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcmptYWhrNmYwbTlweDV5dnJ6OGt2cmVjazg3dmxkNHZnYzJubmd1OGwiXX19LHsidmFsdWUiOjAuMDAwNzc1MDcsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAyOTYyZmQ3Yjk2NWZkNzNkNGE0ZGEyNGEzYWQzNjA5ZjllOGEwZmQxIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0Mjk2MmZkN2I5NjVmZDczZDRhNGRhMjRhM2FkMzYwOWY5ZThhMGZkMTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcTVrOWx0bWplMGF3MDIyZmszeTV3a252ejBlYXpzMDZ5anI1ZHE2ZGEiXX19XSwiZmVlIjowLjAwMDIyNiwiaGV4IjoiMDIwMDAwMDAwMTJmMGUwMDA5MTUxYzVlY2JkMGJhMWVhM2JkMjYxMDRiMzE1YWVlMGY0ZjI1ZTU4NzViMGY0MGQ2YTUxM2YxN2UyZDAwMDAwMDZhNDczMDQ0MDIyMDMwNzk1YzRjODg1M2FhOTg0YmQ0YTQwZTljNjY2ZTI4ZDEyNTY4NGZhMDYxODQ4NTk5Y2ZlZjExMjM3ZmIxMTIwMjIwNzhhZWRkMzc5ZWRmMzZmMDk1MjhlYTU3ODY0YzVjODg5YWVjMzk4Y2YxOTAyZDgxOGIyMTE3MzhiOWM0ZmU5NzQxMjEwMjg2YWMxYjc4ZmY0YzE4MGMyYjk3NDczZDNkZmZkNGYxMzQyZmIyNmQwZjlmODk5ZWUyZGFmY2JjMzA3MDZkY2VmZmZmZmZmZjAyZDc0ODIzMDAwMDAwMDAwMDE5NzZhOTE0ZTViZWRlZGE0YmY2NTA5YTg0NjBjNDdiMzA3OWM1OGZlNjdkYjU2Mjg4YWNjMzJlMDEwMDAwMDAwMDAwMTk3NmE5MTQyOTYyZmQ3Yjk2NWZkNzNkNGE0ZGEyNGEzYWQzNjA5ZjllOGEwZmQxODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiODIwMjNkNTgwYzg2N2RmNWYxYmQxMjZkNTg2MTkwZGZmYzQ3MjY0NTgyYTdkZGY5OWMwNWZhMDI1ZGQxMDQxYSIsImhhc2giOiI4MjAyM2Q1ODBjODY3ZGY1ZjFiZDEyNmQ1ODYxOTBkZmZjNDcyNjQ1ODJhN2RkZjk5YzA1ZmEwMjVkZDEwNDFhIiwidmVyc2lvbiI6Miwic2l6ZSI6MzAzMiwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjBkYTJhNDAxYWFjMWZiZDhmZDBlNTU0Y2I3ZWI2ZTk4OWVkMTVkYTBmMGRmNWNhMTViMDIxOGY1ZmUwZWM1MzIiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ0MDIyMDMxYTM4NjcyNzZhY2NkNzU2MjhiNWIwYmMyMGYyYjFkY2U3ZWI1OGUwMjZiODUzMjNmZjdlN2I0YTI4YWEyYjAwMjIwNTQxOGNkZTg1M2FjZWNlMzk4NTc1OGY2NzNkZmJiOTUzOTQwMjBhNmMyMjEzZGJjMzYxMzQ5NzhiMDRmNWNjMFtBTEx8Rk9SS0lEXSAzMDQ0MDIyMDEzOTY4YmI1ZjU0OTdlOGMwYTI0ZGNjNzE4ZWU0YzA4NzRjYjQxZGVhZGUzMzc3ODNmZTM2ZGFkNGJjZTI1NmYwMjIwNTllZTQ5YTU4N2Q0MDJlNGU5MmJhNWFiY2FlODdjZDA0Yjc3ZmU1YjA5MjY4N2YwNzM0YWFhOWJlMDVlYWE2YltBTEx8Rk9SS0lEXSA1MjIxMDIzZWMxY2Q1M2UxYmIwOGNjZjc0MDBiMDdkNzZlNzllNmExMDA0ZGNmMjQyMmQ4MTIyZDRlNmYxNDQ0ZGRjMzU3MjEwMzJjZjA5YWFjMzU2NzkwZmU5YzQ0NDZjMDMyOWNjMjJiODQxNjMzZTg1MTQ4OGU5MjkwMzcwMTkzYzM4NmIzNTMyMTAyOGE2Y2UxZTc2MTEzMmY0Mjk1NTUxY2ZhYzRjYjgyODRhYjI3M2VlOWRiNzMyNDYwN2E0N2UwMzFlZTRkYTg3NTUzYWUiLCJoZXgiOiIwMDQ3MzA0NDAyMjAzMWEzODY3Mjc2YWNjZDc1NjI4YjViMGJjMjBmMmIxZGNlN2ViNThlMDI2Yjg1MzIzZmY3ZTdiNGEyOGFhMmIwMDIyMDU0MThjZGU4NTNhY2VjZTM5ODU3NThmNjczZGZiYjk1Mzk0MDIwYTZjMjIxM2RiYzM2MTM0OTc4YjA0ZjVjYzA0MTQ3MzA0NDAyMjAxMzk2OGJiNWY1NDk3ZThjMGEyNGRjYzcxOGVlNGMwODc0Y2I0MWRlYWRlMzM3NzgzZmUzNmRhZDRiY2UyNTZmMDIyMDU5ZWU0OWE1ODdkNDAyZTRlOTJiYTVhYmNhZTg3Y2QwNGI3N2ZlNWIwOTI2ODdmMDczNGFhYTliZTA1ZWFhNmI0MTRjNjk1MjIxMDIzZWMxY2Q1M2UxYmIwOGNjZjc0MDBiMDdkNzZlNzllNmExMDA0ZGNmMjQyMmQ4MTIyZDRlNmYxNDQ0ZGRjMzU3MjEwMzJjZjA5YWFjMzU2NzkwZmU5YzQ0NDZjMDMyOWNjMjJiODQxNjMzZTg1MTQ4OGU5MjkwMzcwMTkzYzM4NmIzNTMyMTAyOGE2Y2UxZTc2MTEzMmY0Mjk1NTUxY2ZhYzRjYjgyODRhYjI3M2VlOWRiNzMyNDYwN2E0N2UwMzFlZTRkYTg3NTUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9LHsidHhpZCI6IjIxYjViNGE0NTk4ZmY4ZDgxOTRkNzg5NWQxZDNmYTJmMDU1NjQ1NWRjNGZmZTlkYjY0MmE0ODFiODM3ZmQ3NDgiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ0MDIyMDZjNWY1ZDNlNzllZDUzOTk2YzQxOGRiOWNjYWI0NGY3YzMwNmM4NzRjMzkxNDU1OTkzMTk5OWQzMWM2YjBhMjYwMjIwMmM2M2QxZTliOTkxZGU0Yjc4ODY2OTYzZDI0MzFjMTU0Mjk5NTYzMDg1MzdhZTJiZTU3NzdkMjk2YzdmMmQ0OVtBTEx8Rk9SS0lEXSAzMDQ1MDIyMTAwZTRhYjE4NjU5YjJkN2VmMDNiMTU3YjdiYjYzN2RhNzZkN2RhZDkxOTNlMjRlOTllODQzYjZkZjRkZTA4Y2Q4NjAyMjAyMjEwYTNjMzA3MmQ4MjliN2E0MGRmZTFkMjExNTI4YmJjNWFkNmNjZDU5NzM5ZmIyMDQ5ODFiOGI0NWQ3MWU3W0FMTHxGT1JLSURdIDUyMjEwMjNlYzFjZDUzZTFiYjA4Y2NmNzQwMGIwN2Q3NmU3OWU2YTEwMDRkY2YyNDIyZDgxMjJkNGU2ZjE0NDRkZGMzNTcyMTAzMmNmMDlhYWMzNTY3OTBmZTljNDQ0NmMwMzI5Y2MyMmI4NDE2MzNlODUxNDg4ZTkyOTAzNzAxOTNjMzg2YjM1MzIxMDI4YTZjZTFlNzYxMTMyZjQyOTU1NTFjZmFjNGNiODI4NGFiMjczZWU5ZGI3MzI0NjA3YTQ3ZTAzMWVlNGRhODc1NTNhZSIsImhleCI6IjAwNDczMDQ0MDIyMDZjNWY1ZDNlNzllZDUzOTk2YzQxOGRiOWNjYWI0NGY3YzMwNmM4NzRjMzkxNDU1OTkzMTk5OWQzMWM2YjBhMjYwMjIwMmM2M2QxZTliOTkxZGU0Yjc4ODY2OTYzZDI0MzFjMTU0Mjk5NTYzMDg1MzdhZTJiZTU3NzdkMjk2YzdmMmQ0OTQxNDgzMDQ1MDIyMTAwZTRhYjE4NjU5YjJkN2VmMDNiMTU3YjdiYjYzN2RhNzZkN2RhZDkxOTNlMjRlOTllODQzYjZkZjRkZTA4Y2Q4NjAyMjAyMjEwYTNjMzA3MmQ4MjliN2E0MGRmZTFkMjExNTI4YmJjNWFkNmNjZDU5NzM5ZmIyMDQ5ODFiOGI0NWQ3MWU3NDE0YzY5NTIyMTAyM2VjMWNkNTNlMWJiMDhjY2Y3NDAwYjA3ZDc2ZTc5ZTZhMTAwNGRjZjI0MjJkODEyMmQ0ZTZmMTQ0NGRkYzM1NzIxMDMyY2YwOWFhYzM1Njc5MGZlOWM0NDQ2YzAzMjljYzIyYjg0MTYzM2U4NTE0ODhlOTI5MDM3MDE5M2MzODZiMzUzMjEwMjhhNmNlMWU3NjExMzJmNDI5NTU1MWNmYWM0Y2I4Mjg0YWIyNzNlZTlkYjczMjQ2MDdhNDdlMDMxZWU0ZGE4NzU1M2FlIn0sInNlcXVlbmNlIjo0Mjk0OTY3MjkzfSx7InR4aWQiOiI0OTgzNTg0MjdhOWNmMzMyNTNmYWZlYmQ1NDNiZGU5MjZjNDQyOTZhNzc4NjY0YjA5OTBjYTNiNDgzOTI4ZDQwIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjAgMzA0NTAyMjEwMDkxNTZiNDgyZjYwMTRkZTYyNDdjZmFiYzRlNTZjMTMxM2NmMzI2OTg2ZjUyYzJiMWVhMDhjNmQ0YjMxNjY3MjEwMjIwNzkxMDhhZWNhODEyMjJmYTVlMjJjNGQyOGQzMjM4YWEzMWQ4M2MxNDc3YzFmZjIzMTk4ZWFkZDBhYjRhMWYyYVtBTEx8Rk9SS0lEXSAzMDQ1MDIyMTAwYjdhOWM5OTQ4MDY0ZWNkYTMyOTdlNGM1MzRhZWY5N2I0MWIwYzk2ZDZjM2E0NDU0ZDM2NjhiZTJhZDM1NDY1YjAyMjA0ZjZlMGZkOTg2ZjdjOGZjOTQ5ZGUxZDU0ODgwYmFkNTZkNjNjM2Y4NjE3OWI1MDA4MGM5MDljYzNlY2RlNDY5W0FMTHxGT1JLSURdIDUyMjEwM2I0OTMwMTE4MzkwODIwMGIxZGU2YWJjMGVmYTZhODM5MGJhYjYzN2ViYzUxMGQyMmY3ZTFiNWQzMmJiYjcwNDkyMTAyMmViOWEyN2Y1ZmU3Yzg5MWY0ZGU2Y2JkMGU5ZDMwZjkzMDMxYmI4ZmMyYmY4YzcwNjFlYTE2NzFlMjVjYjI0MTIxMDNiNGViYTdjNTMyMmVhNTc4YmI0NTg5ZjZiZTVjZTQ3NDM0M2I2NGNmYzg1MmVkYTMxMDJjMGRmNmIzNjY0MjZjNTNhZSIsImhleCI6IjAwNDgzMDQ1MDIyMTAwOTE1NmI0ODJmNjAxNGRlNjI0N2NmYWJjNGU1NmMxMzEzY2YzMjY5ODZmNTJjMmIxZWEwOGM2ZDRiMzE2NjcyMTAyMjA3OTEwOGFlY2E4MTIyMmZhNWUyMmM0ZDI4ZDMyMzhhYTMxZDgzYzE0NzdjMWZmMjMxOThlYWRkMGFiNGExZjJhNDE0ODMwNDUwMjIxMDBiN2E5Yzk5NDgwNjRlY2RhMzI5N2U0YzUzNGFlZjk3YjQxYjBjOTZkNmMzYTQ0NTRkMzY2OGJlMmFkMzU0NjViMDIyMDRmNmUwZmQ5ODZmN2M4ZmM5NDlkZTFkNTQ4ODBiYWQ1NmQ2M2MzZjg2MTc5YjUwMDgwYzkwOWNjM2VjZGU0Njk0MTRjNjk1MjIxMDNiNDkzMDExODM5MDgyMDBiMWRlNmFiYzBlZmE2YTgzOTBiYWI2MzdlYmM1MTBkMjJmN2UxYjVkMzJiYmI3MDQ5MjEwMjJlYjlhMjdmNWZlN2M4OTFmNGRlNmNiZDBlOWQzMGY5MzAzMWJiOGZjMmJmOGM3MDYxZWExNjcxZTI1Y2IyNDEyMTAzYjRlYmE3YzUzMjJlYTU3OGJiNDU4OWY2YmU1Y2U0NzQzNDNiNjRjZmM4NTJlZGEzMTAyYzBkZjZiMzY2NDI2YzUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9LHsidHhpZCI6IjU2NDFkNTQ2NGFmM2JkYzU1NTNkMDFmMTA1OGMyY2FmODQxODk2OWVmMDY5ZDM4MDJkM2EzNTU1MDdlMjQyOTUiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ0MDIyMDFiNTNlMjc2MDA2OTAwY2E0NTQxM2U2ZGVhMjc1NTA4OGU5MzYxNmJhOWM4YjNhN2UzNzQ1ZTVmZmU2NjUxODkwMjIwMzdmYTg2NGQzNDczMmEyY2Q5MzhkNjllNDZlZDBhNWE5YjkzMTBhYjg5NWRmMTBjYTQ1NGYwMWE2MWMwZTczOFtBTEx8Rk9SS0lEXSAzMDQ1MDIyMTAwZGM2Njk2NjRiZTRlNmEwMDRkZDNiZTY4OTY1NTRmMjgzMTRmOTZkMzA3Mjk0OTdiZjAzYzZhOTcwMGQ1M2E0NTAyMjA3YThkZDE2NTc4YTIwODA1ZmMyNzc5NjgxMGFmN2IxZWMwZTc1NTgxMWM5ZWZlZWU2OTZlOGE3NmU1ZjA3ODNmW0FMTHxGT1JLSURdIDUyMjEwMmYyZDE4MTQxY2Y5OWJjZTMyNjVmMWIyMTgzMzVlZWQyZWNkNmE4OTFlYzFmYjM0ZWIwYmVmNjZmMjU2NjFkYmUyMTAyNTM0MmFmNWI2ODdmYzEwNmRhNjZiNTJiZjkzOTRkNTE4MTU4NmM4YTE5ZTg4YTExMTIxMWJmZDM2M2IzNjE3YjIxMDJmYjNlOTNmMDcyN2ExYTI1MTliZGMyYWM1MmJkN2M1ODgxN2U4ZjU3MTAxNDczMzhhNzlmNDU2MzJhYWEzNmRmNTNhZSIsImhleCI6IjAwNDczMDQ0MDIyMDFiNTNlMjc2MDA2OTAwY2E0NTQxM2U2ZGVhMjc1NTA4OGU5MzYxNmJhOWM4YjNhN2UzNzQ1ZTVmZmU2NjUxODkwMjIwMzdmYTg2NGQzNDczMmEyY2Q5MzhkNjllNDZlZDBhNWE5YjkzMTBhYjg5NWRmMTBjYTQ1NGYwMWE2MWMwZTczODQxNDgzMDQ1MDIyMTAwZGM2Njk2NjRiZTRlNmEwMDRkZDNiZTY4OTY1NTRmMjgzMTRmOTZkMzA3Mjk0OTdiZjAzYzZhOTcwMGQ1M2E0NTAyMjA3YThkZDE2NTc4YTIwODA1ZmMyNzc5NjgxMGFmN2IxZWMwZTc1NTgxMWM5ZWZlZWU2OTZlOGE3NmU1ZjA3ODNmNDE0YzY5NTIyMTAyZjJkMTgxNDFjZjk5YmNlMzI2NWYxYjIxODMzNWVlZDJlY2Q2YTg5MWVjMWZiMzRlYjBiZWY2NmYyNTY2MWRiZTIxMDI1MzQyYWY1YjY4N2ZjMTA2ZGE2NmI1MmJmOTM5NGQ1MTgxNTg2YzhhMTllODhhMTExMjExYmZkMzYzYjM2MTdiMjEwMmZiM2U5M2YwNzI3YTFhMjUxOWJkYzJhYzUyYmQ3YzU4ODE3ZThmNTcxMDE0NzMzOGE3OWY0NTYzMmFhYTM2ZGY1M2FlIn0sInNlcXVlbmNlIjo0Mjk0OTY3MjkzfSx7InR4aWQiOiI4ZGNmZGY4ZGQxZTYxYWU2Nzg5OWJlNmYwMzk1M2ViZWRiYWQyMDlkNDUxNzVmMmViMjBlZjljNzE1ZWU0MGYzIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjAgMzA0NDAyMjA3YjIwZWRkZTMwM2ZiMzg2ZjA1NWI4MzJjYTE4NTI5ZWU0MGVkZDQ1Y2EyOGM4NDUxZjg2M2I4MDQ3NjJhNmQ2MDIyMDVhMmY1YTA1ZGMxZTA2MmI1MWY1OTBiZDc3MmM2MjIzODU4MGVmYTA4NjhlZWFmY2VjODk3ZGFiNTgwZjFiMzBbQUxMfEZPUktJRF0gMzA0NTAyMjEwMDljMGRkM2VhOTRmODAxMmM2ODFhNDgxMDUwNTA0N2M3NWIwMzc4YjI5MThjMjg5ZDc0ZjBlMmVlZWE4NGE5MDQwMjIwMDZkMDJmNGU4OGQ4OGYyNjIyMGM4N2QyMWRjODRmZDU4ZjY5ZTY1ZmUwNTdmNmI5Njk4ZTBjOGE3N2U4MGNlM1tBTEx8Rk9SS0lEXSA1MjIxMDIzZWMxY2Q1M2UxYmIwOGNjZjc0MDBiMDdkNzZlNzllNmExMDA0ZGNmMjQyMmQ4MTIyZDRlNmYxNDQ0ZGRjMzU3MjEwMzJjZjA5YWFjMzU2NzkwZmU5YzQ0NDZjMDMyOWNjMjJiODQxNjMzZTg1MTQ4OGU5MjkwMzcwMTkzYzM4NmIzNTMyMTAyOGE2Y2UxZTc2MTEzMmY0Mjk1NTUxY2ZhYzRjYjgyODRhYjI3M2VlOWRiNzMyNDYwN2E0N2UwMzFlZTRkYTg3NTUzYWUiLCJoZXgiOiIwMDQ3MzA0NDAyMjA3YjIwZWRkZTMwM2ZiMzg2ZjA1NWI4MzJjYTE4NTI5ZWU0MGVkZDQ1Y2EyOGM4NDUxZjg2M2I4MDQ3NjJhNmQ2MDIyMDVhMmY1YTA1ZGMxZTA2MmI1MWY1OTBiZDc3MmM2MjIzODU4MGVmYTA4NjhlZWFmY2VjODk3ZGFiNTgwZjFiMzA0MTQ4MzA0NTAyMjEwMDljMGRkM2VhOTRmODAxMmM2ODFhNDgxMDUwNTA0N2M3NWIwMzc4YjI5MThjMjg5ZDc0ZjBlMmVlZWE4NGE5MDQwMjIwMDZkMDJmNGU4OGQ4OGYyNjIyMGM4N2QyMWRjODRmZDU4ZjY5ZTY1ZmUwNTdmNmI5Njk4ZTBjOGE3N2U4MGNlMzQxNGM2OTUyMjEwMjNlYzFjZDUzZTFiYjA4Y2NmNzQwMGIwN2Q3NmU3OWU2YTEwMDRkY2YyNDIyZDgxMjJkNGU2ZjE0NDRkZGMzNTcyMTAzMmNmMDlhYWMzNTY3OTBmZTljNDQ0NmMwMzI5Y2MyMmI4NDE2MzNlODUxNDg4ZTkyOTAzNzAxOTNjMzg2YjM1MzIxMDI4YTZjZTFlNzYxMTMyZjQyOTU1NTFjZmFjNGNiODI4NGFiMjczZWU5ZGI3MzI0NjA3YTQ3ZTAzMWVlNGRhODc1NTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiOTY4MmUwNjM4ODRkODdhODE2NDNhZjMyMzAxNTUxMmY2YWVhNmM0YjZkMjI4NGE5ZjQ0NmY4Yzc3NDVjOWYwNiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDUwMjIxMDBhOTU2YTY5YTAyYTFhMjhiN2YxYjgwMTcxYzBjOGJhYjc1MjQ2ZjM5YTRiYTcwZjMwM2UxZDU0NTI4ODZhMDZkMDIyMDJkMDA4ZTU2OTg2ZGZmYmViMjM0NmU1YTk4MjlmMWE3NmVkNTFhNzRhYjNhMmYwN2Y2NWE0YmU1YWMxN2RlZTNbQUxMfEZPUktJRF0gMzA0NDAyMjA1ZGQzNWJjZWU5YjdkNGRkNmEzNTBiNDc0NmZmZWFlNWY3ZDFjODFjMTU0YjQ0YmY3YzlhMjIxNjQ5ZGY5NmZjMDIyMDdmZjY2OTJmMTkxYTNkODYyYmYyYWUzYzBiZjM2NTQ1NTk3OTIyNWQwNTUyZGRkMWYyYmY0M2VmZGI0NzE3NjZbQUxMfEZPUktJRF0gNTIyMTAzNDcwYjY4NTNmNGFiYjRkYjAxMGFjMmMwYmM5YjhjOTJiM2UyNTkyOTQyMjdiOGQwMGM4MDMxZDExMmIyODI4NjIxMDMyNzAwZTdiYzEwMDgzYTc0ZTVlZmZjNjBjYTg4MzY5ZmZkZmQzOWIxMmY1OGMwYjdhZTY5MWY4MTZkZWZmMmZlMjEwMmJkYzZkNmVjZGU5ZDI5NzJhZTRiYzkxYTFlMjg3OTM4M2MxMmYxMjdjYjczMWEzNTJhNDQyOTNhOTdjOGIyMmI1M2FlIiwiaGV4IjoiMDA0ODMwNDUwMjIxMDBhOTU2YTY5YTAyYTFhMjhiN2YxYjgwMTcxYzBjOGJhYjc1MjQ2ZjM5YTRiYTcwZjMwM2UxZDU0NTI4ODZhMDZkMDIyMDJkMDA4ZTU2OTg2ZGZmYmViMjM0NmU1YTk4MjlmMWE3NmVkNTFhNzRhYjNhMmYwN2Y2NWE0YmU1YWMxN2RlZTM0MTQ3MzA0NDAyMjA1ZGQzNWJjZWU5YjdkNGRkNmEzNTBiNDc0NmZmZWFlNWY3ZDFjODFjMTU0YjQ0YmY3YzlhMjIxNjQ5ZGY5NmZjMDIyMDdmZjY2OTJmMTkxYTNkODYyYmYyYWUzYzBiZjM2NTQ1NTk3OTIyNWQwNTUyZGRkMWYyYmY0M2VmZGI0NzE3NjY0MTRjNjk1MjIxMDM0NzBiNjg1M2Y0YWJiNGRiMDEwYWMyYzBiYzliOGM5MmIzZTI1OTI5NDIyN2I4ZDAwYzgwMzFkMTEyYjI4Mjg2MjEwMzI3MDBlN2JjMTAwODNhNzRlNWVmZmM2MGNhODgzNjlmZmRmZDM5YjEyZjU4YzBiN2FlNjkxZjgxNmRlZmYyZmUyMTAyYmRjNmQ2ZWNkZTlkMjk3MmFlNGJjOTFhMWUyODc5MzgzYzEyZjEyN2NiNzMxYTM1MmE0NDI5M2E5N2M4YjIyYjUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9LHsidHhpZCI6ImE1OTNjMGZlYjU4YWFiOGQ5YjA5YWQ5MWFjODc5ZjI3MTU1NmIzNTVmZDQ3NDdjYThiM2NhOWI0MzdiOWZmOTkiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ1MDIyMTAwYzA5YTU3Y2Y0MDYxNTAzYjY0MGYxMWQzYjU1M2ViZmNkNTZhZGI1NjFhNWI0OGE1NWYzNWVlZGU4Y2JiYzY1MzAyMjA2Y2U3NGUyMjEyYzVhMjczOGRiZGZmZWFmNWQyN2E1OWRhNjMwMWNlMmQzMTVkOWYzM2MyMzc0YzIzNDA4ZDJlW0FMTHxGT1JLSURdIDMwNDUwMjIxMDBmMzg5NzUxYWFkYzUwM2M4Mzk0NjM2N2FmY2Y2NjExMzRjYmRjNTIxZGRmNDYwYmU3ZWFkYzBlMTkyMTU3MjI5MDIyMDZjYWJiOTY2MTE4MzAxYTZkODA1YmMxM2FiYjg2ZTM3MWY1N2M4YjY5NmRjZjE0MjJiNGRmMmE0MDAxZDNhZWNbQUxMfEZPUktJRF0gNTIyMTAzNDcwYjY4NTNmNGFiYjRkYjAxMGFjMmMwYmM5YjhjOTJiM2UyNTkyOTQyMjdiOGQwMGM4MDMxZDExMmIyODI4NjIxMDMyNzAwZTdiYzEwMDgzYTc0ZTVlZmZjNjBjYTg4MzY5ZmZkZmQzOWIxMmY1OGMwYjdhZTY5MWY4MTZkZWZmMmZlMjEwMmJkYzZkNmVjZGU5ZDI5NzJhZTRiYzkxYTFlMjg3OTM4M2MxMmYxMjdjYjczMWEzNTJhNDQyOTNhOTdjOGIyMmI1M2FlIiwiaGV4IjoiMDA0ODMwNDUwMjIxMDBjMDlhNTdjZjQwNjE1MDNiNjQwZjExZDNiNTUzZWJmY2Q1NmFkYjU2MWE1YjQ4YTU1ZjM1ZWVkZThjYmJjNjUzMDIyMDZjZTc0ZTIyMTJjNWEyNzM4ZGJkZmZlYWY1ZDI3YTU5ZGE2MzAxY2UyZDMxNWQ5ZjMzYzIzNzRjMjM0MDhkMmU0MTQ4MzA0NTAyMjEwMGYzODk3NTFhYWRjNTAzYzgzOTQ2MzY3YWZjZjY2MTEzNGNiZGM1MjFkZGY0NjBiZTdlYWRjMGUxOTIxNTcyMjkwMjIwNmNhYmI5NjYxMTgzMDFhNmQ4MDViYzEzYWJiODZlMzcxZjU3YzhiNjk2ZGNmMTQyMmI0ZGYyYTQwMDFkM2FlYzQxNGM2OTUyMjEwMzQ3MGI2ODUzZjRhYmI0ZGIwMTBhYzJjMGJjOWI4YzkyYjNlMjU5Mjk0MjI3YjhkMDBjODAzMWQxMTJiMjgyODYyMTAzMjcwMGU3YmMxMDA4M2E3NGU1ZWZmYzYwY2E4ODM2OWZmZGZkMzliMTJmNThjMGI3YWU2OTFmODE2ZGVmZjJmZTIxMDJiZGM2ZDZlY2RlOWQyOTcyYWU0YmM5MWExZTI4NzkzODNjMTJmMTI3Y2I3MzFhMzUyYTQ0MjkzYTk3YzhiMjJiNTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiYzU2ZTM5N2Y3MjI5YTE5ZGUyOGVlZjY3MTZiZDg1YzY5NzBlNGNmNGY3MjBmYjVlZDE3OTJhMmRkOGY5N2VmNyIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDQwMjIwM2E2NTE2OTYyMjkzY2QxZjQ4MTRmMGMyMjc3YTMyNWI4MjAxYWVkYjIwZWNmOWJlNzRiYzU1YTdmNjVmMTQxMDAyMjA1NDI4YzI0NDljNTk3MWM2MjRhZTY3ZDA2NTgyMDhkZWQyMDZmN2UyNTVjNTA3OGU0YzU3NmQ4ZTA5ZmU1NTFmW0FMTHxGT1JLSURdIDMwNDUwMjIxMDA5ZmI2YmQwNWEwZmVjNmI0MDkyM2NjOGM5MjQ3NTFlZTlhODVhOTFlZjdjZGQwZDVlZjcwNTc0MGNhYjI5OTliMDIyMDU0YmEzZGRlMDIyZGNjMmRiMjJmZTAxZjI1MWQ4YjExMmViOGM5MjQ3MDY5NWZlNGE3NmNmZWJjOTFlYzVmYTdbQUxMfEZPUktJRF0gNTIyMTAyM2VjMWNkNTNlMWJiMDhjY2Y3NDAwYjA3ZDc2ZTc5ZTZhMTAwNGRjZjI0MjJkODEyMmQ0ZTZmMTQ0NGRkYzM1NzIxMDMyY2YwOWFhYzM1Njc5MGZlOWM0NDQ2YzAzMjljYzIyYjg0MTYzM2U4NTE0ODhlOTI5MDM3MDE5M2MzODZiMzUzMjEwMjhhNmNlMWU3NjExMzJmNDI5NTU1MWNmYWM0Y2I4Mjg0YWIyNzNlZTlkYjczMjQ2MDdhNDdlMDMxZWU0ZGE4NzU1M2FlIiwiaGV4IjoiMDA0NzMwNDQwMjIwM2E2NTE2OTYyMjkzY2QxZjQ4MTRmMGMyMjc3YTMyNWI4MjAxYWVkYjIwZWNmOWJlNzRiYzU1YTdmNjVmMTQxMDAyMjA1NDI4YzI0NDljNTk3MWM2MjRhZTY3ZDA2NTgyMDhkZWQyMDZmN2UyNTVjNTA3OGU0YzU3NmQ4ZTA5ZmU1NTFmNDE0ODMwNDUwMjIxMDA5ZmI2YmQwNWEwZmVjNmI0MDkyM2NjOGM5MjQ3NTFlZTlhODVhOTFlZjdjZGQwZDVlZjcwNTc0MGNhYjI5OTliMDIyMDU0YmEzZGRlMDIyZGNjMmRiMjJmZTAxZjI1MWQ4YjExMmViOGM5MjQ3MDY5NWZlNGE3NmNmZWJjOTFlYzVmYTc0MTRjNjk1MjIxMDIzZWMxY2Q1M2UxYmIwOGNjZjc0MDBiMDdkNzZlNzllNmExMDA0ZGNmMjQyMmQ4MTIyZDRlNmYxNDQ0ZGRjMzU3MjEwMzJjZjA5YWFjMzU2NzkwZmU5YzQ0NDZjMDMyOWNjMjJiODQxNjMzZTg1MTQ4OGU5MjkwMzcwMTkzYzM4NmIzNTMyMTAyOGE2Y2UxZTc2MTEzMmY0Mjk1NTUxY2ZhYzRjYjgyODRhYjI3M2VlOWRiNzMyNDYwN2E0N2UwMzFlZTRkYTg3NTUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9LHsidHhpZCI6ImRlODQzOWU4YWExZGM3MGEyZDRhODY5Mzc2ZDc3ZmZjNzA5YjVhNzQ2YTI5Mzc1ZTgwOGIwMmNkNmE3N2M1MWIiLCJ2b3V0Ijo1NjUsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDQwMjIwNDAwMzYyNzFkZmEyOTQ2ZWI3OWRlY2EzNzI2Mzc1YTM5NjUzYmM0M2UzNzg1MWRhMGYzZjc4ZjNkNzZhZGY2YzAyMjA2ODhkZDAzODc3NWZiMGU1MjE4MDA2ODVjYzIyMGVlZTA4NWRhODI5YmU4NDM3ODEzMWVkYTNlY2U1OGQzZjdmW0FMTHxGT1JLSURdIDMwNDQwMjIwMWQwYWUxM2E2MGFjN2I3ZjI3ZWM4NDZlZGQ4ZTQxMzg5NTc0N2M3YzVkMWRlNmM0YWFkNzFiNjlkYzIxODBhODAyMjA1MjlkN2Q0ZjFlYTU0ZWUxNWVlM2JhMzE1OWI0NzhhYjI3MmY3ZTM0ZDcyZmJkMjY4ZGNkMWY4OTE1ODY2ZjIzW0FMTHxGT1JLSURdIDUyMjEwMmY3NGE3M2E4ZWMxNWQxYzcxNDI0NjVmZmUxNTM4ZTA3ZTRhN2ExNmFjYjc3MWQyOTM0YWJmYTcyYTNlMTEwMzIyMTAyOWUzMjQzMmQwODhiOGUyYWZjMTBjNzRhMjVmM2I5ZmI1OTRlODAyZDAxZGI5MjEyZDAzYTM1YzhmNDJkZmM2YTIxMDI5NWE3YWZlNmZhNjkzZWEzZTU4NjFhYTAzNzliMTliYmY2NWE2NWJhMzQ4MmE3ZGFjMTVhNmY5NTQzNjgwOTJkNTNhZSIsImhleCI6IjAwNDczMDQ0MDIyMDQwMDM2MjcxZGZhMjk0NmViNzlkZWNhMzcyNjM3NWEzOTY1M2JjNDNlMzc4NTFkYTBmM2Y3OGYzZDc2YWRmNmMwMjIwNjg4ZGQwMzg3NzVmYjBlNTIxODAwNjg1Y2MyMjBlZWUwODVkYTgyOWJlODQzNzgxMzFlZGEzZWNlNThkM2Y3ZjQxNDczMDQ0MDIyMDFkMGFlMTNhNjBhYzdiN2YyN2VjODQ2ZWRkOGU0MTM4OTU3NDdjN2M1ZDFkZTZjNGFhZDcxYjY5ZGMyMTgwYTgwMjIwNTI5ZDdkNGYxZWE1NGVlMTVlZTNiYTMxNTliNDc4YWIyNzJmN2UzNGQ3MmZiZDI2OGRjZDFmODkxNTg2NmYyMzQxNGM2OTUyMjEwMmY3NGE3M2E4ZWMxNWQxYzcxNDI0NjVmZmUxNTM4ZTA3ZTRhN2ExNmFjYjc3MWQyOTM0YWJmYTcyYTNlMTEwMzIyMTAyOWUzMjQzMmQwODhiOGUyYWZjMTBjNzRhMjVmM2I5ZmI1OTRlODAyZDAxZGI5MjEyZDAzYTM1YzhmNDJkZmM2YTIxMDI5NWE3YWZlNmZhNjkzZWEzZTU4NjFhYTAzNzliMTliYmY2NWE2NWJhMzQ4MmE3ZGFjMTVhNmY5NTQzNjgwOTJkNTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiZWZiYjBiNDc1MTc0YjNkMjEyZTMxZDQ0N2FjYzY0YmU3NzQzODcxZjUzYjU0MjlmMGFlNzI4YzJiNDcyZjQxZiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDUwMjIxMDA5Njc5MTYyYzU2NDc2NTI0MmVlZTg5NDJhZGRmZmY4NTIyZjU4YjMzZDI1ZGZiM2Q2OTg0Y2MxMDc2NmU0MTJhMDIyMDdiNjA1NGNmZmRlNjVkOTY2M2FmYzkyMDEyMDJkODA0OTM3MDg4NDkxNjIyODMxMWI2MjZjOTRkMGM4ZWJlNzBbQUxMfEZPUktJRF0gMzA0NDAyMjAzNzRkYTUxOTk5NGI0OWUyM2YxN2RlNWJlYzU4Mzc4MDI3YjFhNmJmOWQzMWFlMDFiOWRkZDEwMjZlYTFjOTE0MDIyMDNiNWVlZmQyNGExYzM4ZDNlNjFkOTE4N2MzYmU2N2FlZTA1YWFjMjY4ZWZjYTc2NGI5ZmJhNjBlMTM0YzA2ZDdbQUxMfEZPUktJRF0gNTIyMTAzNDcwYjY4NTNmNGFiYjRkYjAxMGFjMmMwYmM5YjhjOTJiM2UyNTkyOTQyMjdiOGQwMGM4MDMxZDExMmIyODI4NjIxMDMyNzAwZTdiYzEwMDgzYTc0ZTVlZmZjNjBjYTg4MzY5ZmZkZmQzOWIxMmY1OGMwYjdhZTY5MWY4MTZkZWZmMmZlMjEwMmJkYzZkNmVjZGU5ZDI5NzJhZTRiYzkxYTFlMjg3OTM4M2MxMmYxMjdjYjczMWEzNTJhNDQyOTNhOTdjOGIyMmI1M2FlIiwiaGV4IjoiMDA0ODMwNDUwMjIxMDA5Njc5MTYyYzU2NDc2NTI0MmVlZTg5NDJhZGRmZmY4NTIyZjU4YjMzZDI1ZGZiM2Q2OTg0Y2MxMDc2NmU0MTJhMDIyMDdiNjA1NGNmZmRlNjVkOTY2M2FmYzkyMDEyMDJkODA0OTM3MDg4NDkxNjIyODMxMWI2MjZjOTRkMGM4ZWJlNzA0MTQ3MzA0NDAyMjAzNzRkYTUxOTk5NGI0OWUyM2YxN2RlNWJlYzU4Mzc4MDI3YjFhNmJmOWQzMWFlMDFiOWRkZDEwMjZlYTFjOTE0MDIyMDNiNWVlZmQyNGExYzM4ZDNlNjFkOTE4N2MzYmU2N2FlZTA1YWFjMjY4ZWZjYTc2NGI5ZmJhNjBlMTM0YzA2ZDc0MTRjNjk1MjIxMDM0NzBiNjg1M2Y0YWJiNGRiMDEwYWMyYzBiYzliOGM5MmIzZTI1OTI5NDIyN2I4ZDAwYzgwMzFkMTEyYjI4Mjg2MjEwMzI3MDBlN2JjMTAwODNhNzRlNWVmZmM2MGNhODgzNjlmZmRmZDM5YjEyZjU4YzBiN2FlNjkxZjgxNmRlZmYyZmUyMTAyYmRjNmQ2ZWNkZTlkMjk3MmFlNGJjOTFhMWUyODc5MzgzYzEyZjEyN2NiNzMxYTM1MmE0NDI5M2E5N2M4YjIyYjUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9XSwidm91dCI6W3sidmFsdWUiOjAuNTMwNDA5MjUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIDVmZjBiYmQxNDc5MTVlYjkyMTJhYzZiNDc5NDRhMGMwZmZjNzA3NjQgT1BfRVFVQUwiLCJoZXgiOiJhOTE0NWZmMGJiZDE0NzkxNWViOTIxMmFjNmI0Nzk0NGEwYzBmZmM3MDc2NDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHAwbHB3NzNnN2c0YXdmcDl0cnRnNzJ5NXJxMGwzYzh2c2Y3a2FzN2t2Il19fSx7InZhbHVlIjo2LjAyODUxLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTczNjI3MjU4NzAzOTJiZDZhYWM2OTg4MmJmZTcwYzdkOGQ3YTg3YiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGE3MzYyNzI1ODcwMzkyYmQ2YWFjNjk4ODJiZmU3MGM3ZDhkN2E4N2I4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpubnZmZTlzdXBlOTB0MjQzNWNzMmw3d3JyYTM0YWcwdnVydDRmN2VhIl19fV0sImZlZSI6Ni4xMTJlLTA1LCJoZXgiOiIwMjAwMDAwMDBhMzJjNTBlZmVmNTE4MDI1YmExNWNkZmYwYTA1ZGQxOWU5ODZlZWJiNzRjNTUwZWZkZDhmYmMxYWEwMWE0YTIwZDAwMDAwMDAwZmMwMDQ3MzA0NDAyMjAzMWEzODY3Mjc2YWNjZDc1NjI4YjViMGJjMjBmMmIxZGNlN2ViNThlMDI2Yjg1MzIzZmY3ZTdiNGEyOGFhMmIwMDIyMDU0MThjZGU4NTNhY2VjZTM5ODU3NThmNjczZGZiYjk1Mzk0MDIwYTZjMjIxM2RiYzM2MTM0OTc4YjA0ZjVjYzA0MTQ3MzA0NDAyMjAxMzk2OGJiNWY1NDk3ZThjMGEyNGRjYzcxOGVlNGMwODc0Y2I0MWRlYWRlMzM3NzgzZmUzNmRhZDRiY2UyNTZmMDIyMDU5ZWU0OWE1ODdkNDAyZTRlOTJiYTVhYmNhZTg3Y2QwNGI3N2ZlNWIwOTI2ODdmMDczNGFhYTliZTA1ZWFhNmI0MTRjNjk1MjIxMDIzZWMxY2Q1M2UxYmIwOGNjZjc0MDBiMDdkNzZlNzllNmExMDA0ZGNmMjQyMmQ4MTIyZDRlNmYxNDQ0ZGRjMzU3MjEwMzJjZjA5YWFjMzU2NzkwZmU5YzQ0NDZjMDMyOWNjMjJiODQxNjMzZTg1MTQ4OGU5MjkwMzcwMTkzYzM4NmIzNTMyMTAyOGE2Y2UxZTc2MTEzMmY0Mjk1NTUxY2ZhYzRjYjgyODRhYjI3M2VlOWRiNzMyNDYwN2E0N2UwMzFlZTRkYTg3NTUzYWVmZGZmZmZmZjQ4ZDc3ZjgzMWI0ODJhNjRkYmU5ZmZjNDVkNDU1NjA1MmZmYWQzZDE5NTc4NGQxOWQ4Zjg4ZjU5YTRiNGI1MjEwMDAwMDAwMGZkZmQwMDAwNDczMDQ0MDIyMDZjNWY1ZDNlNzllZDUzOTk2YzQxOGRiOWNjYWI0NGY3YzMwNmM4NzRjMzkxNDU1OTkzMTk5OWQzMWM2YjBhMjYwMjIwMmM2M2QxZTliOTkxZGU0Yjc4ODY2OTYzZDI0MzFjMTU0Mjk5NTYzMDg1MzdhZTJiZTU3NzdkMjk2YzdmMmQ0OTQxNDgzMDQ1MDIyMTAwZTRhYjE4NjU5YjJkN2VmMDNiMTU3YjdiYjYzN2RhNzZkN2RhZDkxOTNlMjRlOTllODQzYjZkZjRkZTA4Y2Q4NjAyMjAyMjEwYTNjMzA3MmQ4MjliN2E0MGRmZTFkMjExNTI4YmJjNWFkNmNjZDU5NzM5ZmIyMDQ5ODFiOGI0NWQ3MWU3NDE0YzY5NTIyMTAyM2VjMWNkNTNlMWJiMDhjY2Y3NDAwYjA3ZDc2ZTc5ZTZhMTAwNGRjZjI0MjJkODEyMmQ0ZTZmMTQ0NGRkYzM1NzIxMDMyY2YwOWFhYzM1Njc5MGZlOWM0NDQ2YzAzMjljYzIyYjg0MTYzM2U4NTE0ODhlOTI5MDM3MDE5M2MzODZiMzUzMjEwMjhhNmNlMWU3NjExMzJmNDI5NTU1MWNmYWM0Y2I4Mjg0YWIyNzNlZTlkYjczMjQ2MDdhNDdlMDMxZWU0ZGE4NzU1M2FlZmRmZmZmZmY0MDhkOTI4M2I0YTMwYzk5YjA2NDg2Nzc2YTI5NDQ2YzkyZGUzYjU0YmRmZWZhNTMzMmYzOWM3YTQyNTg4MzQ5MDAwMDAwMDBmZGZlMDAwMDQ4MzA0NTAyMjEwMDkxNTZiNDgyZjYwMTRkZTYyNDdjZmFiYzRlNTZjMTMxM2NmMzI2OTg2ZjUyYzJiMWVhMDhjNmQ0YjMxNjY3MjEwMjIwNzkxMDhhZWNhODEyMjJmYTVlMjJjNGQyOGQzMjM4YWEzMWQ4M2MxNDc3YzFmZjIzMTk4ZWFkZDBhYjRhMWYyYTQxNDgzMDQ1MDIyMTAwYjdhOWM5OTQ4MDY0ZWNkYTMyOTdlNGM1MzRhZWY5N2I0MWIwYzk2ZDZjM2E0NDU0ZDM2NjhiZTJhZDM1NDY1YjAyMjA0ZjZlMGZkOTg2ZjdjOGZjOTQ5ZGUxZDU0ODgwYmFkNTZkNjNjM2Y4NjE3OWI1MDA4MGM5MDljYzNlY2RlNDY5NDE0YzY5NTIyMTAzYjQ5MzAxMTgzOTA4MjAwYjFkZTZhYmMwZWZhNmE4MzkwYmFiNjM3ZWJjNTEwZDIyZjdlMWI1ZDMyYmJiNzA0OTIxMDIyZWI5YTI3ZjVmZTdjODkxZjRkZTZjYmQwZTlkMzBmOTMwMzFiYjhmYzJiZjhjNzA2MWVhMTY3MWUyNWNiMjQxMjEwM2I0ZWJhN2M1MzIyZWE1NzhiYjQ1ODlmNmJlNWNlNDc0MzQzYjY0Y2ZjODUyZWRhMzEwMmMwZGY2YjM2NjQyNmM1M2FlZmRmZmZmZmY5NTQyZTIwNzU1MzUzYTJkODBkMzY5ZjA5ZTk2MTg4NGFmMmM4YzA1ZjEwMTNkNTVjNWJkZjM0YTQ2ZDU0MTU2MDAwMDAwMDBmZGZkMDAwMDQ3MzA0NDAyMjAxYjUzZTI3NjAwNjkwMGNhNDU0MTNlNmRlYTI3NTUwODhlOTM2MTZiYTljOGIzYTdlMzc0NWU1ZmZlNjY1MTg5MDIyMDM3ZmE4NjRkMzQ3MzJhMmNkOTM4ZDY5ZTQ2ZWQwYTVhOWI5MzEwYWI4OTVkZjEwY2E0NTRmMDFhNjFjMGU3Mzg0MTQ4MzA0NTAyMjEwMGRjNjY5NjY0YmU0ZTZhMDA0ZGQzYmU2ODk2NTU0ZjI4MzE0Zjk2ZDMwNzI5NDk3YmYwM2M2YTk3MDBkNTNhNDUwMjIwN2E4ZGQxNjU3OGEyMDgwNWZjMjc3OTY4MTBhZjdiMWVjMGU3NTU4MTFjOWVmZWVlNjk2ZThhNzZlNWYwNzgzZjQxNGM2OTUyMjEwMmYyZDE4MTQxY2Y5OWJjZTMyNjVmMWIyMTgzMzVlZWQyZWNkNmE4OTFlYzFmYjM0ZWIwYmVmNjZmMjU2NjFkYmUyMTAyNTM0MmFmNWI2ODdmYzEwNmRhNjZiNTJiZjkzOTRkNTE4MTU4NmM4YTE5ZTg4YTExMTIxMWJmZDM2M2IzNjE3YjIxMDJmYjNlOTNmMDcyN2ExYTI1MTliZGMyYWM1MmJkN2M1ODgxN2U4ZjU3MTAxNDczMzhhNzlmNDU2MzJhYWEzNmRmNTNhZWZkZmZmZmZmZjM0MGVlMTVjN2Y5MGViMjJlNWYxNzQ1OWQyMGFkZGJiZTNlOTUwMzZmYmU5OTc4ZTYxYWU2ZDE4ZGRmY2Y4ZDAwMDAwMDAwZmRmZDAwMDA0NzMwNDQwMjIwN2IyMGVkZGUzMDNmYjM4NmYwNTViODMyY2ExODUyOWVlNDBlZGQ0NWNhMjhjODQ1MWY4NjNiODA0NzYyYTZkNjAyMjA1YTJmNWEwNWRjMWUwNjJiNTFmNTkwYmQ3NzJjNjIyMzg1ODBlZmEwODY4ZWVhZmNlYzg5N2RhYjU4MGYxYjMwNDE0ODMwNDUwMjIxMDA5YzBkZDNlYTk0ZjgwMTJjNjgxYTQ4MTA1MDUwNDdjNzViMDM3OGIyOTE4YzI4OWQ3NGYwZTJlZWVhODRhOTA0MDIyMDA2ZDAyZjRlODhkODhmMjYyMjBjODdkMjFkYzg0ZmQ1OGY2OWU2NWZlMDU3ZjZiOTY5OGUwYzhhNzdlODBjZTM0MTRjNjk1MjIxMDIzZWMxY2Q1M2UxYmIwOGNjZjc0MDBiMDdkNzZlNzllNmExMDA0ZGNmMjQyMmQ4MTIyZDRlNmYxNDQ0ZGRjMzU3MjEwMzJjZjA5YWFjMzU2NzkwZmU5YzQ0NDZjMDMyOWNjMjJiODQxNjMzZTg1MTQ4OGU5MjkwMzcwMTkzYzM4NmIzNTMyMTAyOGE2Y2UxZTc2MTEzMmY0Mjk1NTUxY2ZhYzRjYjgyODRhYjI3M2VlOWRiNzMyNDYwN2E0N2UwMzFlZTRkYTg3NTUzYWVmZGZmZmZmZjA2OWY1Yzc0YzdmODQ2ZjRhOTg0MjI2ZDRiNmNlYTZhMmY1MTE1MzAzMmFmNDMxNmE4ODc0ZDg4NjNlMDgyOTYwMDAwMDAwMGZkZmQwMDAwNDgzMDQ1MDIyMTAwYTk1NmE2OWEwMmExYTI4YjdmMWI4MDE3MWMwYzhiYWI3NTI0NmYzOWE0YmE3MGYzMDNlMWQ1NDUyODg2YTA2ZDAyMjAyZDAwOGU1Njk4NmRmZmJlYjIzNDZlNWE5ODI5ZjFhNzZlZDUxYTc0YWIzYTJmMDdmNjVhNGJlNWFjMTdkZWUzNDE0NzMwNDQwMjIwNWRkMzViY2VlOWI3ZDRkZDZhMzUwYjQ3NDZmZmVhZTVmN2QxYzgxYzE1NGI0NGJmN2M5YTIyMTY0OWRmOTZmYzAyMjA3ZmY2NjkyZjE5MWEzZDg2MmJmMmFlM2MwYmYzNjU0NTU5NzkyMjVkMDU1MmRkZDFmMmJmNDNlZmRiNDcxNzY2NDE0YzY5NTIyMTAzNDcwYjY4NTNmNGFiYjRkYjAxMGFjMmMwYmM5YjhjOTJiM2UyNTkyOTQyMjdiOGQwMGM4MDMxZDExMmIyODI4NjIxMDMyNzAwZTdiYzEwMDgzYTc0ZTVlZmZjNjBjYTg4MzY5ZmZkZmQzOWIxMmY1OGMwYjdhZTY5MWY4MTZkZWZmMmZlMjEwMmJkYzZkNmVjZGU5ZDI5NzJhZTRiYzkxYTFlMjg3OTM4M2MxMmYxMjdjYjczMWEzNTJhNDQyOTNhOTdjOGIyMmI1M2FlZmRmZmZmZmY5OWZmYjkzN2I0YTkzYzhiY2E0NzQ3ZmQ1NWIzNTYxNTI3OWY4N2FjOTFhZDA5OWI4ZGFiOGFiNWZlYzA5M2E1MDAwMDAwMDBmZGZlMDAwMDQ4MzA0NTAyMjEwMGMwOWE1N2NmNDA2MTUwM2I2NDBmMTFkM2I1NTNlYmZjZDU2YWRiNTYxYTViNDhhNTVmMzVlZWRlOGNiYmM2NTMwMjIwNmNlNzRlMjIxMmM1YTI3MzhkYmRmZmVhZjVkMjdhNTlkYTYzMDFjZTJkMzE1ZDlmMzNjMjM3NGMyMzQwOGQyZTQxNDgzMDQ1MDIyMTAwZjM4OTc1MWFhZGM1MDNjODM5NDYzNjdhZmNmNjYxMTM0Y2JkYzUyMWRkZjQ2MGJlN2VhZGMwZTE5MjE1NzIyOTAyMjA2Y2FiYjk2NjExODMwMWE2ZDgwNWJjMTNhYmI4NmUzNzFmNTdjOGI2OTZkY2YxNDIyYjRkZjJhNDAwMWQzYWVjNDE0YzY5NTIyMTAzNDcwYjY4NTNmNGFiYjRkYjAxMGFjMmMwYmM5YjhjOTJiM2UyNTkyOTQyMjdiOGQwMGM4MDMxZDExMmIyODI4NjIxMDMyNzAwZTdiYzEwMDgzYTc0ZTVlZmZjNjBjYTg4MzY5ZmZkZmQzOWIxMmY1OGMwYjdhZTY5MWY4MTZkZWZmMmZlMjEwMmJkYzZkNmVjZGU5ZDI5NzJhZTRiYzkxYTFlMjg3OTM4M2MxMmYxMjdjYjczMWEzNTJhNDQyOTNhOTdjOGIyMmI1M2FlZmRmZmZmZmZmNzdlZjlkODJkMmE3OWQxNWVmYjIwZjdmNDRjMGU5N2M2ODViZDE2NjdlZjhlZTI5ZGExMjk3MjdmMzk2ZWM1MDAwMDAwMDBmZGZkMDAwMDQ3MzA0NDAyMjAzYTY1MTY5NjIyOTNjZDFmNDgxNGYwYzIyNzdhMzI1YjgyMDFhZWRiMjBlY2Y5YmU3NGJjNTVhN2Y2NWYxNDEwMDIyMDU0MjhjMjQ0OWM1OTcxYzYyNGFlNjdkMDY1ODIwOGRlZDIwNmY3ZTI1NWM1MDc4ZTRjNTc2ZDhlMDlmZTU1MWY0MTQ4MzA0NTAyMjEwMDlmYjZiZDA1YTBmZWM2YjQwOTIzY2M4YzkyNDc1MWVlOWE4NWE5MWVmN2NkZDBkNWVmNzA1NzQwY2FiMjk5OWIwMjIwNTRiYTNkZGUwMjJkY2MyZGIyMmZlMDFmMjUxZDhiMTEyZWI4YzkyNDcwNjk1ZmU0YTc2Y2ZlYmM5MWVjNWZhNzQxNGM2OTUyMjEwMjNlYzFjZDUzZTFiYjA4Y2NmNzQwMGIwN2Q3NmU3OWU2YTEwMDRkY2YyNDIyZDgxMjJkNGU2ZjE0NDRkZGMzNTcyMTAzMmNmMDlhYWMzNTY3OTBmZTljNDQ0NmMwMzI5Y2MyMmI4NDE2MzNlODUxNDg4ZTkyOTAzNzAxOTNjMzg2YjM1MzIxMDI4YTZjZTFlNzYxMTMyZjQyOTU1NTFjZmFjNGNiODI4NGFiMjczZWU5ZGI3MzI0NjA3YTQ3ZTAzMWVlNGRhODc1NTNhZWZkZmZmZmZmMWJjNTc3NmFjZDAyOGI4MDVlMzcyOTZhNzQ1YTliNzBmYzdmZDc3NjkzODY0YTJkMGFjNzFkYWFlODM5ODRkZTM1MDIwMDAwZmMwMDQ3MzA0NDAyMjA0MDAzNjI3MWRmYTI5NDZlYjc5ZGVjYTM3MjYzNzVhMzk2NTNiYzQzZTM3ODUxZGEwZjNmNzhmM2Q3NmFkZjZjMDIyMDY4OGRkMDM4Nzc1ZmIwZTUyMTgwMDY4NWNjMjIwZWVlMDg1ZGE4MjliZTg0Mzc4MTMxZWRhM2VjZTU4ZDNmN2Y0MTQ3MzA0NDAyMjAxZDBhZTEzYTYwYWM3YjdmMjdlYzg0NmVkZDhlNDEzODk1NzQ3YzdjNWQxZGU2YzRhYWQ3MWI2OWRjMjE4MGE4MDIyMDUyOWQ3ZDRmMWVhNTRlZTE1ZWUzYmEzMTU5YjQ3OGFiMjcyZjdlMzRkNzJmYmQyNjhkY2QxZjg5MTU4NjZmMjM0MTRjNjk1MjIxMDJmNzRhNzNhOGVjMTVkMWM3MTQyNDY1ZmZlMTUzOGUwN2U0YTdhMTZhY2I3NzFkMjkzNGFiZmE3MmEzZTExMDMyMjEwMjllMzI0MzJkMDg4YjhlMmFmYzEwYzc0YTI1ZjNiOWZiNTk0ZTgwMmQwMWRiOTIxMmQwM2EzNWM4ZjQyZGZjNmEyMTAyOTVhN2FmZTZmYTY5M2VhM2U1ODYxYWEwMzc5YjE5YmJmNjVhNjViYTM0ODJhN2RhYzE1YTZmOTU0MzY4MDkyZDUzYWVmZGZmZmZmZjFmZjQ3MmI0YzIyOGU3MGE5ZjQyYjU1MzFmODc0Mzc3YmU2NGNjN2E0NDFkZTMxMmQyYjM3NDUxNDcwYmJiZWYwMDAwMDAwMGZkZmQwMDAwNDgzMDQ1MDIyMTAwOTY3OTE2MmM1NjQ3NjUyNDJlZWU4OTQyYWRkZmZmODUyMmY1OGIzM2QyNWRmYjNkNjk4NGNjMTA3NjZlNDEyYTAyMjA3YjYwNTRjZmZkZTY1ZDk2NjNhZmM5MjAxMjAyZDgwNDkzNzA4ODQ5MTYyMjgzMTFiNjI2Yzk0ZDBjOGViZTcwNDE0NzMwNDQwMjIwMzc0ZGE1MTk5OTRiNDllMjNmMTdkZTViZWM1ODM3ODAyN2IxYTZiZjlkMzFhZTAxYjlkZGQxMDI2ZWExYzkxNDAyMjAzYjVlZWZkMjRhMWMzOGQzZTYxZDkxODdjM2JlNjdhZWUwNWFhYzI2OGVmY2E3NjRiOWZiYTYwZTEzNGMwNmQ3NDE0YzY5NTIyMTAzNDcwYjY4NTNmNGFiYjRkYjAxMGFjMmMwYmM5YjhjOTJiM2UyNTkyOTQyMjdiOGQwMGM4MDMxZDExMmIyODI4NjIxMDMyNzAwZTdiYzEwMDgzYTc0ZTVlZmZjNjBjYTg4MzY5ZmZkZmQzOWIxMmY1OGMwYjdhZTY5MWY4MTZkZWZmMmZlMjEwMmJkYzZkNmVjZGU5ZDI5NzJhZTRiYzkxYTFlMjg3OTM4M2MxMmYxMjdjYjczMWEzNTJhNDQyOTNhOTdjOGIyMmI1M2FlZmRmZmZmZmYwMjFkNTcyOTAzMDAwMDAwMDAxN2E5MTQ1ZmYwYmJkMTQ3OTE1ZWI5MjEyYWM2YjQ3OTQ0YTBjMGZmYzcwNzY0ODdiOGM2ZWUyMzAwMDAwMDAwMTk3NmE5MTRhNzM2MjcyNTg3MDM5MmJkNmFhYzY5ODgyYmZlNzBjN2Q4ZDdhODdiODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiODRhNjI0Zjg2NzNmYmE2OTljMWRlZjhhMzFmM2E0NDA4YTU5ZGNkOTNiOGE5YzBiZjdlZTM0NDIwMTE1ZTJiNiIsImhhc2giOiI4NGE2MjRmODY3M2ZiYTY5OWMxZGVmOGEzMWYzYTQ0MDhhNTlkY2Q5M2I4YTljMGJmN2VlMzQ0MjAxMTVlMmI2IiwidmVyc2lvbiI6Miwic2l6ZSI6MTY0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiYTA3ZTZlNGQzYzg2NTc3MTIyYmFjZGE2NDdkZGZjY2FlNDFhMmMwN2NkMGMyYzljMWE5OTQ4NTFhZTYzYWJhNCIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIiwiaGV4IjoiNDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIn0sInNlcXVlbmNlIjoxfV0sInZvdXQiOlt7InZhbHVlIjowLjAwNTcxMDk0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XSwiZmVlIjoxLjY0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMWE0YWI2M2FlNTE0ODk5MWE5YzJjMGNjZDA3MmMxYWU0Y2FmY2RkNDdhNmNkYmEyMjcxNTc4NjNjNGQ2ZTdlYTAwMDAwMDAwMDQ1NDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxMDEwMDAwMDAwMWQ2YjYwODAwMDAwMDAwMDAyM2FhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODcwMDAwMDAwMCJ9LHsidHhpZCI6Ijg3MTY3MDBjMDExZDcyNzEyZDg1OWMyNjM5NzAxOTI2ZDEzMjcyMzhmMWQzNWYyODg1MzM2OTRmMDY1MzcyYzUiLCJoYXNoIjoiODcxNjcwMGMwMTFkNzI3MTJkODU5YzI2Mzk3MDE5MjZkMTMyNzIzOGYxZDM1ZjI4ODUzMzY5NGYwNjUzNzJjNSIsInZlcnNpb24iOjIsInNpemUiOjE2OTQsImxvY2t0aW1lIjo5NDA4ODMsInZpbiI6W3sidHhpZCI6IjFkZWUzNzk4YTRlMDYwNGM0ZTNkMWYwNzQwMTQ2MmUyMWQ4MzIxNWQ4NTcyYWIxMGRmZTNlMjQyNmUxMzdjZjUiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA3NjEyNDg0YjliMDNiZjkwNjFhMmMyMjM3MjE1ZGNiY2E2YWFlMjNlMGY3ODA0MGVjNjBmMjhlYWRhY2ZkN2NhMDIyMDNjOGQxZWZiM2ExODNjMTg1ZDRkMzhiODk2Zjg0Mjc0N2Q5ZmRmNjc4ZjA2MTRiODc4NDg0M2RhMjkzY2ZkMWNbQUxMfEZPUktJRF0gMDJhMDk4NDBiZDRiNDRkMGZjNWQ1NGQyNTQyY2E5MzcyZDM0MGQyNTRhNDRkN2U4OTBiZjAxYmFhZGI2OTllYzM1IiwiaGV4IjoiNDczMDQ0MDIyMDc2MTI0ODRiOWIwM2JmOTA2MWEyYzIyMzcyMTVkY2JjYTZhYWUyM2UwZjc4MDQwZWM2MGYyOGVhZGFjZmQ3Y2EwMjIwM2M4ZDFlZmIzYTE4M2MxODVkNGQzOGI4OTZmODQyNzQ3ZDlmZGY2NzhmMDYxNGI4Nzg0ODQzZGEyOTNjZmQxYzQxMjEwMmEwOTg0MGJkNGI0NGQwZmM1ZDU0ZDI1NDJjYTkzNzJkMzQwZDI1NGE0NGQ3ZTg5MGJmMDFiYWFkYjY5OWVjMzUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9LHsidHhpZCI6IjFmMDg4OTZmMDE1MzkyNjZhMWE2NGQ5YzJiY2Q2ZjUwZDZiN2VjNzU2YTUzNGM1YWViNzRkNmYzMTVmNTRhOTQiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA1NTQ4MTg5NTM4MDEzOTU1N2M3ZmMwMTUzZThkODRlMTYwZTZmZTRiYTkzYTk2ZWE0ZDhkZGQ2YzUzMzg1NTA5MDIyMDIzYjZjYTA5ZjkwMGQ2ZmI2ZGZjYWJiMjE1MDgzMmE5ZmU3NzZiMTg4OThlZDcyMWU0ZDA4Y2U2YzQ0NGRjYmNbQUxMfEZPUktJRF0gMDJhMDk4NDBiZDRiNDRkMGZjNWQ1NGQyNTQyY2E5MzcyZDM0MGQyNTRhNDRkN2U4OTBiZjAxYmFhZGI2OTllYzM1IiwiaGV4IjoiNDczMDQ0MDIyMDU1NDgxODk1MzgwMTM5NTU3YzdmYzAxNTNlOGQ4NGUxNjBlNmZlNGJhOTNhOTZlYTRkOGRkZDZjNTMzODU1MDkwMjIwMjNiNmNhMDlmOTAwZDZmYjZkZmNhYmIyMTUwODMyYTlmZTc3NmIxODg5OGVkNzIxZTRkMDhjZTZjNDQ0ZGNiYzQxMjEwMmEwOTg0MGJkNGI0NGQwZmM1ZDU0ZDI1NDJjYTkzNzJkMzQwZDI1NGE0NGQ3ZTg5MGJmMDFiYWFkYjY5OWVjMzUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9LHsidHhpZCI6IjNhNzhiZTg4NWJiOGRlZGUxNGVlYTlkZmViZDAwOGU2MWYwMGY4YTM5M2RhYjljY2FiYzIwZDM1Mjg4MGFkMjYiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA0ZWMyOGQzNTBlYjYzNTgxNDNiNDM0MjRjZTlmYzRiNjEzZmUyOWFhMTU3ODI1NmQwMjVkMjc0ZThmN2M1ZTg3MDIyMDRmN2VlNDliNzkyNGVlZTM3YmY4YzdmMTVhN2FlN2FmMjliNDA5ZDk3YjVmZjQxN2JmNGNiYzgyODQyYWM0ZjlbQUxMfEZPUktJRF0gMDI2M2E5MTk0NjFjMTU0M2IzNmFlMWI4MzMwYTVhYzlhZTE5ODY1MWZiN2QxNTQxNDcwNjFmMmE0Y2Y4NDdiYWRjIiwiaGV4IjoiNDczMDQ0MDIyMDRlYzI4ZDM1MGViNjM1ODE0M2I0MzQyNGNlOWZjNGI2MTNmZTI5YWExNTc4MjU2ZDAyNWQyNzRlOGY3YzVlODcwMjIwNGY3ZWU0OWI3OTI0ZWVlMzdiZjhjN2YxNWE3YWU3YWYyOWI0MDlkOTdiNWZmNDE3YmY0Y2JjODI4NDJhYzRmOTQxMjEwMjYzYTkxOTQ2MWMxNTQzYjM2YWUxYjgzMzBhNWFjOWFlMTk4NjUxZmI3ZDE1NDE0NzA2MWYyYTRjZjg0N2JhZGMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9LHsidHhpZCI6IjY4NTc1ZmRmMzVhMDg0MzIwMGY4NTM5MDVkNjA1OTU4NGEyODQxM2I3MTdhNTE5NDc1YjBlNzU4YmJmODFkYWEiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAyNjA5ZDg2MTVmZjFhNTMzYTM4MTRmMjVhMzdkMTRlOWU2MDc3YjFmODFkZmYwNzVlM2NlYjYyN2I5OGIxOTMxMDIyMDdkYWYwODc2YTFkM2MwODEwYTZiYWJlM2VkM2Y4ODc2NGQyN2FmYzk1MTc2YjAzYzIwMjVmNDcxNzk1YTQyOGFbQUxMfEZPUktJRF0gMDJiZWJlN2E5Yzc5MjkyOTM4NWIwMDQwYzZhY2MyOTFlMWZlNGY1MjczMjhkZDRkYzhkMjBmYjZjMjRjNTAyYTdmIiwiaGV4IjoiNDczMDQ0MDIyMDI2MDlkODYxNWZmMWE1MzNhMzgxNGYyNWEzN2QxNGU5ZTYwNzdiMWY4MWRmZjA3NWUzY2ViNjI3Yjk4YjE5MzEwMjIwN2RhZjA4NzZhMWQzYzA4MTBhNmJhYmUzZWQzZjg4NzY0ZDI3YWZjOTUxNzZiMDNjMjAyNWY0NzE3OTVhNDI4YTQxMjEwMmJlYmU3YTljNzkyOTI5Mzg1YjAwNDBjNmFjYzI5MWUxZmU0ZjUyNzMyOGRkNGRjOGQyMGZiNmMyNGM1MDJhN2YifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9LHsidHhpZCI6IjZhYzVmMzI4OGYxNjYyZjFiYTUxMWI1ODgxZTZjNzAxNjM0M2UyODkxZjFhMDQxMjNjYzdjOWIzNzBiOTg1ZGYiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAxZDc5Zjk5ZWJkOWRkYTQ1YWVlZTEyY2Q2ZGEzMzAxMjdjY2ExMGY1NWRiYmVmNTVmMjQ5MGY4MzUxZWI4NzU2MDIyMDU3MzIyMzRhNTM0YzRlODlmOWZkNTM2ZmViNjQ1YTc2NjE4NDlhMTY1ZWE4NjYwN2NmMTc1OTljNGRmZmZhOGZbQUxMfEZPUktJRF0gMDM0ODNmMzdkMGM5YmU2ZTg2OWRlOThkZmJiNDNhYzc5M2UxMzc2Yzc3ZWY0NzhiYWQ5MmM1ZTI1ZjljZTEzNDM3IiwiaGV4IjoiNDczMDQ0MDIyMDFkNzlmOTllYmQ5ZGRhNDVhZWVlMTJjZDZkYTMzMDEyN2NjYTEwZjU1ZGJiZWY1NWYyNDkwZjgzNTFlYjg3NTYwMjIwNTczMjIzNGE1MzRjNGU4OWY5ZmQ1MzZmZWI2NDVhNzY2MTg0OWExNjVlYTg2NjA3Y2YxNzU5OWM0ZGZmZmE4ZjQxMjEwMzQ4M2YzN2QwYzliZTZlODY5ZGU5OGRmYmI0M2FjNzkzZTEzNzZjNzdlZjQ3OGJhZDkyYzVlMjVmOWNlMTM0MzcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9LHsidHhpZCI6IjczZDVhM2UyNGYyZjFkMGI4NzUxMTA4MDZkNWQyMTdiMmQ1OTQ3N2RiOWYxNDBhNzI4OTFmZDFmM2ZmZDgzMTQiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAzOGU3YzVkNTg2OWIwYmEwNGIxODU2OWNhMzExZTE4NWJiMTJjNWQwZGYyNDZmOTg1YWRmMWQ4OWQwNzUxYTI5MDIyMDE0ZGNkODU0YzQ2YTI3MTAyMzhjZDE0YjA4MzkwMWViZWY2YzlhY2JhMjVkNmQwZDg1OTFmZTI4YmMyNDZkYWRbQUxMfEZPUktJRF0gMDJhMDk4NDBiZDRiNDRkMGZjNWQ1NGQyNTQyY2E5MzcyZDM0MGQyNTRhNDRkN2U4OTBiZjAxYmFhZGI2OTllYzM1IiwiaGV4IjoiNDczMDQ0MDIyMDM4ZTdjNWQ1ODY5YjBiYTA0YjE4NTY5Y2EzMTFlMTg1YmIxMmM1ZDBkZjI0NmY5ODVhZGYxZDg5ZDA3NTFhMjkwMjIwMTRkY2Q4NTRjNDZhMjcxMDIzOGNkMTRiMDgzOTAxZWJlZjZjOWFjYmEyNWQ2ZDBkODU5MWZlMjhiYzI0NmRhZDQxMjEwMmEwOTg0MGJkNGI0NGQwZmM1ZDU0ZDI1NDJjYTkzNzJkMzQwZDI1NGE0NGQ3ZTg5MGJmMDFiYWFkYjY5OWVjMzUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9LHsidHhpZCI6Ijg4ZjRmNjFjZTZlNDU4NDlmNzg3MzBlNDRjOWJjYWE4NmZlYjc3YzZlOWQ4ZjYwYTdkM2RlY2FmNjRjNWMyZTkiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAyYmY5ZTgwZjJhZmU5MTA1MzZlNTVhMzJhZmIxZDU1MGNkNDhiOGI4N2NiYTQ5NmViNjkzMGQ0NjY3MGYyMDc3MDIyMDQ5OTQyMTE0NWVlMzBjOTk1NmYzZWMwMDJhM2Q0OTUwYWY3ZjhiNzYwMjYzMTZlYzkyMDkwMzI4ZTdhMjgyOGVbQUxMfEZPUktJRF0gMDM4NWUxYTZmMzJjMmE0NWU1OWUyYzQ5MWQ4YTQ2ZDU5MDE3MzM5YWMyMDgxMzZlNzA5ZTFjNDEzNjBmYjAzNGNlIiwiaGV4IjoiNDczMDQ0MDIyMDJiZjllODBmMmFmZTkxMDUzNmU1NWEzMmFmYjFkNTUwY2Q0OGI4Yjg3Y2JhNDk2ZWI2OTMwZDQ2NjcwZjIwNzcwMjIwNDk5NDIxMTQ1ZWUzMGM5OTU2ZjNlYzAwMmEzZDQ5NTBhZjdmOGI3NjAyNjMxNmVjOTIwOTAzMjhlN2EyODI4ZTQxMjEwMzg1ZTFhNmYzMmMyYTQ1ZTU5ZTJjNDkxZDhhNDZkNTkwMTczMzlhYzIwODEzNmU3MDllMWM0MTM2MGZiMDM0Y2UifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9LHsidHhpZCI6IjhmNzVjODZmODRiZTdlZGNmMjc5OWYzZmQxYzI4MGU3YmFkNzRhOTM1MDhmODk4MmFjNzMxMGQwYTBlZGRiMWMiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0MzAyMjAxZjlhNjBjNGE2NjFmN2U3ODM4MTEzMGQ1ODg0ZWZmZjY5MWI0OGY5NjM0MWFmYzNmZDJhMTE5MjhmNDNiOGE2MDIxZjBhNmJhNDI5OWYwYThlYmQ5OWNkOTRhMzllMjI0MjBjOTEwNjU2ODQyOGU1MzZmZTgyNDJlZjY3ZTI4YmU2W0FMTHxGT1JLSURdIDAzOWYwOGI5ZTBmZDgzZTg1NjBkMDBmYTNiZmY3Y2FiNWUyZTFiY2QxMWFhNDFkMmQyZGNmZTNhZTQxMjZlZTk2NSIsImhleCI6IjQ2MzA0MzAyMjAxZjlhNjBjNGE2NjFmN2U3ODM4MTEzMGQ1ODg0ZWZmZjY5MWI0OGY5NjM0MWFmYzNmZDJhMTE5MjhmNDNiOGE2MDIxZjBhNmJhNDI5OWYwYThlYmQ5OWNkOTRhMzllMjI0MjBjOTEwNjU2ODQyOGU1MzZmZTgyNDJlZjY3ZTI4YmU2NDEyMTAzOWYwOGI5ZTBmZDgzZTg1NjBkMDBmYTNiZmY3Y2FiNWUyZTFiY2QxMWFhNDFkMmQyZGNmZTNhZTQxMjZlZTk2NSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH0seyJ0eGlkIjoiYTM1MGViZmU3MDU1ODRlZTMyYTMyMWNjYWZmNWJmMjE4MTk5NTk3ZWViNjJlN2FlNDA3OTQxYzM4OWVhODk2YyIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDVjMmEyNDQ1MTc4MzYwYjhmOTcxNzM4Y2FmMGRjMzlhODNjMTg2MWM0YjFlOGUxYzI2NzcxMzg5YjY3ODc2YzQwMjIwNGExMDg1NDFkMDdmY2M1MzYwM2NmOTAxMzBhODVkODJlYmZkZWE2ZWM0ZDkzN2E1M2MyZGFkZDZiMDhmYTVjM1tBTEx8Rk9SS0lEXSAwMzZmNzg0ZGMxOGM1OWUxODgyYjI0MjZkYWE5ZDZlODNhZDI3ZjI0NWZiMzliZDE0Y2U2ZjA1NDMyZjNiODEyNjUiLCJoZXgiOiI0NzMwNDQwMjIwNWMyYTI0NDUxNzgzNjBiOGY5NzE3MzhjYWYwZGMzOWE4M2MxODYxYzRiMWU4ZTFjMjY3NzEzODliNjc4NzZjNDAyMjA0YTEwODU0MWQwN2ZjYzUzNjAzY2Y5MDEzMGE4NWQ4MmViZmRlYTZlYzRkOTM3YTUzYzJkYWRkNmIwOGZhNWMzNDEyMTAzNmY3ODRkYzE4YzU5ZTE4ODJiMjQyNmRhYTlkNmU4M2FkMjdmMjQ1ZmIzOWJkMTRjZTZmMDU0MzJmM2I4MTI2NSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH0seyJ0eGlkIjoiYjAxMjA0YTg5YjY0MzJjNWU0MTgxMmNmMzk5OThlZWI2ZGRlYTRkMTJhYWM0M2MzNmRjMzRiY2NlMWQ0OWE0YSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDdhNWUxNGE2NDhmY2M4YWZlZTQ2OTBkM2MzY2Y4YzhlNzkzZmE2MmVmYTNmOGM0YjEzMGJmMjZiZDM0ZDYwMjIwMjIwNGM3ZTQ5MzA5NGQ5NmY2NWVmMzc5ODZiY2JhZTJhNWY1MGI3MDUyN2VmZDdkOTZhNDQ2YmNjMDhhODZmYjllZVtBTEx8Rk9SS0lEXSAwMzYxYzMzNDAzNGEwNmQ2MGIxZmM5YmVlZWY0NmY1MGVkMTBiMWFmYWY3Y2Y1YzgzNTIyZGZjODM5M2QxNmFkMmEiLCJoZXgiOiI0NzMwNDQwMjIwN2E1ZTE0YTY0OGZjYzhhZmVlNDY5MGQzYzNjZjhjOGU3OTNmYTYyZWZhM2Y4YzRiMTMwYmYyNmJkMzRkNjAyMjAyMjA0YzdlNDkzMDk0ZDk2ZjY1ZWYzNzk4NmJjYmFlMmE1ZjUwYjcwNTI3ZWZkN2Q5NmE0NDZiY2MwOGE4NmZiOWVlNDEyMTAzNjFjMzM0MDM0YTA2ZDYwYjFmYzliZWVlZjQ2ZjUwZWQxMGIxYWZhZjdjZjVjODM1MjJkZmM4MzkzZDE2YWQyYSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH0seyJ0eGlkIjoiZjlhZDhlNjU1ZTIxZTA1ZTRhMjhjMGViNGRlMDk0NGRkMDAwMWJmNWY3MWJkMzY1MTI5YThiMTBkNmUxZjJmNCIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDAxZTlkYzhmODU5MWZmN2ExMGE0YTFjNzAwYWVlYTZhNTRkZDgwZDVmNTllYTYxMWI5ZTc1MWNkMzdlNjE5YzcwMjIwNzNhZDVmYzhkNzAyMzAzNDliYjZmYzJmZTU0NGQwYzcyNmNjYTI4MzQ2ZmM4ZGI4OWJiYzUyZDg3MzEzMzM0ZVtBTEx8Rk9SS0lEXSAwMmEwOTg0MGJkNGI0NGQwZmM1ZDU0ZDI1NDJjYTkzNzJkMzQwZDI1NGE0NGQ3ZTg5MGJmMDFiYWFkYjY5OWVjMzUiLCJoZXgiOiI0NzMwNDQwMjIwMDFlOWRjOGY4NTkxZmY3YTEwYTRhMWM3MDBhZWVhNmE1NGRkODBkNWY1OWVhNjExYjllNzUxY2QzN2U2MTljNzAyMjA3M2FkNWZjOGQ3MDIzMDM0OWJiNmZjMmZlNTQ0ZDBjNzI2Y2NhMjgzNDZmYzhkYjg5YmJjNTJkODczMTMzMzRlNDEyMTAyYTA5ODQwYmQ0YjQ0ZDBmYzVkNTRkMjU0MmNhOTM3MmQzNDBkMjU0YTQ0ZDdlODkwYmYwMWJhYWRiNjk5ZWMzNSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMDk5ODMxMSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDc3ZDhiNjhkMmFlMzY4NDhmYjQ4Y2Y5MzAyMjZlMWYxNDUwOTk4ZmIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3N2Q4YjY4ZDJhZTM2ODQ4ZmI0OGNmOTMwMjI2ZTFmMTQ1MDk5OGZiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwbWEzZDVkOXQza3NqOG1mcjhleHEzeHU4YzUyenZjbHZ5Njg4Z3VwNCJdfX0seyJ2YWx1ZSI6MS45MTA3LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2NiMTE4YzIzZmM3NDdlNWJjNDY5MDNjNTg4ZmQxNGUyNGMxZjdlZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdjYjExOGMyM2ZjNzQ3ZTViYzQ2OTAzYzU4OGZkMTRlMjRjMWY3ZWU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA3dHp4eHo4bHI1MGVkdWc2Z3Jja3kwNjk4emZzMGhhYzA5enZ5bGZuIl19fV0sImZlZSI6MS42OTVlLTA1LCJoZXgiOiIwMjAwMDAwMDBiZjU3YzEzNmU0MmUyZTNkZjEwYWI3Mjg1NWQyMTgzMWRlMjYyMTQ0MDA3MWYzZDRlNGM2MGUwYTQ5ODM3ZWUxZDAxMDAwMDAwNmE0NzMwNDQwMjIwNzYxMjQ4NGI5YjAzYmY5MDYxYTJjMjIzNzIxNWRjYmNhNmFhZTIzZTBmNzgwNDBlYzYwZjI4ZWFkYWNmZDdjYTAyMjAzYzhkMWVmYjNhMTgzYzE4NWQ0ZDM4Yjg5NmY4NDI3NDdkOWZkZjY3OGYwNjE0Yjg3ODQ4NDNkYTI5M2NmZDFjNDEyMTAyYTA5ODQwYmQ0YjQ0ZDBmYzVkNTRkMjU0MmNhOTM3MmQzNDBkMjU0YTQ0ZDdlODkwYmYwMWJhYWRiNjk5ZWMzNWZlZmZmZmZmOTQ0YWY1MTVmM2Q2NzRlYjVhNGM1MzZhNzVlY2I3ZDY1MDZmY2QyYjljNGRhNmExNjY5MjUzMDE2Zjg5MDgxZjAxMDAwMDAwNmE0NzMwNDQwMjIwNTU0ODE4OTUzODAxMzk1NTdjN2ZjMDE1M2U4ZDg0ZTE2MGU2ZmU0YmE5M2E5NmVhNGQ4ZGRkNmM1MzM4NTUwOTAyMjAyM2I2Y2EwOWY5MDBkNmZiNmRmY2FiYjIxNTA4MzJhOWZlNzc2YjE4ODk4ZWQ3MjFlNGQwOGNlNmM0NDRkY2JjNDEyMTAyYTA5ODQwYmQ0YjQ0ZDBmYzVkNTRkMjU0MmNhOTM3MmQzNDBkMjU0YTQ0ZDdlODkwYmYwMWJhYWRiNjk5ZWMzNWZlZmZmZmZmMjZhZDgwMjgzNTBkYzJhYmNjYjlkYTkzYTNmODAwMWZlNjA4ZDBlYmRmYTllZTE0ZGVkZWI4NWI4OGJlNzgzYTAxMDAwMDAwNmE0NzMwNDQwMjIwNGVjMjhkMzUwZWI2MzU4MTQzYjQzNDI0Y2U5ZmM0YjYxM2ZlMjlhYTE1NzgyNTZkMDI1ZDI3NGU4ZjdjNWU4NzAyMjA0ZjdlZTQ5Yjc5MjRlZWUzN2JmOGM3ZjE1YTdhZTdhZjI5YjQwOWQ5N2I1ZmY0MTdiZjRjYmM4Mjg0MmFjNGY5NDEyMTAyNjNhOTE5NDYxYzE1NDNiMzZhZTFiODMzMGE1YWM5YWUxOTg2NTFmYjdkMTU0MTQ3MDYxZjJhNGNmODQ3YmFkY2ZlZmZmZmZmYWExZGY4YmI1OGU3YjA3NTk0NTE3YTcxM2I0MTI4NGE1ODU5NjA1ZDkwNTNmODAwMzI4NGEwMzVkZjVmNTc2ODAxMDAwMDAwNmE0NzMwNDQwMjIwMjYwOWQ4NjE1ZmYxYTUzM2EzODE0ZjI1YTM3ZDE0ZTllNjA3N2IxZjgxZGZmMDc1ZTNjZWI2MjdiOThiMTkzMTAyMjA3ZGFmMDg3NmExZDNjMDgxMGE2YmFiZTNlZDNmODg3NjRkMjdhZmM5NTE3NmIwM2MyMDI1ZjQ3MTc5NWE0MjhhNDEyMTAyYmViZTdhOWM3OTI5MjkzODViMDA0MGM2YWNjMjkxZTFmZTRmNTI3MzI4ZGQ0ZGM4ZDIwZmI2YzI0YzUwMmE3ZmZlZmZmZmZmZGY4NWI5NzBiM2M5YzczYzEyMDQxYTFmODllMjQzNjMwMWM3ZTY4MTU4MWI1MWJhZjE2MjE2OGYyOGYzYzU2YTAwMDAwMDAwNmE0NzMwNDQwMjIwMWQ3OWY5OWViZDlkZGE0NWFlZWUxMmNkNmRhMzMwMTI3Y2NhMTBmNTVkYmJlZjU1ZjI0OTBmODM1MWViODc1NjAyMjA1NzMyMjM0YTUzNGM0ZTg5ZjlmZDUzNmZlYjY0NWE3NjYxODQ5YTE2NWVhODY2MDdjZjE3NTk5YzRkZmZmYThmNDEyMTAzNDgzZjM3ZDBjOWJlNmU4NjlkZTk4ZGZiYjQzYWM3OTNlMTM3NmM3N2VmNDc4YmFkOTJjNWUyNWY5Y2UxMzQzN2ZlZmZmZmZmMTQ4M2ZkM2YxZmZkOTEyOGE3NDBmMWI5N2Q0NzU5MmQ3YjIxNWQ2ZDgwMTA1MTg3MGIxZDJmNGZlMmEzZDU3MzAyMDAwMDAwNmE0NzMwNDQwMjIwMzhlN2M1ZDU4NjliMGJhMDRiMTg1NjljYTMxMWUxODViYjEyYzVkMGRmMjQ2Zjk4NWFkZjFkODlkMDc1MWEyOTAyMjAxNGRjZDg1NGM0NmEyNzEwMjM4Y2QxNGIwODM5MDFlYmVmNmM5YWNiYTI1ZDZkMGQ4NTkxZmUyOGJjMjQ2ZGFkNDEyMTAyYTA5ODQwYmQ0YjQ0ZDBmYzVkNTRkMjU0MmNhOTM3MmQzNDBkMjU0YTQ0ZDdlODkwYmYwMWJhYWRiNjk5ZWMzNWZlZmZmZmZmZTljMmM1NjRhZmVjM2Q3ZDBhZjZkOGU5YzY3N2ViNmZhOGNhOWI0Y2U0MzA4N2Y3NDk1OGU0ZTYxY2Y2ZjQ4ODAwMDAwMDAwNmE0NzMwNDQwMjIwMmJmOWU4MGYyYWZlOTEwNTM2ZTU1YTMyYWZiMWQ1NTBjZDQ4YjhiODdjYmE0OTZlYjY5MzBkNDY2NzBmMjA3NzAyMjA0OTk0MjExNDVlZTMwYzk5NTZmM2VjMDAyYTNkNDk1MGFmN2Y4Yjc2MDI2MzE2ZWM5MjA5MDMyOGU3YTI4MjhlNDEyMTAzODVlMWE2ZjMyYzJhNDVlNTllMmM0OTFkOGE0NmQ1OTAxNzMzOWFjMjA4MTM2ZTcwOWUxYzQxMzYwZmIwMzRjZWZlZmZmZmZmMWNkYmVkYTBkMDEwNzNhYzgyODk4ZjUwOTM0YWQ3YmFlNzgwYzJkMTNmOWY3OWYyZGM3ZWJlODQ2ZmM4NzU4ZjAwMDAwMDAwNjk0NjMwNDMwMjIwMWY5YTYwYzRhNjYxZjdlNzgzODExMzBkNTg4NGVmZmY2OTFiNDhmOTYzNDFhZmMzZmQyYTExOTI4ZjQzYjhhNjAyMWYwYTZiYTQyOTlmMGE4ZWJkOTljZDk0YTM5ZTIyNDIwYzkxMDY1Njg0MjhlNTM2ZmU4MjQyZWY2N2UyOGJlNjQxMjEwMzlmMDhiOWUwZmQ4M2U4NTYwZDAwZmEzYmZmN2NhYjVlMmUxYmNkMTFhYTQxZDJkMmRjZmUzYWU0MTI2ZWU5NjVmZWZmZmZmZjZjODllYTg5YzM0MTc5NDBhZWU3NjJlYjdlNTk5OTgxMjFiZmY1YWZjYzIxYTMzMmVlODQ1NTcwZmVlYjUwYTMwMTAwMDAwMDZhNDczMDQ0MDIyMDVjMmEyNDQ1MTc4MzYwYjhmOTcxNzM4Y2FmMGRjMzlhODNjMTg2MWM0YjFlOGUxYzI2NzcxMzg5YjY3ODc2YzQwMjIwNGExMDg1NDFkMDdmY2M1MzYwM2NmOTAxMzBhODVkODJlYmZkZWE2ZWM0ZDkzN2E1M2MyZGFkZDZiMDhmYTVjMzQxMjEwMzZmNzg0ZGMxOGM1OWUxODgyYjI0MjZkYWE5ZDZlODNhZDI3ZjI0NWZiMzliZDE0Y2U2ZjA1NDMyZjNiODEyNjVmZWZmZmZmZjRhOWFkNGUxY2M0YmMzNmRjMzQzYWMyYWQxYTRkZTZkZWI4ZTk5MzljZjEyMThlNGM1MzI2NDliYTgwNDEyYjAwMTAwMDAwMDZhNDczMDQ0MDIyMDdhNWUxNGE2NDhmY2M4YWZlZTQ2OTBkM2MzY2Y4YzhlNzkzZmE2MmVmYTNmOGM0YjEzMGJmMjZiZDM0ZDYwMjIwMjIwNGM3ZTQ5MzA5NGQ5NmY2NWVmMzc5ODZiY2JhZTJhNWY1MGI3MDUyN2VmZDdkOTZhNDQ2YmNjMDhhODZmYjllZTQxMjEwMzYxYzMzNDAzNGEwNmQ2MGIxZmM5YmVlZWY0NmY1MGVkMTBiMWFmYWY3Y2Y1YzgzNTIyZGZjODM5M2QxNmFkMmFmZWZmZmZmZmY0ZjJlMWQ2MTA4YjlhMTI2NWQzMWJmN2Y1MWIwMGQwNGQ5NGUwNGRlYmMwMjg0YTVlZTAyMTVlNjU4ZWFkZjkwMTAwMDAwMDZhNDczMDQ0MDIyMDAxZTlkYzhmODU5MWZmN2ExMGE0YTFjNzAwYWVlYTZhNTRkZDgwZDVmNTllYTYxMWI5ZTc1MWNkMzdlNjE5YzcwMjIwNzNhZDVmYzhkNzAyMzAzNDliYjZmYzJmZTU0NGQwYzcyNmNjYTI4MzQ2ZmM4ZGI4OWJiYzUyZDg3MzEzMzM0ZTQxMjEwMmEwOTg0MGJkNGI0NGQwZmM1ZDU0ZDI1NDJjYTkzNzJkMzQwZDI1NGE0NGQ3ZTg5MGJmMDFiYWFkYjY5OWVjMzVmZWZmZmZmZjAyYTczYjBmMDAwMDAwMDAwMDE5NzZhOTE0NzdkOGI2OGQyYWUzNjg0OGZiNDhjZjkzMDIyNmUxZjE0NTA5OThmYjg4YWMzMDdmNjMwYjAwMDAwMDAwMTk3NmE5MTQ3Y2IxMThjMjNmYzc0N2U1YmM0NjkwM2M1ODhmZDE0ZTI0YzFmN2VlODhhYzUzNWIwZTAwIn0seyJ0eGlkIjoiODk3MGI0YjJlYWNhN2Y5NGQ4YTEyZDU2MzJiZDUyNTgyMTBlMmUzNTVmNzI0MWU1MzcyZjcyYTdjN2NhMDMwOCIsImhhc2giOiI4OTcwYjRiMmVhY2E3Zjk0ZDhhMTJkNTYzMmJkNTI1ODIxMGUyZTM1NWY3MjQxZTUzNzJmNzJhN2M3Y2EwMzA4IiwidmVyc2lvbiI6Miwic2l6ZSI6MjE5LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiMDRjNzg2N2I2NmU5Y2QyZmYwN2VjNzAzYzdkYjVmOWJlMDc0MTI0MTczYjkxODVhNWEwOTUzM2QwMjAzZWUyYiIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiI2NDhlOWJmMTIzOGJjNWZkZGVkMjAxNzNjMWRhMWNmNzhlNDJiMTJhNWNkZjQwMTYwNTc5MWY2MGI0ZWJlZGM4OTZkYjUzMWIxNjhjYTZiNWU1ODk4NDFhZjRlNjRhYWU0M2Q4ZTVlYjVhMzZkNmE3MDE4NzVkNDc1MDg1YTk4YVtBTEx8Rk9SS0lEXSAwMjE3YzJmYmM4MzZiZjg3NmNmMzk4YTJiZjFjODExYzc4MDJkMzQ3NTk3NTQ2MWRmZTliYzY2MDZmM2ZkZTBmMTYiLCJoZXgiOiI0MTY0OGU5YmYxMjM4YmM1ZmRkZWQyMDE3M2MxZGExY2Y3OGU0MmIxMmE1Y2RmNDAxNjA1NzkxZjYwYjRlYmVkYzg5NmRiNTMxYjE2OGNhNmI1ZTU4OTg0MWFmNGU2NGFhZTQzZDhlNWViNWEzNmQ2YTcwMTg3NWQ0NzUwODVhOThhNDEyMTAyMTdjMmZiYzgzNmJmODc2Y2YzOThhMmJmMWM4MTFjNzgwMmQzNDc1OTc1NDYxZGZlOWJjNjYwNmYzZmRlMGYxNiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC45NTU0MTM5OSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDhjYzM2M2ExNDE1YmU0ZDdkNDAwZTI0MTU1OGUzZWM4YzQxYmMwN2UgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4Y2MzNjNhMTQxNWJlNGQ3ZDQwMGUyNDE1NThlM2VjOGM0MWJjMDdlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6eHZ4Y2FwZzlkN2Y0NzVxcjN5ejR2dzhteXZneDdxMGM1YzR6ZXU3ZCJdfX0seyJ2YWx1ZSI6MC4wMzc0NzQ4MSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGMyOWZiMWVjNTE5ZmY2MjdkMGI2MDU4NmE0MDFmYTlkNGIyMjIwM2IgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRjMjlmYjFlYzUxOWZmNjI3ZDBiNjA1ODZhNDAxZmE5ZDRiMjIyMDNiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFycGZsdjB2MngwbHZmN3NrY3pjZGZxcGwydzVrZzNxOHY5eHBjMGRnNyJdfX1dLCJmZWUiOjIuMjdlLTA2LCJoZXgiOiIwMjAwMDAwMDAxMmJlZTAzMDIzZDUzMDk1YTVhMThiOTczNDExMjc0ZTA5YjVmZGJjNzAzYzc3ZWYwMmZjZGU5NjY3Yjg2YzcwNDAxMDAwMDAwNjQ0MTY0OGU5YmYxMjM4YmM1ZmRkZWQyMDE3M2MxZGExY2Y3OGU0MmIxMmE1Y2RmNDAxNjA1NzkxZjYwYjRlYmVkYzg5NmRiNTMxYjE2OGNhNmI1ZTU4OTg0MWFmNGU2NGFhZTQzZDhlNWViNWEzNmQ2YTcwMTg3NWQ0NzUwODVhOThhNDEyMTAyMTdjMmZiYzgzNmJmODc2Y2YzOThhMmJmMWM4MTFjNzgwMmQzNDc1OTc1NDYxZGZlOWJjNjYwNmYzZmRlMGYxNmZmZmZmZmZmMDI5N2Q4YjEwNTAwMDAwMDAwMTk3NmE5MTQ4Y2MzNjNhMTQxNWJlNGQ3ZDQwMGUyNDE1NThlM2VjOGM0MWJjMDdlODhhYzk5MmUzOTAwMDAwMDAwMDAxOTc2YTkxNGMyOWZiMWVjNTE5ZmY2MjdkMGI2MDU4NmE0MDFmYTlkNGIyMjIwM2I4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI4YzlkYmY4MjZmYzRkYjdiNDc1ZDY2NDU3ZWRhNDg2ZTY0ZjQ0ZmRhNTk5MzY5OTFiYzEzNDk4NjY2MGIzZTI2IiwiaGFzaCI6IjhjOWRiZjgyNmZjNGRiN2I0NzVkNjY0NTdlZGE0ODZlNjRmNDRmZGE1OTkzNjk5MWJjMTM0OTg2NjYwYjNlMjYiLCJ2ZXJzaW9uIjoyLCJzaXplIjoxNjQsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI1NmYxZTc5MWZlNmRmZDA1MGM4NmVmOWMyMzE4ZTBiOTNiMWJiNGU4MTRhMTEyNTUyNDI0YTU1MjM1ZTc1NTc2Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEiLCJoZXgiOiI0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEifSwic2VxdWVuY2UiOjF9XSwidm91dCI6W3sidmFsdWUiOjAuMDA1Njc4MTQsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dLCJmZWUiOjEuNjRlLTA2LCJoZXgiOiIwMjAwMDAwMDAxNzY1NWU3MzU1MmE1MjQyNDU1MTJhMTE0ZThiNDFiM2JiOWUwMTgyMzljZWY4NjBjMDVmZDZkZmU5MWU3ZjE1NjAwMDAwMDAwNDU0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEwMTAwMDAwMDAxMDZhYTA4MDAwMDAwMDAwMDIzYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NzAwMDAwMDAwIn0seyJ0eGlkIjoiOGYxMDM0ZTQwYWMxMjc1OWM5NmNkMDAzNThkOTE4ZTI4YjIzOWU5MDIwZGIyNjliMWNkNGUzNDM4YjQ5MWM2ZCIsImhhc2giOiI4ZjEwMzRlNDBhYzEyNzU5Yzk2Y2QwMDM1OGQ5MThlMjhiMjM5ZTkwMjBkYjI2OWIxY2Q0ZTM0MzhiNDkxYzZkIiwidmVyc2lvbiI6Miwic2l6ZSI6MjI2LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiMjBiM2M5MGUyZWU3MDkzNzY5MWU0ZWNlZWIzNWM0MmM1ZDMwN2I5NTAxOGEwYjk4MzQ1NjJhNTFlNDQ0MGExOSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwYWM1NmU5ODk3NTk1YTIwM2VlN2ZjYmEzNDYwMzczZWJhYWQ1MzM1NGM2MDY3ZDJiMmYwNmZhMWJhZjA3OTM0NTAyMjA0ODY4ZDFhYTc5MDYyMGQ0YmI3ZmI0ZDk2ZDM0MmM2ODI3YmY3YmM3NzY1NThiMDgwNjc5YmVmYjczZTIwYzQ5W0FMTHxGT1JLSURdIDAzM2I1ZTBjMDcwOWQ1YzZiNjE3MTk5NGQ3M2UxMmNhZDU0NGNkMTc1MGIxYmRiMGFjZWYyZWQ0NWYzYzc2ZTU3OCIsImhleCI6IjQ4MzA0NTAyMjEwMGFjNTZlOTg5NzU5NWEyMDNlZTdmY2JhMzQ2MDM3M2ViYWFkNTMzNTRjNjA2N2QyYjJmMDZmYTFiYWYwNzkzNDUwMjIwNDg2OGQxYWE3OTA2MjBkNGJiN2ZiNGQ5NmQzNDJjNjgyN2JmN2JjNzc2NTU4YjA4MDY3OWJlZmI3M2UyMGM0OTQxMjEwMzNiNWUwYzA3MDlkNWM2YjYxNzE5OTRkNzNlMTJjYWQ1NDRjZDE3NTBiMWJkYjBhY2VmMmVkNDVmM2M3NmU1NzgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMDc1NTMsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA1ZjBmMzUyMGI1ZGQ5YjM5YzUwNjUyZGZjZjEzYzc2MzYxYTg0NzIxIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NWYwZjM1MjBiNWRkOWIzOWM1MDY1MmRmY2YxM2M3NjM2MWE4NDcyMTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDBzN2RmcWtod2Vrd3c5cWVmZGxuY25jYTNrcjJ6OHl5NmgyZjJldXYiXX19LHsidmFsdWUiOjE3LjIwNDAxMjc5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTZlZTA0Y2I3NzI4NzU5YThkZWU2YmVkM2E3ODEzMTdhM2NlM2QyZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpud3VweHR3dTU4dHg1ZGFlNDc2d25jenZ0NjhuM2E5Y200bXI5cHRtIl19fV0sImZlZSI6Mi43ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTE5MGE0NGU0NTEyYTU2MzQ5ODBiOGEwMTk1N2IzMDVkMmNjNDM1ZWJjZTRlMWU2OTM3MDllNzJlMGVjOWIzMjAwMTAwMDAwMDZiNDgzMDQ1MDIyMTAwYWM1NmU5ODk3NTk1YTIwM2VlN2ZjYmEzNDYwMzczZWJhYWQ1MzM1NGM2MDY3ZDJiMmYwNmZhMWJhZjA3OTM0NTAyMjA0ODY4ZDFhYTc5MDYyMGQ0YmI3ZmI0ZDk2ZDM0MmM2ODI3YmY3YmM3NzY1NThiMDgwNjc5YmVmYjczZTIwYzQ5NDEyMTAzM2I1ZTBjMDcwOWQ1YzZiNjE3MTk5NGQ3M2UxMmNhZDU0NGNkMTc1MGIxYmRiMGFjZWYyZWQ0NWYzYzc2ZTU3OGZmZmZmZmZmMDJlODNmNzMwMDAwMDAwMDAwMTk3NmE5MTQ1ZjBmMzUyMGI1ZGQ5YjM5YzUwNjUyZGZjZjEzYzc2MzYxYTg0NzIxODhhYzdmM2Q4YjY2MDAwMDAwMDAxOTc2YTkxNGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmU4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI4ZjYzNDU1NjM3OTIyZmU4ZDM2YjVkMDA3NWQ4MGE3OTM1NDU5ZTU3MzhiYTNlN2QwYWY3OGI2YTRjOGUwY2NkIiwiaGFzaCI6IjhmNjM0NTU2Mzc5MjJmZThkMzZiNWQwMDc1ZDgwYTc5MzU0NTllNTczOGJhM2U3ZDBhZjc4YjZhNGM4ZTBjY2QiLCJ2ZXJzaW9uIjoxLCJzaXplIjozMzgsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI1NTQxZjI4NzNlYjRlZGNjYjNiZWM3ZTNkOTA2MzhhM2FhZGMzNjgyZTU3YjgyNGY5MmE2N2I2Yzk1Yjg2MTQ3Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwMWJhNjI2NWUyM2Y3OTJkODFmODA5ZDg0ZDcwMzM3MmY3M2RhMTEwY2Y2MzE0ZGM5NDFmZGMyYjBhMGMwMGVmZTAyMjAxMmRjMGNiM2ViYWQzNzA5ZDEzMzA4YWNkMDZkMDAxNjI2ZjllMmI5NGY4ZDlkNWEyMmExNWI1MTFlZDZhZmI3W0FMTHxGT1JLSURdIDAzYmQ2Y2UwMTQzNjVlZTdlY2RhNzBlNzVkZjY5OTUzMDRkZDU1ZjZhMDJmMDcyM2MxZGVhMDUwNjllMzU3NzRmZSIsImhleCI6IjQ3MzA0NDAyMjAxYmE2MjY1ZTIzZjc5MmQ4MWY4MDlkODRkNzAzMzcyZjczZGExMTBjZjYzMTRkYzk0MWZkYzJiMGEwYzAwZWZlMDIyMDEyZGMwY2IzZWJhZDM3MDlkMTMzMDhhY2QwNmQwMDE2MjZmOWUyYjk0ZjhkOWQ1YTIyYTE1YjUxMWVkNmFmYjc0MTIxMDNiZDZjZTAxNDM2NWVlN2VjZGE3MGU3NWRmNjk5NTMwNGRkNTVmNmEwMmYwNzIzYzFkZWEwNTA2OWUzNTc3NGZlIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJmOWRkMjE5OTk1NTU1NWE2NGMxNzM3NjgwMzQ1MzFkNDkxNzEwODRmYmFkMzkxZmRjNTcyZmRlYjdkNTI2NDg5Iiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwNjMyNGVmMDc2Njk5MjM3OTk2ZTg2NzkwNDgxMjNiMDI1YjE1YjFlZTQwYjgwYTk5M2Y0M2NiODFlZTA3NTdjNTAyMjAzMDZjMTYxZGI5YzhiMmJiMzZmZWVlMjE2MDBjMDJmYjAzNjBmNzM4MDM3NzRjNmYzZmZlNDkxYWE4MWMxYWI4W0FMTHxGT1JLSURdIDAzYmQ2Y2UwMTQzNjVlZTdlY2RhNzBlNzVkZjY5OTUzMDRkZDU1ZjZhMDJmMDcyM2MxZGVhMDUwNjllMzU3NzRmZSIsImhleCI6IjQ3MzA0NDAyMjA2MzI0ZWYwNzY2OTkyMzc5OTZlODY3OTA0ODEyM2IwMjViMTViMWVlNDBiODBhOTkzZjQzY2I4MWVlMDc1N2M1MDIyMDMwNmMxNjFkYjljOGIyYmIzNmZlZWUyMTYwMGMwMmZiMDM2MGY3MzgwMzc3NGM2ZjNmZmU0OTFhYTgxYzFhYjg0MTIxMDNiZDZjZTAxNDM2NWVlN2VjZGE3MGU3NWRmNjk5NTMwNGRkNTVmNmEwMmYwNzIzYzFkZWEwNTA2OWUzNTc3NGZlIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjoxLjA0ODA2ODM1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjAwZTg0YzBjOTdhNTYxNjczZWJiMzRlMWM4ZDU4YTI2NWJhMGZjYiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGIwMGU4NGMwYzk3YTU2MTY3M2ViYjM0ZTFjOGQ1OGEyNjViYTBmY2I4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpjcWFweHFlOWE5djlubmF3ZTV1OHlkdHozeHR3czBldnRja25wbjI0Il19fV0sImZlZSI6MC4wMDAyNTUsImhleCI6IjAxMDAwMDAwMDI0NzYxYjg5NTZjN2JhNjkyNGY4MjdiZTU4MjM2ZGNhYWEzMzgwNmQ5ZTNjN2JlYjNjY2VkYjQzZTg3ZjI0MTU1MDAwMDAwMDA2YTQ3MzA0NDAyMjAxYmE2MjY1ZTIzZjc5MmQ4MWY4MDlkODRkNzAzMzcyZjczZGExMTBjZjYzMTRkYzk0MWZkYzJiMGEwYzAwZWZlMDIyMDEyZGMwY2IzZWJhZDM3MDlkMTMzMDhhY2QwNmQwMDE2MjZmOWUyYjk0ZjhkOWQ1YTIyYTE1YjUxMWVkNmFmYjc0MTIxMDNiZDZjZTAxNDM2NWVlN2VjZGE3MGU3NWRmNjk5NTMwNGRkNTVmNmEwMmYwNzIzYzFkZWEwNTA2OWUzNTc3NGZlZmZmZmZmZmY4OTY0NTI3ZGViZmQ3MmM1ZmQ5MWQzYmE0ZjA4NzE5MWQ0MzE0NTAzNjgzNzE3NGNhNjU1NTU5NTk5MjFkZGY5MDEwMDAwMDA2YTQ3MzA0NDAyMjA2MzI0ZWYwNzY2OTkyMzc5OTZlODY3OTA0ODEyM2IwMjViMTViMWVlNDBiODBhOTkzZjQzY2I4MWVlMDc1N2M1MDIyMDMwNmMxNjFkYjljOGIyYmIzNmZlZWUyMTYwMGMwMmZiMDM2MGY3MzgwMzc3NGM2ZjNmZmU0OTFhYTgxYzFhYjg0MTIxMDNiZDZjZTAxNDM2NWVlN2VjZGE3MGU3NWRmNjk5NTMwNGRkNTVmNmEwMmYwNzIzYzFkZWEwNTA2OWUzNTc3NGZlZmZmZmZmZmYwMWIzMzkzZjA2MDAwMDAwMDAxOTc2YTkxNGIwMGU4NGMwYzk3YTU2MTY3M2ViYjM0ZTFjOGQ1OGEyNjViYTBmY2I4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI5MTI1Y2ZkNmQwM2JlMzJhNTQ1NWIzN2MyYzEyYmNkZDdiYWFmN2NkNTAyYmE5YjM0OGJmMzU2N2E1NWVlZDFhIiwiaGFzaCI6IjkxMjVjZmQ2ZDAzYmUzMmE1NDU1YjM3YzJjMTJiY2RkN2JhYWY3Y2Q1MDJiYTliMzQ4YmYzNTY3YTU1ZWVkMWEiLCJ2ZXJzaW9uIjoyLCJzaXplIjoxNTQ3LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiNjljNTI5Nzg1ZWIyNGU4ZTkxNzRjNzM1OGZlNjA5ZTQzNTJiMDkxYjA4NTg1YTYzMmMwZjAzMzYxYWVhZTllZSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIyIDAgNzYwMDljNjMwMGM3NTFjNzg4MDBjZTAxMjA3ZjUyODg1MWNlODgwMGNmMDExMzdmNzU1ZjdmNzdjNTdjODE5ZjY5YzM1MzlkYzQ1NGExNjkwMDAwNTM3OTUxODc2MzAwY2Y1NTdmNzU1MWNmNTE3ZTUzN2E3NTdjNmI3YzZjNzY4MTdiNzU3NzY3NTM3OTUyODc2MzAwY2Y1YTdmNzU1NTdmNzc3NjA2MDAwMDAwMDAwMDAwODc5MTY5NzY4MTdiNzU3YzUxY2Y1MjdlNTM3YTc1N2M2YjdjNmM3NTY3NTM3OTUzODgwMGNmNWY3Zjc1NWE3Zjc3NzYwNjAwMDAwMDAwMDAwMDg3OTE2OTc2ODE3Yjc1N2M1MWNmNTM3ZTUzN2E3NTdjNmI3YzZjNzU2ODY4NTFkMjdiODg1MWNkNTJjNzg4NTFkMTUxY2U4ODUxZDM1MWQwOWQ1MWNjNTFjNjlkNTJjZTAwODc1MmNlMDBjZTg3NzY3YjliNjk3NjYzYzQ1NDlkNjg2MzAyZTgwMzc3NjgwMGNmMDEyNzdmNzUwMTEzN2Y3NzAzNzZhOTE0N2M3ZTAyODhhYzdlNTJjZDg4NTJkMTAwODg1MmNjOWQwMGNjMDBjNjlkMDBjZDAwYzc4ODAwZDEwMGNlODgwMGQzMDBkMDlkMDBkMjAwY2Y4ODUyYzY1MmNjOTQwMjEwMjc5NDAwYTQ3NjAyZTgwM2EwNjM1M2NkNTJjNzg4NTNkMTUyY2U4ODUzZDI1MmNmODg1M2NjNzhhMjY5Njg2ZDc1NTE2Nzc2NTE5YzYzYzAwMDlkNTFjZTAwY2U4OGMzNTI5ZGM0NTI5ZDc4ODI3NzU1OWQ1Mjc5ODI3NzU1OWQ1Mzc5ODI3NzU1OWQ1NDc5ODI3NzU0OWQwMGNmMDEyNzdmNzUwMTEzN2Y3NzAwZDI1MzdhNTQ3YTdlNTQ3YTdlNTQ3YTdlN2I3ZTg4MDBjZDAwYzc4ODAwZDEwMGNlODgwMGQzMDBkMDlkMDBjYzAwYzY5ZDUxZDI1MWNmODg1MWNkNTFjNzg4NTFkMTUxY2U4ODUxZDM1MWQwOWQ1MWNjNTFjNjAyMTAyNzk0YTI3NzY3NzY1MjljNjM1MWNlMDEyMDdmNTI4ODAwY2U1MWNlODhjMDAwYTA2OWMwOGJjMzc4YTA2Mzc2Y2U1Mjc5ODg2ODAwZDIwMGNmODgwMGNkMDBjNzg4MDBkMTAwY2U4ODAwZDMwMGQwOWQwMGNjMDBjNjAyMTAyNzk0YTI2OWM0NTE5Yzc3Nzc3NzY3NTM5ZDAzY2M5YjAxYjI3NWM0NTE5ZDAwZDEwMDg3Njg2ODY4IiwiaGV4IjoiNTIwMDRkZjgwMTc2MDA5YzYzMDBjNzUxYzc4ODAwY2UwMTIwN2Y1Mjg4NTFjZTg4MDBjZjAxMTM3Zjc1NWY3Zjc3YzU3YzgxOWY2OWMzNTM5ZGM0NTRhMTY5MDAwMDUzNzk1MTg3NjMwMGNmNTU3Zjc1NTFjZjUxN2U1MzdhNzU3YzZiN2M2Yzc2ODE3Yjc1Nzc2NzUzNzk1Mjg3NjMwMGNmNWE3Zjc1NTU3Zjc3NzYwNjAwMDAwMDAwMDAwMDg3OTE2OTc2ODE3Yjc1N2M1MWNmNTI3ZTUzN2E3NTdjNmI3YzZjNzU2NzUzNzk1Mzg4MDBjZjVmN2Y3NTVhN2Y3Nzc2MDYwMDAwMDAwMDAwMDA4NzkxNjk3NjgxN2I3NTdjNTFjZjUzN2U1MzdhNzU3YzZiN2M2Yzc1Njg2ODUxZDI3Yjg4NTFjZDUyYzc4ODUxZDE1MWNlODg1MWQzNTFkMDlkNTFjYzUxYzY5ZDUyY2UwMDg3NTJjZTAwY2U4Nzc2N2I5YjY5NzY2M2M0NTQ5ZDY4NjMwMmU4MDM3NzY4MDBjZjAxMjc3Zjc1MDExMzdmNzcwMzc2YTkxNDdjN2UwMjg4YWM3ZTUyY2Q4ODUyZDEwMDg4NTJjYzlkMDBjYzAwYzY5ZDAwY2QwMGM3ODgwMGQxMDBjZTg4MDBkMzAwZDA5ZDAwZDIwMGNmODg1MmM2NTJjYzk0MDIxMDI3OTQwMGE0NzYwMmU4MDNhMDYzNTNjZDUyYzc4ODUzZDE1MmNlODg1M2QyNTJjZjg4NTNjYzc4YTI2OTY4NmQ3NTUxNjc3NjUxOWM2M2MwMDA5ZDUxY2UwMGNlODhjMzUyOWRjNDUyOWQ3ODgyNzc1NTlkNTI3OTgyNzc1NTlkNTM3OTgyNzc1NTlkNTQ3OTgyNzc1NDlkMDBjZjAxMjc3Zjc1MDExMzdmNzcwMGQyNTM3YTU0N2E3ZTU0N2E3ZTU0N2E3ZTdiN2U4ODAwY2QwMGM3ODgwMGQxMDBjZTg4MDBkMzAwZDA5ZDAwY2MwMGM2OWQ1MWQyNTFjZjg4NTFjZDUxYzc4ODUxZDE1MWNlODg1MWQzNTFkMDlkNTFjYzUxYzYwMjEwMjc5NGEyNzc2Nzc2NTI5YzYzNTFjZTAxMjA3ZjUyODgwMGNlNTFjZTg4YzAwMGEwNjljMDhiYzM3OGEwNjM3NmNlNTI3OTg4NjgwMGQyMDBjZjg4MDBjZDAwYzc4ODAwZDEwMGNlODgwMGQzMDBkMDlkMDBjYzAwYzYwMjEwMjc5NGEyNjljNDUxOWM3Nzc3Nzc2NzUzOWQwM2NjOWIwMWIyNzVjNDUxOWQwMGQxMDA4NzY4Njg2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH0seyJ0eGlkIjoiOTdkYmQ2M2YxMzhlMTQyNTFjNjdmYmNhM2NhY2Q3MzAyNzgxYTRlNjY5ODY3NzJkNjM3MzM1MmI4OTg0OThkOCIsInZvdXQiOjc2LCJzY3JpcHRTaWciOnsiYXNtIjoiMiAwIDc2MDA5YzYzMDBjNzUxYzc4ODAwY2UwMTIwN2Y1Mjg4NTFjZTg4MDBjZjAxMTM3Zjc1NWY3Zjc3YzU3YzgxOWY2OWMzNTM5ZGM0NTRhMTY5MDAwMDUzNzk1MTg3NjMwMGNmNTU3Zjc1NTFjZjUxN2U1MzdhNzU3YzZiN2M2Yzc2ODE3Yjc1Nzc2NzUzNzk1Mjg3NjMwMGNmNWE3Zjc1NTU3Zjc3NzYwNjAwMDAwMDAwMDAwMDg3OTE2OTc2ODE3Yjc1N2M1MWNmNTI3ZTUzN2E3NTdjNmI3YzZjNzU2NzUzNzk1Mzg4MDBjZjVmN2Y3NTVhN2Y3Nzc2MDYwMDAwMDAwMDAwMDA4NzkxNjk3NjgxN2I3NTdjNTFjZjUzN2U1MzdhNzU3YzZiN2M2Yzc1Njg2ODUxZDI3Yjg4NTFjZDUyYzc4ODUxZDE1MWNlODg1MWQzNTFkMDlkNTFjYzUxYzY5ZDUyY2UwMDg3NTJjZTAwY2U4Nzc2N2I5YjY5NzY2M2M0NTQ5ZDY4NjMwMmU4MDM3NzY4MDBjZjAxMjc3Zjc1MDExMzdmNzcwMzc2YTkxNDdjN2UwMjg4YWM3ZTUyY2Q4ODUyZDEwMDg4NTJjYzlkMDBjYzAwYzY5ZDAwY2QwMGM3ODgwMGQxMDBjZTg4MDBkMzAwZDA5ZDAwZDIwMGNmODg1MmM2NTJjYzk0MDIxMDI3OTQwMGE0NzYwMmU4MDNhMDYzNTNjZDUyYzc4ODUzZDE1MmNlODg1M2QyNTJjZjg4NTNjYzc4YTI2OTY4NmQ3NTUxNjc3NjUxOWM2M2MwMDA5ZDUxY2UwMGNlODhjMzUyOWRjNDUyOWQ3ODgyNzc1NTlkNTI3OTgyNzc1NTlkNTM3OTgyNzc1NTlkNTQ3OTgyNzc1NDlkMDBjZjAxMjc3Zjc1MDExMzdmNzcwMGQyNTM3YTU0N2E3ZTU0N2E3ZTU0N2E3ZTdiN2U4ODAwY2QwMGM3ODgwMGQxMDBjZTg4MDBkMzAwZDA5ZDAwY2MwMGM2OWQ1MWQyNTFjZjg4NTFjZDUxYzc4ODUxZDE1MWNlODg1MWQzNTFkMDlkNTFjYzUxYzYwMjEwMjc5NGEyNzc2Nzc2NTI5YzYzNTFjZTAxMjA3ZjUyODgwMGNlNTFjZTg4YzAwMGEwNjljMDhiYzM3OGEwNjM3NmNlNTI3OTg4NjgwMGQyMDBjZjg4MDBjZDAwYzc4ODAwZDEwMGNlODgwMGQzMDBkMDlkMDBjYzAwYzYwMjEwMjc5NGEyNjljNDUxOWM3Nzc3Nzc2NzUzOWQwM2NjOWIwMWIyNzVjNDUxOWQwMGQxMDA4NzY4Njg2OCIsImhleCI6IjUyMDA0ZGY4MDE3NjAwOWM2MzAwYzc1MWM3ODgwMGNlMDEyMDdmNTI4ODUxY2U4ODAwY2YwMTEzN2Y3NTVmN2Y3N2M1N2M4MTlmNjljMzUzOWRjNDU0YTE2OTAwMDA1Mzc5NTE4NzYzMDBjZjU1N2Y3NTUxY2Y1MTdlNTM3YTc1N2M2YjdjNmM3NjgxN2I3NTc3Njc1Mzc5NTI4NzYzMDBjZjVhN2Y3NTU1N2Y3Nzc2MDYwMDAwMDAwMDAwMDA4NzkxNjk3NjgxN2I3NTdjNTFjZjUyN2U1MzdhNzU3YzZiN2M2Yzc1Njc1Mzc5NTM4ODAwY2Y1ZjdmNzU1YTdmNzc3NjA2MDAwMDAwMDAwMDAwODc5MTY5NzY4MTdiNzU3YzUxY2Y1MzdlNTM3YTc1N2M2YjdjNmM3NTY4Njg1MWQyN2I4ODUxY2Q1MmM3ODg1MWQxNTFjZTg4NTFkMzUxZDA5ZDUxY2M1MWM2OWQ1MmNlMDA4NzUyY2UwMGNlODc3NjdiOWI2OTc2NjNjNDU0OWQ2ODYzMDJlODAzNzc2ODAwY2YwMTI3N2Y3NTAxMTM3Zjc3MDM3NmE5MTQ3YzdlMDI4OGFjN2U1MmNkODg1MmQxMDA4ODUyY2M5ZDAwY2MwMGM2OWQwMGNkMDBjNzg4MDBkMTAwY2U4ODAwZDMwMGQwOWQwMGQyMDBjZjg4NTJjNjUyY2M5NDAyMTAyNzk0MDBhNDc2MDJlODAzYTA2MzUzY2Q1MmM3ODg1M2QxNTJjZTg4NTNkMjUyY2Y4ODUzY2M3OGEyNjk2ODZkNzU1MTY3NzY1MTljNjNjMDAwOWQ1MWNlMDBjZTg4YzM1MjlkYzQ1MjlkNzg4Mjc3NTU5ZDUyNzk4Mjc3NTU5ZDUzNzk4Mjc3NTU5ZDU0Nzk4Mjc3NTQ5ZDAwY2YwMTI3N2Y3NTAxMTM3Zjc3MDBkMjUzN2E1NDdhN2U1NDdhN2U1NDdhN2U3YjdlODgwMGNkMDBjNzg4MDBkMTAwY2U4ODAwZDMwMGQwOWQwMGNjMDBjNjlkNTFkMjUxY2Y4ODUxY2Q1MWM3ODg1MWQxNTFjZTg4NTFkMzUxZDA5ZDUxY2M1MWM2MDIxMDI3OTRhMjc3Njc3NjUyOWM2MzUxY2UwMTIwN2Y1Mjg4MDBjZTUxY2U4OGMwMDBhMDY5YzA4YmMzNzhhMDYzNzZjZTUyNzk4ODY4MDBkMjAwY2Y4ODAwY2QwMGM3ODgwMGQxMDBjZTg4MDBkMzAwZDA5ZDAwY2MwMGM2MDIxMDI3OTRhMjY5YzQ1MTljNzc3Nzc3Njc1MzlkMDNjYzliMDFiMjc1YzQ1MTlkMDBkMTAwODc2ODY4NjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9LHsidHhpZCI6ImQxZDEwZjg1NGUzZGNkNzY1MmU4NWE5NTk4ZTNkYzgyNTBmOTQ4MDdlYzdkMzc5ZGYxYTRhYzFmODNmM2FiMTgiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiYzlhNDI2MDM4MDhmMjhlOGI0ZDc0ZmYzNzIxNjQwMWY1N2MxNzY4Y2Y2OTQzZThkOTdkOWVjZTgyZGI4NjM5MzM4YzY0ZmMwMzAzNmE1YTJiMDU2NmI4ZWQ4ODAwNmJjZjU4NDRjN2MyOWY5YmE4ODUxNDY5ZDMzNDI4NzI3N2RbQUxMfEZPUktJRF0gMDNlZjVkNmFhNDNkZTRjOWJjOGExYjdmMmMyZTMyNWUwYjliMjg2Njc4Mjc3OTI0ODIyNGJlNGQyYzBjNjMwZjVkIiwiaGV4IjoiNDFjOWE0MjYwMzgwOGYyOGU4YjRkNzRmZjM3MjE2NDAxZjU3YzE3NjhjZjY5NDNlOGQ5N2Q5ZWNlODJkYjg2MzkzMzhjNjRmYzAzMDM2YTVhMmIwNTY2YjhlZDg4MDA2YmNmNTg0NGM3YzI5ZjliYTg4NTE0NjlkMzM0Mjg3Mjc3ZDQxMjEwM2VmNWQ2YWE0M2RlNGM5YmM4YTFiN2YyYzJlMzI1ZTBiOWIyODY2NzgyNzc5MjQ4MjI0YmU0ZDJjMGM2MzBmNWQifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9XSwidm91dCI6W3sidmFsdWUiOjFlLTA1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBiNmVmMjlkYTUyNDk3YTI0M2E3M2E3MDExNWRlZGU2ZTM4NjUzMTQ4Y2NkYmFhZmEyNGY3ZjY2MmI4YmM1YjM3IE9QX0VRVUFMIiwiaGV4IjoiYWEyMGI2ZWYyOWRhNTI0OTdhMjQzYTczYTcwMTE1ZGVkZTZlMzg2NTMxNDhjY2RiYWFmYTI0ZjdmNjYyYjhiYzViMzc4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3bXc3Mnc2MmZ5aDVmcDZ3d25zejl3N21laHJzZWYzZnJ4ZGgyaDZ5bm1sdmM0Y2gzZG53enV0bHdldWsiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6IjVhNGY2YjI1MjQzYzFhMmRhYmIyNDM0ZTNkOWU1NzRmNjVjMzE3NjRjZTBlN2ViNDEyN2E0NmZhNzQ2NTc2OTEiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJtaW50aW5nIiwiY29tbWl0bWVudCI6IjAwY2E5YTNiMDAwMGUxZjUwNTAwMDA1YTYyMDIwMGI1ODMwZTAwYTZiYjU0YTk3MDI5NDYwMDFmMTRkMzNjOGYxODdkOWYyNDM0MzM2OCJ9fX0seyJ2YWx1ZSI6MWUtMDUsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA4NjQwMzY1N2YwYzdiMTc4OWViMjM0NzJmMTA3MjUwNjE5NDBmN2I0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ODY0MDM2NTdmMGM3YjE3ODllYjIzNDcyZjEwNzI1MDYxOTQwZjdiNDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenJ5cWRqaDdycm16N3k3a2c2ODl1Zzh5NXJwanM4aGtzNW1tZXhhNDMiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6IjVhNGY2YjI1MjQzYzFhMmRhYmIyNDM0ZTNkOWU1NzRmNjVjMzE3NjRjZTBlN2ViNDEyN2E0NmZhNzQ2NTc2OTEiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJub25lIiwiY29tbWl0bWVudCI6IjIyMDEwMiJ9fX0seyJ2YWx1ZSI6MWUtMDUsIm4iOjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhNmJiNTRhOTcwMjk0NjAwMWYxNGQzM2M4ZjE4N2Q5ZjI0MzQzMzY4IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YTZiYjU0YTk3MDI5NDYwMDFmMTRkMzNjOGYxODdkOWYyNDM0MzM2ODg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxem50azQ5ZndxNTV2cXFsem5mbmVyY2MwazBqZ2RwbmRxcnl4azJ1OHYiXX19LHsidmFsdWUiOjUuNWUtMDUsIm4iOjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA4NjQwMzY1N2YwYzdiMTc4OWViMjM0NzJmMTA3MjUwNjE5NDBmN2I0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ODY0MDM2NTdmMGM3YjE3ODllYjIzNDcyZjEwNzI1MDYxOTQwZjdiNDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenJ5cWRqaDdycm16N3k3a2c2ODl1Zzh5NXJwanM4aGtzNW1tZXhhNDMiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6IjVhNGY2YjI1MjQzYzFhMmRhYmIyNDM0ZTNkOWU1NzRmNjVjMzE3NjRjZTBlN2ViNDEyN2E0NmZhNzQ2NTc2OTEiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJtaW50aW5nIiwiY29tbWl0bWVudCI6IiJ9fX1dLCJmZWUiOjEuN2UtMDUsImhleCI6IjAyMDAwMDAwMDNlZWU5ZWExYTM2MDMwZjJjNjM1YTU4MDgxYjA5MmIzNWU0MDllNjhmMzVjNzc0OTE4ZTRlYjI1ZTc4MjljNTY5MDAwMDAwMDBmZGZkMDE1MjAwNGRmODAxNzYwMDljNjMwMGM3NTFjNzg4MDBjZTAxMjA3ZjUyODg1MWNlODgwMGNmMDExMzdmNzU1ZjdmNzdjNTdjODE5ZjY5YzM1MzlkYzQ1NGExNjkwMDAwNTM3OTUxODc2MzAwY2Y1NTdmNzU1MWNmNTE3ZTUzN2E3NTdjNmI3YzZjNzY4MTdiNzU3NzY3NTM3OTUyODc2MzAwY2Y1YTdmNzU1NTdmNzc3NjA2MDAwMDAwMDAwMDAwODc5MTY5NzY4MTdiNzU3YzUxY2Y1MjdlNTM3YTc1N2M2YjdjNmM3NTY3NTM3OTUzODgwMGNmNWY3Zjc1NWE3Zjc3NzYwNjAwMDAwMDAwMDAwMDg3OTE2OTc2ODE3Yjc1N2M1MWNmNTM3ZTUzN2E3NTdjNmI3YzZjNzU2ODY4NTFkMjdiODg1MWNkNTJjNzg4NTFkMTUxY2U4ODUxZDM1MWQwOWQ1MWNjNTFjNjlkNTJjZTAwODc1MmNlMDBjZTg3NzY3YjliNjk3NjYzYzQ1NDlkNjg2MzAyZTgwMzc3NjgwMGNmMDEyNzdmNzUwMTEzN2Y3NzAzNzZhOTE0N2M3ZTAyODhhYzdlNTJjZDg4NTJkMTAwODg1MmNjOWQwMGNjMDBjNjlkMDBjZDAwYzc4ODAwZDEwMGNlODgwMGQzMDBkMDlkMDBkMjAwY2Y4ODUyYzY1MmNjOTQwMjEwMjc5NDAwYTQ3NjAyZTgwM2EwNjM1M2NkNTJjNzg4NTNkMTUyY2U4ODUzZDI1MmNmODg1M2NjNzhhMjY5Njg2ZDc1NTE2Nzc2NTE5YzYzYzAwMDlkNTFjZTAwY2U4OGMzNTI5ZGM0NTI5ZDc4ODI3NzU1OWQ1Mjc5ODI3NzU1OWQ1Mzc5ODI3NzU1OWQ1NDc5ODI3NzU0OWQwMGNmMDEyNzdmNzUwMTEzN2Y3NzAwZDI1MzdhNTQ3YTdlNTQ3YTdlNTQ3YTdlN2I3ZTg4MDBjZDAwYzc4ODAwZDEwMGNlODgwMGQzMDBkMDlkMDBjYzAwYzY5ZDUxZDI1MWNmODg1MWNkNTFjNzg4NTFkMTUxY2U4ODUxZDM1MWQwOWQ1MWNjNTFjNjAyMTAyNzk0YTI3NzY3NzY1MjljNjM1MWNlMDEyMDdmNTI4ODAwY2U1MWNlODhjMDAwYTA2OWMwOGJjMzc4YTA2Mzc2Y2U1Mjc5ODg2ODAwZDIwMGNmODgwMGNkMDBjNzg4MDBkMTAwY2U4ODAwZDMwMGQwOWQwMGNjMDBjNjAyMTAyNzk0YTI2OWM0NTE5Yzc3Nzc3NzY3NTM5ZDAzY2M5YjAxYjI3NWM0NTE5ZDAwZDEwMDg3Njg2ODY4ZmVmZmZmZmZkODk4ODQ4OTJiMzU3MzYzMmQ3Nzg2NjllNmE0ODEyNzMwZDdhYzNjY2FmYjY3MWMyNTE0OGUxMzNmZDZkYjk3NGMwMDAwMDBmZGZkMDE1MjAwNGRmODAxNzYwMDljNjMwMGM3NTFjNzg4MDBjZTAxMjA3ZjUyODg1MWNlODgwMGNmMDExMzdmNzU1ZjdmNzdjNTdjODE5ZjY5YzM1MzlkYzQ1NGExNjkwMDAwNTM3OTUxODc2MzAwY2Y1NTdmNzU1MWNmNTE3ZTUzN2E3NTdjNmI3YzZjNzY4MTdiNzU3NzY3NTM3OTUyODc2MzAwY2Y1YTdmNzU1NTdmNzc3NjA2MDAwMDAwMDAwMDAwODc5MTY5NzY4MTdiNzU3YzUxY2Y1MjdlNTM3YTc1N2M2YjdjNmM3NTY3NTM3OTUzODgwMGNmNWY3Zjc1NWE3Zjc3NzYwNjAwMDAwMDAwMDAwMDg3OTE2OTc2ODE3Yjc1N2M1MWNmNTM3ZTUzN2E3NTdjNmI3YzZjNzU2ODY4NTFkMjdiODg1MWNkNTJjNzg4NTFkMTUxY2U4ODUxZDM1MWQwOWQ1MWNjNTFjNjlkNTJjZTAwODc1MmNlMDBjZTg3NzY3YjliNjk3NjYzYzQ1NDlkNjg2MzAyZTgwMzc3NjgwMGNmMDEyNzdmNzUwMTEzN2Y3NzAzNzZhOTE0N2M3ZTAyODhhYzdlNTJjZDg4NTJkMTAwODg1MmNjOWQwMGNjMDBjNjlkMDBjZDAwYzc4ODAwZDEwMGNlODgwMGQzMDBkMDlkMDBkMjAwY2Y4ODUyYzY1MmNjOTQwMjEwMjc5NDAwYTQ3NjAyZTgwM2EwNjM1M2NkNTJjNzg4NTNkMTUyY2U4ODUzZDI1MmNmODg1M2NjNzhhMjY5Njg2ZDc1NTE2Nzc2NTE5YzYzYzAwMDlkNTFjZTAwY2U4OGMzNTI5ZGM0NTI5ZDc4ODI3NzU1OWQ1Mjc5ODI3NzU1OWQ1Mzc5ODI3NzU1OWQ1NDc5ODI3NzU0OWQwMGNmMDEyNzdmNzUwMTEzN2Y3NzAwZDI1MzdhNTQ3YTdlNTQ3YTdlNTQ3YTdlN2I3ZTg4MDBjZDAwYzc4ODAwZDEwMGNlODgwMGQzMDBkMDlkMDBjYzAwYzY5ZDUxZDI1MWNmODg1MWNkNTFjNzg4NTFkMTUxY2U4ODUxZDM1MWQwOWQ1MWNjNTFjNjAyMTAyNzk0YTI3NzY3NzY1MjljNjM1MWNlMDEyMDdmNTI4ODAwY2U1MWNlODhjMDAwYTA2OWMwOGJjMzc4YTA2Mzc2Y2U1Mjc5ODg2ODAwZDIwMGNmODgwMGNkMDBjNzg4MDBkMTAwY2U4ODAwZDMwMGQwOWQwMGNjMDBjNjAyMTAyNzk0YTI2OWM0NTE5Yzc3Nzc3NzY3NTM5ZDAzY2M5YjAxYjI3NWM0NTE5ZDAwZDEwMDg3Njg2ODY4ZmVmZmZmZmYxOGFiZjM4MzFmYWNhNGYxOWQzNzdkZWMwNzQ4Zjk1MDgyZGNlMzk4OTU1YWU4NTI3NmNkM2Q0ZTg1MGZkMWQxMDAwMDAwMDA2NDQxYzlhNDI2MDM4MDhmMjhlOGI0ZDc0ZmYzNzIxNjQwMWY1N2MxNzY4Y2Y2OTQzZThkOTdkOWVjZTgyZGI4NjM5MzM4YzY0ZmMwMzAzNmE1YTJiMDU2NmI4ZWQ4ODAwNmJjZjU4NDRjN2MyOWY5YmE4ODUxNDY5ZDMzNDI4NzI3N2Q0MTIxMDNlZjVkNmFhNDNkZTRjOWJjOGExYjdmMmMyZTMyNWUwYjliMjg2Njc4Mjc3OTI0ODIyNGJlNGQyYzBjNjMwZjVkZmVmZmZmZmYwNGU4MDMwMDAwMDAwMDAwMDA2ZGVmOTE3NjY1NzRmYTQ2N2ExMmI0N2UwZWNlNjQxN2MzNjU0ZjU3OWUzZDRlNDNiMmFiMmQxYTNjMjQyNTZiNGY1YTYyMjcwMGNhOWEzYjAwMDBlMWY1MDUwMDAwNWE2MjAyMDBiNTgzMGUwMGE2YmI1NGE5NzAyOTQ2MDAxZjE0ZDMzYzhmMTg3ZDlmMjQzNDMzNjhhYTIwYjZlZjI5ZGE1MjQ5N2EyNDNhNzNhNzAxMTVkZWRlNmUzODY1MzE0OGNjZGJhYWZhMjRmN2Y2NjJiOGJjNWIzNzg3ZTgwMzAwMDAwMDAwMDAwMDNmZWY5MTc2NjU3NGZhNDY3YTEyYjQ3ZTBlY2U2NDE3YzM2NTRmNTc5ZTNkNGU0M2IyYWIyZDFhM2MyNDI1NmI0ZjVhNjAwMzIyMDEwMjc2YTkxNDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQ4OGFjZTgwMzAwMDAwMDAwMDAwMDE5NzZhOTE0YTZiYjU0YTk3MDI5NDYwMDFmMTRkMzNjOGYxODdkOWYyNDM0MzM2ODg4YWM3YzE1MDAwMDAwMDAwMDAwM2JlZjkxNzY2NTc0ZmE0NjdhMTJiNDdlMGVjZTY0MTdjMzY1NGY1NzllM2Q0ZTQzYjJhYjJkMWEzYzI0MjU2YjRmNWEyMjc2YTkxNDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQ4OGFjMDAwMDAwMDAifSx7InR4aWQiOiI5M2Q0NjNlYjZhNDIwZjM1ODBkYjhlYmU1MDcxYWY2ZGZlNTJhYjRjNjU3YzZkY2U3Zjg5ZTQ3ZDJjZmE5NGQ2IiwiaGFzaCI6IjkzZDQ2M2ViNmE0MjBmMzU4MGRiOGViZTUwNzFhZjZkZmU1MmFiNGM2NTdjNmRjZTdmODllNDdkMmNmYTk0ZDYiLCJ2ZXJzaW9uIjoyLCJzaXplIjoyNDQxLCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiMjg4OTlkNjgxMTVlOWZkYThlZWFiYmM0NWEwMDg5NjVkNzQ3MzU5NGRkZmFmMWYyYTFkNGU2Yjc5YzhlYmYyZiIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiI5MWJmYTVhMjQ0MDFkYTk4OGFhYzQwNzcwMDU3NjJmZWJlODgzYzZiYTJiM2MzZTU5YmVlNzY4NDY1MzkyZmU0Yzg1YjU3NzI4MDhiZDg4MTBjN2Q2YWMyNTc3ZTYwYzdjMDYyMDdiNDRlMjVjZGYzYzlhN2EwYTkwZjdiYmEyYVtBTEx8Rk9SS0lEXSAwMjRjZDc3NDFjOGU2MTYzMTQ1MDc5NDY1MjA3ODNhZGQyZTkwMDk1MDBiYWI5Y2JkMDg5M2EwNjE4MTcxZmJlYjUiLCJoZXgiOiI0MTkxYmZhNWEyNDQwMWRhOTg4YWFjNDA3NzAwNTc2MmZlYmU4ODNjNmJhMmIzYzNlNTliZWU3Njg0NjUzOTJmZTRjODViNTc3MjgwOGJkODgxMGM3ZDZhYzI1NzdlNjBjN2MwNjIwN2I0NGUyNWNkZjNjOWE3YTBhOTBmN2JiYTJhNDEyMTAyNGNkNzc0MWM4ZTYxNjMxNDUwNzk0NjUyMDc4M2FkZDJlOTAwOTUwMGJhYjljYmQwODkzYTA2MTgxNzFmYmViNSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiODI0MzllM2ViMWViNjI1OWFhY2FhMThlM2E5YmU5NTY1YTJkMzdkODlhMjQ4MjUzZWUxM2Q5MGI5YTlhMDJiYiIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiI1ZDc0MjEyODhhMTQ4ZjgyNzhmZmU2NjRjMGJiN2E3MzE4NWRmNWM3NTNlZjg1MTllNjU1YjBkNDc3M2FkNWM5YzcyODFhNWRhNzFhMmU4MzEwNzFhNzMzM2QyNmRiYjlkYmI5ZGYwZTcwMzM5ODI4OGUxNTM1NTE0Y2ZhOGZjNFtBTEx8Rk9SS0lEXSAwMjRjZDc3NDFjOGU2MTYzMTQ1MDc5NDY1MjA3ODNhZGQyZTkwMDk1MDBiYWI5Y2JkMDg5M2EwNjE4MTcxZmJlYjUiLCJoZXgiOiI0MTVkNzQyMTI4OGExNDhmODI3OGZmZTY2NGMwYmI3YTczMTg1ZGY1Yzc1M2VmODUxOWU2NTViMGQ0NzczYWQ1YzljNzI4MWE1ZGE3MWEyZTgzMTA3MWE3MzMzZDI2ZGJiOWRiYjlkZjBlNzAzMzk4Mjg4ZTE1MzU1MTRjZmE4ZmM0NDEyMTAyNGNkNzc0MWM4ZTYxNjMxNDUwNzk0NjUyMDc4M2FkZDJlOTAwOTUwMGJhYjljYmQwODkzYTA2MTgxNzFmYmViNSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYmI1ZGVjZThjNDM4MjIwZjNlYTY3YTM5YTEwMzkxMGVlZTVmNWExZGE0ODM5ZTg0ZDNhYjk5ODMzM2Q4MWNjYSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiJlMzc4NzdhZDMyOTA3NDM3ZTYzZDFmOTk5NTk4YzYyMTY2NGQyYjFlZmI2YTE2YjI4MTVjNWRiNzA1MmUwYjJmYWU5OGRlNzYyOGViNDcwNDZhZDYzZGY4MmU2ZmZjZjIxNDA0ZGNlMjYxM2MxY2FkYzBhM2U4ZTU3YzQzMDBlOVtBTEx8Rk9SS0lEXSAwMmRiYjg0NmFkYzcwMjU0ZmI3ZGE2YTIzMDc3NWFhYTg1MjU3ZTIzY2MzMGVjNTBiMzhmNDRmZTM1NjZlNTA0OTYiLCJoZXgiOiI0MWUzNzg3N2FkMzI5MDc0MzdlNjNkMWY5OTk1OThjNjIxNjY0ZDJiMWVmYjZhMTZiMjgxNWM1ZGI3MDUyZTBiMmZhZTk4ZGU3NjI4ZWI0NzA0NmFkNjNkZjgyZTZmZmNmMjE0MDRkY2UyNjEzYzFjYWRjMGEzZThlNTdjNDMwMGU5NDEyMTAyZGJiODQ2YWRjNzAyNTRmYjdkYTZhMjMwNzc1YWFhODUyNTdlMjNjYzMwZWM1MGIzOGY0NGZlMzU2NmU1MDQ5NiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNmUyMTUyMDgyYTc5YjJkYmU5N2NhZWZlODkwOTcyODZlZjY3ZWE4YTA3YWRhZDI2MTU4ZGIzYWUxNzgyMTEwZiIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiJkMjM3MTNlNjUyOGJkMTMyYmY2ZDQ3N2Q4YTYxYzE4M2NkN2IyNTUwMTdhZDVlZTgyYzkyOGExNDM5YjZjMGY1YmFmZTAwYWU1OWViMDgxNmI1Y2M5MTQ5MTVlMGJhYjI4YmE0ZmM4NzVmYjMyZDBmYmFkY2RkMjhhMDk2OTBiNltBTEx8Rk9SS0lEXSAwMzkyZmYwZGRjMDUxNTQzZGNkMTQzNmNhMzdkNTAwMzI3ZDFmZDU1ZDVmZWIxY2NmNDMxYjA1YTdiMDI0ZjFhYWIiLCJoZXgiOiI0MWQyMzcxM2U2NTI4YmQxMzJiZjZkNDc3ZDhhNjFjMTgzY2Q3YjI1NTAxN2FkNWVlODJjOTI4YTE0MzliNmMwZjViYWZlMDBhZTU5ZWIwODE2YjVjYzkxNDkxNWUwYmFiMjhiYTRmYzg3NWZiMzJkMGZiYWRjZGQyOGEwOTY5MGI2NDEyMTAzOTJmZjBkZGMwNTE1NDNkY2QxNDM2Y2EzN2Q1MDAzMjdkMWZkNTVkNWZlYjFjY2Y0MzFiMDVhN2IwMjRmMWFhYiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiODk3YzNlMWY0YjA0NDdjZTMwZjQyOTNlMDE1NDE5YTFlYjNiM2NiZmQ3ZTVhNzY2ZjE0OWE5ZWQyMTJkM2UwMSIsInZvdXQiOjMsInNjcmlwdFNpZyI6eyJhc20iOiI3ZjU4ODZiMjExNjE0ZDU2N2E5ODMwNWE2YmM1M2E5MmYzNjZiNzdhM2FhNzdmMzViNzNjMmU4NWFkNjI1NGE3NjJmOTA0MGY2NDZkNTMzYTk1MmNhYWUzM2IyODZlNWM4ZmY0MDhlMzk2ZjJiM2JjODVjODhiNTVhOTIzOTRkNltBTEx8Rk9SS0lEXSAwMjNiOWMzNzBmYmYxMjEyZGE4MDBjOTMxYmE2ZGY0M2VkNjIwOGQxYjc3MGVlMDQ1MGNkNGEzMTA5ZTNjMDY5NjciLCJoZXgiOiI0MTdmNTg4NmIyMTE2MTRkNTY3YTk4MzA1YTZiYzUzYTkyZjM2NmI3N2EzYWE3N2YzNWI3M2MyZTg1YWQ2MjU0YTc2MmY5MDQwZjY0NmQ1MzNhOTUyY2FhZTMzYjI4NmU1YzhmZjQwOGUzOTZmMmIzYmM4NWM4OGI1NWE5MjM5NGQ2NDEyMTAyM2I5YzM3MGZiZjEyMTJkYTgwMGM5MzFiYTZkZjQzZWQ2MjA4ZDFiNzcwZWUwNDUwY2Q0YTMxMDllM2MwNjk2NyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYWNjMWNmNjZjZDRlNWQxMTM1NzJhZjZjMDRjZDFhYzUzYjlmYThmYTkzNWI4N2ZmMWE2NjI0MzU5M2NiMzliNiIsInZvdXQiOjUsInNjcmlwdFNpZyI6eyJhc20iOiJkODU4OGU3Njg3YWQxNGMxNjUxMGQzNjU3MWFmYWU0ZDhmYTNlMjc2MWFmNDJkNmQ0YmM2MzkxNjkyYzIxN2UyMGMxYjdlNzFlZDdmNjA4ZDE0MWI1ZTJhYWU0MDEwZjFmNGVkODQ4YjRmNjNjMWNjZjBiYTNmM2U1YTNjMTBiY1tBTEx8Rk9SS0lEXSAwMzMxNjAxNjZmMjdhNzU1NmY0ZGJlODAzMWFhMGRlNGQ4NTI0ZDgyYjYzMDU0YjZiMmRmNmJjZTc5MDhmMmZiNGQiLCJoZXgiOiI0MWQ4NTg4ZTc2ODdhZDE0YzE2NTEwZDM2NTcxYWZhZTRkOGZhM2UyNzYxYWY0MmQ2ZDRiYzYzOTE2OTJjMjE3ZTIwYzFiN2U3MWVkN2Y2MDhkMTQxYjVlMmFhZTQwMTBmMWY0ZWQ4NDhiNGY2M2MxY2NmMGJhM2YzZTVhM2MxMGJjNDEyMTAzMzE2MDE2NmYyN2E3NTU2ZjRkYmU4MDMxYWEwZGU0ZDg1MjRkODJiNjMwNTRiNmIyZGY2YmNlNzkwOGYyZmI0ZCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZWRhN2Y5MTBjZGNiOWIyZmZkMDc2ZTRiZmU5ODliMDJiMmJiMmRmOGRmZjYzNmEzYWY2NTAyNDY1ZDYwYjlmNiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIyZWE2OTc1NzE1N2FlNTE5OWQwODgyZjA0YTUyYWRiMzY5ZjdlN2NiOTY1MTIyMDViNWJmY2UwYmNmODM3NGNjMGVjYWY3NTE0MzBhOWNjZDQyZWNlNjE0OTE5YmZmZTBlNjNjMTQ1YWExMWIyNzZhYmI1NDhiY2U3MzQxZDcyMVtBTEx8Rk9SS0lEXSAwMmZlYWEzMTYwODkzZTM5MmQzYWFiYmY1ZTQyZjMyMmE3MzE4NmQ5MzUwMjczMDlmODEzMWE3YTQ5MDkyYWE0Y2UiLCJoZXgiOiI0MTJlYTY5NzU3MTU3YWU1MTk5ZDA4ODJmMDRhNTJhZGIzNjlmN2U3Y2I5NjUxMjIwNWI1YmZjZTBiY2Y4Mzc0Y2MwZWNhZjc1MTQzMGE5Y2NkNDJlY2U2MTQ5MTliZmZlMGU2M2MxNDVhYTExYjI3NmFiYjU0OGJjZTczNDFkNzIxNDEyMTAyZmVhYTMxNjA4OTNlMzkyZDNhYWJiZjVlNDJmMzIyYTczMTg2ZDkzNTAyNzMwOWY4MTMxYTdhNDkwOTJhYTRjZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZTUyM2UyNzQ2NTJlNjg3ZDc2MzliNTJiNjNkODg3YzAyMjA5MGQ0ODNhYWJmM2Q4Yzg3Y2NjYTUwZTI5YTUyNyIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiIxM2FhOTAzNTc4YTAyNmZhMDdmY2ViNTFmNzFkN2M1NTg1ODAzMzFhMzFlZjQ2YWI1ZThjZTlmZDU3NmRhZTI2NzVjOGMwMGMwMThhMjUzZGI5N2Y2ZmIxMjkxM2Y2NThiMDlhNTZlMTJiOTJiODU2OTU0NjI1OTBmMTJhZDcyZFtBTEx8Rk9SS0lEXSAwM2IxNWRiNjc2MWFlZWE4MTAzNDg5NmFkOTQxYjBlZGUxMGM4OGQ2ZTYwMGY3ZDI3MDYzMzVkNTEyOTRmNjM4OTUiLCJoZXgiOiI0MTEzYWE5MDM1NzhhMDI2ZmEwN2ZjZWI1MWY3MWQ3YzU1ODU4MDMzMWEzMWVmNDZhYjVlOGNlOWZkNTc2ZGFlMjY3NWM4YzAwYzAxOGEyNTNkYjk3ZjZmYjEyOTEzZjY1OGIwOWE1NmUxMmI5MmI4NTY5NTQ2MjU5MGYxMmFkNzJkNDEyMTAzYjE1ZGI2NzYxYWVlYTgxMDM0ODk2YWQ5NDFiMGVkZTEwYzg4ZDZlNjAwZjdkMjcwNjMzNWQ1MTI5NGY2Mzg5NSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiM2EyMTNhOWM5NGM0NDcwMDFiNjY5MjM4ZThmYzJlM2ZmZDIxNTIyOGI2M2E3YTA1NzJkZDU0NWFiMWQxMWZiNSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIxN2IzY2UzOTljYTM4OGE2MzUyMWZjOTc2ZWQ0YzkwNzA3OWZhMTQ1ZWZlMGRmZGY4MTBiODdkMmI2YmMzOTU0MzhhMzg5MjQzNWQ1MDA5YTFlNmFmMzNhNjE2ZTIyYzE4MTdjZTdiYWFiODJiYzUxYTRkNGEzZjNiYjNiNDQxN1tBTEx8Rk9SS0lEXSAwM2ViMWExNzlkMmI1NDkzZmUyMWUxMTAwMGViNzM5ZTk4YjBmYzg1NThhMWNjNDk2ZGRlNTg1NzAwMDc4MmRjYjkiLCJoZXgiOiI0MTE3YjNjZTM5OWNhMzg4YTYzNTIxZmM5NzZlZDRjOTA3MDc5ZmExNDVlZmUwZGZkZjgxMGI4N2QyYjZiYzM5NTQzOGEzODkyNDM1ZDUwMDlhMWU2YWYzM2E2MTZlMjJjMTgxN2NlN2JhYWI4MmJjNTFhNGQ0YTNmM2JiM2I0NDE3NDEyMTAzZWIxYTE3OWQyYjU0OTNmZTIxZTExMDAwZWI3MzllOThiMGZjODU1OGExY2M0OTZkZGU1ODU3MDAwNzgyZGNiOSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZjk4YjIwMzAxZDYyY2YyZDdhNDE5NTUzNDE5YzEzNDFmYThjZThkNzc2NjFkMWE3NWY1MjkyYWI2MDY2Y2UzOSIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiI2Njc1NTMzOTU5ZTM3MWE1ZmFlYWUzZDdiNGRhN2NmOWIyMDNhYThkNjJjZTgxNzRmYjEzZGUyNmExMjhhZGRiNmFlNDExYTgwMjA4OWQ0MzZiNzU5ZDEzN2QwZmU1OWM5MTk3ZGU5OTU0ZTYyMjMzYWU4MTFjOGQyNGExNjRlZFtBTEx8Rk9SS0lEXSAwM2ViMWExNzlkMmI1NDkzZmUyMWUxMTAwMGViNzM5ZTk4YjBmYzg1NThhMWNjNDk2ZGRlNTg1NzAwMDc4MmRjYjkiLCJoZXgiOiI0MTY2NzU1MzM5NTllMzcxYTVmYWVhZTNkN2I0ZGE3Y2Y5YjIwM2FhOGQ2MmNlODE3NGZiMTNkZTI2YTEyOGFkZGI2YWU0MTFhODAyMDg5ZDQzNmI3NTlkMTM3ZDBmZTU5YzkxOTdkZTk5NTRlNjIyMzNhZTgxMWM4ZDI0YTE2NGVkNDEyMTAzZWIxYTE3OWQyYjU0OTNmZTIxZTExMDAwZWI3MzllOThiMGZjODU1OGExY2M0OTZkZGU1ODU3MDAwNzgyZGNiOSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYzc0YmFmMmRiYWQzNjI4NTljMzg0NTQ3ZWVkNWI4NzQ4OTFiMmU0NjIyMGYzY2EyOWMwNDljYTc4ZDI2MjkwNyIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI4ZmNlZWVkZGY5OWIzMjhlZGZiOTNmMzA3MjQzM2FkYTNmOGRmMTA1MWNkNGYxY2I3ZWFiZWRjMTEwNzYxNjdmMDg1Y2ZkM2Y0ODA3MTVjYTg3MmNhNjEyODMyZjBkNTQzNDVlMzM2YzAyM2I4ODI3NDU0MzNlN2EzNTViYzcwZVtBTEx8Rk9SS0lEXSAwMjRkYWZlZDY1OTllZTQzODQ5MTg4NDU0YzZiNjFmYmFhYjNkZTdjMDcwYWEwZDJkZWVlNTBjNzE1ODhjN2RmY2QiLCJoZXgiOiI0MThmY2VlZWRkZjk5YjMyOGVkZmI5M2YzMDcyNDMzYWRhM2Y4ZGYxMDUxY2Q0ZjFjYjdlYWJlZGMxMTA3NjE2N2YwODVjZmQzZjQ4MDcxNWNhODcyY2E2MTI4MzJmMGQ1NDM0NWUzMzZjMDIzYjg4Mjc0NTQzM2U3YTM1NWJjNzBlNDEyMTAyNGRhZmVkNjU5OWVlNDM4NDkxODg0NTRjNmI2MWZiYWFiM2RlN2MwNzBhYTBkMmRlZWU1MGM3MTU4OGM3ZGZjZCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMjE3N2M2NzQyNGEzNjBlMzBjNzg5ZWRkOTc1OTA2Yzk3MzQzNGM1NjE3Y2EwYTU2MWUxOWU0YjlhNDI3NDRjYSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIxNzQ2Njc1ZmFjYjI0NjFlYTU3MmQ3ZmZlYzNmOWIwZjgxMGIzMGNiMGRjMWFkY2I4ZTM5NjU0ZTA0MzJmZTJkOWE1ZjEyMDNjY2I5MjdjYTQwNzdiMjNhMGMwZmJiZDk5MTQ0NmNiYzBjM2VkMWRlNzBkMmE4MTI0Zjc3OWZkMltBTEx8Rk9SS0lEXSAwMmYzOGZjYThjYTFlYzZjNzBjYzNkZWI1YTI0MjBkNjRkZTlkMjczMDA5OWJlYzk3ZDdhZGMxMDdjMWFiNzY1YTYiLCJoZXgiOiI0MTE3NDY2NzVmYWNiMjQ2MWVhNTcyZDdmZmVjM2Y5YjBmODEwYjMwY2IwZGMxYWRjYjhlMzk2NTRlMDQzMmZlMmQ5YTVmMTIwM2NjYjkyN2NhNDA3N2IyM2EwYzBmYmJkOTkxNDQ2Y2JjMGMzZWQxZGU3MGQyYTgxMjRmNzc5ZmQyNDEyMTAyZjM4ZmNhOGNhMWVjNmM3MGNjM2RlYjVhMjQyMGQ2NGRlOWQyNzMwMDk5YmVjOTdkN2FkYzEwN2MxYWI3NjVhNiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMDBmNDJmOTk2MmRmZDExMGFiZGJhZTBhYTEyNTA3ZTI2OGFjODI2NDE5MTQxZGVjOTRjYmUwMzYwNjcyMTg0OCIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzZjJjZTlmNTVlYjJjZmQzZTY3NmVmNTkxNGViZTQ5NDFlZWY0NGFmNjZkOWRjNTNmZjE1YzEwZGY2NjcxMzU2YTQzZDQzNzI1MDFjMjAyZWM3YWVlMTE4ZDUxOGIzNGI2ZmVlMjg5NGZmMTg2Y2ZjMGMwZDU4MWE3Y2I0YTU1OFtBTEx8Rk9SS0lEXSAwM2YwN2FjY2FiZTk4ZDYxZjNjODQxN2IzNTVlOTY0NDdhNDY5NDE2N2I4MjdkMzQzNjIzOWMxZmEyYmUwNTkyNzUiLCJoZXgiOiI0MTNmMmNlOWY1NWViMmNmZDNlNjc2ZWY1OTE0ZWJlNDk0MWVlZjQ0YWY2NmQ5ZGM1M2ZmMTVjMTBkZjY2NzEzNTZhNDNkNDM3MjUwMWMyMDJlYzdhZWUxMThkNTE4YjM0YjZmZWUyODk0ZmYxODZjZmMwYzBkNTgxYTdjYjRhNTU4NDEyMTAzZjA3YWNjYWJlOThkNjFmM2M4NDE3YjM1NWU5NjQ0N2E0Njk0MTY3YjgyN2QzNDM2MjM5YzFmYTJiZTA1OTI3NSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiM2Q0MjExOTUyNTc4ZjE2MjZhNzIyNzViMzZmMzZjZTMzN2RlNDc1MGE2MTg2ZjY3YjE5MDRjMDkyYzFiMzNjMSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiI1ZTQ0NjRlMGYyZDUyZTIxZDhhOGVhMjI2ODA4NDM0ZWVjMWFlMzMyMjJkMDNjMGY0ZWFmYTRkM2IwYjZkNTUzZDM2NmRmZWJmN2Y1MTg3NjBlOTM5MzBkYWM2NTk5MDcxNWQxY2QxMWM4M2YzZDJiODI3ZDU2OGI4Mzc3MTgwYltBTEx8Rk9SS0lEXSAwMmMxYzlhYWJmZWM1MDg2YzAzNmMxNGFkZThlNGVkOGIxYjQ0MGIzYjk2MWQ2ZDk1NDg4Y2U3MGI3YzM4NWI3M2UiLCJoZXgiOiI0MTVlNDQ2NGUwZjJkNTJlMjFkOGE4ZWEyMjY4MDg0MzRlZWMxYWUzMzIyMmQwM2MwZjRlYWZhNGQzYjBiNmQ1NTNkMzY2ZGZlYmY3ZjUxODc2MGU5MzkzMGRhYzY1OTkwNzE1ZDFjZDExYzgzZjNkMmI4MjdkNTY4YjgzNzcxODBiNDEyMTAyYzFjOWFhYmZlYzUwODZjMDM2YzE0YWRlOGU0ZWQ4YjFiNDQwYjNiOTYxZDZkOTU0ODhjZTcwYjdjMzg1YjczZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiODljOTFjNTI3OTNlYjM2ZGVmMzFlMDNhZmVhMjYyYzIwMDIyZjg0MjNlNGJhZjk5ZjViOWE4MmYxMzMwOTk5MyIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiJlMDAzZGQxNzk2NmI4NjJjZWZlMDY0NzAzMDg5MTA0MjFjMTg4ZTMwZmQ1MDBhY2MxODE3YzRmMTE4YzdmMWQ4NmZkZDFiMGM5NDI0ZTUyMGMzNmM0NzMxMDhhZDA2MzliYzI4MTFjY2EyMjAxMTVmZWJhNjA3Yjc1YWY2MTM3M1tBTEx8Rk9SS0lEXSAwMzc5NTlkYjBlNmJmYmZlMTU2NjBmOGIzYWUxYTNmOWJmNTY0YzM3ZWMzNjc1MjIzY2IxNTQzNTM5MjIyZTIyN2IiLCJoZXgiOiI0MWUwMDNkZDE3OTY2Yjg2MmNlZmUwNjQ3MDMwODkxMDQyMWMxODhlMzBmZDUwMGFjYzE4MTdjNGYxMThjN2YxZDg2ZmRkMWIwYzk0MjRlNTIwYzM2YzQ3MzEwOGFkMDYzOWJjMjgxMWNjYTIyMDExNWZlYmE2MDdiNzVhZjYxMzczNDEyMTAzNzk1OWRiMGU2YmZiZmUxNTY2MGY4YjNhZTFhM2Y5YmY1NjRjMzdlYzM2NzUyMjNjYjE1NDM1MzkyMjJlMjI3YiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMmZmMzY4ODIwMzliYjNlN2I3MmVmZDE5ZGExMGU3ODEyMzJkMWRlZDczYWU4MzczYjEwZWRmZmVhMjI3NjljOCIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMzU2ZTFjMjQzYzRlNTM2OTVkZDc0OGQxZGM0ODk3MWYzZGIzZDgwYjllZjFlYTE1NzNiNmM4MzBhNzc0MTNkMzUxNzQxZjNhZTUzMzgyZTdjYzNiMGMxOTQzZjIyOGExYzVhMjAyOTE5YmUxMjdjOGExMDA3ZTIwZGI3MzRlMVtBTEx8Rk9SS0lEXSAwMzc5NTlkYjBlNmJmYmZlMTU2NjBmOGIzYWUxYTNmOWJmNTY0YzM3ZWMzNjc1MjIzY2IxNTQzNTM5MjIyZTIyN2IiLCJoZXgiOiI0MTMzNTZlMWMyNDNjNGU1MzY5NWRkNzQ4ZDFkYzQ4OTcxZjNkYjNkODBiOWVmMWVhMTU3M2I2YzgzMGE3NzQxM2QzNTE3NDFmM2FlNTMzODJlN2NjM2IwYzE5NDNmMjI4YTFjNWEyMDI5MTliZTEyN2M4YTEwMDdlMjBkYjczNGUxNDEyMTAzNzk1OWRiMGU2YmZiZmUxNTY2MGY4YjNhZTFhM2Y5YmY1NjRjMzdlYzM2NzUyMjNjYjE1NDM1MzkyMjJlMjI3YiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMTgzNGRjOWRhYjg4NDM3ZWEyZmY2MDlmNzA4MWYzOTA3YTA4ZDYyYWU4M2M4NWRmMjI1MmMxOTkwZmNkMGIyNyIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiJlZjdjODk1ZDM1YmIyYTU5YmQwYzNjNzJmMWYwMWFlNzg1ODViNzQ1NzEzYWI1YTQ1N2RkOGM2MmE3NGJhMzY4MzRkYzgzNWQ0ZjUwMTlkN2M4YzEwZmZjMDE2NDU0ZmI4ZjAzY2NkNzhhZjEyNjdiYjYxYTk0NTU1ZTE3ZTg3YVtBTEx8Rk9SS0lEXSAwMzJlODJlNjAzOGZkOTQ1OGU0ZDdiNThkZmE2YmZlMTUxZDYyYWYxZTQ0NDBiOGNhOGIwYjNlZjlkMzg1NDlhZWUiLCJoZXgiOiI0MWVmN2M4OTVkMzViYjJhNTliZDBjM2M3MmYxZjAxYWU3ODU4NWI3NDU3MTNhYjVhNDU3ZGQ4YzYyYTc0YmEzNjgzNGRjODM1ZDRmNTAxOWQ3YzhjMTBmZmMwMTY0NTRmYjhmMDNjY2Q3OGFmMTI2N2JiNjFhOTQ1NTVlMTdlODdhNDEyMTAzMmU4MmU2MDM4ZmQ5NDU4ZTRkN2I1OGRmYTZiZmUxNTFkNjJhZjFlNDQ0MGI4Y2E4YjBiM2VmOWQzODU0OWFlZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MS45NzYyNDQzNSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGQ2NzIyYmFlOGVkNDkyMGFhMjZlZWM4ZGRlZmFhOWQ3MzE2NTViMzYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkNjcyMmJhZThlZDQ5MjBhYTI2ZWVjOGRkZWZhYTlkNzMxNjU1YjM2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFydDh5MmF3M20yZnl6NHpkbWtnbWhoNjQ4dG56ZTJteGM2OHB3cTl4bSJdfX1dLCJmZWUiOjIuNDQxZS0wNSwiaGV4IjoiMDIwMDAwMDAxMTJmYmY4ZTljYjdlNmQ0YTFmMmYxZmFkZDk0MzU0N2Q3NjU4OTAwNWFjNGJiZWE4ZWRhOWY1ZTExNjg5ZDg5MjgwMTAwMDAwMDY0NDE5MWJmYTVhMjQ0MDFkYTk4OGFhYzQwNzcwMDU3NjJmZWJlODgzYzZiYTJiM2MzZTU5YmVlNzY4NDY1MzkyZmU0Yzg1YjU3NzI4MDhiZDg4MTBjN2Q2YWMyNTc3ZTYwYzdjMDYyMDdiNDRlMjVjZGYzYzlhN2EwYTkwZjdiYmEyYTQxMjEwMjRjZDc3NDFjOGU2MTYzMTQ1MDc5NDY1MjA3ODNhZGQyZTkwMDk1MDBiYWI5Y2JkMDg5M2EwNjE4MTcxZmJlYjVmZmZmZmZmZmJiMDI5YTlhMGJkOTEzZWU1MzgyMjQ5YWQ4MzcyZDVhNTZlOTliM2E4ZWExY2FhYTU5NjJlYmIxM2U5ZTQzODIwMTAwMDAwMDY0NDE1ZDc0MjEyODhhMTQ4ZjgyNzhmZmU2NjRjMGJiN2E3MzE4NWRmNWM3NTNlZjg1MTllNjU1YjBkNDc3M2FkNWM5YzcyODFhNWRhNzFhMmU4MzEwNzFhNzMzM2QyNmRiYjlkYmI5ZGYwZTcwMzM5ODI4OGUxNTM1NTE0Y2ZhOGZjNDQxMjEwMjRjZDc3NDFjOGU2MTYzMTQ1MDc5NDY1MjA3ODNhZGQyZTkwMDk1MDBiYWI5Y2JkMDg5M2EwNjE4MTcxZmJlYjVmZmZmZmZmZmNhMWNkODMzODM5OWFiZDM4NDllODNhNDFkNWE1ZmVlMGU5MTAzYTEzOTdhYTYzZTBmMjIzOGM0ZThlYzVkYmIwMDAwMDAwMDY0NDFlMzc4NzdhZDMyOTA3NDM3ZTYzZDFmOTk5NTk4YzYyMTY2NGQyYjFlZmI2YTE2YjI4MTVjNWRiNzA1MmUwYjJmYWU5OGRlNzYyOGViNDcwNDZhZDYzZGY4MmU2ZmZjZjIxNDA0ZGNlMjYxM2MxY2FkYzBhM2U4ZTU3YzQzMDBlOTQxMjEwMmRiYjg0NmFkYzcwMjU0ZmI3ZGE2YTIzMDc3NWFhYTg1MjU3ZTIzY2MzMGVjNTBiMzhmNDRmZTM1NjZlNTA0OTZmZmZmZmZmZjBmMTE4MjE3YWViMzhkMTUyNmFkYWQwNzhhZWE2N2VmODY3MjA5ODlmZWFlN2NlOWRiYjI3OTJhMDg1MjIxNmUwMTAwMDAwMDY0NDFkMjM3MTNlNjUyOGJkMTMyYmY2ZDQ3N2Q4YTYxYzE4M2NkN2IyNTUwMTdhZDVlZTgyYzkyOGExNDM5YjZjMGY1YmFmZTAwYWU1OWViMDgxNmI1Y2M5MTQ5MTVlMGJhYjI4YmE0ZmM4NzVmYjMyZDBmYmFkY2RkMjhhMDk2OTBiNjQxMjEwMzkyZmYwZGRjMDUxNTQzZGNkMTQzNmNhMzdkNTAwMzI3ZDFmZDU1ZDVmZWIxY2NmNDMxYjA1YTdiMDI0ZjFhYWJmZmZmZmZmZjAxM2UyZDIxZWRhOTQ5ZjE2NmE3ZTVkN2JmM2MzYmViYTExOTU0MDEzZTI5ZjQzMGNlNDcwNDRiMWYzZTdjODkwMzAwMDAwMDY0NDE3ZjU4ODZiMjExNjE0ZDU2N2E5ODMwNWE2YmM1M2E5MmYzNjZiNzdhM2FhNzdmMzViNzNjMmU4NWFkNjI1NGE3NjJmOTA0MGY2NDZkNTMzYTk1MmNhYWUzM2IyODZlNWM4ZmY0MDhlMzk2ZjJiM2JjODVjODhiNTVhOTIzOTRkNjQxMjEwMjNiOWMzNzBmYmYxMjEyZGE4MDBjOTMxYmE2ZGY0M2VkNjIwOGQxYjc3MGVlMDQ1MGNkNGEzMTA5ZTNjMDY5NjdmZmZmZmZmZmI2MzljYjkzMzUyNDY2MWFmZjg3NWI5M2ZhYTg5ZjNiYzUxYWNkMDQ2Y2FmNzIzNTExNWQ0ZWNkNjZjZmMxYWMwNTAwMDAwMDY0NDFkODU4OGU3Njg3YWQxNGMxNjUxMGQzNjU3MWFmYWU0ZDhmYTNlMjc2MWFmNDJkNmQ0YmM2MzkxNjkyYzIxN2UyMGMxYjdlNzFlZDdmNjA4ZDE0MWI1ZTJhYWU0MDEwZjFmNGVkODQ4YjRmNjNjMWNjZjBiYTNmM2U1YTNjMTBiYzQxMjEwMzMxNjAxNjZmMjdhNzU1NmY0ZGJlODAzMWFhMGRlNGQ4NTI0ZDgyYjYzMDU0YjZiMmRmNmJjZTc5MDhmMmZiNGRmZmZmZmZmZmY2Yjk2MDVkNDYwMjY1YWZhMzM2ZjZkZmY4MmRiYmIyMDI5Yjk4ZmU0YjZlMDdmZDJmOWJjYmNkMTBmOWE3ZWQwMDAwMDAwMDY0NDEyZWE2OTc1NzE1N2FlNTE5OWQwODgyZjA0YTUyYWRiMzY5ZjdlN2NiOTY1MTIyMDViNWJmY2UwYmNmODM3NGNjMGVjYWY3NTE0MzBhOWNjZDQyZWNlNjE0OTE5YmZmZTBlNjNjMTQ1YWExMWIyNzZhYmI1NDhiY2U3MzQxZDcyMTQxMjEwMmZlYWEzMTYwODkzZTM5MmQzYWFiYmY1ZTQyZjMyMmE3MzE4NmQ5MzUwMjczMDlmODEzMWE3YTQ5MDkyYWE0Y2VmZmZmZmZmZjI3YTUyOTBlYTVjYzdjYzhkOGYzYWIzYTQ4MGQwOTIyYzA4N2Q4NjMyYmI1Mzk3NjdkNjgyZTY1NzRlMjIzZTUwMjAwMDAwMDY0NDExM2FhOTAzNTc4YTAyNmZhMDdmY2ViNTFmNzFkN2M1NTg1ODAzMzFhMzFlZjQ2YWI1ZThjZTlmZDU3NmRhZTI2NzVjOGMwMGMwMThhMjUzZGI5N2Y2ZmIxMjkxM2Y2NThiMDlhNTZlMTJiOTJiODU2OTU0NjI1OTBmMTJhZDcyZDQxMjEwM2IxNWRiNjc2MWFlZWE4MTAzNDg5NmFkOTQxYjBlZGUxMGM4OGQ2ZTYwMGY3ZDI3MDYzMzVkNTEyOTRmNjM4OTVmZmZmZmZmZmI1MWZkMWIxNWE1NGRkNzIwNTdhM2FiNjI4NTIyMWZkM2YyZWZjZTgzODkyNjYxYjAwNDdjNDk0OWMzYTIxM2EwMTAwMDAwMDY0NDExN2IzY2UzOTljYTM4OGE2MzUyMWZjOTc2ZWQ0YzkwNzA3OWZhMTQ1ZWZlMGRmZGY4MTBiODdkMmI2YmMzOTU0MzhhMzg5MjQzNWQ1MDA5YTFlNmFmMzNhNjE2ZTIyYzE4MTdjZTdiYWFiODJiYzUxYTRkNGEzZjNiYjNiNDQxNzQxMjEwM2ViMWExNzlkMmI1NDkzZmUyMWUxMTAwMGViNzM5ZTk4YjBmYzg1NThhMWNjNDk2ZGRlNTg1NzAwMDc4MmRjYjlmZmZmZmZmZjM5Y2U2NjYwYWI5MjUyNWZhN2QxNjE3NmQ3ZTg4Y2ZhNDExMzljNDE1Mzk1NDE3YTJkY2Y2MjFkMzAyMDhiZjkwMjAwMDAwMDY0NDE2Njc1NTMzOTU5ZTM3MWE1ZmFlYWUzZDdiNGRhN2NmOWIyMDNhYThkNjJjZTgxNzRmYjEzZGUyNmExMjhhZGRiNmFlNDExYTgwMjA4OWQ0MzZiNzU5ZDEzN2QwZmU1OWM5MTk3ZGU5OTU0ZTYyMjMzYWU4MTFjOGQyNGExNjRlZDQxMjEwM2ViMWExNzlkMmI1NDkzZmUyMWUxMTAwMGViNzM5ZTk4YjBmYzg1NThhMWNjNDk2ZGRlNTg1NzAwMDc4MmRjYjlmZmZmZmZmZjA3MjkyNjhkYTc5YzA0OWNhMjNjMGYyMjQ2MmUxYjg5NzRiOGQ1ZWU0NzQ1Mzg5Yzg1NjJkM2JhMmRhZjRiYzcwMDAwMDAwMDY0NDE4ZmNlZWVkZGY5OWIzMjhlZGZiOTNmMzA3MjQzM2FkYTNmOGRmMTA1MWNkNGYxY2I3ZWFiZWRjMTEwNzYxNjdmMDg1Y2ZkM2Y0ODA3MTVjYTg3MmNhNjEyODMyZjBkNTQzNDVlMzM2YzAyM2I4ODI3NDU0MzNlN2EzNTViYzcwZTQxMjEwMjRkYWZlZDY1OTllZTQzODQ5MTg4NDU0YzZiNjFmYmFhYjNkZTdjMDcwYWEwZDJkZWVlNTBjNzE1ODhjN2RmY2RmZmZmZmZmZmNhNDQyN2E0YjllNDE5MWU1NjBhY2ExNzU2NGM0MzczYzkwNjU5OTdkZDllNzgwY2UzNjBhMzI0NzRjNjc3MjEwMTAwMDAwMDY0NDExNzQ2Njc1ZmFjYjI0NjFlYTU3MmQ3ZmZlYzNmOWIwZjgxMGIzMGNiMGRjMWFkY2I4ZTM5NjU0ZTA0MzJmZTJkOWE1ZjEyMDNjY2I5MjdjYTQwNzdiMjNhMGMwZmJiZDk5MTQ0NmNiYzBjM2VkMWRlNzBkMmE4MTI0Zjc3OWZkMjQxMjEwMmYzOGZjYThjYTFlYzZjNzBjYzNkZWI1YTI0MjBkNjRkZTlkMjczMDA5OWJlYzk3ZDdhZGMxMDdjMWFiNzY1YTZmZmZmZmZmZjQ4MTg3MjA2MzZlMGNiOTRlYzFkMTQxOTY0ODJhYzY4ZTIwNzI1YTEwYWFlZGJhYjEwZDFkZjYyOTkyZmY0MDAwMTAwMDAwMDY0NDEzZjJjZTlmNTVlYjJjZmQzZTY3NmVmNTkxNGViZTQ5NDFlZWY0NGFmNjZkOWRjNTNmZjE1YzEwZGY2NjcxMzU2YTQzZDQzNzI1MDFjMjAyZWM3YWVlMTE4ZDUxOGIzNGI2ZmVlMjg5NGZmMTg2Y2ZjMGMwZDU4MWE3Y2I0YTU1ODQxMjEwM2YwN2FjY2FiZTk4ZDYxZjNjODQxN2IzNTVlOTY0NDdhNDY5NDE2N2I4MjdkMzQzNjIzOWMxZmEyYmUwNTkyNzVmZmZmZmZmZmMxMzMxYjJjMDk0YzkwYjE2NzZmMThhNjUwNDdkZTM3ZTM2Y2YzMzY1YjI3NzI2YTYyZjE3ODI1OTUxMTQyM2QwMTAwMDAwMDY0NDE1ZTQ0NjRlMGYyZDUyZTIxZDhhOGVhMjI2ODA4NDM0ZWVjMWFlMzMyMjJkMDNjMGY0ZWFmYTRkM2IwYjZkNTUzZDM2NmRmZWJmN2Y1MTg3NjBlOTM5MzBkYWM2NTk5MDcxNWQxY2QxMWM4M2YzZDJiODI3ZDU2OGI4Mzc3MTgwYjQxMjEwMmMxYzlhYWJmZWM1MDg2YzAzNmMxNGFkZThlNGVkOGIxYjQ0MGIzYjk2MWQ2ZDk1NDg4Y2U3MGI3YzM4NWI3M2VmZmZmZmZmZjkzOTkzMDEzMmZhOGI5ZjU5OWFmNGIzZTQyZjgyMjAwYzI2MmEyZmUzYWUwMzFlZjZkYjMzZTc5NTIxY2M5ODkwMTAwMDAwMDY0NDFlMDAzZGQxNzk2NmI4NjJjZWZlMDY0NzAzMDg5MTA0MjFjMTg4ZTMwZmQ1MDBhY2MxODE3YzRmMTE4YzdmMWQ4NmZkZDFiMGM5NDI0ZTUyMGMzNmM0NzMxMDhhZDA2MzliYzI4MTFjY2EyMjAxMTVmZWJhNjA3Yjc1YWY2MTM3MzQxMjEwMzc5NTlkYjBlNmJmYmZlMTU2NjBmOGIzYWUxYTNmOWJmNTY0YzM3ZWMzNjc1MjIzY2IxNTQzNTM5MjIyZTIyN2JmZmZmZmZmZmM4NjkyN2EyZmVkZjBlYjE3MzgzYWU3M2VkMWQyZDIzODFlNzEwZGExOWZkMmViN2U3YjM5YjAzODI2OGYzMmYwMTAwMDAwMDY0NDEzMzU2ZTFjMjQzYzRlNTM2OTVkZDc0OGQxZGM0ODk3MWYzZGIzZDgwYjllZjFlYTE1NzNiNmM4MzBhNzc0MTNkMzUxNzQxZjNhZTUzMzgyZTdjYzNiMGMxOTQzZjIyOGExYzVhMjAyOTE5YmUxMjdjOGExMDA3ZTIwZGI3MzRlMTQxMjEwMzc5NTlkYjBlNmJmYmZlMTU2NjBmOGIzYWUxYTNmOWJmNTY0YzM3ZWMzNjc1MjIzY2IxNTQzNTM5MjIyZTIyN2JmZmZmZmZmZjI3MGJjZDBmOTljMTUyMjJkZjg1M2NlODJhZDYwODdhOTBmMzgxNzA5ZjYwZmZhMjdlNDM4OGFiOWRkYzM0MTgwMDAwMDAwMDY0NDFlZjdjODk1ZDM1YmIyYTU5YmQwYzNjNzJmMWYwMWFlNzg1ODViNzQ1NzEzYWI1YTQ1N2RkOGM2MmE3NGJhMzY4MzRkYzgzNWQ0ZjUwMTlkN2M4YzEwZmZjMDE2NDU0ZmI4ZjAzY2NkNzhhZjEyNjdiYjYxYTk0NTU1ZTE3ZTg3YTQxMjEwMzJlODJlNjAzOGZkOTQ1OGU0ZDdiNThkZmE2YmZlMTUxZDYyYWYxZTQ0NDBiOGNhOGIwYjNlZjlkMzg1NDlhZWVmZmZmZmZmZjAxNzM4MmM3MGIwMDAwMDAwMDE5NzZhOTE0ZDY3MjJiYWU4ZWQ0OTIwYWEyNmVlYzhkZGVmYWE5ZDczMTY1NWIzNjg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6Ijk2YWMxNmIyMTFjZTM4YmFkNDliZDg2NGE0YmUwODA0MzE4MDRiZmI4N2YxNWYyMWVkN2NmMjBiNzljOTY0NmUiLCJoYXNoIjoiOTZhYzE2YjIxMWNlMzhiYWQ0OWJkODY0YTRiZTA4MDQzMTgwNGJmYjg3ZjE1ZjIxZWQ3Y2YyMGI3OWM5NjQ2ZSIsInZlcnNpb24iOjIsInNpemUiOjI1NywibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImJhNGVkMzI0MzFmZTA4ZmE1MmNjYWVjZTdlMmYxYTAyM2U5YzE3ZGUwYzBlMDY5ZTY5YmNiZjQxNDllMWE2ZmYiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAzNGVkM2Y1NjA5ZWQ4ZDZlZTMwNzc0NWEyYjE2MTFkMTk1NWNhOGI2ZjFkNGVkYWU1MjU1OTE1YmJjZDdhYjA5MDIyMDc3NDBhMzY2MDRiYTcxOTZmMzEyYTdiYzQxOGJkYmIzZDM3MTkwY2NmYTY5NzI0YjUyYjk5NTA3ODVmZjVmODFbQUxMfEZPUktJRF0gMDQ3MDU4NGU4N2YzMTQ5MjRmMDBjNWNhNzhkZDBkNzE0N2EzZDEyNTEzNWYwYTlmODllYWY1NjE1ZmNkMTg5OWYwY2VjNDM2NzkwZTA4YzM5NDdiMDdiMzYwMzliYjM2ZTZlMTI4M2UzOTQ1OTI0MmQ0ODI0OWM0ZTVmYmM2ZGU3ZSIsImhleCI6IjQ3MzA0NDAyMjAzNGVkM2Y1NjA5ZWQ4ZDZlZTMwNzc0NWEyYjE2MTFkMTk1NWNhOGI2ZjFkNGVkYWU1MjU1OTE1YmJjZDdhYjA5MDIyMDc3NDBhMzY2MDRiYTcxOTZmMzEyYTdiYzQxOGJkYmIzZDM3MTkwY2NmYTY5NzI0YjUyYjk5NTA3ODVmZjVmODE0MTQxMDQ3MDU4NGU4N2YzMTQ5MjRmMDBjNWNhNzhkZDBkNzE0N2EzZDEyNTEzNWYwYTlmODllYWY1NjE1ZmNkMTg5OWYwY2VjNDM2NzkwZTA4YzM5NDdiMDdiMzYwMzliYjM2ZTZlMTI4M2UzOTQ1OTI0MmQ0ODI0OWM0ZTVmYmM2ZGU3ZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMzU5MTc1MiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDM5MDIwZjVlN2RhZTliZGVlMTE1MWZiNzBlYWQzNTZiOTAwYzczNGEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQzOTAyMGY1ZTdkYWU5YmRlZTExNTFmYjcwZWFkMzU2YjkwMGM3MzRhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxdXN5cjY3MGtoZmhoaHB6NTBtd3I0ZHg0NGVxcnJuZmdhbTU2dWZ1OCJdfX0seyJ2YWx1ZSI6MTcuMzg4MTYwMTgsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3NzExZWFiNmZmY2M5OWU0Mzc2YWMzOWY0MDIyZGU0MTNlMWVhMmE5IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NzcxMWVhYjZmZmNjOTllNDM3NmFjMzlmNDAyMmRlNDEzZTFlYTJhOTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcG0zcjY0a2xseGZuZXBoZHRwZTdzcHptZXFudTg0ejR5ejkyN3FjY2siXX19XSwiZmVlIjowLjAwMDEwMDAxLCJoZXgiOiIwMjAwMDAwMDAxZmZhNmUxNDk0MWJmYmM2OTllMDYwZTBjZGUxNzljM2UwMjFhMmY3ZWNlYWVjYzUyZmEwOGZlMzEyNGQzNGViYTAxMDAwMDAwOGE0NzMwNDQwMjIwMzRlZDNmNTYwOWVkOGQ2ZWUzMDc3NDVhMmIxNjExZDE5NTVjYThiNmYxZDRlZGFlNTI1NTkxNWJiY2Q3YWIwOTAyMjA3NzQwYTM2NjA0YmE3MTk2ZjMxMmE3YmM0MThiZGJiM2QzNzE5MGNjZmE2OTcyNGI1MmI5OTUwNzg1ZmY1ZjgxNDE0MTA0NzA1ODRlODdmMzE0OTI0ZjAwYzVjYTc4ZGQwZDcxNDdhM2QxMjUxMzVmMGE5Zjg5ZWFmNTYxNWZjZDE4OTlmMGNlYzQzNjc5MGUwOGMzOTQ3YjA3YjM2MDM5YmIzNmU2ZTEyODNlMzk0NTkyNDJkNDgyNDljNGU1ZmJjNmRlN2VmZmZmZmZmZjAyNDhjZTM2MDAwMDAwMDAwMDE5NzZhOTE0MzkwMjBmNWU3ZGFlOWJkZWUxMTUxZmI3MGVhZDM1NmI5MDBjNzM0YTg4YWMxMjNhYTQ2NzAwMDAwMDAwMTk3NmE5MTQ3NzExZWFiNmZmY2M5OWU0Mzc2YWMzOWY0MDIyZGU0MTNlMWVhMmE5ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiOTk2YzA4YmRlNWVjNGFiMDM1MzY5YzRlYTNiN2U5OTUzZjU5MGU0ZTgyY2M2MWMwZThjYTE0NjhjYzAzOTQyNCIsImhhc2giOiI5OTZjMDhiZGU1ZWM0YWIwMzUzNjljNGVhM2I3ZTk5NTNmNTkwZTRlODJjYzYxYzBlOGNhMTQ2OGNjMDM5NDI0IiwidmVyc2lvbiI6Miwic2l6ZSI6MzcyLCJsb2NrdGltZSI6OTQwODgzLCJ2aW4iOlt7InR4aWQiOiI5N2Y4NWE1MTk2NjAzMDI4MjAzMGE2MTdiOTNhOWRjYjYyODU5YmE1YmRiYTkyN2ExY2FjYTVjZTRiZTA5MWVkIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwMmY4YzM2OWE1OGY4ZTZkMTVhMDU4Mjc1NDNhODg3NGMwM2UzMTcxNzBhZDMyYTUxZjBiMDY0NTUwZTdmYjNjZDAyMjA2ZGJhODcwYTRhZWNlMmNlY2U1NWU0YTU4NWU3NjUwMzYxOGNiNzcxNjE0ODU0NTUzYTY1YjgxMDRiNTdhYzNiW0FMTHxGT1JLSURdIDAzNzExMWNkMjNkMGEwNDQwZTc5ZDUwZDNlNGJjNTA0MjM0MGY0MGUyN2M0OTZlNDEzNTU1NDIyYTQyMjA4OTBmZCIsImhleCI6IjQ3MzA0NDAyMjAyZjhjMzY5YTU4ZjhlNmQxNWEwNTgyNzU0M2E4ODc0YzAzZTMxNzE3MGFkMzJhNTFmMGIwNjQ1NTBlN2ZiM2NkMDIyMDZkYmE4NzBhNGFlY2UyY2VjZTU1ZTRhNTg1ZTc2NTAzNjE4Y2I3NzE2MTQ4NTQ1NTNhNjViODEwNGI1N2FjM2I0MTIxMDM3MTExY2QyM2QwYTA0NDBlNzlkNTBkM2U0YmM1MDQyMzQwZjQwZTI3YzQ5NmU0MTM1NTU0MjJhNDIyMDg5MGZkIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk0fSx7InR4aWQiOiJiMzA4MDE2YWExMzIwMDMyNTFjYjRlNDA0OGI4ZWZmYTc1OTQ0NTQ0NTQ3YmU5NTI1YzQ1NzA2MGY4OGI5NWFiIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwMjI3ZjEyNWVkZTc1NzAxZDU4MzllZTUzZjk0ZWU4MWY0ZGQ4ZDVlNDY0MDRhNTk1Nzk2ZWNkNmRlMWVhMDU4ODAyMjAzZTIzNDZiOTlkNzEzMWIzMzNhYjZmZGNiODA2ZTBkYTM2NWZiMDA5ZDNlYjcwMGIwNzg5Y2IwZmUxY2U3MzczW0FMTHxGT1JLSURdIDAyMzM4MTE2ODdhOWIwMGFhZTVjYWZjZmEyODUwNTg3M2I3ZTA4YTQ0YzE2MGQ4MzA1YzM4ZjQ1NmQ5MDcxNWVmZCIsImhleCI6IjQ3MzA0NDAyMjAyMjdmMTI1ZWRlNzU3MDFkNTgzOWVlNTNmOTRlZTgxZjRkZDhkNWU0NjQwNGE1OTU3OTZlY2Q2ZGUxZWEwNTg4MDIyMDNlMjM0NmI5OWQ3MTMxYjMzM2FiNmZkY2I4MDZlMGRhMzY1ZmIwMDlkM2ViNzAwYjA3ODljYjBmZTFjZTczNzM0MTIxMDIzMzgxMTY4N2E5YjAwYWFlNWNhZmNmYTI4NTA1ODczYjdlMDhhNDRjMTYwZDgzMDVjMzhmNDU2ZDkwNzE1ZWZkIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk0fV0sInZvdXQiOlt7InZhbHVlIjowLjE3MzQxNDA4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjY3OGUyMDdmYWRhYjUyMTI5Y2M1YWQ0YzJjNjY2NDFiY2VmMjQ3NSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDI2NzhlMjA3ZmFkYWI1MjEyOWNjNWFkNGMyYzY2NjQxYmNlZjI0NzU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFuODNjczhsdGR0MmdmZmUzZGRmc2t4dmVxbWVtZXl3NXk3cGR3MGhoIl19fSx7InZhbHVlIjowLjIwNDA2MzMsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA4ZDlmY2Q5YWI3ODg2MjY0MWVmOTE3NmJhZDBjZTEwMzZjZmUwOGY4IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OGQ5ZmNkOWFiNzg4NjI2NDFlZjkxNzZiYWQwY2UxMDM2Y2ZlMDhmODg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenhlbG52Nms3eXh5ZXE3bHl0a2h0Z3Z1eXBrZWxzZ2xxM2c2bDZ3eGwiXX19XSwiZmVlIjozLjcyZS0wNiwiaGV4IjoiMDIwMDAwMDAwMmVkOTFlMDRiY2VhNWFjMWM3YTkyYmFiZGE1OWI4NTYyY2I5ZDNhYjkxN2E2MzAyMDI4MzA2MDk2NTE1YWY4OTcwMDAwMDAwMDZhNDczMDQ0MDIyMDJmOGMzNjlhNThmOGU2ZDE1YTA1ODI3NTQzYTg4NzRjMDNlMzE3MTcwYWQzMmE1MWYwYjA2NDU1MGU3ZmIzY2QwMjIwNmRiYTg3MGE0YWVjZTJjZWNlNTVlNGE1ODVlNzY1MDM2MThjYjc3MTYxNDg1NDU1M2E2NWI4MTA0YjU3YWMzYjQxMjEwMzcxMTFjZDIzZDBhMDQ0MGU3OWQ1MGQzZTRiYzUwNDIzNDBmNDBlMjdjNDk2ZTQxMzU1NTQyMmE0MjIwODkwZmRmZWZmZmZmZmFiOTU4YmY4NjA3MDQ1NWM1MmU5N2I1NDQ0NDU5NDc1ZmFlZmI4NDg0MDRlY2I1MTMyMDAzMmExNmEwMTA4YjMwMTAwMDAwMDZhNDczMDQ0MDIyMDIyN2YxMjVlZGU3NTcwMWQ1ODM5ZWU1M2Y5NGVlODFmNGRkOGQ1ZTQ2NDA0YTU5NTc5NmVjZDZkZTFlYTA1ODgwMjIwM2UyMzQ2Yjk5ZDcxMzFiMzMzYWI2ZmRjYjgwNmUwZGEzNjVmYjAwOWQzZWI3MDBiMDc4OWNiMGZlMWNlNzM3MzQxMjEwMjMzODExNjg3YTliMDBhYWU1Y2FmY2ZhMjg1MDU4NzNiN2UwOGE0NGMxNjBkODMwNWMzOGY0NTZkOTA3MTVlZmRmZWZmZmZmZjAyZTA5YjA4MDEwMDAwMDAwMDE5NzZhOTE0MjY3OGUyMDdmYWRhYjUyMTI5Y2M1YWQ0YzJjNjY2NDFiY2VmMjQ3NTg4YWMzYTYwMzcwMTAwMDAwMDAwMTk3NmE5MTQ4ZDlmY2Q5YWI3ODg2MjY0MWVmOTE3NmJhZDBjZTEwMzZjZmUwOGY4ODhhYzUzNWIwZTAwIn0seyJ0eGlkIjoiOTlkMmM5MDQ2NDU4OWFiNmU1OWYwZTBmYTAxMTdjMmMyYjM4OGYwZmYxZWVjNTNkN2NkNDJhM2I4YThjMzVmMiIsImhhc2giOiI5OWQyYzkwNDY0NTg5YWI2ZTU5ZjBlMGZhMDExN2MyYzJiMzg4ZjBmZjFlZWM1M2Q3Y2Q0MmEzYjhhOGMzNWYyIiwidmVyc2lvbiI6Miwic2l6ZSI6MTY0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiYmQxZjUxNGQ2M2E1YzRlMTM3ZTJlNjJmZTVlMDg3YzI4NTczMDgzODBiYzAxM2M5MDgwM2VkOGFiZDgyMmFmYiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIiwiaGV4IjoiNDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIn0sInNlcXVlbmNlIjoxfV0sInZvdXQiOlt7InZhbHVlIjowLjAxMTI3Nzg0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XSwiZmVlIjoxLjY0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMWZiMmE4MmJkOGFlZDAzMDhjOTEzYzAwYjM4MDg3Mzg1YzI4N2UwZTUyZmU2ZTIzN2UxYzRhNTYzNGQ1MTFmYmQwMDAwMDAwMDQ1NDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxMDEwMDAwMDAwMTY4MzUxMTAwMDAwMDAwMDAyM2FhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODcwMDAwMDAwMCJ9LHsidHhpZCI6IjliOGFhOTlkZGRkOTAzMmRlMDE2NjcxOTQ5MGJjOGZkY2Y1YWZlMDY5OGVjYWIzNTg3NjZmYTUwMWQzNjE1MzIiLCJoYXNoIjoiOWI4YWE5OWRkZGQ5MDMyZGUwMTY2NzE5NDkwYmM4ZmRjZjVhZmUwNjk4ZWNhYjM1ODc2NmZhNTAxZDM2MTUzMiIsInZlcnNpb24iOjIsInNpemUiOjI5MywibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImY1N2UwMjBmNWQ2ZTM1NmE1OGM1MGMyYTk4OTA2MzBhNjIzZDk0ZWE5MDA0YjcxM2VkYzM1MmFmNDI4ZmY4MDgiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA3YzZmMWU1ZTVmYzA5MDQ2OGVlYjE0NzU4OGU4M2NiM2U4Y2NkNTNmNGQxYTBkZTY0ZDA0NmIzOTY5ZDkzODVhMDIyMDMzMmU1YTdjYzQwYTBmODI5NmU2OWM3OTI4NjA0NDRlZDBkMzRlMDJmODQwMGQ5NDEzNDA2NjVjZGY2MzUzYjhbQUxMfEZPUktJRF0gMDIyYTkxZjgxMjI2OWU3OWFjOTY4ZmJlNjk3MTY3YWFlZDVhOTQ3ZTI0MWVhY2Q1YTVmYTQ0YTU3YTI0YmU1MDY5IiwiaGV4IjoiNDczMDQ0MDIyMDdjNmYxZTVlNWZjMDkwNDY4ZWViMTQ3NTg4ZTgzY2IzZThjY2Q1M2Y0ZDFhMGRlNjRkMDQ2YjM5NjlkOTM4NWEwMjIwMzMyZTVhN2NjNDBhMGY4Mjk2ZTY5Yzc5Mjg2MDQ0NGVkMGQzNGUwMmY4NDAwZDk0MTM0MDY2NWNkZjYzNTNiODQxMjEwMjJhOTFmODEyMjY5ZTc5YWM5NjhmYmU2OTcxNjdhYWVkNWE5NDdlMjQxZWFjZDVhNWZhNDRhNTdhMjRiZTUwNjkifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMDA4MDc5NTgsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAzM2RhMGZjYjdiMjIwZmRiMTkwMDYwN2RhNjE4YTg2N2Q4MDUzZWNiIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MzNkYTBmY2I3YjIyMGZkYjE5MDA2MDdkYTYxOGE4NjdkODA1M2VjYjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWVhNXI3dDB2M3Fsa2NlcXBzOG1mc2M0cG5hc3BmN2V2cmVlN2Z0cnIiXX19LHsidmFsdWUiOjAuMTI2Njk4MTIsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhNmU1ZGNjZmM2MjljZGE2MjMwNjI0YzlmODM5ZmNmYTNjMmE0YTJjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YTZlNWRjY2ZjNjI5Y2RhNjIzMDYyNGM5ZjgzOWZjZmEzYzJhNGEyYzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxem53dGh4MGNjNXVtZjNycWNqdm43cGVsbmFyYzJqMjlzZzU5cXduMDgiXX19LHsidmFsdWUiOjAuMjQxNjM3NTUsIm4iOjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAzOTJlODA2ZTRkYzUzZDFhM2VjODYwYTQyMjE2NjVjOWRiODZhYzI4IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MzkyZTgwNmU0ZGM1M2QxYTNlYzg2MGE0MjIxNjY1YzlkYjg2YWMyODg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXVqYXFyd2Zoem42eDM3ZXBzMmdnc2t2aHlhaHA0djlxaHh6dW03dmYiXX19LHsidmFsdWUiOjYxOS4zMDg5NjI2NiwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGQzZWViNWIyMTA4ZWIwMDBmZWY5NDA4NGFmNWQzZDA2N2YyZThiOWMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkM2VlYjViMjEwOGViMDAwZmVmOTQwODRhZjVkM2QwNjdmMmU4YjljODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyZjdhZGRqeno4dHFxODdsOXFnZnQ2YTg1cjg3dDV0bnN3NjRjM3M2MiJdfX1dLCJmZWUiOjEuNjkyZS0wNSwiaGV4IjoiMDIwMDAwMDAwMTA4Zjg4ZjQyYWY1MmMzZWQxM2I3MDQ5MGVhOTQzZDYyMGE2MzkwOTgyYTBjYzU1ODZhMzU2ZTVkMGYwMjdlZjUwMzAwMDAwMDZhNDczMDQ0MDIyMDdjNmYxZTVlNWZjMDkwNDY4ZWViMTQ3NTg4ZTgzY2IzZThjY2Q1M2Y0ZDFhMGRlNjRkMDQ2YjM5NjlkOTM4NWEwMjIwMzMyZTVhN2NjNDBhMGY4Mjk2ZTY5Yzc5Mjg2MDQ0NGVkMGQzNGUwMmY4NDAwZDk0MTM0MDY2NWNkZjYzNTNiODQxMjEwMjJhOTFmODEyMjY5ZTc5YWM5NjhmYmU2OTcxNjdhYWVkNWE5NDdlMjQxZWFjZDVhNWZhNDRhNTdhMjRiZTUwNjlmZmZmZmZmZjA0MTY1NDBjMDAwMDAwMDAwMDE5NzZhOTE0MzNkYTBmY2I3YjIyMGZkYjE5MDA2MDdkYTYxOGE4NjdkODA1M2VjYjg4YWM3NDUzYzEwMDAwMDAwMDAwMTk3NmE5MTRhNmU1ZGNjZmM2MjljZGE2MjMwNjI0YzlmODM5ZmNmYTNjMmE0YTJjODhhY2FiYjU3MDAxMDAwMDAwMDAxOTc2YTkxNDM5MmU4MDZlNGRjNTNkMWEzZWM4NjBhNDIyMTY2NWM5ZGI4NmFjMjg4OGFjOGE3YjVlNmIwZTAwMDAwMDE5NzZhOTE0ZDNlZWI1YjIxMDhlYjAwMGZlZjk0MDg0YWY1ZDNkMDY3ZjJlOGI5Yzg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6IjlkMmViMWQ3YmVhM2RhNDI0NWM4YjQ3Mzk3NGFlNDAwNGEyYjU3YzJlMDYwZTU0M2EyZDdmNDE0OGZiYzUwM2YiLCJoYXNoIjoiOWQyZWIxZDdiZWEzZGE0MjQ1YzhiNDczOTc0YWU0MDA0YTJiNTdjMmUwNjBlNTQzYTJkN2Y0MTQ4ZmJjNTAzZiIsInZlcnNpb24iOjEsInNpemUiOjIyNSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjczNWI1ZTlhYjgyZGI1ZmQ2NjVjYmQ3ZjAxMTA1ZDhkNzkxMDc2NWI1NzA3M2E0MGJhM2ZmYmZhMjU1MWRkZTQiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA1OWJlODhjYWEzY2RmNjEwOWE0ODgwZmMzYTI5NjMzOTMxZDdhNmVlMTQ3MWFkNjAyM2ZhZTA2OGU0ZWM5MDNmMDIyMDY4OTcwZjQ3MTk2NWVjNzQyMzk4MDQ5NzUyZmNlODJlNTczZDQ5Yzg2NzFmOGE1MDdlYWZhNmM2NjFjOTUyYTBbQUxMfEZPUktJRF0gMDI4Y2NiMzNkMDVjNzBjM2UwOWM4NGM4MTA4NDE0NDdiZDBmOGU0ODU1ZDcxMmUwOTFmZmE2NWM2ZGE2YmI3ZmNjIiwiaGV4IjoiNDczMDQ0MDIyMDU5YmU4OGNhYTNjZGY2MTA5YTQ4ODBmYzNhMjk2MzM5MzFkN2E2ZWUxNDcxYWQ2MDIzZmFlMDY4ZTRlYzkwM2YwMjIwNjg5NzBmNDcxOTY1ZWM3NDIzOTgwNDk3NTJmY2U4MmU1NzNkNDljODY3MWY4YTUwN2VhZmE2YzY2MWM5NTJhMDQxMjEwMjhjY2IzM2QwNWM3MGMzZTA5Yzg0YzgxMDg0MTQ0N2JkMGY4ZTQ4NTVkNzEyZTA5MWZmYTY1YzZkYTZiYjdmY2MifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuNjUwMjQzMjcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAyYzVmYWViM2QzNjZlYjk0M2M5YTk1YjFiZDc5Zjk3ZDIyMmUyYzI3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MmM1ZmFlYjNkMzY2ZWI5NDNjOWE5NWIxYmQ3OWY5N2QyMjJlMmMyNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWs5bHQ0bjZkbndoOXB1bjIybXIwdGVsOTdqeXQzdnl1MHJ4Y2pnMjciXX19LHsidmFsdWUiOjEuNTY4OTE4OTksIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAyZDJmZTk2YzJjYWZkNWRlNzk0NjhiZTAyZTc0NWNiMTlhMDQwMTc3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MmQyZmU5NmMyY2FmZDVkZTc5NDY4YmUwMmU3NDVjYjE5YTA0MDE3Nzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWtqbDZ0djlqaGF0aG5lZzY5N3F0bjV0amNlNXBxcHd1c3FlYWpoc3YiXX19XSwiZmVlIjoyLjI2ZS0wNiwiaGV4IjoiMDEwMDAwMDAwMWU0ZGQ1MTI1ZmFmYjNmYmE0MDNhMDc1NzViNzYxMDc5OGQ1ZDEwMDE3ZmJkNWM2NmZkYjUyZGI4OWE1ZTViNzMwMDAwMDAwMDZhNDczMDQ0MDIyMDU5YmU4OGNhYTNjZGY2MTA5YTQ4ODBmYzNhMjk2MzM5MzFkN2E2ZWUxNDcxYWQ2MDIzZmFlMDY4ZTRlYzkwM2YwMjIwNjg5NzBmNDcxOTY1ZWM3NDIzOTgwNDk3NTJmY2U4MmU1NzNkNDljODY3MWY4YTUwN2VhZmE2YzY2MWM5NTJhMDQxMjEwMjhjY2IzM2QwNWM3MGMzZTA5Yzg0YzgxMDg0MTQ0N2JkMGY4ZTQ4NTVkNzEyZTA5MWZmYTY1YzZkYTZiYjdmY2NmZmZmZmZmZjAyNDczMWUwMDMwMDAwMDAwMDE5NzZhOTE0MmM1ZmFlYjNkMzY2ZWI5NDNjOWE5NWIxYmQ3OWY5N2QyMjJlMmMyNzg4YWNmYmZhNTkwOTAwMDAwMDAwMTk3NmE5MTQyZDJmZTk2YzJjYWZkNWRlNzk0NjhiZTAyZTc0NWNiMTlhMDQwMTc3ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiOWViY2I0NDZlYjRhNDYyODRiNzVlMWYyMjYxMTM5ODUwZDAxZDUyMjI5ODAyYTMyMmU4ZDYxM2NkZDJmOTY5YyIsImhhc2giOiI5ZWJjYjQ0NmViNGE0NjI4NGI3NWUxZjIyNjExMzk4NTBkMDFkNTIyMjk4MDJhMzIyZThkNjEzY2RkMmY5NjljIiwidmVyc2lvbiI6MSwic2l6ZSI6MzczLCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiMjg4MmEyNTdhMjhhNzg0MWU4OTZlZjZiYTM3YjlmM2Y0NjA2MzI0ODk0MjU4YzgwZjg5MGU1YjIxNmZhZGU2NCIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDU1MzczYjM5NzJhMzNjYjA4OTc3NzRjMTYzYWNkMmIyMmZhOTkwNDRkNmUwMjllNjg3ZWY0Yjc5NDc5ZjlhMzkwMjIwMTRkMzdiNDQzMjU4Njk0OTA3OTYxYzQzMDU4ODgwMzgxMWNlYzA1YjA4MGI2YWVhNTU0YzY5NDM4NzZlMWI5MltBTEx8Rk9SS0lEXSAwMmIyMjlmNDViNGMzODY5MmUzYjE3YjFhMjgwZWYyZDgzZDRhNWU4ZjZkMzMyODM1MzdiNDViZjhjMjExMGY4ZjMiLCJoZXgiOiI0NzMwNDQwMjIwNTUzNzNiMzk3MmEzM2NiMDg5Nzc3NGMxNjNhY2QyYjIyZmE5OTA0NGQ2ZTAyOWU2ODdlZjRiNzk0NzlmOWEzOTAyMjAxNGQzN2I0NDMyNTg2OTQ5MDc5NjFjNDMwNTg4ODAzODExY2VjMDViMDgwYjZhZWE1NTRjNjk0Mzg3NmUxYjkyNDEyMTAyYjIyOWY0NWI0YzM4NjkyZTNiMTdiMWEyODBlZjJkODNkNGE1ZThmNmQzMzI4MzUzN2I0NWJmOGMyMTEwZjhmMyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMjAwZmRjYTU2ZTlkOTQ1OTYzYWVhYzAyZmJiNGYzNDIxZmY4NmVhZWFmNjdkZjVjNDAxNzI5Y2UxOWE4NDNlZCIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwZmU2OGE3OGYxNjQxYjdhYmY0MGRjYWNmNmRiZjNjNTFkMWQ1Y2FmNjEwY2NhYjgyYTEzYmY5Nzc5NmYxYmIyOTAyMjAyMzNjNzVmYjM3ZWU5MTdlN2Y4ZmI1MDhhOGM3MGE5NzYyNWUyMTRmZmExZGM0ZDY0NWQ0ODQyYmFlMWU2MzIwW0FMTHxGT1JLSURdIDAzMTQ5N2VjMGQyYjYxMzNhMDc4OTQzODAwMDc1ZGY1YjBiYTY0OTE0OGEyOWUyYzYwNGQ0MTMwMDc1MGM0MDkwMSIsImhleCI6IjQ4MzA0NTAyMjEwMGZlNjhhNzhmMTY0MWI3YWJmNDBkY2FjZjZkYmYzYzUxZDFkNWNhZjYxMGNjYWI4MmExM2JmOTc3OTZmMWJiMjkwMjIwMjMzYzc1ZmIzN2VlOTE3ZTdmOGZiNTA4YThjNzBhOTc2MjVlMjE0ZmZhMWRjNGQ2NDVkNDg0MmJhZTFlNjMyMDQxMjEwMzE0OTdlYzBkMmI2MTMzYTA3ODk0MzgwMDA3NWRmNWIwYmE2NDkxNDhhMjllMmM2MDRkNDEzMDA3NTBjNDA5MDEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjEuMzQ5Mjk0NCwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGU4MjUxZmI2ZWUzMzdlZTc2NmNkNzVhNWY0YTJhZWJiNWJiNGUyMTggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRlODI1MWZiNmVlMzM3ZWU3NjZjZDc1YTVmNGEyYWViYjViYjRlMjE4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyNXoyOGFrYWNlaGFlbXhlNDY2dGE5ejQ2YTRoZDh6cnF6Mm1weGtsdSJdfX0seyJ2YWx1ZSI6MC4xNTExNTMzNiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGQ1MDk3ODFjZjlmMzZlMzJkMTRiNTVhODg1NzY1OTgxZjc5Y2FjMmIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkNTA5NzgxY2Y5ZjM2ZTMyZDE0YjU1YTg4NTc2NTk4MWY3OWNhYzJiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyMnNqN3F1bDhla3V2azNmZDI2M3B0a3R4cWwwODl2OXZjMGxkMG52cCJdfX1dLCJmZWUiOjMuNzRlLTA2LCJoZXgiOiIwMTAwMDAwMDAyNjRkZWZhMTZiMmU1OTBmODgwOGMyNTk0NDgzMjA2NDYzZjlmN2JhMzZiZWY5NmU4NDE3ODhhYTI1N2EyODIyODAwMDAwMDAwNmE0NzMwNDQwMjIwNTUzNzNiMzk3MmEzM2NiMDg5Nzc3NGMxNjNhY2QyYjIyZmE5OTA0NGQ2ZTAyOWU2ODdlZjRiNzk0NzlmOWEzOTAyMjAxNGQzN2I0NDMyNTg2OTQ5MDc5NjFjNDMwNTg4ODAzODExY2VjMDViMDgwYjZhZWE1NTRjNjk0Mzg3NmUxYjkyNDEyMTAyYjIyOWY0NWI0YzM4NjkyZTNiMTdiMWEyODBlZjJkODNkNGE1ZThmNmQzMzI4MzUzN2I0NWJmOGMyMTEwZjhmM2ZmZmZmZmZmZWQ0M2E4MTljZTI5MTc0MDVjZGY2N2FmYWU2ZWY4MWY0MmYzYjRmYjAyYWNhZTYzNTk5NDlkNmVhNWRjMGYyMDAwMDAwMDAwNmI0ODMwNDUwMjIxMDBmZTY4YTc4ZjE2NDFiN2FiZjQwZGNhY2Y2ZGJmM2M1MWQxZDVjYWY2MTBjY2FiODJhMTNiZjk3Nzk2ZjFiYjI5MDIyMDIzM2M3NWZiMzdlZTkxN2U3ZjhmYjUwOGE4YzcwYTk3NjI1ZTIxNGZmYTFkYzRkNjQ1ZDQ4NDJiYWUxZTYzMjA0MTIxMDMxNDk3ZWMwZDJiNjEzM2EwNzg5NDM4MDAwNzVkZjViMGJhNjQ5MTQ4YTI5ZTJjNjA0ZDQxMzAwNzUwYzQwOTAxZmZmZmZmZmYwMjIwZGMwYTA4MDAwMDAwMDAxOTc2YTkxNGU4MjUxZmI2ZWUzMzdlZTc2NmNkNzVhNWY0YTJhZWJiNWJiNGUyMTg4OGFjNDhhNGU2MDAwMDAwMDAwMDE5NzZhOTE0ZDUwOTc4MWNmOWYzNmUzMmQxNGI1NWE4ODU3NjU5ODFmNzljYWMyYjg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6ImEyNDc2MjlhMTViY2VjOGIwMzlkYzQ0YzExOTcxOGM3NDI3YzI5NDRkMmI4OWY5OTA1NjFiYjc2YWFmMDUzZWEiLCJoYXNoIjoiYTI0NzYyOWExNWJjZWM4YjAzOWRjNDRjMTE5NzE4Yzc0MjdjMjk0NGQyYjg5Zjk5MDU2MWJiNzZhYWYwNTNlYSIsInZlcnNpb24iOjEsInNpemUiOjM3MiwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjI0ODFjMjgxMDEzMzI5NmFjYmU1NDRlNTNiMDJlZjBiMjVlMWM0YjI1NzczMzA3NzUxOTVhODljMGUwM2FhNjIiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ0MDIyMDA0MzBiMDg2MTJiNTg0NjYxY2UxMzQ1ZDU5OTdlM2Q5OTQzZmFlNTQ3OTcwZmI1ODZhNjNlMGY5YWZiMWQxNmIwMjIwMTA5YzdhYzA2MTk2ZjFmOTU1YzZjNTg1YTE0MWNkNzZlN2EwOGY4NDE2OTZlY2E1YmY5ZDA5OTQ2ZDcwMjVjYltBTEx8Rk9SS0lEXSAzMDQ1MDIyMTAwYzY0NGYwMWQ5MGJkYzQxYWVhZjY3ZTJlMWJmMzVmMTUwMmIzNDE2ZjNhMjUwYzAyZTkwOWIwNTgxNTM3NDgyZDAyMjA3YzczZTNmMzA2MThlZjJjZDJjM2FkOGI5ZGMxM2RiZThiOTJhYTNjOGM0ZDAxM2QzN2ZmZjJjODcyNmM0YjhjW0FMTHxGT1JLSURdIDUyMjEwM2ZmMTQ1MDI4M2YwODU2OGFjZGI0ZDVmNTY5ZjMyZTRjZDRkOGMxOTYwZWEwNDlhMjA1NDM2ZjY5Zjk5MTZkZjgyMTAyMzBlZTZhZWM2NWJjMGRiN2U5Y2Y1MDdiMzMwNjdmNjgxMDQ3ZTE4MGU5MTkwNmUxZmRlMWJiNTQ5ZjIzM2IyNDIxMDM2NjA1ODQ4MmVjY2NiNDcwNzViZTlkMWQzZWRiNDZkZjMzMWMwNGZhNTEyNmNkNmZkOWRjNmNlZTA3MTIzN2I0NTNhZSIsImhleCI6IjAwNDczMDQ0MDIyMDA0MzBiMDg2MTJiNTg0NjYxY2UxMzQ1ZDU5OTdlM2Q5OTQzZmFlNTQ3OTcwZmI1ODZhNjNlMGY5YWZiMWQxNmIwMjIwMTA5YzdhYzA2MTk2ZjFmOTU1YzZjNTg1YTE0MWNkNzZlN2EwOGY4NDE2OTZlY2E1YmY5ZDA5OTQ2ZDcwMjVjYjQxNDgzMDQ1MDIyMTAwYzY0NGYwMWQ5MGJkYzQxYWVhZjY3ZTJlMWJmMzVmMTUwMmIzNDE2ZjNhMjUwYzAyZTkwOWIwNTgxNTM3NDgyZDAyMjA3YzczZTNmMzA2MThlZjJjZDJjM2FkOGI5ZGMxM2RiZThiOTJhYTNjOGM0ZDAxM2QzN2ZmZjJjODcyNmM0YjhjNDE0YzY5NTIyMTAzZmYxNDUwMjgzZjA4NTY4YWNkYjRkNWY1NjlmMzJlNGNkNGQ4YzE5NjBlYTA0OWEyMDU0MzZmNjlmOTkxNmRmODIxMDIzMGVlNmFlYzY1YmMwZGI3ZTljZjUwN2IzMzA2N2Y2ODEwNDdlMTgwZTkxOTA2ZTFmZGUxYmI1NDlmMjMzYjI0MjEwMzY2MDU4NDgyZWNjY2I0NzA3NWJlOWQxZDNlZGI0NmRmMzMxYzA0ZmE1MTI2Y2Q2ZmQ5ZGM2Y2VlMDcxMjM3YjQ1M2FlIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjo1LjI1NzgsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBlOWZjNWQ0MjZjN2M5MTIzNjAyMmQ5OTdlZDI1M2IwMGE4YTA2MGE3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZTlmYzVkNDI2YzdjOTEyMzYwMjJkOTk3ZWQyNTNiMDBhOGEwNjBhNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcjVsY2gyemQzN2Z6Z21xeXR2ZTBtZjk4dnEyM2dycTV1ZnE0NDA2bXAiXX19LHsidmFsdWUiOjAuNDc5MTIwMTEsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIDM4OGMyYzE3OTNjYmFjNzEyNDc2NjJhMWQ5NjllNTFhMWRjZTYzOTIgT1BfRVFVQUwiLCJoZXgiOiJhOTE0Mzg4YzJjMTc5M2NiYWM3MTI0NzY2MmExZDk2OWU1MWExZGNlNjM5Mjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHF1Z2N0cWhqMDk2Y3VmeXdlMzJya3RmdTVkcG1ubnJqZ3N6bnV1ZGwyIl19fV0sImZlZSI6Ny40MmUtMDYsImhleCI6IjAxMDAwMDAwMDE2MmFhMDMwZTljYTg5NTUxNzczMDczNTdiMmM0ZTEyNTBiZWYwMjNiZTU0NGU1Y2I2YTI5MzMwMTgxYzI4MTI0MDEwMDAwMDBmZGZkMDAwMDQ3MzA0NDAyMjAwNDMwYjA4NjEyYjU4NDY2MWNlMTM0NWQ1OTk3ZTNkOTk0M2ZhZTU0Nzk3MGZiNTg2YTYzZTBmOWFmYjFkMTZiMDIyMDEwOWM3YWMwNjE5NmYxZjk1NWM2YzU4NWExNDFjZDc2ZTdhMDhmODQxNjk2ZWNhNWJmOWQwOTk0NmQ3MDI1Y2I0MTQ4MzA0NTAyMjEwMGM2NDRmMDFkOTBiZGM0MWFlYWY2N2UyZTFiZjM1ZjE1MDJiMzQxNmYzYTI1MGMwMmU5MDliMDU4MTUzNzQ4MmQwMjIwN2M3M2UzZjMwNjE4ZWYyY2QyYzNhZDhiOWRjMTNkYmU4YjkyYWEzYzhjNGQwMTNkMzdmZmYyYzg3MjZjNGI4YzQxNGM2OTUyMjEwM2ZmMTQ1MDI4M2YwODU2OGFjZGI0ZDVmNTY5ZjMyZTRjZDRkOGMxOTYwZWEwNDlhMjA1NDM2ZjY5Zjk5MTZkZjgyMTAyMzBlZTZhZWM2NWJjMGRiN2U5Y2Y1MDdiMzMwNjdmNjgxMDQ3ZTE4MGU5MTkwNmUxZmRlMWJiNTQ5ZjIzM2IyNDIxMDM2NjA1ODQ4MmVjY2NiNDcwNzViZTlkMWQzZWRiNDZkZjMzMWMwNGZhNTEyNmNkNmZkOWRjNmNlZTA3MTIzN2I0NTNhZWZmZmZmZmZmMDIyMGM0NTYxZjAwMDAwMDAwMTk3NmE5MTRlOWZjNWQ0MjZjN2M5MTIzNjAyMmQ5OTdlZDI1M2IwMGE4YTA2MGE3ODhhYzRiMTRkYjAyMDAwMDAwMDAxN2E5MTQzODhjMmMxNzkzY2JhYzcxMjQ3NjYyYTFkOTY5ZTUxYTFkY2U2MzkyODcwMDAwMDAwMCJ9LHsidHhpZCI6ImEyYWY4ZmIwNzJmNjljNjY1Mjc0ODVjNTZiNGY5YWJhYzY0M2FkYzc3YTFkZWVhM2U1ZWVkYTdiZmZhZmYzMGIiLCJoYXNoIjoiYTJhZjhmYjA3MmY2OWM2NjUyNzQ4NWM1NmI0ZjlhYmFjNjQzYWRjNzdhMWRlZWEzZTVlZWRhN2JmZmFmZjMwYiIsInZlcnNpb24iOjIsInNpemUiOjM3NCwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjZkOTBjMjM3MzkzYjE5NjcxYjQ5NjMyYWU3NmRiZDdhM2UyODIzMzk4YzdiYWU2ZmQzNzRiOTRlZTZmYzJkMjAiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMGRlYWZmNDNmZGRlNTYxNjNhNmE1MjE2Mzc3NWI3M2YxZDc0NjI0MjhkMjk1YWNiOTU4OWQxY2ZmZGNiOGI0YWQwMjIwMTI1OWY0MzQ4YjBkNGUzMmU4MmY5NDMwYTM4OTYyNmFlZDgzOTI5YmE4ZDNhYzkzMGZjNWUwNGY1NjhiZDViNltBTEx8Rk9SS0lEXSAwMmQ4MjEyMzc1YTQwNmQ2MjI4NTZhMjc1NDBjYmM3YTUxOGU0N2IwMjg3MzdjYjBhOTJhMTA0MmM5ZDMxNWI0ZDciLCJoZXgiOiI0ODMwNDUwMjIxMDBkZWFmZjQzZmRkZTU2MTYzYTZhNTIxNjM3NzViNzNmMWQ3NDYyNDI4ZDI5NWFjYjk1ODlkMWNmZmRjYjhiNGFkMDIyMDEyNTlmNDM0OGIwZDRlMzJlODJmOTQzMGEzODk2MjZhZWQ4MzkyOWJhOGQzYWM5MzBmYzVlMDRmNTY4YmQ1YjY0MTIxMDJkODIxMjM3NWE0MDZkNjIyODU2YTI3NTQwY2JjN2E1MThlNDdiMDI4NzM3Y2IwYTkyYTEwNDJjOWQzMTViNGQ3In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI4NzZjOTBiOWVhZjY3MWU1NjQ3Yjk4ZDBlOGE4NzE2M2FhOWY1NzhiMTI4MWE1NWJmYzQ0MjRmY2ZmZTdmNDA1Iiwidm91dCI6NDU1LCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMGExMDUzMDM2ZDdlMWIwNjBiMGE5NTg4NzNlNmVhMWY1YzIwODZlYjNkNTVjNjZjN2Q0ZGQ4ZjBkYzcxZGNhMzEwMjIwNzRlYzFjYjFiOGJlM2UwMzY1YzAyZjY5ZmY2MWRlNDBiMzZkMDgxMjM2ZDYxYzc4NDA3ZmZmYjE5Y2IyMTVkZltBTEx8Rk9SS0lEXSAwMmY4ODI2OWQ2MTUwNzYyZjk0NjQxOTM1OWQwMWIxZmFmMDExYzhkMWUyNGViYjNjZGJhNmM1MTA1MzcxOGRhZGMiLCJoZXgiOiI0ODMwNDUwMjIxMDBhMTA1MzAzNmQ3ZTFiMDYwYjBhOTU4ODczZTZlYTFmNWMyMDg2ZWIzZDU1YzY2YzdkNGRkOGYwZGM3MWRjYTMxMDIyMDc0ZWMxY2IxYjhiZTNlMDM2NWMwMmY2OWZmNjFkZTQwYjM2ZDA4MTIzNmQ2MWM3ODQwN2ZmZmIxOWNiMjE1ZGY0MTIxMDJmODgyNjlkNjE1MDc2MmY5NDY0MTkzNTlkMDFiMWZhZjAxMWM4ZDFlMjRlYmIzY2RiYTZjNTEwNTM3MThkYWRjIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjozNC4zNDEzLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTM1MWFkMzViZmQzZTk3YzAzN2RmMzlmNWZlMDdjYjA3MTg5MmQyZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDUzNTFhZDM1YmZkM2U5N2MwMzdkZjM5ZjVmZTA3Y2IwNzE4OTJkMmU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBmNHJ0ZjRobGY3amxxcjBoZWU3aGxxMGpjOHJ6ZmQ5Y3N3bWNjbDQwIl19fSx7InZhbHVlIjowLjMyMjI2NTk4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTljZDg4NWYyY2ZmOWNlZTU3ZDUwZjc1ZmE4MTU4YzNmZTllOTQ5MCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGU5Y2Q4ODVmMmNmZjljZWU1N2Q1MGY3NWZhODE1OGMzZmU5ZTk0OTA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI1dW16emw5bmxlZW1qaDY1OGh0NzVwdHJwbGE4NTVqcWpzazhmMjJ5Il19fV0sImZlZSI6Ny42NGUtMDYsImhleCI6IjAyMDAwMDAwMDIyMDJkZmNlNjRlYjk3NGQzNmZhZTdiOGMzOTIzMjgzZTdhYmQ2ZGU3MmE2MzQ5MWI2NzE5M2IzOTM3YzI5MDZkMDAwMDAwMDA2YjQ4MzA0NTAyMjEwMGRlYWZmNDNmZGRlNTYxNjNhNmE1MjE2Mzc3NWI3M2YxZDc0NjI0MjhkMjk1YWNiOTU4OWQxY2ZmZGNiOGI0YWQwMjIwMTI1OWY0MzQ4YjBkNGUzMmU4MmY5NDMwYTM4OTYyNmFlZDgzOTI5YmE4ZDNhYzkzMGZjNWUwNGY1NjhiZDViNjQxMjEwMmQ4MjEyMzc1YTQwNmQ2MjI4NTZhMjc1NDBjYmM3YTUxOGU0N2IwMjg3MzdjYjBhOTJhMTA0MmM5ZDMxNWI0ZDdmZmZmZmZmZjA1ZjRlN2ZmZmMyNDQ0ZmM1YmE1ODExMjhiNTc5ZmFhNjM3MWE4ZThkMDk4N2I2NGU1NzFmNmVhYjk5MDZjODdjNzAxMDAwMDZiNDgzMDQ1MDIyMTAwYTEwNTMwMzZkN2UxYjA2MGIwYTk1ODg3M2U2ZWExZjVjMjA4NmViM2Q1NWM2NmM3ZDRkZDhmMGRjNzFkY2EzMTAyMjA3NGVjMWNiMWI4YmUzZTAzNjVjMDJmNjlmZjYxZGU0MGIzNmQwODEyMzZkNjFjNzg0MDdmZmZiMTljYjIxNWRmNDEyMTAyZjg4MjY5ZDYxNTA3NjJmOTQ2NDE5MzU5ZDAxYjFmYWYwMTFjOGQxZTI0ZWJiM2NkYmE2YzUxMDUzNzE4ZGFkY2ZmZmZmZmZmMDI1MGFhYjBjYzAwMDAwMDAwMTk3NmE5MTQ1MzUxYWQzNWJmZDNlOTdjMDM3ZGYzOWY1ZmUwN2NiMDcxODkyZDJlODhhYzI2YmRlYjAxMDAwMDAwMDAxOTc2YTkxNGU5Y2Q4ODVmMmNmZjljZWU1N2Q1MGY3NWZhODE1OGMzZmU5ZTk0OTA4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJhNTMwYmU0N2U0NjlmMjMxODM2NGVjYjBiZWY1YzkyMzY4MjM5NmVhMjFiZTVmYWJiM2Y4YzkzMTAwMTQzYmExIiwiaGFzaCI6ImE1MzBiZTQ3ZTQ2OWYyMzE4MzY0ZWNiMGJlZjVjOTIzNjgyMzk2ZWEyMWJlNWZhYmIzZjhjOTMxMDAxNDNiYTEiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyMTksImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI3ZmQ4YzMwNDNhYTBkNDY4OGQ2NjM4NGEyZTNkM2MyNDgzZDBkNjRlMzJmZWQxMzk2ZmFkM2YyNTY2MzEzZDY2Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMzZDhjMzYyMjMxZmE2NjhjM2JhZDdjM2VmYjU4YjA4NWRkZjM3ODU2ZDczMWM1YzE3YzI0ZTBlOGFhMDNhNzYxNjBkMjk5ZjIwN2E3Y2FmYTFjMjY0OGUxYTZlNjA3ZmM4ZGVlODNmM2ViNjE3OGY0ODg3N2U1Mzc4ZWExYjRlW0FMTHxGT1JLSURdIDAzNWVlNTZjODZhY2QzNDAyMzJmNTdmOGRkZWI4MjY3OTk3ODhmMzEyZjk5NDUxODkwM2QyOWFhNmQ4ZmQ1OTFkNCIsImhleCI6IjQxMzNkOGMzNjIyMzFmYTY2OGMzYmFkN2MzZWZiNThiMDg1ZGRmMzc4NTZkNzMxYzVjMTdjMjRlMGU4YWEwM2E3NjE2MGQyOTlmMjA3YTdjYWZhMWMyNjQ4ZTFhNmU2MDdmYzhkZWU4M2YzZWI2MTc4ZjQ4ODc3ZTUzNzhlYTFiNGU0MTIxMDM1ZWU1NmM4NmFjZDM0MDIzMmY1N2Y4ZGRlYjgyNjc5OTc4OGYzMTJmOTk0NTE4OTAzZDI5YWE2ZDhmZDU5MWQ0In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjAxNzE1MSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDg0Y2JkNmMyYzA2MzRjOGJkNzI4OTY4MzUwMjhiNzhmNzIyNTNkMTEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4NGNiZDZjMmMwNjM0YzhiZDcyODk2ODM1MDI4Yjc4ZjcyMjUzZDExODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6enZoNGt6Y3AzNWV6N2g5enRneDVwZ2s3OGh5ZmZhenlmZ3loY2FjZSJdfX0seyJ2YWx1ZSI6MC4wMTY4NDY3NCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGI4Y2YzZWM3MWI2ZThkODM3NmE5Yjk3ZTFmMTIzZWViNDllOGJlYmQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRiOGNmM2VjNzFiNmU4ZDgzNzZhOWI5N2UxZjEyM2VlYjQ5ZThiZWJkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6dXY3MGs4cmRoZ21xbWs0eHVodThjajhtNDVuNjk3aDU4ZzR3dHdodyJdfX1dLCJmZWUiOjIuMjZlLTA2LCJoZXgiOiIwMTAwMDAwMDAxNjYzZDMxNjYyNTNmYWQ2ZjM5ZDFmZTMyNGVkNmQwODMyNDNjM2QyZTRhMzg2NjhkNjhkNGEwM2EwNGMzZDg3ZjAwMDAwMDAwNjQ0MTMzZDhjMzYyMjMxZmE2NjhjM2JhZDdjM2VmYjU4YjA4NWRkZjM3ODU2ZDczMWM1YzE3YzI0ZTBlOGFhMDNhNzYxNjBkMjk5ZjIwN2E3Y2FmYTFjMjY0OGUxYTZlNjA3ZmM4ZGVlODNmM2ViNjE3OGY0ODg3N2U1Mzc4ZWExYjRlNDEyMTAzNWVlNTZjODZhY2QzNDAyMzJmNTdmOGRkZWI4MjY3OTk3ODhmMzEyZjk5NDUxODkwM2QyOWFhNmQ4ZmQ1OTFkNGZmZmZmZmZmMDI5YzJiMWEwMDAwMDAwMDAwMTk3NmE5MTQ4NGNiZDZjMmMwNjM0YzhiZDcyODk2ODM1MDI4Yjc4ZjcyMjUzZDExODhhY2MyYjQxOTAwMDAwMDAwMDAxOTc2YTkxNGI4Y2YzZWM3MWI2ZThkODM3NmE5Yjk3ZTFmMTIzZWViNDllOGJlYmQ4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJhNWFjNGJkNjRhZmY2ZTk0ZDQ0OWNmZDdmNjVmNzczZGVlN2I1YWM1Y2IyMzRiMDBiODhjZTE5NDBkMzM3MTJjIiwiaGFzaCI6ImE1YWM0YmQ2NGFmZjZlOTRkNDQ5Y2ZkN2Y2NWY3NzNkZWU3YjVhYzVjYjIzNGIwMGI4OGNlMTk0MGQzMzcxMmMiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyMTksImxvY2t0aW1lIjo5NDA4ODMsInZpbiI6W3sidHhpZCI6IjAzMzkyZDRmOTk0OTUwYmQ5YjkzODQ2YTA0NTYwYTcyNTYyNDE2OTc1ZDk1MGNkZDM4MWVhNTY0NjA1ZjE1ODkiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNWYxYmZkNjdkMDRiM2YyN2QwMDZlNDI3ZTE2Y2I3NDdjODAxNDE1MDZlZGE1ZmFlYzJmZTJmN2ZhYTcwNmE1YjU3YTBlMGI3NDVhY2MwODQ2MTUxYmQyZGVjZWRjMjNmZDI4OTczY2I0MzVjODBmYzA1MGNmODA3YzZkNGI5YTdbQUxMfEZPUktJRF0gMDIzZDNlNmJhNmU2MDViYTUyY2IxMmM1MDBkNjlkZjYwZDUxN2UwMjJkY2U5MGU0YTg5OWZkMzMxNDVhNTUyNzRkIiwiaGV4IjoiNDE1ZjFiZmQ2N2QwNGIzZjI3ZDAwNmU0MjdlMTZjYjc0N2M4MDE0MTUwNmVkYTVmYWVjMmZlMmY3ZmFhNzA2YTViNTdhMGUwYjc0NWFjYzA4NDYxNTFiZDJkZWNlZGMyM2ZkMjg5NzNjYjQzNWM4MGZjMDUwY2Y4MDdjNmQ0YjlhNzQxMjEwMjNkM2U2YmE2ZTYwNWJhNTJjYjEyYzUwMGQ2OWRmNjBkNTE3ZTAyMmRjZTkwZTRhODk5ZmQzMzE0NWE1NTI3NGQifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9XSwidm91dCI6W3sidmFsdWUiOjZlLTA1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTc1YzFkMDg4NmJlYTZmY2EwZTNhNWZlOTFkZDM0YTJkNmVlYzQzNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGU3NWMxZDA4ODZiZWE2ZmNhMGUzYTVmZTkxZGQzNGEyZDZlZWM0Mzc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJuNGM4Z2dzNmwyZGw5cXV3amxheXdheGozZGRta3l4dXlxN3VxOG1xIl19fSx7InZhbHVlIjowLjAwMjA4MywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDFmYTRjNGIxYTIyNmM1MWQ4YmM4NzQ4YjExMzBkZDc5ZDM1MzVmNmMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxZmE0YzRiMWEyMjZjNTFkOGJjODc0OGIxMTMwZGQ3OWQzNTM1ZjZjODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxMDZmMzkzNWdudjI4dnRlcDZna3lmc200dWF4NTZsZHMzOG1seGZjNCJdfX1dLCJmZWUiOjNlLTA2LCJoZXgiOiIwMTAwMDAwMDAxODkxNTVmNjA2NGE1MWUzOGRkMGM5NTVkOTcxNjI0NTY3MjBhNTYwNDZhODQ5MzliYmQ1MDQ5OTk0ZjJkMzkwMzAxMDAwMDAwNjQ0MTVmMWJmZDY3ZDA0YjNmMjdkMDA2ZTQyN2UxNmNiNzQ3YzgwMTQxNTA2ZWRhNWZhZWMyZmUyZjdmYWE3MDZhNWI1N2EwZTBiNzQ1YWNjMDg0NjE1MWJkMmRlY2VkYzIzZmQyODk3M2NiNDM1YzgwZmMwNTBjZjgwN2M2ZDRiOWE3NDEyMTAyM2QzZTZiYTZlNjA1YmE1MmNiMTJjNTAwZDY5ZGY2MGQ1MTdlMDIyZGNlOTBlNGE4OTlmZDMzMTQ1YTU1Mjc0ZGZlZmZmZmZmMDI3MDE3MDAwMDAwMDAwMDAwMTk3NmE5MTRlNzVjMWQwODg2YmVhNmZjYTBlM2E1ZmU5MWRkMzRhMmQ2ZWVjNDM3ODhhY2FjMmQwMzAwMDAwMDAwMDAxOTc2YTkxNDFmYTRjNGIxYTIyNmM1MWQ4YmM4NzQ4YjExMzBkZDc5ZDM1MzVmNmM4OGFjNTM1YjBlMDAifSx7InR4aWQiOiJhNWMxNDZlZWNhZWM3MzRlNjg0YzExYWJmMmYzOGY5NzdiODdjOTllNmY2ZDc5NGQzMDA5ZjQwNDE2ZDhmYzE4IiwiaGFzaCI6ImE1YzE0NmVlY2FlYzczNGU2ODRjMTFhYmYyZjM4Zjk3N2I4N2M5OWU2ZjZkNzk0ZDMwMDlmNDA0MTZkOGZjMTgiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyMTksImxvY2t0aW1lIjo5NDA4ODMsInZpbiI6W3sidHhpZCI6IjJlMGY5YjM3OGRhYWI3YWMxYzlkNTRiMjQxNDkyNmQ3ZjA5NzYxM2QyNmU1N2I5ODcxZWRkNjViNGYyM2NkZjYiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiM2IzNzFlMmM1ZTY1YzU3MmFmZjNkMGU0OWE3MDAxNTU0NGIzOGViZGNkODVkZjJlMWVhOTBiYWM2ZmE5YWFjMTVmY2JiZTc0ODZkYzE1OWI3YzQ2YmYzNDZiMjQ5ZTY5ODlkNjM5NjcxNDQyNTY5N2Q1NmFlZTk5YzU1OWQ0ZmNbQUxMfEZPUktJRF0gMDI5M2VmZmQ3YTA3YmFkMjdlYTI3YzVlYmQwOTJlNDA2NDMzY2Q1YjU2MTExY2U4NTJiZjY1ZjgzMGMzNTljNTA4IiwiaGV4IjoiNDEzYjM3MWUyYzVlNjVjNTcyYWZmM2QwZTQ5YTcwMDE1NTQ0YjM4ZWJkY2Q4NWRmMmUxZWE5MGJhYzZmYTlhYWMxNWZjYmJlNzQ4NmRjMTU5YjdjNDZiZjM0NmIyNDllNjk4OWQ2Mzk2NzE0NDI1Njk3ZDU2YWVlOTljNTU5ZDRmYzQxMjEwMjkzZWZmZDdhMDdiYWQyN2VhMjdjNWViZDA5MmU0MDY0MzNjZDViNTYxMTFjZTg1MmJmNjVmODMwYzM1OWM1MDgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTR9XSwidm91dCI6W3sidmFsdWUiOjAuMTIxODQ3NjEsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA4ODk1OWQ0ZjUyYWRhZGY4ZDQxZGU2YjExOTJmYzJhMzY2MTRhMzE2IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ODg5NTlkNGY1MmFkYWRmOGQ0MWRlNmIxMTkyZmMyYTM2NjE0YTMxNjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenlmdDgyMDIyazZtN3g1cmhudHp4ZjBjMjNrdjk5cnpjYzI3OXdxZGQiXX19LHsidmFsdWUiOjAuMjE4MTMwNDksIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhZjY3MzdlZTRlNDdjMTY3NDZlNzc1MzExOTZiN2M1ZWEyYTFjNGYyIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YWY2NzM3ZWU0ZTQ3YzE2NzQ2ZTc3NTMxMTk2YjdjNWVhMmExYzRmMjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemhrd2Rsd2ZlcnV6ZTZ4dWE2bnp4dHQwMzAyOWd3eTdnZTBkaHY3ZTkiXX19XSwiZmVlIjoyLjE5ZS0wNSwiaGV4IjoiMDEwMDAwMDAwMWY2Y2QyMzRmNWJkNmVkNzE5ODdiZTUyNjNkNjE5N2YwZDcyNjQ5NDFiMjU0OWQxY2FjYjdhYThkMzc5YjBmMmUwMDAwMDAwMDY0NDEzYjM3MWUyYzVlNjVjNTcyYWZmM2QwZTQ5YTcwMDE1NTQ0YjM4ZWJkY2Q4NWRmMmUxZWE5MGJhYzZmYTlhYWMxNWZjYmJlNzQ4NmRjMTU5YjdjNDZiZjM0NmIyNDllNjk4OWQ2Mzk2NzE0NDI1Njk3ZDU2YWVlOTljNTU5ZDRmYzQxMjEwMjkzZWZmZDdhMDdiYWQyN2VhMjdjNWViZDA5MmU0MDY0MzNjZDViNTYxMTFjZTg1MmJmNjVmODMwYzM1OWM1MDhmZWZmZmZmZjAyYjllY2I5MDAwMDAwMDAwMDE5NzZhOTE0ODg5NTlkNGY1MmFkYWRmOGQ0MWRlNmIxMTkyZmMyYTM2NjE0YTMxNjg4YWMzOWQ3NGMwMTAwMDAwMDAwMTk3NmE5MTRhZjY3MzdlZTRlNDdjMTY3NDZlNzc1MzExOTZiN2M1ZWEyYTFjNGYyODhhYzUzNWIwZTAwIn0seyJ0eGlkIjoiYTgyZmM3MWRlYWJhOWZkNWU3OTU2M2Y5OTgyOTZjZjlkMWM2NGI1ZTA4MjdjYWZkMDFlMDhmNDRmZThhNzU5ZSIsImhhc2giOiJhODJmYzcxZGVhYmE5ZmQ1ZTc5NTYzZjk5ODI5NmNmOWQxYzY0YjVlMDgyN2NhZmQwMWUwOGY0NGZlOGE3NTllIiwidmVyc2lvbiI6MSwic2l6ZSI6MjI2LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiZDk3ZGM4MWY3YjM1NzkxZWE0YWViNTIyZTYyMzZlY2UyZjMwNDY1MmY2N2QyOTFkNGY0MDA0ZWIzZDExNmE1MiIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwZjhkMGRjZjBmODQwMTc0YTU3YjI2OTE2ZDlmNWRkNjNlODA2ODFhMzA2ZTUwNThjODg0NzYzNmRhMjU1MDk5MDAyMjA0Yjg5YjM5ODI2YzRjOWIxMDQ3MjE1MzIwZmUzMDA5NWM2OWI0YWFkYjFmNTc1ZDEwNmI4ZWFlNWJlNmYyMTNmW0FMTHxGT1JLSURdIDAyM2VkYjk5MDQ2ODkxZmQwN2EyMTg5Yzc2MTY5NjhkMjNmZjIxNjVkNDJlY2ViYzI2OTM5NjZmZTcxZTE2MTIwMyIsImhleCI6IjQ4MzA0NTAyMjEwMGY4ZDBkY2YwZjg0MDE3NGE1N2IyNjkxNmQ5ZjVkZDYzZTgwNjgxYTMwNmU1MDU4Yzg4NDc2MzZkYTI1NTA5OTAwMjIwNGI4OWIzOTgyNmM0YzliMTA0NzIxNTMyMGZlMzAwOTVjNjliNGFhZGIxZjU3NWQxMDZiOGVhZTViZTZmMjEzZjQxMjEwMjNlZGI5OTA0Njg5MWZkMDdhMjE4OWM3NjE2OTY4ZDIzZmYyMTY1ZDQyZWNlYmMyNjkzOTY2ZmU3MWUxNjEyMDMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMTMwNTUwNjksIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhNWE1MTk2YWYxM2I5YjI3YjEyY2YyODliNWJlYTllMjk5NzYxMWQyIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YTVhNTE5NmFmMTNiOWIyN2IxMmNmMjg5YjViZWE5ZTI5OTc2MTFkMjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemo2Mnh0Mjd5YWVrZmEzOW5lZ25kZDc0ODNmamFzMzZnMnRqc2owZ2UiXX19LHsidmFsdWUiOjAuMDUyMDM0MDUsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAwOWY5MmRmOWFkMTkwMTdiYTllNzJjNmMxMjY5YjE2OWNmYzVlZTQzIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MDlmOTJkZjlhZDE5MDE3YmE5ZTcyYzZjMTI2OWIxNjljZmM1ZWU0Mzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXlsanQwZTQ1dnN6N2FmdXVreGN5bmZrOTV1bDMwd2d2NG4wcnVlaDkiXX19XSwiZmVlIjoyLjI2ZS0wNiwiaGV4IjoiMDEwMDAwMDAwMTUyNmExMTNkZWIwNDQwNGYxZDI5N2RmNjUyNDYzMDJmY2U2ZTIzZTYyMmI1YWVhNDFlNzkzNTdiMWZjODdkZDkwMTAwMDAwMDZiNDgzMDQ1MDIyMTAwZjhkMGRjZjBmODQwMTc0YTU3YjI2OTE2ZDlmNWRkNjNlODA2ODFhMzA2ZTUwNThjODg0NzYzNmRhMjU1MDk5MDAyMjA0Yjg5YjM5ODI2YzRjOWIxMDQ3MjE1MzIwZmUzMDA5NWM2OWI0YWFkYjFmNTc1ZDEwNmI4ZWFlNWJlNmYyMTNmNDEyMTAyM2VkYjk5MDQ2ODkxZmQwN2EyMTg5Yzc2MTY5NjhkMjNmZjIxNjVkNDJlY2ViYzI2OTM5NjZmZTcxZTE2MTIwM2ZmZmZmZmZmMDI1ZDM0YzcwMDAwMDAwMDAwMTk3NmE5MTRhNWE1MTk2YWYxM2I5YjI3YjEyY2YyODliNWJlYTllMjk5NzYxMWQyODhhY2NkNjU0ZjAwMDAwMDAwMDAxOTc2YTkxNDA5ZjkyZGY5YWQxOTAxN2JhOWU3MmM2YzEyNjliMTY5Y2ZjNWVlNDM4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJhYjI0NWQxMmUzOGE1ZjE5ZGIwY2RhNGUwM2YwNTc3YTRhYzA3NGMyNjg3NmQyODUyNDk1ZjgxZjhiN2Q5ZDczIiwiaGFzaCI6ImFiMjQ1ZDEyZTM4YTVmMTlkYjBjZGE0ZTAzZjA1NzdhNGFjMDc0YzI2ODc2ZDI4NTI0OTVmODFmOGI3ZDlkNzMiLCJ2ZXJzaW9uIjoxLCJzaXplIjozNzIsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI0Nzg5NmJiY2RlYzNiYTc5ODFmMGFhZGI3N2E0NWE5MTAxYjkxZDM1YTNmMDk4MTA4MTdhNzI2OTExM2NiZjVlIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwMDZiMzEzOGZjYjI5YzA2YTRkZDYyN2M1YzNlYWZmOTQ1Y2MyNzIyZjY2ODUyOTgxZDg4NTVmMjM3MGM4NjgwYzAyMjA3NTgwMGM3OWMyMDQ3ZGUxZjI5M2ZjZDY1NTMwY2FjNmVmOTcyOWI4MzljY2Y5YWEzMzQzOTYzMWUyOWIyNmUzW0FMTHxGT1JLSURdIDAyM2U5Y2VjMDFmYjk1MzFhMmZiOTk1YzE3MjQyYzk3OWEzN2I0M2JlMzgxZGQxNzU2NjA3ZDgzNTYyYTNmMDYxNSIsImhleCI6IjQ3MzA0NDAyMjAwNmIzMTM4ZmNiMjljMDZhNGRkNjI3YzVjM2VhZmY5NDVjYzI3MjJmNjY4NTI5ODFkODg1NWYyMzcwYzg2ODBjMDIyMDc1ODAwYzc5YzIwNDdkZTFmMjkzZmNkNjU1MzBjYWM2ZWY5NzI5YjgzOWNjZjlhYTMzNDM5NjMxZTI5YjI2ZTM0MTIxMDIzZTljZWMwMWZiOTUzMWEyZmI5OTVjMTcyNDJjOTc5YTM3YjQzYmUzODFkZDE3NTY2MDdkODM1NjJhM2YwNjE1In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjMTI1YjU2NTIwNTFiOTVhYjEyMDUzY2I4YTQ2MTc5Yzg2YjlmZWY1YWViNGUyNzQxZDIyNTIwMTFhNWNkZTNhIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwNDVmNzk5NjU3ZGQ4MzBlMzBjYThhYTkxMzBiNzAwZDY0MmMwZGMxYTk5YjIxMGE4NjNjZTU5ZjM5NzYwZDMyNjAyMjA3YWQyZWQ3ZmU4OTRmY2RjNzkyYzU2MzU0ODA1NjVmZGVmNWQ3MDg2MWNiY2RhZDNiMGU3ZGQ2NTZhN2I1YWRiW0FMTHxGT1JLSURdIDAzMDk2YzQ4NDdhODA2ZDY2NjYzMGMyZjZmYjM1YmMyZmRkYjk5ZDBlNzk0MzYyZDRlZWVmZGU2MTQyMzc0MTQ2NSIsImhleCI6IjQ3MzA0NDAyMjA0NWY3OTk2NTdkZDgzMGUzMGNhOGFhOTEzMGI3MDBkNjQyYzBkYzFhOTliMjEwYTg2M2NlNTlmMzk3NjBkMzI2MDIyMDdhZDJlZDdmZTg5NGZjZGM3OTJjNTYzNTQ4MDU2NWZkZWY1ZDcwODYxY2JjZGFkM2IwZTdkZDY1NmE3YjVhZGI0MTIxMDMwOTZjNDg0N2E4MDZkNjY2NjMwYzJmNmZiMzViYzJmZGRiOTlkMGU3OTQzNjJkNGVlZWZkZTYxNDIzNzQxNDY1In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjo1LjQ2ZS0wNiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDc5ZWY0Mjg4MTk0ZDc4NWEwNTgwNDhkNGEwYWJjMTdjYzk2M2ZiYWEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3OWVmNDI4ODE5NGQ3ODVhMDU4MDQ4ZDRhMGFiYzE3Y2M5NjNmYmFhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwdTc3czVncjl4aHNrczlzcHlkZmc5dGM5N3ZqY2xtNGd3ZWs0a2tjaiJdfX0seyJ2YWx1ZSI6MC4yMDAyMjgwMSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDlmOGM3OTFmZTFjMmU3YzVkYjBhM2JmMTZjY2FiOTIyMjdhMjMxNDQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5ZjhjNzkxZmUxYzJlN2M1ZGIwYTNiZjE2Y2NhYjkyMjI3YTIzMTQ0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6MGNjN2dsdThwdzAzd21wZ2Fsem14Mmh5M3owZzMzZ3M3MmpwZzJkNiJdfX1dLCJmZWUiOjQuNzRlLTA2LCJoZXgiOiIwMTAwMDAwMDAyNWViZjNjMTE2OTcyN2E4MTEwOThmMGEzMzUxZGI5MDE5MTVhYTQ3N2RiYWFmMDgxNzliYWMzZGViYzZiODk0NzAwMDAwMDAwNmE0NzMwNDQwMjIwMDZiMzEzOGZjYjI5YzA2YTRkZDYyN2M1YzNlYWZmOTQ1Y2MyNzIyZjY2ODUyOTgxZDg4NTVmMjM3MGM4NjgwYzAyMjA3NTgwMGM3OWMyMDQ3ZGUxZjI5M2ZjZDY1NTMwY2FjNmVmOTcyOWI4MzljY2Y5YWEzMzQzOTYzMWUyOWIyNmUzNDEyMTAyM2U5Y2VjMDFmYjk1MzFhMmZiOTk1YzE3MjQyYzk3OWEzN2I0M2JlMzgxZGQxNzU2NjA3ZDgzNTYyYTNmMDYxNWZmZmZmZmZmM2FkZTVjMWEwMTUyMjIxZDc0ZTJiNGFlZjVmZWI5ODY5YzE3NDY4YWNiNTMyMGIxNWFiOTUxMjA2NWI1MjVjMTAwMDAwMDAwNmE0NzMwNDQwMjIwNDVmNzk5NjU3ZGQ4MzBlMzBjYThhYTkxMzBiNzAwZDY0MmMwZGMxYTk5YjIxMGE4NjNjZTU5ZjM5NzYwZDMyNjAyMjA3YWQyZWQ3ZmU4OTRmY2RjNzkyYzU2MzU0ODA1NjVmZGVmNWQ3MDg2MWNiY2RhZDNiMGU3ZGQ2NTZhN2I1YWRiNDEyMTAzMDk2YzQ4NDdhODA2ZDY2NjYzMGMyZjZmYjM1YmMyZmRkYjk5ZDBlNzk0MzYyZDRlZWVmZGU2MTQyMzc0MTQ2NWZmZmZmZmZmMDIyMjAyMDAwMDAwMDAwMDAwMTk3NmE5MTQ3OWVmNDI4ODE5NGQ3ODVhMDU4MDQ4ZDRhMGFiYzE3Y2M5NjNmYmFhODhhYzExODYzMTAxMDAwMDAwMDAxOTc2YTkxNDlmOGM3OTFmZTFjMmU3YzVkYjBhM2JmMTZjY2FiOTIyMjdhMjMxNDQ4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJhZTIwZjczOTQxYjc2YjAyYmI0NjFjYWJmNDQ0ZTkzOWU1ZjczYmNkZTMwZDM0NTdiOTE2NDllZGQyMWM1ZjE2IiwiaGFzaCI6ImFlMjBmNzM5NDFiNzZiMDJiYjQ2MWNhYmY0NDRlOTM5ZTVmNzNiY2RlMzBkMzQ1N2I5MTY0OWVkZDIxYzVmMTYiLCJ2ZXJzaW9uIjoyLCJzaXplIjo3MjYsImxvY2t0aW1lIjo5MDAxLCJ2aW4iOlt7InR4aWQiOiI0MzBjNjg0MmYyMDUzOWNmOTVlOGQ4NjEwZTdmNzUzMzk1ZjNkNjQ2YzkwOGUzNzg0NzhiMWUzN2JlYzQ3NzdlIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjAzYjFhZWRlYzg0MTIwOTE5N2U4YjczNTVmOTcwNDNjZmUxZjY0MTA5NzhjNTkxODU4MjVmMzgwYjNhN2EzMmVhYzE4MTA1NDRkMzRiYTI4NmQ4YjNkZWRiNGJiNDRkOWI4ZmZkZTJhZTI3MDg5NjE4YWRmYTcxZmRhYmNlNjM5W0FMTHxGT1JLSURdIDAyMDU4YzNlNmY1ODk2YjViMWQyNTc2YWVkNDFlZDA2OTQwOGVhNzY1YmIxZTBjZjgzYjJhY2NhMGE3N2ExMGExMiIsImhleCI6IjQxMDNiMWFlZGVjODQxMjA5MTk3ZThiNzM1NWY5NzA0M2NmZTFmNjQxMDk3OGM1OTE4NTgyNWYzODBiM2E3YTMyZWFjMTgxMDU0NGQzNGJhMjg2ZDhiM2RlZGI0YmI0NGQ5YjhmZmRlMmFlMjcwODk2MThhZGZhNzFmZGFiY2U2Mzk0MTIxMDIwNThjM2U2ZjU4OTZiNWIxZDI1NzZhZWQ0MWVkMDY5NDA4ZWE3NjViYjFlMGNmODNiMmFjY2EwYTc3YTEwYTEyIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI0MzBjNjg0MmYyMDUzOWNmOTVlOGQ4NjEwZTdmNzUzMzk1ZjNkNjQ2YzkwOGUzNzg0NzhiMWUzN2JlYzQ3NzdlIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjAgMjAyYTFjNzliZjI1OWZlODBlZDQyMTYyMzFiZDFkN2E2MzhhMWYzZTI4YmM0NzdlYzM2YjEwYWQ0NTQ1YWQzNDY2MjBmNmZmM2NhMDdhZDk2ZjI5NDRjOTM3ZTdjNDg3MDdmNWE1MTJiNjAwYzIyYjY4MDlhMjAzYjQwMWMzMWQzNjA5NTI3OTAwOWM2MzAwY2UwMTIwN2Y3NTg4YzBkMjc2ODI3NzVhYTI2OWMwY2Y3ODU2N2Y3NTgxN2M1NjdmNzU4MWEwNjk1ODdmNzc1MjdmNzU4MTAwYTA2OWMwY2NjMGM2YTI2OWMwY2RjMGM3ODhjMGQxYzBjZTg3Nzc3NzY3NTI3OTUxOWM2M2MwY2Y1NjdmNzc1MjdmNzU4MWMwY2NjMGM2N2I5M2EyNjljMGNkYzBjNzg4YzBkMWMwY2U4OGMwZDJjMGNmODc3Nzc3Nzc2NzdiNTI5ZDAwY2UwMTIwN2Y3NTdiODhjMGNkYzBjNzg4YzBkMWMwY2U4OGMwZDJjMGNmODc3NzY4NjgiLCJoZXgiOiIwMDRjYzcyMDJhMWM3OWJmMjU5ZmU4MGVkNDIxNjIzMWJkMWQ3YTYzOGExZjNlMjhiYzQ3N2VjMzZiMTBhZDQ1NDVhZDM0NjYyMGY2ZmYzY2EwN2FkOTZmMjk0NGM5MzdlN2M0ODcwN2Y1YTUxMmI2MDBjMjJiNjgwOWEyMDNiNDAxYzMxZDM2MDk1Mjc5MDA5YzYzMDBjZTAxMjA3Zjc1ODhjMGQyNzY4Mjc3NWFhMjY5YzBjZjc4NTY3Zjc1ODE3YzU2N2Y3NTgxYTA2OTU4N2Y3NzUyN2Y3NTgxMDBhMDY5YzBjY2MwYzZhMjY5YzBjZGMwYzc4OGMwZDFjMGNlODc3Nzc3Njc1Mjc5NTE5YzYzYzBjZjU2N2Y3NzUyN2Y3NTgxYzBjY2MwYzY3YjkzYTI2OWMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Nzc3NzY3N2I1MjlkMDBjZTAxMjA3Zjc1N2I4OGMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Njg2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNDMwYzY4NDJmMjA1MzljZjk1ZThkODYxMGU3Zjc1MzM5NWYzZDY0NmM5MDhlMzc4NDc4YjFlMzdiZWM0Nzc3ZSIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiJkNjRkOThmMzc3Y2EyZDIzYmI2YzM5YmE5ZTQ5OTM0NWEzMDhkY2ExNjU2ZWI0YmJmM2E2ZTRjYzYzZmVkNjNkMmIwYjMzZmQ1MDA5YzA5NTYxOTFkMzhkNDZkOGUxYmMxZDExY2E2ODE4ZmM0N2U2NTM2YjZkOGZmNWJiYTE0N1tBTEx8Rk9SS0lEXSAwMjA1OGMzZTZmNTg5NmI1YjFkMjU3NmFlZDQxZWQwNjk0MDhlYTc2NWJiMWUwY2Y4M2IyYWNjYTBhNzdhMTBhMTIiLCJoZXgiOiI0MWQ2NGQ5OGYzNzdjYTJkMjNiYjZjMzliYTllNDk5MzQ1YTMwOGRjYTE2NTZlYjRiYmYzYTZlNGNjNjNmZWQ2M2QyYjBiMzNmZDUwMDljMDk1NjE5MWQzOGQ0NmQ4ZTFiYzFkMTFjYTY4MThmYzQ3ZTY1MzZiNmQ4ZmY1YmJhMTQ3NDEyMTAyMDU4YzNlNmY1ODk2YjViMWQyNTc2YWVkNDFlZDA2OTQwOGVhNzY1YmIxZTBjZjgzYjJhY2NhMGE3N2ExMGExMiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6OGUtMDYsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3MmMwM2UzZWI3MDc2YTE0ODc3ZjYzOTlmMDJjZTMyYTUyZDNlMzdkIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NzJjMDNlM2ViNzA3NmExNDg3N2Y2Mzk5ZjAyY2UzMmE1MmQzZTM3ZDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGV2cTAzN2t1cms1OXk4MGEzZW51cHZ1djQ5OTVscjA1djY3YXV6aHoiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6IjA5MzYxZGMzMDFiNDAzYTIwOTY4MmJjMjAwYjYxMmE1ZjUwNzg3YzRlNzM3Yzk0NDI5NmZkOTdhYTAzY2ZmZjYiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJub25lIiwiY29tbWl0bWVudCI6IiJ9fX0seyJ2YWx1ZSI6MC4wMTI4NCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgODM3NTE2NDg2ZWM4NjI1Zjc2Mzc4Zjg4NDA1MjhjZjIxZWIxMjVkNmRhOTkyNDdiOTQ4ZjhmNTBiY2Y5NDRiMSBPUF9FUVVBTCIsImhleCI6ImFhMjA4Mzc1MTY0ODZlYzg2MjVmNzYzNzhmODg0MDUyOGNmMjFlYjEyNWQ2ZGE5OTI0N2I5NDhmOGY1MGJjZjk0NGIxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3BoMjlqZ2RteXh5aG1reDc4Y3NzemozbmVwYXZmOTZtZGZqZnJtamo4Yzc1OXVsOXp0ejR4c2MwbmU0Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiIwMTcxMWUzOWU3YmYzYjhjYTBkOWE2ZmM2ZWEzMmUzNDBjYWExZDY0ZGM3ZDFkYzUxZmFlMjBmZDY2NzU1NTU4IiwiYW1vdW50IjoiMCIsIm5mdCI6eyJjYXBhYmlsaXR5IjoibXV0YWJsZSIsImNvbW1pdG1lbnQiOiIwOTg1YTg2OTAwMDBlODAzNjMwMCJ9fX0seyJ2YWx1ZSI6MC44NjMzMzA1LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNzJjMDNlM2ViNzA3NmExNDg3N2Y2Mzk5ZjAyY2UzMmE1MmQzZTM3ZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDcyYzAzZTNlYjcwNzZhMTQ4NzdmNjM5OWYwMmNlMzJhNTJkM2UzN2Q4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBldnEwMzdrdXJrNTl5ODBhM2VudXB2dXY0OTk1bHIwNXY2N2F1emh6Il19fV0sImZlZSI6Ny4yNmUtMDYsImhleCI6IjAyMDAwMDAwMDM3ZTc3YzRiZTM3MWU4YjQ3NzhlMzA4Yzk0NmQ2ZjM5NTMzNzU3ZjBlNjFkOGU4OTVjZjM5MDVmMjQyNjgwYzQzMDAwMDAwMDA2NDQxMDNiMWFlZGVjODQxMjA5MTk3ZThiNzM1NWY5NzA0M2NmZTFmNjQxMDk3OGM1OTE4NTgyNWYzODBiM2E3YTMyZWFjMTgxMDU0NGQzNGJhMjg2ZDhiM2RlZGI0YmI0NGQ5YjhmZmRlMmFlMjcwODk2MThhZGZhNzFmZGFiY2U2Mzk0MTIxMDIwNThjM2U2ZjU4OTZiNWIxZDI1NzZhZWQ0MWVkMDY5NDA4ZWE3NjViYjFlMGNmODNiMmFjY2EwYTc3YTEwYTEyZmZmZmZmZmY3ZTc3YzRiZTM3MWU4YjQ3NzhlMzA4Yzk0NmQ2ZjM5NTMzNzU3ZjBlNjFkOGU4OTVjZjM5MDVmMjQyNjgwYzQzMDEwMDAwMDBjYTAwNGNjNzIwMmExYzc5YmYyNTlmZTgwZWQ0MjE2MjMxYmQxZDdhNjM4YTFmM2UyOGJjNDc3ZWMzNmIxMGFkNDU0NWFkMzQ2NjIwZjZmZjNjYTA3YWQ5NmYyOTQ0YzkzN2U3YzQ4NzA3ZjVhNTEyYjYwMGMyMmI2ODA5YTIwM2I0MDFjMzFkMzYwOTUyNzkwMDljNjMwMGNlMDEyMDdmNzU4OGMwZDI3NjgyNzc1YWEyNjljMGNmNzg1NjdmNzU4MTdjNTY3Zjc1ODFhMDY5NTg3Zjc3NTI3Zjc1ODEwMGEwNjljMGNjYzBjNmEyNjljMGNkYzBjNzg4YzBkMWMwY2U4Nzc3Nzc2NzUyNzk1MTljNjNjMGNmNTY3Zjc3NTI3Zjc1ODFjMGNjYzBjNjdiOTNhMjY5YzBjZGMwYzc4OGMwZDFjMGNlODhjMGQyYzBjZjg3Nzc3Nzc3Njc3YjUyOWQwMGNlMDEyMDdmNzU3Yjg4YzBjZGMwYzc4OGMwZDFjMGNlODhjMGQyYzBjZjg3Nzc2ODY4ZmZmZmZmZmY3ZTc3YzRiZTM3MWU4YjQ3NzhlMzA4Yzk0NmQ2ZjM5NTMzNzU3ZjBlNjFkOGU4OTVjZjM5MDVmMjQyNjgwYzQzMDIwMDAwMDA2NDQxZDY0ZDk4ZjM3N2NhMmQyM2JiNmMzOWJhOWU0OTkzNDVhMzA4ZGNhMTY1NmViNGJiZjNhNmU0Y2M2M2ZlZDYzZDJiMGIzM2ZkNTAwOWMwOTU2MTkxZDM4ZDQ2ZDhlMWJjMWQxMWNhNjgxOGZjNDdlNjUzNmI2ZDhmZjViYmExNDc0MTIxMDIwNThjM2U2ZjU4OTZiNWIxZDI1NzZhZWQ0MWVkMDY5NDA4ZWE3NjViYjFlMGNmODNiMmFjY2EwYTc3YTEwYTEyZmZmZmZmZmYwMzIwMDMwMDAwMDAwMDAwMDAzYmVmZjZmZjNjYTA3YWQ5NmYyOTQ0YzkzN2U3YzQ4NzA3ZjVhNTEyYjYwMGMyMmI2ODA5YTIwM2I0MDFjMzFkMzYwOTIwNzZhOTE0NzJjMDNlM2ViNzA3NmExNDg3N2Y2Mzk5ZjAyY2UzMmE1MmQzZTM3ZDg4YWNhMDk3MTMwMDAwMDAwMDAwNTBlZjU4NTU3NTY2ZmQyMGFlMWZjNTFkN2RkYzY0MWRhYTBjMzQyZWEzNmVmY2E2ZDlhMDhjM2JiZmU3MzkxZTcxMDE2MTBhMDk4NWE4NjkwMDAwZTgwMzYzMDBhYTIwODM3NTE2NDg2ZWM4NjI1Zjc2Mzc4Zjg4NDA1MjhjZjIxZWIxMjVkNmRhOTkyNDdiOTQ4ZjhmNTBiY2Y5NDRiMTg3N2E1NjI1MDUwMDAwMDAwMDE5NzZhOTE0NzJjMDNlM2ViNzA3NmExNDg3N2Y2Mzk5ZjAyY2UzMmE1MmQzZTM3ZDg4YWMyOTIzMDAwMCJ9LHsidHhpZCI6ImIyZDQxMzExNTJiYjI5ZmQ1NjZhOThiNzYzMzk3YWIxNjJjMmFlMTM3OTIwOWJlYTRkMjU5NGE2YzU3ODQwNDQiLCJoYXNoIjoiYjJkNDEzMTE1MmJiMjlmZDU2NmE5OGI3NjMzOTdhYjE2MmMyYWUxMzc5MjA5YmVhNGQyNTk0YTZjNTc4NDA0NCIsInZlcnNpb24iOjIsInNpemUiOjIyNSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjIwYjNjOTBlMmVlNzA5Mzc2OTFlNGVjZWViMzVjNDJjNWQzMDdiOTUwMThhMGI5ODM0NTYyYTUxZTQ0NDBhMTkiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA3MWYzOGE5ODU0M2ZlZDI2MWU1NThiZDg1NmY0YzM2NTVkZGExMzIyZDRmMmY5MmI0ZWYyMzNhMGQwMjFkMDBiMDIyMDU5NzliYTRkNGYzZGNjNDAzNTg2YjIyNDIwOGYwZTNkOGJkNmQ5NjdjODM0MTM5YWMyYzMxZDRmNGY2YmQyMDhbQUxMfEZPUktJRF0gMDMzYjVlMGMwNzA5ZDVjNmI2MTcxOTk0ZDczZTEyY2FkNTQ0Y2QxNzUwYjFiZGIwYWNlZjJlZDQ1ZjNjNzZlNTc4IiwiaGV4IjoiNDczMDQ0MDIyMDcxZjM4YTk4NTQzZmVkMjYxZTU1OGJkODU2ZjRjMzY1NWRkYTEzMjJkNGYyZjkyYjRlZjIzM2EwZDAyMWQwMGIwMjIwNTk3OWJhNGQ0ZjNkY2M0MDM1ODZiMjI0MjA4ZjBlM2Q4YmQ2ZDk2N2M4MzQxMzlhYzJjMzFkNGY0ZjZiZDIwODQxMjEwMzNiNWUwYzA3MDlkNWM2YjYxNzE5OTRkNzNlMTJjYWQ1NDRjZDE3NTBiMWJkYjBhY2VmMmVkNDVmM2M3NmU1NzgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMDU1NywibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGYwYzY4NGZjMGUzNDc1MDQxYWJhOWU3NDE1NmJlNDBkNTIwOWU0OWUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmMGM2ODRmYzBlMzQ3NTA0MWFiYTllNzQxNTZiZTQwZDUyMDllNDllODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyY3ZkcDh1cGM2ODJwcTZoMjA4Zzl0dHVzeDR5ejB5bmNyeXIyNXU3NiJdfX0seyJ2YWx1ZSI6MTcuMjIzODQyNzgsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhNmVlMDRjYjc3Mjg3NTlhOGRlZTZiZWQzYTc4MTMxN2EzY2UzZDJlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YTZlZTA0Y2I3NzI4NzU5YThkZWU2YmVkM2E3ODEzMTdhM2NlM2QyZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxem53dXB4dHd1NTh0eDVkYWU0NzZ3bmN6dnQ2OG4zYTljbTRtcjlwdG0iXX19XSwiZmVlIjoyLjdlLTA2LCJoZXgiOiIwMjAwMDAwMDAxMTkwYTQ0ZTQ1MTJhNTYzNDk4MGI4YTAxOTU3YjMwNWQyY2M0MzVlYmNlNGUxZTY5MzcwOWU3MmUwZWM5YjMyMDAyMDAwMDAwNmE0NzMwNDQwMjIwNzFmMzhhOTg1NDNmZWQyNjFlNTU4YmQ4NTZmNGMzNjU1ZGRhMTMyMmQ0ZjJmOTJiNGVmMjMzYTBkMDIxZDAwYjAyMjA1OTc5YmE0ZDRmM2RjYzQwMzU4NmIyMjQyMDhmMGUzZDhiZDZkOTY3YzgzNDEzOWFjMmMzMWQ0ZjRmNmJkMjA4NDEyMTAzM2I1ZTBjMDcwOWQ1YzZiNjE3MTk5NGQ3M2UxMmNhZDU0NGNkMTc1MGIxYmRiMGFjZWYyZWQ0NWYzYzc2ZTU3OGZmZmZmZmZmMDJkMGZkNTQwMDAwMDAwMDAwMTk3NmE5MTRmMGM2ODRmYzBlMzQ3NTA0MWFiYTllNzQxNTZiZTQwZDUyMDllNDllODhhYzk2N2ZhOTY2MDAwMDAwMDAxOTc2YTkxNGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmU4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJiM2NkYzUzNmRlNjBmZjMzNTFmZjgxMTQwOTA4OGUxNGY0ZWUyM2Y0NmVjMjJiNjBiMzlhNGY5M2NmOTFmMDI4IiwiaGFzaCI6ImIzY2RjNTM2ZGU2MGZmMzM1MWZmODExNDA5MDg4ZTE0ZjRlZTIzZjQ2ZWMyMmI2MGIzOWE0ZjkzY2Y5MWYwMjgiLCJ2ZXJzaW9uIjoxLCJzaXplIjo2NzAsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiIxNjI4OGRlN2ZmODQwMDgxNzgwOTBmMWVmMTRkZjhkYzZiODk5NzliYzhhYWIzZDg0MTc4OTZhZTY2MDlhYWU5Iiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjAgMzA0NTAyMjEwMDhmMWNhZmNlNTYzY2U0YTI1ZWMwMTY5YzlkODY0MTAwY2FlMDIwYzdlZDI5YzEzZGY4NTk4ZDA1MzYwZDkzMzAwMjIwNDRhOTExNWI3MWE3ODVjM2ZlM2VjYTgwNWIyNDI3OTE4MTA4ZDY1MjdmNTZiN2FlYzczZWFhNzAzMTE2YWIwMFtBTEx8Rk9SS0lEXSAzMDQ1MDIyMTAwY2ZkM2FkZmUxZjFiZDA3ODg4YWI5MzlhNGE0NmYxNTNhMjEzMGI5ZDIyMTk1ZWViYjk4NzM2NWM4Zjk1NWY0YzAyMjAxOGRhN2IzNmQyYWYzYzg1OTJjMDMxNDYyYWVhYzRhMDAwZWFmNmEzZjUwYmY4YTNkMzMzZTQxOTIyYjc1MTAwW0FMTHxGT1JLSURdIDUyMjEwMmJmYWZhODA3NzMyYzg0Mzg5Yjg0N2Y5YzllOTE3M2Y3ZTJjMjNiZmFlYzA3OTI3NmJkNWNlMjAwYTE2ZGRiZDgyMTAyMjBiMGZkMzM2NTdlYWFjYjBkMmEzMTJmZmJkZmJhMTc2N2Y0NWM4Nzc4OGJiNTU2ZWQzNGQ1NDA0YjdkY2FhYjIxMDMyYjNhZmVhMDNlMTg1ZDg3NTA2YjliMGRjMjRhNTljNDEzODIxYTJiZmZkMzliZGRkMzJmNzJiNTE4ZTZmMmE4NTNhZSIsImhleCI6IjAwNDgzMDQ1MDIyMTAwOGYxY2FmY2U1NjNjZTRhMjVlYzAxNjljOWQ4NjQxMDBjYWUwMjBjN2VkMjljMTNkZjg1OThkMDUzNjBkOTMzMDAyMjA0NGE5MTE1YjcxYTc4NWMzZmUzZWNhODA1YjI0Mjc5MTgxMDhkNjUyN2Y1NmI3YWVjNzNlYWE3MDMxMTZhYjAwNDE0ODMwNDUwMjIxMDBjZmQzYWRmZTFmMWJkMDc4ODhhYjkzOWE0YTQ2ZjE1M2EyMTMwYjlkMjIxOTVlZWJiOTg3MzY1YzhmOTU1ZjRjMDIyMDE4ZGE3YjM2ZDJhZjNjODU5MmMwMzE0NjJhZWFjNGEwMDBlYWY2YTNmNTBiZjhhM2QzMzNlNDE5MjJiNzUxMDA0MTRjNjk1MjIxMDJiZmFmYTgwNzczMmM4NDM4OWI4NDdmOWM5ZTkxNzNmN2UyYzIzYmZhZWMwNzkyNzZiZDVjZTIwMGExNmRkYmQ4MjEwMjIwYjBmZDMzNjU3ZWFhY2IwZDJhMzEyZmZiZGZiYTE3NjdmNDVjODc3ODhiYjU1NmVkMzRkNTQwNGI3ZGNhYWIyMTAzMmIzYWZlYTAzZTE4NWQ4NzUwNmI5YjBkYzI0YTU5YzQxMzgyMWEyYmZmZDM5YmRkZDMyZjcyYjUxOGU2ZjJhODUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjEwNzFjNzYwMmZiZmQ5YTQzOGIwZWE5NjI4OGQ1MTA1MDJjMmUzYzc0OWQ4YzE4Y2M0OWE4MzhlNjIyODFjNDciLCJ2b3V0Ijo4Nywic2NyaXB0U2lnIjp7ImFzbSI6IjAgMzA0NTAyMjEwMGRhYjE5OTRjYjE5NGJkZTBmZDk3ZjNmMWJiMWRlZmM3MmUzNDhkOGFiOTZkN2ZkYzBiZDg2MDU4MWFjYzQ2ZTYwMjIwNGMzY2E3MDcxNDA0YzdjZDFiMDU3ZmNlNzFkZDNmM2Q1NTdiOGE0ZDRhM2UxODk4ZjIxZDQzYzIyN2FhZDcxMFtBTEx8Rk9SS0lEXSAzMDQ1MDIyMTAwODMzZmIyNTNjYzk0YzgxZGJiOTcxOTdkMWMxMDA3ZjNiNzIwNzkyYzg2ODY2MDdlZWU3NDE3ZGVhN2UyNzUyNjAyMjAxMDliMmIxNDMzMjg3YWQyMTA2ZGMxZTgwZTNkMDE2OGFjZWVjYzNkNDVlYzE1YzY0YzAyMzc0MTJlM2YzMWNlW0FMTHxGT1JLSURdIDUyMjEwM2Q4ZWZmMjk2OTk5NDEzNjMxZmJhMmFjYmZhYTBlMWYwNjIzYWY5M2FhODFmN2IxYjRmZmY4YjVkOTBlYWY5ZjQyMTAzZWU4MzA0OGYxYjYxZTdhMjQ4YmI0NzcyNWNiNzRmMmU4YjUyZWIzMjBkMGYwMWRkYzRhYzYxNzEzNjYwNGIzMjIxMDJhYWQyY2Y5YjZjNGIxNmM1MTA5ZGVlMjljMjhkYTJjZGI0Yjc4OTEwNmQ0NmExMDcwM2Q2ZWMxZTQ3Njg1Zjc4NTNhZSIsImhleCI6IjAwNDgzMDQ1MDIyMTAwZGFiMTk5NGNiMTk0YmRlMGZkOTdmM2YxYmIxZGVmYzcyZTM0OGQ4YWI5NmQ3ZmRjMGJkODYwNTgxYWNjNDZlNjAyMjA0YzNjYTcwNzE0MDRjN2NkMWIwNTdmY2U3MWRkM2YzZDU1N2I4YTRkNGEzZTE4OThmMjFkNDNjMjI3YWFkNzEwNDE0ODMwNDUwMjIxMDA4MzNmYjI1M2NjOTRjODFkYmI5NzE5N2QxYzEwMDdmM2I3MjA3OTJjODY4NjYwN2VlZTc0MTdkZWE3ZTI3NTI2MDIyMDEwOWIyYjE0MzMyODdhZDIxMDZkYzFlODBlM2QwMTY4YWNlZWNjM2Q0NWVjMTVjNjRjMDIzNzQxMmUzZjMxY2U0MTRjNjk1MjIxMDNkOGVmZjI5Njk5OTQxMzYzMWZiYTJhY2JmYWEwZTFmMDYyM2FmOTNhYTgxZjdiMWI0ZmZmOGI1ZDkwZWFmOWY0MjEwM2VlODMwNDhmMWI2MWU3YTI0OGJiNDc3MjVjYjc0ZjJlOGI1MmViMzIwZDBmMDFkZGM0YWM2MTcxMzY2MDRiMzIyMTAyYWFkMmNmOWI2YzRiMTZjNTEwOWRlZTI5YzI4ZGEyY2RiNGI3ODkxMDZkNDZhMTA3MDNkNmVjMWU0NzY4NWY3ODUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjE5OS40NCwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDYxNzhiNDk1MmQxZGYwODJiNjAyYWEwYmZkMGQwMjQxYzViOTVmMjAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2MTc4YjQ5NTJkMWRmMDgyYjYwMmFhMGJmZDBkMDI0MWM1Yjk1ZjIwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwc2gzZHk0OTV3bHBxNGtxMjRxaGxnZHFmcXV0dzJseXFlN2Q1NGdlbSJdfX0seyJ2YWx1ZSI6OTc1LjIyNDYyNTEyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCA1NjYzMWU0YzNiM2M5MGUwZDQ1ZTg2NzRiYTMyMjYyMWM2N2FhNDc1IE9QX0VRVUFMIiwiaGV4IjoiYTkxNDU2NjMxZTRjM2IzYzkwZTBkNDVlODY3NGJhMzIyNjIxYzY3YWE0NzU4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBwdHh4OGp2OHY3ZnBjeDV0NnI4Znczanljc3V2NzR5dzVhZDUwNjJ4ciJdfX1dLCJmZWUiOjAuMDAwMjA3NiwiaGV4IjoiMDEwMDAwMDAwMmU5YWEwOTY2YWU5Njc4NDFkOGIzYWFjODliOTc4OTZiZGNmODRkZjExZTBmMDk3ODgxMDA4NGZmZTc4ZDI4MTYwMTAwMDAwMGZkZmUwMDAwNDgzMDQ1MDIyMTAwOGYxY2FmY2U1NjNjZTRhMjVlYzAxNjljOWQ4NjQxMDBjYWUwMjBjN2VkMjljMTNkZjg1OThkMDUzNjBkOTMzMDAyMjA0NGE5MTE1YjcxYTc4NWMzZmUzZWNhODA1YjI0Mjc5MTgxMDhkNjUyN2Y1NmI3YWVjNzNlYWE3MDMxMTZhYjAwNDE0ODMwNDUwMjIxMDBjZmQzYWRmZTFmMWJkMDc4ODhhYjkzOWE0YTQ2ZjE1M2EyMTMwYjlkMjIxOTVlZWJiOTg3MzY1YzhmOTU1ZjRjMDIyMDE4ZGE3YjM2ZDJhZjNjODU5MmMwMzE0NjJhZWFjNGEwMDBlYWY2YTNmNTBiZjhhM2QzMzNlNDE5MjJiNzUxMDA0MTRjNjk1MjIxMDJiZmFmYTgwNzczMmM4NDM4OWI4NDdmOWM5ZTkxNzNmN2UyYzIzYmZhZWMwNzkyNzZiZDVjZTIwMGExNmRkYmQ4MjEwMjIwYjBmZDMzNjU3ZWFhY2IwZDJhMzEyZmZiZGZiYTE3NjdmNDVjODc3ODhiYjU1NmVkMzRkNTQwNGI3ZGNhYWIyMTAzMmIzYWZlYTAzZTE4NWQ4NzUwNmI5YjBkYzI0YTU5YzQxMzgyMWEyYmZmZDM5YmRkZDMyZjcyYjUxOGU2ZjJhODUzYWVmZmZmZmZmZjQ3MWMyODYyOGU4MzlhYzQ4Y2MxZDg0OWM3ZTNjMjAyMDU1MThkMjg5NmVhYjAzOGE0ZDliZjJmNjBjNzcxMTA1NzAwMDAwMGZkZmUwMDAwNDgzMDQ1MDIyMTAwZGFiMTk5NGNiMTk0YmRlMGZkOTdmM2YxYmIxZGVmYzcyZTM0OGQ4YWI5NmQ3ZmRjMGJkODYwNTgxYWNjNDZlNjAyMjA0YzNjYTcwNzE0MDRjN2NkMWIwNTdmY2U3MWRkM2YzZDU1N2I4YTRkNGEzZTE4OThmMjFkNDNjMjI3YWFkNzEwNDE0ODMwNDUwMjIxMDA4MzNmYjI1M2NjOTRjODFkYmI5NzE5N2QxYzEwMDdmM2I3MjA3OTJjODY4NjYwN2VlZTc0MTdkZWE3ZTI3NTI2MDIyMDEwOWIyYjE0MzMyODdhZDIxMDZkYzFlODBlM2QwMTY4YWNlZWNjM2Q0NWVjMTVjNjRjMDIzNzQxMmUzZjMxY2U0MTRjNjk1MjIxMDNkOGVmZjI5Njk5OTQxMzYzMWZiYTJhY2JmYWEwZTFmMDYyM2FmOTNhYTgxZjdiMWI0ZmZmOGI1ZDkwZWFmOWY0MjEwM2VlODMwNDhmMWI2MWU3YTI0OGJiNDc3MjVjYjc0ZjJlOGI1MmViMzIwZDBmMDFkZGM0YWM2MTcxMzY2MDRiMzIyMTAyYWFkMmNmOWI2YzRiMTZjNTEwOWRlZTI5YzI4ZGEyY2RiNGI3ODkxMDZkNDZhMTA3MDNkNmVjMWU0NzY4NWY3ODUzYWVmZmZmZmZmZjAyMDA0YWMxYTQwNDAwMDAwMDE5NzZhOTE0NjE3OGI0OTUyZDFkZjA4MmI2MDJhYTBiZmQwZDAyNDFjNWI5NWYyMDg4YWMzMGFmY2FiNDE2MDAwMDAwMTdhOTE0NTY2MzFlNGMzYjNjOTBlMGQ0NWU4Njc0YmEzMjI2MjFjNjdhYTQ3NTg3MDAwMDAwMDAifSx7InR4aWQiOiJiM2U4NWMxZmFkZDY4MTgyNDZmMjU5MTA4MmViOGU4ZjA5YmZlODAzMzQ1Nzk3YTQyMDI1MmIyN2JiNTA5OWE4IiwiaGFzaCI6ImIzZTg1YzFmYWRkNjgxODI0NmYyNTkxMDgyZWI4ZThmMDliZmU4MDMzNDU3OTdhNDIwMjUyYjI3YmI1MDk5YTgiLCJ2ZXJzaW9uIjoyLCJzaXplIjoyNzM4LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiMjcyM2EyYmMzY2Y0OTdhMmZjMWY2MzYxMzQ2Yjc3NzllNTYyNzg5NGRmY2Q0OTY4ZDVhNzhhYzNlY2NkZDlkNCIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDUwMjIxMDBiNTMzZmEzYzhmNmMzMWU4MzczNzM3NDI5ZGU0ZjNiNDRkOTY0ZTZjZDkyODUyYjBiZDVlYzc2NmFiYWQzOGNiMDIyMDdmOTNmNDY3MWNhMmNjZWUxMTRlYzYxMDY2MTIwNjc4ODk2N2Y0YmY3ZmVjZTEyZmI1NDM5NTI4Y2UyYjc5OThbQUxMfEZPUktJRF0gMzA0NDAyMjAyNDdkMDNjYmQ3NjBkOWQ5ZmY4MTg1NGZlNDVlZDgyZjE3N2VmNjVjZGI3ZTIxMzAzMDlhZGNhYzA2OGE4NTRiMDIyMDYzNmM4NzQxNzUwNjVjODU3ODAyM2ZmYzA1OTVkMWE3YWRkNzQ3MWZkZWFmZTEzMmM3MjNlMWY4ZWE3OTJhZTRbQUxMfEZPUktJRF0gNTIyMTAyYTc5YzY1Y2U5ODliZmQzMDI2ZjYxZjA0Mjk2NjU1MzhkZDM4ZTg2ZGU3NzVhMmM0NDllMmM4NTFmMzY0NWRkZDIxMDNjODk3NDIxNTVhZjIwZGFmZGVjM2QwN2NhMjlmNTBiMWU5NDZjMzZiMWFjZjI2MWE4Y2VhNTRjMzAzYTFkNDc3MjEwM2YwZGZkODM4OWNiN2NkN2Y3ZmYyYTc1ZGQyZTBjMzkzNTMxN2U0YWYzMWI0ZjQyZWNlODBiZWU5YWIxMWE2NDM1M2FlIiwiaGV4IjoiMDA0ODMwNDUwMjIxMDBiNTMzZmEzYzhmNmMzMWU4MzczNzM3NDI5ZGU0ZjNiNDRkOTY0ZTZjZDkyODUyYjBiZDVlYzc2NmFiYWQzOGNiMDIyMDdmOTNmNDY3MWNhMmNjZWUxMTRlYzYxMDY2MTIwNjc4ODk2N2Y0YmY3ZmVjZTEyZmI1NDM5NTI4Y2UyYjc5OTg0MTQ3MzA0NDAyMjAyNDdkMDNjYmQ3NjBkOWQ5ZmY4MTg1NGZlNDVlZDgyZjE3N2VmNjVjZGI3ZTIxMzAzMDlhZGNhYzA2OGE4NTRiMDIyMDYzNmM4NzQxNzUwNjVjODU3ODAyM2ZmYzA1OTVkMWE3YWRkNzQ3MWZkZWFmZTEzMmM3MjNlMWY4ZWE3OTJhZTQ0MTRjNjk1MjIxMDJhNzljNjVjZTk4OWJmZDMwMjZmNjFmMDQyOTY2NTUzOGRkMzhlODZkZTc3NWEyYzQ0OWUyYzg1MWYzNjQ1ZGRkMjEwM2M4OTc0MjE1NWFmMjBkYWZkZWMzZDA3Y2EyOWY1MGIxZTk0NmMzNmIxYWNmMjYxYThjZWE1NGMzMDNhMWQ0NzcyMTAzZjBkZmQ4Mzg5Y2I3Y2Q3ZjdmZjJhNzVkZDJlMGMzOTM1MzE3ZTRhZjMxYjRmNDJlY2U4MGJlZTlhYjExYTY0MzUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9LHsidHhpZCI6IjJkZjk2MjBlZTE3YmE1ZjMzMzViNGY4ZTgzZTIyZjhkYjU5YmMxMzZmNDcwYzFiZGMwYTUxZTIwNmE2MTIyODgiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ1MDIyMTAwZjE4YWI4MDQ5YzMwZThhZmQyY2JhMTg3YTYzY2Q1MmQ4MDNhM2MyNzVhMDAwMTUzM2ZiYzExMTEzZTliYmVmNjAyMjAwYzA0ZDM2ZjJmMThkZjQ0ZWQ2MTU1YTc1Y2JmYmQ3NTI3MjY4YWI4ZDljZWFiZjMxNmUxODk3MGZhNjFjOWYyW0FMTHxGT1JLSURdIDMwNDUwMjIxMDA4MmVlZGZmOWFlNTFlYzgyODIyYmFiYzdlYzA0Y2ZhZWFlODQ5NTViMWJiN2ZlOWZkMGU2NWYwNjI5NTZmYWM1MDIyMDY0YjE1ZDhhMDA1OTJkYzdjNGFjYzJmNGYyM2QxZDkxZWJjZWMxNzk0NDJmM2MwNDE3NGJlNTBjNjZiMjczNWFbQUxMfEZPUktJRF0gNTIyMTAyYTc5YzY1Y2U5ODliZmQzMDI2ZjYxZjA0Mjk2NjU1MzhkZDM4ZTg2ZGU3NzVhMmM0NDllMmM4NTFmMzY0NWRkZDIxMDNjODk3NDIxNTVhZjIwZGFmZGVjM2QwN2NhMjlmNTBiMWU5NDZjMzZiMWFjZjI2MWE4Y2VhNTRjMzAzYTFkNDc3MjEwM2YwZGZkODM4OWNiN2NkN2Y3ZmYyYTc1ZGQyZTBjMzkzNTMxN2U0YWYzMWI0ZjQyZWNlODBiZWU5YWIxMWE2NDM1M2FlIiwiaGV4IjoiMDA0ODMwNDUwMjIxMDBmMThhYjgwNDljMzBlOGFmZDJjYmExODdhNjNjZDUyZDgwM2EzYzI3NWEwMDAxNTMzZmJjMTExMTNlOWJiZWY2MDIyMDBjMDRkMzZmMmYxOGRmNDRlZDYxNTVhNzVjYmZiZDc1MjcyNjhhYjhkOWNlYWJmMzE2ZTE4OTcwZmE2MWM5ZjI0MTQ4MzA0NTAyMjEwMDgyZWVkZmY5YWU1MWVjODI4MjJiYWJjN2VjMDRjZmFlYWU4NDk1NWIxYmI3ZmU5ZmQwZTY1ZjA2Mjk1NmZhYzUwMjIwNjRiMTVkOGEwMDU5MmRjN2M0YWNjMmY0ZjIzZDFkOTFlYmNlYzE3OTQ0MmYzYzA0MTc0YmU1MGM2NmIyNzM1YTQxNGM2OTUyMjEwMmE3OWM2NWNlOTg5YmZkMzAyNmY2MWYwNDI5NjY1NTM4ZGQzOGU4NmRlNzc1YTJjNDQ5ZTJjODUxZjM2NDVkZGQyMTAzYzg5NzQyMTU1YWYyMGRhZmRlYzNkMDdjYTI5ZjUwYjFlOTQ2YzM2YjFhY2YyNjFhOGNlYTU0YzMwM2ExZDQ3NzIxMDNmMGRmZDgzODljYjdjZDdmN2ZmMmE3NWRkMmUwYzM5MzUzMTdlNGFmMzFiNGY0MmVjZTgwYmVlOWFiMTFhNjQzNTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiNjJkYjA2ZTg3NWY3MjZkZTI5ZDFhNzI4MTQyODg0NTFhYzgwMDU0N2RhNzIyMDI2MjFhZjFjMGEyMGIzMzE2ZSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDUwMjIxMDBmMmY5YWEyMTU2MGIyMGUyZjVkMTU5YWJjODNlYzQ4NDUzMWJlMjY1N2ZmMDBiZGE4M2Q2ZjllMGNmNjk0YTZjMDIyMDMwZGEzZmU2ZWVlMjJjYjFjOWQzYmZjZDFkODEwYWE0NTJjOWMyNmVlMGVhODFiOTFiNDNiMjNlODU5NGE3ZmRbQUxMfEZPUktJRF0gMzA0NDAyMjA0Nzc1N2RkZTQ2MDhkZGY5MTRkNGIyMTZmMTFmMDA2ZmM5ZjhlYWM2ZmMwMDkyNzE4ODc3NGVlNmUzMjc0Y2EzMDIyMDUzNjAwZDU0YzNmNDE3MTU1NGI4NDkyYjliOWU4NzZhZDkzMWEyNThjYjkyMDRkMzRlZmJiNzRkZTYyODNmMmVbQUxMfEZPUktJRF0gNTIyMTAyNDZjZDI5YTNhYzVkNmM3Zjc5NGI3NjAxNGEyODU3ZTNmMmM0ZTZiOGI0ZTg1YTQ4NGEyMzRkODEyNzA1N2YzMDIxMDI5M2ExZjk1MjkzZDJhNDJiNjI4NTk3YjZjZGFmMmJmMDlhNGE5YjFiMGE5OWE4YTZjMzhlMmNlYmE4MmNkMzJkMjEwM2NiODJiNmVmMTA1OTZlMjhkZDk0N2IwNzY4MGU3ZTE3MjU3OGNjYTMyMTA5ODJjZTUzODFkZjFkODg4NjdkNTY1M2FlIiwiaGV4IjoiMDA0ODMwNDUwMjIxMDBmMmY5YWEyMTU2MGIyMGUyZjVkMTU5YWJjODNlYzQ4NDUzMWJlMjY1N2ZmMDBiZGE4M2Q2ZjllMGNmNjk0YTZjMDIyMDMwZGEzZmU2ZWVlMjJjYjFjOWQzYmZjZDFkODEwYWE0NTJjOWMyNmVlMGVhODFiOTFiNDNiMjNlODU5NGE3ZmQ0MTQ3MzA0NDAyMjA0Nzc1N2RkZTQ2MDhkZGY5MTRkNGIyMTZmMTFmMDA2ZmM5ZjhlYWM2ZmMwMDkyNzE4ODc3NGVlNmUzMjc0Y2EzMDIyMDUzNjAwZDU0YzNmNDE3MTU1NGI4NDkyYjliOWU4NzZhZDkzMWEyNThjYjkyMDRkMzRlZmJiNzRkZTYyODNmMmU0MTRjNjk1MjIxMDI0NmNkMjlhM2FjNWQ2YzdmNzk0Yjc2MDE0YTI4NTdlM2YyYzRlNmI4YjRlODVhNDg0YTIzNGQ4MTI3MDU3ZjMwMjEwMjkzYTFmOTUyOTNkMmE0MmI2Mjg1OTdiNmNkYWYyYmYwOWE0YTliMWIwYTk5YThhNmMzOGUyY2ViYTgyY2QzMmQyMTAzY2I4MmI2ZWYxMDU5NmUyOGRkOTQ3YjA3NjgwZTdlMTcyNTc4Y2NhMzIxMDk4MmNlNTM4MWRmMWQ4ODg2N2Q1NjUzYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9LHsidHhpZCI6IjYyZGIwNmU4NzVmNzI2ZGUyOWQxYTcyODE0Mjg4NDUxYWM4MDA1NDdkYTcyMjAyNjIxYWYxYzBhMjBiMzMxNmUiLCJ2b3V0IjoyLCJzY3JpcHRTaWciOnsiYXNtIjoiMCAzMDQ1MDIyMTAwZWRhMWU1MGNmMmE3YTRiYmM0NGUwMjNjNjI5MjI4ZjI0NmUyZWE2NGFhYWZlNzkzZTcxMTlkNzVkZDg0OGI5YjAyMjA2Mzc4NGM1NWI0NmIwNTk0NThkMjQ0OGQ5ODFkOTE0ZGJjMDlmYzc1MjQyMGUxMjFlNTI4ZmUxODE2NmYwZjljW0FMTHxGT1JLSURdIDMwNDUwMjIxMDA5YjMyYTg5YzQ0NjJjODlkZmM0OTIyZDk5NTIzOTk0ZmE5NjQ3MmRhYTY3ODY4YTA3NzY2MGU1NDRiODM0NmQ3MDIyMDIyOTdlMGQ4Nzg2ZDI3MzViMjAxMzY3NDZmYjJiYmMxYTY3ZjZmZjQ3NzJiY2ZlMjQ1NGY3MDgzOTJmZDNkNzdbQUxMfEZPUktJRF0gNTIyMTAyZjhhYWNkMTNlYjRiOWRhOWMzNWM2MGY5MjY2N2RjOTg0MGM4MTE4NzczNTE0MjllNmU4ZjZiYmFhY2VlNzMwOTIxMDNhNWVkMGNkNTk0ZWRmNzFiNjFlYmE3OTFiYmUwMTMyZDhlZDRkNTQ5YzY0ZTg3ZWNlYjkxMzZmNGVkMzk1NWYxMjEwMzEyOTIwYWFiZDhiNDQ0ZjVkNGFmMGE1YzgzNWNkYmNmNmIxMDE4ZjQyYjc0MmVjMjkzYTE0NTUxNmQzMDdkZDU1M2FlIiwiaGV4IjoiMDA0ODMwNDUwMjIxMDBlZGExZTUwY2YyYTdhNGJiYzQ0ZTAyM2M2MjkyMjhmMjQ2ZTJlYTY0YWFhZmU3OTNlNzExOWQ3NWRkODQ4YjliMDIyMDYzNzg0YzU1YjQ2YjA1OTQ1OGQyNDQ4ZDk4MWQ5MTRkYmMwOWZjNzUyNDIwZTEyMWU1MjhmZTE4MTY2ZjBmOWM0MTQ4MzA0NTAyMjEwMDliMzJhODljNDQ2MmM4OWRmYzQ5MjJkOTk1MjM5OTRmYTk2NDcyZGFhNjc4NjhhMDc3NjYwZTU0NGI4MzQ2ZDcwMjIwMjI5N2UwZDg3ODZkMjczNWIyMDEzNjc0NmZiMmJiYzFhNjdmNmZmNDc3MmJjZmUyNDU0ZjcwODM5MmZkM2Q3NzQxNGM2OTUyMjEwMmY4YWFjZDEzZWI0YjlkYTljMzVjNjBmOTI2NjdkYzk4NDBjODExODc3MzUxNDI5ZTZlOGY2YmJhYWNlZTczMDkyMTAzYTVlZDBjZDU5NGVkZjcxYjYxZWJhNzkxYmJlMDEzMmQ4ZWQ0ZDU0OWM2NGU4N2VjZWI5MTM2ZjRlZDM5NTVmMTIxMDMxMjkyMGFhYmQ4YjQ0NGY1ZDRhZjBhNWM4MzVjZGJjZjZiMTAxOGY0MmI3NDJlYzI5M2ExNDU1MTZkMzA3ZGQ1NTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiNjJkYjA2ZTg3NWY3MjZkZTI5ZDFhNzI4MTQyODg0NTFhYzgwMDU0N2RhNzIyMDI2MjFhZjFjMGEyMGIzMzE2ZSIsInZvdXQiOjMsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDQwMjIwMDljOTM3MTQ3MGZlNTIxZmVlOWYwMTRiY2UwZTQ1NmM4MTJiYTgxNmE4NmIxYjllZWYxOWFhYWI5MWM0Mjg3YzAyMjA3MzRkZmJlNzQwZGExZjQyYTRlZWFiMjA3MDIzZmIzNDViZTE4NmFkYTEyMmM5ZDg5NTZiOGFjMDNjY2I4ZjE2W0FMTHxGT1JLSURdIDMwNDQwMjIwMGQwODg3YTNmOTk5MmIxN2YxMTE5OWNkMjVjODVjNzJjZmZiZmM0ZDE5YWFiMDBmNDYwY2U5ZGE3ZTViOTNjYTAyMjA3MzVlM2E5OTdlZjMwNTRhYjA0NDVjNDE3NTZmYjdhMGUwNDU0MGZmZDM3YzdlM2JkY2M5NDJlNTRlNmEwMGM5W0FMTHxGT1JLSURdIDUyMjEwM2IzMmRhYzRjODQ3OGUzODExNjRmYWI4NjUzYWQyMjIwZjgwZWZkMTRkMWY1NDU0NjEwMTYwNGYzNzE0OGFkNjYyMTAyMmQxNzgyZGUxZmNlNWYxYTdlZDUxNDliZmNlZGMzMGRjYjg5MDA0ZTYzNTYyM2I5YmM2NjhiYmUxODFjNzBjODIxMDI0ZDA0YjFkNWRkNWNmYjY3NGUyMDQyODM4OGI5M2M1NTVmZGUyZDAzN2IxNGE1NGI0ZThjOTc4OTQ3OGU5Y2IwNTNhZSIsImhleCI6IjAwNDczMDQ0MDIyMDA5YzkzNzE0NzBmZTUyMWZlZTlmMDE0YmNlMGU0NTZjODEyYmE4MTZhODZiMWI5ZWVmMTlhYWFiOTFjNDI4N2MwMjIwNzM0ZGZiZTc0MGRhMWY0MmE0ZWVhYjIwNzAyM2ZiMzQ1YmUxODZhZGExMjJjOWQ4OTU2YjhhYzAzY2NiOGYxNjQxNDczMDQ0MDIyMDBkMDg4N2EzZjk5OTJiMTdmMTExOTljZDI1Yzg1YzcyY2ZmYmZjNGQxOWFhYjAwZjQ2MGNlOWRhN2U1YjkzY2EwMjIwNzM1ZTNhOTk3ZWYzMDU0YWIwNDQ1YzQxNzU2ZmI3YTBlMDQ1NDBmZmQzN2M3ZTNiZGNjOTQyZTU0ZTZhMDBjOTQxNGM2OTUyMjEwM2IzMmRhYzRjODQ3OGUzODExNjRmYWI4NjUzYWQyMjIwZjgwZWZkMTRkMWY1NDU0NjEwMTYwNGYzNzE0OGFkNjYyMTAyMmQxNzgyZGUxZmNlNWYxYTdlZDUxNDliZmNlZGMzMGRjYjg5MDA0ZTYzNTYyM2I5YmM2NjhiYmUxODFjNzBjODIxMDI0ZDA0YjFkNWRkNWNmYjY3NGUyMDQyODM4OGI5M2M1NTVmZGUyZDAzN2IxNGE1NGI0ZThjOTc4OTQ3OGU5Y2IwNTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiNjJkYjA2ZTg3NWY3MjZkZTI5ZDFhNzI4MTQyODg0NTFhYzgwMDU0N2RhNzIyMDI2MjFhZjFjMGEyMGIzMzE2ZSIsInZvdXQiOjQsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDUwMjIxMDBkYzNlZmJiN2U3ODBjNWQ5NzExZWZmMGQyZWE1NTQwYTEzM2RmMzBkZTVhYmM0ZWQwZGI4ZDFkMGM4YTg5NzM1MDIyMDZkMTI0MGExMjFmYWYzNjk2OTU0ZDFkZjUwMjAxNTJjZmQwZmVlNjlmYzhmMmFlZmRjYjFlODA1ZGJhMjdhZjVbQUxMfEZPUktJRF0gMzA0NTAyMjEwMGEyM2RmZTI0ZjE5ZTU2Njc2MjQzNWQ3ZWM4ZGI1ZmNhZTVjZTk2N2UyNzAzZDA4ZTI1ZWQwMTE1MmI3MWZkYjkwMjIwNDk0Mjc4MWExM2MwNzYwNjc3YmIxNjJkMDE5OTA4YTMxZWUyYTljYjcwYzE1ODQyNDE4ZmNjMDU2NTI2MmU0Y1tBTEx8Rk9SS0lEXSA1MjIxMDMxZDIyMGJjYTdkNjc3N2ZhMGQyZjFjOWE5NTc2YjM4M2NmN2NlYjNmMmRiZGEzMmNkYTJiMmE0NGVjNzUxMDY3MjEwMzgzNTQ2NzllZTUyNGQ3YTVjMWMxMDBmMWIzOWEwNDc3MzkzYzY1YzJjNDM1ODlhZWVhNGNjMmU4YmU5NTMyMDcyMTAyZTUyYzU5YzAyN2NlNWU1ZmQxMzJmODFmMTgxMGIxNGU3ODZlNzc1YjBiYzBkYzc3ZjhiOWQ5MjNiZDMxZWMxMTUzYWUiLCJoZXgiOiIwMDQ4MzA0NTAyMjEwMGRjM2VmYmI3ZTc4MGM1ZDk3MTFlZmYwZDJlYTU1NDBhMTMzZGYzMGRlNWFiYzRlZDBkYjhkMWQwYzhhODk3MzUwMjIwNmQxMjQwYTEyMWZhZjM2OTY5NTRkMWRmNTAyMDE1MmNmZDBmZWU2OWZjOGYyYWVmZGNiMWU4MDVkYmEyN2FmNTQxNDgzMDQ1MDIyMTAwYTIzZGZlMjRmMTllNTY2NzYyNDM1ZDdlYzhkYjVmY2FlNWNlOTY3ZTI3MDNkMDhlMjVlZDAxMTUyYjcxZmRiOTAyMjA0OTQyNzgxYTEzYzA3NjA2NzdiYjE2MmQwMTk5MDhhMzFlZTJhOWNiNzBjMTU4NDI0MThmY2MwNTY1MjYyZTRjNDE0YzY5NTIyMTAzMWQyMjBiY2E3ZDY3NzdmYTBkMmYxYzlhOTU3NmIzODNjZjdjZWIzZjJkYmRhMzJjZGEyYjJhNDRlYzc1MTA2NzIxMDM4MzU0Njc5ZWU1MjRkN2E1YzFjMTAwZjFiMzlhMDQ3NzM5M2M2NWMyYzQzNTg5YWVlYTRjYzJlOGJlOTUzMjA3MjEwMmU1MmM1OWMwMjdjZTVlNWZkMTMyZjgxZjE4MTBiMTRlNzg2ZTc3NWIwYmMwZGM3N2Y4YjlkOTIzYmQzMWVjMTE1M2FlIn0sInNlcXVlbmNlIjo0Mjk0OTY3MjkzfSx7InR4aWQiOiJmM2JiZWY1M2Y2YmQxODkzMmYyODE3ZGU1N2JjM2JkZGZhYmMwNmE3NGMwZWU1NGY3YWUwMDUyYTc2ODM4ZjkwIiwidm91dCI6Miwic2NyaXB0U2lnIjp7ImFzbSI6IjAgMzA0NDAyMjA0NzE3ZWYzNTJhZDY5ODMyOTFjNDBjMjFlYTk1MGU1NzQ5ODQ3Y2Q2ZjQyNWNhNjJjZjZjMjQ0YzI1ZDU5NTU0MDIyMDI0ZWNhNDFiZmY5MDcwMjk1OGEwMDM4ZWZmZTFlOGU0NzY1M2I4ZTMyZGMwN2MyYmZkOGY2MmNkODg0ODJiNTBbQUxMfEZPUktJRF0gMzA0NDAyMjA1N2UwMzFiNzUwODVjYzIwYWU0YTBjNjgwYzRhODU2NzAxOTkzM2FmZTQzZDI3YzdlNGRhZGRlYWNhZDQ4OTg4MDIyMDY3MzNkYWU5MzcwZTA1NGI1ZWY4MTFhODMxMzUxMzQyNzdhZjI1OGI0M2JjMWUwNGZkZGQyOWMwZmIwNDNmN2ZbQUxMfEZPUktJRF0gNTIyMTAyMjMyNWYzMGI1OGI3OTg2MmFkNzE1ZGRmNzEzMWE3ZTYwYzA4M2RkNjJkZmFhZTQ0MDYyMzhjZWNlZmViNGU2MDIxMDJhMWJiOGViNjhhMDZmYTBjYWY0OWQ4ZGZmZmQzODJjNTJlZDgzYzg2OGUwMWQ3Yjg4YWNjNGI2YTUzODBjNGI5MjEwM2Y4YzFhNGI1YWU1NDJlOGYzOTE5NDAyNTE4OGY0NzljODg1N2EyMmIwZWEwNzRhMzkwY2M5YjA2NzAxMTk2Yjk1M2FlIiwiaGV4IjoiMDA0NzMwNDQwMjIwNDcxN2VmMzUyYWQ2OTgzMjkxYzQwYzIxZWE5NTBlNTc0OTg0N2NkNmY0MjVjYTYyY2Y2YzI0NGMyNWQ1OTU1NDAyMjAyNGVjYTQxYmZmOTA3MDI5NThhMDAzOGVmZmUxZThlNDc2NTNiOGUzMmRjMDdjMmJmZDhmNjJjZDg4NDgyYjUwNDE0NzMwNDQwMjIwNTdlMDMxYjc1MDg1Y2MyMGFlNGEwYzY4MGM0YTg1NjcwMTk5MzNhZmU0M2QyN2M3ZTRkYWRkZWFjYWQ0ODk4ODAyMjA2NzMzZGFlOTM3MGUwNTRiNWVmODExYTgzMTM1MTM0Mjc3YWYyNThiNDNiYzFlMDRmZGRkMjljMGZiMDQzZjdmNDE0YzY5NTIyMTAyMjMyNWYzMGI1OGI3OTg2MmFkNzE1ZGRmNzEzMWE3ZTYwYzA4M2RkNjJkZmFhZTQ0MDYyMzhjZWNlZmViNGU2MDIxMDJhMWJiOGViNjhhMDZmYTBjYWY0OWQ4ZGZmZmQzODJjNTJlZDgzYzg2OGUwMWQ3Yjg4YWNjNGI2YTUzODBjNGI5MjEwM2Y4YzFhNGI1YWU1NDJlOGYzOTE5NDAyNTE4OGY0NzljODg1N2EyMmIwZWEwNzRhMzkwY2M5YjA2NzAxMTk2Yjk1M2FlIn0sInNlcXVlbmNlIjo0Mjk0OTY3MjkzfSx7InR4aWQiOiJmM2JiZWY1M2Y2YmQxODkzMmYyODE3ZGU1N2JjM2JkZGZhYmMwNmE3NGMwZWU1NGY3YWUwMDUyYTc2ODM4ZjkwIiwidm91dCI6Mywic2NyaXB0U2lnIjp7ImFzbSI6IjAgMzA0NTAyMjEwMGZmYTk3NmZiMmE4Y2QwNWFlY2U0NjIzZDBiMTRjYWE2ODUxNmU5OGIxYzY2Mzk5ZmI0YmYwMjMxNTJmNWQxMjMwMjIwMDdmY2VhMmU0M2U4MDFjZDM5MjQ2NmEzZDFiZmEzYjlmMTVjYjkxOTAyMzczNTlhY2NiN2VhOWY5N2MzNTA0ZltBTEx8Rk9SS0lEXSAzMDQ0MDIyMDU3ZjAzYzJjMGRhZDM5YTQ5M2MyNTQ3NTZkOWQ0NDk4MGI5MmVkYmU3OWZjODg1NDg1MDUzMWM0ODNlMTNjOTUwMjIwNTc4MGRkYTc1MjU0NTIxZDMwNDdmZjk5NmVmYzU5YzhjMjk4NDRhMDNhYzViMGQ4NjllNGM5YjZkYTViNWM0M1tBTEx8Rk9SS0lEXSA1MjIxMDNkMmQyN2FhMWFlZTc4Mjg5ODQyYThhY2U2ZjlhN2M3ZDc3MWYyMTNhMjAwZDc1YmRjNDQ4NzgyOGFkZGIyYTNhMjEwM2ViYTY3YTU5NWE4YjhiYjc5Yjk2ZGYyMmZkNDkxYjRhZWQ5MmVkZGFiNWE2MDM1MjEwZjY0MWYzNGNmZjZhYjYyMTAzYTYyYTFiOTkwNjkyNmViYmI3ZDkxZjNmN2MxYWUwNDEwNWMxNGE4ODNhYmE0MGVmZmMyNTBmMWEyZjIyYjJjODUzYWUiLCJoZXgiOiIwMDQ4MzA0NTAyMjEwMGZmYTk3NmZiMmE4Y2QwNWFlY2U0NjIzZDBiMTRjYWE2ODUxNmU5OGIxYzY2Mzk5ZmI0YmYwMjMxNTJmNWQxMjMwMjIwMDdmY2VhMmU0M2U4MDFjZDM5MjQ2NmEzZDFiZmEzYjlmMTVjYjkxOTAyMzczNTlhY2NiN2VhOWY5N2MzNTA0ZjQxNDczMDQ0MDIyMDU3ZjAzYzJjMGRhZDM5YTQ5M2MyNTQ3NTZkOWQ0NDk4MGI5MmVkYmU3OWZjODg1NDg1MDUzMWM0ODNlMTNjOTUwMjIwNTc4MGRkYTc1MjU0NTIxZDMwNDdmZjk5NmVmYzU5YzhjMjk4NDRhMDNhYzViMGQ4NjllNGM5YjZkYTViNWM0MzQxNGM2OTUyMjEwM2QyZDI3YWExYWVlNzgyODk4NDJhOGFjZTZmOWE3YzdkNzcxZjIxM2EyMDBkNzViZGM0NDg3ODI4YWRkYjJhM2EyMTAzZWJhNjdhNTk1YThiOGJiNzliOTZkZjIyZmQ0OTFiNGFlZDkyZWRkYWI1YTYwMzUyMTBmNjQxZjM0Y2ZmNmFiNjIxMDNhNjJhMWI5OTA2OTI2ZWJiYjdkOTFmM2Y3YzFhZTA0MTA1YzE0YTg4M2FiYTQwZWZmYzI1MGYxYTJmMjJiMmM4NTNhZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5M30seyJ0eGlkIjoiZjNiYmVmNTNmNmJkMTg5MzJmMjgxN2RlNTdiYzNiZGRmYWJjMDZhNzRjMGVlNTRmN2FlMDA1MmE3NjgzOGY5MCIsInZvdXQiOjQsInNjcmlwdFNpZyI6eyJhc20iOiIwIDMwNDUwMjIxMDBlMTVhZGVlOGEzNmE5ZmEzYzY2N2ZlYmRmNTQwZjFjMTc2MWUyMzI1NzkyNTgxMzM5MzFlZDFhZjc5MzI4MjU5MDIyMDFiMDJmOWZjMjIyMThjMTgxOTIwYzE3ZGU4Yjg2MWNjYTExY2U5MDk5MGNmOGQwZDlmMzU4YjExODVhMDM3ZmJbQUxMfEZPUktJRF0gMzA0NTAyMjEwMGNiOGI2MTg4NDdlZmFhMWI2NTJlNjVlM2M0YWRiZDJkODE0YjU4NTcxZmJjZjE2ZjA2YjkwZDE1Mzg1YjExODMwMjIwMTg2MzU2NjQ0YzYyNmMxZDhiMzQwMWZhNWM0OTkxZjc3NWYwMDc3ZjFlZWY0MTQyNDFlNDRhYzEwNzRmOWI5MltBTEx8Rk9SS0lEXSA1MjIxMDIxOTdkNDI3MzY5MzY0NzY5ZDMwMzEzYzA4NDU5NjE4YzI0OWNiZDU3NDA2MDdlYWYyZDUzYTRkOWU3YmRjZmZmMjEwMmE1MWNhMmJiOWU4YTQwYmU0OGI1YTUxZjU3OTY0MTliODk2NTYyMjIyMDY0ZWQ3NzRkN2NmZWJiZDg5MTNmOWMyMTAyZmVhNDc3MGFmMWE5MmQwOTFlMzUwYWY5ZDQ3NmE0YjZmNjY4ZTlmNTM1Nzc0YTM4MmZmYTkwODUzNTllMDMyYzUzYWUiLCJoZXgiOiIwMDQ4MzA0NTAyMjEwMGUxNWFkZWU4YTM2YTlmYTNjNjY3ZmViZGY1NDBmMWMxNzYxZTIzMjU3OTI1ODEzMzkzMWVkMWFmNzkzMjgyNTkwMjIwMWIwMmY5ZmMyMjIxOGMxODE5MjBjMTdkZThiODYxY2NhMTFjZTkwOTkwY2Y4ZDBkOWYzNThiMTE4NWEwMzdmYjQxNDgzMDQ1MDIyMTAwY2I4YjYxODg0N2VmYWExYjY1MmU2NWUzYzRhZGJkMmQ4MTRiNTg1NzFmYmNmMTZmMDZiOTBkMTUzODViMTE4MzAyMjAxODYzNTY2NDRjNjI2YzFkOGIzNDAxZmE1YzQ5OTFmNzc1ZjAwNzdmMWVlZjQxNDI0MWU0NGFjMTA3NGY5YjkyNDE0YzY5NTIyMTAyMTk3ZDQyNzM2OTM2NDc2OWQzMDMxM2MwODQ1OTYxOGMyNDljYmQ1NzQwNjA3ZWFmMmQ1M2E0ZDllN2JkY2ZmZjIxMDJhNTFjYTJiYjllOGE0MGJlNDhiNWE1MWY1Nzk2NDE5Yjg5NjU2MjIyMjA2NGVkNzc0ZDdjZmViYmQ4OTEzZjljMjEwMmZlYTQ3NzBhZjFhOTJkMDkxZTM1MGFmOWQ0NzZhNGI2ZjY2OGU5ZjUzNTc3NGEzODJmZmE5MDg1MzU5ZTAzMmM1M2FlIn0sInNlcXVlbmNlIjo0Mjk0OTY3MjkzfV0sInZvdXQiOlt7InZhbHVlIjoxNy44MDUzNTIwMSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gxNjAgN2RkZDZkODllMGM3MmRlN2MyMzUwMjI2ZTdhYWNhNzlmZTFjY2ZiMSBPUF9FUVVBTCIsImhleCI6ImE5MTQ3ZGRkNmQ4OWUwYzcyZGU3YzIzNTAyMjZlN2FhY2E3OWZlMWNjZmIxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwcDdhNm12ZnVycmptZTd6eDVwemRlYTJlZnVsdTh4MGt5c2Y0cTBsNWUiXX19LHsidmFsdWUiOjE3MC4yLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTY4ZWE1MjE5N2VjNzUxMGFmNTEzOGJhMGNlZjVjNTk3MmY1NWYzYiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGU2OGVhNTIxOTdlYzc1MTBhZjUxMzhiYTBjZWY1YzU5NzJmNTVmM2I4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJuZ2FmZnBqbGs4Mnk5MDJ5dXQ1cjgwdDN2aDlhMmw4dmU2enNqY2s4Il19fV0sImZlZSI6NS41MTZlLTA1LCJoZXgiOiIwMjAwMDAwMDA5ZDRkOWNkZWNjMzhhYTdkNTY4NDljZGRmOTQ3ODYyZTU3OTc3NmIzNDYxNjMxZmZjYTI5N2Y0M2NiY2EyMjMyNzAxMDAwMDAwZmRmZDAwMDA0ODMwNDUwMjIxMDBiNTMzZmEzYzhmNmMzMWU4MzczNzM3NDI5ZGU0ZjNiNDRkOTY0ZTZjZDkyODUyYjBiZDVlYzc2NmFiYWQzOGNiMDIyMDdmOTNmNDY3MWNhMmNjZWUxMTRlYzYxMDY2MTIwNjc4ODk2N2Y0YmY3ZmVjZTEyZmI1NDM5NTI4Y2UyYjc5OTg0MTQ3MzA0NDAyMjAyNDdkMDNjYmQ3NjBkOWQ5ZmY4MTg1NGZlNDVlZDgyZjE3N2VmNjVjZGI3ZTIxMzAzMDlhZGNhYzA2OGE4NTRiMDIyMDYzNmM4NzQxNzUwNjVjODU3ODAyM2ZmYzA1OTVkMWE3YWRkNzQ3MWZkZWFmZTEzMmM3MjNlMWY4ZWE3OTJhZTQ0MTRjNjk1MjIxMDJhNzljNjVjZTk4OWJmZDMwMjZmNjFmMDQyOTY2NTUzOGRkMzhlODZkZTc3NWEyYzQ0OWUyYzg1MWYzNjQ1ZGRkMjEwM2M4OTc0MjE1NWFmMjBkYWZkZWMzZDA3Y2EyOWY1MGIxZTk0NmMzNmIxYWNmMjYxYThjZWE1NGMzMDNhMWQ0NzcyMTAzZjBkZmQ4Mzg5Y2I3Y2Q3ZjdmZjJhNzVkZDJlMGMzOTM1MzE3ZTRhZjMxYjRmNDJlY2U4MGJlZTlhYjExYTY0MzUzYWVmZGZmZmZmZjg4MjI2MTZhMjAxZWE1YzBiZGMxNzBmNDM2YzE5YmI1OGQyZmUyODM4ZTRmNWIzM2YzYTU3YmUxMGU2MmY5MmQwMTAwMDAwMGZkZmUwMDAwNDgzMDQ1MDIyMTAwZjE4YWI4MDQ5YzMwZThhZmQyY2JhMTg3YTYzY2Q1MmQ4MDNhM2MyNzVhMDAwMTUzM2ZiYzExMTEzZTliYmVmNjAyMjAwYzA0ZDM2ZjJmMThkZjQ0ZWQ2MTU1YTc1Y2JmYmQ3NTI3MjY4YWI4ZDljZWFiZjMxNmUxODk3MGZhNjFjOWYyNDE0ODMwNDUwMjIxMDA4MmVlZGZmOWFlNTFlYzgyODIyYmFiYzdlYzA0Y2ZhZWFlODQ5NTViMWJiN2ZlOWZkMGU2NWYwNjI5NTZmYWM1MDIyMDY0YjE1ZDhhMDA1OTJkYzdjNGFjYzJmNGYyM2QxZDkxZWJjZWMxNzk0NDJmM2MwNDE3NGJlNTBjNjZiMjczNWE0MTRjNjk1MjIxMDJhNzljNjVjZTk4OWJmZDMwMjZmNjFmMDQyOTY2NTUzOGRkMzhlODZkZTc3NWEyYzQ0OWUyYzg1MWYzNjQ1ZGRkMjEwM2M4OTc0MjE1NWFmMjBkYWZkZWMzZDA3Y2EyOWY1MGIxZTk0NmMzNmIxYWNmMjYxYThjZWE1NGMzMDNhMWQ0NzcyMTAzZjBkZmQ4Mzg5Y2I3Y2Q3ZjdmZjJhNzVkZDJlMGMzOTM1MzE3ZTRhZjMxYjRmNDJlY2U4MGJlZTlhYjExYTY0MzUzYWVmZGZmZmZmZjZlMzFiMzIwMGExY2FmMjEyNjIwNzJkYTQ3MDU4MGFjNTE4NDI4MTQyOGE3ZDEyOWRlMjZmNzc1ZTgwNmRiNjIwMTAwMDAwMGZkZmQwMDAwNDgzMDQ1MDIyMTAwZjJmOWFhMjE1NjBiMjBlMmY1ZDE1OWFiYzgzZWM0ODQ1MzFiZTI2NTdmZjAwYmRhODNkNmY5ZTBjZjY5NGE2YzAyMjAzMGRhM2ZlNmVlZTIyY2IxYzlkM2JmY2QxZDgxMGFhNDUyYzljMjZlZTBlYTgxYjkxYjQzYjIzZTg1OTRhN2ZkNDE0NzMwNDQwMjIwNDc3NTdkZGU0NjA4ZGRmOTE0ZDRiMjE2ZjExZjAwNmZjOWY4ZWFjNmZjMDA5MjcxODg3NzRlZTZlMzI3NGNhMzAyMjA1MzYwMGQ1NGMzZjQxNzE1NTRiODQ5MmI5YjllODc2YWQ5MzFhMjU4Y2I5MjA0ZDM0ZWZiYjc0ZGU2MjgzZjJlNDE0YzY5NTIyMTAyNDZjZDI5YTNhYzVkNmM3Zjc5NGI3NjAxNGEyODU3ZTNmMmM0ZTZiOGI0ZTg1YTQ4NGEyMzRkODEyNzA1N2YzMDIxMDI5M2ExZjk1MjkzZDJhNDJiNjI4NTk3YjZjZGFmMmJmMDlhNGE5YjFiMGE5OWE4YTZjMzhlMmNlYmE4MmNkMzJkMjEwM2NiODJiNmVmMTA1OTZlMjhkZDk0N2IwNzY4MGU3ZTE3MjU3OGNjYTMyMTA5ODJjZTUzODFkZjFkODg4NjdkNTY1M2FlZmRmZmZmZmY2ZTMxYjMyMDBhMWNhZjIxMjYyMDcyZGE0NzA1ODBhYzUxODQyODE0MjhhN2QxMjlkZTI2Zjc3NWU4MDZkYjYyMDIwMDAwMDBmZGZlMDAwMDQ4MzA0NTAyMjEwMGVkYTFlNTBjZjJhN2E0YmJjNDRlMDIzYzYyOTIyOGYyNDZlMmVhNjRhYWFmZTc5M2U3MTE5ZDc1ZGQ4NDhiOWIwMjIwNjM3ODRjNTViNDZiMDU5NDU4ZDI0NDhkOTgxZDkxNGRiYzA5ZmM3NTI0MjBlMTIxZTUyOGZlMTgxNjZmMGY5YzQxNDgzMDQ1MDIyMTAwOWIzMmE4OWM0NDYyYzg5ZGZjNDkyMmQ5OTUyMzk5NGZhOTY0NzJkYWE2Nzg2OGEwNzc2NjBlNTQ0YjgzNDZkNzAyMjAyMjk3ZTBkODc4NmQyNzM1YjIwMTM2NzQ2ZmIyYmJjMWE2N2Y2ZmY0NzcyYmNmZTI0NTRmNzA4MzkyZmQzZDc3NDE0YzY5NTIyMTAyZjhhYWNkMTNlYjRiOWRhOWMzNWM2MGY5MjY2N2RjOTg0MGM4MTE4NzczNTE0MjllNmU4ZjZiYmFhY2VlNzMwOTIxMDNhNWVkMGNkNTk0ZWRmNzFiNjFlYmE3OTFiYmUwMTMyZDhlZDRkNTQ5YzY0ZTg3ZWNlYjkxMzZmNGVkMzk1NWYxMjEwMzEyOTIwYWFiZDhiNDQ0ZjVkNGFmMGE1YzgzNWNkYmNmNmIxMDE4ZjQyYjc0MmVjMjkzYTE0NTUxNmQzMDdkZDU1M2FlZmRmZmZmZmY2ZTMxYjMyMDBhMWNhZjIxMjYyMDcyZGE0NzA1ODBhYzUxODQyODE0MjhhN2QxMjlkZTI2Zjc3NWU4MDZkYjYyMDMwMDAwMDBmYzAwNDczMDQ0MDIyMDA5YzkzNzE0NzBmZTUyMWZlZTlmMDE0YmNlMGU0NTZjODEyYmE4MTZhODZiMWI5ZWVmMTlhYWFiOTFjNDI4N2MwMjIwNzM0ZGZiZTc0MGRhMWY0MmE0ZWVhYjIwNzAyM2ZiMzQ1YmUxODZhZGExMjJjOWQ4OTU2YjhhYzAzY2NiOGYxNjQxNDczMDQ0MDIyMDBkMDg4N2EzZjk5OTJiMTdmMTExOTljZDI1Yzg1YzcyY2ZmYmZjNGQxOWFhYjAwZjQ2MGNlOWRhN2U1YjkzY2EwMjIwNzM1ZTNhOTk3ZWYzMDU0YWIwNDQ1YzQxNzU2ZmI3YTBlMDQ1NDBmZmQzN2M3ZTNiZGNjOTQyZTU0ZTZhMDBjOTQxNGM2OTUyMjEwM2IzMmRhYzRjODQ3OGUzODExNjRmYWI4NjUzYWQyMjIwZjgwZWZkMTRkMWY1NDU0NjEwMTYwNGYzNzE0OGFkNjYyMTAyMmQxNzgyZGUxZmNlNWYxYTdlZDUxNDliZmNlZGMzMGRjYjg5MDA0ZTYzNTYyM2I5YmM2NjhiYmUxODFjNzBjODIxMDI0ZDA0YjFkNWRkNWNmYjY3NGUyMDQyODM4OGI5M2M1NTVmZGUyZDAzN2IxNGE1NGI0ZThjOTc4OTQ3OGU5Y2IwNTNhZWZkZmZmZmZmNmUzMWIzMjAwYTFjYWYyMTI2MjA3MmRhNDcwNTgwYWM1MTg0MjgxNDI4YTdkMTI5ZGUyNmY3NzVlODA2ZGI2MjA0MDAwMDAwZmRmZTAwMDA0ODMwNDUwMjIxMDBkYzNlZmJiN2U3ODBjNWQ5NzExZWZmMGQyZWE1NTQwYTEzM2RmMzBkZTVhYmM0ZWQwZGI4ZDFkMGM4YTg5NzM1MDIyMDZkMTI0MGExMjFmYWYzNjk2OTU0ZDFkZjUwMjAxNTJjZmQwZmVlNjlmYzhmMmFlZmRjYjFlODA1ZGJhMjdhZjU0MTQ4MzA0NTAyMjEwMGEyM2RmZTI0ZjE5ZTU2Njc2MjQzNWQ3ZWM4ZGI1ZmNhZTVjZTk2N2UyNzAzZDA4ZTI1ZWQwMTE1MmI3MWZkYjkwMjIwNDk0Mjc4MWExM2MwNzYwNjc3YmIxNjJkMDE5OTA4YTMxZWUyYTljYjcwYzE1ODQyNDE4ZmNjMDU2NTI2MmU0YzQxNGM2OTUyMjEwMzFkMjIwYmNhN2Q2Nzc3ZmEwZDJmMWM5YTk1NzZiMzgzY2Y3Y2ViM2YyZGJkYTMyY2RhMmIyYTQ0ZWM3NTEwNjcyMTAzODM1NDY3OWVlNTI0ZDdhNWMxYzEwMGYxYjM5YTA0NzczOTNjNjVjMmM0MzU4OWFlZWE0Y2MyZThiZTk1MzIwNzIxMDJlNTJjNTljMDI3Y2U1ZTVmZDEzMmY4MWYxODEwYjE0ZTc4NmU3NzViMGJjMGRjNzdmOGI5ZDkyM2JkMzFlYzExNTNhZWZkZmZmZmZmOTA4ZjgzNzYyYTA1ZTA3YTRmZTUwZTRjYTcwNmJjZmFkZDNiYmM1N2RlMTcyODJmOTMxOGJkZjY1M2VmYmJmMzAyMDAwMDAwZmMwMDQ3MzA0NDAyMjA0NzE3ZWYzNTJhZDY5ODMyOTFjNDBjMjFlYTk1MGU1NzQ5ODQ3Y2Q2ZjQyNWNhNjJjZjZjMjQ0YzI1ZDU5NTU0MDIyMDI0ZWNhNDFiZmY5MDcwMjk1OGEwMDM4ZWZmZTFlOGU0NzY1M2I4ZTMyZGMwN2MyYmZkOGY2MmNkODg0ODJiNTA0MTQ3MzA0NDAyMjA1N2UwMzFiNzUwODVjYzIwYWU0YTBjNjgwYzRhODU2NzAxOTkzM2FmZTQzZDI3YzdlNGRhZGRlYWNhZDQ4OTg4MDIyMDY3MzNkYWU5MzcwZTA1NGI1ZWY4MTFhODMxMzUxMzQyNzdhZjI1OGI0M2JjMWUwNGZkZGQyOWMwZmIwNDNmN2Y0MTRjNjk1MjIxMDIyMzI1ZjMwYjU4Yjc5ODYyYWQ3MTVkZGY3MTMxYTdlNjBjMDgzZGQ2MmRmYWFlNDQwNjIzOGNlY2VmZWI0ZTYwMjEwMmExYmI4ZWI2OGEwNmZhMGNhZjQ5ZDhkZmZmZDM4MmM1MmVkODNjODY4ZTAxZDdiODhhY2M0YjZhNTM4MGM0YjkyMTAzZjhjMWE0YjVhZTU0MmU4ZjM5MTk0MDI1MTg4ZjQ3OWM4ODU3YTIyYjBlYTA3NGEzOTBjYzliMDY3MDExOTZiOTUzYWVmZGZmZmZmZjkwOGY4Mzc2MmEwNWUwN2E0ZmU1MGU0Y2E3MDZiY2ZhZGQzYmJjNTdkZTE3MjgyZjkzMThiZGY2NTNlZmJiZjMwMzAwMDAwMGZkZmQwMDAwNDgzMDQ1MDIyMTAwZmZhOTc2ZmIyYThjZDA1YWVjZTQ2MjNkMGIxNGNhYTY4NTE2ZTk4YjFjNjYzOTlmYjRiZjAyMzE1MmY1ZDEyMzAyMjAwN2ZjZWEyZTQzZTgwMWNkMzkyNDY2YTNkMWJmYTNiOWYxNWNiOTE5MDIzNzM1OWFjY2I3ZWE5Zjk3YzM1MDRmNDE0NzMwNDQwMjIwNTdmMDNjMmMwZGFkMzlhNDkzYzI1NDc1NmQ5ZDQ0OTgwYjkyZWRiZTc5ZmM4ODU0ODUwNTMxYzQ4M2UxM2M5NTAyMjA1NzgwZGRhNzUyNTQ1MjFkMzA0N2ZmOTk2ZWZjNTljOGMyOTg0NGEwM2FjNWIwZDg2OWU0YzliNmRhNWI1YzQzNDE0YzY5NTIyMTAzZDJkMjdhYTFhZWU3ODI4OTg0MmE4YWNlNmY5YTdjN2Q3NzFmMjEzYTIwMGQ3NWJkYzQ0ODc4MjhhZGRiMmEzYTIxMDNlYmE2N2E1OTVhOGI4YmI3OWI5NmRmMjJmZDQ5MWI0YWVkOTJlZGRhYjVhNjAzNTIxMGY2NDFmMzRjZmY2YWI2MjEwM2E2MmExYjk5MDY5MjZlYmJiN2Q5MWYzZjdjMWFlMDQxMDVjMTRhODgzYWJhNDBlZmZjMjUwZjFhMmYyMmIyYzg1M2FlZmRmZmZmZmY5MDhmODM3NjJhMDVlMDdhNGZlNTBlNGNhNzA2YmNmYWRkM2JiYzU3ZGUxNzI4MmY5MzE4YmRmNjUzZWZiYmYzMDQwMDAwMDBmZGZlMDAwMDQ4MzA0NTAyMjEwMGUxNWFkZWU4YTM2YTlmYTNjNjY3ZmViZGY1NDBmMWMxNzYxZTIzMjU3OTI1ODEzMzkzMWVkMWFmNzkzMjgyNTkwMjIwMWIwMmY5ZmMyMjIxOGMxODE5MjBjMTdkZThiODYxY2NhMTFjZTkwOTkwY2Y4ZDBkOWYzNThiMTE4NWEwMzdmYjQxNDgzMDQ1MDIyMTAwY2I4YjYxODg0N2VmYWExYjY1MmU2NWUzYzRhZGJkMmQ4MTRiNTg1NzFmYmNmMTZmMDZiOTBkMTUzODViMTE4MzAyMjAxODYzNTY2NDRjNjI2YzFkOGIzNDAxZmE1YzQ5OTFmNzc1ZjAwNzdmMWVlZjQxNDI0MWU0NGFjMTA3NGY5YjkyNDE0YzY5NTIyMTAyMTk3ZDQyNzM2OTM2NDc2OWQzMDMxM2MwODQ1OTYxOGMyNDljYmQ1NzQwNjA3ZWFmMmQ1M2E0ZDllN2JkY2ZmZjIxMDJhNTFjYTJiYjllOGE0MGJlNDhiNWE1MWY1Nzk2NDE5Yjg5NjU2MjIyMjA2NGVkNzc0ZDdjZmViYmQ4OTEzZjljMjEwMmZlYTQ3NzBhZjFhOTJkMDkxZTM1MGFmOWQ0NzZhNGI2ZjY2OGU5ZjUzNTc3NGEzODJmZmE5MDg1MzU5ZTAzMmM1M2FlZmRmZmZmZmYwMmExY2YyMDZhMDAwMDAwMDAxN2E5MTQ3ZGRkNmQ4OWUwYzcyZGU3YzIzNTAyMjZlN2FhY2E3OWZlMWNjZmIxODcwMDk3NzhmNjAzMDAwMDAwMTk3NmE5MTRlNjhlYTUyMTk3ZWM3NTEwYWY1MTM4YmEwY2VmNWM1OTcyZjU1ZjNiODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiYjRmNjdiMzBmMmI2NTc4OGUyZTc4MDU5ZWRmZTI3NWE2YjgwN2ZkNWZhMzUzYmYxNDNkYzNlOWFlMWU1YzU3OCIsImhhc2giOiJiNGY2N2IzMGYyYjY1Nzg4ZTJlNzgwNTllZGZlMjc1YTZiODA3ZmQ1ZmEzNTNiZjE0M2RjM2U5YWUxZTVjNTc4IiwidmVyc2lvbiI6MSwic2l6ZSI6NjM1LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiMjZhNzFmNjFiYzE0Zjk2ZWY1MDdmYmY4MTAzNTljMTFhNmQ4ZmRkYzk5MGZhYTYzYjUxNTM4YzFlYTczZTdhMSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwOTcyNzYyYWQ5ZjhmYTljYWIyMzcxOWJiMTExNjNlODE2MzBkNDhhNDFhOGZiYzhhYzM3YzBkNWNjYWE3NGQzYjAyMjA3YmJkNGFkZmNhM2ViMWM4ZTIxNjhjNjI1NDY4ZDRhN2U5MGRmYmJlZjcwMTA0YmExNmJlNTUyMmQxMDBiZjBkW0FMTHxGT1JLSURdIDAyZmE4YmRjMjhlNDNjYjJmMDY5MTY4NDE4OGE4MjlmZWI1ZDk1MGI0ZTE1ZWYyODcyZjViOGE2YTc3NjJmZTFhOCIsImhleCI6IjQ4MzA0NTAyMjEwMDk3Mjc2MmFkOWY4ZmE5Y2FiMjM3MTliYjExMTYzZTgxNjMwZDQ4YTQxYThmYmM4YWMzN2MwZDVjY2FhNzRkM2IwMjIwN2JiZDRhZGZjYTNlYjFjOGUyMTY4YzYyNTQ2OGQ0YTdlOTBkZmJiZWY3MDEwNGJhMTZiZTU1MjJkMTAwYmYwZDQxMjEwMmZhOGJkYzI4ZTQzY2IyZjA2OTE2ODQxODhhODI5ZmViNWQ5NTBiNGUxNWVmMjg3MmY1YjhhNmE3NzYyZmUxYTgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjRjZTBiY2Y2OWI0NWJjZGU5ZjVmYmFhNTRiNjkwNTUxNWU2MWI5ZmM4NGEyMGFlNzIyZWFkYWIxNDM0Mjc3YzgiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMDljOTJlMmMzMDBhNzdmNzI5YzRlZmVjMjhlZjJjZjY2YzlmN2M4YzZiMzg3NGQ4OThiOGU4YzM3MGVhMmEwMjUwMjIwNzAyODFjMGNkNDdjZTA5YjIwZTNlN2Y5NGIyMjAxZjVjMGU4YTI0NTE3MmUxMWNmMjAzMzQwNzZlMDg3YWZjOVtBTEx8Rk9SS0lEXSAwMmI5M2ZkMTkxZjY2Nzg1YjM0Mzc4NzdlYWFjMTExMzFkMjE0ZDZmZjYyMzNlMjMxMjAyN2JmZjRjYmU0OWFiYjQiLCJoZXgiOiI0ODMwNDUwMjIxMDA5YzkyZTJjMzAwYTc3ZjcyOWM0ZWZlYzI4ZWYyY2Y2NmM5ZjdjOGM2YjM4NzRkODk4YjhlOGMzNzBlYTJhMDI1MDIyMDcwMjgxYzBjZDQ3Y2UwOWIyMGUzZTdmOTRiMjIwMWY1YzBlOGEyNDUxNzJlMTFjZjIwMzM0MDc2ZTA4N2FmYzk0MTIxMDJiOTNmZDE5MWY2Njc4NWIzNDM3ODc3ZWFhYzExMTMxZDIxNGQ2ZmY2MjMzZTIzMTIwMjdiZmY0Y2JlNDlhYmI0In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIyMmUyZTA0YzUwYmIyYTA4MDJmZjllNmNiZGRjOWUzNDZmYWExZjY4NGQ5MDFjYjY2NjVjZDFmOGUxNDc2YWNlIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDA4NmY4ZTA5ZjFmZjU4MGU1MjhiOTYyOGYzZTA4ZWQ5NmZlODYyYTNlYTcxNTc4MjUyN2FlYTk5OGQ5Yzk4ZTE2MDIyMDM3YmM2OTgyNjJiZmQ3NWIzZTYwMWU0MGE2Y2FjZTQ2MDVmMDdjNmU2YWIwMDU4MDBmODFjYjI0OWVkMjc5YjNbQUxMfEZPUktJRF0gMDI4ZDYzNTAzYjRiNWEzMzFiOTBlYTVmYzkwY2JkYWVkNThjZWUwYWIwODA4OGJhYWE5MjRiNjJkNzA3NGZlYzIzIiwiaGV4IjoiNDgzMDQ1MDIyMTAwODZmOGUwOWYxZmY1ODBlNTI4Yjk2MjhmM2UwOGVkOTZmZTg2MmEzZWE3MTU3ODI1MjdhZWE5OThkOWM5OGUxNjAyMjAzN2JjNjk4MjYyYmZkNzViM2U2MDFlNDBhNmNhY2U0NjA1ZjA3YzZlNmFiMDA1ODAwZjgxY2IyNDllZDI3OWIzNDEyMTAyOGQ2MzUwM2I0YjVhMzMxYjkwZWE1ZmM5MGNiZGFlZDU4Y2VlMGFiMDgwODhiYWFhOTI0YjYyZDcwNzRmZWMyMyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMzZiNWQ2MGQ2YjkxMzlkOWIwZDdmZDg1ZDM1NTMxMzFiN2I1OGQ5MWIwYTM2NmE2NDQ3ODIwYWE5ZmYxMDhjNiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDQ0YzgxYmVmMTZkODllODc1NTAzYWI2MzQyMTMwY2NiYjRjOTNjMDNmOWMwOTU2YmU0ODliODIzY2M4NTQ5YjQwMjIwN2VhZTQ0MDRmMjUzZGNlMTU4MDJkMzllOTM0ZTE4MjcyNzdhYTc1MGQ3MjQ2NWU5ZjlkNjlkZmViODJkYjM4NltBTEx8Rk9SS0lEXSAwM2FkN2VhNTQ2Mjc0MTljY2NhMmEzZGIzZTE3ZGJmYWNkMTE2OTQwYTc0OWU5YjMyOWJlYTViZGJkN2NhNmM0YzQiLCJoZXgiOiI0NzMwNDQwMjIwNDRjODFiZWYxNmQ4OWU4NzU1MDNhYjYzNDIxMzBjY2JiNGM5M2MwM2Y5YzA5NTZiZTQ4OWI4MjNjYzg1NDliNDAyMjA3ZWFlNDQwNGYyNTNkY2UxNTgwMmQzOWU5MzRlMTgyNzI3N2FhNzUwZDcyNDY1ZTlmOWQ2OWRmZWI4MmRiMzg2NDEyMTAzYWQ3ZWE1NDYyNzQxOWNjY2EyYTNkYjNlMTdkYmZhY2QxMTY5NDBhNzQ5ZTliMzI5YmVhNWJkYmQ3Y2E2YzRjNCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wOTkzMzAwNSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDcxYjFmZGE3NmI5MzhlMjVlZDI2NDYyYWRkMzk2YjFmNWMwZGI5YjIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3MWIxZmRhNzZiOTM4ZTI1ZWQyNjQ2MmFkZDM5NmIxZjVjMGRiOWIyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwY21ybGQ4ZHdmY3VmMGR5ZXJ6NGhmZWR2MDRjcmRla2cyNmV6NDRrbSJdfX1dLCJmZWUiOjEuNDU3ZS0wNSwiaGV4IjoiMDEwMDAwMDAwNGExZTc3M2VhYzEzODE1YjU2M2FhMGY5OWRjZmRkOGE2MTE5YzM1MTBmOGZiMDdmNTZlZjkxNGJjNjExZmE3MjYwMTAwMDAwMDZiNDgzMDQ1MDIyMTAwOTcyNzYyYWQ5ZjhmYTljYWIyMzcxOWJiMTExNjNlODE2MzBkNDhhNDFhOGZiYzhhYzM3YzBkNWNjYWE3NGQzYjAyMjA3YmJkNGFkZmNhM2ViMWM4ZTIxNjhjNjI1NDY4ZDRhN2U5MGRmYmJlZjcwMTA0YmExNmJlNTUyMmQxMDBiZjBkNDEyMTAyZmE4YmRjMjhlNDNjYjJmMDY5MTY4NDE4OGE4MjlmZWI1ZDk1MGI0ZTE1ZWYyODcyZjViOGE2YTc3NjJmZTFhOGZmZmZmZmZmYzg3NzQyNDNiMWRhZWEyMmU3MGFhMjg0ZmNiOTYxNWU1MTA1Njk0YmE1YmE1ZjlmZGViYzQ1OWJmNmJjZTA0YzAxMDAwMDAwNmI0ODMwNDUwMjIxMDA5YzkyZTJjMzAwYTc3ZjcyOWM0ZWZlYzI4ZWYyY2Y2NmM5ZjdjOGM2YjM4NzRkODk4YjhlOGMzNzBlYTJhMDI1MDIyMDcwMjgxYzBjZDQ3Y2UwOWIyMGUzZTdmOTRiMjIwMWY1YzBlOGEyNDUxNzJlMTFjZjIwMzM0MDc2ZTA4N2FmYzk0MTIxMDJiOTNmZDE5MWY2Njc4NWIzNDM3ODc3ZWFhYzExMTMxZDIxNGQ2ZmY2MjMzZTIzMTIwMjdiZmY0Y2JlNDlhYmI0ZmZmZmZmZmZjZTZhNDdlMWY4ZDE1YzY2YjYxYzkwNGQ2ODFmYWE2ZjM0OWVkY2JkNmM5ZWZmMDIwODJhYmI1MDRjZTBlMjIyMDAwMDAwMDA2YjQ4MzA0NTAyMjEwMDg2ZjhlMDlmMWZmNTgwZTUyOGI5NjI4ZjNlMDhlZDk2ZmU4NjJhM2VhNzE1NzgyNTI3YWVhOTk4ZDljOThlMTYwMjIwMzdiYzY5ODI2MmJmZDc1YjNlNjAxZTQwYTZjYWNlNDYwNWYwN2M2ZTZhYjAwNTgwMGY4MWNiMjQ5ZWQyNzliMzQxMjEwMjhkNjM1MDNiNGI1YTMzMWI5MGVhNWZjOTBjYmRhZWQ1OGNlZTBhYjA4MDg4YmFhYTkyNGI2MmQ3MDc0ZmVjMjNmZmZmZmZmZmM2MDhmMTlmYWEyMDc4NDRhNjY2YTNiMDkxOGRiNWI3MzEzMTU1ZDM4NWZkZDdiMGQ5Mzk5MTZiMGRkNmI1MzYwMDAwMDAwMDZhNDczMDQ0MDIyMDQ0YzgxYmVmMTZkODllODc1NTAzYWI2MzQyMTMwY2NiYjRjOTNjMDNmOWMwOTU2YmU0ODliODIzY2M4NTQ5YjQwMjIwN2VhZTQ0MDRmMjUzZGNlMTU4MDJkMzllOTM0ZTE4MjcyNzdhYTc1MGQ3MjQ2NWU5ZjlkNjlkZmViODJkYjM4NjQxMjEwM2FkN2VhNTQ2Mjc0MTljY2NhMmEzZGIzZTE3ZGJmYWNkMTE2OTQwYTc0OWU5YjMyOWJlYTViZGJkN2NhNmM0YzRmZmZmZmZmZjAxY2Q5MDk3MDAwMDAwMDAwMDE5NzZhOTE0NzFiMWZkYTc2YjkzOGUyNWVkMjY0NjJhZGQzOTZiMWY1YzBkYjliMjg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6ImI2NTM3NGJlZDNlMGUzMjY5ZDUwNGU3ZWUxNmMzOWUwN2NkNmYzOWVkYjkyMzQxNjE0YWQ1NGIxZGI3ZDQ5NzciLCJoYXNoIjoiYjY1Mzc0YmVkM2UwZTMyNjlkNTA0ZTdlZTE2YzM5ZTA3Y2Q2ZjM5ZWRiOTIzNDE2MTRhZDU0YjFkYjdkNDk3NyIsInZlcnNpb24iOjEsInNpemUiOjM3NCwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImI5ZDQ4NGIwOTBkMmU2MzU0YjBjMWZiYjQ0MGRlNTMwZTYwNzMwZDZiOWMyYjYzMzlkYjU1NTc3MTkwYmM2YzQiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMGY5NjMzODllOWIzZjYyMzI0NTkyZjc1M2Q1NWY2MTU3YTk5MTM5ZTY4N2RmM2FhOTg1ZWM4ZjEzNTQ3MzY3NDkwMjIwMzQ5MTFhZWQxMzJkZTUyYTJmOGQyMTM4MWI0NzIzODhhZWNlYTI5MzQyODVkYWYwMmFmMDc2M2NkOTcwOWZlNFtBTEx8Rk9SS0lEXSAwMjM5NGVkODE1OGJkY2FmZjMzOTY4YzQxMzE5MTcyZjk5ODVlNmYxOTIzMzQ3MzMwMmI4MDQ1MDEzNDY0MDFjNjYiLCJoZXgiOiI0ODMwNDUwMjIxMDBmOTYzMzg5ZTliM2Y2MjMyNDU5MmY3NTNkNTVmNjE1N2E5OTEzOWU2ODdkZjNhYTk4NWVjOGYxMzU0NzM2NzQ5MDIyMDM0OTExYWVkMTMyZGU1MmEyZjhkMjEzODFiNDcyMzg4YWVjZWEyOTM0Mjg1ZGFmMDJhZjA3NjNjZDk3MDlmZTQ0MTIxMDIzOTRlZDgxNThiZGNhZmYzMzk2OGM0MTMxOTE3MmY5OTg1ZTZmMTkyMzM0NzMzMDJiODA0NTAxMzQ2NDAxYzY2In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJiZjg2MGQzZDMxMjVjZjYxNTc2ZWEyZTIyMjI4YjRjNDYzYWVmMWFkNGZiMGZkMWRlMTUxNWMyZWEwYzdhOTIwIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDA5MTZhNGRjNzdmYTNiNTIzMjZhMWRlOTg4MDVlMGE1MmRiZjdhYjMxMzY5Zjk4Yzc4NjdhOTZhZTk0ZTdhNTFiMDIyMDZkYTMzMGE5OGExYzFlY2RhNDYwZGUyNWE5MDM5MThmYjZkM2M5NWM0ZmIwZjZkMDRhODU5ZDg1N2IwMDc2MTlbQUxMfEZPUktJRF0gMDIzOTRlZDgxNThiZGNhZmYzMzk2OGM0MTMxOTE3MmY5OTg1ZTZmMTkyMzM0NzMzMDJiODA0NTAxMzQ2NDAxYzY2IiwiaGV4IjoiNDgzMDQ1MDIyMTAwOTE2YTRkYzc3ZmEzYjUyMzI2YTFkZTk4ODA1ZTBhNTJkYmY3YWIzMTM2OWY5OGM3ODY3YTk2YWU5NGU3YTUxYjAyMjA2ZGEzMzBhOThhMWMxZWNkYTQ2MGRlMjVhOTAzOTE4ZmI2ZDNjOTVjNGZiMGY2ZDA0YTg1OWQ4NTdiMDA3NjE5NDEyMTAyMzk0ZWQ4MTU4YmRjYWZmMzM5NjhjNDEzMTkxNzJmOTk4NWU2ZjE5MjMzNDczMzAyYjgwNDUwMTM0NjQwMWM2NiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMDA4Mzk5NSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGQwMDk3NDAyMmYxNTQ4YTUyMjNlMTBlYTRlYmJhZWFmNDY0YWNmNGUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkMDA5NzQwMjJmMTU0OGE1MjIzZTEwZWE0ZWJiYWVhZjQ2NGFjZjRlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyZ3FqYXF6OXUyNTNmZno4Y2d3NW40bTQ2aDV2amswZmNjeG4zYWFqeCJdfX0seyJ2YWx1ZSI6MC4zNjUsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3MDAxOTRkMGFhMjI0ODYyZWYwNTQ4ZTJkMGY3NjYzNGEwMGI5MjM4IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NzAwMTk0ZDBhYTIyNDg2MmVmMDU0OGUyZDBmNzY2MzRhMDBiOTIzODg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGNxcjl4czRnM3lzY2gwcTR5dzk1OGh2YzYycXp1ajhxZzVuM25qaDMiXX19XSwiZmVlIjowLjAwMDI4MDUsImhleCI6IjAxMDAwMDAwMDJjNGM2MGIxOTc3NTViNTlkMzNiNmMyYjlkNjMwMDdlNjMwZTUwZDQ0YmIxZjBjNGIzNWU2ZDI5MGIwODRkNGI5MDAwMDAwMDA2YjQ4MzA0NTAyMjEwMGY5NjMzODllOWIzZjYyMzI0NTkyZjc1M2Q1NWY2MTU3YTk5MTM5ZTY4N2RmM2FhOTg1ZWM4ZjEzNTQ3MzY3NDkwMjIwMzQ5MTFhZWQxMzJkZTUyYTJmOGQyMTM4MWI0NzIzODhhZWNlYTI5MzQyODVkYWYwMmFmMDc2M2NkOTcwOWZlNDQxMjEwMjM5NGVkODE1OGJkY2FmZjMzOTY4YzQxMzE5MTcyZjk5ODVlNmYxOTIzMzQ3MzMwMmI4MDQ1MDEzNDY0MDFjNjZmZmZmZmZmZjIwYTljN2EwMmU1YzUxZTExZGZkYjA0ZmFkZjFhZTYzYzRiNDI4MjJlMmEyNmU1NzYxY2YyNTMxM2QwZDg2YmYwMDAwMDAwMDZiNDgzMDQ1MDIyMTAwOTE2YTRkYzc3ZmEzYjUyMzI2YTFkZTk4ODA1ZTBhNTJkYmY3YWIzMTM2OWY5OGM3ODY3YTk2YWU5NGU3YTUxYjAyMjA2ZGEzMzBhOThhMWMxZWNkYTQ2MGRlMjVhOTAzOTE4ZmI2ZDNjOTVjNGZiMGY2ZDA0YTg1OWQ4NTdiMDA3NjE5NDEyMTAyMzk0ZWQ4MTU4YmRjYWZmMzM5NjhjNDEzMTkxNzJmOTk4NWU2ZjE5MjMzNDczMzAyYjgwNDUwMTM0NjQwMWM2NmZmZmZmZmZmMDIxYjQ4MDEwMDAwMDAwMDAwMTk3NmE5MTRkMDA5NzQwMjJmMTU0OGE1MjIzZTEwZWE0ZWJiYWVhZjQ2NGFjZjRlODhhYzIwZjIyYzAyMDAwMDAwMDAxOTc2YTkxNDcwMDE5NGQwYWEyMjQ4NjJlZjA1NDhlMmQwZjc2NjM0YTAwYjkyMzg4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJiYWYxNTlmZWRlNWY1NDMzMTA3NjA0MDRiZmI0Y2Y5MGQzMWQ5MDExZDVlNjZkYjdlYzZmOWM5Y2U1NjRmNTZlIiwiaGFzaCI6ImJhZjE1OWZlZGU1ZjU0MzMxMDc2MDQwNGJmYjRjZjkwZDMxZDkwMTFkNWU2NmRiN2VjNmY5YzljZTU2NGY1NmUiLCJ2ZXJzaW9uIjoxLCJzaXplIjoyMjYsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiIzZDQyMTE5NTI1NzhmMTYyNmE3MjI3NWIzNmYzNmNlMzM3ZGU0NzUwYTYxODZmNjdiMTkwNGMwOTJjMWIzM2MxIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBhNzY0N2ViNWZkMjBjYzFmOTIyNTJjOGMxYzFkYTIxMzA2OWRmOTRmNzcyYjQxM2M4ZGQwYWM2MzllNzU2MWQyMDIyMDFmNzE0MTBkNDlkNjQ1OGQ4MjFkZWY3ZjZmNjg5NjgyODExYjA1MGU3MjlkNjczNDg5OTZlOGJlOTg0MzJkMWJbQUxMfEZPUktJRF0gMDM5M2FiYzU0MWNlOWUzYThiOWVjMDVjNDgxOWFjNjIxZTQwZDQ2ZDZmMWY3NmYzMzE4OTRlZTYxYWM2MzQwMTc3IiwiaGV4IjoiNDgzMDQ1MDIyMTAwYTc2NDdlYjVmZDIwY2MxZjkyMjUyYzhjMWMxZGEyMTMwNjlkZjk0Zjc3MmI0MTNjOGRkMGFjNjM5ZTc1NjFkMjAyMjAxZjcxNDEwZDQ5ZDY0NThkODIxZGVmN2Y2ZjY4OTY4MjgxMWIwNTBlNzI5ZDY3MzQ4OTk2ZThiZTk4NDMyZDFiNDEyMTAzOTNhYmM1NDFjZTllM2E4YjllYzA1YzQ4MTlhYzYyMWU0MGQ0NmQ2ZjFmNzZmMzMxODk0ZWU2MWFjNjM0MDE3NyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4zODgyNTg0NSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGQyYzZjNTkzNjY1MzIwY2JhMGE1M2U3ZmUyMDcxZDM3NmY5NzA5ODcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkMmM2YzU5MzY2NTMyMGNiYTBhNTNlN2ZlMjA3MWQzNzZmOTcwOTg3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyZnZkM3ZudmVmanBqYXE1NWw4bGNzOHI1bWtsOWNmc3V4cjI4aGNtbiJdfX0seyJ2YWx1ZSI6MC4xMjA0NzI0MSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDEzYTNiZjQ1ODQ5MjNjMWVlMTE3YjQ0NjVmNDEyMjAyZTUwMzZlYTggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxM2EzYmY0NTg0OTIzYzFlZTExN2I0NDY1ZjQxMjIwMmU1MDM2ZWE4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZjY4MDY5c2pmcmM4aHB6NzZ5dmg2cHlncHcycW13NHFoZDZ0YTZqcyJdfX1dLCJmZWUiOjIuMjZlLTA2LCJoZXgiOiIwMTAwMDAwMDAxYzEzMzFiMmMwOTRjOTBiMTY3NmYxOGE2NTA0N2RlMzdlMzZjZjMzNjViMjc3MjZhNjJmMTc4MjU5NTExNDIzZDAwMDAwMDAwNmI0ODMwNDUwMjIxMDBhNzY0N2ViNWZkMjBjYzFmOTIyNTJjOGMxYzFkYTIxMzA2OWRmOTRmNzcyYjQxM2M4ZGQwYWM2MzllNzU2MWQyMDIyMDFmNzE0MTBkNDlkNjQ1OGQ4MjFkZWY3ZjZmNjg5NjgyODExYjA1MGU3MjlkNjczNDg5OTZlOGJlOTg0MzJkMWI0MTIxMDM5M2FiYzU0MWNlOWUzYThiOWVjMDVjNDgxOWFjNjIxZTQwZDQ2ZDZmMWY3NmYzMzE4OTRlZTYxYWM2MzQwMTc3ZmZmZmZmZmYwMjc1NmY1MDAyMDAwMDAwMDAxOTc2YTkxNGQyYzZjNTkzNjY1MzIwY2JhMGE1M2U3ZmUyMDcxZDM3NmY5NzA5ODc4OGFjODlkM2I3MDAwMDAwMDAwMDE5NzZhOTE0MTNhM2JmNDU4NDkyM2MxZWUxMTdiNDQ2NWY0MTIyMDJlNTAzNmVhODg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6ImJjNTNmMDczZGRmN2U1ZDBjYzkxODllNTg1YzE5YTA4NWU2ZTdlOTAzNGVlNDM2YmY0NWUwNGY4MTAwZDg5MjYiLCJoYXNoIjoiYmM1M2YwNzNkZGY3ZTVkMGNjOTE4OWU1ODVjMTlhMDg1ZTZlN2U5MDM0ZWU0MzZiZjQ1ZTA0ZjgxMDBkODkyNiIsInZlcnNpb24iOjIsInNpemUiOjIyNSwibG9ja3RpbWUiOjk0MDg4MywidmluIjpbeyJ0eGlkIjoiZThjOTI3NTlmNjQzNTdlMjg4MGJjZGZjNmZjNTcwNGVmZjZkZTljMzRiOGQ4MGU2Nzc0NWRlNmZhNWYzOThjZSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDcwNmU3MTNjY2VmMTViM2NlMWExMThlNjI4YTRmNGI1OWEwODVmNzY5NTI1ZTI0M2ZiZjNkZDIwMmNmMmUyZTcwMjIwNzA0ZTJmODRiYjU1ZGViYzZmM2QxMzk0Y2RhNzFkYzU3Yjc1MjViYzBmMGYyZTJjMmQ2MjUxZmY0ZDk2YzkxYltBTEx8Rk9SS0lEXSAwMjFjOTNiNGFkZjBiZDQzMTY1NmY0YTliNWM4NGE3NDdmMWJjMWI0MDI2NzA4ZjJlZjU4MzNlMDgwODNkZjNkYzEiLCJoZXgiOiI0NzMwNDQwMjIwNzA2ZTcxM2NjZWYxNWIzY2UxYTExOGU2MjhhNGY0YjU5YTA4NWY3Njk1MjVlMjQzZmJmM2RkMjAyY2YyZTJlNzAyMjA3MDRlMmY4NGJiNTVkZWJjNmYzZDEzOTRjZGE3MWRjNTdiNzUyNWJjMGYwZjJlMmMyZDYyNTFmZjRkOTZjOTFiNDEyMTAyMWM5M2I0YWRmMGJkNDMxNjU2ZjRhOWI1Yzg0YTc0N2YxYmMxYjQwMjY3MDhmMmVmNTgzM2UwODA4M2RmM2RjMSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMTU2NDg4NSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGY3ZDBkNjcwNzQwMjU0OTZlM2M5Y2JiYzE4Zjc3ZWMxNzYzMDc4MDQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmN2QwZDY3MDc0MDI1NDk2ZTNjOWNiYmMxOGY3N2VjMTc2MzA3ODA0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFybWFwNG5zd3NwOWY5aHJlODltY3g4aDBtcWh2dnJjcXNuN2dscjZtaiJdfX0seyJ2YWx1ZSI6My4xMTQzNDUyMiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDQ4MTY1NWFkMWFmNDBjM2UxNjIxMWM5MzExNzcyZDM1NDg4ZDZiNTcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0ODE2NTVhZDFhZjQwYzNlMTYyMTFjOTMxMTc3MmQzNTQ4OGQ2YjU3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFweXB2NGRkcnQ2cWMwc2t5eXdmeHl0aDk1NjUzcnR0MnV3dTJxMmRuZCJdfX1dLCJmZWUiOjIuMjVlLTA2LCJoZXgiOiIwMjAwMDAwMDAxY2U5OGYzYTU2ZmRlNDU3N2U2ODA4ZDRiYzNlOTZkZmY0ZTcwYzU2ZmZjY2QwYjg4ZTI1NzQzZjY1OTI3YzllODAwMDAwMDAwNmE0NzMwNDQwMjIwNzA2ZTcxM2NjZWYxNWIzY2UxYTExOGU2MjhhNGY0YjU5YTA4NWY3Njk1MjVlMjQzZmJmM2RkMjAyY2YyZTJlNzAyMjA3MDRlMmY4NGJiNTVkZWJjNmYzZDEzOTRjZGE3MWRjNTdiNzUyNWJjMGYwZjJlMmMyZDYyNTFmZjRkOTZjOTFiNDEyMTAyMWM5M2I0YWRmMGJkNDMxNjU2ZjRhOWI1Yzg0YTc0N2YxYmMxYjQwMjY3MDhmMmVmNTgzM2UwODA4M2RmM2RjMWZlZmZmZmZmMDJkNWUwMTcwMDAwMDAwMDAwMTk3NmE5MTRmN2QwZDY3MDc0MDI1NDk2ZTNjOWNiYmMxOGY3N2VjMTc2MzA3ODA0ODhhYzFhMWQ5MDEyMDAwMDAwMDAxOTc2YTkxNDQ4MTY1NWFkMWFmNDBjM2UxNjIxMWM5MzExNzcyZDM1NDg4ZDZiNTc4OGFjNTM1YjBlMDAifSx7InR4aWQiOiJiZDNlMDBjYmRlODc4MWM2N2FhMzM2YWM0MzQ2Y2I5MmY4MWQ3ZDY4ODk3YmIxMTcxN2FmMDg1NTBlOTg2NTA1IiwiaGFzaCI6ImJkM2UwMGNiZGU4NzgxYzY3YWEzMzZhYzQzNDZjYjkyZjgxZDdkNjg4OTdiYjExNzE3YWYwODU1MGU5ODY1MDUiLCJ2ZXJzaW9uIjoyLCJzaXplIjoyMjUsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiIzYzg5NTQwMDU4NWRlMGUyNTcyMzU0N2M1NGJjOTE1NmQyZDk5ZTBkYTc4NTM4NjlmMzEwMWJiYmJlMjJhZjYzIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwMjA0ZGUyYWZlNGRiZTliNGJlNWQxZWU5NmM1OWYwM2RjMzBjMTgzMzQwOTc2YjFhNjJiNjVlZGVlNDE5M2FmNTAyMjAwN2UwYjExYTkxYTE0ZDIwZGM1YjM1OTE5YjUyN2Q3ZTBhNWZiZWM5NTgwNjljZTk2MmMwMjY0MDQxMGJlZTUzW0FMTHxGT1JLSURdIDAyZjdlNmQyZmE0ZGMyYTE5MjNhNjBkNzgyNjRjZWM3NjVkOGQzNDRjMzRjNzFlOWNhOWY1YzFmYWQ2N2M5YjBkYiIsImhleCI6IjQ3MzA0NDAyMjAyMDRkZTJhZmU0ZGJlOWI0YmU1ZDFlZTk2YzU5ZjAzZGMzMGMxODMzNDA5NzZiMWE2MmI2NWVkZWU0MTkzYWY1MDIyMDA3ZTBiMTFhOTFhMTRkMjBkYzViMzU5MTliNTI3ZDdlMGE1ZmJlYzk1ODA2OWNlOTYyYzAyNjQwNDEwYmVlNTM0MTIxMDJmN2U2ZDJmYTRkYzJhMTkyM2E2MGQ3ODI2NGNlYzc2NWQ4ZDM0NGMzNGM3MWU5Y2E5ZjVjMWZhZDY3YzliMGRiIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjA5ODEzNDY2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNzM5NjdkY2IzOWY5YTNmZTEwYzIyMGU5YmU2OGNhNjU4MmE4NzAxYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDczOTY3ZGNiMzlmOWEzZmUxMGMyMjBlOWJlNjhjYTY1ODJhODcwMWM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBlZXZsd3Q4OHU2OGxzc2Nnc3duMG5nZWZqYzkycnNyczl5ZHd3cmplIl19fSx7InZhbHVlIjowLjExNDgzMjA2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgM2U4Y2UyOTI3YWZmZGUyMjBmMDNkMzkzOTQ5MTVkMTFjMjA3NTk2YiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDNlOGNlMjkyN2FmZmRlMjIwZjAzZDM5Mzk0OTE1ZDExYzIwNzU5NmI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFsZ2VjNWowdGxhdWdzMHEwZmU4OXkzdDVndXlwNmVkdm5sZWZlcGR5Il19fV0sImZlZSI6Mi4yOGUtMDYsImhleCI6IjAyMDAwMDAwMDE2M2FmMjJiZWJiMWIxMGYzNjkzODg1YTcwZDllZDlkMjU2OTFiYzU0N2M1NDIzNTdlMmUwNWQ1ODAwNTQ4OTNjMDEwMDAwMDA2YTQ3MzA0NDAyMjAyMDRkZTJhZmU0ZGJlOWI0YmU1ZDFlZTk2YzU5ZjAzZGMzMGMxODMzNDA5NzZiMWE2MmI2NWVkZWU0MTkzYWY1MDIyMDA3ZTBiMTFhOTFhMTRkMjBkYzViMzU5MTliNTI3ZDdlMGE1ZmJlYzk1ODA2OWNlOTYyYzAyNjQwNDEwYmVlNTM0MTIxMDJmN2U2ZDJmYTRkYzJhMTkyM2E2MGQ3ODI2NGNlYzc2NWQ4ZDM0NGMzNGM3MWU5Y2E5ZjVjMWZhZDY3YzliMGRiZmZmZmZmZmYwMmRhYmQ5NTAwMDAwMDAwMDAxOTc2YTkxNDczOTY3ZGNiMzlmOWEzZmUxMGMyMjBlOWJlNjhjYTY1ODJhODcwMWM4OGFjNDYzOGFmMDAwMDAwMDAwMDE5NzZhOTE0M2U4Y2UyOTI3YWZmZGUyMjBmMDNkMzkzOTQ5MTVkMTFjMjA3NTk2Yjg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6ImM0MjRkYmY0Y2E2NjhiMDMzMDAzMTJiNjlkZGE0YmE3OGI2M2E3MjRkZDUzNzliYmZiOWI2NjJjYjJkNzA0MjYiLCJoYXNoIjoiYzQyNGRiZjRjYTY2OGIwMzMwMDMxMmI2OWRkYTRiYTc4YjYzYTcyNGRkNTM3OWJiZmI5YjY2MmNiMmQ3MDQyNiIsInZlcnNpb24iOjEsInNpemUiOjI1OSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImJjNDAzYmUxODQ2ZGFlMDAzZTdiMGExYzFjNzFjMTE5ZGM1OGEzMjc3MGUxNjQzYjVjMWY4N2Q3MDQzYjg5YzEiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAwYjAyZTIyNjMyODZlZjdiZTY5MDg5MDAwYWY2MDFmZGUwMWMwNWZiZDY4OTFiZGE2NTljMWRmMTJlMGNhZDVlMDIyMDQyNzdiNWE2MDZlMGI0ZTBiMmJjYTQwYTIxMjdlMWJmZGUwNThlM2Q0OWJlYzY0N2U3YTllYjJiZGU5NzYzY2JbQUxMfEZPUktJRF0gMDIwOGM2YjZkOTdjYTNkNjI1ZmJhNTZiNjQ2MGJkY2NlYjNlMDZmNTU1NWMxMTg0NTkwYjMzMzljMGY1ODc5YmUxIiwiaGV4IjoiNDczMDQ0MDIyMDBiMDJlMjI2MzI4NmVmN2JlNjkwODkwMDBhZjYwMWZkZTAxYzA1ZmJkNjg5MWJkYTY1OWMxZGYxMmUwY2FkNWUwMjIwNDI3N2I1YTYwNmUwYjRlMGIyYmNhNDBhMjEyN2UxYmZkZTA1OGUzZDQ5YmVjNjQ3ZTdhOWViMmJkZTk3NjNjYjQxMjEwMjA4YzZiNmQ5N2NhM2Q2MjVmYmE1NmI2NDYwYmRjY2ViM2UwNmY1NTU1YzExODQ1OTBiMzMzOWMwZjU4NzliZTEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuOTA0MjY5NTcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5MGYyMjg1Mzk1MTk5MzE1NDBmMGUxZGUyNWRiYzAwOTM0OThjN2E1IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemcweTJ6bmo1dmV4OTJxN3JzYXVmd21jcXluZnh4ODU1ZDg2NmN5NnQiXX19LHsidmFsdWUiOjAuMTA0NzcwNDYsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAzOWM0M2VhZTBiOTdkYmUxMGZjZGQ5NGEwMzQ4MzhmMTAyNTBmMjExIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MzljNDNlYWUwYjk3ZGJlMTBmY2RkOTRhMDM0ODM4ZjEwMjUwZjIxMTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXV1ZzA0d3B3dGFoY2cwZWh2NTVxNmc4cmNzeTU4anp5Y2RqbXByMzYiXX19LHsidmFsdWUiOjAuMDYwNzUwMTIsIm4iOjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBkOTczYTJhMDE3M2I0YzlkNDRiNjlkMjk1ZDQyMDE5YTA3YjBhZWVkIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZDk3M2EyYTAxNzNiNGM5ZDQ0YjY5ZDI5NWQ0MjAxOWEwN2IwYWVlZDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcnZoOGc0cXp1YTVlODJ5azZ3ampoMnpxeGRxMHY5d2E1cmxxcjM3cG0iXX19XSwiZmVlIjo1LjIyZS0wNiwiaGV4IjoiMDEwMDAwMDAwMWMxODkzYjA0ZDc4NzFmNWMzYjY0ZTE3MDI3YTM1OGRjMTljMTcxMWMxYzBhN2IzZTAwYWU2ZDg0ZTEzYjQwYmMwMDAwMDAwMDZhNDczMDQ0MDIyMDBiMDJlMjI2MzI4NmVmN2JlNjkwODkwMDBhZjYwMWZkZTAxYzA1ZmJkNjg5MWJkYTY1OWMxZGYxMmUwY2FkNWUwMjIwNDI3N2I1YTYwNmUwYjRlMGIyYmNhNDBhMjEyN2UxYmZkZTA1OGUzZDQ5YmVjNjQ3ZTdhOWViMmJkZTk3NjNjYjQxMjEwMjA4YzZiNmQ5N2NhM2Q2MjVmYmE1NmI2NDYwYmRjY2ViM2UwNmY1NTU1YzExODQ1OTBiMzMzOWMwZjU4NzliZTFmZmZmZmZmZjAzNGRjZTYzMDUwMDAwMDAwMDE5NzZhOTE0OTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNTg4YWNmNmRkOWYwMDAwMDAwMDAwMTk3NmE5MTQzOWM0M2VhZTBiOTdkYmUxMGZjZGQ5NGEwMzQ4MzhmMTAyNTBmMjExODhhYzg0YjI1YzAwMDAwMDAwMDAxOTc2YTkxNGQ5NzNhMmEwMTczYjRjOWQ0NGI2OWQyOTVkNDIwMTlhMDdiMGFlZWQ4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJjNGI3MjZmM2M4MjdiNzQ0NjVjNDFmNDFjMzBiN2FkM2UzMjkyZjViZmI3MDEyMTcxZTc0ZWNjYTUwYzU3YjJjIiwiaGFzaCI6ImM0YjcyNmYzYzgyN2I3NDQ2NWM0MWY0MWMzMGI3YWQzZTMyOTJmNWJmYjcwMTIxNzFlNzRlY2NhNTBjNTdiMmMiLCJ2ZXJzaW9uIjoyLCJzaXplIjoyMTM0LCJsb2NrdGltZSI6NDIsInZpbiI6W3sidHhpZCI6ImNkY2RmNmQzMTI2OGI3MTg4YTg0ZWQ4Y2MyMmRmNWNjNGFmYmNlZDA2NGZmNTFhOTNmYTkxMTg1OWQ3Mzg2NjEiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNGRlN2FlN2IxMGQ2ZDZkOWJiYzAxNzBhYjJjMmVkNDQ1MGVkNGNiMTg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNGRlN2FlN2IxMGQ2ZDZkOWJiYzAxNzBhYjJjMmVkNDQ1MGVkNGNiMTg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NzFhNzE0OWVlMGI0ZTE0OGQ3N2FhZGI0YzBlNzFhMTJmMGYzODJhOTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NzFhNzE0OWVlMGI0ZTE0OGQ3N2FhZGI0YzBlNzFhMTJmMGYzODJhOTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjZGNkZjZkMzEyNjhiNzE4OGE4NGVkOGNjMjJkZjVjYzRhZmJjZWQwNjRmZjUxYTkzZmE5MTE4NTlkNzM4NjYxIiwidm91dCI6Miwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRmZTgyN2M2NTM4MzIwNzM5NzhiN2JkNThlZWM4NDBlZTQ0YmM2NzA4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRmZTgyN2M2NTM4MzIwNzM5NzhiN2JkNThlZWM4NDBlZTQ0YmM2NzA4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImNkY2RmNmQzMTI2OGI3MTg4YTg0ZWQ4Y2MyMmRmNWNjNGFmYmNlZDA2NGZmNTFhOTNmYTkxMTg1OWQ3Mzg2NjEiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDFiMjEzNDliODAxOWVjNzM4YThkYzZmMmZjNDJkYWNiYjA2NDQxZDA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDFiMjEzNDliODAxOWVjNzM4YThkYzZmMmZjNDJkYWNiYjA2NDQxZDA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOjQsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0MmExOGVmMDZiMWI3MWIyZTM3OTdhOTZhY2U5MGEwNGI5YjI4YzEyYjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0MmExOGVmMDZiMWI3MWIyZTM3OTdhOTZhY2U5MGEwNGI5YjI4YzEyYjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjZGNkZjZkMzEyNjhiNzE4OGE4NGVkOGNjMjJkZjVjYzRhZmJjZWQwNjRmZjUxYTkzZmE5MTE4NTlkNzM4NjYxIiwidm91dCI6MTQsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZDA3MzZlZTkwMTE3ZjRlZDdlOWJhZDVhZTA5YjBhYjBjODkyOTZiNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZDA3MzZlZTkwMTE3ZjRlZDdlOWJhZDVhZTA5YjBhYjBjODkyOTZiNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjZGNkZjZkMzEyNjhiNzE4OGE4NGVkOGNjMjJkZjVjYzRhZmJjZWQwNjRmZjUxYTkzZmE5MTE4NTlkNzM4NjYxIiwidm91dCI6MTgsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0Y2Q2ZDU2N2RjNjM0YWM4ZmE4ODU2MWU4N2IyOGY5MTE4YThhZGE3YTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0Y2Q2ZDU2N2RjNjM0YWM4ZmE4ODU2MWU4N2IyOGY5MTE4YThhZGE3YTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjZGNkZjZkMzEyNjhiNzE4OGE4NGVkOGNjMjJkZjVjYzRhZmJjZWQwNjRmZjUxYTkzZmE5MTE4NTlkNzM4NjYxIiwidm91dCI6MjQsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0YTczZDcwODllNDhhYmYzN2I0ZGIxNWNmZWI0YmQ0NTdiNWExMDgxZjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0YTczZDcwODllNDhhYmYzN2I0ZGIxNWNmZWI0YmQ0NTdiNWExMDgxZjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjZGNkZjZkMzEyNjhiNzE4OGE4NGVkOGNjMjJkZjVjYzRhZmJjZWQwNjRmZjUxYTkzZmE5MTE4NTlkNzM4NjYxIiwidm91dCI6MzIsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWI4Y2Y5YWI3YjgzNTRhZTQ5MGQzYTk2Y2M4Y2U0MTE4Nzg3ZmNmMjQwNTZkOGY0NGNiOGFkZWNmYWUxYjNhIiwidm91dCI6NTQsInNjcmlwdFNpZyI6eyJhc20iOiIwMDJlZDkxODc1ZjI2MjhiYzYwOTcxZjY0ZjNiODc1YjFkNmM3M2FjMmFiZWI3NDM3MDBmNTUxOTE5OGUyOTE4NDE5NWQ0YzNhOTk4YWNhZWFhNjRmMGMzMTQ4ZmYyMDc3ODE5MjZiYTgyOWMwNGZmYWU4MTQxNjQ0MTFhMTYxMFtBTEx8Rk9SS0lEfFVUWE9TXSAwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjMiLCJoZXgiOiI0MTAwMmVkOTE4NzVmMjYyOGJjNjA5NzFmNjRmM2I4NzViMWQ2YzczYWMyYWJlYjc0MzcwMGY1NTE5MTk4ZTI5MTg0MTk1ZDRjM2E5OThhY2FlYWE2NGYwYzMxNDhmZjIwNzc4MTkyNmJhODI5YzA0ZmZhZTgxNDE2NDQxMWExNjEwNjEyMTAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2MyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOjczLCJzY3JpcHRTaWciOnsiYXNtIjoiNTQ2ZGQxNGUyYTZhYjBjY2M2NTk3NmIzN2JhYTgxMTVmODEzNTY2Mjc5YzNhOWM1MjU0MTRmYWY2MGE3ZTdhOGQ1YzVhMGY4YTBhZjQwN2Q4MjQ1MDRkNThiYTM3ZTQyZTM4NjUyNjI0NzhiODliYjAzMmMzOTFiYjFiY2ZmMmFbQUxMfEZPUktJRHxVVFhPU10gMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzIiwiaGV4IjoiNDE1NDZkZDE0ZTJhNmFiMGNjYzY1OTc2YjM3YmFhODExNWY4MTM1NjYyNzljM2E5YzUyNTQxNGZhZjYwYTdlN2E4ZDVjNWEwZjhhMGFmNDA3ZDgyNDUwNGQ1OGJhMzdlNDJlMzg2NTI2MjQ3OGI4OWJiMDMyYzM5MWJiMWJjZmYyYTYxMjEwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjEwMy4yODIyMzM3MSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NCBPUF9FUVVBTCIsImhleCI6ImFhMjA1MGZjMTZmZmIzNzdjZDRlMWQ4YjRkZWZhMzc1NmQxMjIxYmFjZmUzMzA0ZTc3MDVkZTQxNjdkNzdjMWJhMzY0ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGcwYzlobGtkbXU2bnNhM2R4N2xnbTRkNWZ6cndrMHV2Y3l1YWM5bWVxazA0bXVydzNrZ3loZjVtbjd4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDg3OTI2OSJ9fSx7InZhbHVlIjo5Mi4wNDExMTI5NCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMCBPUF9FUVVBTCIsImhleCI6ImFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjB3ZzlwMGx5NXRmMm5mOXY3NzNoM2Y5eGxyanB3eHFmN25oMjlzNXQ4M2N6NjVzN2t0cWFocjl5YWdkIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDM0ODIxNCJ9fSx7InZhbHVlIjo3MS4wMTUzMDYyMiwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNiBPUF9FUVVBTCIsImhleCI6ImFhMjAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmswbjA2ZDR2ejZ5ZGZwbXNuemp3cHN5Y2hsYTNtcGdlNWdweDlteHlqMmMwYWh2c3FydmRmeGN2N3lrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzM1NDkxMiJ9fSx7InZhbHVlIjozNS45ODgyOTA5NCwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTcwMDE2MyJ9fSx7InZhbHVlIjoxNy40NjQ3MDYxNCwibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZCBPUF9FUVVBTCIsImhleCI6ImFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnN0cWttNTRkdHZucHlxeHQ1bTVuN3Nqc240ZW5ybHhjNTI2eHl4bG5qa2F5Y2R6ZmV1NjlyZXl6bXF4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODI1MDY5In19LHsidmFsdWUiOjExLjMyNTcwNTg1LCJuIjo1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBiMzQ3ZDY4YzhkMmNkYjMyMDY2YmU5YWY4MDRjNmZmYjhhYmJiNDQxYmRmMzZjYWYzODM0MzZiY2ZjYWEzNWNiIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGIzNDdkNjhjOGQyY2RiMzIwNjZiZTlhZjgwNGM2ZmZiOGFiYmI0NDFiZGYzNmNhZjM4MzQzNmJjZmNhYTM1Y2I4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3ZTUwNDV2MzVrZGt2c3hkMDU2bHF6dmRsYWM0d2E1Z3g3bHhtOTA4cTZyZDA4dTRnNnVrbjM3Mmdnd2wiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI1MzUwNTEifX0seyJ2YWx1ZSI6MTAuNTIzMTEzMiwibiI6Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2MyYmNjYTIwODlhOWYwZjhkYWVjMGQzYjQ1YTZjNTI1ODFjNTY3NjY1Y2M1MWViZGE5YTIwMmQ4YTMzZGNmYyBPUF9FUVVBTCIsImhleCI6ImFhMjA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZDd6aG45enB6ZGY3cnVkNG1xZDhkejZkM2Y5czh6a3dlanVjNTB0bTJkenF0djJ4MHcwYzU0eTM5aHA2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDk3MTM1In19LHsidmFsdWUiOjUuMjA2NTEyMDEsIm4iOjcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwZjFjMzNiYzdkYTA5OTFkMjZmOGJiZjE1MGM5MDM1NjhiMmMzZGM4NzI3MzcwODYzZjRjM2Q5Zjk2NTViMzc5MTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cDBjdXh3NzhtZ3llcjVuMDN3bDMycnlzeDQ1dDlzN3VzdW5ud3pycjducGFuN3Q5dHZtZXpyYXUzanVyOSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjI0NTk2NyJ9fSx7InZhbHVlIjo0LjM5MzgyMzM0LCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YW15bmxlcHYzcG5nbnE2cmhxNTk1eDBsZm5nNncwd3A4NWpxM3JydjJ3amNsdmh0M3VneDRrYWo0aHUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMDc1NzQifX0seyJ2YWx1ZSI6OGUtMDYsIm4iOjksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxOTE0OSJ9fSx7InZhbHVlIjozNy45MjUzOTUxMywibiI6MTAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX19XSwiZmVlIjoyLjEzNGUtMDUsImhleCI6IjAyMDAwMDAwMGI2MTg2NzM5ZDg1MTFhOTNmYTk1MWZmNjRkMGNlZmI0YWNjZjUyZGMyOGNlZDg0OGExOGI3NjgxMmQzZjZjZGNkMDAwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGRlN2FlN2IxMGQ2ZDZkOWJiYzAxNzBhYjJjMmVkNDQ1MGVkNGNiMTg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmNjE4NjczOWQ4NTExYTkzZmE5NTFmZjY0ZDBjZWZiNGFjY2Y1MmRjMjhjZWQ4NDhhMThiNzY4MTJkM2Y2Y2RjZDAxMDAwMDAwNDU0NDc0NjM3NmE5MTQ3MWE3MTQ5ZWUwYjRlMTQ4ZDc3YWFkYjRjMGU3MWExMmYwZjM4MmE5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjYxODY3MzlkODUxMWE5M2ZhOTUxZmY2NGQwY2VmYjRhY2NmNTJkYzI4Y2VkODQ4YTE4Yjc2ODEyZDNmNmNkY2QwMjAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY2MTg2NzM5ZDg1MTFhOTNmYTk1MWZmNjRkMGNlZmI0YWNjZjUyZGMyOGNlZDg0OGExOGI3NjgxMmQzZjZjZGNkMDMwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDFiMjEzNDliODAxOWVjNzM4YThkYzZmMmZjNDJkYWNiYjA2NDQxZDA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmNjE4NjczOWQ4NTExYTkzZmE5NTFmZjY0ZDBjZWZiNGFjY2Y1MmRjMjhjZWQ4NDhhMThiNzY4MTJkM2Y2Y2RjZDA0MDAwMDAwNDU0NDc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjYxODY3MzlkODUxMWE5M2ZhOTUxZmY2NGQwY2VmYjRhY2NmNTJkYzI4Y2VkODQ4YTE4Yjc2ODEyZDNmNmNkY2QwZTAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZDA3MzZlZTkwMTE3ZjRlZDdlOWJhZDVhZTA5YjBhYjBjODkyOTZiNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY2MTg2NzM5ZDg1MTFhOTNmYTk1MWZmNjRkMGNlZmI0YWNjZjUyZGMyOGNlZDg0OGExOGI3NjgxMmQzZjZjZGNkMTIwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGNkNmQ1NjdkYzYzNGFjOGZhODg1NjFlODdiMjhmOTExOGE4YWRhN2E4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmNjE4NjczOWQ4NTExYTkzZmE5NTFmZjY0ZDBjZWZiNGFjY2Y1MmRjMjhjZWQ4NDhhMThiNzY4MTJkM2Y2Y2RjZDE4MDAwMDAwNDU0NDc0NjM3NmE5MTRhNzNkNzA4OWU0OGFiZjM3YjRkYjE1Y2ZlYjRiZDQ1N2I1YTEwODFmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjYxODY3MzlkODUxMWE5M2ZhOTUxZmY2NGQwY2VmYjRhY2NmNTJkYzI4Y2VkODQ4YTE4Yjc2ODEyZDNmNmNkY2QyMDAwMDAwMDQ1NDQ3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MzYwMDAwMDA2NDQxMDAyZWQ5MTg3NWYyNjI4YmM2MDk3MWY2NGYzYjg3NWIxZDZjNzNhYzJhYmViNzQzNzAwZjU1MTkxOThlMjkxODQxOTVkNGMzYTk5OGFjYWVhYTY0ZjBjMzE0OGZmMjA3NzgxOTI2YmE4MjljMDRmZmFlODE0MTY0NDExYTE2MTA2MTIxMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzZmZmZmZmZmY2MTg2NzM5ZDg1MTFhOTNmYTk1MWZmNjRkMGNlZmI0YWNjZjUyZGMyOGNlZDg0OGExOGI3NjgxMmQzZjZjZGNkNDkwMDAwMDA2NDQxNTQ2ZGQxNGUyYTZhYjBjY2M2NTk3NmIzN2JhYTgxMTVmODEzNTY2Mjc5YzNhOWM1MjU0MTRmYWY2MGE3ZTdhOGQ1YzVhMGY4YTBhZjQwN2Q4MjQ1MDRkNThiYTM3ZTQyZTM4NjUyNjI0NzhiODliYjAzMmMzOTFiYjFiY2ZmMmE2MTIxMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzZmZmZmZmZmYwYjhiMmU5YzY3MDIwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVhNTczNGEwMGFhMjA1MGZjMTZmZmIzNzdjZDRlMWQ4YjRkZWZhMzc1NmQxMjIxYmFjZmUzMzA0ZTc3MDVkZTQxNjdkNzdjMWJhMzY0ODdiZTk3OWIyNDAyMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlMzY1OTQyMDBhYTIwMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMDg3ZmVjMTQ4YTcwMTAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTIwMzEzMzAwYWEyMDJjZjliZjRkYWIwNWEyMzUyMWRjMjYyOTM4MzAyNjJmZmVjNzYxNDY2ODgwOThiYjMxMjRhYzNmYjc2NDAwMzY4NzI2YzY4MWQ2MDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmU0M2YxMTkwMGFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODdkNjA2MTk2ODAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlZWQ5NjBjMDBhYTIwMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZDg3ZDlhNzgxNDMwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTBiMmEwODAwYWEyMGIzNDdkNjhjOGQyY2RiMzIwNjZiZTlhZjgwNGM2ZmZiOGFiYmI0NDFiZGYzNmNhZjM4MzQzNmJjZmNhYTM1Y2I4NzE4ZmZiODNlMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVlZjk1MDcwMGFhMjA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjODdjMTgxMDgxZjAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlY2ZjMDAzMDBhYTIwZjFjMzNiYzdkYTA5OTFkMjZmOGJiZjE1MGM5MDM1NjhiMmMzZGM4NzI3MzcwODYzZjRjM2Q5Zjk2NTViMzc5MTg3M2U3MTMwMWEwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZWQ2MmEwMzAwYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NzIwMDMwMDAwMDAwMDAwMDAzZWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmRjZDRhNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWM3OThmMGRlMjAwMDAwMDAwMTk3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYzJhMDAwMDAwIn0seyJ0eGlkIjoiYzViNjRiNjk0MDdkYzgxNTk5Y2RjZjM1Yjk2YzJhZTMxNDlhYTI2MTc2YzNiMDE5OTgzYzAzYjdjYjdmODEzNyIsImhhc2giOiJjNWI2NGI2OTQwN2RjODE1OTljZGNmMzViOTZjMmFlMzE0OWFhMjYxNzZjM2IwMTk5ODNjMDNiN2NiN2Y4MTM3IiwidmVyc2lvbiI6Miwic2l6ZSI6MTY0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiM2MzNDQ0NTVhZTJkMWEzNDlkYzM4NzZkMGYyYjc5ZTcyOWQ1NmFiNDVjYzdlNDZiZGUxYTA1YmIyYTAwYmQ1ZCIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIiwiaGV4IjoiNDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIn0sInNlcXVlbmNlIjoxfV0sInZvdXQiOlt7InZhbHVlIjowLjAwNTY2NTAyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XSwiZmVlIjoxLjY0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTVkYmQwMDJhYmIwNTFhZGU2YmU0Yzc1Y2I0NmFkNTI5ZTc3OTJiMGY2ZDg3YzM5ZDM0MWEyZGFlNTU0NDM0M2MwMDAwMDAwMDQ1NDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxMDEwMDAwMDAwMWU2YTQwODAwMDAwMDAwMDAyM2FhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODcwMDAwMDAwMCJ9LHsidHhpZCI6ImM4MzA5ZTczMTczYjVlNmZjOTFiZDg4MzdlMzQyZTE2M2IzNTZlZTE5ODkxNmViODk5NTJhZjZiMjRhMWEyMzMiLCJoYXNoIjoiYzgzMDllNzMxNzNiNWU2ZmM5MWJkODgzN2UzNDJlMTYzYjM1NmVlMTk4OTE2ZWI4OTk1MmFmNmIyNGExYTIzMyIsInZlcnNpb24iOjEsInNpemUiOjM3MywibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjA3MWZjMDExMWE1MTEyN2EwNDVkOWEzOGJiNTI4ZWI1ZDE4ZThkYzJkNGVjZmYxODE2MGJkZjVjYjgwOTYzZGUiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMGNjZWNhZDU3MmVkMTgxZmNlN2FmMTQwNjEwMjQ1ZmE3Njk0MWM2OWQ5ZGE3YzI0ZTA0N2YzYTljN2RjZTQxMzYwMjIwNDE1OGZlNWMxMDNjYzc4NGY2MzA5Yjc1NDZlYjUxNTYwN2FhZmQzZDEyNTA3YmUwYjgwMWJkMjliYTNlNGExM1tBTEx8Rk9SS0lEXSAwMjUwZjEyZDkxOTQ0ZWM2N2M1YzkxMDkzZTZlODY2MGQwNzI3NzU4MTRmN2NkZjc3MDNjNWZjYTM1MWQ2NjQ1NmUiLCJoZXgiOiI0ODMwNDUwMjIxMDBjY2VjYWQ1NzJlZDE4MWZjZTdhZjE0MDYxMDI0NWZhNzY5NDFjNjlkOWRhN2MyNGUwNDdmM2E5YzdkY2U0MTM2MDIyMDQxNThmZTVjMTAzY2M3ODRmNjMwOWI3NTQ2ZWI1MTU2MDdhYWZkM2QxMjUwN2JlMGI4MDFiZDI5YmEzZTRhMTM0MTIxMDI1MGYxMmQ5MTk0NGVjNjdjNWM5MTA5M2U2ZTg2NjBkMDcyNzc1ODE0ZjdjZGY3NzAzYzVmY2EzNTFkNjY0NTZlIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJlNTFhYjhhYWU5ZmYwODgwZjg1OTFmMTQ4ZTFiNGE2ZDE5Mzg2ODQyNDM5NTc3ODM0ZDgwYTUyMTliZmQ0ZGE3Iiwidm91dCI6Mywic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwMTZlODUyMjYyNjczZmVhYTQzOGExY2VkMjdmZGJhMTBhY2Y2ODUzMzQyODg0OGI2Yzg3YmZmYzI5ZGUxMzE5OTAyMjA3NTY3N2VlNDE5Zjg3MDE1YWZhOTFjNzFmZjFjZTRiNWY1YWRiY2ZlMGM4MWJjYWIxNTdjZDJjNzlmNDhjYmQ5W0FMTHxGT1JLSURdIDAyNTBmMTJkOTE5NDRlYzY3YzVjOTEwOTNlNmU4NjYwZDA3Mjc3NTgxNGY3Y2RmNzcwM2M1ZmNhMzUxZDY2NDU2ZSIsImhleCI6IjQ3MzA0NDAyMjAxNmU4NTIyNjI2NzNmZWFhNDM4YTFjZWQyN2ZkYmExMGFjZjY4NTMzNDI4ODQ4YjZjODdiZmZjMjlkZTEzMTk5MDIyMDc1Njc3ZWU0MTlmODcwMTVhZmE5MWM3MWZmMWNlNGI1ZjVhZGJjZmUwYzgxYmNhYjE1N2NkMmM3OWY0OGNiZDk0MTIxMDI1MGYxMmQ5MTk0NGVjNjdjNWM5MTA5M2U2ZTg2NjBkMDcyNzc1ODE0ZjdjZGY3NzAzYzVmY2EzNTFkNjY0NTZlIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjAzNTExNDMsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3M2JmMzlmMjUxZGQ3ZGNhMmIxOWZiMDAwNmMzNmM5YTlkOGRiZGEwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NzNiZjM5ZjI1MWRkN2RjYTJiMTlmYjAwMDZjMzZjOWE5ZDhkYmRhMDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGVtN3cwajI4d2htajN0cjhhc3Fwa3JkamRmbXJkYTVxcXBqdHZyOHAiXX19LHsidmFsdWUiOjE2NDQyMC42MjE3ODYyNSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDVlYTNjZmZmOWM2OTM3NGQ5NGU5NWIwODM4YWVhNzg1NDlmYjU0MWEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1ZWEzY2ZmZjljNjkzNzRkOTRlOTViMDgzOGFlYTc4NTQ5ZmI1NDFhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwMDI4bmxsbjM1bndudjVhOWRzc3c5dzU3ejVuNzY1cmdlbnIzc3V3NiJdfX1dLCJmZWUiOjMuNzZlLTA2LCJoZXgiOiIwMTAwMDAwMDAyZGU2MzA5Yjg1Y2RmMGIxNjE4ZmZlY2Q0YzI4ZDhlZDFiNThlNTJiYjM4OWE1ZDA0N2ExMjUxMWExMWMwMWYwNzAwMDAwMDAwNmI0ODMwNDUwMjIxMDBjY2VjYWQ1NzJlZDE4MWZjZTdhZjE0MDYxMDI0NWZhNzY5NDFjNjlkOWRhN2MyNGUwNDdmM2E5YzdkY2U0MTM2MDIyMDQxNThmZTVjMTAzY2M3ODRmNjMwOWI3NTQ2ZWI1MTU2MDdhYWZkM2QxMjUwN2JlMGI4MDFiZDI5YmEzZTRhMTM0MTIxMDI1MGYxMmQ5MTk0NGVjNjdjNWM5MTA5M2U2ZTg2NjBkMDcyNzc1ODE0ZjdjZGY3NzAzYzVmY2EzNTFkNjY0NTZlZmZmZmZmZmZhNzRkZmQ5YjIxYTU4MDRkODM3Nzk1NDM0MjY4MzgxOTZkNGExYjhlMTQxZjU5Zjg4MDA4ZmZlOWFhYjgxYWU1MDMwMDAwMDA2YTQ3MzA0NDAyMjAxNmU4NTIyNjI2NzNmZWFhNDM4YTFjZWQyN2ZkYmExMGFjZjY4NTMzNDI4ODQ4YjZjODdiZmZjMjlkZTEzMTk5MDIyMDc1Njc3ZWU0MTlmODcwMTVhZmE5MWM3MWZmMWNlNGI1ZjVhZGJjZmUwYzgxYmNhYjE1N2NkMmM3OWY0OGNiZDk0MTIxMDI1MGYxMmQ5MTk0NGVjNjdjNWM5MTA5M2U2ZTg2NjBkMDcyNzc1ODE0ZjdjZGY3NzAzYzVmY2EzNTFkNjY0NTZlZmZmZmZmZmYwMjg2OTQzNTAwMDAwMDAwMDAxOTc2YTkxNDczYmYzOWYyNTFkZDdkY2EyYjE5ZmIwMDA2YzM2YzlhOWQ4ZGJkYTA4OGFjNDE4OTQ2MzdmNDBlMDAwMDE5NzZhOTE0NWVhM2NmZmY5YzY5Mzc0ZDk0ZTk1YjA4MzhhZWE3ODU0OWZiNTQxYTg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6ImM5Yjk3OWI1Yzc1YTEzMjUwYzQ1ZWExOWIxZTFiMWE0ZDFhY2RmZWI2ZGFmYWY2N2UzNjE4NjI2M2YxYmU3MGMiLCJoYXNoIjoiYzliOTc5YjVjNzVhMTMyNTBjNDVlYTE5YjFlMWIxYTRkMWFjZGZlYjZkYWZhZjY3ZTM2MTg2MjYzZjFiZTcwYyIsInZlcnNpb24iOjIsInNpemUiOjE2NCwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImQ0Y2ZlNzI0ZWJmN2FkNDgxNWZkNGFkOTQyODU0MWU3YTJhMDE3ZjBlMGFlMjliZDU0NmE5ODAyY2U4MGE2NWUiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiNTFiMjc1YzM1MTlkYzQ1MTlkMDI0MDAyMDJhNDAwYzBjNjdiNTI3OTkzYTA2M2MwYzdjMGNkODhjMGM2MDIyODExOTUwNGM2ODU3NDRmOTY2ZWEwNjM3ODc3NjhjMGM2YzBjYzk0NmVhMjY5NmQ2N2MwY2MwMDlkYzBjZDAxNmE4ODY4NzU1MSIsImhleCI6IjQ0NTFiMjc1YzM1MTlkYzQ1MTlkMDI0MDAyMDJhNDAwYzBjNjdiNTI3OTkzYTA2M2MwYzdjMGNkODhjMGM2MDIyODExOTUwNGM2ODU3NDRmOTY2ZWEwNjM3ODc3NjhjMGM2YzBjYzk0NmVhMjY5NmQ2N2MwY2MwMDlkYzBjZDAxNmE4ODY4NzU1MSJ9LCJzZXF1ZW5jZSI6MX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMDU2NDM3LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XSwiZmVlIjoxLjY0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTVlYTY4MGNlMDI5ODZhNTRiZDI5YWVlMGYwMTdhMGEyZTc0MTg1NDJkOTRhZmQxNTQ4YWRmN2ViMjRlN2NmZDQwMDAwMDAwMDQ1NDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxMDEwMDAwMDAwMTkyOWMwODAwMDAwMDAwMDAyM2FhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODcwMDAwMDAwMCJ9LHsidHhpZCI6ImNiN2I3ZGNhOWI4N2NlMTlkNDRjMTdhMTczNjYwZjgwOWI4YzMwNzhmNjU3YmY3NzI5NzJiZWMwYmM4OWYyMDUiLCJoYXNoIjoiY2I3YjdkY2E5Yjg3Y2UxOWQ0NGMxN2ExNzM2NjBmODA5YjhjMzA3OGY2NTdiZjc3Mjk3MmJlYzBiYzg5ZjIwNSIsInZlcnNpb24iOjEsInNpemUiOjM3MiwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjZkOTU5MWMwM2FkZjQyY2ZkOTFjNWYyZjkyZjIyNjVmYjg0NGMyNjZlMTA4YTRkZjIxNTY2YTVhMWZlNjUxODMiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAwNDA2YjYwMDZjMmI4ZDdlN2EyMDMzZDgwY2FjYzExNGIzM2YwZGY2YzViYTc1YWU3MjI2YzI5NGIwMjJlYjJjMDIyMDAyOTQ0NGE5NGE3Y2JiOGJiOWZmMzg5NzdmYjE4MzIwMGU0MzE0MDFkYjYzZGE1Mzk5NjcxZjc2Y2NlM2NjNTNbQUxMfEZPUktJRF0gMDMzYTFhOGI3MTJlNjRmOTVmNDcyNDU5ZThlMTA3ZDYwMTg4MThkNWY0MDBiNWJhZTRhZmI0MTBkZjY4YmIxNWQ2IiwiaGV4IjoiNDczMDQ0MDIyMDA0MDZiNjAwNmMyYjhkN2U3YTIwMzNkODBjYWNjMTE0YjMzZjBkZjZjNWJhNzVhZTcyMjZjMjk0YjAyMmViMmMwMjIwMDI5NDQ0YTk0YTdjYmI4YmI5ZmYzODk3N2ZiMTgzMjAwZTQzMTQwMWRiNjNkYTUzOTk2NzFmNzZjY2UzY2M1MzQxMjEwMzNhMWE4YjcxMmU2NGY5NWY0NzI0NTllOGUxMDdkNjAxODgxOGQ1ZjQwMGI1YmFlNGFmYjQxMGRmNjhiYjE1ZDYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImFhNjc2YjM5MzMwNzBlNjI1ZGVlZjMyN2M0YzBhNDMyNzRhOTFmMjA0OWY1NTMyZWVhMWE2YzI4ZWI4NTJjODgiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA2ZmMxNWVmMTgyYjliNWY0NjM3Y2MyNTBjYWFiMDI0OWYyODc2M2E5NDJkMTU3NTIwZGUyZDU1ZjI5MmQ5NWM0MDIyMDYwMTQ5NWJiOTZhNDAyNDUyNTI4ZTNlYWEwNTM4MTJhM2IzMjk2NGY4MzY5ZTU1ZTA2ODM2YmMxODQwZDMwOTBbQUxMfEZPUktJRF0gMDIxODVkN2M4ZmRjYzI5Yjk2OTc4ZDYxN2UxY2E4ZDM1NDE0NTQ1ZWM5ZTQ4NzNmOTEzNDI1ZTBjNGQzZTY1MGNkIiwiaGV4IjoiNDczMDQ0MDIyMDZmYzE1ZWYxODJiOWI1ZjQ2MzdjYzI1MGNhYWIwMjQ5ZjI4NzYzYTk0MmQxNTc1MjBkZTJkNTVmMjkyZDk1YzQwMjIwNjAxNDk1YmI5NmE0MDI0NTI1MjhlM2VhYTA1MzgxMmEzYjMyOTY0ZjgzNjllNTVlMDY4MzZiYzE4NDBkMzA5MDQxMjEwMjE4NWQ3YzhmZGNjMjliOTY5NzhkNjE3ZTFjYThkMzU0MTQ1NDVlYzllNDg3M2Y5MTM0MjVlMGM0ZDNlNjUwY2QifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjUuNDZlLTA2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNDJkZTkwZTAwZGFhY2MzMzVlNzVmMWI3NTllZTU0YzgwNTdkNDcxOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDQyZGU5MGUwMGRhYWNjMzM1ZTc1ZjFiNzU5ZWU1NGM4MDU3ZDQ3MTg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBwZGF5OHFwazR2Y3Y2N3doY213azB3Mm55cTJsMjhycXJsZnd6c2tqIl19fSx7InZhbHVlIjowLjAwMzQ3MDk2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMWI0ZDk5NDc0OGQ0YjQ2NWUzYzdlNWZjOTU3OGFmZjU4NDQwNTZmOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDFiNGQ5OTQ3NDhkNGI0NjVlM2M3ZTVmYzk1NzhhZmY1ODQ0MDU2Zjg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFkNW14MjhmcjJ0Z2UwcmNsamxlOXRjNGw2Y2dzemtscXFweGNqcXc2Il19fV0sImZlZSI6NC43NGUtMDYsImhleCI6IjAxMDAwMDAwMDI4MzUxZTYxZjVhNmE1NjIxZGZhNDA4ZTE2NmMyNDRiODVmMjZmMjkyMmY1ZjFjZDljZjQyZGYzYWMwOTE5NTZkMDEwMDAwMDA2YTQ3MzA0NDAyMjAwNDA2YjYwMDZjMmI4ZDdlN2EyMDMzZDgwY2FjYzExNGIzM2YwZGY2YzViYTc1YWU3MjI2YzI5NGIwMjJlYjJjMDIyMDAyOTQ0NGE5NGE3Y2JiOGJiOWZmMzg5NzdmYjE4MzIwMGU0MzE0MDFkYjYzZGE1Mzk5NjcxZjc2Y2NlM2NjNTM0MTIxMDMzYTFhOGI3MTJlNjRmOTVmNDcyNDU5ZThlMTA3ZDYwMTg4MThkNWY0MDBiNWJhZTRhZmI0MTBkZjY4YmIxNWQ2ZmZmZmZmZmY4ODJjODVlYjI4NmMxYWVhMmU1M2Y1NDkyMDFmYTk3NDMyYTRjMGM0MjdmM2VlNWQ2MjBlMDczMzM5NmI2N2FhMDEwMDAwMDA2YTQ3MzA0NDAyMjA2ZmMxNWVmMTgyYjliNWY0NjM3Y2MyNTBjYWFiMDI0OWYyODc2M2E5NDJkMTU3NTIwZGUyZDU1ZjI5MmQ5NWM0MDIyMDYwMTQ5NWJiOTZhNDAyNDUyNTI4ZTNlYWEwNTM4MTJhM2IzMjk2NGY4MzY5ZTU1ZTA2ODM2YmMxODQwZDMwOTA0MTIxMDIxODVkN2M4ZmRjYzI5Yjk2OTc4ZDYxN2UxY2E4ZDM1NDE0NTQ1ZWM5ZTQ4NzNmOTEzNDI1ZTBjNGQzZTY1MGNkZmZmZmZmZmYwMjIyMDIwMDAwMDAwMDAwMDAxOTc2YTkxNDQyZGU5MGUwMGRhYWNjMzM1ZTc1ZjFiNzU5ZWU1NGM4MDU3ZDQ3MTg4OGFjZDg0YjA1MDAwMDAwMDAwMDE5NzZhOTE0MWI0ZDk5NDc0OGQ0YjQ2NWUzYzdlNWZjOTU3OGFmZjU4NDQwNTZmODg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6ImNiYjNlMmUyY2ZkMzhiZTM0ZWE3YjU5ZTE5MTNjMGQzNDVjZWJjNDJiYzk2NjRjMjgyOTAxZWMyY2MxZWJjYjMiLCJoYXNoIjoiY2JiM2UyZTJjZmQzOGJlMzRlYTdiNTllMTkxM2MwZDM0NWNlYmM0MmJjOTY2NGMyODI5MDFlYzJjYzFlYmNiMyIsInZlcnNpb24iOjEsInNpemUiOjE5MSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImFjYmRkM2I4ZjMwZmNhMzFjODUxMjVhN2Y3MDk4MzU3MDM3M2NhMjkxNjA1N2JkNzEzZDMwY2Q2YTkzZjdjMjQiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA1YTkxMWQwM2NjNWFlZDc3MjRjNjU5ZTBmYjlhYzI4NWZjYTM0ZjI1NWRkYjM0YjI0MDAzYmYyZTNmZjcyNjZiMDIyMDM2NDliMTc3ZWUxNzhmZTgzYmRhYzBjODhiNzIwMjVkY2RjN2NiNDNjMzJiM2IwMDg2YTBhMDNhNjkxN2RkOThbQUxMfEZPUktJRF0gMDI3MDc0NDU2MzFlMDY1NzJmNzg5MmYyNzljNDk5M2ZhZTBhNTliMGZkMTAxMTZmZTUyZjY4Y2MzYWJjMDhmYmYzIiwiaGV4IjoiNDczMDQ0MDIyMDVhOTExZDAzY2M1YWVkNzcyNGM2NTllMGZiOWFjMjg1ZmNhMzRmMjU1ZGRiMzRiMjQwMDNiZjJlM2ZmNzI2NmIwMjIwMzY0OWIxNzdlZTE3OGZlODNiZGFjMGM4OGI3MjAyNWRjZGM3Y2I0M2MzMmIzYjAwODZhMGEwM2E2OTE3ZGQ5ODQxMjEwMjcwNzQ0NTYzMWUwNjU3MmY3ODkyZjI3OWM0OTkzZmFlMGE1OWIwZmQxMDExNmZlNTJmNjhjYzNhYmMwOGZiZjMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuNDk5ODU2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNjU0MTNmN2ZjOGVkNzRhZWU4MzZiNzVmZTk1ZTY0ZWQ0NTRhZjMwYiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDY1NDEzZjdmYzhlZDc0YWVlODM2Yjc1ZmU5NWU2NGVkNDU0YWYzMGI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBqNXowbWxlcmtoZnRoZ3g2bTRsNjI3dm5rNTJqaG5wdjR6a2Z1NnVqIl19fV0sImZlZSI6MC4wMDAxNDQsImhleCI6IjAxMDAwMDAwMDEyNDdjM2ZhOWQ2MGNkMzEzZDc3YjA1MTYyOWNhNzMwMzU3ODMwOWY3YTcyNTUxYzgzMWNhMGZmM2I4ZDNiZGFjMDAwMDAwMDA2YTQ3MzA0NDAyMjA1YTkxMWQwM2NjNWFlZDc3MjRjNjU5ZTBmYjlhYzI4NWZjYTM0ZjI1NWRkYjM0YjI0MDAzYmYyZTNmZjcyNjZiMDIyMDM2NDliMTc3ZWUxNzhmZTgzYmRhYzBjODhiNzIwMjVkY2RjN2NiNDNjMzJiM2IwMDg2YTBhMDNhNjkxN2RkOTg0MTIxMDI3MDc0NDU2MzFlMDY1NzJmNzg5MmYyNzljNDk5M2ZhZTBhNTliMGZkMTAxMTZmZTUyZjY4Y2MzYWJjMDhmYmYzZmZmZmZmZmYwMTQwYjhmYTAyMDAwMDAwMDAxOTc2YTkxNDY1NDEzZjdmYzhlZDc0YWVlODM2Yjc1ZmU5NWU2NGVkNDU0YWYzMGI4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJjYmQ0M2YzNjNjNjJmNWQzZDg0NTEzMmY3YjUyMmVmMjNhM2Y1OGM4ODcwNTlkZjY2NTI1ZGU1NzZkOTQ3MDEyIiwiaGFzaCI6ImNiZDQzZjM2M2M2MmY1ZDNkODQ1MTMyZjdiNTIyZWYyM2EzZjU4Yzg4NzA1OWRmNjY1MjVkZTU3NmQ5NDcwMTIiLCJ2ZXJzaW9uIjoyLCJzaXplIjoxOTIsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI4MDc4YWM4MzA1OWM3YmM1OTZiZTE4MmNlZDhhMjQzM2NjMTEyYmNhNDMwOTI2YmY1ZDFjYjI0MmRkNTc4ODlkIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBkOTQ1MzI3ZDIxNTQ2ZDY3YjU5MjQxNjI5YWVlZDYxNTIwMmUyNTcwNmNmYzY3OTljMzJkZDkwOWY2MjU3YTA3MDIyMDc1NjAwZjZjNDNlMjdmNTJmZjgzMDIzODM2YzU0OGRjMGFlY2RhMTJiNTBjMzFmMDFhYzVkMTVjM2ExNzYzNWVbQUxMfEZPUktJRF0gMDI1MzM4Y2IyYWE3MWZhN2RmYzJmODUwZjQzZDMwNzZiMTZhNTg4MTJmYWJlODY4NjQ4MTJhYzkyMmQyY2U1MTNhIiwiaGV4IjoiNDgzMDQ1MDIyMTAwZDk0NTMyN2QyMTU0NmQ2N2I1OTI0MTYyOWFlZWQ2MTUyMDJlMjU3MDZjZmM2Nzk5YzMyZGQ5MDlmNjI1N2EwNzAyMjA3NTYwMGY2YzQzZTI3ZjUyZmY4MzAyMzgzNmM1NDhkYzBhZWNkYTEyYjUwYzMxZjAxYWM1ZDE1YzNhMTc2MzVlNDEyMTAyNTMzOGNiMmFhNzFmYTdkZmMyZjg1MGY0M2QzMDc2YjE2YTU4ODEyZmFiZTg2ODY0ODEyYWM5MjJkMmNlNTEzYSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MzEuNjc5NjU5NDIsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2NzY0YzlkYmRjYjVhMzlhYWIxMjU3NzUyZTdhOGVhYTI0NmY1OTE4IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0Njc2NGM5ZGJkY2I1YTM5YWFiMTI1Nzc1MmU3YThlYWEyNDZmNTkxODg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcG5rZmp3bW1qNjY4eDR0emZ0aDJ0bjYzNjR6Z202ZXJxOXdqY3IzbnciXX19XSwiZmVlIjoxLjk0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTlkODg1N2RkNDJiMjFjNWRiZjI2MDk0M2NhMmIxMWNjMzMyNDhhZWQyYzE4YmU5NmM1N2I5YzA1ODNhYzc4ODAwMTAwMDAwMDZiNDgzMDQ1MDIyMTAwZDk0NTMyN2QyMTU0NmQ2N2I1OTI0MTYyOWFlZWQ2MTUyMDJlMjU3MDZjZmM2Nzk5YzMyZGQ5MDlmNjI1N2EwNzAyMjA3NTYwMGY2YzQzZTI3ZjUyZmY4MzAyMzgzNmM1NDhkYzBhZWNkYTEyYjUwYzMxZjAxYWM1ZDE1YzNhMTc2MzVlNDEyMTAyNTMzOGNiMmFhNzFmYTdkZmMyZjg1MGY0M2QzMDc2YjE2YTU4ODEyZmFiZTg2ODY0ODEyYWM5MjJkMmNlNTEzYWZmZmZmZmZmMDFmNjUyZDNiYzAwMDAwMDAwMTk3NmE5MTQ2NzY0YzlkYmRjYjVhMzlhYWIxMjU3NzUyZTdhOGVhYTI0NmY1OTE4ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiY2MwOWFlNjBjZDY2OGJhYzk5MzQ2YmFhNTAxZDQyZmZmYjEwYzA4ZmZiMWIwZmRlNzU4NjZhYmFlYTc4MmU4NCIsImhhc2giOiJjYzA5YWU2MGNkNjY4YmFjOTkzNDZiYWE1MDFkNDJmZmZiMTBjMDhmZmIxYjBmZGU3NTg2NmFiYWVhNzgyZTg0IiwidmVyc2lvbiI6MSwic2l6ZSI6MjI2LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiYmUzM2RlZmRkYmM1ZTYyNGRiN2YxZTAyZTVjMmVlZmM3ZjQ4NGU3ZDU4OGMxYmZiZmFiOTM4NjQzNjNlNDRhZSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwYTkzNDRhMGE4OTg3MDhlN2Q1MGE3MWI3MWViZTM4MTJlMjY2MjBiZGNhMzZkOWU1NTQzOTAwNmEzMWY0OWI1YjAyMjAxNDBlYmFhOGY5MjhmZjVkMTk2NzM5MjM3MTY4YTBhYTkwOTY5OTlmNDcxMjVmM2IwNDkyMjczZjJkNDlhYmFhW0FMTHxGT1JLSURdIDAzMDE1ZjY0NzkxOWJmNDdjYjVhYzc3ODk3MmExZDIxYjQyNDZhODY5MDBlOTc4MWQ2MDM1N2VmNDM0YzM3ZjFjMCIsImhleCI6IjQ4MzA0NTAyMjEwMGE5MzQ0YTBhODk4NzA4ZTdkNTBhNzFiNzFlYmUzODEyZTI2NjIwYmRjYTM2ZDllNTU0MzkwMDZhMzFmNDliNWIwMjIwMTQwZWJhYThmOTI4ZmY1ZDE5NjczOTIzNzE2OGEwYWE5MDk2OTk5ZjQ3MTI1ZjNiMDQ5MjI3M2YyZDQ5YWJhYTQxMjEwMzAxNWY2NDc5MTliZjQ3Y2I1YWM3Nzg5NzJhMWQyMWI0MjQ2YTg2OTAwZTk3ODFkNjAzNTdlZjQzNGMzN2YxYzAifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMDEyODM4MDcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5MDNhMTlmODBmNDA5YmRkMzAzZWE4M2QyMDY1NjZjNWFiNTFmMGY4IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OTAzYTE5ZjgwZjQwOWJkZDMwM2VhODNkMjA2NTY2YzVhYjUxZjBmODg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemdyNXgwY3BhcWZoaGZzODY1cjZncjl2bXo2azUwc2xxMjlzdmNsc20iXX19LHsidmFsdWUiOjAuMDA3OTM5NzIsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAzN2MxOGI5ZDVlM2I1NDM1YTE0MWI1ZTBiODZlMjFhYzU0MWE0ZTMyIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MzdjMThiOWQ1ZTNiNTQzNWExNDFiNWUwYjg2ZTIxYWM1NDFhNGUzMjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcW11cnp1YXRjYTRnZGRwZ3g2N3B3cnd5eGs5Z3hqd3hndThmdjZxa2EiXX19XSwiZmVlIjoyLjI2ZS0wNiwiaGV4IjoiMDEwMDAwMDAwMWFlNDQzZTM2NjQzOGI5ZmFmYjFiOGM1ODdkNGU0ODdmZmNlZWMyZTUwMjFlN2ZkYjI0ZTZjNWRiZmRkZTMzYmUwMDAwMDAwMDZiNDgzMDQ1MDIyMTAwYTkzNDRhMGE4OTg3MDhlN2Q1MGE3MWI3MWViZTM4MTJlMjY2MjBiZGNhMzZkOWU1NTQzOTAwNmEzMWY0OWI1YjAyMjAxNDBlYmFhOGY5MjhmZjVkMTk2NzM5MjM3MTY4YTBhYTkwOTY5OTlmNDcxMjVmM2IwNDkyMjczZjJkNDlhYmFhNDEyMTAzMDE1ZjY0NzkxOWJmNDdjYjVhYzc3ODk3MmExZDIxYjQyNDZhODY5MDBlOTc4MWQ2MDM1N2VmNDM0YzM3ZjFjMGZmZmZmZmZmMDJkZjk2MTMwMDAwMDAwMDAwMTk3NmE5MTQ5MDNhMTlmODBmNDA5YmRkMzAzZWE4M2QyMDY1NjZjNWFiNTFmMGY4ODhhYzc0MWQwYzAwMDAwMDAwMDAxOTc2YTkxNDM3YzE4YjlkNWUzYjU0MzVhMTQxYjVlMGI4NmUyMWFjNTQxYTRlMzI4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJjZGNkZjZkMzEyNjhiNzE4OGE4NGVkOGNjMjJkZjVjYzRhZmJjZWQwNjRmZjUxYTkzZmE5MTE4NTlkNzM4NjYxIiwiaGFzaCI6ImNkY2RmNmQzMTI2OGI3MTg4YTg0ZWQ4Y2MyMmRmNWNjNGFmYmNlZDA2NGZmNTFhOTNmYTkxMTg1OWQ3Mzg2NjEiLCJ2ZXJzaW9uIjoyLCJzaXplIjoxNDE5MywibG9ja3RpbWUiOjQyLCJ2aW4iOlt7InR4aWQiOiJlM2U0OWZkNDFhMzM4YTQ4ZmVmODZkNjJiZDE4YTQ5NjIzZTJkMmJjYzUxMjEzNmNiZjdkNzNmNzYzYWExZDVhIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkZTdhZTdiMTBkNmQ2ZDliYmMwMTcwYWIyYzJlZDQ0NTBlZDRjYjE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkZTdhZTdiMTBkNmQ2ZDliYmMwMTcwYWIyYzJlZDQ0NTBlZDRjYjE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImUzZTQ5ZmQ0MWEzMzhhNDhmZWY4NmQ2MmJkMThhNDk2MjNlMmQyYmNjNTEyMTM2Y2JmN2Q3M2Y3NjNhYTFkNWEiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDcxYTcxNDllZTBiNGUxNDhkNzdhYWRiNGMwZTcxYTEyZjBmMzgyYTk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDcxYTcxNDllZTBiNGUxNDhkNzdhYWRiNGMwZTcxYTEyZjBmMzgyYTk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJlM2U0OWZkNDFhMzM4YTQ4ZmVmODZkNjJiZDE4YTQ5NjIzZTJkMmJjYzUxMjEzNmNiZjdkNzNmNzYzYWExZDVhIiwidm91dCI6Mywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQxYjIxMzQ5YjgwMTllYzczOGE4ZGM2ZjJmYzQyZGFjYmIwNjQ0MWQwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQxYjIxMzQ5YjgwMTllYzczOGE4ZGM2ZjJmYzQyZGFjYmIwNjQ0MWQwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImUzZTQ5ZmQ0MWEzMzhhNDhmZWY4NmQ2MmJkMThhNDk2MjNlMmQyYmNjNTEyMTM2Y2JmN2Q3M2Y3NjNhYTFkNWEiLCJ2b3V0Ijo0LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDJhMThlZjA2YjFiNzFiMmUzNzk3YTk2YWNlOTBhMDRiOWIyOGMxMmI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDJhMThlZjA2YjFiNzFiMmUzNzk3YTk2YWNlOTBhMDRiOWIyOGMxMmI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOjksInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6MTAsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZDZjOWQwN2EwMzVhZTVjZjMxN2Y1ZjQwNDY0OWFmYjgwODM3YjRhMTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZDZjOWQwN2EwMzVhZTVjZjMxN2Y1ZjQwNDY0OWFmYjgwODM3YjRhMTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OTkyOTk5NGJmZGMyNjdhOGViNjFhZDQ2Yjk2NzY2NjA0NzYzZTU1NmExNDkxMzc1Mzg2MzQ0OWU4YzUwMTYzIiwidm91dCI6MTgsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ODg0MjQ5NWY5ODhkOTA3YzhlY2Y4ZjZmZjE3ZTkwYTM5ZTg5ZDJhNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ODg0MjQ5NWY5ODhkOTA3YzhlY2Y4ZjZmZjE3ZTkwYTM5ZTg5ZDJhNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OTkyOTk5NGJmZGMyNjdhOGViNjFhZDQ2Yjk2NzY2NjA0NzYzZTU1NmExNDkxMzc1Mzg2MzQ0OWU4YzUwMTYzIiwidm91dCI6MTksInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NDU5ZGU0MjY1NGQ5ZDJhY2ZlMzE1MWUwOGJlYTFmYTVjNDFjYjEyYzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NDU5ZGU0MjY1NGQ5ZDJhY2ZlMzE1MWUwOGJlYTFmYTVjNDFjYjEyYzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6MTIsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6MTEsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6MTMsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6MTQsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6MTUsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJlM2U0OWZkNDFhMzM4YTQ4ZmVmODZkNjJiZDE4YTQ5NjIzZTJkMmJjYzUxMjEzNmNiZjdkNzNmNzYzYWExZDVhIiwidm91dCI6NSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkMDczNmVlOTAxMTdmNGVkN2U5YmFkNWFlMDliMGFiMGM4OTI5NmI0ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkMDczNmVlOTAxMTdmNGVkN2U5YmFkNWFlMDliMGFiMGM4OTI5NmI0ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0IjoxNiwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ0NWRjMTQyNDg4OGJjNDliMzRiZWQ5ODAyMzMwM2NlZjFlMDhmYjcwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ0NWRjMTQyNDg4OGJjNDliMzRiZWQ5ODAyMzMwM2NlZjFlMDhmYjcwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc5OTI5OTk0YmZkYzI2N2E4ZWI2MWFkNDZiOTY3NjY2MDQ3NjNlNTU2YTE0OTEzNzUzODYzNDQ5ZThjNTAxNjMiLCJ2b3V0IjoyMCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQyYTE4ZWYwNmIxYjcxYjJlMzc5N2E5NmFjZTkwYTA0YjliMjhjMTJiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0IjoxNywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ5NTY5OWNjOTAyYzJkOWJlMTQzOGIzODZmNjc4Y2NmNmFmMDY4ODIzODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ5NTY5OWNjOTAyYzJkOWJlMTQzOGIzODZmNjc4Y2NmNmFmMDY4ODIzODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImUzZTQ5ZmQ0MWEzMzhhNDhmZWY4NmQ2MmJkMThhNDk2MjNlMmQyYmNjNTEyMTM2Y2JmN2Q3M2Y3NjNhYTFkNWEiLCJ2b3V0Ijo2LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNGNkNmQ1NjdkYzYzNGFjOGZhODg1NjFlODdiMjhmOTExOGE4YWRhN2E4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNGNkNmQ1NjdkYzYzNGFjOGZhODg1NjFlODdiMjhmOTExOGE4YWRhN2E4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzk5Mjk5OTRiZmRjMjY3YThlYjYxYWQ0NmI5Njc2NjYwNDc2M2U1NTZhMTQ5MTM3NTM4NjM0NDllOGM1MDE2MyIsInZvdXQiOjIxLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDFiZTBiYjBlYTE1YmQwYTViYjM5MTM4MjI0YjRlMWVjZDEyYmY5YWI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDFiZTBiYjBlYTE1YmQwYTViYjM5MTM4MjI0YjRlMWVjZDEyYmY5YWI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOjIzLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDYzOWMwN2Q1ZTI0MTNhZWQ0YzE0YzVjMmI4ZGJjMzgyYWMzZDk1MjI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDYzOWMwN2Q1ZTI0MTNhZWQ0YzE0YzVjMmI4ZGJjMzgyYWMzZDk1MjI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOjI0LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDlkY2I2NDRjMmQyMjcxMDhhOTk4ZWE4MGY0NjM5ZjY2YjI4MGUxZjA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDlkY2I2NDRjMmQyMjcxMDhhOTk4ZWE4MGY0NjM5ZjY2YjI4MGUxZjA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOjIxLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDc0NTc2Yzc3YWVmM2ZiNTg3ZjhjNmIyZmFmODhlMzdkOGVhYWY4Yjg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDc0NTc2Yzc3YWVmM2ZiNTg3ZjhjNmIyZmFmODhlMzdkOGVhYWY4Yjg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOjIzLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDRhMDg1MzA2NTBlODYxZDM0YWNlZTQzMTUyZTU0ZGUyN2QxYmNmODI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDRhMDg1MzA2NTBlODYxZDM0YWNlZTQzMTUyZTU0ZGUyN2QxYmNmODI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOjcsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0YTczZDcwODllNDhhYmYzN2I0ZGIxNWNmZWI0YmQ0NTdiNWExMDgxZjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0YTczZDcwODllNDhhYmYzN2I0ZGIxNWNmZWI0YmQ0NTdiNWExMDgxZjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWI4Y2Y5YWI3YjgzNTRhZTQ5MGQzYTk2Y2M4Y2U0MTE4Nzg3ZmNmMjQwNTZkOGY0NGNiOGFkZWNmYWUxYjNhIiwidm91dCI6MjgsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWI4Y2Y5YWI3YjgzNTRhZTQ5MGQzYTk2Y2M4Y2U0MTE4Nzg3ZmNmMjQwNTZkOGY0NGNiOGFkZWNmYWUxYjNhIiwidm91dCI6MjksInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0MWVmMzZkYjM4MzZlYjk1NWEzYTU2MTRkNTJmZWE2NTY3ODMwMDJhMjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0MWVmMzZkYjM4MzZlYjk1NWEzYTU2MTRkNTJmZWE2NTY3ODMwMDJhMjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6MTgsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0OTlmOWM0MTFmODM5MTM2ODNhNmRmNWI0NTM2ZjdhYzJiOWQwZmJjNTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0OTlmOWM0MTFmODM5MTM2ODNhNmRmNWI0NTM2ZjdhYzJiOWQwZmJjNTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6MjAsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3OGNiNzUyZmU5NjRhYzhhZmIzMGIwMjQ5MWExOWQ3OTBhNDdlODgzOTZiMTIyMDI0M2ZjNGU1ZjZkNTQ4Nzk5Iiwidm91dCI6MTksInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0NGEwODUzMDY1MGU4NjFkMzRhY2VlNDMxNTJlNTRkZTI3ZDFiY2Y4Mjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0NGEwODUzMDY1MGU4NjFkMzRhY2VlNDMxNTJlNTRkZTI3ZDFiY2Y4Mjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWI4Y2Y5YWI3YjgzNTRhZTQ5MGQzYTk2Y2M4Y2U0MTE4Nzg3ZmNmMjQwNTZkOGY0NGNiOGFkZWNmYWUxYjNhIiwidm91dCI6MzAsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0YWI0ZTZlMGI5YmZjOTdlYjhhMWJhMjVkOTViZGFhNTAzZGYxYzNhMjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0YWI0ZTZlMGI5YmZjOTdlYjhhMWJhMjVkOTViZGFhNTAzZGYxYzNhMjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI3NWI4Y2Y5YWI3YjgzNTRhZTQ5MGQzYTk2Y2M4Y2U0MTE4Nzg3ZmNmMjQwNTZkOGY0NGNiOGFkZWNmYWUxYjNhIiwidm91dCI6MzEsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0M2Y4MWE3NmM2NDBjMWIwY2E4YTBlNGNhZTQ2MWRjMWU5OTkwY2ExYTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0M2Y4MWE3NmM2NDBjMWIwY2E4YTBlNGNhZTQ2MWRjMWU5OTkwY2ExYTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJlM2U0OWZkNDFhMzM4YTQ4ZmVmODZkNjJiZDE4YTQ5NjIzZTJkMmJjYzUxMjEzNmNiZjdkNzNmNzYzYWExZDVhIiwidm91dCI6OCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRhNzg3YTI3NzRjYjFjM2I1ODdmOTA0OGY2NmU3ZGM5ZjU3NDM0NDczODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRhNzg3YTI3NzRjYjFjM2I1ODdmOTA0OGY2NmU3ZGM5ZjU3NDM0NDczODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0IjoyMiwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ4ODQyNDk1Zjk4OGQ5MDdjOGVjZjhmNmZmMTdlOTBhMzllODlkMmE0ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ4ODQyNDk1Zjk4OGQ5MDdjOGVjZjhmNmZmMTdlOTBhMzllODlkMmE0ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0IjoyNCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkM2U3ODZjNmE0YWYxMzc1MTFhYzg0MTIyZTc4MTVhMTA2NzQ5MjY5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkM2U3ODZjNmE0YWYxMzc1MTFhYzg0MTIyZTc4MTVhMTA2NzQ5MjY5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0IjozMiwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ2YmExNTJkYjdkZWIyOWFlN2EyNTI4M2FkMzFkMDhjNjhiYWY1M2EwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ2YmExNTJkYjdkZWIyOWFlN2EyNTI4M2FkMzFkMDhjNjhiYWY1M2EwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0IjozMywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkODQ2MDU2OWYwMzFiMDk4MGYzOTQ5YTA3ODI2M2UzYjQwMzYzOTYyODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkODQ2MDU2OWYwMzFiMDk4MGYzOTQ5YTA3ODI2M2UzYjQwMzYzOTYyODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0IjozNCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQzZWQwOGEyMDg3MDhhMjVlOTE3ODk3OTRhMDE2NTMzZWQ3YWRiOTI2ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQzZWQwOGEyMDg3MDhhMjVlOTE3ODk3OTRhMDE2NTMzZWQ3YWRiOTI2ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0IjozNSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ3ODY0MmJkNGVhYTM4NjQ3NmJlNjhjYWYxYTNjN2U3YmI5YWQ0NTYxODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ3ODY0MmJkNGVhYTM4NjQ3NmJlNjhjYWYxYTNjN2U3YmI5YWQ0NTYxODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0IjozNiwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQxYzQ1Yzc0NzJmNGIyNmRkZDQwNDY5OTMxMGFmY2EwNzQxYmEwNDBmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQxYzQ1Yzc0NzJmNGIyNmRkZDQwNDY5OTMxMGFmY2EwNzQxYmEwNDBmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0IjoyNSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQyNDJhMjE5NmNlMTM3NjE1Y2M2M2NmY2ZiZDU1NGMyNTA5M2Q0MjhiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQyNDJhMjE5NmNlMTM3NjE1Y2M2M2NmY2ZiZDU1NGMyNTA5M2Q0MjhiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0IjoyNiwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkYzFhYjBiZjhmNDcwMzUyNTk1MDNhZTFhMThjNDYyNDdiZmIxMTFkODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkYzFhYjBiZjhmNDcwMzUyNTk1MDNhZTFhMThjNDYyNDdiZmIxMTFkODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0IjozNywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ1MzdmYjg3ZDQwNzMxNTg0MTZhMGQ0MTcxMDY2NTE1MTM0NzI2NGEwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ1MzdmYjg3ZDQwNzMxNTg0MTZhMGQ0MTcxMDY2NTE1MTM0NzI2NGEwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0IjoyOCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ1MzdmYjg3ZDQwNzMxNTg0MTZhMGQ0MTcxMDY2NTE1MTM0NzI2NGEwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ1MzdmYjg3ZDQwNzMxNTg0MTZhMGQ0MTcxMDY2NTE1MTM0NzI2NGEwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4Y2I3NTJmZTk2NGFjOGFmYjMwYjAyNDkxYTE5ZDc5MGE0N2U4ODM5NmIxMjIwMjQzZmM0ZTVmNmQ1NDg3OTkiLCJ2b3V0IjoyNywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ3N2I0ZWM3MDZiOGY3NTMxYzgxMjE5YWNkYmE4MDdkYTBlZjNlN2IzODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ3N2I0ZWM3MDZiOGY3NTMxYzgxMjE5YWNkYmE4MDdkYTBlZjNlN2IzODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0IjozOCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ1MzdmYjg3ZDQwNzMxNTg0MTZhMGQ0MTcxMDY2NTE1MTM0NzI2NGEwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ1MzdmYjg3ZDQwNzMxNTg0MTZhMGQ0MTcxMDY2NTE1MTM0NzI2NGEwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0IjozOSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQyNDJhMjE5NmNlMTM3NjE1Y2M2M2NmY2ZiZDU1NGMyNTA5M2Q0MjhiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQyNDJhMjE5NmNlMTM3NjE1Y2M2M2NmY2ZiZDU1NGMyNTA5M2Q0MjhiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo0MCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQxYzQ1Yzc0NzJmNGIyNmRkZDQwNDY5OTMxMGFmY2EwNzQxYmEwNDBmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQxYzQ1Yzc0NzJmNGIyNmRkZDQwNDY5OTMxMGFmY2EwNzQxYmEwNDBmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo0MSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ5N2Q2ZDE5OTY3OWRlZjk0MmJjZmQ2MjI1ZjQzZGQ5NTU2OGUzZWE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ5N2Q2ZDE5OTY3OWRlZjk0MmJjZmQ2MjI1ZjQzZGQ5NTU2OGUzZWE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo0Miwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRhY2M0ZDFjMDEyZmRkYzUwYTNhOGE1YjdhMTE0ZmQzZjk4N2EyYTk5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRhY2M0ZDFjMDEyZmRkYzUwYTNhOGE1YjdhMTE0ZmQzZjk4N2EyYTk5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo0Mywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ0YTA4NTMwNjUwZTg2MWQzNGFjZWU0MzE1MmU1NGRlMjdkMWJjZjgyODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ0YTA4NTMwNjUwZTg2MWQzNGFjZWU0MzE1MmU1NGRlMjdkMWJjZjgyODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo0NSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRlMTgxODFjZjY3MzMwMGE1OGE4MjM1NmYxMTY3OTdmNjRlYjAwYTE1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRlMTgxODFjZjY3MzMwMGE1OGE4MjM1NmYxMTY3OTdmNjRlYjAwYTE1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo0NCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo2MSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ4ODQyNDk1Zjk4OGQ5MDdjOGVjZjhmNmZmMTdlOTBhMzllODlkMmE0ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ4ODQyNDk1Zjk4OGQ5MDdjOGVjZjhmNmZmMTdlOTBhMzllODlkMmE0ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo0OSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQyNGE3OGVjZWQ1MDY3NWFiYzRhODU3OWY1ZDRhM2I3NGYzMzk3ZTU4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQyNGE3OGVjZWQ1MDY3NWFiYzRhODU3OWY1ZDRhM2I3NGYzMzk3ZTU4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo0Nywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQyZWVhN2MwZDdlYmVkZjhhMDY5MzM3NTI1MzZmNmQzMDg5MzNmZTg1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQyZWVhN2MwZDdlYmVkZjhhMDY5MzM3NTI1MzZmNmQzMDg5MzNmZTg1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo0Niwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ1YTlkMzFhNjExNDUwMzgxODc4Yjk3ODNjODkwNDBhMjg3ZDZiMGUzODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ1YTlkMzFhNjExNDUwMzgxODc4Yjk3ODNjODkwNDBhMjg3ZDZiMGUzODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo0OCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRlOGU2YzU4MWRmMTVmMTIwNTFlYTU5ZmQzMDhjMmEwZjc0MzVmZTZlODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRlOGU2YzU4MWRmMTVmMTIwNTFlYTU5ZmQzMDhjMmEwZjc0MzVmZTZlODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo1MCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkMGNlMzJjMmRjMTIyOTY5YTZjZGJmZGEyZjFmNmE2MmYxMjA3ZDRiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkMGNlMzJjMmRjMTIyOTY5YTZjZGJmZGEyZjFmNmE2MmYxMjA3ZDRiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo1Miwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRlODhkNzFlMjdlYTBkYzJiNThhYzBhMzUzOTljNjQ3ZjVkZWI4YzhmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRlODhkNzFlMjdlYTBkYzJiNThhYzBhMzUzOTljNjQ3ZjVkZWI4YzhmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc1YjhjZjlhYjdiODM1NGFlNDkwZDNhOTZjYzhjZTQxMTg3ODdmY2YyNDA1NmQ4ZjQ0Y2I4YWRlY2ZhZTFiM2EiLCJ2b3V0Ijo1Mywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ3NmEwZTI5YTU5OGQ4NWUzYTE2MTdlYWVjMGQ5MWVjMGEyM2Y5ODZjODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ3NmEwZTI5YTU5OGQ4NWUzYTE2MTdlYWVjMGQ5MWVjMGEyM2Y5ODZjODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo2NSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ5MWNiMGY3NjM0Nzg5MWY0Mjc3MDVmNTRkOTNkOTUxMGEwM2UzMDZiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ5MWNiMGY3NjM0Nzg5MWY0Mjc3MDVmNTRkOTNkOTUxMGEwM2UzMDZiODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo2Niwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ0YTA4NTMwNjUwZTg2MWQzNGFjZWU0MzE1MmU1NGRlMjdkMWJjZjgyODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ0YTA4NTMwNjUwZTg2MWQzNGFjZWU0MzE1MmU1NGRlMjdkMWJjZjgyODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo2Nywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ2NjRhOWQ1ZmRhYTk5YjIxZmM5MThjMTVkNDc3MzRiOTdkYmNlZjgxODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ2NjRhOWQ1ZmRhYTk5YjIxZmM5MThjMTVkNDc3MzRiOTdkYmNlZjgxODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo2OSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQxYzQ1Yzc0NzJmNGIyNmRkZDQwNDY5OTMxMGFmY2EwNzQxYmEwNDBmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQxYzQ1Yzc0NzJmNGIyNmRkZDQwNDY5OTMxMGFmY2EwNzQxYmEwNDBmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo2OCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQzNDEwZmU3Y2IwNzhlYThkNTZlMDdlMjEzNzE5OWY1NzM2MTQ4Yzc1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQzNDEwZmU3Y2IwNzhlYThkNTZlMDdlMjEzNzE5OWY1NzM2MTQ4Yzc1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo3MSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQzNDEwZmU3Y2IwNzhlYThkNTZlMDdlMjEzNzE5OWY1NzM2MTQ4Yzc1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQzNDEwZmU3Y2IwNzhlYThkNTZlMDdlMjEzNzE5OWY1NzM2MTQ4Yzc1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo2MCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ1MjkyM2I2NjhmZTI0ZThhMzFlNDIxM2M5ZjVhMWQwZmI5NDc3NDVlODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ1MjkyM2I2NjhmZTI0ZThhMzFlNDIxM2M5ZjVhMWQwZmI5NDc3NDVlODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo3NSwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQ2NjRhOWQ1ZmRhYTk5YjIxZmM5MThjMTVkNDc3MzRiOTdkYmNlZjgxODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQ2NjRhOWQ1ZmRhYTk5YjIxZmM5MThjMTVkNDc3MzRiOTdkYmNlZjgxODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo2Miwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQwMjZjZjA5MjAzMjhkYWJiNTlhM2M2NTRhM2U4ZWE3MzZhOTM1OTU5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQwMjZjZjA5MjAzMjhkYWJiNTlhM2M2NTRhM2U4ZWE3MzZhOTM1OTU5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo2NCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkNTA0MjBiZWIzYjIyNTI3MjFhN2VmODQwZWEwMTVmNDg5NzNjMDc3ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkNTA0MjBiZWIzYjIyNTI3MjFhN2VmODQwZWEwMTVmNDg5NzNjMDc3ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjYwMDMzZjA4NzAyMTUyMmYwNmNiMjEwMTE1MmZmYzRhNmNlZTg3OGNhZjJkYmQ2MTM2NGNhNDQyYjI2YjJhZTgiLCJ2b3V0Ijo2Mywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQzNmMwMDIwZGQzOWU3Y2Q2NmMyMWYyMzdkYzUzZDM4NDY2MWE1NTdmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQzNmMwMDIwZGQzOWU3Y2Q2NmMyMWYyMzdkYzUzZDM4NDY2MWE1NTdmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjljZGM4Y2E2MDNkMWNmNTBhYTEwNmFkMDkzY2M3YjUyNTc4ODc1ZTgzZmFjZGVjMjVlZGM5ZTZlMzMwZjcxMmYiLCJ2b3V0IjoxMSwic2NyaXB0U2lnIjp7ImFzbSI6IjNiNzIzMzhmNWMyZmRhY2JhOWQ5NTJhYmY5OTg4Mjk2M2M3MmMyOTAwNDczYzk4NTRmOTgyZDAxMWU5OWViMWM2NTFiZDcxN2U5MGUxY2Q2NTYxMjA5Y2M3Zjg2MGEwN2Q0ZmM5MWIxZmViMDA0MjNhMzhmMmJhZjY1ZTYwMDAyW0FMTHxGT1JLSUR8VVRYT1NdIDAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2MyIsImhleCI6IjQxM2I3MjMzOGY1YzJmZGFjYmE5ZDk1MmFiZjk5ODgyOTYzYzcyYzI5MDA0NzNjOTg1NGY5ODJkMDExZTk5ZWIxYzY1MWJkNzE3ZTkwZTFjZDY1NjEyMDljYzdmODYwYTA3ZDRmYzkxYjFmZWIwMDQyM2EzOGYyYmFmNjVlNjAwMDI2MTIxMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJlM2U0OWZkNDFhMzM4YTQ4ZmVmODZkNjJiZDE4YTQ5NjIzZTJkMmJjYzUxMjEzNmNiZjdkNzNmNzYzYWExZDVhIiwidm91dCI6MTAsInNjcmlwdFNpZyI6eyJhc20iOiJmMmVjMmY5ODNlZDhhMGMyMmFiODdhYTgzYTlmMWMzYWUyZTVlN2RkNGZmOWQ0Y2Y2NTJlOTE4ZWQzM2NlODliYTY1ZWExYmQ5ZWQ5ZGIwMDVlZWE5ODllYWQxYWEyYTk5ZjY2YTFhYWM4YWVmNzBhYTM1N2M1ODE1ODM2MzQyMFtBTEx8Rk9SS0lEfFVUWE9TXSAwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjMiLCJoZXgiOiI0MWYyZWMyZjk4M2VkOGEwYzIyYWI4N2FhODNhOWYxYzNhZTJlNWU3ZGQ0ZmY5ZDRjZjY1MmU5MThlZDMzY2U4OWJhNjVlYTFiZDllZDlkYjAwNWVlYTk4OWVhZDFhYTJhOTlmNjZhMWFhYzhhZWY3MGFhMzU3YzU4MTU4MzYzNDIwNjEyMTAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2MyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MTAzLjM3MDQ4MTAyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1MGZjMTZmZmIzNzdjZDRlMWQ4YjRkZWZhMzc1NmQxMjIxYmFjZmUzMzA0ZTc3MDVkZTQxNjdkNzdjMWJhMzY0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkZzBjOWhsa2RtdTZuc2EzZHg3bGdtNGQ1Znpyd2swdXZjeXVhYzltZXFrMDRtdXJ3M2tneWhmNW1uN3giXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0ODc1MDkxIn19LHsidmFsdWUiOjkyLjExOTc0OTc5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDFlZTQxNDJmZjkyOGI0YWE2OTJiM2RlOGRlMjkyOWJlMzkwNWM2MDI3ZDNiYThiMGEyY2YxYzBiNTQ4N2FjYjA4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2MHdnOXAwbHk1dGYybmY5djc3M2gzZjl4bHJqcHd4cWY3bmgyOXM1dDgzY3o2NXM3a3RxYWhyOXlhZ2QiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0MzQ0NDkxIn19LHsidmFsdWUiOjcxLjA3NTk4OTQ1LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDJjZjliZjRkYWIwNWEyMzUyMWRjMjYyOTM4MzAyNjJmZmVjNzYxNDY2ODgwOThiYjMxMjRhYzNmYjc2NDAwMzY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2azBuMDZkNHZ6NnlkZnBtc256andwc3ljaGxhM21wZ2U1Z3B4OW14eWoyYzBhaHZzcXJ2ZGZ4Y3Y3eWsiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzMzUyMDM5In19LHsidmFsdWUiOjM2LjAxOTA0NDQyLCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDY3YWQxYzY0OWIwOWJhNThiYTE5ZWIzYWQzNWY0MjQxYmEwNjBlOGJiODk3YWRjZjZiZGRkMzZhODBkOGU4YTY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkbjY2OHJ5bnZ5bTVrOTZyODRuNDU2bGdmcW01cHN3M3d1ZjB0dzBkMHdheDY1cW1yNTJ2cGZuMjVtdGsiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxNjk4NzA3In19LHsidmFsdWUiOjE3LjQ3OTYxODIsIm4iOjQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDIwYjA1Yjc0YWI1NmM5ODQ4MDMyZTliYTRmZDA5NDI3NWNjYzdmMzYyOGFkMTg4NmZjZTU2ZTkzMGQxMjczY2QgT1BfRVFVQUwiLCJoZXgiOiJhYTIwMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHZzdHFrbTU0ZHR2bnB5cXh0NW01bjdzanNuNGVucmx4YzUyNnh5eGxuamtheWNkemZldTY5cmV5em1xeCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjgyNDM2MyJ9fSx7InZhbHVlIjozLjMwMDA5NTcxLCJuIjo1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YW15bmxlcHYzcG5nbnE2cmhxNTk1eDBsZm5nNncwd3A4NWpxM3JydjJ3amNsdmh0M3VneDRrYWo0aHUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxNTU2NDQifX0seyJ2YWx1ZSI6Mi44NzM3ODA0MSwibiI6Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgZWZiYzU1NmQ1Y2ZlOTBhNDU4ZGJmNGQ1N2JkNzg3ZjMzNGJhZmIxYTkzM2ZmYzMzMjIyNjdlYmY0NGMzNGZiMCBPUF9FUVVBTCIsImhleCI6ImFhMjBlZmJjNTU2ZDVjZmU5MGE0NThkYmY0ZDU3YmQ3ODdmMzM0YmFmYjFhOTMzZmZjMzMyMjI2N2ViZjQ0YzM0ZmIwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMGhtYzR0ZHRubGZwZnpjbTA2ZDI3N2hzbGVuZndobXIyZm5sbHBueWduOGEwNnljZDhtcWRhNWV2cHMyIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTM1NTM4In19LHsidmFsdWUiOjIuMTM0NzE1MDUsIm4iOjcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGE2NGE2NTMwOTE2MzMyOTgwNjI3NWE1YTk5YjI3ODUyNGIyMWE0NjU1OWRlNDdlZmNjZTA5OGQzZWI1YjJhMGUgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTY0YTY1MzA5MTYzMzI5ODA2Mjc1YTVhOTliMjc4NTI0YjIxYTQ2NTU5ZGU0N2VmY2NlMDk4ZDNlYjViMmEwZTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdueTVlZnNqOTNuOXhxeHlhZDk0eGRqMHBmeWtnZHl2NHZhdTNsMGVuc2YzNWx0dHY0cXUyeHJzMzZ3aCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjEwMDY4MyJ9fSx7InZhbHVlIjoyLjEzMjEwMzUyLCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAwZWY0ZmFhZTgwOWI5ZTM1YmE3NTVmMTUxM2RjZmFhMzNhMDMwZGRkZjU2OGIzNTlhNjM3ZTY3Y2U1YjcyODc2IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDBlZjRmYWFlODA5YjllMzViYTc1NWYxNTEzZGNmYWEzM2EwMzBkZGRmNTY4YjM1OWE2MzdlNjdjZTViNzI4NzY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2ODBmNzR3c3pkZXVkZDZ3NDAzMnk3dWwyM241cWNkbWg2azN2NmU1Y203dmw4OWt1NTh2ODJraGZoOTQiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMDA1NTkifX0seyJ2YWx1ZSI6Mi41MzIxOTUwMSwibiI6OSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYSBPUF9FUVVBTCIsImhleCI6ImFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGdqNjluMnUyOHQwenZnNTBscjN1a210OXZ4cHBnZGNyZjlhcTNjdTU2bGRzcHl4bDR3NWhzZnltOHM3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTE5NDI3In19LHsidmFsdWUiOjIuNTM2OTYxNiwibiI6MTAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnajY5bjJ1Mjh0MHp2ZzUwbHIzdWttdDl2eHBwZ2RjcmY5YXEzY3U1Nmxkc3B5eGw0dzVoc2Z5bThzNyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjExOTY1MiJ9fSx7InZhbHVlIjoyLjUzMTY4OTU5LCJuIjoxMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYSBPUF9FUVVBTCIsImhleCI6ImFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGdqNjluMnUyOHQwenZnNTBscjN1a210OXZ4cHBnZGNyZjlhcTNjdTU2bGRzcHl4bDR3NWhzZnltOHM3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTE5NDA0In19LHsidmFsdWUiOjIuNTMyMjEyMTEsIm4iOjEyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkZ2o2OW4ydTI4dDB6dmc1MGxyM3VrbXQ5dnhwcGdkY3JmOWFxM2N1NTZsZHNweXhsNHc1aHNmeW04czciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMTk0MjkifX0seyJ2YWx1ZSI6Mi40OTAwNTAwMiwibiI6MTMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnajY5bjJ1Mjh0MHp2ZzUwbHIzdWttdDl2eHBwZ2RjcmY5YXEzY3U1Nmxkc3B5eGw0dzVoc2Z5bThzNyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjExNzQzOSJ9fSx7InZhbHVlIjoxMS4zMzUzNzk2NiwibiI6MTQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGIzNDdkNjhjOGQyY2RiMzIwNjZiZTlhZjgwNGM2ZmZiOGFiYmI0NDFiZGYzNmNhZjM4MzQzNmJjZmNhYTM1Y2IgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdlNTA0NXYzNWtka3ZzeGQwNTZscXp2ZGxhYzR3YTVneDdseG05MDhxNnJkMDh1NGc2dWtuMzcyZ2d3bCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjUzNDU5MyJ9fSx7InZhbHVlIjoyLjQ2MjA1MTc0LCJuIjoxNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYSBPUF9FUVVBTCIsImhleCI6ImFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGdqNjluMnUyOHQwenZnNTBscjN1a210OXZ4cHBnZGNyZjlhcTNjdTU2bGRzcHl4bDR3NWhzZnltOHM3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTE2MTIwIn19LHsidmFsdWUiOjEuNzg5OTM4MjUsIm4iOjE2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDIwYjA1Yjc0YWI1NmM5ODQ4MDMyZTliYTRmZDA5NDI3NWNjYzdmMzYyOGFkMTg4NmZjZTU2ZTkzMGQxMjczY2Q4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2c3Rxa201NGR0dm5weXF4dDVtNW43c2pzbjRlbnJseGM1MjZ4eXhsbmprYXljZHpmZXU2OXJleXptcXgiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI4NDQyMSJ9fSx7InZhbHVlIjoyLjM0NzI0NjI5LCJuIjoxNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYzhkZDk3N2FlOTkxMmI3MjEzNDFiYWYzMDg2ODdiZTViOWVmYjMyY2MwZmJmMjY5NjY2OTBiNjE5OGUyMDFjZiBPUF9FUVVBTCIsImhleCI6ImFhMjBjOGRkOTc3YWU5OTEyYjcyMTM0MWJhZjMwODY4N2JlNWI5ZWZiMzJjYzBmYmYyNjk2NjY5MGI2MTk4ZTIwMWNmODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMHlkbTltNmF4Z2prdXNuZ3hhMHh6cmcwMGptbm1hbjlucTBodW5mdmU1c2tjdmN1Z3F1N3J1YWd3dmVsIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTEwNzA0In19LHsidmFsdWUiOjEwLjUzMjExMTEyLCJuIjoxOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2MyYmNjYTIwODlhOWYwZjhkYWVjMGQzYjQ1YTZjNTI1ODFjNTY3NjY1Y2M1MWViZGE5YTIwMmQ4YTMzZGNmYyBPUF9FUVVBTCIsImhleCI6ImFhMjA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZDd6aG45enB6ZGY3cnVkNG1xZDhkejZkM2Y5czh6a3dlanVjNTB0bTJkenF0djJ4MHcwYzU0eTM5aHA2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDk2NzA5In19LHsidmFsdWUiOjEuMjQxMTQ0OTgsIm4iOjE5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBjYjk0NWUyNjUzNDM2YjA1MGViNWZkNjRiMzU3MDUwYTIzYTBmMjdhYWZmOWQxZTUyOTYwYjgwNjA4YzkxMmRlIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGNiOTQ1ZTI2NTM0MzZiMDUwZWI1ZmQ2NGIzNTcwNTBhMjNhMGYyN2FhZmY5ZDFlNTI5NjBiODA2MDhjOTEyZGU4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwOWVnaDN4MmRwa2twZ3draDdrZnY2aHE1OXo4ZzhqMDJobG41MDk5OXN0c3BzZ2V5ZmR1MGdkbmR5aHEiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI1ODUzOCJ9fSx7InZhbHVlIjowLjY5NzMzNzk3LCJuIjoyMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYTIwZjE2OTc3ODk3NzFhMjdkODBjZGIyODJhZGM2NTE1NWIxYWQxNGZkY2IzMmUxYjI2ZmRkMzg2ZDg4NzM1MyBPUF9FUVVBTCIsImhleCI6ImFhMjBhMjBmMTY5Nzc4OTc3MWEyN2Q4MGNkYjI4MmFkYzY1MTU1YjFhZDE0ZmRjYjMyZTFiMjZmZGQzODZkODg3MzUzODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzNxNzk1aDB6dGhyZ25hc3J4bTlxNGRjZWc0dHZkZHpuN3Vrdmhwa2ZoYTZ3cmQzcGU0eDh5bjVqcjhxIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzI4OTEifX0seyJ2YWx1ZSI6MC42ODUzMTU3MSwibiI6MjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDE0NTY3NzY1MWVjMTIyMWNjN2YwZmIxZWI5NmU5ZTM0YjkxMGMzYTViMjcyOTQyYzk1ZTUzYzRkYjk5YmNhNzIgT1BfRVFVQUwiLCJoZXgiOiJhYTIwMTQ1Njc3NjUxZWMxMjIxY2M3ZjBmYjFlYjk2ZTllMzRiOTEwYzNhNWIyNzI5NDJjOTVlNTNjNGRiOTliY2E3Mjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHYyOXZhbTlybXFqeTh4ODdyYTNhd3R3bmM2dGp5eHI1a2U4OTlwdmpoam5jbmRlbjA5OHlkbjJ2eGE2cyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjMyMzI0In19LHsidmFsdWUiOjEuMjU0Mjg3NjUsIm4iOjIyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyZGQ3MDU1YzkyNjI1MzI1MmViNDRmN2VhNDEzOWRiMjQ1ZTQ5MmFhZGMxOWU5ZTVjZmU5NWEyZWM4N2NiMDYwIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDJkZDcwNTVjOTI2MjUzMjUyZWI0NGY3ZWE0MTM5ZGIyNDVlNDkyYWFkYzE5ZTllNWNmZTk1YTJlYzg3Y2IwNjA4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2a2F3cDJ1amYzOXhmZndrMzhoYWZxbm5rZXl0ZXlqNHR3cG42MDllbDU0NXRrZzBqY3hxbjQ4Yzd6eWUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI1OTE1NyJ9fSx7InZhbHVlIjoxLjIzMzY0MjYxLCJuIjoyMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZCBPUF9FUVVBTCIsImhleCI6ImFhMjBhYWVhNjQ5ZjcxOTNhYjQzMjk5OTk5M2JkOGQyY2NjZTVmYWJkM2FmMDFlYmZlNDFhMGUwYTM1ODEwNDRkNDZkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzR3NWV5bHd4ZjZrc2Vmbnh2bmhreGplbjg5bDI3bjR1cTdobGpwNXJzMnhrcXNnbjJ4NjhnbnV2emVrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTgxODQifX0seyJ2YWx1ZSI6NS4yMTA5NDc1OSwibiI6MjQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwZjFjMzNiYzdkYTA5OTFkMjZmOGJiZjE1MGM5MDM1NjhiMmMzZGM4NzI3MzcwODYzZjRjM2Q5Zjk2NTViMzc5MTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cDBjdXh3NzhtZ3llcjVuMDN3bDMycnlzeDQ1dDlzN3VzdW5ud3pycjducGFuN3Q5dHZtZXpyYXUzanVyOSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjI0NTc1NyJ9fSx7InZhbHVlIjowLjU1NjMxODkyLCJuIjoyNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2FmZDRkZThhZmZiMDdjYjY2ODMxMzRhZDU5ZTljN2E3NDNhNzBlNjE0YWQ2MjFhZmY1YTExMDM3YzAxMTVlMiBPUF9FUVVBTCIsImhleCI6ImFhMjA3YWZkNGRlOGFmZmIwN2NiNjY4MzEzNGFkNTllOWM3YTc0M2E3MGU2MTRhZDYyMWFmZjVhMTEwMzdjMDExNWUyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGEwNm4wZzRsYXMwam14c3ZmNTQ0djduM2E4Z3duc3VjMjI2Y3M2bGFkcHpxbXVxeTI3eTZqa2V1NnpnIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjYyNDEifX0seyJ2YWx1ZSI6MC41NTEwNTA2NCwibiI6MjYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDY4ZDY4NGRlMDRiODUwMWZiOTJkNjFmMTI3ZWFhOGE4OGU5ODRjMTQ1ZTMyMDZmZWNmODRlNzRlNGY4ZGY5NDUgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNjhkNjg0ZGUwNGI4NTAxZmI5MmQ2MWYxMjdlYWE4YTg4ZTk4NGMxNDVlMzIwNmZlY2Y4NGU3NGU0ZjhkZjk0NTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGQ1ZGRweDdxanU5cThhZTk0c2x6ZmwyNHo1Z2F4enZ6MzByeXBoN2U3end3bmowM2h1NTJ6cGQzbXI5biJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjI1OTkxIn19LHsidmFsdWUiOjEuMDc3NjYzMDksIm4iOjI3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzZjk5MmQxMjkwNDExMzc0OGM0NDQzYzE2NGVjZjEyZjQ4ZDFiMDc1MTgwOGUyYjUzNzE4NDk3ZTEyZjk0YjQxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNmOTkyZDEyOTA0MTEzNzQ4YzQ0NDNjMTY0ZWNmMTJmNDhkMWIwNzUxODA4ZTJiNTM3MTg0OTdlMTJmOTRiNDE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2bGVqdGdqanBxM3hheXZnM3B1emU4djd5aDUzNWRzdzV2cTNjNDR4dXZ5amxzamw5OTV6N3E4dzJ5OGsiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI1MDgyOCJ9fSx7InZhbHVlIjoxLjA2Mjc3NjI2LCJuIjoyOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2FmZDRkZThhZmZiMDdjYjY2ODMxMzRhZDU5ZTljN2E3NDNhNzBlNjE0YWQ2MjFhZmY1YTExMDM3YzAxMTVlMiBPUF9FUVVBTCIsImhleCI6ImFhMjA3YWZkNGRlOGFmZmIwN2NiNjY4MzEzNGFkNTllOWM3YTc0M2E3MGU2MTRhZDYyMWFmZjVhMTEwMzdjMDExNWUyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGEwNm4wZzRsYXMwam14c3ZmNTQ0djduM2E4Z3duc3VjMjI2Y3M2bGFkcHpxbXVxeTI3eTZqa2V1NnpnIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTAxMjUifX0seyJ2YWx1ZSI6MS4wNjM1MjEwMywibiI6MjksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGFhZWE2NDlmNzE5M2FiNDMyOTk5OTkzYmQ4ZDJjY2NlNWZhYmQzYWYwMWViZmU0MWEwZTBhMzU4MTA0NGQ0NmQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHc0dzVleWx3eGY2a3NlZm54dm5oa3hqZW44OWwyN240dXE3aGxqcDVyczJ4a3FzZ24yeDY4Z251dnplayJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjUwMTYxIn19LHsidmFsdWUiOjAuNTI1Njg3ODMsIm4iOjMwLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA5ZTcyNTU1MTM4MTI4ZTA1Yzg0NWI3MGEzMDg0YjE5ZGE0MTRhNTAzZGZiNzU5NmQxMWZjZjU0MjI4MDBhMTkwIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDllNzI1NTUxMzgxMjhlMDVjODQ1YjcwYTMwODRiMTlkYTQxNGE1MDNkZmI3NTk2ZDExZmNmNTQyMjgwMGExOTA4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3MDh5NDIzOHFmZ3Vwd2dna21zNXZ5eWt4dzZnOTk5cTAwbXdrdGR6ODcwMnMzZ3F6c2VxbnJudmEyeWEiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyNDc5NiJ9fSx7InZhbHVlIjowLjUxNjQzMDAyLCJuIjozMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWRlZDBiYzBhNTFiZTkyZmI2NDY0MjY3ZDk5NDA1NGVkMGU3MzJhYmE0NDgwZDE2ZDEzYTMwNGFkM2IwNTZlMSBPUF9FUVVBTCIsImhleCI6ImFhMjAxZGVkMGJjMGE1MWJlOTJmYjY0NjQyNjdkOTk0MDU0ZWQwZTczMmFiYTQ0ODBkMTZkMTNhMzA0YWQzYjA1NmUxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnc3Nno3cTU1ZDdqdGFrZ2VweDBrdjVxNDhkcGVlajR3anlzcmdrNnlhcnFqa25rcHR3enA3aDZ4NzI5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjQzNTkifX0seyJ2YWx1ZSI6NC4zOTc1ODMwMywibiI6MzIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwM2JiMjRmZjkwYjIyMTlhMjYwZDBlZTBhMTY4NjdmZDMzNDY5Y2Y3MDRmNDkwMjIzMWIxNGU5NjNlY2JhZTNjNDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHZhbXlubGVwdjNwbmducTZyaHE1OTV4MGxmbmc2dzB3cDg1anEzcnJ2MndqY2x2aHQzdWd4NGthajRodSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjIwNzM5NiJ9fSx7InZhbHVlIjowLjk4OTU1NzUzLCJuIjozMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYTY0YTY1MzA5MTYzMzI5ODA2Mjc1YTVhOTliMjc4NTI0YjIxYTQ2NTU5ZGU0N2VmY2NlMDk4ZDNlYjViMmEwZSBPUF9FUVVBTCIsImhleCI6ImFhMjBhNjRhNjUzMDkxNjMzMjk4MDYyNzVhNWE5OWIyNzg1MjRiMjFhNDY1NTlkZTQ3ZWZjY2UwOThkM2ViNWIyYTBlODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd255NWVmc2o5M245eHF4eWFkOTR4ZGowcGZ5a2dkeXY0dmF1M2wwZW5zZjM1bHR0djRxdTJ4cnMzNndoIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDY2NzIifX0seyJ2YWx1ZSI6MC45NzUzNzQ3OCwibiI6MzQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDdhZmQ0ZGU4YWZmYjA3Y2I2NjgzMTM0YWQ1OWU5YzdhNzQzYTcwZTYxNGFkNjIxYWZmNWExMTAzN2MwMTE1ZTIgT1BfRVFVQUwiLCJoZXgiOiJhYTIwN2FmZDRkZThhZmZiMDdjYjY2ODMxMzRhZDU5ZTljN2E3NDNhNzBlNjE0YWQ2MjFhZmY1YTExMDM3YzAxMTVlMjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRhMDZuMGc0bGFzMGpteHN2ZjU0NHY3bjNhOGd3bnN1YzIyNmNzNmxhZHB6cW11cXkyN3k2amtldTZ6ZyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ2MDAyIn19LHsidmFsdWUiOjAuNDg1MTAwNDUsIm4iOjM1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBmZTllYjcyMzQ2MjljMWI4Y2I0OGZiNmIwZTNiNzVkOWI3NmE2ZjE4NmI2MTU2OTI4ZmVlNTk0NGNjOTBkNDY0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMGZlOWViNzIzNDYyOWMxYjhjYjQ4ZmI2YjBlM2I3NWQ5Yjc2YTZmMTg2YjYxNTY5MjhmZWU1OTQ0Y2M5MGQ0NjQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwbGZhZGVyZ2M1dXJ3eHRmcmFra3IzbXdodm13Nm4wcnA0a3o0NWozbGg5ajN4dmpyMnhnanE1amw2dTgiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMjg4MiJ9fSx7InZhbHVlIjowLjQ4NDMwMjA1LCJuIjozNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMzExOWYyOWU5MDNkNDY4YmYzZDYxOGRlMGM1NTBmMGIwMjE4NWJiNDZiMTNmOGNmYzRjOGJjODNhNzJjNmZiZSBPUF9FUVVBTCIsImhleCI6ImFhMjAzMTE5ZjI5ZTkwM2Q0NjhiZjNkNjE4ZGUwYzU1MGYwYjAyMTg1YmI0NmIxM2Y4Y2ZjNGM4YmM4M2E3MmM2ZmJlODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmMzbnU1N2pxNzVkemxuNmN2ZHVyejRwdTlzeXh6bWszNDM4N3gwY255dGVxYTg5M2htdXNsZXQ0MHljIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjI4NDMifX0seyJ2YWx1ZSI6MC40ODU4MDAwNywibiI6MzcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGFhYWJkMDViOGMxNWI2MTRmNmYzNzBjOWJlNDVmYTE5YjhmNWI4YTkwNGQwMmNmOGE2NWM3ODYwODVkNzI2MzIgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYWFhYmQwNWI4YzE1YjYxNGY2ZjM3MGM5YmU0NWZhMTliOGY1YjhhOTA0ZDAyY2Y4YTY1Yzc4NjA4NWQ3MjYzMjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHc0Mmg1em0zczJtdjk4azdkY3ZuMGo5bGd2bTNhZGM0eXpkcXQ4YzVldzhzY3k5NnVucnllODl5ZXZtZSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjIyOTE0In19LHsidmFsdWUiOjAuNDU2ODkzNjEsIm4iOjM4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzZjlkMzczMjg4NzQ4MDA4YzU0OGZmNDUwODIxOWZmM2RmMmU5ZTRkYWRiZjI0M2FmMWU1N2U0NmJmMmI1Y2U5IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNmOWQzNzMyODg3NDgwMDhjNTQ4ZmY0NTA4MjE5ZmYzZGYyZTllNGRhZGJmMjQzYWYxZTU3ZTQ2YmYyYjVjZTk4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2bGU2ZGVqM3A2Z3F6eDlmcmw1MnpwcG5sZWE3dDU3ZmtrbTdmcDY3OGpodTM0bDlkd3dqMmd6MmtrYXkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMTU1MSJ9fSx7InZhbHVlIjowLjQzOTYxMjQxLCJuIjozOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMzdkMTZkYzAxNjhjZDA3ZWMzMmI1ZWMxZTM4MTdmMjVmYTRjZTU4OTQ3ODNlZGY5ZDFiY2QyZmFmZDEwMDJmYiBPUF9FUVVBTCIsImhleCI6ImFhMjAzN2QxNmRjMDE2OGNkMDdlYzMyYjVlYzFlMzgxN2YyNWZhNGNlNTg5NDc4M2VkZjlkMWJjZDJmYWZkMTAwMmZiODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdm1hem13cXo2eGRxbGtyOWQwdnJjdXAwdWpsNW44OTM5cmM4bTBlNng3ZDk3aGF6cXAwa2V6M3RtZWpmIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjA3MzYifX0seyJ2YWx1ZSI6MC44NjI4MjQ5MSwibiI6NDAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGMzZTdkY2NlNjNlMzdhYjJkZTNiZjhhN2Q5MzJjZmEwMjkzNzQ5MDJjNjMxMGU4ZWRjYjMxMTgxMjM0NTc2N2EgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYzNlN2RjY2U2M2UzN2FiMmRlM2JmOGE3ZDkzMmNmYTAyOTM3NDkwMmM2MzEwZThlZGNiMzExODEyMzQ1NzY3YTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cDBwNzBoeHd2MDNoNHZrNzgwdTIwa2ZqZTdzempkNmZxdHJyenI1d21qZTNycWZyZzRtODV4MjcwZjU2diJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQwNjk1In19LHsidmFsdWUiOjAuODQyNDAyMDIsIm4iOjQxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAxZDc5Nzc5ZDZiODIzNjI4MTljYzFhNWE5NTFiOWQ3YzlmNzJlN2ExYWY0NzU3NzZkOTQ0ZjM1NGQ3NjMzOWYzIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDFkNzk3NzlkNmI4MjM2MjgxOWNjMWE1YTk1MWI5ZDdjOWY3MmU3YTFhZjQ3NTc3NmQ5NDRmMzU0ZDc2MzM5ZjM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2d2hqYXVhZHdwcnYycWVlc2Q5NDlnbW40N2Y3dWg4NXhoNXc0bWttOXoweDR4aHZ2dWx4YzZyNjM0aDYiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzOTczMSJ9fSx7InZhbHVlIjowLjQyMTY4MjU3LCJuIjo0Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOGRhZDcxYTk3YmMxZGM5ZTJiZWQ5ZjZhYTE0YmNlMTk4OThkM2NlODE0MmY2NWQyMTg1ODFkYWY0Mjc0NTMxYyBPUF9FUVVBTCIsImhleCI6ImFhMjA4ZGFkNzFhOTdiYzFkYzllMmJlZDlmNmFhMTRiY2UxOTg5OGQzY2U4MTQyZjY1ZDIxODU4MWRhZjQyNzQ1MzFjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3g2NnVkZjAwcWFlODN0YWswazRnMnRlY3ZjbnJmdWFxMno3ZXdqcnB2cG10Nnp3M2YzYzc1cmd6azN5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTk4ODkifX0seyJ2YWx1ZSI6MC43ODkwMjkwNCwibiI6NDMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDhkYWQ3MWE5N2JjMWRjOWUyYmVkOWY2YWExNGJjZTE5ODk4ZDNjZTgxNDJmNjVkMjE4NTgxZGFmNDI3NDUzMWMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwOGRhZDcxYTk3YmMxZGM5ZTJiZWQ5ZjZhYTE0YmNlMTk4OThkM2NlODE0MmY2NWQyMTg1ODFkYWY0Mjc0NTMxYzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHd4NjZ1ZGYwMHFhZTgzdGFrMGs0ZzJ0ZWN2Y25yZnVhcTJ6N2V3anJwdnBtdDZ6dzNmM2M3NXJnemszeSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjM3MjE0In19LHsidmFsdWUiOjAuNzg2NjAzMTksIm4iOjQ0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA0NjE5ZjBiMjlkMWVlNzA1NDY3ODZiOGIzMGM3NWJiZTViMTViZGI5OGQwMWE2YWRkM2E0MTQ3MDAwZjg2ZTYxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDQ2MTlmMGIyOWQxZWU3MDU0Njc4NmI4YjMwYzc1YmJlNWIxNWJkYjk4ZDAxYTZhZGQzYTQxNDcwMDBmODZlNjE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkcnBudTlqbjUwd3dwMngwcDRja3Z4OHR3bDlrOWRhaHh4c3JmNGQ2d2pwZ3VxcWxwaHh6ZXVwdGR4Nm4iXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzNzEwMCJ9fSx7InZhbHVlIjowLjM2OTIyNTIxLCJuIjo0NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOGRhZDcxYTk3YmMxZGM5ZTJiZWQ5ZjZhYTE0YmNlMTk4OThkM2NlODE0MmY2NWQyMTg1ODFkYWY0Mjc0NTMxYyBPUF9FUVVBTCIsImhleCI6ImFhMjA4ZGFkNzFhOTdiYzFkYzllMmJlZDlmNmFhMTRiY2UxOTg5OGQzY2U4MTQyZjY1ZDIxODU4MWRhZjQyNzQ1MzFjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3g2NnVkZjAwcWFlODN0YWswazRnMnRlY3ZjbnJmdWFxMno3ZXdqcnB2cG10Nnp3M2YzYzc1cmd6azN5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTc0MTYifX0seyJ2YWx1ZSI6MC4yOTk4MDMzNiwibiI6NDYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGMzZTdkY2NlNjNlMzdhYjJkZTNiZjhhN2Q5MzJjZmEwMjkzNzQ5MDJjNjMxMGU4ZWRjYjMxMTgxMjM0NTc2N2EgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYzNlN2RjY2U2M2UzN2FiMmRlM2JmOGE3ZDkzMmNmYTAyOTM3NDkwMmM2MzEwZThlZGNiMzExODEyMzQ1NzY3YTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cDBwNzBoeHd2MDNoNHZrNzgwdTIwa2ZqZTdzempkNmZxdHJyenI1d21qZTNycWZyZzRtODV4MjcwZjU2diJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjE0MTQyIn19LHsidmFsdWUiOjAuMjk3NDAyODMsIm4iOjQ3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzN2QxNmRjMDE2OGNkMDdlYzMyYjVlYzFlMzgxN2YyNWZhNGNlNTg5NDc4M2VkZjlkMWJjZDJmYWZkMTAwMmZiIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDM3ZDE2ZGMwMTY4Y2QwN2VjMzJiNWVjMWUzODE3ZjI1ZmE0Y2U1ODk0NzgzZWRmOWQxYmNkMmZhZmQxMDAyZmI4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2bWF6bXdxejZ4ZHFsa3I5ZDB2cmN1cDB1amw1bjg5MzlyYzhtMGU2eDdkOTdoYXpxcDBrZXozdG1lamYiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxNDAyOCJ9fSx7InZhbHVlIjowLjI5Mzc4NTY3LCJuIjo0OCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMzZiODU4ZjY3OWM1YjA5YjA4OGVmZjE5MDdmMTVkMjk4YjliNzUxMzI1NTVmZjZhMWQyMTA2MGQxNmZkODQ2OCBPUF9FUVVBTCIsImhleCI6ImFhMjAzNmI4NThmNjc5YzViMDliMDg4ZWZmMTkwN2YxNWQyOThiOWI3NTEzMjU1NWZmNmExZDIxMDYwZDE2ZmQ4NDY4ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdm10c2s4azA4em1weGNnM21sM2pwbDN0NTVjaHhtNHp2ajR0bG0ycjVzc3ZyZ2tsa3p4c3FmeDduemN0Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTM4NTgifX0seyJ2YWx1ZSI6MC4yNjU0Mzg0MiwibiI6NDksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGI0Y2MxY2RmNjE3N2M3ZGU3YjBiYTA0MjBjNGQ4N2E5ZDNkM2JhYTdhMTRmMDIyOTc4NWNhNGZjZTA1ZWQ0YWQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYjRjYzFjZGY2MTc3YzdkZTdiMGJhMDQyMGM0ZDg3YTlkM2QzYmFhN2ExNGYwMjI5Nzg1Y2E0ZmNlMDVlZDRhZDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHc2dmM4eGx2OW11MGhubXB3c3l5cnpkczc1YTg1YTY1N3M1N3EzZjBwdzJmbDhxdG0yMjZ4NGczNnRlOSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjEyNTIwIn19LHsidmFsdWUiOjAuMjQyMjE3MSwibiI6NTAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGFhZWE2NDlmNzE5M2FiNDMyOTk5OTkzYmQ4ZDJjY2NlNWZhYmQzYWYwMWViZmU0MWEwZTBhMzU4MTA0NGQ0NmQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHc0dzVleWx3eGY2a3NlZm54dm5oa3hqZW44OWwyN240dXE3aGxqcDVyczJ4a3FzZ24yeDY4Z251dnplayJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjExNDI2In19LHsidmFsdWUiOjAuMjIwMzgwODUsIm4iOjUxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAxNzRlYzg4YzMwZTlmMzQ2OTA4MjVmNGNkNWY2NzE0MDQ5NTk2Y2RjYjdkZjA4NThjZTRiMWNmN2E1ODJlYjFlIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDE3NGVjODhjMzBlOWYzNDY5MDgyNWY0Y2Q1ZjY3MTQwNDk1OTZjZGNiN2RmMDg1OGNlNGIxY2Y3YTU4MmViMWU4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2dDVhanl2eHI1bHgzNXNzZjA1ZTQwa3c5cXlqa3R2bWptYTd6emNlZTkzZWFhOXN0NDN1Y2Q5bHdybDciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMDM5NSJ9fSx7InZhbHVlIjowLjIxNjU4MTI2LCJuIjo1Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMzZiN2I3ODk4ZGFhYTg5NWQxMTNhZjk1OTBkNmE0OWRlMDZjZTBjNDEwYTZhNDc0ZGIwMTVlMTJmMzQ2MTRiOSBPUF9FUVVBTCIsImhleCI6ImFhMjAzNmI3Yjc4OThkYWFhODk1ZDExM2FmOTU5MGQ2YTQ5ZGUwNmNlMGM0MTBhNmE0NzRkYjAxNWUxMmYzNDYxNGI5ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdm10MGR1ZjNrNDIzOXczendoZXR5eGs1anc3cW04cWNzZzJkZnI1bXZxNHV5aG5nYzJ0anQ0aGdjbGRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTAyMTcifX0seyJ2YWx1ZSI6MC4xMjIxMjM2NCwibiI6NTMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGE2NGE2NTMwOTE2MzMyOTgwNjI3NWE1YTk5YjI3ODUyNGIyMWE0NjU1OWRlNDdlZmNjZTA5OGQzZWI1YjJhMGUgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTY0YTY1MzA5MTYzMzI5ODA2Mjc1YTVhOTliMjc4NTI0YjIxYTQ2NTU5ZGU0N2VmY2NlMDk4ZDNlYjViMmEwZTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdueTVlZnNqOTNuOXhxeHlhZDk0eGRqMHBmeWtnZHl2NHZhdTNsMGVuc2YzNWx0dHY0cXUyeHJzMzZ3aCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjU3NjEifX0seyJ2YWx1ZSI6MC4yMDI4NzYxMiwibiI6NTQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGZhODkwNDZjMTM0ZjNiYTdkOGU4OTY1NGFiZDhiZWU4MjM4NTU4NTNkNzU0NmZhY2I3Nzk2ZDhlNGYzOTM0ODEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwZmE4OTA0NmMxMzRmM2JhN2Q4ZTg5NjU0YWJkOGJlZTgyMzg1NTg1M2Q3NTQ2ZmFjYjc3OTZkOGU0ZjM5MzQ4MTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cDBhZ2pwcnZ6ZDhuaGY3Y2F6dDlmMjdjaG01ejhwMmMyMHQ0Z21hdmthdWttcmowOHk2Z3o3dGpyZ2h6ZyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6Ijk1NzAifX0seyJ2YWx1ZSI6MC4yMDU5MDUwNywibiI6NTUsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDFmYTJhMDQzMDk0YmQyYmY0NTA2ZGMzNDdkMWM2YzE4NTJhMTA1NWVlMDVhYzYyMzQyNGMxY2Q1ZGY1MjhiOWMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwMWZhMmEwNDMwOTRiZDJiZjQ1MDZkYzM0N2QxYzZjMTg1MmExMDU1ZWUwNWFjNjIzNDI0YzFjZDVkZjUyOGI5Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHYwNjlnenJwOTlhOTA2OXFtd3JnbGd1ZHN2OTlnZzl0bXM5NDMzcmdmeHBlNHdsMjI5ZWN1ZHZ2bHgzOCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6Ijk3MTMifX0seyJ2YWx1ZSI6MC4yMDM1NDE1OSwibiI6NTYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDliZDUzYjk5NmY4ZDhhMGE1ZWM0MzFlN2NlOTRiOGFlNWIyYjU3NTcwZDY2OGY2YTcyMTdlM2JmMDczZDU1MDYgT1BfRVFVQUwiLCJoZXgiOiJhYTIwOWJkNTNiOTk2ZjhkOGEwYTVlYzQzMWU3Y2U5NGI4YWU1YjJiNTc1NzBkNjY4ZjZhNzIxN2UzYmYwNzNkNTUwNjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdkYTJ3dWVkN3hjNXpqN2NzYzcwbjU1aHpoOWsyNmgydXhrZHJtMndndDc4MGM4ODQyc3ZlcDhweXh0OCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6Ijk2MDIifX0seyJ2YWx1ZSI6MC4yMDI0Mzg0OCwibiI6NTcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDcwZjZlZGM1NTY1N2FiYzhhZWViZDdkMWU0N2JhZGFmZWQ2NmI0MzJkNWM4OTllMzgxZWIyZmI5YWI4YjczOWQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNzBmNmVkYzU1NjU3YWJjOGFlZWJkN2QxZTQ3YmFkYWZlZDY2YjQzMmQ1Yzg5OWUzODFlYjJmYjlhYjhiNzM5ZDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRjMGRtdzkyZXQ2aGo5d2EwdGFyZXJtNGtoNzZlNDV4dDJ1M3gwcnM4NGpsd2R0M2RlZTZ0Y3RuOG5hdCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6Ijk1NDkifX0seyJ2YWx1ZSI6MC4xOTY0MTkwOCwibiI6NTgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDNmNDQ1NTU2YmJlNWM1NTYxNzhkZTg2NTVmMTkwNzUyMGY0NmQwNDMyZjBkMWZlODliMzVhODI2ZTNmZDEzNTQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwM2Y0NDU1NTZiYmU1YzU1NjE3OGRlODY1NWYxOTA3NTIwZjQ2ZDA0MzJmMGQxZmU4OWIzNWE4MjZlM2ZkMTM1NDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHZsNWc0MmtoMGp1MjRzaDNoNXgyaGNlcWFmcTcza3NndmhzNjhsZ252NjZzZmhybDVmNGdldms3eHd4eSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjkyNjUifX0seyJ2YWx1ZSI6MC4xOTI0NTM4LCJuIjo1OSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOGNkZWY3YmViNzEzZDI1NjY0NjhmYWNhY2Q3NGI5YjI5MjUyODljNDJiNzRiYjM4OGRjNDFkMTNhMTFmZGZhZiBPUF9FUVVBTCIsImhleCI6ImFhMjA4Y2RlZjdiZWI3MTNkMjU2NjQ2OGZhY2FjZDc0YjliMjkyNTI4OWM0MmI3NGJiMzg4ZGM0MWQxM2ExMWZkZmFmODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3hkYWFhN2t1ZmF5NG55ZHJhdjRudDVoeGVmeTU1ZmNzNGhmd2VjM2h6cDZ5YXBybDA2N242aHVwcmdrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiOTA3OCJ9fSx7InZhbHVlIjowLjE4NTQ3ODU0LCJuIjo2MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmIwOWQ1MWVlNzVkZDYwYjc1Nzg5ZjIyZWZlOTFlNzk5ZGRlMjkxYjBjYjdjYmIwNTY2ZGI5OTgwMjdhMzYwMiBPUF9FUVVBTCIsImhleCI6ImFhMjAyYjA5ZDUxZWU3NWRkNjBiNzU3ODlmMjJlZmU5MWU3OTlkZGUyOTFiMGNiN2NiYjA1NjZkYjk5ODAyN2EzNjAyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjRzbjRnN3Vhd2F2em00MHowajltbGZyZXVlbWgzZnJ2eHQwamFzMmVrbW54cXowZ21xeXYwYWo2ZmZnIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODc1MCJ9fSx7InZhbHVlIjowLjEwNDIwODIyLCJuIjo2MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2VkMjY3YzdlMzM1OGNjYjhkNWIzMjY3YjJmYjJlYWZmMzc0YmE1ZGI2YWNjNjlhYzFlYWRkOTQyNzIxZWNhYyBPUF9FUVVBTCIsImhleCI6ImFhMjAzZWQyNjdjN2UzMzU4Y2NiOGQ1YjMyNjdiMmZiMmVhZmYzNzRiYTVkYjZhY2M2OWFjMWVhZGQ5NDI3MjFlY2FjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmxkeWU3OHV2NmNlanVkdHZleDB2aG05NmhseGE5NnRrbTJlMzU2Yzg0ZG05cDh5OGsyY3ZxcTJkZ2NjIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDkxNiJ9fSx7InZhbHVlIjowLjEwMzY3NTMzLCJuIjo2Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZCBPUF9FUVVBTCIsImhleCI6ImFhMjBhYWVhNjQ5ZjcxOTNhYjQzMjk5OTk5M2JkOGQyY2NjZTVmYWJkM2FmMDFlYmZlNDFhMGUwYTM1ODEwNDRkNDZkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzR3NWV5bHd4ZjZrc2Vmbnh2bmhreGplbjg5bDI3bjR1cTdobGpwNXJzMnhrcXNnbjJ4NjhnbnV2emVrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDg5MSJ9fSx7InZhbHVlIjowLjEwMTcwNDEyLCJuIjo2Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTY2NzlmZTcyOGM2YzE4NzU5Yjc5ZWE0ZjIwYjJlOThiMmNkOWEyNTcwMWM1MzMyMDRkYmFkZWViYWViYzcxNSBPUF9FUVVBTCIsImhleCI6ImFhMjA1NjY3OWZlNzI4YzZjMTg3NTliNzllYTRmMjBiMmU5OGIyY2Q5YTI1NzAxYzUzMzIwNGRiYWRlZWJhZWJjNzE1ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZHR4MDhsODlycnZycDZlazcwMmZ1c3Q5NnZ0OW52Nnk0Y3BjNWVqcW5kNm1tNDZhMHIzMjV0eWh3dHI2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDc5OSJ9fSx7InZhbHVlIjowLjA5NjcwOTIsIm4iOjY0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzN2QxNmRjMDE2OGNkMDdlYzMyYjVlYzFlMzgxN2YyNWZhNGNlNTg5NDc4M2VkZjlkMWJjZDJmYWZkMTAwMmZiIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDM3ZDE2ZGMwMTY4Y2QwN2VjMzJiNWVjMWUzODE3ZjI1ZmE0Y2U1ODk0NzgzZWRmOWQxYmNkMmZhZmQxMDAyZmI4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2bWF6bXdxejZ4ZHFsa3I5ZDB2cmN1cDB1amw1bjg5MzlyYzhtMGU2eDdkOTdoYXpxcDBrZXozdG1lamYiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0NTYyIn19LHsidmFsdWUiOjAuMDk3MDY5MDYsIm4iOjY1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzZDE0MzM3ZTRhZGVmMzM5NmIxYTE2N2Y0YzVhMGM4Nzg1NGVmMTJjZGMxMGQ2MDcwZmY5Yjg0ZDUwNGFhZTMxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNkMTQzMzdlNGFkZWYzMzk2YjFhMTY3ZjRjNWEwYzg3ODU0ZWYxMmNkYzEwZDYwNzBmZjliODRkNTA0YWFlMzE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2NzNndm03ZnQwMHh3dHRyZ3Q4N256NnBqcmMybmgzOW53cHA0czhwbHVtc24yc2YyaHJ6M3BqOWNmNmoiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0NTc5In19LHsidmFsdWUiOjAuMDc5MTQ3MjEsIm4iOjY2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzZDE0MzM3ZTRhZGVmMzM5NmIxYTE2N2Y0YzVhMGM4Nzg1NGVmMTJjZGMxMGQ2MDcwZmY5Yjg0ZDUwNGFhZTMxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNkMTQzMzdlNGFkZWYzMzk2YjFhMTY3ZjRjNWEwYzg3ODU0ZWYxMmNkYzEwZDYwNzBmZjliODRkNTA0YWFlMzE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2NzNndm03ZnQwMHh3dHRyZ3Q4N256NnBqcmMybmgzOW53cHA0czhwbHVtc24yc2YyaHJ6M3BqOWNmNmoiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzNzM0In19LHsidmFsdWUiOjAuMDc3NDkyNjUsIm4iOjY3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA4ZjE0OTQwNmJlNWM3ZGZkNDI5OTNjNWJmZjNhYWE1ZDRhMTcwZmUwYTQyYzk4ZjgxMzM4ZDg4YzFlZjI4MzI1IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDhmMTQ5NDA2YmU1YzdkZmQ0Mjk5M2M1YmZmM2FhYTVkNGExNzBmZTBhNDJjOThmODEzMzhkODhjMWVmMjgzMjU4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3ODNmOXF4aGV3OG1sMnpueTc5aGxlNjRmdzU1OWMwdXpqemV4OGN6dnVkM3JxNzcycGoydWxtMHBwNzciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzNjU2In19LHsidmFsdWUiOjAuMDY3OTA0ODYsIm4iOjY4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1NjY3OWZlNzI4YzZjMTg3NTliNzllYTRmMjBiMmU5OGIyY2Q5YTI1NzAxYzUzMzIwNGRiYWRlZWJhZWJjNzE1IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDU2Njc5ZmU3MjhjNmMxODc1OWI3OWVhNGYyMGIyZTk4YjJjZDlhMjU3MDFjNTMzMjA0ZGJhZGVlYmFlYmM3MTU4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkdHgwOGw4OXJydnJwNmVrNzAyZnVzdDk2dnQ5bnY2eTRjcGM1ZWpxbmQ2bW00NmEwcjMyNXR5aHd0cjYiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzMjA0In19LHsidmFsdWUiOjAuMDYxNzI5NjYsIm4iOjY5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyZDMzYzA1YmNlOWI5OTA3NmZiNzFlZmZkM2U3MjZhODRkZmUyNjY3YzVhZTc1YzcyYjUyNGYyOWQ2NGNlMmVhIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDJkMzNjMDViY2U5Yjk5MDc2ZmI3MWVmZmQzZTcyNmE4NGRmZTI2NjdjNWFlNzVjNzJiNTI0ZjI5ZDY0Y2UyZWE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2a244c3ptZTZkZWpwbTBrdTAwbDVsOHk2NXltbDN4dmx6NnVhdzg5ZGZ5NzJ3a2ZuM3c1ZGQ1d3BrOTMiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyOTEzIn19LHsidmFsdWUiOjAuMDU5ODU1MjMsIm4iOjcwLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA2YWFjMzhkYmFlMWE5YzAyYWE2NGZjYjY4YzA1MGUyNmEyZjVlZjcwNWNiMjhmNmNhNjhlYmU2ODIzOWU4Y2NlIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDZhYWMzOGRiYWUxYTljMDJhYTY0ZmNiNjhjMDUwZTI2YTJmNWVmNzA1Y2IyOGY2Y2E2OGViZTY4MjM5ZThjY2U4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkNDJjd3htNGNkZmNxNDJ2bjd0ZHJxOXBjbjI5YTAwd3B3dDlybXY1Njh0dTZwcm42eHZ1cXh0YTluM3EiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyODI1In19LHsidmFsdWUiOjAuMDUzNDkxNzYsIm4iOjcxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBjMmZhOGI1MmZhMzY4NTU5NWQ3Y2FkYjBiYjJkOTNmMWIzM2Q3ZGM1N2NiMTdhMDI5MjkzOGE4MzEyYWRiNDU3IE9QX0VRVUFMIiwiaGV4IjoiYWEyMGMyZmE4YjUyZmEzNjg1NTk1ZDdjYWRiMGJiMmQ5M2YxYjMzZDdkYzU3Y2IxN2EwMjkyOTM4YTgzMTJhZGI0NTc4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwcDA0ejZqbGdtZzJrMmEwamttcHdlZGowY214MHRhYzQ3dHo3c3pqMmZjNHFjajRrNjl3ODRnazY4eTIiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyNTI1In19LHsidmFsdWUiOjhlLTA2LCJuIjo3Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZjBlZGN4bGZuemt0bTc3ZXFnODc0anVwMjh0YTZhNnU1cnk1c2Z1OSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjU5MzMwNCJ9fSx7InZhbHVlIjozNy42MjUzMTY1NCwibiI6NzMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX19XSwiZmVlIjowLjAwMDE0MTkzLCJoZXgiOiIwMjAwMDAwMDRhNWExZGFhNjNmNzczN2RiZjZjMTMxMmM1YmNkMmUyMjM5NmE0MThiZDYyNmRmOGZlNDg4YTMzMWFkNDlmZTRlMzAwMDAwMDAwNDU0NDc0NjM3NmE5MTRkZTdhZTdiMTBkNmQ2ZDliYmMwMTcwYWIyYzJlZDQ0NTBlZDRjYjE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjVhMWRhYTYzZjc3MzdkYmY2YzEzMTJjNWJjZDJlMjIzOTZhNDE4YmQ2MjZkZjhmZTQ4OGEzMzFhZDQ5ZmU0ZTMwMTAwMDAwMDQ1NDQ3NDYzNzZhOTE0NzFhNzE0OWVlMGI0ZTE0OGQ3N2FhZGI0YzBlNzFhMTJmMGYzODJhOTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY1YTFkYWE2M2Y3NzM3ZGJmNmMxMzEyYzViY2QyZTIyMzk2YTQxOGJkNjI2ZGY4ZmU0ODhhMzMxYWQ0OWZlNGUzMDIwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGZlODI3YzY1MzgzMjA3Mzk3OGI3YmQ1OGVlYzg0MGVlNDRiYzY3MDg4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmNWExZGFhNjNmNzczN2RiZjZjMTMxMmM1YmNkMmUyMjM5NmE0MThiZDYyNmRmOGZlNDg4YTMzMWFkNDlmZTRlMzAzMDAwMDAwNDU0NDc0NjM3NmE5MTQxYjIxMzQ5YjgwMTllYzczOGE4ZGM2ZjJmYzQyZGFjYmIwNjQ0MWQwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjVhMWRhYTYzZjc3MzdkYmY2YzEzMTJjNWJjZDJlMjIzOTZhNDE4YmQ2MjZkZjhmZTQ4OGEzMzFhZDQ5ZmU0ZTMwNDAwMDAwMDQ1NDQ3NDYzNzZhOTE0MmExOGVmMDZiMWI3MWIyZTM3OTdhOTZhY2U5MGEwNGI5YjI4YzEyYjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MDkwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGE3ODdhMjc3NGNiMWMzYjU4N2Y5MDQ4ZjY2ZTdkYzlmNTc0MzQ0NzM4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmOTk4NzU0NmQ1ZjRlZmM0MzAyMjJiMTk2ODNlODQ3MGE3OTlkYTE5MTI0YjAzMGZiOGFhYzY0ZTkyZjc1Y2I3ODBhMDAwMDAwNDU0NDc0NjM3NmE5MTRkNmM5ZDA3YTAzNWFlNWNmMzE3ZjVmNDA0NjQ5YWZiODA4MzdiNGExODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjYzMDFjNWU4NDkzNDg2NTMzNzkxMTQ2YTU1M2U3NjA0NjY3Njk2NmJkNDFhYjY4ZTdhMjZkY2JmOTQ5OTkyNzkxMjAwMDAwMDQ1NDQ3NDYzNzZhOTE0ODg0MjQ5NWY5ODhkOTA3YzhlY2Y4ZjZmZjE3ZTkwYTM5ZTg5ZDJhNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY2MzAxYzVlODQ5MzQ4NjUzMzc5MTE0NmE1NTNlNzYwNDY2NzY5NjZiZDQxYWI2OGU3YTI2ZGNiZjk0OTk5Mjc5MTMwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDQ1OWRlNDI2NTRkOWQyYWNmZTMxNTFlMDhiZWExZmE1YzQxY2IxMmM4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmOTk4NzU0NmQ1ZjRlZmM0MzAyMjJiMTk2ODNlODQ3MGE3OTlkYTE5MTI0YjAzMGZiOGFhYzY0ZTkyZjc1Y2I3ODBjMDAwMDAwNDU0NDc0NjM3NmE5MTQ0NWRjMTQyNDg4OGJjNDliMzRiZWQ5ODAyMzMwM2NlZjFlMDhmYjcwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjk5ODc1NDZkNWY0ZWZjNDMwMjIyYjE5NjgzZTg0NzBhNzk5ZGExOTEyNGIwMzBmYjhhYWM2NGU5MmY3NWNiNzgwYjAwMDAwMDQ1NDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MGQwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDQ1ZGMxNDI0ODg4YmM0OWIzNGJlZDk4MDIzMzAzY2VmMWUwOGZiNzA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmOTk4NzU0NmQ1ZjRlZmM0MzAyMjJiMTk2ODNlODQ3MGE3OTlkYTE5MTI0YjAzMGZiOGFhYzY0ZTkyZjc1Y2I3ODBlMDAwMDAwNDU0NDc0NjM3NmE5MTQ0NWRjMTQyNDg4OGJjNDliMzRiZWQ5ODAyMzMwM2NlZjFlMDhmYjcwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjk5ODc1NDZkNWY0ZWZjNDMwMjIyYjE5NjgzZTg0NzBhNzk5ZGExOTEyNGIwMzBmYjhhYWM2NGU5MmY3NWNiNzgwZjAwMDAwMDQ1NDQ3NDYzNzZhOTE0NDVkYzE0MjQ4ODhiYzQ5YjM0YmVkOTgwMjMzMDNjZWYxZTA4ZmI3MDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY1YTFkYWE2M2Y3NzM3ZGJmNmMxMzEyYzViY2QyZTIyMzk2YTQxOGJkNjI2ZGY4ZmU0ODhhMzMxYWQ0OWZlNGUzMDUwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGQwNzM2ZWU5MDExN2Y0ZWQ3ZTliYWQ1YWUwOWIwYWIwYzg5Mjk2YjQ4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmOTk4NzU0NmQ1ZjRlZmM0MzAyMjJiMTk2ODNlODQ3MGE3OTlkYTE5MTI0YjAzMGZiOGFhYzY0ZTkyZjc1Y2I3ODEwMDAwMDAwNDU0NDc0NjM3NmE5MTQ0NWRjMTQyNDg4OGJjNDliMzRiZWQ5ODAyMzMwM2NlZjFlMDhmYjcwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjYzMDFjNWU4NDkzNDg2NTMzNzkxMTQ2YTU1M2U3NjA0NjY3Njk2NmJkNDFhYjY4ZTdhMjZkY2JmOTQ5OTkyNzkxNDAwMDAwMDQ1NDQ3NDYzNzZhOTE0MmExOGVmMDZiMWI3MWIyZTM3OTdhOTZhY2U5MGEwNGI5YjI4YzEyYjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MTEwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDk1Njk5Y2M5MDJjMmQ5YmUxNDM4YjM4NmY2NzhjY2Y2YWYwNjg4MjM4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmNWExZGFhNjNmNzczN2RiZjZjMTMxMmM1YmNkMmUyMjM5NmE0MThiZDYyNmRmOGZlNDg4YTMzMWFkNDlmZTRlMzA2MDAwMDAwNDU0NDc0NjM3NmE5MTRjZDZkNTY3ZGM2MzRhYzhmYTg4NTYxZTg3YjI4ZjkxMThhOGFkYTdhODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjYzMDFjNWU4NDkzNDg2NTMzNzkxMTQ2YTU1M2U3NjA0NjY3Njk2NmJkNDFhYjY4ZTdhMjZkY2JmOTQ5OTkyNzkxNTAwMDAwMDQ1NDQ3NDYzNzZhOTE0MWJlMGJiMGVhMTViZDBhNWJiMzkxMzgyMjRiNGUxZWNkMTJiZjlhYjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MTcwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDYzOWMwN2Q1ZTI0MTNhZWQ0YzE0YzVjMmI4ZGJjMzgyYWMzZDk1MjI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTE4MDAwMDAwNDU0NDc0NjM3NmE5MTQ5ZGNiNjQ0YzJkMjI3MTA4YTk5OGVhODBmNDYzOWY2NmIyODBlMWYwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjk5ODc1NDZkNWY0ZWZjNDMwMjIyYjE5NjgzZTg0NzBhNzk5ZGExOTEyNGIwMzBmYjhhYWM2NGU5MmY3NWNiNzgxNTAwMDAwMDQ1NDQ3NDYzNzZhOTE0NzQ1NzZjNzdhZWYzZmI1ODdmOGM2YjJmYWY4OGUzN2Q4ZWFhZjhiODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MTcwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDRhMDg1MzA2NTBlODYxZDM0YWNlZTQzMTUyZTU0ZGUyN2QxYmNmODI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmNWExZGFhNjNmNzczN2RiZjZjMTMxMmM1YmNkMmUyMjM5NmE0MThiZDYyNmRmOGZlNDg4YTMzMWFkNDlmZTRlMzA3MDAwMDAwNDU0NDc0NjM3NmE5MTRhNzNkNzA4OWU0OGFiZjM3YjRkYjE1Y2ZlYjRiZDQ1N2I1YTEwODFmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjNhMWJhZWNmZGU4YWNiNDQ4ZjZkMDUyNGNmN2Y3ODE4NDFjZWM4NmNhOWQzOTBlNDRhMzViOGI3OWFjZmI4NzUxYzAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MWQwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDFlZjM2ZGIzODM2ZWI5NTVhM2E1NjE0ZDUyZmVhNjU2NzgzMDAyYTI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmOTk4NzU0NmQ1ZjRlZmM0MzAyMjJiMTk2ODNlODQ3MGE3OTlkYTE5MTI0YjAzMGZiOGFhYzY0ZTkyZjc1Y2I3ODEyMDAwMDAwNDU0NDc0NjM3NmE5MTQ5OWY5YzQxMWY4MzkxMzY4M2E2ZGY1YjQ1MzZmN2FjMmI5ZDBmYmM1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjk5ODc1NDZkNWY0ZWZjNDMwMjIyYjE5NjgzZTg0NzBhNzk5ZGExOTEyNGIwMzBmYjhhYWM2NGU5MmY3NWNiNzgxNDAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MTMwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDRhMDg1MzA2NTBlODYxZDM0YWNlZTQzMTUyZTU0ZGUyN2QxYmNmODI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTFlMDAwMDAwNDU0NDc0NjM3NmE5MTRhYjRlNmUwYjliZmM5N2ViOGExYmEyNWQ5NWJkYWE1MDNkZjFjM2EyODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjNhMWJhZWNmZGU4YWNiNDQ4ZjZkMDUyNGNmN2Y3ODE4NDFjZWM4NmNhOWQzOTBlNDRhMzViOGI3OWFjZmI4NzUxZjAwMDAwMDQ1NDQ3NDYzNzZhOTE0M2Y4MWE3NmM2NDBjMWIwY2E4YTBlNGNhZTQ2MWRjMWU5OTkwY2ExYTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY1YTFkYWE2M2Y3NzM3ZGJmNmMxMzEyYzViY2QyZTIyMzk2YTQxOGJkNjI2ZGY4ZmU0ODhhMzMxYWQ0OWZlNGUzMDgwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGE3ODdhMjc3NGNiMWMzYjU4N2Y5MDQ4ZjY2ZTdkYzlmNTc0MzQ0NzM4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmOTk4NzU0NmQ1ZjRlZmM0MzAyMjJiMTk2ODNlODQ3MGE3OTlkYTE5MTI0YjAzMGZiOGFhYzY0ZTkyZjc1Y2I3ODE2MDAwMDAwNDU0NDc0NjM3NmE5MTQ4ODQyNDk1Zjk4OGQ5MDdjOGVjZjhmNmZmMTdlOTBhMzllODlkMmE0ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjk5ODc1NDZkNWY0ZWZjNDMwMjIyYjE5NjgzZTg0NzBhNzk5ZGExOTEyNGIwMzBmYjhhYWM2NGU5MmY3NWNiNzgxODAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZDNlNzg2YzZhNGFmMTM3NTExYWM4NDEyMmU3ODE1YTEwNjc0OTI2OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MjAwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDZiYTE1MmRiN2RlYjI5YWU3YTI1MjgzYWQzMWQwOGM2OGJhZjUzYTA4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTIxMDAwMDAwNDU0NDc0NjM3NmE5MTRkODQ2MDU2OWYwMzFiMDk4MGYzOTQ5YTA3ODI2M2UzYjQwMzYzOTYyODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjNhMWJhZWNmZGU4YWNiNDQ4ZjZkMDUyNGNmN2Y3ODE4NDFjZWM4NmNhOWQzOTBlNDRhMzViOGI3OWFjZmI4NzUyMjAwMDAwMDQ1NDQ3NDYzNzZhOTE0M2VkMDhhMjA4NzA4YTI1ZTkxNzg5Nzk0YTAxNjUzM2VkN2FkYjkyNjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MjMwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDc4NjQyYmQ0ZWFhMzg2NDc2YmU2OGNhZjFhM2M3ZTdiYjlhZDQ1NjE4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTI0MDAwMDAwNDU0NDc0NjM3NmE5MTQxYzQ1Yzc0NzJmNGIyNmRkZDQwNDY5OTMxMGFmY2EwNzQxYmEwNDBmODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjk5ODc1NDZkNWY0ZWZjNDMwMjIyYjE5NjgzZTg0NzBhNzk5ZGExOTEyNGIwMzBmYjhhYWM2NGU5MmY3NWNiNzgxOTAwMDAwMDQ1NDQ3NDYzNzZhOTE0MjQyYTIxOTZjZTEzNzYxNWNjNjNjZmNmYmQ1NTRjMjUwOTNkNDI4Yjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MWEwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGRjMWFiMGJmOGY0NzAzNTI1OTUwM2FlMWExOGM0NjI0N2JmYjExMWQ4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTI1MDAwMDAwNDU0NDc0NjM3NmE5MTQ1MzdmYjg3ZDQwNzMxNTg0MTZhMGQ0MTcxMDY2NTE1MTM0NzI2NGEwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjk5ODc1NDZkNWY0ZWZjNDMwMjIyYjE5NjgzZTg0NzBhNzk5ZGExOTEyNGIwMzBmYjhhYWM2NGU5MmY3NWNiNzgxYzAwMDAwMDQ1NDQ3NDYzNzZhOTE0NTM3ZmI4N2Q0MDczMTU4NDE2YTBkNDE3MTA2NjUxNTEzNDcyNjRhMDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmY5OTg3NTQ2ZDVmNGVmYzQzMDIyMmIxOTY4M2U4NDcwYTc5OWRhMTkxMjRiMDMwZmI4YWFjNjRlOTJmNzVjYjc4MWIwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDc3YjRlYzcwNmI4Zjc1MzFjODEyMTlhY2RiYTgwN2RhMGVmM2U3YjM4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTI2MDAwMDAwNDU0NDc0NjM3NmE5MTQ1MzdmYjg3ZDQwNzMxNTg0MTZhMGQ0MTcxMDY2NTE1MTM0NzI2NGEwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjNhMWJhZWNmZGU4YWNiNDQ4ZjZkMDUyNGNmN2Y3ODE4NDFjZWM4NmNhOWQzOTBlNDRhMzViOGI3OWFjZmI4NzUyNzAwMDAwMDQ1NDQ3NDYzNzZhOTE0MjQyYTIxOTZjZTEzNzYxNWNjNjNjZmNmYmQ1NTRjMjUwOTNkNDI4Yjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MjgwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDFjNDVjNzQ3MmY0YjI2ZGRkNDA0Njk5MzEwYWZjYTA3NDFiYTA0MGY4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTI5MDAwMDAwNDU0NDc0NjM3NmE5MTQ5N2Q2ZDE5OTY3OWRlZjk0MmJjZmQ2MjI1ZjQzZGQ5NTU2OGUzZWE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjNhMWJhZWNmZGU4YWNiNDQ4ZjZkMDUyNGNmN2Y3ODE4NDFjZWM4NmNhOWQzOTBlNDRhMzViOGI3OWFjZmI4NzUyYTAwMDAwMDQ1NDQ3NDYzNzZhOTE0YWNjNGQxYzAxMmZkZGM1MGEzYThhNWI3YTExNGZkM2Y5ODdhMmE5OTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MmIwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDRhMDg1MzA2NTBlODYxZDM0YWNlZTQzMTUyZTU0ZGUyN2QxYmNmODI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTJkMDAwMDAwNDU0NDc0NjM3NmE5MTRlMTgxODFjZjY3MzMwMGE1OGE4MjM1NmYxMTY3OTdmNjRlYjAwYTE1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjNhMWJhZWNmZGU4YWNiNDQ4ZjZkMDUyNGNmN2Y3ODE4NDFjZWM4NmNhOWQzOTBlNDRhMzViOGI3OWFjZmI4NzUyYzAwMDAwMDQ1NDQ3NDYzNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZlODJhNmJiMjQyYTQ0YzM2NjFiZDJkYWY4Yzg3ZWU2YzRhZmMyZjE1MDEyMWNiMDYyZjUyMjE3MDA4M2YwMzYwM2QwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDg4NDI0OTVmOTg4ZDkwN2M4ZWNmOGY2ZmYxN2U5MGEzOWU4OWQyYTQ4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTMxMDAwMDAwNDU0NDc0NjM3NmE5MTQyNGE3OGVjZWQ1MDY3NWFiYzRhODU3OWY1ZDRhM2I3NGYzMzk3ZTU4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjNhMWJhZWNmZGU4YWNiNDQ4ZjZkMDUyNGNmN2Y3ODE4NDFjZWM4NmNhOWQzOTBlNDRhMzViOGI3OWFjZmI4NzUyZjAwMDAwMDQ1NDQ3NDYzNzZhOTE0MmVlYTdjMGQ3ZWJlZGY4YTA2OTMzNzUyNTM2ZjZkMzA4OTMzZmU4NTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MmUwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDVhOWQzMWE2MTE0NTAzODE4NzhiOTc4M2M4OTA0MGEyODdkNmIwZTM4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTMwMDAwMDAwNDU0NDc0NjM3NmE5MTRlOGU2YzU4MWRmMTVmMTIwNTFlYTU5ZmQzMDhjMmEwZjc0MzVmZTZlODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjNhMWJhZWNmZGU4YWNiNDQ4ZjZkMDUyNGNmN2Y3ODE4NDFjZWM4NmNhOWQzOTBlNDRhMzViOGI3OWFjZmI4NzUzMjAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZDBjZTMyYzJkYzEyMjk2OWE2Y2RiZmRhMmYxZjZhNjJmMTIwN2Q0Yjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmYzYTFiYWVjZmRlOGFjYjQ0OGY2ZDA1MjRjZjdmNzgxODQxY2VjODZjYTlkMzkwZTQ0YTM1YjhiNzlhY2ZiODc1MzQwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGU4OGQ3MWUyN2VhMGRjMmI1OGFjMGEzNTM5OWM2NDdmNWRlYjhjOGY4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmM2ExYmFlY2ZkZThhY2I0NDhmNmQwNTI0Y2Y3Zjc4MTg0MWNlYzg2Y2E5ZDM5MGU0NGEzNWI4Yjc5YWNmYjg3NTM1MDAwMDAwNDU0NDc0NjM3NmE5MTQ3NmEwZTI5YTU5OGQ4NWUzYTE2MTdlYWVjMGQ5MWVjMGEyM2Y5ODZjODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZmU4MmE2YmIyNDJhNDRjMzY2MWJkMmRhZjhjODdlZTZjNGFmYzJmMTUwMTIxY2IwNjJmNTIyMTcwMDgzZjAzNjA0MTAwMDAwMDQ1NDQ3NDYzNzZhOTE0OTFjYjBmNzYzNDc4OTFmNDI3NzA1ZjU0ZDkzZDk1MTBhMDNlMzA2Yjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZlODJhNmJiMjQyYTQ0YzM2NjFiZDJkYWY4Yzg3ZWU2YzRhZmMyZjE1MDEyMWNiMDYyZjUyMjE3MDA4M2YwMzYwNDIwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDRhMDg1MzA2NTBlODYxZDM0YWNlZTQzMTUyZTU0ZGUyN2QxYmNmODI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmZTgyYTZiYjI0MmE0NGMzNjYxYmQyZGFmOGM4N2VlNmM0YWZjMmYxNTAxMjFjYjA2MmY1MjIxNzAwODNmMDM2MDQzMDAwMDAwNDU0NDc0NjM3NmE5MTQ2NjRhOWQ1ZmRhYTk5YjIxZmM5MThjMTVkNDc3MzRiOTdkYmNlZjgxODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZmU4MmE2YmIyNDJhNDRjMzY2MWJkMmRhZjhjODdlZTZjNGFmYzJmMTUwMTIxY2IwNjJmNTIyMTcwMDgzZjAzNjA0NTAwMDAwMDQ1NDQ3NDYzNzZhOTE0MWM0NWM3NDcyZjRiMjZkZGQ0MDQ2OTkzMTBhZmNhMDc0MWJhMDQwZjg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZlODJhNmJiMjQyYTQ0YzM2NjFiZDJkYWY4Yzg3ZWU2YzRhZmMyZjE1MDEyMWNiMDYyZjUyMjE3MDA4M2YwMzYwNDQwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDM0MTBmZTdjYjA3OGVhOGQ1NmUwN2UyMTM3MTk5ZjU3MzYxNDhjNzU4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmZTgyYTZiYjI0MmE0NGMzNjYxYmQyZGFmOGM4N2VlNmM0YWZjMmYxNTAxMjFjYjA2MmY1MjIxNzAwODNmMDM2MDQ3MDAwMDAwNDU0NDc0NjM3NmE5MTQzNDEwZmU3Y2IwNzhlYThkNTZlMDdlMjEzNzE5OWY1NzM2MTQ4Yzc1ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZmU4MmE2YmIyNDJhNDRjMzY2MWJkMmRhZjhjODdlZTZjNGFmYzJmMTUwMTIxY2IwNjJmNTIyMTcwMDgzZjAzNjAzYzAwMDAwMDQ1NDQ3NDYzNzZhOTE0NTI5MjNiNjY4ZmUyNGU4YTMxZTQyMTNjOWY1YTFkMGZiOTQ3NzQ1ZTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZlODJhNmJiMjQyYTQ0YzM2NjFiZDJkYWY4Yzg3ZWU2YzRhZmMyZjE1MDEyMWNiMDYyZjUyMjE3MDA4M2YwMzYwNGIwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDY2NGE5ZDVmZGFhOTliMjFmYzkxOGMxNWQ0NzczNGI5N2RiY2VmODE4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmZTgyYTZiYjI0MmE0NGMzNjYxYmQyZGFmOGM4N2VlNmM0YWZjMmYxNTAxMjFjYjA2MmY1MjIxNzAwODNmMDM2MDNlMDAwMDAwNDU0NDc0NjM3NmE5MTQwMjZjZjA5MjAzMjhkYWJiNTlhM2M2NTRhM2U4ZWE3MzZhOTM1OTU5ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZmU4MmE2YmIyNDJhNDRjMzY2MWJkMmRhZjhjODdlZTZjNGFmYzJmMTUwMTIxY2IwNjJmNTIyMTcwMDgzZjAzNjA0MDAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZDUwNDIwYmViM2IyMjUyNzIxYTdlZjg0MGVhMDE1ZjQ4OTczYzA3Nzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZlODJhNmJiMjQyYTQ0YzM2NjFiZDJkYWY4Yzg3ZWU2YzRhZmMyZjE1MDEyMWNiMDYyZjUyMjE3MDA4M2YwMzYwM2YwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDM2YzAwMjBkZDM5ZTdjZDY2YzIxZjIzN2RjNTNkMzg0NjYxYTU1N2Y4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmMmY3MTBmMzM2ZTllZGM1ZWMyZGVhYzNmZTg3NTg4NTc1MjdiY2M5M2QwNmExMGFhNTBjZmQxMDNhNjhjZGM5YzBiMDAwMDAwNjQ0MTNiNzIzMzhmNWMyZmRhY2JhOWQ5NTJhYmY5OTg4Mjk2M2M3MmMyOTAwNDczYzk4NTRmOTgyZDAxMWU5OWViMWM2NTFiZDcxN2U5MGUxY2Q2NTYxMjA5Y2M3Zjg2MGEwN2Q0ZmM5MWIxZmViMDA0MjNhMzhmMmJhZjY1ZTYwMDAyNjEyMTAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2M2ZmZmZmZmZmNWExZGFhNjNmNzczN2RiZjZjMTMxMmM1YmNkMmUyMjM5NmE0MThiZDYyNmRmOGZlNDg4YTMzMWFkNDlmZTRlMzBhMDAwMDAwNjQ0MWYyZWMyZjk4M2VkOGEwYzIyYWI4N2FhODNhOWYxYzNhZTJlNWU3ZGQ0ZmY5ZDRjZjY1MmU5MThlZDMzY2U4OWJhNjVlYTFiZDllZDlkYjAwNWVlYTk4OWVhZDFhYTJhOTlmNjZhMWFhYzhhZWY3MGFhMzU3YzU4MTU4MzYzNDIwNjEyMTAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2M2ZmZmZmZmZmNGEyNmQ2MjI2ODAyMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlNTM2MzRhMDBhYTIwNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NDg3NDM5NTEzMjUwMjAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZWFiNGE0MjAwYWEyMDFlZTQxNDJmZjkyOGI0YWE2OTJiM2RlOGRlMjkyOWJlMzkwNWM2MDI3ZDNiYThiMGEyY2YxYzBiNTQ4N2FjYjA4NzYxNWFhNWE3MDEwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVlNzI1MzMwMGFhMjAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2ODczYWIzYjBkNjAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlOTNlYjE5MDBhYTIwNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNjg3ZGNjNzJmNjgwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTJiOTQwYzAwYWEyMDIwYjA1Yjc0YWI1NmM5ODQ4MDMyZTliYTRmZDA5NDI3NWNjYzdmMzYyOGFkMTg4NmZjZTU2ZTkzMGQxMjczY2Q4N2UzOGJhYjEzMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVmYzVmMDIwMGFhMjAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0ODc3OTBhMjExMTAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlNzIxMTAyMDBhYTIwZWZiYzU1NmQ1Y2ZlOTBhNDU4ZGJmNGQ1N2JkNzg3ZjMzNGJhZmIxYTkzM2ZmYzMzMjIyNjdlYmY0NGMzNGZiMDg3MTE1MWI5MGMwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTRiODkwMTAwYWEyMGE2NGE2NTMwOTE2MzMyOTgwNjI3NWE1YTk5YjI3ODUyNGIyMWE0NjU1OWRlNDdlZmNjZTA5OGQzZWI1YjJhMGU4N2YwNTRiNTBjMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVjZjg4MDEwMGFhMjAwZWY0ZmFhZTgwOWI5ZTM1YmE3NTVmMTUxM2RjZmFhMzNhMDMwZGRkZjU2OGIzNTlhNjM3ZTY3Y2U1YjcyODc2ODdhZGQyMTcwZjAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlODNkMjAxMDBhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3YTAxODFmMGYwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTY0ZDMwMTAwYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4NzNmMGQxNzBmMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmU2Y2QyMDEwMGFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODc1YmQ5MTcwZjAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlODVkMjAxMDBhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3Y2E4M2Q3MGUwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZWJmY2EwMTAwYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4N2FlNmE5MDQzMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmU0MTI4MDgwMGFhMjBiMzQ3ZDY4YzhkMmNkYjMyMDY2YmU5YWY4MDRjNmZmYjhhYmJiNDQxYmRmMzZjYWYzODM0MzZiY2ZjYWEzNWNiODdmNmNhYWMwZTAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlOThjNTAxMDBhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3YTEzYWFiMGEwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZWM1NDkwMTAwYWEyMDIwYjA1Yjc0YWI1NmM5ODQ4MDMyZTliYTRmZDA5NDI3NWNjYzdmMzYyOGFkMTg4NmZjZTU2ZTkzMGQxMjczY2Q4NzE1OWRmZDBkMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmU3MGIwMDEwMGFhMjBjOGRkOTc3YWU5OTEyYjcyMTM0MWJhZjMwODY4N2JlNWI5ZWZiMzJjYzBmYmYyNjk2NjY5MGI2MTk4ZTIwMWNmODdlOGI5YzYzZTAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlNDU5NDA3MDBhYTIwN2MyYmNjYTIwODlhOWYwZjhkYWVjMGQzYjQ1YTZjNTI1ODFjNTY3NjY1Y2M1MWViZGE5YTIwMmQ4YTMzZGNmYzg3NDJkNjY1MDcwMDAwMDAwMDQ4ZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZGFhZTRhYTIwY2I5NDVlMjY1MzQzNmIwNTBlYjVmZDY0YjM1NzA1MGEyM2EwZjI3YWFmZjlkMWU1Mjk2MGI4MDYwOGM5MTJkZTg3YTUwZDI4MDQwMDAwMDAwMDQ4ZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZDdiODBhYTIwYTIwZjE2OTc3ODk3NzFhMjdkODBjZGIyODJhZGM2NTE1NWIxYWQxNGZkY2IzMmUxYjI2ZmRkMzg2ZDg4NzM1Mzg3NzNiNTE1MDQwMDAwMDAwMDQ4ZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZDQ0N2VhYTIwMTQ1Njc3NjUxZWMxMjIxY2M3ZjBmYjFlYjk2ZTllMzRiOTEwYzNhNWIyNzI5NDJjOTVlNTNjNGRiOTliY2E3Mjg3MWRlNDc5MDcwMDAwMDAwMDQ4ZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZDE1ZTdhYTIwMmRkNzA1NWM5MjYyNTMyNTJlYjQ0ZjdlYTQxMzlkYjI0NWU0OTJhYWRjMTllOWU1Y2ZlOTVhMmVjODdjYjA2MDg3YTU2MzVhMDcwMDAwMDAwMDQ4ZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZDQ4ZTNhYTIwYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZDg3Njc0NjBmMWYwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZWZkYmYwMzAwYWEyMGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTE4NzE0ZTA1MDAzMDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQ4MTY2YWEyMDdhZmQ0ZGU4YWZmYjA3Y2I2NjgzMTM0YWQ1OWU5YzdhNzQzYTcwZTYxNGFkNjIxYWZmNWExMTAzN2MwMTE1ZTI4NzI4ZDY0ODAzMDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQ4NzY1YWEyMDY4ZDY4NGRlMDRiODUwMWZiOTJkNjFmMTI3ZWFhOGE4OGU5ODRjMTQ1ZTMyMDZmZWNmODRlNzRlNGY4ZGY5NDU4NzI1NjI2YzA2MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQ4Y2M2YWEyMDNmOTkyZDEyOTA0MTEzNzQ4YzQ0NDNjMTY0ZWNmMTJmNDhkMWIwNzUxODA4ZTJiNTM3MTg0OTdlMTJmOTRiNDE4N2ZhYWE1NTA2MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmRjZGMzYWEyMDdhZmQ0ZGU4YWZmYjA3Y2I2NjgzMTM0YWQ1OWU5YzdhNzQzYTcwZTYxNGFkNjIxYWZmNWExMTAzN2MwMTE1ZTI4N2U3Y2Q1NjA2MDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmRmMWMzYWEyMGFhZWE2NDlmNzE5M2FiNDMyOTk5OTkzYmQ4ZDJjY2NlNWZhYmQzYWYwMWViZmU0MWEwZTBhMzU4MTA0NGQ0NmQ4N2NmMjIyMjAzMDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmRkYzYwYWEyMDllNzI1NTUxMzgxMjhlMDVjODQ1YjcwYTMwODRiMTlkYTQxNGE1MDNkZmI3NTk2ZDExZmNmNTQyMjgwMGExOTA4NzdhMDIxNDAzMDAwMDAwMDA0OGVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmQyNzVmYWEyMDFkZWQwYmMwYTUxYmU5MmZiNjQ2NDI2N2Q5OTQwNTRlZDBlNzMyYWJhNDQ4MGQxNmQxM2EzMDRhZDNiMDU2ZTE4N2RmMmQzNjFhMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmUyNDJhMDMwMGFhMjAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0ODdlOWYxZTUwNTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkNTBiNmFhMjBhNjRhNjUzMDkxNjMzMjk4MDYyNzVhNWE5OWIyNzg1MjRiMjFhNDY1NTlkZTQ3ZWZjY2UwOThkM2ViNWIyYTBlODdjNjRkZDAwNTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkYjJiM2FhMjA3YWZkNGRlOGFmZmIwN2NiNjY4MzEzNGFkNTllOWM3YTc0M2E3MGU2MTRhZDYyMWFmZjVhMTEwMzdjMDExNWUyODc1ZDM0ZTQwMjAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkNjI1OWFhMjBmZTllYjcyMzQ2MjljMWI4Y2I0OGZiNmIwZTNiNzVkOWI3NmE2ZjE4NmI2MTU2OTI4ZmVlNTk0NGNjOTBkNDY0ODc3ZGZjZTIwMjAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkM2I1OWFhMjAzMTE5ZjI5ZTkwM2Q0NjhiZjNkNjE4ZGUwYzU1MGYwYjAyMTg1YmI0NmIxM2Y4Y2ZjNGM4YmM4M2E3MmM2ZmJlODdhNzQ1ZTUwMjAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkODI1OWFhMjBhYWFiZDA1YjhjMTViNjE0ZjZmMzcwYzliZTQ1ZmExOWI4ZjViOGE5MDRkMDJjZjhhNjVjNzg2MDg1ZDcyNjMyODcxMTJhYjkwMjAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkMmY1NGFhMjAzZjlkMzczMjg4NzQ4MDA4YzU0OGZmNDUwODIxOWZmM2RmMmU5ZTRkYWRiZjI0M2FmMWU1N2U0NmJmMmI1Y2U5ODc5OWNiOWUwMjAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkMDA1MWFhMjAzN2QxNmRjMDE2OGNkMDdlYzMyYjVlYzFlMzgxN2YyNWZhNGNlNTg5NDc4M2VkZjlkMWJjZDJmYWZkMTAwMmZiODdmYjkwMjQwNTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkZjc5ZWFhMjBjM2U3ZGNjZTYzZTM3YWIyZGUzYmY4YTdkOTMyY2ZhMDI5Mzc0OTAyYzYzMTBlOGVkY2IzMTE4MTIzNDU3NjdhODc0YTY3MDUwNTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkMzM5YmFhMjAxZDc5Nzc5ZDZiODIzNjI4MTljYzFhNWE5NTFiOWQ3YzlmNzJlN2ExYWY0NzU3NzZkOTQ0ZjM1NGQ3NjMzOWYzODdjMTZmODMwMjAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkYjE0ZGFhMjA4ZGFkNzFhOTdiYzFkYzllMmJlZDlmNmFhMTRiY2UxOTg5OGQzY2U4MTQyZjY1ZDIxODU4MWRhZjQyNzQ1MzFjODc3OGY2YjMwNDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkNWU5MWFhMjA4ZGFkNzFhOTdiYzFkYzllMmJlZDlmNmFhMTRiY2UxOTg5OGQzY2U4MTQyZjY1ZDIxODU4MWRhZjQyNzQ1MzFjODdkZjQyYjAwNDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkZWM5MGFhMjA0NjE5ZjBiMjlkMWVlNzA1NDY3ODZiOGIzMGM3NWJiZTViMTViZGI5OGQwMWE2YWRkM2E0MTQ3MDAwZjg2ZTYxODc5OTY0MzMwMjAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkMDg0NGFhMjA4ZGFkNzFhOTdiYzFkYzllMmJlZDlmNmFhMTRiY2UxOTg5OGQzY2U4MTQyZjY1ZDIxODU4MWRhZjQyNzQ1MzFjODdiMDc2YzkwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkM2UzN2FhMjBjM2U3ZGNjZTYzZTM3YWIyZGUzYmY4YTdkOTMyY2ZhMDI5Mzc0OTAyYzYzMTBlOGVkY2IzMTE4MTIzNDU3NjdhODdmYmNjYzUwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkY2MzNmFhMjAzN2QxNmRjMDE2OGNkMDdlYzMyYjVlYzFlMzgxN2YyNWZhNGNlNTg5NDc4M2VkZjlkMWJjZDJmYWZkMTAwMmZiODcwNzQ4YzAwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkMjIzNmFhMjAzNmI4NThmNjc5YzViMDliMDg4ZWZmMTkwN2YxNWQyOThiOWI3NTEzMjU1NWZmNmExZDIxMDYwZDE2ZmQ4NDY4ODdlMjA2OTUwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkZTgzMGFhMjBiNGNjMWNkZjYxNzdjN2RlN2IwYmEwNDIwYzRkODdhOWQzZDNiYWE3YTE0ZjAyMjk3ODVjYTRmY2UwNWVkNGFkODcwZTk4NzEwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkYTIyY2FhMjBhYWVhNjQ5ZjcxOTNhYjQzMjk5OTk5M2JkOGQyY2NjZTVmYWJkM2FmMDFlYmZlNDFhMGUwYTM1ODEwNDRkNDZkODc0NTQ2NTAwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkOWIyOGFhMjAxNzRlYzg4YzMwZTlmMzQ2OTA4MjVmNGNkNWY2NzE0MDQ5NTk2Y2RjYjdkZjA4NThjZTRiMWNmN2E1ODJlYjFlODcwZTdhNGEwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkZTkyN2FhMjAzNmI3Yjc4OThkYWFhODk1ZDExM2FmOTU5MGQ2YTQ5ZGUwNmNlMGM0MTBhNmE0NzRkYjAxNWUxMmYzNDYxNGI5ODc4YzU4YmEwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkODExNmFhMjBhNjRhNjUzMDkxNjMzMjk4MDYyNzVhNWE5OWIyNzg1MjRiMjFhNDY1NTlkZTQ3ZWZjY2UwOThkM2ViNWIyYTBlODc3YzkwMzUwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkNjIyNWFhMjBmYTg5MDQ2YzEzNGYzYmE3ZDhlODk2NTRhYmQ4YmVlODIzODU1ODUzZDc1NDZmYWNiNzc5NmQ4ZTRmMzkzNDgxODdhYjJmM2EwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkZjEyNWFhMjAxZmEyYTA0MzA5NGJkMmJmNDUwNmRjMzQ3ZDFjNmMxODUyYTEwNTVlZTA1YWM2MjM0MjRjMWNkNWRmNTI4YjljODc2Zjk0MzYwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkODIyNWFhMjA5YmQ1M2I5OTZmOGQ4YTBhNWVjNDMxZTdjZTk0YjhhZTViMmI1NzU3MGQ2NjhmNmE3MjE3ZTNiZjA3M2Q1NTA2ODc4OGU1MzQwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkNGQyNWFhMjA3MGY2ZWRjNTU2NTdhYmM4YWVlYmQ3ZDFlNDdiYWRhZmVkNjZiNDMyZDVjODk5ZTM4MWViMmZiOWFiOGI3MzlkODczNGI2MmIwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkMzEyNGFhMjAzZjQ0NTU1NmJiZTVjNTU2MTc4ZGU4NjU1ZjE5MDc1MjBmNDZkMDQzMmYwZDFmZTg5YjM1YTgyNmUzZmQxMzU0ODc0NGE5MjUwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkNzYyM2FhMjA4Y2RlZjdiZWI3MTNkMjU2NjQ2OGZhY2FjZDc0YjliMjkyNTI4OWM0MmI3NGJiMzg4ZGM0MWQxM2ExMWZkZmFmODc4ZTA0MWIwMTAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkMmUyMmFhMjAyYjA5ZDUxZWU3NWRkNjBiNzU3ODlmMjJlZmU5MWU3OTlkZGUyOTFiMGNiN2NiYjA1NjZkYjk5ODAyN2EzNjAyODc1NjAyOWYwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkMzQxM2FhMjAzZWQyNjdjN2UzMzU4Y2NiOGQ1YjMyNjdiMmZiMmVhZmYzNzRiYTVkYjZhY2M2OWFjMWVhZGQ5NDI3MjFlY2FjODcyZDMyOWUwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkMWIxM2FhMjBhYWVhNjQ5ZjcxOTNhYjQzMjk5OTk5M2JkOGQyY2NjZTVmYWJkM2FmMDFlYmZlNDFhMGUwYTM1ODEwNDRkNDZkODcyYzMwOWIwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkYmYxMmFhMjA1NjY3OWZlNzI4YzZjMTg3NTliNzllYTRmMjBiMmU5OGIyY2Q5YTI1NzAxYzUzMzIwNGRiYWRlZWJhZWJjNzE1ODcwODkxOTMwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkZDIxMWFhMjAzN2QxNmRjMDE2OGNkMDdlYzMyYjVlYzFlMzgxN2YyNWZhNGNlNTg5NDc4M2VkZjlkMWJjZDJmYWZkMTAwMmZiODc5YTFkOTQwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkZTMxMWFhMjAzZDE0MzM3ZTRhZGVmMzM5NmIxYTE2N2Y0YzVhMGM4Nzg1NGVmMTJjZGMxMGQ2MDcwZmY5Yjg0ZDUwNGFhZTMxODdlMWM0NzgwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkOTYwZWFhMjAzZDE0MzM3ZTRhZGVmMzM5NmIxYTE2N2Y0YzVhMGM4Nzg1NGVmMTJjZGMxMGQ2MDcwZmY5Yjg0ZDUwNGFhZTMxODc5MTNlNzYwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkNDgwZWFhMjA4ZjE0OTQwNmJlNWM3ZGZkNDI5OTNjNWJmZjNhYWE1ZDRhMTcwZmUwYTQyYzk4ZjgxMzM4ZDg4YzFlZjI4MzI1ODc1NjlkNjcwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkODQwY2FhMjA1NjY3OWZlNzI4YzZjMTg3NTliNzllYTRmMjBiMmU5OGIyY2Q5YTI1NzAxYzUzMzIwNGRiYWRlZWJhZWJjNzE1ODcyNjMxNWUwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkNjEwYmFhMjAyZDMzYzA1YmNlOWI5OTA3NmZiNzFlZmZkM2U3MjZhODRkZmUyNjY3YzVhZTc1YzcyYjUyNGYyOWQ2NGNlMmVhODdmMzU0NWIwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkMDkwYmFhMjA2YWFjMzhkYmFlMWE5YzAyYWE2NGZjYjY4YzA1MGUyNmEyZjVlZjcwNWNiMjhmNmNhNjhlYmU2ODIzOWU4Y2NlODczODlmNTEwMDAwMDAwMDAwNDhlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZkZGQwOWFhMjBjMmZhOGI1MmZhMzY4NTU5NWQ3Y2FkYjBiYjJkOTNmMWIzM2Q3ZGM1N2NiMTdhMDI5MjkzOGE4MzEyYWRiNDU3ODcyMDAzMDAwMDAwMDAwMDAwNDBlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlOTgwZDA5MDA3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYzQ2YWQ0M2UwMDAwMDAwMDAxOTc2YTkxNDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDc4OGFjMmEwMDAwMDAifSx7InR4aWQiOiJjZTE4YzQ4MGZlOTgxMjUxYjgzNGU5MjczOTkzMDc3ZjVkOWU4YWQ3NGVhZGNkNzAyYzllYjg5YWYxMDAwNzhkIiwiaGFzaCI6ImNlMThjNDgwZmU5ODEyNTFiODM0ZTkyNzM5OTMwNzdmNWQ5ZThhZDc0ZWFkY2Q3MDJjOWViODlhZjEwMDA3OGQiLCJ2ZXJzaW9uIjoyLCJzaXplIjo5NDYsImxvY2t0aW1lIjo5MDAxLCJ2aW4iOlt7InR4aWQiOiJlMWI4NTY3N2Y0NjJmZTc2YmExNzBlNmMzM2I1OTgyZWE1NTg3NjUwZTNjYjQzYTA1NzY1MjE4ZGFlZDA2ZTNiIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6ImNkODJhODY5MDVjYzE3MDBlYmNiMTcwMDYwYjcwMDAwIGUzMGJkOTBjZGU4NGFmZjdiZjA3Mjc5ZjMyMTUzNzNlN2Y5YWM0YzFkNDI4ZTMzODllMTcyMWQ2OGUyYjg2OWUyMTI1ZTNjMDdjNzI0YTE3OWVjZjk2NTc5NmZjZWQ4YzJiYjZlMDY0YzE0YmI2ZTEwMjYwNmI5ZjI3MjU3MTZkIDAgMjA1NjFmZDRhNGRjN2NkZTFiMDZmMjI0ZWM3MDM4MGU4YjI3MThhMDRhYTBlZjI0OWUyN2IyODc1MzFlNjRlYzM5MjEwMmQwOWRiMDhhZjFmZjRlODQ1MzkxOWNjODY2YTRiZTQyN2Q3YmZlMThmMmMwNWU1NDQ0YzE5NmZjZjZmZDI4MTg1Mjc5MDA5YzYzNTM3OTgyNzcwMTQwOWQ1MzdhNTQ3OTdiYmJjMDAwOWQ1MWNlMDA4NzkxNjk1MmNlMDA4OGMzNTM5ZDAwY2MwMGM2YTI2OTAwY2QwMGM3ODgwMGQxMDBjZTg4NTI3OTU0N2Y3NTgxNTM3OTU4N2Y3NzU0N2Y3NTgxNTQ3YTVjN2Y3NzU0N2Y3NTgxNTFjZjU2N2Y3NzUyN2Y3NTgxNTM3YTU2ODA3YzUyODA3ZTdiNTQ4MDdlN2M1NDgwN2UwMGNjMDBjNmEyNjkwMGNkMDBjNzg4MDBkMTAwY2U4ODUxZDI4ODUxZDE1MWNlODg1MmQxMDA4OGM0NTM5Yzc3Nzc2NzdiNTE5ZDAwY2U3Yjg3Nzc2OCIsImhleCI6IjEwY2Q4MmE4NjkwNWNjMTcwMGViY2IxNzAwNjBiNzAwMDA0MGUzMGJkOTBjZGU4NGFmZjdiZjA3Mjc5ZjMyMTUzNzNlN2Y5YWM0YzFkNDI4ZTMzODllMTcyMWQ2OGUyYjg2OWUyMTI1ZTNjMDdjNzI0YTE3OWVjZjk2NTc5NmZjZWQ4YzJiYjZlMDY0YzE0YmI2ZTEwMjYwNmI5ZjI3MjU3MTZkMDA0Y2QxMjA1NjFmZDRhNGRjN2NkZTFiMDZmMjI0ZWM3MDM4MGU4YjI3MThhMDRhYTBlZjI0OWUyN2IyODc1MzFlNjRlYzM5MjEwMmQwOWRiMDhhZjFmZjRlODQ1MzkxOWNjODY2YTRiZTQyN2Q3YmZlMThmMmMwNWU1NDQ0YzE5NmZjZjZmZDI4MTg1Mjc5MDA5YzYzNTM3OTgyNzcwMTQwOWQ1MzdhNTQ3OTdiYmJjMDAwOWQ1MWNlMDA4NzkxNjk1MmNlMDA4OGMzNTM5ZDAwY2MwMGM2YTI2OTAwY2QwMGM3ODgwMGQxMDBjZTg4NTI3OTU0N2Y3NTgxNTM3OTU4N2Y3NzU0N2Y3NTgxNTQ3YTVjN2Y3NzU0N2Y3NTgxNTFjZjU2N2Y3NzUyN2Y3NTgxNTM3YTU2ODA3YzUyODA3ZTdiNTQ4MDdlN2M1NDgwN2UwMGNjMDBjNmEyNjkwMGNkMDBjNzg4MDBkMTAwY2U4ODUxZDI4ODUxZDE1MWNlODg1MmQxMDA4OGM0NTM5Yzc3Nzc2NzdiNTE5ZDAwY2U3Yjg3Nzc2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZTFiODU2NzdmNDYyZmU3NmJhMTcwZTZjMzNiNTk4MmVhNTU4NzY1MGUzY2I0M2EwNTc2NTIxOGRhZWQwNmUzYiIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIwIDIwMTc3ZGI1OGJlNDgyNjRjYjFmMWFjZWYwZWYzMzI1NDFmNDVjM2U0OTY4YjA2ZGQ5Y2I1ZDJjMTRmMTM3YmFkOTIwYzFjM2YxYjAwMDYzMDEzNjA2OWU4NmM2MTdhYjZlZWVjZjY1ZmQyMjU5M2ZkYTU1NzhiYTlhNDQyNjczNDM1ZTUyNzkwMDljNjMwMGNlMDEyMDdmNzU4OGMwZDI3NjgyNzc2MGEyNjljMGNmNzg1ODdmNzc1NDdmNzU4MTc4NTg3Zjc3NTQ3Zjc1ODFhMDY5N2M1NjdmNzU4MTdjNTY3Zjc1ODFhMDY5YzBjY2MwYzZhMjY5YzBjZGMwYzc4OGMwZDFjMGNlODc3Nzc3Njc1Mjc5NTE5YzYzYzBjZjU2N2Y3NzUyN2Y3NTgxYzBjY2MwYzY3YjkzYTI2OWMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Nzc3NzY3N2I1MjlkMDBjZTAxMjA3Zjc1N2I4OGMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Njg2OCIsImhleCI6IjAwNGNjZjIwMTc3ZGI1OGJlNDgyNjRjYjFmMWFjZWYwZWYzMzI1NDFmNDVjM2U0OTY4YjA2ZGQ5Y2I1ZDJjMTRmMTM3YmFkOTIwYzFjM2YxYjAwMDYzMDEzNjA2OWU4NmM2MTdhYjZlZWVjZjY1ZmQyMjU5M2ZkYTU1NzhiYTlhNDQyNjczNDM1ZTUyNzkwMDljNjMwMGNlMDEyMDdmNzU4OGMwZDI3NjgyNzc2MGEyNjljMGNmNzg1ODdmNzc1NDdmNzU4MTc4NTg3Zjc3NTQ3Zjc1ODFhMDY5N2M1NjdmNzU4MTdjNTY3Zjc1ODFhMDY5YzBjY2MwYzZhMjY5YzBjZGMwYzc4OGMwZDFjMGNlODc3Nzc3Njc1Mjc5NTE5YzYzYzBjZjU2N2Y3NzUyN2Y3NTgxYzBjY2MwYzY3YjkzYTI2OWMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Nzc3NzY3N2I1MjlkMDBjZTAxMjA3Zjc1N2I4OGMwY2RjMGM3ODhjMGQxYzBjZTg4YzBkMmMwY2Y4Nzc3Njg2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZTFiODU2NzdmNDYyZmU3NmJhMTcwZTZjMzNiNTk4MmVhNTU4NzY1MGUzY2I0M2EwNTc2NTIxOGRhZWQwNmUzYiIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiI2YmJhODVlMmJmOTJlYmFmZTZjZGQ3OWQyYzlkZDNiNTllOWQ5NjM3NjA4MmRkZDcyMzRkMThmZjNmNDg1NmI2YWYyNDNiZTNiNjVhZDczZjMzM2FmMTI4NDQ5M2E3MmU4MjhkZGZkNjQxZThiYTljNjRmNmE0YjA1NjRiNmNmZVtBTEx8Rk9SS0lEfFVUWE9TXSAwMjQ1NDk2ZWJiZjNkOTBmYTk0YTM5NWEzYjZjYzY4NTJmYmM2MjFlZTQxNDdiYmFjMDU4MGE1OWQ4MDNjYWU4ZjgiLCJoZXgiOiI0MTZiYmE4NWUyYmY5MmViYWZlNmNkZDc5ZDJjOWRkM2I1OWU5ZDk2Mzc2MDgyZGRkNzIzNGQxOGZmM2Y0ODU2YjZhZjI0M2JlM2I2NWFkNzNmMzMzYWYxMjg0NDkzYTcyZTgyOGRkZmQ2NDFlOGJhOWM2NGY2YTRiMDU2NGI2Y2ZlNjEyMTAyNDU0OTZlYmJmM2Q5MGZhOTRhMzk1YTNiNmNjNjg1MmZiYzYyMWVlNDE0N2JiYWMwNTgwYTU5ZDgwM2NhZThmOCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MWUtMDUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDNiNzFiZWQyM2JmN2U2MDZkMmY5MTc4ZDg1ZTE1MTcwZDY4YzQ1YTEwYTllODZkYjUyMGFjMDhlM2VmNDFmNmEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwM2I3MWJlZDIzYmY3ZTYwNmQyZjkxNzhkODVlMTUxNzBkNjhjNDVhMTBhOWU4NmRiNTIwYWMwOGUzZWY0MWY2YTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHZhaHIwa2o4MG03dnBramx5dGNtcDBwMjljZGRyejk1eTlmYXBrbTJnOXZwcjM3N3MwazU1NzRkcjJ3YyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiNWU0MzczMjY0NDlhYmE3ODU1ZGEzZjU5MjJmZDY1Y2ZlZTZlYWIxN2M2ODY5ZTA2MzYwMTYzMDBiMGYxYzNjMSIsImFtb3VudCI6IjAiLCJuZnQiOnsiY2FwYWJpbGl0eSI6Im5vbmUiLCJjb21taXRtZW50IjoiIn19fSx7InZhbHVlIjowLjA2MDI1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBlZDY3MzA5YjM2NDI0YzFiM2YyNWUwOGYxOWQ4ZWFkNGVhZGU5MTdjNDc2ZTViMTY1MjBkZWFjNjNkNjVkNGE5IE9QX0VRVUFMIiwiaGV4IjoiYWEyMGVkNjczMDliMzY0MjRjMWIzZjI1ZTA4ZjE5ZDhlYWQ0ZWFkZTkxN2M0NzZlNWIxNjUyMGRlYWM2M2Q2NWQ0YTk4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwa2t3dnlteGVweWN4ZWx5aHNnN3h3Y2F0Mnc0aDUzMDNya3VrY2syZ3g3NDMzYXZoMjJqbHYzOXBkenQiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImQwZDQ2ZjVjYmQ4MjE4OGFjZWRlMGQzZTQ5Yzc1NzAwYzE5Y2I4MzMxYTMwMTAxZjBiYjZhMjYwMDY2YWM5NzIiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJtdXRhYmxlIiwiY29tbWl0bWVudCI6ImNkODJhODY5MDAwMGU4MDNlYmNiMTcwMDYwYjcwMDAwIn19fSx7InZhbHVlIjowLjAzMTg1MjIyLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTI1ZTliY2VjZWEwZTlhMTI5ZWRlMzkyYTA3NWRhMmVmMDNhMjJjMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkyNWU5YmNlY2VhMGU5YTEyOWVkZTM5MmEwNzVkYTJlZjAzYTIyYzE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpmOWF4N3dlNnN3bmdmZmFoM2U5Z3I0bWdoMHF3M3pjeTZrcTVhZmF1Il19fV0sImZlZSI6OS40NmUtMDYsImhleCI6IjAyMDAwMDAwMDMzYjZlZDBhZThkMjE2NTU3YTA0M2NiZTM1MDc2NThhNTJlOThiNTMzNmMwZTE3YmE3NmZlNjJmNDc3NTZiOGUxMDAwMDAwMDBmZDI2MDExMGNkODJhODY5MDVjYzE3MDBlYmNiMTcwMDYwYjcwMDAwNDBlMzBiZDkwY2RlODRhZmY3YmYwNzI3OWYzMjE1MzczZTdmOWFjNGMxZDQyOGUzMzg5ZTE3MjFkNjhlMmI4NjllMjEyNWUzYzA3YzcyNGExNzllY2Y5NjU3OTZmY2VkOGMyYmI2ZTA2NGMxNGJiNmUxMDI2MDZiOWYyNzI1NzE2ZDAwNGNkMTIwNTYxZmQ0YTRkYzdjZGUxYjA2ZjIyNGVjNzAzODBlOGIyNzE4YTA0YWEwZWYyNDllMjdiMjg3NTMxZTY0ZWMzOTIxMDJkMDlkYjA4YWYxZmY0ZTg0NTM5MTljYzg2NmE0YmU0MjdkN2JmZTE4ZjJjMDVlNTQ0NGMxOTZmY2Y2ZmQyODE4NTI3OTAwOWM2MzUzNzk4Mjc3MDE0MDlkNTM3YTU0Nzk3YmJiYzAwMDlkNTFjZTAwODc5MTY5NTJjZTAwODhjMzUzOWQwMGNjMDBjNmEyNjkwMGNkMDBjNzg4MDBkMTAwY2U4ODUyNzk1NDdmNzU4MTUzNzk1ODdmNzc1NDdmNzU4MTU0N2E1YzdmNzc1NDdmNzU4MTUxY2Y1NjdmNzc1MjdmNzU4MTUzN2E1NjgwN2M1MjgwN2U3YjU0ODA3ZTdjNTQ4MDdlMDBjYzAwYzZhMjY5MDBjZDAwYzc4ODAwZDEwMGNlODg1MWQyODg1MWQxNTFjZTg4NTJkMTAwODhjNDUzOWM3Nzc3Njc3YjUxOWQwMGNlN2I4Nzc3NjhmZmZmZmZmZjNiNmVkMGFlOGQyMTY1NTdhMDQzY2JlMzUwNzY1OGE1MmU5OGI1MzM2YzBlMTdiYTc2ZmU2MmY0Nzc1NmI4ZTEwMTAwMDAwMGQyMDA0Y2NmMjAxNzdkYjU4YmU0ODI2NGNiMWYxYWNlZjBlZjMzMjU0MWY0NWMzZTQ5NjhiMDZkZDljYjVkMmMxNGYxMzdiYWQ5MjBjMWMzZjFiMDAwNjMwMTM2MDY5ZTg2YzYxN2FiNmVlZWNmNjVmZDIyNTkzZmRhNTU3OGJhOWE0NDI2NzM0MzVlNTI3OTAwOWM2MzAwY2UwMTIwN2Y3NTg4YzBkMjc2ODI3NzYwYTI2OWMwY2Y3ODU4N2Y3NzU0N2Y3NTgxNzg1ODdmNzc1NDdmNzU4MWEwNjk3YzU2N2Y3NTgxN2M1NjdmNzU4MWEwNjljMGNjYzBjNmEyNjljMGNkYzBjNzg4YzBkMWMwY2U4Nzc3Nzc2NzUyNzk1MTljNjNjMGNmNTY3Zjc3NTI3Zjc1ODFjMGNjYzBjNjdiOTNhMjY5YzBjZGMwYzc4OGMwZDFjMGNlODhjMGQyYzBjZjg3Nzc3Nzc3Njc3YjUyOWQwMGNlMDEyMDdmNzU3Yjg4YzBjZGMwYzc4OGMwZDFjMGNlODhjMGQyYzBjZjg3Nzc2ODY4ZmZmZmZmZmYzYjZlZDBhZThkMjE2NTU3YTA0M2NiZTM1MDc2NThhNTJlOThiNTMzNmMwZTE3YmE3NmZlNjJmNDc3NTZiOGUxMDIwMDAwMDA2NDQxNmJiYTg1ZTJiZjkyZWJhZmU2Y2RkNzlkMmM5ZGQzYjU5ZTlkOTYzNzYwODJkZGQ3MjM0ZDE4ZmYzZjQ4NTZiNmFmMjQzYmUzYjY1YWQ3M2YzMzNhZjEyODQ0OTNhNzJlODI4ZGRmZDY0MWU4YmE5YzY0ZjZhNGIwNTY0YjZjZmU2MTIxMDI0NTQ5NmViYmYzZDkwZmE5NGEzOTVhM2I2Y2M2ODUyZmJjNjIxZWU0MTQ3YmJhYzA1ODBhNTlkODAzY2FlOGY4ZmZmZmZmZmYwM2U4MDMwMDAwMDAwMDAwMDA0NWVmYzFjM2YxYjAwMDYzMDEzNjA2OWU4NmM2MTdhYjZlZWVjZjY1ZmQyMjU5M2ZkYTU1NzhiYTlhNDQyNjczNDM1ZTIwYWEyMDNiNzFiZWQyM2JmN2U2MDZkMmY5MTc4ZDg1ZTE1MTcwZDY4YzQ1YTEwYTllODZkYjUyMGFjMDhlM2VmNDFmNmE4NzI4ZWY1YjAwMDAwMDAwMDA1NmVmNzJjOTZhMDY2MGEyYjYwYjFmMTAzMDFhMzNiODljYzEwMDU3Yzc0OTNlMGRkZWNlOGExODgyYmQ1YzZmZDRkMDYxMTBjZDgyYTg2OTAwMDBlODAzZWJjYjE3MDA2MGI3MDAwMGFhMjBlZDY3MzA5YjM2NDI0YzFiM2YyNWUwOGYxOWQ4ZWFkNGVhZGU5MTdjNDc2ZTViMTY1MjBkZWFjNjNkNjVkNGE5ODc0NjlhMzAwMDAwMDAwMDAwMTk3NmE5MTQ5MjVlOWJjZWNlYTBlOWExMjllZGUzOTJhMDc1ZGEyZWYwM2EyMmMxODhhYzI5MjMwMDAwIn0seyJ0eGlkIjoiZDFkMTBmODU0ZTNkY2Q3NjUyZTg1YTk1OThlM2RjODI1MGY5NDgwN2VjN2QzNzlkZjFhNGFjMWY4M2YzYWIxOCIsImhhc2giOiJkMWQxMGY4NTRlM2RjZDc2NTJlODVhOTU5OGUzZGM4MjUwZjk0ODA3ZWM3ZDM3OWRmMWE0YWMxZjgzZjNhYjE4IiwidmVyc2lvbiI6Miwic2l6ZSI6Mzk0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiNzAxOTJhNmE5YmRmMWNjMTFlYWJlOWU0NTU0MWFkM2U0NzlhZjYwMTJmN2EwMmNiMDg2Y2ZiZGRmY2RkYTdiMiIsInZvdXQiOjMsInNjcmlwdFNpZyI6eyJhc20iOiJiNDEyMDVhZDViZDMxMWY4NGI2NWYxYWIwOTk0ZGRkMDIxZmRiYTE2YjVkOTA2ZTQxMTA0MmE3MWQzNjlkM2FkMmQxZjM1YWNmN2EwMzAxMDRiYzIxYTQzMzMzZTA2Y2QxOTVmNDQ0YTVjMWE1NTc4NTRjNWUwODZkMTUwYzA4ZFtBTEx8Rk9SS0lEXSAwM2VmNWQ2YWE0M2RlNGM5YmM4YTFiN2YyYzJlMzI1ZTBiOWIyODY2NzgyNzc5MjQ4MjI0YmU0ZDJjMGM2MzBmNWQiLCJoZXgiOiI0MWI0MTIwNWFkNWJkMzExZjg0YjY1ZjFhYjA5OTRkZGQwMjFmZGJhMTZiNWQ5MDZlNDExMDQyYTcxZDM2OWQzYWQyZDFmMzVhY2Y3YTAzMDEwNGJjMjFhNDMzMzNlMDZjZDE5NWY0NDRhNWMxYTU1Nzg1NGM1ZTA4NmQxNTBjMDhkNDEyMTAzZWY1ZDZhYTQzZGU0YzliYzhhMWI3ZjJjMmUzMjVlMGI5YjI4NjY3ODI3NzkyNDgyMjRiZTRkMmMwYzYzMGY1ZCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH0seyJ0eGlkIjoiNDc1YTU0MzllMjZlMDcwMTBmMzRhZjQwYzgxYjczNzcxZWIyYWIyNjI2ZTY2ZTFkYjFkNzdiMmVjNjFmN2Q3MSIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiJmY2M0ZWE3ZjFhYTgzY2YyZGEyNjA0MTkzZmZmYTg4MmIzZWVhMDg3ZTMyYTc0ODA5NGEyMmJlMmZhYTc1YTBjNmZkNGQyYzRjYzNlMzM2YjE3MDhiNjVkYzA1ZTQxZDM3OGM2NWQ5OTlkZDM5ZTEzZjFjMDVjMWJlOWQyNzkyN1tBTEx8Rk9SS0lEXSAwM2VmNWQ2YWE0M2RlNGM5YmM4YTFiN2YyYzJlMzI1ZTBiOWIyODY2NzgyNzc5MjQ4MjI0YmU0ZDJjMGM2MzBmNWQiLCJoZXgiOiI0MWZjYzRlYTdmMWFhODNjZjJkYTI2MDQxOTNmZmZhODgyYjNlZWEwODdlMzJhNzQ4MDk0YTIyYmUyZmFhNzVhMGM2ZmQ0ZDJjNGNjM2UzMzZiMTcwOGI2NWRjMDVlNDFkMzc4YzY1ZDk5OWRkMzllMTNmMWMwNWMxYmU5ZDI3OTI3NDEyMTAzZWY1ZDZhYTQzZGU0YzliYzhhMWI3ZjJjMmUzMjVlMGI5YjI4NjY3ODI3NzkyNDgyMjRiZTRkMmMwYzYzMGY1ZCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NH1dLCJ2b3V0IjpbeyJ2YWx1ZSI6OC4yZS0wNSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4NjQwMzY1N2YwYzdiMTc4OWViMjM0NzJmMTA3MjUwNjE5NDBmN2I0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6cnlxZGpoN3JybXo3eTdrZzY4OXVnOHk1cnBqczhoa3M1bW1leGE0MyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiNWE0ZjZiMjUyNDNjMWEyZGFiYjI0MzRlM2Q5ZTU3NGY2NWMzMTc2NGNlMGU3ZWI0MTI3YTQ2ZmE3NDY1NzY5MSIsImFtb3VudCI6IjAiLCJuZnQiOnsiY2FwYWJpbGl0eSI6Im1pbnRpbmciLCJjb21taXRtZW50IjoiIn19fSx7InZhbHVlIjowLjA3MTM4ODE4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODY0MDM2NTdmMGM3YjE3ODllYjIzNDcyZjEwNzI1MDYxOTQwZjdiNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpyeXFkamg3cnJtejd5N2tnNjg5dWc4eTVycGpzOGhrczVtbWV4YTQzIl19fV0sImZlZSI6MWUtMDUsImhleCI6IjAyMDAwMDAwMDJiMmE3ZGRmY2RkZmI2YzA4Y2IwMjdhMmYwMWY2OWE0NzNlYWQ0MTU1ZTRlOWFiMWVjMTFjZGY5YjZhMmExOTcwMDMwMDAwMDA2NDQxYjQxMjA1YWQ1YmQzMTFmODRiNjVmMWFiMDk5NGRkZDAyMWZkYmExNmI1ZDkwNmU0MTEwNDJhNzFkMzY5ZDNhZDJkMWYzNWFjZjdhMDMwMTA0YmMyMWE0MzMzM2UwNmNkMTk1ZjQ0NGE1YzFhNTU3ODU0YzVlMDg2ZDE1MGMwOGQ0MTIxMDNlZjVkNmFhNDNkZTRjOWJjOGExYjdmMmMyZTMyNWUwYjliMjg2Njc4Mjc3OTI0ODIyNGJlNGQyYzBjNjMwZjVkZmVmZmZmZmY3MTdkMWZjNjJlN2JkN2IxMWQ2ZWU2MjYyNmFiYjIxZTc3NzMxYmM4NDBhZjM0MGYwMTA3NmVlMjM5NTQ1YTQ3MDEwMDAwMDA2NDQxZmNjNGVhN2YxYWE4M2NmMmRhMjYwNDE5M2ZmZmE4ODJiM2VlYTA4N2UzMmE3NDgwOTRhMjJiZTJmYWE3NWEwYzZmZDRkMmM0Y2MzZTMzNmIxNzA4YjY1ZGMwNWU0MWQzNzhjNjVkOTk5ZGQzOWUxM2YxYzA1YzFiZTlkMjc5Mjc0MTIxMDNlZjVkNmFhNDNkZTRjOWJjOGExYjdmMmMyZTMyNWUwYjliMjg2Njc4Mjc3OTI0ODIyNGJlNGQyYzBjNjMwZjVkZmVmZmZmZmYwMjA4MjAwMDAwMDAwMDAwMDAzYmVmOTE3NjY1NzRmYTQ2N2ExMmI0N2UwZWNlNjQxN2MzNjU0ZjU3OWUzZDRlNDNiMmFiMmQxYTNjMjQyNTZiNGY1YTIyNzZhOTE0ODY0MDM2NTdmMGM3YjE3ODllYjIzNDcyZjEwNzI1MDYxOTQwZjdiNDg4YWMwMmVlNmMwMDAwMDAwMDAwMTk3NmE5MTQ4NjQwMzY1N2YwYzdiMTc4OWViMjM0NzJmMTA3MjUwNjE5NDBmN2I0ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiZDQ5OTExZjhhYTZhM2RhZjgwOTc0YmVhZDY5NjA2NGFjNTVjYmM0NjcyMDU5YzVlM2FlMzliODJkMzc4MzQ4MCIsImhhc2giOiJkNDk5MTFmOGFhNmEzZGFmODA5NzRiZWFkNjk2MDY0YWM1NWNiYzQ2NzIwNTljNWUzYWUzOWI4MmQzNzgzNDgwIiwidmVyc2lvbiI6MSwic2l6ZSI6MTE0MDgsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiIwZGMwZTMzODA0YTVlYWYwYzBjMzAzMGI0Y2ZjY2QzN2U1NWFiNzg1NWZlMTc4YTYyOTZkN2FlNWMwOWZkZDUzIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6ImJhYjQ3YWNlYWQzYzczZTliMmNkOTVkMjc4ZWZjYmU3ZTQzYWViOWE1NzBjM2QyMDNlODFmMTk1MDk4YTc1ODllYzVmNzA3Mjg1Zjk1ZDhiNGY1MGVhNjIzYmZiZDEyODI5NDMyYzU2NmQ1NzgyMWI2ZjM1NjkzYjNiNWY2N2RmW0FMTHxGT1JLSURdIDAyMDkxYzE1OTAyMTI4NjAyYzEyNGI2YjJlMDJjMjI3MTNiYjdkNGM2MTQzNTg1ZDQyZGZiZDg1MmY5YzZlMzRjYyIsImhleCI6IjQxYmFiNDdhY2VhZDNjNzNlOWIyY2Q5NWQyNzhlZmNiZTdlNDNhZWI5YTU3MGMzZDIwM2U4MWYxOTUwOThhNzU4OWVjNWY3MDcyODVmOTVkOGI0ZjUwZWE2MjNiZmJkMTI4Mjk0MzJjNTY2ZDU3ODIxYjZmMzU2OTNiM2I1ZjY3ZGY0MTIxMDIwOTFjMTU5MDIxMjg2MDJjMTI0YjZiMmUwMmMyMjcxM2JiN2Q0YzYxNDM1ODVkNDJkZmJkODUyZjljNmUzNGNjIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIxMGYyMWNkYTJiYTg2ZTU0ZmM0N2FiM2ZhZjdkNTg3MDNiMjcxZDM1NDI1MWVhOTU1YWE1NTk3OTY5ZWQ3OWI5Iiwidm91dCI6MTIsInNjcmlwdFNpZyI6eyJhc20iOiIxZjRkODE1ODJiZTlhMTI5YWEzZTgwNjI2NzhjNTU5ZDllZTc2NGVhZmJiNGJkYzcyYjkwYWY4YjE1ZWY3NjIwNWUyZjRiZjA0OGU4NjJhYzVkMjFkMTcwNWYxZjg2MWRkZmNhNWY5YjQ5MGY1ZGNiMTgzMzczMTExOTA3MTUyM1tBTEx8Rk9SS0lEXSAwMmQzNmY1MzIzMzg2Zjc2MzNhOGYyYTM5ZmI2MDY1YjZmNzJiZGRlMjYyM2Q0MmM3MmU1NmRiZjliYzNiMmQ2OWUiLCJoZXgiOiI0MTFmNGQ4MTU4MmJlOWExMjlhYTNlODA2MjY3OGM1NTlkOWVlNzY0ZWFmYmI0YmRjNzJiOTBhZjhiMTVlZjc2MjA1ZTJmNGJmMDQ4ZTg2MmFjNWQyMWQxNzA1ZjFmODYxZGRmY2E1ZjliNDkwZjVkY2IxODMzNzMxMTE5MDcxNTIzNDEyMTAyZDM2ZjUzMjMzODZmNzYzM2E4ZjJhMzlmYjYwNjViNmY3MmJkZGUyNjIzZDQyYzcyZTU2ZGJmOWJjM2IyZDY5ZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMWMzY2RlMGYyOWRiN2UzNzRjYWYwOTdmOGE4NTY2MjM2MjMxZGVhYWFiOTc0NmUxZmIzYTYzOGVmM2FjN2RkOCIsInZvdXQiOjI1LCJzY3JpcHRTaWciOnsiYXNtIjoiNzIyOTVjYzhlOTIyYzBhNmMxZGE4N2NmMmUyYmRhOGUyYTNmY2JmZjY0OWIyN2MyYWNhNmZmYzJlYTU1YWQ1MDYxOWZlOTkxNDcyYzU2ODY4Y2FiZDRjNTcxZTgxMmE3MjZiZGNhYWUyNjNiZGNjZWM2MTU5YmFjZWMzYWE3OTlbQUxMfEZPUktJRF0gMDNlN2U1MjFjNWNmYWI0MjY2NzQ0NmI2YWRlNGI2NGFkZDA4NTJhMjdhYzQ5YWI1MjAyMTM4NTY4YzNjZjkyY2QwIiwiaGV4IjoiNDE3MjI5NWNjOGU5MjJjMGE2YzFkYTg3Y2YyZTJiZGE4ZTJhM2ZjYmZmNjQ5YjI3YzJhY2E2ZmZjMmVhNTVhZDUwNjE5ZmU5OTE0NzJjNTY4NjhjYWJkNGM1NzFlODEyYTcyNmJkY2FhZTI2M2JkY2NlYzYxNTliYWNlYzNhYTc5OTQxMjEwM2U3ZTUyMWM1Y2ZhYjQyNjY3NDQ2YjZhZGU0YjY0YWRkMDg1MmEyN2FjNDlhYjUyMDIxMzg1NjhjM2NmOTJjZDAifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjJmMjMwNTBlYjUzMTIxY2MyN2Y1N2M2MzEyMTcwNTY3ODAyYmE1MGVlNzdlNmIxYmFmZTAzMDVkZTdlMWMyODgiLCJ2b3V0Ijo5LCJzY3JpcHRTaWciOnsiYXNtIjoiM2Y5M2JkNTVjNjg3NmJhNzYzODIwMjkzMjJkMWRmODI0NGIzYmQzYmNlNmE4YmQ4ZTM0ODQwNjkzZjAzZTA1OWNhNDI3YjJmY2FmNTFhNmY4ZWY3N2U1MDIyNmI3M2JiMDQ3ZGY3NmYwOGU5MjJkM2RkZGM4MzNlNTEzNmJkNWNbQUxMfEZPUktJRF0gMDNmMzc2NTdhYmJmODY5YjZhZjU2MzQ4OTIwMWQzZGQ3ODk3MWY5YzAwMzcxYzU2ZmJmOTYxYzE3NTViMDczMjQ2IiwiaGV4IjoiNDEzZjkzYmQ1NWM2ODc2YmE3NjM4MjAyOTMyMmQxZGY4MjQ0YjNiZDNiY2U2YThiZDhlMzQ4NDA2OTNmMDNlMDU5Y2E0MjdiMmZjYWY1MWE2ZjhlZjc3ZTUwMjI2YjczYmIwNDdkZjc2ZjA4ZTkyMmQzZGRkYzgzM2U1MTM2YmQ1YzQxMjEwM2YzNzY1N2FiYmY4NjliNmFmNTYzNDg5MjAxZDNkZDc4OTcxZjljMDAzNzFjNTZmYmY5NjFjMTc1NWIwNzMyNDYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjMxNTBlY2QyNDFmOTc0OGZmY2RjZWIyYWI5N2YyODc0MjhmZmY3NDUwMDNhNmIyNmRkZDU4YzM5NjQ5YjIxNjAiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMTlkYWJhZTZjODY1OWE3NzBlZDBlYjJjZDYyNTk0ZDNjYjZlNmIyNWUxZDI2OWM0N2FjYzM1NzkzMDA3ZTllNzVmYTA4MGI3ZWFkODdhNzU5YzU3ZmEzYjZkN2NmNTRmNDc2NGIzYmEzZGVkMDJhMWJmODM0NTY0M2RiODU0MTNbQUxMfEZPUktJRF0gMDMyMmU3MmE4M2IxMjZkMjk1MzVlZGJiOWQ2NDViMTRmYmY2YTgxMGExYTI0MTA3ZDA4ZGJlNmY2ZTZhMmZiZjMxIiwiaGV4IjoiNDExOWRhYmFlNmM4NjU5YTc3MGVkMGViMmNkNjI1OTRkM2NiNmU2YjI1ZTFkMjY5YzQ3YWNjMzU3OTMwMDdlOWU3NWZhMDgwYjdlYWQ4N2E3NTljNTdmYTNiNmQ3Y2Y1NGY0NzY0YjNiYTNkZWQwMmExYmY4MzQ1NjQzZGI4NTQxMzQxMjEwMzIyZTcyYTgzYjEyNmQyOTUzNWVkYmI5ZDY0NWIxNGZiZjZhODEwYTFhMjQxMDdkMDhkYmU2ZjZlNmEyZmJmMzEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjM4ZmI1ZTA4ZjhiYTE0YjVkYmJlZmYxYzQ4NTQ0NGY1YzQ5NTMyZThjMDA1MDQ3ZTIzZDI1NTEyNzY1ZjVlNzUiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiN2U4YjU4YmU3MjcxMDU3YjQ0NWY5OTMxMjQ1MDExOWJlODM5ZDk0YzQ4ZTUyMDhkYzNlOTgwZTVjM2I5OTYzMjFmMjNiZDc1ZjdiNTJmNWFmYzYxYjU3ODgwNDA4MzU4ZjUwNWIyMjZkNGNhZDhlNThhZDQ4NmYwZDBkOWYzMTlbQUxMfEZPUktJRF0gMDJhMGNjNDQzMTg2Yzc1MGUxOTUyNGNhZTE1YWZmMzQzYmQ1ZDllNzE0MDJlOTBkNjlhYzFlY2U2OGVhMGM5YmNmIiwiaGV4IjoiNDE3ZThiNThiZTcyNzEwNTdiNDQ1Zjk5MzEyNDUwMTE5YmU4MzlkOTRjNDhlNTIwOGRjM2U5ODBlNWMzYjk5NjMyMWYyM2JkNzVmN2I1MmY1YWZjNjFiNTc4ODA0MDgzNThmNTA1YjIyNmQ0Y2FkOGU1OGFkNDg2ZjBkMGQ5ZjMxOTQxMjEwMmEwY2M0NDMxODZjNzUwZTE5NTI0Y2FlMTVhZmYzNDNiZDVkOWU3MTQwMmU5MGQ2OWFjMWVjZTY4ZWEwYzliY2YifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjM4ZmI1ZTA4ZjhiYTE0YjVkYmJlZmYxYzQ4NTQ0NGY1YzQ5NTMyZThjMDA1MDQ3ZTIzZDI1NTEyNzY1ZjVlNzUiLCJ2b3V0IjoyNSwic2NyaXB0U2lnIjp7ImFzbSI6IjUwNzBmMDc5OWNiOTI0NGY2MmEwYWIxMzU5OWEwY2FiM2M0NTJiYjdkMDc4YmU4MDNmYTY0YzEwNDg5NDI4YjhiZmYyZTM4Nzk0MjdkMWUyZTBmNjJkZjUwYzQyOWQyNDU0NmFhZjRlNTg4NWEzZDQ1NTExYzYwYzFkMWY2ZmIxW0FMTHxGT1JLSURdIDAzZGMxNDhhZmJjOTczNTE2MGQyYWUyNTFkNzk4ZjdlMGIwYjZmZTlkNmM3ZDgxNGI3MjJjNjVjNmY4ZmExZmUwYyIsImhleCI6IjQxNTA3MGYwNzk5Y2I5MjQ0ZjYyYTBhYjEzNTk5YTBjYWIzYzQ1MmJiN2QwNzhiZTgwM2ZhNjRjMTA0ODk0MjhiOGJmZjJlMzg3OTQyN2QxZTJlMGY2MmRmNTBjNDI5ZDI0NTQ2YWFmNGU1ODg1YTNkNDU1MTFjNjBjMWQxZjZmYjE0MTIxMDNkYzE0OGFmYmM5NzM1MTYwZDJhZTI1MWQ3OThmN2UwYjBiNmZlOWQ2YzdkODE0YjcyMmM2NWM2ZjhmYTFmZTBjIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIzOGZiNWUwOGY4YmExNGI1ZGJiZWZmMWM0ODU0NDRmNWM0OTUzMmU4YzAwNTA0N2UyM2QyNTUxMjc2NWY1ZTc1Iiwidm91dCI6MjksInNjcmlwdFNpZyI6eyJhc20iOiI5MTEyOTA0YTc3MThjNjgwNDVjNGE0MzZkY2I4MWRhYjg4M2JlNjdhY2ZlZDRjNTlhNWFmMzQ1ODkwY2JlNDM4NjU2OTA5Njk4MjEyZDRhOWU4MmMwYTE4NTMwZTU5MjRlYmY3YjU5NmYwYjgwYWZlNjQyNDlkZjBmNDAxZGYzZFtBTEx8Rk9SS0lEXSAwMmQ4M2YyZGUzMWViM2U0OGI2M2Y5ZjQ2NWY1NGExMTAyNzAwNmJkMjQ4ZGMyM2E4MDc1MGE4N2M4MjBiMzFhZGQiLCJoZXgiOiI0MTkxMTI5MDRhNzcxOGM2ODA0NWM0YTQzNmRjYjgxZGFiODgzYmU2N2FjZmVkNGM1OWE1YWYzNDU4OTBjYmU0Mzg2NTY5MDk2OTgyMTJkNGE5ZTgyYzBhMTg1MzBlNTkyNGViZjdiNTk2ZjBiODBhZmU2NDI0OWRmMGY0MDFkZjNkNDEyMTAyZDgzZjJkZTMxZWIzZTQ4YjYzZjlmNDY1ZjU0YTExMDI3MDA2YmQyNDhkYzIzYTgwNzUwYTg3YzgyMGIzMWFkZCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOjQwLCJzY3JpcHRTaWciOnsiYXNtIjoiYmY5YjZhZGM0MmI0NTFjMWQ5Y2I0NWYwOGFjN2IwNDlmZDc2ODA2OTgwOTYzZDIwMmNjMTliYzM1OWE2MGUyZmQ1ZWJjYTNiZjA0MDM2Njk3ZjVhYmM0ODJmMTNlNzRlZDU1ZmE0NTczNDAzZjdmOTBhNGMyNGNmMzczYzQ3YmVbQUxMfEZPUktJRF0gMDNmYzc2ZjI4MDNmZjRlZjAyZDA4MTA2M2JhNDRkYmVkMDFmNWJkMGYzNTU2MGVhMDZhZWYxMTVlZTgxMTUwYTc3IiwiaGV4IjoiNDFiZjliNmFkYzQyYjQ1MWMxZDljYjQ1ZjA4YWM3YjA0OWZkNzY4MDY5ODA5NjNkMjAyY2MxOWJjMzU5YTYwZTJmZDVlYmNhM2JmMDQwMzY2OTdmNWFiYzQ4MmYxM2U3NGVkNTVmYTQ1NzM0MDNmN2Y5MGE0YzI0Y2YzNzNjNDdiZTQxMjEwM2ZjNzZmMjgwM2ZmNGVmMDJkMDgxMDYzYmE0NGRiZWQwMWY1YmQwZjM1NTYwZWEwNmFlZjExNWVlODExNTBhNzcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjM4ZmI1ZTA4ZjhiYTE0YjVkYmJlZmYxYzQ4NTQ0NGY1YzQ5NTMyZThjMDA1MDQ3ZTIzZDI1NTEyNzY1ZjVlNzUiLCJ2b3V0Ijo0MSwic2NyaXB0U2lnIjp7ImFzbSI6IjE1OTk4MWYzMjRjMTBkZDQ1YzJhYjExZDkyYmQxN2Y0OTRkNDY5MTUwZGQ4N2RkNmExMjY0YmZhOWZkZDhmMDFjZmYwNTJiNTY2MzhkMmRkZTU1OWUwZDM4ZjczNjAzNmRkNzZmNjdlNGFjODk0OWUyMmE2YjkxODA5OGQzY2Q1W0FMTHxGT1JLSURdIDAzMmQ0YTBkZjQ2M2Q3ZWY2N2Y0ZDE2YzhiMWQwNjM5MTVhODk3ZjZlNGEyZGIyMjAxYjNmZjkzNDg3YWM2NTc0ZiIsImhleCI6IjQxMTU5OTgxZjMyNGMxMGRkNDVjMmFiMTFkOTJiZDE3ZjQ5NGQ0NjkxNTBkZDg3ZGQ2YTEyNjRiZmE5ZmRkOGYwMWNmZjA1MmI1NjYzOGQyZGRlNTU5ZTBkMzhmNzM2MDM2ZGQ3NmY2N2U0YWM4OTQ5ZTIyYTZiOTE4MDk4ZDNjZDU0MTIxMDMyZDRhMGRmNDYzZDdlZjY3ZjRkMTZjOGIxZDA2MzkxNWE4OTdmNmU0YTJkYjIyMDFiM2ZmOTM0ODdhYzY1NzRmIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIzZTMyZTdlYTkzZDYwYWM4NjAyMmNlZjBlN2JmYTY2OWZmYmJiOTRmNjU2MDhkOTMwMjBjZDZmYmY5NjQwMzg2Iiwidm91dCI6NjIsInNjcmlwdFNpZyI6eyJhc20iOiJhZmY2OWY2Y2JkYWJiMDUwMmY1YmU2NDk4MGY3MDY0ZTgyNzBhNGZiY2UxYmY4ZmNmMTNhOWE1NmZlZWY4MDRkYTgxNDY5MDVhOTQxMWU5NDMwZTZmYWMxODU3NWQ5ZTgxOThjOThkYzFkMGM2YjM0YzBhYjkzOTI1MjdjZDU3MVtBTEx8Rk9SS0lEXSAwMjExYWM3ZThkNTE2YmU2ZDBiZDMzMzRkM2RiOTU1NjBhYmRjOWEwZGViNzU2ZmNkMDI4ZjAwOWZlNmVjZGI3OWQiLCJoZXgiOiI0MWFmZjY5ZjZjYmRhYmIwNTAyZjViZTY0OTgwZjcwNjRlODI3MGE0ZmJjZTFiZjhmY2YxM2E5YTU2ZmVlZjgwNGRhODE0NjkwNWE5NDExZTk0MzBlNmZhYzE4NTc1ZDllODE5OGM5OGRjMWQwYzZiMzRjMGFiOTM5MjUyN2NkNTcxNDEyMTAyMTFhYzdlOGQ1MTZiZTZkMGJkMzMzNGQzZGI5NTU2MGFiZGM5YTBkZWI3NTZmY2QwMjhmMDA5ZmU2ZWNkYjc5ZCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOjMxLCJzY3JpcHRTaWciOnsiYXNtIjoiYzc5ODY2OGFjOWUwNDc2Y2ZkYmRjZDRlODVjYjlmYjY0MmJmOWU5ZGVlYTZiYjI2M2NlMjVhZWY4NjRjODJkYTgwZGFjOTc0ZjI4NWJlNWI1MDUyZjMwNjA2ZDAxZGFlOGExNTI5YjVmMmRjMDQ2ZDMxNWJlMzAxZDY4NTM5MTBbQUxMfEZPUktJRF0gMDMwY2ZhYjU0NDZhNDgyZTMzYjRkZDIyNWU0NGNlMGJmZTAxNDk0YWJkNzIxOGY5NWM1ODAzNDk2NzE0ZmM4YzRmIiwiaGV4IjoiNDFjNzk4NjY4YWM5ZTA0NzZjZmRiZGNkNGU4NWNiOWZiNjQyYmY5ZTlkZWVhNmJiMjYzY2UyNWFlZjg2NGM4MmRhODBkYWM5NzRmMjg1YmU1YjUwNTJmMzA2MDZkMDFkYWU4YTE1MjliNWYyZGMwNDZkMzE1YmUzMDFkNjg1MzkxMDQxMjEwMzBjZmFiNTQ0NmE0ODJlMzNiNGRkMjI1ZTQ0Y2UwYmZlMDE0OTRhYmQ3MjE4Zjk1YzU4MDM0OTY3MTRmYzhjNGYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjNmMDEyYTBhY2Y4YTZmOGQ4MjExYmI1MzFiN2I4NGZjYTdmMjk3NTk1OGI4NTQ1NGIyODE4ZDc3ZmY3Y2QwY2EiLCJ2b3V0IjozNiwic2NyaXB0U2lnIjp7ImFzbSI6IjRlZTgwYTE2OGIwZDlkMGJlMDMxMzQ5NzdmOGM0MWI1ZjNkM2E0MGU4ZjlkMjhiMWMwZWJiZmEwMzgyMzc0YzY2NmQ4MWM5NWQ0ZWQ3YmZkZGMzYzc3NDBjNjZkN2ZkMjhiYTQzMTRjNzJjMmMxMjFmYTFjNWMwNzRlYzg2MzkyW0FMTHxGT1JLSURdIDAzN2QzODMwMTEwOTM3MTIyN2RmNTQ5MjliZGVmMTRjZTZiM2RmM2MwNjM5YWRjNTE4Y2Y5NDExNWVkZWE2ZTRmNiIsImhleCI6IjQxNGVlODBhMTY4YjBkOWQwYmUwMzEzNDk3N2Y4YzQxYjVmM2QzYTQwZThmOWQyOGIxYzBlYmJmYTAzODIzNzRjNjY2ZDgxYzk1ZDRlZDdiZmRkYzNjNzc0MGM2NmQ3ZmQyOGJhNDMxNGM3MmMyYzEyMWZhMWM1YzA3NGVjODYzOTI0MTIxMDM3ZDM4MzAxMTA5MzcxMjI3ZGY1NDkyOWJkZWYxNGNlNmIzZGYzYzA2MzlhZGM1MThjZjk0MTE1ZWRlYTZlNGY2In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiIzZjAxMmEwYWNmOGE2ZjhkODIxMWJiNTMxYjdiODRmY2E3ZjI5NzU5NThiODU0NTRiMjgxOGQ3N2ZmN2NkMGNhIiwidm91dCI6MzcsInNjcmlwdFNpZyI6eyJhc20iOiJhNmQ2ZGQzZDNkYzI5YjE4Yjk4NDhiNGM5YjQzNzQzNDMxYzBjMWU3OThiNDY3ZTc1YzRjNDI2ZDQ4NzBkNzE3ODMwMTY5MWNmN2M1MzVhOTk0ZDM2ZTYxZGZjYjk0ZDY5Mzk0MDhiYzZlODc3ZTY5MGM0MjU4OTUwNTNmMTI1NltBTEx8Rk9SS0lEXSAwMmYyODQ4YTZkMzU5MmMwNGY2M2UzNDk2YmZmMmI5MTlkMGIyZDg4NjcyNjE2ZjdmODkyODkzZGE5MGZkOWM0MmEiLCJoZXgiOiI0MWE2ZDZkZDNkM2RjMjliMThiOTg0OGI0YzliNDM3NDM0MzFjMGMxZTc5OGI0NjdlNzVjNGM0MjZkNDg3MGQ3MTc4MzAxNjkxY2Y3YzUzNWE5OTRkMzZlNjFkZmNiOTRkNjkzOTQwOGJjNmU4NzdlNjkwYzQyNTg5NTA1M2YxMjU2NDEyMTAyZjI4NDhhNmQzNTkyYzA0ZjYzZTM0OTZiZmYyYjkxOWQwYjJkODg2NzI2MTZmN2Y4OTI4OTNkYTkwZmQ5YzQyYSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOjQ2LCJzY3JpcHRTaWciOnsiYXNtIjoiYzhhNWVhMDdlZTYwYzlmYzBlYThkNjk3MGFiMGVlOWYyYjM0NzJjYjJiNDVlYjIxYWFhZDcxMjU3NmE2ZThkYmM5OTBhOTBhMzAyNWYyYjJlM2FhN2NjNjE2NDQ4YWUyOWY3NmRlNjk1MzEzMmQ0M2JjNmI1OGUxNmI0YWYwNDNbQUxMfEZPUktJRF0gMDM5MDI4NzJhNDVhZjc2YjkwNWRiNTVjZWExMmZmNWM5NTkyOTNjYmFhYzA0MmJiMDcxY2Q3OTQ1YTI3YzQ2NDhjIiwiaGV4IjoiNDFjOGE1ZWEwN2VlNjBjOWZjMGVhOGQ2OTcwYWIwZWU5ZjJiMzQ3MmNiMmI0NWViMjFhYWFkNzEyNTc2YTZlOGRiYzk5MGE5MGEzMDI1ZjJiMmUzYWE3Y2M2MTY0NDhhZTI5Zjc2ZGU2OTUzMTMyZDQzYmM2YjU4ZTE2YjRhZjA0MzQxMjEwMzkwMjg3MmE0NWFmNzZiOTA1ZGI1NWNlYTEyZmY1Yzk1OTI5M2NiYWFjMDQyYmIwNzFjZDc5NDVhMjdjNDY0OGMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjQ1ZmI1MjA5MTgxYmEzNGViM2JhMzA1N2FiZjA0OTM5YzM5NTU5OTdmNjhiYzdlNDk2MmRlMzM5NzlkZDM4ODgiLCJ2b3V0IjoxNiwic2NyaXB0U2lnIjp7ImFzbSI6IjNlZGI5ZTg4OTk2ODZkMDU2NjA3ZjJjMWRmYzFlOWQ2ODAyZTYwNTY2NjU2MzIxMmEwMjY4NzZkYTM0MzYzM2U4NGY1MmYyMDJkNmRjNmIzNmUxMjY5YzkxZThlYzkyMjY5ZjhmNmE4NTFmZWE1MTUzN2Y5MmMwYWJlODI5NjY0W0FMTHxGT1JLSURdIDAyYTNlNDhiNzFhYTA4ZTdiN2Q4YTkxMTUwODMzOTJjMmEwODkzYjM0Y2FiMDg3OTlkNDhjM2U1ZWEzMDcyNTNkMCIsImhleCI6IjQxM2VkYjllODg5OTY4NmQwNTY2MDdmMmMxZGZjMWU5ZDY4MDJlNjA1NjY2NTYzMjEyYTAyNjg3NmRhMzQzNjMzZTg0ZjUyZjIwMmQ2ZGM2YjM2ZTEyNjljOTFlOGVjOTIyNjlmOGY2YTg1MWZlYTUxNTM3ZjkyYzBhYmU4Mjk2NjQ0MTIxMDJhM2U0OGI3MWFhMDhlN2I3ZDhhOTExNTA4MzM5MmMyYTA4OTNiMzRjYWIwODc5OWQ0OGMzZTVlYTMwNzI1M2QwIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI0NWZiNTIwOTE4MWJhMzRlYjNiYTMwNTdhYmYwNDkzOWMzOTU1OTk3ZjY4YmM3ZTQ5NjJkZTMzOTc5ZGQzODg4Iiwidm91dCI6MTcsInNjcmlwdFNpZyI6eyJhc20iOiI2NzkzNGE3NWVhYjRhY2NlZDdkMDQ5ODhhZTc3MmY4YTMxZGFiMmY3N2YzYzRmNWVhNjUxYWExMzExZjliMWM0ZDI1ODI4MjA5NTlhMWNiZWY4NDMxMjliMGNhNDk0NTIyNDdmMmIzN2NhODk5YzRhMzk4NWM5YmQxYWE2MTcyNVtBTEx8Rk9SS0lEXSAwMzc4MjVmNzM5NDVhYTgwMGFlNTU2MTRlMjBlNWFhNDIzNTc0MDk5OTI3MGY3MzI5NmI2MGJiODEzODRiNTkwMjgiLCJoZXgiOiI0MTY3OTM0YTc1ZWFiNGFjY2VkN2QwNDk4OGFlNzcyZjhhMzFkYWIyZjc3ZjNjNGY1ZWE2NTFhYTEzMTFmOWIxYzRkMjU4MjgyMDk1OWExY2JlZjg0MzEyOWIwY2E0OTQ1MjI0N2YyYjM3Y2E4OTljNGEzOTg1YzliZDFhYTYxNzI1NDEyMTAzNzgyNWY3Mzk0NWFhODAwYWU1NTYxNGUyMGU1YWE0MjM1NzQwOTk5MjcwZjczMjk2YjYwYmI4MTM4NGI1OTAyOCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNDVmYjUyMDkxODFiYTM0ZWIzYmEzMDU3YWJmMDQ5MzljMzk1NTk5N2Y2OGJjN2U0OTYyZGUzMzk3OWRkMzg4OCIsInZvdXQiOjQ0LCJzY3JpcHRTaWciOnsiYXNtIjoiOWJiNzFhMmUyNmYyMjQzNTUzODgyNDk3YjczN2RlY2JkNzU4M2NiNThmNDc2NjdhNWEwZjE1MWFiMmU5ZjU4MmQwYTM1NTYxNzAxNzdmMjNhOTgxYzcxMzk1MmNlNzJjNWQyYWUzNmM3OWI0NzQ3OTY1YTk4NjE4YjY4ZTJiNjBbQUxMfEZPUktJRF0gMDNiNjEzY2ZlN2M3YTk1OThmMjk2YTEyOGRkMDFkMDFjYjc4Y2I0N2FmZGQ3Zjg4MTNhMDZlODFkZWM4ODI0Mzg3IiwiaGV4IjoiNDE5YmI3MWEyZTI2ZjIyNDM1NTM4ODI0OTdiNzM3ZGVjYmQ3NTgzY2I1OGY0NzY2N2E1YTBmMTUxYWIyZTlmNTgyZDBhMzU1NjE3MDE3N2YyM2E5ODFjNzEzOTUyY2U3MmM1ZDJhZTM2Yzc5YjQ3NDc5NjVhOTg2MThiNjhlMmI2MDQxMjEwM2I2MTNjZmU3YzdhOTU5OGYyOTZhMTI4ZGQwMWQwMWNiNzhjYjQ3YWZkZDdmODgxM2EwNmU4MWRlYzg4MjQzODcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjQ1ZmI1MjA5MTgxYmEzNGViM2JhMzA1N2FiZjA0OTM5YzM5NTU5OTdmNjhiYzdlNDk2MmRlMzM5NzlkZDM4ODgiLCJ2b3V0Ijo0OCwic2NyaXB0U2lnIjp7ImFzbSI6ImIyYmRiYzhiMGRlZGYzOGQyN2VhOWJiOWE5NWI2ZmQ0NjExYWYxNDJjZjNmODM5ZmVjZmQ1OTFkN2VlOWY3Y2Y4NTEwMjNmYTUwNDczYWNlOWNkNTc3MTc2NmYxZmZlNGMwOWM5ZTE2NTY5MTk3YTg4ODE3MTY1ZTI5M2NiYzQ5W0FMTHxGT1JLSURdIDAzZTFlOGE0ZDQxOWE4N2FjYmRlMGJmYjAzYzY5YTljNTA2NGI2MjY2NzIxNTgyY2M5MWI0YmE2MjEzYjE2ODY2NyIsImhleCI6IjQxYjJiZGJjOGIwZGVkZjM4ZDI3ZWE5YmI5YTk1YjZmZDQ2MTFhZjE0MmNmM2Y4MzlmZWNmZDU5MWQ3ZWU5ZjdjZjg1MTAyM2ZhNTA0NzNhY2U5Y2Q1NzcxNzY2ZjFmZmU0YzA5YzllMTY1NjkxOTdhODg4MTcxNjVlMjkzY2JjNDk0MTIxMDNlMWU4YTRkNDE5YTg3YWNiZGUwYmZiMDNjNjlhOWM1MDY0YjYyNjY3MjE1ODJjYzkxYjRiYTYyMTNiMTY4NjY3In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI1ZWIxNTNlZWJjNWVmNWJiYmI5Y2E4ZjZjNWQ0MWE3YWI5M2IzZGExOGEwYmVhYzFjMTE1NWNjOGRjYTk2ZjAyIiwidm91dCI6MzAsInNjcmlwdFNpZyI6eyJhc20iOiJkYmM3MTIyMGZlNWY0YWQ4NTExMGQ3MTc2Njc1YjllMmRlZjNjNDgwZGMxZjcwMTEyOTk3N2M5ODgxYmYzZDdjZmNmM2EwOWEwNmQxMTEzYzJlYjQ0OWFlMTE1Y2RhZWEwZTkzZWRmYTYzNDRhZWI1NDQ0NTNjNzI2YTg0NWEwZltBTEx8Rk9SS0lEXSAwM2NmYTFmOWI1YjA5NjQ5YTA3NTE4ODhlNTI4YjQ3MjU5YTQwNmY5MGI0MGZiZjRjYmMyYzk1MmEwYWY4OGQ2ODMiLCJoZXgiOiI0MWRiYzcxMjIwZmU1ZjRhZDg1MTEwZDcxNzY2NzViOWUyZGVmM2M0ODBkYzFmNzAxMTI5OTc3Yzk4ODFiZjNkN2NmY2YzYTA5YTA2ZDExMTNjMmViNDQ5YWUxMTVjZGFlYTBlOTNlZGZhNjM0NGFlYjU0NDQ1M2M3MjZhODQ1YTBmNDEyMTAzY2ZhMWY5YjViMDk2NDlhMDc1MTg4OGU1MjhiNDcyNTlhNDA2ZjkwYjQwZmJmNGNiYzJjOTUyYTBhZjg4ZDY4MyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNWViMTUzZWViYzVlZjViYmJiOWNhOGY2YzVkNDFhN2FiOTNiM2RhMThhMGJlYWMxYzExNTVjYzhkY2E5NmYwMiIsInZvdXQiOjQ0LCJzY3JpcHRTaWciOnsiYXNtIjoiNjMzZDBlMDJlYWZlOTE3ZTNiOTAwNzNlNWI1MzQ5ZDNkNjg4ODY4MzEyNGMxYmM1OGY5ZDlmZjc2NmZmMTA5MWI2OTBkYjYzNzY5ZGFiMjU2NWU3NDcxZTE0ZDExYmQ2ODI0MGI1MjM4NmEwZmUzMjc3ZGJjYjRjOTY5MzZkNGNbQUxMfEZPUktJRF0gMDM2NzllODI3MWM2YjQ2NjkyNmQzN2Y2OWEzMTc3YjQ5YTY1ZjhkMjczMWM5OGQ0MTA3MWRkYTY5NDQ5MzE5NDM3IiwiaGV4IjoiNDE2MzNkMGUwMmVhZmU5MTdlM2I5MDA3M2U1YjUzNDlkM2Q2ODg4NjgzMTI0YzFiYzU4ZjlkOWZmNzY2ZmYxMDkxYjY5MGRiNjM3NjlkYWIyNTY1ZTc0NzFlMTRkMTFiZDY4MjQwYjUyMzg2YTBmZTMyNzdkYmNiNGM5NjkzNmQ0YzQxMjEwMzY3OWU4MjcxYzZiNDY2OTI2ZDM3ZjY5YTMxNzdiNDlhNjVmOGQyNzMxYzk4ZDQxMDcxZGRhNjk0NDkzMTk0MzcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjZiMDMwNTBiNmU3NmI4YjEwNTJjMjYzZDVhMzc2M2FlYTM5NmI2NjYxOTFiZTA2MThiYTZhZDk0MWEwM2Q3Y2MiLCJ2b3V0IjozLCJzY3JpcHRTaWciOnsiYXNtIjoiMDRlODhiNTZmZTUwMjdlMTEzNzBkY2JlMTViM2ZjZDVhNDZiYTFhMjExN2JmMTEzMGY2ZDVkNjU1MTc1OTk2OTRiYjEyMjI2YjlmNWE0MjJjMDg2N2I1NmJkOWM5YTExZDRjODc1NDNmMmQwZDg5MWUxMmJkOGQyNjc4MjQwZjJbQUxMfEZPUktJRF0gMDNjNDUxYzdhZmQzYTc2NWY3NjhmOGEyZjI3ZjFlYWZhYWVkYTc2MTZlODkwNmVjZjRjMTBkZTdjYjI3NzlhNWMzIiwiaGV4IjoiNDEwNGU4OGI1NmZlNTAyN2UxMTM3MGRjYmUxNWIzZmNkNWE0NmJhMWEyMTE3YmYxMTMwZjZkNWQ2NTUxNzU5OTY5NGJiMTIyMjZiOWY1YTQyMmMwODY3YjU2YmQ5YzlhMTFkNGM4NzU0M2YyZDBkODkxZTEyYmQ4ZDI2NzgyNDBmMjQxMjEwM2M0NTFjN2FmZDNhNzY1Zjc2OGY4YTJmMjdmMWVhZmFhZWRhNzYxNmU4OTA2ZWNmNGMxMGRlN2NiMjc3OWE1YzMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjZiMDMwNTBiNmU3NmI4YjEwNTJjMjYzZDVhMzc2M2FlYTM5NmI2NjYxOTFiZTA2MThiYTZhZDk0MWEwM2Q3Y2MiLCJ2b3V0IjoxNCwic2NyaXB0U2lnIjp7ImFzbSI6IjNjYTY3YTZlMzFmN2QwZmJhYWI3MWNiMzM4MzU5MjAzYWJjZDcyMWQ4M2U1OWUyMDhkOTY4YzFjM2Y4M2M2ZTk3MDQwYmJmZTg3M2QyOGU1ZGRjMmZlZDI5YjU0MmQ2YzZiM2ZmYjg5NWNiOTIzNjE0MWM2OWMzNzBkYWZkYjQ4W0FMTHxGT1JLSURdIDAyNGY5MmRhYTAzZTk3MDY4NWIxZGM5ZTJiYmUwYWU3M2ZhMWVlZDcyNzNjNGUyOWY5NTZlYmUwMjg3YmEwM2QzNiIsImhleCI6IjQxM2NhNjdhNmUzMWY3ZDBmYmFhYjcxY2IzMzgzNTkyMDNhYmNkNzIxZDgzZTU5ZTIwOGQ5NjhjMWMzZjgzYzZlOTcwNDBiYmZlODczZDI4ZTVkZGMyZmVkMjliNTQyZDZjNmIzZmZiODk1Y2I5MjM2MTQxYzY5YzM3MGRhZmRiNDg0MTIxMDI0ZjkyZGFhMDNlOTcwNjg1YjFkYzllMmJiZTBhZTczZmExZWVkNzI3M2M0ZTI5Zjk1NmViZTAyODdiYTAzZDM2In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI2YjAzMDUwYjZlNzZiOGIxMDUyYzI2M2Q1YTM3NjNhZWEzOTZiNjY2MTkxYmUwNjE4YmE2YWQ5NDFhMDNkN2NjIiwidm91dCI6MjksInNjcmlwdFNpZyI6eyJhc20iOiJlMDM5NzBiZWRkMGRiYTNkNmY1YzVlNTM1MjZhNDU5ZWU4NTZkMDMxZWQxMTdiMzUwMzY3MmJmNTUwMTljZTBlNmIwMjZiOWFmYTlhNmQ3ZjFmYzlhODYxMmNmZDdkMjE0MGM1Yzc1ZDk2ZjE0ZjZhNzRlYzRmYjE5NWE5NTQ4OFtBTEx8Rk9SS0lEXSAwM2YwYzA2YWIwZmQ5NTE1Yjk4Yzg3N2FlYTBlOTlhNTg2ZTVjZDM3NTU1MzkyZjgyODg3YWY4MGE5OGI0NTEwYjYiLCJoZXgiOiI0MWUwMzk3MGJlZGQwZGJhM2Q2ZjVjNWU1MzUyNmE0NTllZTg1NmQwMzFlZDExN2IzNTAzNjcyYmY1NTAxOWNlMGU2YjAyNmI5YWZhOWE2ZDdmMWZjOWE4NjEyY2ZkN2QyMTQwYzVjNzVkOTZmMTRmNmE3NGVjNGZiMTk1YTk1NDg4NDEyMTAzZjBjMDZhYjBmZDk1MTViOThjODc3YWVhMGU5OWE1ODZlNWNkMzc1NTUzOTJmODI4ODdhZjgwYTk4YjQ1MTBiNiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNmIwMzA1MGI2ZTc2YjhiMTA1MmMyNjNkNWEzNzYzYWVhMzk2YjY2NjE5MWJlMDYxOGJhNmFkOTQxYTAzZDdjYyIsInZvdXQiOjQwLCJzY3JpcHRTaWciOnsiYXNtIjoiMDllYjA2NGRmMTA4ZGFkOTE1ZGZjNmM2YjFjNjk5NThhNDI3NjAwOGNlZDEzYzZhNjlmNzRiYTY4ZTUzYTVkMjY2NzcxYjQxZTBjYzhhODEzOTcyYzY4MzlmYThhYzdhNTNmYjUzZTQ2ODJkYzc0OTAxMmNmMTY1NjA1YzI3ZjBbQUxMfEZPUktJRF0gMDM3MjM5NTdkMGEwNTkzNmNjNWE4ZTYyMGJjMzMyMTQxMmNhNGFkZTk3NzVjYmE3OWU1NTM3NTU2OWUwZDlkZGFlIiwiaGV4IjoiNDEwOWViMDY0ZGYxMDhkYWQ5MTVkZmM2YzZiMWM2OTk1OGE0Mjc2MDA4Y2VkMTNjNmE2OWY3NGJhNjhlNTNhNWQyNjY3NzFiNDFlMGNjOGE4MTM5NzJjNjgzOWZhOGFjN2E1M2ZiNTNlNDY4MmRjNzQ5MDEyY2YxNjU2MDVjMjdmMDQxMjEwMzcyMzk1N2QwYTA1OTM2Y2M1YThlNjIwYmMzMzIxNDEyY2E0YWRlOTc3NWNiYTc5ZTU1Mzc1NTY5ZTBkOWRkYWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjZjODI0ODQ3ZGY5MDViOTQ4YzlhNjVhZDdkMzE5MmVhY2Q0YTlhN2U5NjA1ZjlkN2Q2MTY4Y2ViNzZjYmRlYjYiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNzRjNTgwMzY5NWViZGU5YmYwOGI1ZjJjYWQ4Zjc3MzkxOWEyMGU2NGZiZWM5YTY1MDJlZjhjY2NjZTc0MGU5ZmU5NjRkMGVhOGUxYzMyNGVkNWFjZDdmNWE3YjZmMjk2NTRlYmQ5ZWQ0MzkwNjEyNjU5ZjUzNWVhZTJiOTg2MWFbQUxMfEZPUktJRF0gMDJjYWNkYTVmMWEyMmMzOWNjZDdkZGQxMmE2NmFjZDBkYjBmNGQ5Njk3M2FkMzIxYzhlYWNkMDMxNDBjNTM4MTBjIiwiaGV4IjoiNDE3NGM1ODAzNjk1ZWJkZTliZjA4YjVmMmNhZDhmNzczOTE5YTIwZTY0ZmJlYzlhNjUwMmVmOGNjY2NlNzQwZTlmZTk2NGQwZWE4ZTFjMzI0ZWQ1YWNkN2Y1YTdiNmYyOTY1NGViZDllZDQzOTA2MTI2NTlmNTM1ZWFlMmI5ODYxYTQxMjEwMmNhY2RhNWYxYTIyYzM5Y2NkN2RkZDEyYTY2YWNkMGRiMGY0ZDk2OTczYWQzMjFjOGVhY2QwMzE0MGM1MzgxMGMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjZmODRjNWRmNjE0Yzk3MTBmNDUzMzFhYTgzNDgxY2M0NTM2M2YwMTUyYmQwMzMxNjdhOGNlZTAzZjNiNmIyMmIiLCJ2b3V0IjoxNiwic2NyaXB0U2lnIjp7ImFzbSI6IjI2NzFjYjNmZGNjNzUzZTQ3NWExN2U0YzhiNjY5NTMyYTczYzg2NmRhYWRjYTRjOTUyZTAzNTVkMmIwOWMzYzlhNjFlZGJkODRiYjQ5NzRmMzk2YjMwZGRiZDc4ODcwYWU0YmNjY2Q2MzBhOWRkYThlNjRiZGQ2MTk5OThhZDk1W0FMTHxGT1JLSURdIDAyYmZkZTNiODJmYzRiZGRhMzM3YjkxODUyMmM3MGNhNWY2NjU0MWMwY2E4NzU1YWY4MzgwNmQ4YzU5M2ZjZDZlNiIsImhleCI6IjQxMjY3MWNiM2ZkY2M3NTNlNDc1YTE3ZTRjOGI2Njk1MzJhNzNjODY2ZGFhZGNhNGM5NTJlMDM1NWQyYjA5YzNjOWE2MWVkYmQ4NGJiNDk3NGYzOTZiMzBkZGJkNzg4NzBhZTRiY2NjZDYzMGE5ZGRhOGU2NGJkZDYxOTk5OGFkOTU0MTIxMDJiZmRlM2I4MmZjNGJkZGEzMzdiOTE4NTIyYzcwY2E1ZjY2NTQxYzBjYTg3NTVhZjgzODA2ZDhjNTkzZmNkNmU2In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI2Zjg0YzVkZjYxNGM5NzEwZjQ1MzMxYWE4MzQ4MWNjNDUzNjNmMDE1MmJkMDMzMTY3YThjZWUwM2YzYjZiMjJiIiwidm91dCI6MzIsInNjcmlwdFNpZyI6eyJhc20iOiIxODAwNzBmMmExZTBiZWI5OTA0Y2I4YjM1MmYyMDMxNjYwNmI0ZTY3Mjk3MDJjZmEyYjZlODEyZTYwNDYzODJjNzU5YmQxMmFhNjA4MjkxOGFkMDk0MmJlYzI0YTMxYjE0NGZiY2YyNjg3ZDM3MjMzNThiMjU0NTllZmNlM2YxMFtBTEx8Rk9SS0lEXSAwMzliNDE2YmNkM2UwYjQzNTkyNWE5ZGE0M2Y0MmE4NzVhYmJkZTRkYTc3ZjJjNWU4NTM5ODI3MDNmMTFlMGFlNDYiLCJoZXgiOiI0MTE4MDA3MGYyYTFlMGJlYjk5MDRjYjhiMzUyZjIwMzE2NjA2YjRlNjcyOTcwMmNmYTJiNmU4MTJlNjA0NjM4MmM3NTliZDEyYWE2MDgyOTE4YWQwOTQyYmVjMjRhMzFiMTQ0ZmJjZjI2ODdkMzcyMzM1OGIyNTQ1OWVmY2UzZjEwNDEyMTAzOWI0MTZiY2QzZTBiNDM1OTI1YTlkYTQzZjQyYTg3NWFiYmRlNGRhNzdmMmM1ZTg1Mzk4MjcwM2YxMWUwYWU0NiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNmY4NGM1ZGY2MTRjOTcxMGY0NTMzMWFhODM0ODFjYzQ1MzYzZjAxNTJiZDAzMzE2N2E4Y2VlMDNmM2I2YjIyYiIsInZvdXQiOjQyLCJzY3JpcHRTaWciOnsiYXNtIjoiMDJkNTgwMTRhYTc1YTdjNmI2MWU0Yjc5YTVhMDFlMjM1MjFhNTgwZTE2NzhjZjQxNTBhMDNjNmIxNDU0NDRlNGE0NWJjNTNiY2U2YzRiZjg2Zjk3YmZjMWFkM2JlMDMyOWJmOTE4YjcwNDlhNTZlYWJiZDVlNDJkNzk3Mjk3OTBbQUxMfEZPUktJRF0gMDI0ZjM3YmZiZWI2MTU1NGY3NmRiMjNiMTA1ZWE5YjJiN2ZlZDQyOGVmYmJiMDJhZTM2YTdjMjUzZmNjOGI0NTk4IiwiaGV4IjoiNDEwMmQ1ODAxNGFhNzVhN2M2YjYxZTRiNzlhNWEwMWUyMzUyMWE1ODBlMTY3OGNmNDE1MGEwM2M2YjE0NTQ0NGU0YTQ1YmM1M2JjZTZjNGJmODZmOTdiZmMxYWQzYmUwMzI5YmY5MThiNzA0OWE1NmVhYmJkNWU0MmQ3OTcyOTc5MDQxMjEwMjRmMzdiZmJlYjYxNTU0Zjc2ZGIyM2IxMDVlYTliMmI3ZmVkNDI4ZWZiYmIwMmFlMzZhN2MyNTNmY2M4YjQ1OTgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6Ijc4YzYyYzEyYjhkZWQ4OTQyODM4YWZiZjNkZGViODMzMWQxZTdlYzI0YzZkZDlkMzFjZTRmZGM1NDdjMDhhNzIiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiOThmZjZhNTQ2Y2RmMWIwYzI5MmMyMzllZjgzNTdmNDIzZDM3NWFiODk3OWJmNmM5ZjYwY2ZiZjJmZDI3ZmYwNmYyMTU5YWRkMjM2NmU4ZmU5ZDhmZDNkMWFiZGE4N2I1YzNkMzkwZmE1NmNhOTA3ZjQzMzZjMDNlMmMzY2IyZTJbQUxMfEZPUktJRF0gMDM2ZDcwZTAwMjA1NWQ0ODU3MTQ2OTY0ZDAwZmZlNzM3MjRiMmY3YzA0MTE4YTFmZTVmODk5YjA4ZTQzMjhjM2FiIiwiaGV4IjoiNDE5OGZmNmE1NDZjZGYxYjBjMjkyYzIzOWVmODM1N2Y0MjNkMzc1YWI4OTc5YmY2YzlmNjBjZmJmMmZkMjdmZjA2ZjIxNTlhZGQyMzY2ZThmZTlkOGZkM2QxYWJkYTg3YjVjM2QzOTBmYTU2Y2E5MDdmNDMzNmMwM2UyYzNjYjJlMjQxMjEwMzZkNzBlMDAyMDU1ZDQ4NTcxNDY5NjRkMDBmZmU3MzcyNGIyZjdjMDQxMThhMWZlNWY4OTliMDhlNDMyOGMzYWIifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjdhNTcyMDY1MGNlYTI3MWJlM2Q1YTNmNzhhZWMwNTgyMWUwN2Q5YTU5MjZhMTU3ODA1NDdkOTdhNjdmMmJkNTUiLCJ2b3V0IjoyMCwic2NyaXB0U2lnIjp7ImFzbSI6ImRlYTlhODMzNWQxMzIzNDRkMjhlZWM0YzY1MDE0ZjgyMzYwYzkzMmQyYzQxY2ZkMTRhYjU3YWFkZDMxYjRhMzAyZDZhN2ZhNDEwNjQ3NmNjY2U5MTJmNzNjOTZhYzI5NDJkNjA1NDFhNTk0OWE3NDk1ZWMyMjU3ZWUwYzljMjlmW0FMTHxGT1JLSURdIDAyYzQ2NTI3M2RiZWZkYmNlMjcwMDRjYmZjMTFhZjNjMzQ0YzQ1NTU2YmI4OWEzN2NlNzc4YWY0NDBiNmRiZTNlZiIsImhleCI6IjQxZGVhOWE4MzM1ZDEzMjM0NGQyOGVlYzRjNjUwMTRmODIzNjBjOTMyZDJjNDFjZmQxNGFiNTdhYWRkMzFiNGEzMDJkNmE3ZmE0MTA2NDc2Y2NjZTkxMmY3M2M5NmFjMjk0MmQ2MDU0MWE1OTQ5YTc0OTVlYzIyNTdlZTBjOWMyOWY0MTIxMDJjNDY1MjczZGJlZmRiY2UyNzAwNGNiZmMxMWFmM2MzNDRjNDU1NTZiYjg5YTM3Y2U3NzhhZjQ0MGI2ZGJlM2VmIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI5MDBiNjYzZGRkMDc2MmIzNGZkODE2OGQzYWNlY2UwZTg4ZTM4MjFiZTczYWM5ZGEwOGQ3YTQ0NGU4YjczMDlmIiwidm91dCI6MjEsInNjcmlwdFNpZyI6eyJhc20iOiI1NTFjMjMwOWJhNDI2NzFjODQ2MGFiN2IyMzZmMmZkZjhjMjQ1MDBiODc4NGJiM2RlYTBjNTZkYTdiYzQ3NjI3MmM2ZDc3NzdhODhhNWU1YTdkY2UwMDY1NDAzNzA5MTk3Mzg2YWIzZWZiNzViMmQ3ZjgwZmRmMmNhOGU4ZmJhN1tBTEx8Rk9SS0lEXSAwMjhmOTI1OTJlZDQzYjM2ZmFjYzY3NWE1ZWVlZTFhOTkzZDgxZWFlOWI4YTBhZjkyZDZkMWMzY2ZkMGUwNzZlODQiLCJoZXgiOiI0MTU1MWMyMzA5YmE0MjY3MWM4NDYwYWI3YjIzNmYyZmRmOGMyNDUwMGI4Nzg0YmIzZGVhMGM1NmRhN2JjNDc2MjcyYzZkNzc3N2E4OGE1ZTVhN2RjZTAwNjU0MDM3MDkxOTczODZhYjNlZmI3NWIyZDdmODBmZGYyY2E4ZThmYmE3NDEyMTAyOGY5MjU5MmVkNDNiMzZmYWNjNjc1YTVlZWVlMWE5OTNkODFlYWU5YjhhMGFmOTJkNmQxYzNjZmQwZTA3NmU4NCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiOWZmODFlNGFjZDIxNzU3ZGIxMzJiNzU5ZjRlOWJlZTBlYzhhOTk2YjgyZWY3YTY4NGFmMTE1MTI1ZjEyMmZjOCIsInZvdXQiOjgsInNjcmlwdFNpZyI6eyJhc20iOiJkZWEyODlhNjViMWE4OGIzYWY3ZDQzYmRkODNhMThhZTVhZTEyOTdlZjk4Zjc0MTlkYzk2MjQ0OWVlYmU4YjA0NzA1ZmQzM2ZiYzUzMDBjYmIxZjE3Mzg4ZTdhZTIwODZkMGJlNTcyOTdjZWU3YzI1NDA5OTk3NjUxZDA4NTJmZVtBTEx8Rk9SS0lEXSAwMzQ1OGFiZmRlYWEzN2UxMmM0MWFlOGMyNDc5YjAxOWZkNWVhMTY0YzYyZDM3ZmU2NzcxMGJjMjUyZGY2MjhkMTgiLCJoZXgiOiI0MWRlYTI4OWE2NWIxYTg4YjNhZjdkNDNiZGQ4M2ExOGFlNWFlMTI5N2VmOThmNzQxOWRjOTYyNDQ5ZWViZThiMDQ3MDVmZDMzZmJjNTMwMGNiYjFmMTczODhlN2FlMjA4NmQwYmU1NzI5N2NlZTdjMjU0MDk5OTc2NTFkMDg1MmZlNDEyMTAzNDU4YWJmZGVhYTM3ZTEyYzQxYWU4YzI0NzliMDE5ZmQ1ZWExNjRjNjJkMzdmZTY3NzEwYmMyNTJkZjYyOGQxOCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYWVlY2M5OTJlMTE2NGY2NGFlODEyMTlmNGU0NGY0NDM0NjA1NWI2MWZmOTdjYjkzMTZhMWRjMWExMWQ0MWQ2ZSIsInZvdXQiOjQsInNjcmlwdFNpZyI6eyJhc20iOiIzZjBhNTRkM2E0MWRlMGU4OTQ1YTZlZDY4MWZhMWM2OTliYzVlZGQ1MjEyZDIyNWFiNmRjYWNkYzdkZGQ4YmYxNTdmZWJiNjI0ZDk0NTY5ZjZiNjNmMDc2MTVjMDMxZTcyYjUzYTdjYjQzNDFhYzRhZjRmMGMzZGJlMWJlNjE5NltBTEx8Rk9SS0lEXSAwMzUxNGRlNjdmMGJjYzdkOGRhYzk2ZGE1OTJiMGFjMTQyOTUwNGE4NjhhOWJlMTEyNGM3YzA0NmRkNmQ4YmMxMzUiLCJoZXgiOiI0MTNmMGE1NGQzYTQxZGUwZTg5NDVhNmVkNjgxZmExYzY5OWJjNWVkZDUyMTJkMjI1YWI2ZGNhY2RjN2RkZDhiZjE1N2ZlYmI2MjRkOTQ1NjlmNmI2M2YwNzYxNWMwMzFlNzJiNTNhN2NiNDM0MWFjNGFmNGYwYzNkYmUxYmU2MTk2NDEyMTAzNTE0ZGU2N2YwYmNjN2Q4ZGFjOTZkYTU5MmIwYWMxNDI5NTA0YTg2OGE5YmUxMTI0YzdjMDQ2ZGQ2ZDhiYzEzNSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYWYxN2ZlZDJhMmEyMGYyNjhjY2RmY2U4MzYwNGNjMzQzYTg4M2I0ODVmMDM3M2E2MGY1ZGRjMjUzMmRjOWQ3MCIsInZvdXQiOjYsInNjcmlwdFNpZyI6eyJhc20iOiJiOWMyMTg4MTZiYzM3ODMyNTUxZjI4MDU2MGZhYjBlYWM3ZWJiOGY2NzZjZjA1NzA1YjI0NWYyMzlhYTQyZTQyMjgyYWE0OTIzNTUzMWYwOTQ3M2ZmNThhNWZhZTUxMzMyNDgwYjIwNDk2N2Q4M2JmYmJmYTRjY2RmOWU4MmRlOFtBTEx8Rk9SS0lEXSAwMjFhNDE4YzQwZGVhYWY1MjU5NWJjZDI1N2I5MGE0ZTFmMGQxZThkYzhjZDgyNjRhNTEwNzUwMDk2MmI0ZDBlMTQiLCJoZXgiOiI0MWI5YzIxODgxNmJjMzc4MzI1NTFmMjgwNTYwZmFiMGVhYzdlYmI4ZjY3NmNmMDU3MDViMjQ1ZjIzOWFhNDJlNDIyODJhYTQ5MjM1NTMxZjA5NDczZmY1OGE1ZmFlNTEzMzI0ODBiMjA0OTY3ZDgzYmZiYmZhNGNjZGY5ZTgyZGU4NDEyMTAyMWE0MThjNDBkZWFhZjUyNTk1YmNkMjU3YjkwYTRlMWYwZDFlOGRjOGNkODI2NGE1MTA3NTAwOTYyYjRkMGUxNCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYWYxN2ZlZDJhMmEyMGYyNjhjY2RmY2U4MzYwNGNjMzQzYTg4M2I0ODVmMDM3M2E2MGY1ZGRjMjUzMmRjOWQ3MCIsInZvdXQiOjM1LCJzY3JpcHRTaWciOnsiYXNtIjoiZGI3N2MwZTY4YmNhYjQxZGFhMDY2ZDQxYWRkNmQwMDQzZjQxZWMxMjkzMzBkOTE5YTAyYzY5ZTBlOTZiODQ5ZjUzNzY5ZDQxYzFjMzMxZGJkZGY1N2U4NDQ4NGYzYjBlMmZkMjdiZDM3OWU1ZjQyZDE0MTVkYWYzNjY1Y2I0MDZbQUxMfEZPUktJRF0gMDMxMDkyOTU1M2QwZGQxMjMwYTYyYmM3MDQ1MzBjODViMGVmMmYzZWI1ZmJmNTY3YzI0ZDU0MGUzMWExZjgwMGVlIiwiaGV4IjoiNDFkYjc3YzBlNjhiY2FiNDFkYWEwNjZkNDFhZGQ2ZDAwNDNmNDFlYzEyOTMzMGQ5MTlhMDJjNjllMGU5NmI4NDlmNTM3NjlkNDFjMWMzMzFkYmRkZjU3ZTg0NDg0ZjNiMGUyZmQyN2JkMzc5ZTVmNDJkMTQxNWRhZjM2NjVjYjQwNjQxMjEwMzEwOTI5NTUzZDBkZDEyMzBhNjJiYzcwNDUzMGM4NWIwZWYyZjNlYjVmYmY1NjdjMjRkNTQwZTMxYTFmODAwZWUifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImFmMTdmZWQyYTJhMjBmMjY4Y2NkZmNlODM2MDRjYzM0M2E4ODNiNDg1ZjAzNzNhNjBmNWRkYzI1MzJkYzlkNzAiLCJ2b3V0Ijo0Niwic2NyaXB0U2lnIjp7ImFzbSI6IjgzZjAwNDMxMjJmNmQwZDI2ZDI1MDM1ZWIwZTcyNDllYzRiYjRkMWE5NzllYTNlYTc2NjY4OGZiZDcxYThlNDE4ZmI2NWZmNWFmZGNiMGUxOTQ3MTdiMjExMzhlMDM2Y2JkMGU1OWEzNDdjMzRjZTgxNjA0NDI4MjFkNWJlOThjW0FMTHxGT1JLSURdIDAyNzRlYmJkYzhkOWU3YzQzYzU2MDEzZTA3MGY5MjQ3MzBkMjcwY2NhMzY5YjkyYTYyNGUzOTk0NTdhNWIyNWQwMSIsImhleCI6IjQxODNmMDA0MzEyMmY2ZDBkMjZkMjUwMzVlYjBlNzI0OWVjNGJiNGQxYTk3OWVhM2VhNzY2Njg4ZmJkNzFhOGU0MThmYjY1ZmY1YWZkY2IwZTE5NDcxN2IyMTEzOGUwMzZjYmQwZTU5YTM0N2MzNGNlODE2MDQ0MjgyMWQ1YmU5OGM0MTIxMDI3NGViYmRjOGQ5ZTdjNDNjNTYwMTNlMDcwZjkyNDczMGQyNzBjY2EzNjliOTJhNjI0ZTM5OTQ1N2E1YjI1ZDAxIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJhZjE3ZmVkMmEyYTIwZjI2OGNjZGZjZTgzNjA0Y2MzNDNhODgzYjQ4NWYwMzczYTYwZjVkZGMyNTMyZGM5ZDcwIiwidm91dCI6NDksInNjcmlwdFNpZyI6eyJhc20iOiI1MzY1OWFhZmNjY2Q3MzMwMWNiYWVmM2I0MDQ3NzM0ZjliNzRjZGRmYmYzOGMxYjI1OWJjZTg4ZWQzMTFiOThkOGM3MWQ2OWFhODhhNDAyMTU4YzIzNmIxOWQ4NWFkYmQwZWRjNGIwZDQ5MmFlNTkwZWNlNmQ3NjFlMjQyMjc2Y1tBTEx8Rk9SS0lEXSAwM2NjM2U2NWU4ZjY2MjE2ZDdmMTYyNzI5NzQwOTdlYjkxYjg1N2RjYzdkMGNlMDU2MzIyYjYwM2IxZDc0NTMyZDEiLCJoZXgiOiI0MTUzNjU5YWFmY2NjZDczMzAxY2JhZWYzYjQwNDc3MzRmOWI3NGNkZGZiZjM4YzFiMjU5YmNlODhlZDMxMWI5OGQ4YzcxZDY5YWE4OGE0MDIxNThjMjM2YjE5ZDg1YWRiZDBlZGM0YjBkNDkyYWU1OTBlY2U2ZDc2MWUyNDIyNzZjNDEyMTAzY2MzZTY1ZThmNjYyMTZkN2YxNjI3Mjk3NDA5N2ViOTFiODU3ZGNjN2QwY2UwNTYzMjJiNjAzYjFkNzQ1MzJkMSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYjRjMDUwOWJjNzRmMzk1ZjZkZDVhZDE3OTk5MWZjMGNjODViMDk5Mjk3NTMyMmY5YzlhMjBkYTA4NDk3NTZkNSIsInZvdXQiOjM2LCJzY3JpcHRTaWciOnsiYXNtIjoiODExZjVlOGU2ODJlNTM0MGMxNWYzMzliZTllNDkyNzQ4MTkzOTBjZTEzNmZjNzdjMTIyZjI2Njg0MmVhYjU0YTE3ZDFmMzBmYjAzN2M1YTNlNTljMzk5ZGI2MGQ0YTUzMjlmZjM3MGIxNWY5NGFmMDM3NDFiZTUxNDkxNzAzNjlbQUxMfEZPUktJRF0gMDI0YWU3MWZlMWMxOTc4OGU1NDM1ZGViZDU2YWNkMTRmYWMyYjE0MGE2MzVmM2MyNzA1Mzk4ODVmNWRhZmFjZjUwIiwiaGV4IjoiNDE4MTFmNWU4ZTY4MmU1MzQwYzE1ZjMzOWJlOWU0OTI3NDgxOTM5MGNlMTM2ZmM3N2MxMjJmMjY2ODQyZWFiNTRhMTdkMWYzMGZiMDM3YzVhM2U1OWMzOTlkYjYwZDRhNTMyOWZmMzcwYjE1Zjk0YWYwMzc0MWJlNTE0OTE3MDM2OTQxMjEwMjRhZTcxZmUxYzE5Nzg4ZTU0MzVkZWJkNTZhY2QxNGZhYzJiMTQwYTYzNWYzYzI3MDUzOTg4NWY1ZGFmYWNmNTAifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImJjOGFmNDM4MTgzNGM3NzRhZTZmOGM3ODM4YTIxNjY0NTM5YzA1MDM4YmE3ZGViYzNkMGFjZjFkZWJjY2UyNDAiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNTBmZmY2Y2MwOTQzNDA5OWVlZmU4YWQ1YTZlYTFjNzY3OGQzYTk5Yjk5YjQ3OWM2MTMwMjg5ZjBmMGI0NmZjOTdmYzg5ODZhY2ZiMjEwODFjMDBmMWNmNzFkYjlkZDkzYWQxYWFiNmQ0NTdlMTZlNWZjNDlkMzE0ODQ0YWQ2MWVbQUxMfEZPUktJRF0gMDM4NmRhMjk4YTc0ZDU4OTVhMmY2MmU4YTBmZjFkYWU2MmY0YzllNDlmYjliZTJiMzYxMDhkNTU3ZjY2NGE1MzRkIiwiaGV4IjoiNDE1MGZmZjZjYzA5NDM0MDk5ZWVmZThhZDVhNmVhMWM3Njc4ZDNhOTliOTliNDc5YzYxMzAyODlmMGYwYjQ2ZmM5N2ZjODk4NmFjZmIyMTA4MWMwMGYxY2Y3MWRiOWRkOTNhZDFhYWI2ZDQ1N2UxNmU1ZmM0OWQzMTQ4NDRhZDYxZTQxMjEwMzg2ZGEyOThhNzRkNTg5NWEyZjYyZThhMGZmMWRhZTYyZjRjOWU0OWZiOWJlMmIzNjEwOGQ1NTdmNjY0YTUzNGQifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImJjOGFmNDM4MTgzNGM3NzRhZTZmOGM3ODM4YTIxNjY0NTM5YzA1MDM4YmE3ZGViYzNkMGFjZjFkZWJjY2UyNDAiLCJ2b3V0Ijo0LCJzY3JpcHRTaWciOnsiYXNtIjoiZDYwY2E5NzVhYmY2NDQ4N2Q2YjE5MDgwMzRmYzg1NTgwZWUzM2U1YzU3ZmMwNzJhZDZjMTQzYjhlMWI1NWZmNWZmNWVhY2M3OGE1M2U2NWFkZjZmMjYxMGIxNzJlNGM1MzIyYmJjOTA3MWVjOWU3MDkzMTdkMjNjZDZjOWYyMDRbQUxMfEZPUktJRF0gMDNhN2MxMWU1ODAyMGQ2N2VkM2I1ZWMzYmU2MzdkYzI2N2IzOTI4MTUxN2UxZGQwYzQ4ZTk4M2Q5ODgxNTc2MjkxIiwiaGV4IjoiNDFkNjBjYTk3NWFiZjY0NDg3ZDZiMTkwODAzNGZjODU1ODBlZTMzZTVjNTdmYzA3MmFkNmMxNDNiOGUxYjU1ZmY1ZmY1ZWFjYzc4YTUzZTY1YWRmNmYyNjEwYjE3MmU0YzUzMjJiYmM5MDcxZWM5ZTcwOTMxN2QyM2NkNmM5ZjIwNDQxMjEwM2E3YzExZTU4MDIwZDY3ZWQzYjVlYzNiZTYzN2RjMjY3YjM5MjgxNTE3ZTFkZDBjNDhlOTgzZDk4ODE1NzYyOTEifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImJjOGFmNDM4MTgzNGM3NzRhZTZmOGM3ODM4YTIxNjY0NTM5YzA1MDM4YmE3ZGViYzNkMGFjZjFkZWJjY2UyNDAiLCJ2b3V0IjoxOSwic2NyaXB0U2lnIjp7ImFzbSI6IjdlOTAyYmFhZTUzN2VhMzFmYjI0MTE2NGZkOTM1YWI5NGI1OWQ3MWY1MjBjMzI1ZDM2ZDNhZWE4NTY1ZTdiYzk1Mzc4NTVhMGVjODYyMWE4NTVlZmZlYmI5NGRmZDVmMDVhNTY0OTZlNGQ5ZmUxNTg2ZDViMjdiZDY3YjhhYmU2W0FMTHxGT1JLSURdIDAyZDQyZmE1MmQ1NGU1ZGQzNGM1YTM5MDg5YTQ5YmNiNmEyMTU0NTZlZTlhNjkxOTNkZTMzZmFmOTIwYWQzZjBhZiIsImhleCI6IjQxN2U5MDJiYWFlNTM3ZWEzMWZiMjQxMTY0ZmQ5MzVhYjk0YjU5ZDcxZjUyMGMzMjVkMzZkM2FlYTg1NjVlN2JjOTUzNzg1NWEwZWM4NjIxYTg1NWVmZmViYjk0ZGZkNWYwNWE1NjQ5NmU0ZDlmZTE1ODZkNWIyN2JkNjdiOGFiZTY0MTIxMDJkNDJmYTUyZDU0ZTVkZDM0YzVhMzkwODlhNDliY2I2YTIxNTQ1NmVlOWE2OTE5M2RlMzNmYWY5MjBhZDNmMGFmIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJiYzhhZjQzODE4MzRjNzc0YWU2ZjhjNzgzOGEyMTY2NDUzOWMwNTAzOGJhN2RlYmMzZDBhY2YxZGViY2NlMjQwIiwidm91dCI6MjMsInNjcmlwdFNpZyI6eyJhc20iOiIzODhjMDc4ZTRiNGJiN2M1ZTYwNWJiZWI0YTQyZDFhODQ0ZDk1ODE5NmJhYTBmMTUyZjZmZmQ5ZWMzNjYwYmVhYzc3OGZlZWI0NGE0ZDVjYTFkOTBlMTQ4NTFjOThhMWU1ZjQxYjM4YzQzYzgxZWMzZjgxNTM3Y2Q0MmM3ODc5M1tBTEx8Rk9SS0lEXSAwMjhmZjdkZDU3NGU0NTMwMWJhYWI0Njg3NjIzYmU1Mjg2ZDU4OTFmNjIwOTUxZWQ0MTIzY2U0NTliYzZkMmI3YzgiLCJoZXgiOiI0MTM4OGMwNzhlNGI0YmI3YzVlNjA1YmJlYjRhNDJkMWE4NDRkOTU4MTk2YmFhMGYxNTJmNmZmZDllYzM2NjBiZWFjNzc4ZmVlYjQ0YTRkNWNhMWQ5MGUxNDg1MWM5OGExZTVmNDFiMzhjNDNjODFlYzNmODE1MzdjZDQyYzc4NzkzNDEyMTAyOGZmN2RkNTc0ZTQ1MzAxYmFhYjQ2ODc2MjNiZTUyODZkNTg5MWY2MjA5NTFlZDQxMjNjZTQ1OWJjNmQyYjdjOCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOjI2LCJzY3JpcHRTaWciOnsiYXNtIjoiMWM3ZjkxYTU0YzAwMDBhMjFhMTc2MjIzYWE4NDM2MWMyNDZkMjM0NDZjMjcyNWU0ZmI2MDJhYTdiNGJjNDU1YTczZjQxMjNiZjBmY2MxZDY0YjgxZWViMzIzZDFlMjU5YWQ5M2I5ZTc2NDAxMTgwMTBiMzc0NWMwMmI5OTBiNmJbQUxMfEZPUktJRF0gMDMyYTBmNTkzYmYyYzM5YzhiZTE4NWQ5N2RlMzM0ZDQ3ZDcwYmU2YmFhNWQ0ODQ0YmRlYjY3ZTI3YjNlYjUwMTdhIiwiaGV4IjoiNDExYzdmOTFhNTRjMDAwMGEyMWExNzYyMjNhYTg0MzYxYzI0NmQyMzQ0NmMyNzI1ZTRmYjYwMmFhN2I0YmM0NTVhNzNmNDEyM2JmMGZjYzFkNjRiODFlZWIzMjNkMWUyNTlhZDkzYjllNzY0MDExODAxMGIzNzQ1YzAyYjk5MGI2YjQxMjEwMzJhMGY1OTNiZjJjMzljOGJlMTg1ZDk3ZGUzMzRkNDdkNzBiZTZiYWE1ZDQ4NDRiZGViNjdlMjdiM2ViNTAxN2EifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImJjOGFmNDM4MTgzNGM3NzRhZTZmOGM3ODM4YTIxNjY0NTM5YzA1MDM4YmE3ZGViYzNkMGFjZjFkZWJjY2UyNDAiLCJ2b3V0IjozNSwic2NyaXB0U2lnIjp7ImFzbSI6ImNlNzYyMjAwN2Y4OTBjMzEwMGQ3YzBjZTkyZGFmNmViNDQ5ZmVkYjg4ZTQ0YzBjMGYxYmVhNmE5MDk3ZmJmZWY1MjgyMjVhNTM2MmM4MDUwMjhmMTQwMTdiNjJhMDMzMDAyZTFmYTE5ZTUxYzhiNDFjNWRlYmVmNmE3YTM1ZmMxW0FMTHxGT1JLSURdIDAzMTlkOTJlZmM1OTg5YjE3NGIwOTgyYWNjYjRmNWU1MDM0YjRjZmQzYWM1YmNjODU2OWNhMDdiZGFiMzg1YTc2YiIsImhleCI6IjQxY2U3NjIyMDA3Zjg5MGMzMTAwZDdjMGNlOTJkYWY2ZWI0NDlmZWRiODhlNDRjMGMwZjFiZWE2YTkwOTdmYmZlZjUyODIyNWE1MzYyYzgwNTAyOGYxNDAxN2I2MmEwMzMwMDJlMWZhMTllNTFjOGI0MWM1ZGViZWY2YTdhMzVmYzE0MTIxMDMxOWQ5MmVmYzU5ODliMTc0YjA5ODJhY2NiNGY1ZTUwMzRiNGNmZDNhYzViY2M4NTY5Y2EwN2JkYWIzODVhNzZiIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJiYzhhZjQzODE4MzRjNzc0YWU2ZjhjNzgzOGEyMTY2NDUzOWMwNTAzOGJhN2RlYmMzZDBhY2YxZGViY2NlMjQwIiwidm91dCI6MzcsInNjcmlwdFNpZyI6eyJhc20iOiJhZjY4MjNiNzIyN2JiMzhhMTIyMDBiZmVhOWQ1OGYxMTE3MzIzNTc1NTU3YWNiYmM0ZmE3NjExMDg5NzdmOTYwNTZhM2ZmY2JlYWM4NzRmNGJhMTY1NDFkMjUyZjQyYWE4NzVhMTM1ZmZlYTkyZmVhNTM1YTFlNWQ3ZGJlMjMyNltBTEx8Rk9SS0lEXSAwMzk3Y2RiMmMyNGZmM2VmYTU0NmZlY2E2N2M2NmFiZTY1MzY3OTUxZDQ0OTJmOWQ1MzA4Y2IxZTgzNjEzYWJiYWIiLCJoZXgiOiI0MWFmNjgyM2I3MjI3YmIzOGExMjIwMGJmZWE5ZDU4ZjExMTczMjM1NzU1NTdhY2JiYzRmYTc2MTEwODk3N2Y5NjA1NmEzZmZjYmVhYzg3NGY0YmExNjU0MWQyNTJmNDJhYTg3NWExMzVmZmVhOTJmZWE1MzVhMWU1ZDdkYmUyMzI2NDEyMTAzOTdjZGIyYzI0ZmYzZWZhNTQ2ZmVjYTY3YzY2YWJlNjUzNjc5NTFkNDQ5MmY5ZDUzMDhjYjFlODM2MTNhYmJhYiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOjQ4LCJzY3JpcHRTaWciOnsiYXNtIjoiOWZiZTc3YjM2MzI5MzFjMTNmNjA5MmYzOWQ4MTZjZTc4Y2NlY2YwZmY5NzJhZTI5MDk0M2JhZDZjODc5NGFjNTA4NTI3NTg0YmY3NjA1OWRjYTdjN2UzZGZlNDk1NmM2ZGZiNzJjNGJkYjdmZWI4YTg5NWY5NzA2NTkzMDE5OGZbQUxMfEZPUktJRF0gMDJlYTA5NjBjYWUwNDkzNWZkNDI5MThjMGQ3YjMwNGUxODg1YzVlZTRiNjFlZDcyYWE4N2ZiODcxYTc2ZjhhM2JjIiwiaGV4IjoiNDE5ZmJlNzdiMzYzMjkzMWMxM2Y2MDkyZjM5ZDgxNmNlNzhjY2VjZjBmZjk3MmFlMjkwOTQzYmFkNmM4Nzk0YWM1MDg1Mjc1ODRiZjc2MDU5ZGNhN2M3ZTNkZmU0OTU2YzZkZmI3MmM0YmRiN2ZlYjhhODk1Zjk3MDY1OTMwMTk4ZjQxMjEwMmVhMDk2MGNhZTA0OTM1ZmQ0MjkxOGMwZDdiMzA0ZTE4ODVjNWVlNGI2MWVkNzJhYTg3ZmI4NzFhNzZmOGEzYmMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImJjOGFmNDM4MTgzNGM3NzRhZTZmOGM3ODM4YTIxNjY0NTM5YzA1MDM4YmE3ZGViYzNkMGFjZjFkZWJjY2UyNDAiLCJ2b3V0Ijo2OSwic2NyaXB0U2lnIjp7ImFzbSI6Ijk0ODdlYWJlNzZmZTI5NDM0OGZmZDEyM2RhNmQzNTcyMGEwNTU2MzhmY2Y5M2EzNGE4Njc1YjY4ZTFkNmU2M2Q5Y2M1NmNhMGNlYjhhZWNhMjE5ZmEwM2ZlZDVmNzI0NDk0Yjc5MDE5YmNiMWU0M2FjOWU2Yzg0YWYwOTc3MTNhW0FMTHxGT1JLSURdIDAzZDYyYTYwYTI1Mjk5NTQxYTY4NDkzZjM1NmQ0MzVlYjY0YzQ2OTdmOWRhOGIxYzkxMTk3NWFjNTQxODJiZjkyMiIsImhleCI6IjQxOTQ4N2VhYmU3NmZlMjk0MzQ4ZmZkMTIzZGE2ZDM1NzIwYTA1NTYzOGZjZjkzYTM0YTg2NzViNjhlMWQ2ZTYzZDljYzU2Y2EwY2ViOGFlY2EyMTlmYTAzZmVkNWY3MjQ0OTRiNzkwMTliY2IxZTQzYWM5ZTZjODRhZjA5NzcxM2E0MTIxMDNkNjJhNjBhMjUyOTk1NDFhNjg0OTNmMzU2ZDQzNWViNjRjNDY5N2Y5ZGE4YjFjOTExOTc1YWM1NDE4MmJmOTIyIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJjNjM1NDllM2ZhNjY1NjAxYTI5OGNmNDZiY2UyNTM4OGY1ZGMxZGNiMGI5MWMyMmU4ZmQ2NWE0ZmRhNDkwZjIyIiwidm91dCI6NjcsInNjcmlwdFNpZyI6eyJhc20iOiJiOWEyZDYyMmY1MjBiZjg5YjMxN2JjZGM4ZDQ5ZTVjOTcxNzQ2Mzg1ZjUwNzNjMGY4NWJjNTVjZGY5NTNhNmQxZTk0NWMzZjg3YmM2NDg4YjAwZTE3MTg2YjI3Mzg2MzA5NjAyZTY5NWUxM2IyNzY5ZTQxMjNlNDU1MjBkZjI2M1tBTEx8Rk9SS0lEXSAwMmVjOWMxYmYzN2Y4YjQ3NjJjOTQzNDliNjY0NjQzNWEyNzdmOGJiNDg3NTgyNjE1NjNlYzA2ZDkwYzUwMjVlZDEiLCJoZXgiOiI0MWI5YTJkNjIyZjUyMGJmODliMzE3YmNkYzhkNDllNWM5NzE3NDYzODVmNTA3M2MwZjg1YmM1NWNkZjk1M2E2ZDFlOTQ1YzNmODdiYzY0ODhiMDBlMTcxODZiMjczODYzMDk2MDJlNjk1ZTEzYjI3NjllNDEyM2U0NTUyMGRmMjYzNDEyMTAyZWM5YzFiZjM3ZjhiNDc2MmM5NDM0OWI2NjQ2NDM1YTI3N2Y4YmI0ODc1ODI2MTU2M2VjMDZkOTBjNTAyNWVkMSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiY2NlOWU2NDM3MmU0OGY4OWY0YTI3ZGYwYTI0OWQxNDViZWY4MzFjYjEzNzRjYjBhNTQ5NTMyOWNmODk0NDdkZCIsInZvdXQiOjM4LCJzY3JpcHRTaWciOnsiYXNtIjoiYmNkMmFlYmI4YmZkMzA2YzQyNWEwMWVmMzUwNDA5NTg0MTIzOTgxOTAwNzQwZmIyNGM1MDdlNzA5NzU3YmFmN2MwNDMxNGRjZTQ2ZDU2OTI4NTRkMjI5NDcxNDFlYmUwMGViMzlmMGVhYjgzODVhMDE4YzFlOTM4NWU4MzMyNzlbQUxMfEZPUktJRF0gMDM2NTA0NmQwNWQxOGNiODZjMTQyM2IwMjFkOWE4N2ZkYzc2MTIwNmNkYjViNDcxZjY3NTU1MjE4OGM5OTM5MGI4IiwiaGV4IjoiNDFiY2QyYWViYjhiZmQzMDZjNDI1YTAxZWYzNTA0MDk1ODQxMjM5ODE5MDA3NDBmYjI0YzUwN2U3MDk3NTdiYWY3YzA0MzE0ZGNlNDZkNTY5Mjg1NGQyMjk0NzE0MWViZTAwZWIzOWYwZWFiODM4NWEwMThjMWU5Mzg1ZTgzMzI3OTQxMjEwMzY1MDQ2ZDA1ZDE4Y2I4NmMxNDIzYjAyMWQ5YTg3ZmRjNzYxMjA2Y2RiNWI0NzFmNjc1NTUyMTg4Yzk5MzkwYjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImQwNWQ4MGM3MmNjZTFkOTczNWVhY2I3YTNlMDgxNDEwOTNhMzBiNWVlMDA5MTEwMzNmNjRjYWM1OGUzNDE3MTYiLCJ2b3V0Ijo5LCJzY3JpcHRTaWciOnsiYXNtIjoiYWEzNTY3ZTQ3OTk4ZTNhZmYyOTBhY2UyZGE4MGRmMjA1YmY5NGZhMzE3MGEzODEzODJkNzQyOTkxOTllOWQ4Y2I5NmMzMWZmMzgxODc5M2JjNzE2NDc3YzdiZDMyMjI2OGVhMDk2NGY4MzQ3OWFkODI3ZjgzMWJhZGNjM2U1NTVbQUxMfEZPUktJRF0gMDNiNTUyMGE4YjMwOWM4YjA4MzhiNTI2YTc0NjJlOTM5Njg4NTdhYjQwYjVlMzdlMTk4NWM1MjkwOWUyM2YwODE3IiwiaGV4IjoiNDFhYTM1NjdlNDc5OThlM2FmZjI5MGFjZTJkYTgwZGYyMDViZjk0ZmEzMTcwYTM4MTM4MmQ3NDI5OTE5OWU5ZDhjYjk2YzMxZmYzODE4NzkzYmM3MTY0NzdjN2JkMzIyMjY4ZWEwOTY0ZjgzNDc5YWQ4MjdmODMxYmFkY2MzZTU1NTQxMjEwM2I1NTIwYThiMzA5YzhiMDgzOGI1MjZhNzQ2MmU5Mzk2ODg1N2FiNDBiNWUzN2UxOTg1YzUyOTA5ZTIzZjA4MTcifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImQwNWQ4MGM3MmNjZTFkOTczNWVhY2I3YTNlMDgxNDEwOTNhMzBiNWVlMDA5MTEwMzNmNjRjYWM1OGUzNDE3MTYiLCJ2b3V0IjoxMCwic2NyaXB0U2lnIjp7ImFzbSI6IjZhNDgwMzZkZmIyMDViM2U0NjI5OTI5YTI1OWMyMDI3OGI3NDdkYmI0MDUxYjRiMzk5NzE3ZmI1NDA0MmY0YzI3OGU2ZWQ2ODliZjQ1YmIyMzNiYzU4YWFiZThlYzI5YTc1Y2I1NDQxMzVkZGQ1ZDg3NzIwMTVlMzZhNTRiMmJjW0FMTHxGT1JLSURdIDAyZThhNTllNTA1YzM1NzBmOWE0MWM3YWE2NTEyNWZmOWI3ZGU5ZGJlZmZkOGY4NDc1MzgxMjFiNWZkMWU2ZGI4NiIsImhleCI6IjQxNmE0ODAzNmRmYjIwNWIzZTQ2Mjk5MjlhMjU5YzIwMjc4Yjc0N2RiYjQwNTFiNGIzOTk3MTdmYjU0MDQyZjRjMjc4ZTZlZDY4OWJmNDViYjIzM2JjNThhYWJlOGVjMjlhNzVjYjU0NDEzNWRkZDVkODc3MjAxNWUzNmE1NGIyYmM0MTIxMDJlOGE1OWU1MDVjMzU3MGY5YTQxYzdhYTY1MTI1ZmY5YjdkZTlkYmVmZmQ4Zjg0NzUzODEyMWI1ZmQxZTZkYjg2In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJkMDVkODBjNzJjY2UxZDk3MzVlYWNiN2EzZTA4MTQxMDkzYTMwYjVlZTAwOTExMDMzZjY0Y2FjNThlMzQxNzE2Iiwidm91dCI6NDQsInNjcmlwdFNpZyI6eyJhc20iOiI0NGMyMDdiMzQwYTk1ODc5NDRiYjY5NmM4ZTNiYjA0M2UzYTliYTliYzE3ZDExZDM1N2JkMTVjMDFjMTBjNzEwNTc4NTEyNzcyZTIyYTI1MDExODIwYzJhZWM3ZThlMWMyYjA1NTg3ZWZkZTNhNDBiOTM5NGZlYzE1MDYxMTVmZFtBTEx8Rk9SS0lEXSAwMjM3Y2RjOGY1MTI0NzFmNjA4MzQ2YTIyNWExNWU2NzZhNWU4YTNlMWRhNGY3MmY4NjFlYTM0ZGZlOTZmODY1YTkiLCJoZXgiOiI0MTQ0YzIwN2IzNDBhOTU4Nzk0NGJiNjk2YzhlM2JiMDQzZTNhOWJhOWJjMTdkMTFkMzU3YmQxNWMwMWMxMGM3MTA1Nzg1MTI3NzJlMjJhMjUwMTE4MjBjMmFlYzdlOGUxYzJiMDU1ODdlZmRlM2E0MGI5Mzk0ZmVjMTUwNjExNWZkNDEyMTAyMzdjZGM4ZjUxMjQ3MWY2MDgzNDZhMjI1YTE1ZTY3NmE1ZThhM2UxZGE0ZjcyZjg2MWVhMzRkZmU5NmY4NjVhOSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZDA1ZDgwYzcyY2NlMWQ5NzM1ZWFjYjdhM2UwODE0MTA5M2EzMGI1ZWUwMDkxMTAzM2Y2NGNhYzU4ZTM0MTcxNiIsInZvdXQiOjU1LCJzY3JpcHRTaWciOnsiYXNtIjoiZjNlNjAwM2YzNTgyODcxZDQ4YWYxNmE0YjM3ODgwNTNlN2NhNzYyYzczNjE3YmFiMmFlNWFlNmY5N2IxM2QzYjA0YzllYjgxMzVkZTg5OTA0NGQ4ZTJkYzcyYmFkYmRkODE5MWJmM2JiZDUyODhlOTg2ZDQyMzFiYTMzOWM1NTRbQUxMfEZPUktJRF0gMDNmN2M1OGE4MWI3ZDBhYWU2N2YzYzE2Zjk4ODNlZTMyZTAzODA0OTgzY2IzMzVkM2E1MjAwM2FiNzcxMTBiNzA4IiwiaGV4IjoiNDFmM2U2MDAzZjM1ODI4NzFkNDhhZjE2YTRiMzc4ODA1M2U3Y2E3NjJjNzM2MTdiYWIyYWU1YWU2Zjk3YjEzZDNiMDRjOWViODEzNWRlODk5MDQ0ZDhlMmRjNzJiYWRiZGQ4MTkxYmYzYmJkNTI4OGU5ODZkNDIzMWJhMzM5YzU1NDQxMjEwM2Y3YzU4YTgxYjdkMGFhZTY3ZjNjMTZmOTg4M2VlMzJlMDM4MDQ5ODNjYjMzNWQzYTUyMDAzYWI3NzExMGI3MDgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImQwNWQ4MGM3MmNjZTFkOTczNWVhY2I3YTNlMDgxNDEwOTNhMzBiNWVlMDA5MTEwMzNmNjRjYWM1OGUzNDE3MTYiLCJ2b3V0Ijo3Miwic2NyaXB0U2lnIjp7ImFzbSI6ImI3NWVmOWRiNjMwMWU4ZTRkNzM4MTVjZGE1Yjc4OTVhZGVkZTdiMTg5NzJhYmUwN2QwYTEyNmI3NGIwM2I4ZTM3OTA2NzYwMzIyZjNjYmZhZTE5Mjg0N2Y4ODAxZDI5YmI4M2JlMDJiM2FjYWEyZTZiYTMxMDUxODRhODM1MjUzW0FMTHxGT1JLSURdIDAzYjNmZjcxN2FkZTBiZDE2ZTJiNmI3YTYxMzk4ZjFjMzk2NTI4OGZhYjdiMjNmNTc3YjA3ZDU2NmZjN2YwY2VhNCIsImhleCI6IjQxYjc1ZWY5ZGI2MzAxZThlNGQ3MzgxNWNkYTViNzg5NWFkZWRlN2IxODk3MmFiZTA3ZDBhMTI2Yjc0YjAzYjhlMzc5MDY3NjAzMjJmM2NiZmFlMTkyODQ3Zjg4MDFkMjliYjgzYmUwMmIzYWNhYTJlNmJhMzEwNTE4NGE4MzUyNTM0MTIxMDNiM2ZmNzE3YWRlMGJkMTZlMmI2YjdhNjEzOThmMWMzOTY1Mjg4ZmFiN2IyM2Y1NzdiMDdkNTY2ZmM3ZjBjZWE0In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJlMWMyNDM4ZjI1YjY1MmNmNWEzZTQzY2NiOGFjNzM1NDk0YTkyYjdiODA3OGUzMDkxODQwNWE5OTdkYjEzNWFiIiwidm91dCI6NTEsInNjcmlwdFNpZyI6eyJhc20iOiJiMTFlYWUyNDkyMzYwMzFmZTgyNWNkNWIzNjJjN2IzYmZkYjczMjgwNDE1ZmQxZjg1MDNjYzJjMjUwMDE1OTNkYjIzMDgwZDgxNDMxYmI0ZmE0ZjA4MzZkYTE3MmU5ODQ0NTkyYjlmZWQ4NDk2ZGZhZWU0NjVhNDZhMWJhZDEwNFtBTEx8Rk9SS0lEXSAwMjYwNWMyNzI3YTRkMTdlNTcxN2ZlMGMyNDc5MWFjYTI3NmQwMzllODZhYWMwNzZmMWMwNWM3MmIxODFjYmIyOTAiLCJoZXgiOiI0MWIxMWVhZTI0OTIzNjAzMWZlODI1Y2Q1YjM2MmM3YjNiZmRiNzMyODA0MTVmZDFmODUwM2NjMmMyNTAwMTU5M2RiMjMwODBkODE0MzFiYjRmYTRmMDgzNmRhMTcyZTk4NDQ1OTJiOWZlZDg0OTZkZmFlZTQ2NWE0NmExYmFkMTA0NDEyMTAyNjA1YzI3MjdhNGQxN2U1NzE3ZmUwYzI0NzkxYWNhMjc2ZDAzOWU4NmFhYzA3NmYxYzA1YzcyYjE4MWNiYjI5MCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZTZiZDZkNDA2OTllYmViNTM5NzgzOTg5YzdlNDZkYTNmNjE1MTAxZTEzOWE2ODQ4ZjRiNDQwNTY2ZmU1MTlkZSIsInZvdXQiOjE5LCJzY3JpcHRTaWciOnsiYXNtIjoiYTA5ZDM1MmJkMGJjNDM1ZWM2OTZiNTQ3MmM3MDM3MTYwZjYxNDkwNmQxYjQ4MGJlYmUwN2NkZDg2ZTdkZjFlZmQ2MzUwMmQxOTg2OWJiZTVhNmIwMTAxNDc5ZWViYmFiMzZmZGMwZjczN2JkMjBmZDI3ZGJhMTRhMzAwOTE4NmVbQUxMfEZPUktJRF0gMDMzODhkYTFmYzk3ZTg5NGM2N2Y2MGU0YjcxYzdjODMwYmViY2M2OTg4NjgwM2MzY2IzZjhhYTQwODFlZTJmZDVkIiwiaGV4IjoiNDFhMDlkMzUyYmQwYmM0MzVlYzY5NmI1NDcyYzcwMzcxNjBmNjE0OTA2ZDFiNDgwYmViZTA3Y2RkODZlN2RmMWVmZDYzNTAyZDE5ODY5YmJlNWE2YjAxMDE0NzllZWJiYWIzNmZkYzBmNzM3YmQyMGZkMjdkYmExNGEzMDA5MTg2ZTQxMjEwMzM4OGRhMWZjOTdlODk0YzY3ZjYwZTRiNzFjN2M4MzBiZWJjYzY5ODg2ODAzYzNjYjNmOGFhNDA4MWVlMmZkNWQifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6ImU2YmQ2ZDQwNjk5ZWJlYjUzOTc4Mzk4OWM3ZTQ2ZGEzZjYxNTEwMWUxMzlhNjg0OGY0YjQ0MDU2NmZlNTE5ZGUiLCJ2b3V0Ijo2NSwic2NyaXB0U2lnIjp7ImFzbSI6ImE2OGM5NWQyYmE3YmMyZjJiNmI4MmY0ZGQwMDJlZWE5ZTY4NjkwY2E2NjZjZDYzODk4MTU3NzRmZmQ5Njc1NzFkMmFjN2QyZDY5ODkwZmU0MTg0NzQzNmUxNzEwZDVjODMwM2U2NDVkNWY1YzM0YWVlY2VmZjIyOGY4MWQwMzQ0W0FMTHxGT1JLSURdIDAzYWU2MDcyNTA2ZTIwOTcwMGNmMTBlYjM1MDhlODI0Nzk4MDI1Nzg0MWRiNjBlZDQ0NWYwYTQzMWU1YWQ1NWVkNiIsImhleCI6IjQxYTY4Yzk1ZDJiYTdiYzJmMmI2YjgyZjRkZDAwMmVlYTllNjg2OTBjYTY2NmNkNjM4OTgxNTc3NGZmZDk2NzU3MWQyYWM3ZDJkNjk4OTBmZTQxODQ3NDM2ZTE3MTBkNWM4MzAzZTY0NWQ1ZjVjMzRhZWVjZWZmMjI4ZjgxZDAzNDQ0MTIxMDNhZTYwNzI1MDZlMjA5NzAwY2YxMGViMzUwOGU4MjQ3OTgwMjU3ODQxZGI2MGVkNDQ1ZjBhNDMxZTVhZDU1ZWQ2In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJlNmJkNmQ0MDY5OWViZWI1Mzk3ODM5ODljN2U0NmRhM2Y2MTUxMDFlMTM5YTY4NDhmNGI0NDA1NjZmZTUxOWRlIiwidm91dCI6ODcsInNjcmlwdFNpZyI6eyJhc20iOiJiMTQ0NTA3MzA4ZGIyNGRmYWQyZDg2NzAzNmQ3ODkyNWFiZjFhZTk5N2YxOTk4ZTIxMTk5OTc2NzNmYmQxZjQxZTJkYjdlYjgzODg3NzBjNDMxOWRiYjkxODlkZmNkOTM0MGEzMGY5ZjM0Yzg0NzA1OWU0MjBmZjIzZWQ5ZDhlNVtBTEx8Rk9SS0lEXSAwM2I4NWNhMWM1MDk3MTFmMDllOTEwODNkYjYyODVmZTU0N2YyNDZiMTllZmZmNWZhMGIwMDNkYTlmZWNjYmFjNWUiLCJoZXgiOiI0MWIxNDQ1MDczMDhkYjI0ZGZhZDJkODY3MDM2ZDc4OTI1YWJmMWFlOTk3ZjE5OThlMjExOTk5NzY3M2ZiZDFmNDFlMmRiN2ViODM4ODc3MGM0MzE5ZGJiOTE4OWRmY2Q5MzQwYTMwZjlmMzRjODQ3MDU5ZTQyMGZmMjNlZDlkOGU1NDEyMTAzYjg1Y2ExYzUwOTcxMWYwOWU5MTA4M2RiNjI4NWZlNTQ3ZjI0NmIxOWVmZmY1ZmEwYjAwM2RhOWZlY2NiYWM1ZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiZTZjNDE4Nzg2ZmQwNzExMDYzYjI4MGY5ZjU2YWMxOTY1MzEwY2E5MDBjNzRhNmU3ZThjNmY5M2ZiNTg2NTU4MyIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIxOTBmOTAwMzU3MmU3ZWI5MTQ1ODdjZTdiY2VlNzcwZjdjMWFmMzNjZDYzMzExZGI2ZGNlNDQ3OGU3MDdiYTE5NGEwMWVhMDEwZmVjNjY5MjE0NzVhNjE5NDM5ODAxODFiMDFlMzJiNjMwNjMzNzM5YzRlM2ExN2UyNWE2NmRhYVtBTEx8Rk9SS0lEXSAwMzFiMjI5Zjc4MWU2YTE0M2JjNWU2NDQ2N2JmZDMyZmY1NDRkMTg4OWIxM2YyZmY2NDBlNGM0NmY2MDcwNGRkYjYiLCJoZXgiOiI0MTE5MGY5MDAzNTcyZTdlYjkxNDU4N2NlN2JjZWU3NzBmN2MxYWYzM2NkNjMzMTFkYjZkY2U0NDc4ZTcwN2JhMTk0YTAxZWEwMTBmZWM2NjkyMTQ3NWE2MTk0Mzk4MDE4MWIwMWUzMmI2MzA2MzM3MzljNGUzYTE3ZTI1YTY2ZGFhNDEyMTAzMWIyMjlmNzgxZTZhMTQzYmM1ZTY0NDY3YmZkMzJmZjU0NGQxODg5YjEzZjJmZjY0MGU0YzQ2ZjYwNzA0ZGRiNiJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfUkVUVVJOIDU5MjAwNzAgMzFlMDFmZWU5OWRhMGEwYWFkNDFlYzU4OThlZjY2MDE1NmQyMDcyYTBlMzJkYzE3NDgwMWMzNTM1NTA0ZDU3ZiIsImhleCI6IjZhMDQ0NjU1NWEwMDIwMzFlMDFmZWU5OWRhMGEwYWFkNDFlYzU4OThlZjY2MDE1NmQyMDcyYTBlMzJkYzE3NDgwMWMzNTM1NTA0ZDU3ZiIsInR5cGUiOiJudWxsZGF0YSJ9fSx7InZhbHVlIjowLjAwNjI3NTkzLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMWNhODE2Y2FlMTVkNTkwYWVkMzc4ZDU5ZjlmMmZiMWYyZDA4NDc5NiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDFjYTgxNmNhZTE1ZDU5MGFlZDM3OGQ1OWY5ZjJmYjFmMmQwODQ3OTY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF3MnM5azJ1OXc0anpoZHg3eDRuNzBqbHYwajZ6ejhqYzlwZzl1NXVzIl19fSx7InZhbHVlIjowLjAwNjMyMTQxLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDUxNWE5NjFhNDFiYThjNThkZDJiNjVlOWYwOWExODA1MGI5MWNiYSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQ1MTVhOTYxYTQxYmE4YzU4ZGQyYjY1ZTlmMDlhMTgwNTBiOTFjYmE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXIyM3QydHA1c2Q2MzN2ZDYybTlhOGNmNXhxOXB3Z3VoZ2x4bmhnbDdqIl19fSx7InZhbHVlIjowLjAwNzYyNzA3LCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2MyMDA0ZGVmMzhmMThkNTU0MDJmMGMyNWE4MTZlYzZhYWYwNmYwNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdjMjAwNGRlZjM4ZjE4ZDU1NDAyZjBjMjVhODE2ZWM2YWFmMDZmMDY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA3enFweDc3dzgzMzQyNXF0Y3Z5azVwZG1yMjR1cjBxY3lxczJlMGphIl19fSx7InZhbHVlIjowLjAxMDE1OTA4LCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZWMyOThmNDdhNTQ3NWE2MzdiYTQ1MmZlNGM3NDkxNDc1ZDFlMTE0MSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGVjMjk4ZjQ3YTU0NzVhNjM3YmE0NTJmZTRjNzQ5MTQ3NWQxZTExNDE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJrem5yNjg1NHI0NWNtbTUzZjB1bnI1ajlyNDY4czNneTZxeW5qZTN6Il19fSx7InZhbHVlIjowLjAxMTU0OTgzLCJuIjo1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTVhMjYzY2VmZGEwZmI5YWIyNGRiOTUyODczNTUwZDk5Yjc0OWRjNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGU1YTI2M2NlZmRhMGZiOWFiMjRkYjk1Mjg3MzU1MGQ5OWI3NDlkYzc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJqNnljN3dsa3MwaHg0amZrdTQ5cGU0MnJ2ZWtheWFjdWczcWZ4MDNzIl19fSx7InZhbHVlIjowLjAxNzczNjk2LCJuIjo2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDMyZGVkYWQyNjNhMDBmMGQ2YmYzMTVhNWE5N2UwN2ExMTgzOWRmNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQzMmRlZGFkMjYzYTAwZjBkNmJmMzE1YTVhOTdlMDdhMTE4MzlkZjU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJmam1tZGR5Y2FxcHV4a2h1YzQ1azVodXBhcHJxdWE3NTNrNWU4anB1Il19fSx7InZhbHVlIjowLjAxODI0NTUzLCJuIjo3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTdlMmQ3MDIwNmIxZWNmZGIwZmY4NzM0ZmIwNDg5NWQ2MDBiYTU5YyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU3ZTJkNzAyMDZiMWVjZmRiMGZmODczNGZiMDQ4OTVkNjAwYmE1OWM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB0Nzk0Y3pxNmM3ZWxkc2w3cm5mN2N5Mzl3a3F6YTluc2szdGZ1M3k2Il19fSx7InZhbHVlIjowLjAyMTE0MTkyLCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjk2N2VhNjI5ODk0MzgwZWE5MDQ2OThkZWVkZGRiZmFhNDBiZWNjYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDI5NjdlYTYyOTg5NDM4MGVhOTA0Njk4ZGVlZGRkYmZhYTQwYmVjY2M4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXE1azA2bnpuejJyc3I0ZnEzNWNtbWthbTBhMmd6bHZlc3lnYWZybTA0Il19fSx7InZhbHVlIjowLjAyMTg0NjU0LCJuIjo5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMGQ2OTJmNDFjNzQyZDRmYmZhM2EyYzU0NzQ5NThjYTE3ZmY4ZDg5NSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDBkNjkyZjQxYzc0MmQ0ZmJmYTNhMmM1NDc0OTU4Y2ExN2ZmOGQ4OTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF4a2p0NnBjYXBkZjdsNjhnazlnYXk0M2pzaGw3eGNqNXJ4a21xbm0wIl19fSx7InZhbHVlIjowLjAzMjc3MTY3LCJuIjoxMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDcwNjczYmEyZjUwODY5OWE2ZjlmMDk2YTYwYTcwNjA0OGZlZWI5NWQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3MDY3M2JhMmY1MDg2OTlhNmY5ZjA5NmE2MGE3MDYwNDhmZWViOTVkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwY3h3d2F6NzV5eG54bjBudXlrNWM5OHFjemdsbTRldDV0a2tqZTVsayJdfX0seyJ2YWx1ZSI6MC4wMzI3Nzk5NywibiI6MTEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAyYmZkZDViZGVjOWZkOTkzMzM2M2VhY2Y4OWU4ZDg1MTA1NjQ3Y2NjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MmJmZGQ1YmRlYzlmZDk5MzMzNjNlYWNmODllOGQ4NTEwNTY0N2NjYzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcTRsbTRkYWFqMGFueWVudjA0dmx6MGdtcGdzMmVydWVzdno4N3JjbDgiXX19LHsidmFsdWUiOjAuMDMzMTMzOCwibiI6MTIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAwODNlYTMyNGVkZTY1ZTZkYmQ4ZWUyNWRmZDVhNDk3Y2NjOGM1OTNhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MDgzZWEzMjRlZGU2NWU2ZGJkOGVlMjVkZmQ1YTQ5N2NjYzhjNTkzYTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXlyYWdleWFobjl1bWRhM20zOW1sMjZmOTd2ZXJ6ZThncWhkbDd0NnEiXX19LHsidmFsdWUiOjAuMDM0MTc1NzIsIm4iOjEzLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYmJmZTRkMzk2YzM5Y2RkNzkzMzdjMzkzM2NjYTk1NTE0ZWQwZDkzNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGJiZmU0ZDM5NmMzOWNkZDc5MzM3YzM5MzNjY2E5NTUxNGVkMGQ5MzU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXphbHVuZmVkc3V1bTR1bnhscGV4MHgyajRnNWE1eGV4NWxwdnZxMnVyIl19fSx7InZhbHVlIjowLjAzNTYxNDA0LCJuIjoxNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDk1OTMxYzY3YzY2NzQ4Y2ZkMGIyMmM3NTcwMWJkNGY5YTljYjc2YzIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5NTkzMWM2N2M2Njc0OGNmZDBiMjJjNzU3MDFiZDRmOWE5Y2I3NmMyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6MmV4OHI4Y2VuNTNuN3NrZ2s4MnVxbTZudTZuam1rY2duNWtnbTh0eCJdfX0seyJ2YWx1ZSI6MC4wMzY2ODIzMSwibiI6MTUsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2MDYzM2IxZWJlYWY0NGNjMTZmYWUxOTk3ODQxMjU5MDQ2MjFhZGRhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NjA2MzNiMWViZWFmNDRjYzE2ZmFlMTk5Nzg0MTI1OTA0NjIxYWRkYTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcHN4eHdjN2g2aDVmbnFrbHRzZWo3enB5a2d5dmdkZG1neTJwcDQ4cHgiXX19LHsidmFsdWUiOjAuMDM3MDE5MDksIm4iOjE2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTJjM2E5ODFiMmI1ZTFkYTUwMTJmYWU1YzQzZmZkNjYyYzI5NDc5MyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGEyYzNhOTgxYjJiNWUxZGE1MDEyZmFlNWM0M2ZmZDY2MmMyOTQ3OTM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXozdjgydnBrMjY3cmtqc3p0YXd0M3BsbDRuemMyMjhqdmFwdnNqbXBoIl19fSx7InZhbHVlIjowLjA0MTMzMDQ4LCJuIjoxNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDBlYjY2Njk4MGNlYmE3MjI0MDFhOTk2OWI3Y2ViMmZlYjk2MDc4MDAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwZWI2NjY5ODBjZWJhNzIyNDAxYTk5NjliN2NlYjJmZWI5NjA3ODAwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxOHR2ZTVjcG40NndnanFyMnZrbmQ3d2t0bHRqY3JjcXF6Zm1yeHJ2YSJdfX0seyJ2YWx1ZSI6MC4wNDQwNjU1NSwibiI6MTgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBkMmI5Nzg2MTkwZGE2ZTkzOTM3ZWI0OGE3NDIwZjMwYzUzMzZhMmVhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZDJiOTc4NjE5MGRhNmU5MzkzN2ViNDhhNzQyMGYzMGM1MzM2YTJlYTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcmZ0ajdycGpyZHhheXVuMDY2ZzVhcHE3dng5eGQ0emFnZmV6Y2szNnEiXX19LHsidmFsdWUiOjAuMDQ0NzIxNDEsIm4iOjE5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZjNhZWIyODRiNzAzZDFmNmExMGVhZTY2OWMxMTNkMjVlYzMwZmZiOSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGYzYWViMjg0YjcwM2QxZjZhMTBlYWU2NjljMTEzZDI1ZWMzMGZmYjk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJlNmF2NXlrdXBhcmE0cHA2aHhkOHEzODVqN2N2OGxoeWh0aDBjNWZuIl19fSx7InZhbHVlIjowLjA0NTAxNDk0LCJuIjoyMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDU2ODMyMjg2Mzc5ZjUxNjAyYWFlY2NhOTJmNTFkYzAyY2M1MzAwMTYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1NjgzMjI4NjM3OWY1MTYwMmFhZWNjYTkyZjUxZGMwMmNjNTMwMDE2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwdGd4ZzV4eDcwNHpjcDI0bXgyanQ2M21zcHZjNWNxemN3eHFocDM2YSJdfX0seyJ2YWx1ZSI6MC4wNDUyNDA3LCJuIjoyMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDA2ZmM0ZDg0ZWMzNzdlNTAyZDE2MzBkNjE3M2NkZWQxY2ExNjA3ZGMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwNmZjNGQ4NGVjMzc3ZTUwMmQxNjMwZDYxNzNjZGVkMWNhMTYwN2RjODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxcjBjbnZ5YXNtaHU1cGR6Y2NkdjlldW1tZ3U1OXM4bXM3czYzdXV1ayJdfX0seyJ2YWx1ZSI6MC4wNDg5Mzk0MSwibiI6MjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAzMGE1OTI5NDlkM2ZjYzdiZTRmMWIzM2YwY2NiYTM4YTkxZmZlZDIxIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MzBhNTkyOTQ5ZDNmY2M3YmU0ZjFiMzNmMGNjYmEzOGE5MWZmZWQyMTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWMydHk1NW41bHVjN2x5N3hlbjdyeHQ1dzlmcmxsZHl5ZDlyamQ5ejUiXX19LHsidmFsdWUiOjAuMDUxMTAyNjMsIm4iOjIzLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTNiMjIxYmZkNWQ3NzAzNDljYjFmNWY4MjEwMzgwYTRmMTNlNzExMyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGUzYjIyMWJmZDVkNzcwMzQ5Y2IxZjVmODIxMDM4MGE0ZjEzZTcxMTM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXIzbXlnZGw2aHRocWR5dWs4NmxzZ2dyc3pqMHowbjN6dmV3bnQ3ZHIzIl19fSx7InZhbHVlIjowLjA1MzI2ODM4LCJuIjoyNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDI5MGYwNDkyNmM1OTE4OTczYjI4NTU0NTg0MjM2MGNlMjk5Y2FjZGQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQyOTBmMDQ5MjZjNTkxODk3M2IyODU1NDU4NDIzNjBjZTI5OWNhY2RkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxNXM3cHlqZDN2MzM5ZW05cDI1dHBwcnZyOHpuODl2bTVuNGh1d2t2NyJdfX0seyJ2YWx1ZSI6MC4wNTM2NzY2NiwibiI6MjUsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxODllYjgzNjkxM2RhMDM3NjBlZjE5YWM3OTAxNmFiZjRlYzNjOThkIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTg5ZWI4MzY5MTNkYTAzNzYwZWYxOWFjNzkwMTZhYmY0ZWMzYzk4ZDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXZmYXdwa2p5NzZxZG1xYXV2NmM3Z3BkMmw1YXM3ZjM1NWc1Zmt4NGciXX19LHsidmFsdWUiOjAuMDU1ODEzMzUsIm4iOjI2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZWNmZWU3YWI4NzhlODBkZjBjODk0YWZmYzQyNjhhNzA2YmRmOGM4ZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGVjZmVlN2FiODc4ZTgwZGYwYzg5NGFmZmM0MjY4YTcwNmJkZjhjOGQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJrMGFlYXRzNzhncGhjdjM5OTBsM3B4M2ZjeGhodXYzNWVzOThoN3F0Il19fSx7InZhbHVlIjowLjA1NjQzODQyLCJuIjoyNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDRlNmFmMWNmMjI5YmQ4YjM2OGNhMDYyMmYyMDZhNGJkM2FmODAyNDYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0ZTZhZjFjZjIyOWJkOGIzNjhjYTA2MjJmMjA2YTRiZDNhZjgwMjQ2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwOHg0dXcweTJkYTN2bWdlZ3J6OXVzeDVqN240N3F6Z2NtdXYwcWhtMyJdfX0seyJ2YWx1ZSI6MC4wNjY3ODcyOSwibiI6MjgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2N2QwNmFkMDcwZWRkNTYzZWQ3MTFhMjkxMjAzODJiZDA0ODk5ZDMwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NjdkMDZhZDA3MGVkZDU2M2VkNzExYTI5MTIwMzgyYmQwNDg5OWQzMDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcG5hcTZrc3dya2EyY2xkd3lkemp5c3JzMjdzZnp2YXhxcGFqMzJycWYiXX19LHsidmFsdWUiOjAuMDY4MTc1MzMsIm4iOjI5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMGM0YjcwMDBmMWFhNTIyYzNhMWQzZTk5Njc1ZmM0MmRhY2RkMWZiYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDBjNGI3MDAwZjFhYTUyMmMzYTFkM2U5OTY3NWZjNDJkYWNkZDFmYmM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF4eWt1cXE3eDQ5eXRwNnI1bGZqZTZsY3NrNmVoZ2xoc3ozZnF1dDhnIl19fSx7InZhbHVlIjowLjA3MDIyNzA3LCJuIjozMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkwOGJkNTI1OWI0YjQ0ZTE4NWIwOGUxMjgyZjY1MmNlM2MxM2ZiNDUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5MDhiZDUyNTliNGI0NGUxODViMDhlMTI4MmY2NTJjZTNjMTNmYjQ1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6Z2doNGY5bmQ5NWZjdjlrejhwOXFoazJ0OHJjeWxtZzV1azd5dmxmYyJdfX0seyJ2YWx1ZSI6MC4wNzE1NDIyOCwibiI6MzEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA1MzhmZTFkNzQ2Nzc2NjFjMTIxYWZjMGE4Y2RiNTFiN2I4NzY1NDZlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NTM4ZmUxZDc0Njc3NjYxYzEyMWFmYzBhOGNkYjUxYjdiODc2NTQ2ZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGZjbGN3aGdlbWt2OHFqcnQ3cTRyeG0yeG1tc2FqNWRjczZ4cmQ5MmQiXX19LHsidmFsdWUiOjAuMDczMDU3NSwibiI6MzIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAzY2U0YTNmMzMwNzk2ZGE1MWQwNGNkNTBmNjRkY2VkOGM3NmRmZTdmIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0M2NlNGEzZjMzMDc5NmRhNTFkMDRjZDUwZjY0ZGNlZDhjNzZkZmU3Zjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcTd3ZmdsbnhwdWttZmdhcW54NHBhamRlbXZ2d20wNzB1ejhncjcyY2ciXX19LHsidmFsdWUiOjAuMDc0MTQxMDcsIm4iOjMzLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNGY1ZjE2Yzk2ZDRkMjVmMzFkY2E0ZjRhMWE2ZDVhMjAyODE2ZTBkMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDRmNWYxNmM5NmQ0ZDI1ZjMxZGNhNGY0YTFhNmQ1YTIwMjgxNmUwZDA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA4NDc5a2ZkNHhqdHVjYWVmODU1eG5kdGdzenM5aHE2cXIwc3Voejl2Il19fSx7InZhbHVlIjowLjA3NTQ2NTcyLCJuIjozNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGFmMGNkZTNiM2IyZDg0YmRlZjk4YzBlNDE1ODVmYzUwYzA1MjQ3M2UgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhZjBjZGUzYjNiMmQ4NGJkZWY5OGMwZTQxNTg1ZmM1MGMwNTI0NzNlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6aHNlaDNtOHZrY2YwMDBucnF3Zzl2OWwzZ3ZxNWo4OGMzMnBydjd4eiJdfX0seyJ2YWx1ZSI6MC4wNzcyNDcxNiwibiI6MzUsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3Y2FhY2QxMTg4YTY2MDUxODk0ZGM2YTg0OGUxYjdkODk5ZDE1MTllIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0N2NhYWNkMTE4OGE2NjA1MTg5NGRjNmE4NDhlMWI3ZDg5OWQxNTE5ZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDcyNG5nMzN6bnhxNXZmZmhyMnNqOHBrbHZmbjUyM25jYTYwMHZweHciXX19LHsidmFsdWUiOjAuMDc4NDA0MzcsIm4iOjM2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYzMyODMzNzU4MzEzNmQyOTQ1Nzc3MGViNjI2NzE1MTdlOTIxMDAzMyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGMzMjgzMzc1ODMxMzZkMjk0NTc3NzBlYjYyNjcxNTE3ZTkyMTAwMzM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJwanN2bTRzdmZrNjIyOXdhY3drY244ejV0N2pnZ3F4dnh5NGRrcjh6Il19fSx7InZhbHVlIjowLjA4MTE5NDI0LCJuIjozNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDQ1YzBlODAzYTA2MmQyODVhMmRiMWY5ZWFlNDk4OGRlMWY5MTViMzQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0NWMwZTgwM2EwNjJkMjg1YTJkYjFmOWVhZTQ5ODhkZTFmOTE1YjM0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwenVwNnFyNXAzZDlwZHptdjBlYXRqZjNyMHBseTJteHMzd205ajUzcyJdfX0seyJ2YWx1ZSI6MC4wODEyMDk4NCwibiI6MzgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAwOWYwYzI0MDNjOWY1OTkwMDEwOWM5NTM0NGY5MmYzNjk1ZjU1NTA0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MDlmMGMyNDAzYzlmNTk5MDAxMDljOTUzNDRmOTJmMzY5NWY1NTUwNDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXlscHNqcThqMDRueXFwcDh5NHgzOGU5dW1mdGEyNHFzMDRramh2M2oiXX19LHsidmFsdWUiOjAuMDg0MjI3MTIsIm4iOjM5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNWNiZWMxZWUwNzI3MTYwNmU5YjZiY2Y3MWVjMDQ0NzlmNjMyZDMyZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDVjYmVjMWVlMDcyNzE2MDZlOWI2YmNmNzFlYzA0NDc5ZjYzMmQzMmY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB3dGFzMHdxdW4zdnBoZms2NzB3OGtxZzN1bHZ2a245dTdsbTltNDNzIl19fSx7InZhbHVlIjowLjA4NTE3MTAzLCJuIjo0MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGYwMjMyNzU1Njc2NTM3ZmViMGJiZDc4ZDFlZDFiMjc1ZmZhN2RmOGEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmMDIzMjc1NTY3NjUzN2ZlYjBiYmQ3OGQxZWQxYjI3NWZmYTdkZjhhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyY3p4ZjY0dmFqbjBsNHNoMHRjNjhrM2tmNmxsZjdsM2d0Y3drdmVqcyJdfX0seyJ2YWx1ZSI6MC4wOTMxMTQwOCwibiI6NDEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAwNDI4YmM1Y2Q4NDA1MTk5ZmU2MTNiNGM2MzRjZTAxYWYxNTJkYmI5IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MDQyOGJjNWNkODQwNTE5OWZlNjEzYjRjNjM0Y2UwMWFmMTUyZGJiOTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXp6MzB6dW1wcTlyeDA3dnlhNWNjNnZ1cWQwejVrbWh5ZGZsN3M3cG0iXX19LHsidmFsdWUiOjAuMDk0ODQzMzUsIm4iOjQyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZWQ4YWMwN2IwOTFkYzJlYTU2NDMyMWVmMjgzYWMyMWIyY2RkZGEyYiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGVkOGFjMDdiMDkxZGMyZWE1NjQzMjFlZjI4M2FjMjFiMmNkZGRhMmI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJrYzRzcm1weXd1OTZqa2d2czc3MnA2Y2dkamVodzY5dnFrM3o2YXhlIl19fSx7InZhbHVlIjowLjA5NTM1NzUxLCJuIjo0Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGEzMGQzNzJjM2QxYjA5MDgzMDY3MTg4NWFjNDQ0ZThhYTgyMmU4OTAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhMzBkMzcyYzNkMWIwOTA4MzA2NzE4ODVhYzQ0NGU4YWE4MjJlODkwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6M3M2ZGV2ODVkc2p6cHN2dXZndHR6eWY2OTJzZ2hnanEzNHc3OTVlaCJdfX0seyJ2YWx1ZSI6MC4wOTY5MDc1OCwibiI6NDQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA1MGE2MmViZGY4MjlhMTFlMzhmY2RlNmYyODk4MjcxNzgwYmY1MmQzIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NTBhNjJlYmRmODI5YTExZTM4ZmNkZTZmMjg5ODI3MTc4MGJmNTJkMzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGcydnQ0YWxxNTZ6ODNjbG4weDcyeWN5dXRjcDA2ajZ2YWEzMHYweGciXX19LHsidmFsdWUiOjAuMDk4MjEwMTgsIm4iOjQ1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjk5ZTU5NjUzZDM0Y2M2ZjJmMTAwNGEzZGQwMTVjZDJhMjhiNzljZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGI5OWU1OTY1M2QzNGNjNmYyZjEwMDRhM2RkMDE1Y2QyYTI4Yjc5Y2Y4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp1ZXVrdDk4NTZ2Y21lMHpxejI4aGdwdG5mMjl6bWVldWRlYzdtOWswIl19fSx7InZhbHVlIjowLjEwMDE0MjI2LCJuIjo0Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGI5NDFiMmQ3MjQzY2E5ODIxMjViNDJjOWU0YzdkZTA5OWM0MzIyNGEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRiOTQxYjJkNzI0M2NhOTgyMTI1YjQyYzllNGM3ZGUwOTljNDMyMjRhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6dTVydmtoeXM3Mm5xc2p0ZHB2bmV4OG1jeWVjc2V6ZmdmdzhodmptcSJdfX0seyJ2YWx1ZSI6MC4xMDAyNTk0NiwibiI6NDcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA0MjE1NWIzYmIyYjA0OWMyNWMwNTM2NTYzZjIyM2M0OWRiY2M0ODZlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NDIxNTViM2JiMmIwNDljMjVjMDUzNjU2M2YyMjNjNDlkYmNjNDg2ZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcHBwMmtlbWsyY3luc2p1cTVtOXYwZXo4M3lhaG56Z2RjcHZwaG1wNGMiXX19LHsidmFsdWUiOjAuMTAxMDM4ODcsIm4iOjQ4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNDAxZjQyNzZjMGFmNGJiNTJhNTBmYTI2MTQxMWUzZDNiMGVkMTdhOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDQwMWY0Mjc2YzBhZjRiYjUyYTUwZmEyNjE0MTFlM2QzYjBlZDE3YTg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBxcDdzbmtjemg1aGRmMjJyYXp2OXEzdTBmbXBtZ2g0cTh2N2N6Nmh6Il19fSx7InZhbHVlIjowLjEwMTQ4NDUsIm4iOjQ5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZmEyNzk5MGZiODczNTA5MmVlYjJiODlhNTY1NDZmZmQyNTcyMDJjMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGZhMjc5OTBmYjg3MzUwOTJlZWIyYjg5YTU2NTQ2ZmZkMjU3MjAyYzI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJhejB4ZzBocGU0cHlod2sydWY1NGo1ZGw3ajJ1c3pjZ2ZubjU4cnE0Il19fSx7InZhbHVlIjowLjEwNTc1NjI0LCJuIjo1MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDA2NzExOGFkNjBjNTFlZmRkNTAzMWE2ODZmOWNmYjE2YzIyMWY4OWIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwNjcxMThhZDYwYzUxZWZkZDUwMzFhNjg2ZjljZmIxNmMyMjFmODliODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxcjh6eDlkdnJ6M2FsdzRxdmR4c211dWx2dHZ5ZzBjbnZuNzVreHhlOSJdfX0seyJ2YWx1ZSI6MC4xMTQwOTk4OSwibiI6NTEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBkY2FkYzc4ZTRkZWYzOGIyYzU2NTU3ZWE3ZTE0NjA1N2Q4MWZiMDVmIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZGNhZGM3OGU0ZGVmMzhiMmM1NjU1N2VhN2UxNDYwNTdkODFmYjA1Zjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcncybTN1d2ZoaG4zdms5djR0NzVsczV2cHRhczhhc3R1cjY3dXBtaGUiXX19LHsidmFsdWUiOjAuMTE2MzQ2NTcsIm4iOjUyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjI4YzNkZGM5YjFmNGRjMTdjMmE5ZGY5MDk4OTA4ZmYwMDM0NjY3OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGIyOGMzZGRjOWIxZjRkYzE3YzJhOWRmOTA5ODkwOGZmMDAzNDY2Nzk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXplZ2Mwd3VudjA1bXN0dTkyd2xqenZmcHJsc3FkcngweTgzZ3NncmhyIl19fSx7InZhbHVlIjowLjEyMzMyNzIzLCJuIjo1Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGRiZWYyNGNiYzdjODZmZTA3NTBmNWUyNGViMDU2OTZjNTY2NzQ3OWMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkYmVmMjRjYmM3Yzg2ZmUwNzUwZjVlMjRlYjA1Njk2YzU2Njc0NzljODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyZDc3Znh0Y2x5eGxjcjRwYTB6ZjZjOWQ5azl2ZTY4bnMzNzhtbHJtZSJdfX0seyJ2YWx1ZSI6MC4xMjUyMDUwNywibiI6NTQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2NWU4NWIxYTQzNjNjZDE4MGM1OTQ0N2E5YmNmMGRlZGM5NDNkYTY3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NjVlODViMWE0MzYzY2QxODBjNTk0NDdhOWJjZjBkZWRjOTQzZGE2Nzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGo3c2tjNmdkM3U2eHF2dDl6ODR4NzBwaGt1anM3NnZ1ZmhxcXlrMHciXX19LHsidmFsdWUiOjAuMTI5MTQxNDEsIm4iOjU1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOWRjY2E3NjJkMGM1MTI2YWY1YWFhMDc5YzkwMzMyMDVlZTUwMDczZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDlkY2NhNzYyZDBjNTEyNmFmNWFhYTA3OWM5MDMzMjA1ZWU1MDA3M2Q4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp3dWVmbXo2cnozeTZoNDQyczhuamdyeGd6N3U1cTg4NWxnbTkwenpuIl19fSx7InZhbHVlIjowLjEzMTU5NzQ3LCJuIjo1Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDM0Y2FjNTc5YWJlNmY4MDlmOWE1OTJmNGU0MmI2ZTZkOWYzMjY1ZTAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQzNGNhYzU3OWFiZTZmODA5ZjlhNTkyZjRlNDJiNmU2ZDlmMzI2NWUwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxNnY0M3RlNDBuMHN6MGU1a2YwZmVwdGRla2U3dm45dXE0ajNhZWVycyJdfX0seyJ2YWx1ZSI6MC4xMzQ5MzQ4NiwibiI6NTcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAwNzc5ZWEyOWJjN2UyZmJlNjgxYjcxNjk0NjhjMjBkOGM2NTJkYjMwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MDc3OWVhMjliYzdlMmZiZTY4MWI3MTY5NDY4YzIwZDhjNjUyZGIzMDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXJobjYzZmgzbHpsMG5ncmRja2ozNXZ5cnZ2djVrbXhxY200bTJjZTIiXX19LHsidmFsdWUiOjAuMTM2MDMyNjksIm4iOjU4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOWNiYWJhZWUzMjU3NTEzMTdkMzEzMmUyNGNhNGI0NjM1MDg4NTE1NCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDljYmFiYWVlMzI1NzUxMzE3ZDMxMzJlMjRjYTRiNDYzNTA4ODUxNTQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp3dDR3aHd4ZnQ0enZ0YXh5ZXd5bjl5azMzNHB6ejMyc2V2czg4ZWNmIl19fSx7InZhbHVlIjowLjEzODA1MzA0LCJuIjo1OSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDlhN2I2MTZjZjg1YzgyMzFmYTE0OWQ2OTI4NTY2MmJiZGNhOGYxNTkgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5YTdiNjE2Y2Y4NWM4MjMxZmExNDlkNjkyODU2NjJiYmRjYThmMTU5ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZDhrY3R2bHB3Z3l2MDZ6andrajJ6a3YyYWFlMjgzdHlta2twNTdkZiJdfX0seyJ2YWx1ZSI6MC4xNDAzNjc1NiwibiI6NjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3MmY3Y2UxMGI5M2FiM2NlYTdjYzVlZDI0YTNkMmNmODhlM2VlNTUwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NzJmN2NlMTBiOTNhYjNjZWE3Y2M1ZWQyNGEzZDJjZjg4ZTNlZTU1MDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGUwMG5zc2h5YXQ4bjQ4ZTMwZHlqM2E5bnVndTBoOTJxd2xyNXZycDIiXX19LHsidmFsdWUiOjAuMTQzNDUwODEsIm4iOjYxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODZkM2I3YzNhZjkwYjRmOTkxYjY4ZTk0Y2RiZGNkODg1ZjJmY2Q1NyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg2ZDNiN2MzYWY5MGI0Zjk5MWI2OGU5NGNkYmRjZDg4NWYyZmNkNTc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpyZDhkN3I0N2d0Zjd2M2s2OGZmbmRhZWt5OTd0N2QydWhmbGFycjRwIl19fSx7InZhbHVlIjowLjE0OTYxNzc0LCJuIjo2Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDFlNjJlZTJjNGFkMmNhZWY4MGQwYzhiNGJkZWJiY2U5ODI4ZDI1OTUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxZTYyZWUyYzRhZDJjYWVmODBkMGM4YjRiZGViYmNlOTgyOGQyNTk1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxMHg5bTN2ZnRmdjRtdXE2cnl0ZjAwdGhuNWM5cmY5ajU1eXVjeXRmbSJdfX0seyJ2YWx1ZSI6MC4xNTM3ODQ4NywibiI6NjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5NjM1MGFkZmYwZWMwNDA0YTVhOWI4ODBjZjgzOTZlMmRlZDVhZjVkIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OTYzNTBhZGZmMGVjMDQwNGE1YTliODgwY2Y4Mzk2ZTJkZWQ1YWY1ZDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenRyMnprbDdya3FncDk5NHh1Z3BudXJqbTNkYTRkMHQ1Mjc2eHBzZXIiXX19LHsidmFsdWUiOjAuMTU1MTM3NDMsIm4iOjY0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYmMxNzg4Y2E5YTY2NzcwZDVkMzc1NGI4ZWUwNGIyN2ZkMjYzZWI4ZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGJjMTc4OGNhOWE2Njc3MGQ1ZDM3NTRiOGVlMDRiMjdmZDI2M2ViOGQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXo3cDB6eDJuZm44d3IyYXhhMnQzbXN5a2ZsYXljbHQzNW10ZHh6cmg5Il19fSx7InZhbHVlIjowLjE1ODY5NjI0LCJuIjo2NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGI4MzRkNWU5MTZmODRhNjIyZDgyM2Y3ODBmMmEzMjQ0ODhlMGRmYWEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRiODM0ZDVlOTE2Zjg0YTYyMmQ4MjNmNzgwZjJhMzI0NDg4ZTBkZmFhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6dXJmNDBmem11eTVjM2RzZ2xoc3JlMnhmemczY3hsNGdsbmx2ZHhkNyJdfX0seyJ2YWx1ZSI6MC4xNTk0MzY5NywibiI6NjYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3YzcyYTZkOTdmNjI0NDUxZGRiZGRhOTcxMzAyY2VlMWUxY2MxMmU4IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0N2M3MmE2ZDk3ZjYyNDQ1MWRkYmRkYTk3MTMwMmNlZTFlMWNjMTJlODg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDc4OWZrZTBhM3lnNXdhaGhkZnd5Y3plbXM3cm5xamFxd3djNnJ0bWsiXX19LHsidmFsdWUiOjAuMTY4MjY0NzgsIm4iOjY3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODk2MDUwYzc4YmQyZjFjODJkNzE3YmIzZTFlM2MwZWI2MjMwYzRiNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg5NjA1MGM3OGJkMmYxYzgyZDcxN2JiM2UxZTNjMGViNjIzMGM0YjY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp5a3E1eDgzMGYwcmpwZHc5YW04YzByY3I0a3l2eHlrY2p4Z2MyZjkyIl19fSx7InZhbHVlIjowLjE3MjYxMTQ4LCJuIjo2OCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGZiMjk4NjJlYWU5YTQxMWQ5MWNhZTFmN2I5ZDNhYzBmYmVmYTc5NTggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmYjI5ODYyZWFlOWE0MTFkOTFjYWUxZjdiOWQzYWMwZmJlZmE3OTU4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyYWpucDN3NDZkeXo4djNldHNsMHd3bjRzOG1hN25ldHE0cHRneWpkMyJdfX0seyJ2YWx1ZSI6MC4yMjk3ODkxNywibiI6NjksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA4MjJmM2Q0NTVhYzA2ODYyYmViZDMyZjM5ZTJmNmE3ODFjODYxM2JkIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ODIyZjNkNDU1YWMwNjg2MmJlYmQzMmYzOWUyZjZhNzgxYzg2MTNiZDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenB6NzAyOXR0cXhzYzQ3aDVlMDg4MzBkZnVwZXBzbmg1YTdmY3o0bGoiXX19LHsidmFsdWUiOjAuMjM0MjA1MSwibiI6NzAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBkNmIwZjNkMDMwMWIzOGRiNzEzZDAxYzkwZjBiNmUxZTM3NzI3M2U4IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZDZiMGYzZDAzMDFiMzhkYjcxM2QwMWM5MGYwYjZlMWUzNzcyNzNlODg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcnR0cHU3c3hxZG4za20zODVxdWpyY3RkYzByd3VubmFxcHJyY3Nhc3EiXX19LHsidmFsdWUiOjAuMjQxMzM3NzYsIm4iOjcxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTg3NjAwMzkyYjhiMzk0ODA5MWU0YzRkZWY0MmYwZmEyNzZjMGE4MiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDk4NzYwMDM5MmI4YjM5NDgwOTFlNGM0ZGVmNDJmMGZhMjc2YzBhODI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp2OHZxcGU5dzluampxZnJleHltbTZ6N3JhendtcTJzZ3FkeHlqdTB3Il19fSx7InZhbHVlIjowLjI0MTM2ODk5LCJuIjo3Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGEzNGYwYjVhNTM3YWIxNTE1Y2ViODhkOTM4ZGI0YTBiYzY2OTdhOWEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhMzRmMGI1YTUzN2FiMTUxNWNlYjg4ZDkzOGRiNGEwYmM2Njk3YTlhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6MzU3ejY2MmRhdHo1MnVhd3lkand4bWZnOXV2NnQ2bmdkMzNuZ21oeCJdfX0seyJ2YWx1ZSI6MC4yNDg0MTg3NCwibiI6NzMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2NTkyYzZjMjYwZmMzNzQ3ODBhZjMyMTJhOGYwODdlM2RiM2EwZTgwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NjU5MmM2YzI2MGZjMzc0NzgwYWYzMjEyYThmMDg3ZTNkYjNhMGU4MDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGplOTNrenZyN3J3M3VxNHVlcDkyOHNzbDNha3dzd3NxMmF4YXlwODMiXX19LHsidmFsdWUiOjAuMjU2MTEyOTIsIm4iOjc0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgM2FjMDZiOGU2M2ZmOGE1ZTRjODA4NmE2OTA0ZmE4MDBlYmZkZjFhMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDNhYzA2YjhlNjNmZjhhNWU0YzgwODZhNjkwNGZhODAwZWJmZGYxYTE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFhdnE2dXd2MGxjNWhqdnN6cjJkeXowNHFxd2hsMDM1eWczc21ydXN4Il19fSx7InZhbHVlIjowLjI1NzUzOTE1LCJuIjo3NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDA5OWNkMGMyZWZiZDE0YzVkNGQ5YTgxYWY3NzU3ZWQ1ZjllMzZiMGUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwOTljZDBjMmVmYmQxNGM1ZDRkOWE4MWFmNzc1N2VkNWY5ZTM2YjBlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxeWVlNXh6YTc3M2YzdzVteDVwNGFtNDBtMmxuY210cGNhdXFoMDk3cyJdfX0seyJ2YWx1ZSI6MC4yNjQ2MDQ2MSwibiI6NzYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2ZTBlY2QyZjY4MWRmZTMxZjQ3MTI4NmIxY2NmMDI5ZGU4ODE1Y2IwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NmUwZWNkMmY2ODFkZmUzMWY0NzEyODZiMWNjZjAyOWRlODgxNWNiMDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGhxYW5mMGRxd2x1djA1d3k1eGs4eDBxMnc3M3EydWtxc3BmOHY1eTciXX19LHsidmFsdWUiOjAuMjY4MTczNjIsIm4iOjc3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDBjZWZkNWVjOWM2NzRjNjliZWM3OWY4NGM3ZWY5MmFhNGU2YWJjOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQwY2VmZDVlYzljNjc0YzY5YmVjNzlmODRjN2VmOTJhYTRlNmFiYzg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJndmFsMjdlOHI4ZjM1bWEzdWxzbnI3bHk0MmZlNHRlcXY2cnRldHpnIl19fSx7InZhbHVlIjowLjI4NjkyNTE5LCJuIjo3OCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDVhMzQ1MjI1NjMzZjNkZDVkNzRmYWFiYzZlNjk0YTFlMDZmZGI5YTEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1YTM0NTIyNTYzM2YzZGQ1ZDc0ZmFhYmM2ZTY5NGExZTA2ZmRiOWExODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwZHJnNTM5dnZsbm00d2hmNzR0Y21uZmZnMHFkbGRlNXl6dWR2bDdhMCJdfX0seyJ2YWx1ZSI6MC4zMDQyMTgwNywibiI6NzksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBjYTE1NzNkYzBhZDQzYjI1NGM0NGI0YzhlODc3OTdjMzcyMDM0MmZhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0Y2ExNTczZGMwYWQ0M2IyNTRjNDRiNGM4ZTg3Nzk3YzM3MjAzNDJmYTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcjlwMnU3dXB0MnJrZjJ2Z2o2djM2cmhqbHBoeXE2emxnYXRtbWZmcm0iXX19LHsidmFsdWUiOjAuMzI5OTk1NzUsIm4iOjgwLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTc4ZWIwZGY2OTczMGJkNzJlNmZjNDEzOGQxNDdkM2JkOTBlNDgyYSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU3OGViMGRmNjk3MzBiZDcyZTZmYzQxMzhkMTQ3ZDNiZDkwZTQ4MmE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB0Y2F2eGxkOWVzaDRld2RsenA4cmc1MDVhYWpyamc5ZzZ1eHlta3h1Il19fSx7InZhbHVlIjowLjM3ODcxODc4LCJuIjo4MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDYyNDgwZDc4NzM5MmFlZTA1NjhlMDU2MDYxMGQ4ZjdhNzNjY2NjN2YgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2MjQ4MGQ3ODczOTJhZWUwNTY4ZTA1NjA2MTBkOGY3YTczY2NjYzdmODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwM3lzcnRjd3dmMmFjemszY3prcWNnZDNhYTg4bnh2MHVlbXozc3c2NyJdfX0seyJ2YWx1ZSI6MC40MDU2OTA5LCJuIjo4Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDk1MTFjZTVkMjNkMjczZDcwNmE0ZjczZjlhYzMwYTk1OGYwZjgxZmIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5NTExY2U1ZDIzZDI3M2Q3MDZhNGY3M2Y5YWMzMGE5NThmMGY4MWZiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6MjNybmpheTBmODg0Y3g1bm1ubHhrcnAyMmM3cnVwbHZoenUweHozaiJdfX0seyJ2YWx1ZSI6MC40Mjg2OTM2NywibiI6ODMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBkYzIwYTBlNjc3MmJlNTEzY2I1NzY5ZmY3MWNiMGVlNmNjOGMyZWJhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZGMyMGEwZTY3NzJiZTUxM2NiNTc2OWZmNzFjYjBlZTZjYzhjMmViYTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcnd6cGc4eHd1NDcyeTd0MmE1bDd1d3RwbW52ZXJwd2hnZ3J1NWM3anEiXX19LHsidmFsdWUiOjAuNDM3NjMzMTIsIm4iOjg0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNmZjM2RmOGY5N2ExYmY1NTdiMDQyNmU4NmI1ZmFiN2QxYTBlOGRlMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDZmYzNkZjhmOTdhMWJmNTU3YjA0MjZlODZiNWZhYjdkMWEwZThkZTE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBodThodTBqN3NtNzR0bXFzbndzNjZsNGQ3MzVyNWR1eTV3cTdsdHR4Il19fSx7InZhbHVlIjowLjU3NjIyMzYsIm4iOjg1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZGQ2ZTY5ZDhlZWZkZTFjMzQ5MmJiYzIzMzJmNzhkODVjODUyMTRjNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGRkNmU2OWQ4ZWVmZGUxYzM0OTJiYmMyMzMyZjc4ZDg1Yzg1MjE0YzY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ3a3U2d2NhbTc3cnM2Zjl3N3p4dmhoM2t6dXM1czVjY2o3c3RrbGNxIl19fV0sImZlZSI6MC4wMDAxMTQ4LCJoZXgiOiIwMTAwMDAwMDNjNTNkZDlmYzBlNTdhNmQyOWE2NzhlMTVmODViNzVhZTUzN2NkZmM0YzBiMDNjM2MwZjBlYWE1MDQzOGUzYzAwZDAwMDAwMDAwNjQ0MWJhYjQ3YWNlYWQzYzczZTliMmNkOTVkMjc4ZWZjYmU3ZTQzYWViOWE1NzBjM2QyMDNlODFmMTk1MDk4YTc1ODllYzVmNzA3Mjg1Zjk1ZDhiNGY1MGVhNjIzYmZiZDEyODI5NDMyYzU2NmQ1NzgyMWI2ZjM1NjkzYjNiNWY2N2RmNDEyMTAyMDkxYzE1OTAyMTI4NjAyYzEyNGI2YjJlMDJjMjI3MTNiYjdkNGM2MTQzNTg1ZDQyZGZiZDg1MmY5YzZlMzRjY2ZmZmZmZmZmYjk3OWVkNjk3OTU5YTU1YTk1ZWE1MTQyMzUxZDI3M2I3MDU4N2RhZjNmYWI0N2ZjNTQ2ZWE4MmJkYTFjZjIxMDBjMDAwMDAwNjQ0MTFmNGQ4MTU4MmJlOWExMjlhYTNlODA2MjY3OGM1NTlkOWVlNzY0ZWFmYmI0YmRjNzJiOTBhZjhiMTVlZjc2MjA1ZTJmNGJmMDQ4ZTg2MmFjNWQyMWQxNzA1ZjFmODYxZGRmY2E1ZjliNDkwZjVkY2IxODMzNzMxMTE5MDcxNTIzNDEyMTAyZDM2ZjUzMjMzODZmNzYzM2E4ZjJhMzlmYjYwNjViNmY3MmJkZGUyNjIzZDQyYzcyZTU2ZGJmOWJjM2IyZDY5ZWZmZmZmZmZmZDg3ZGFjZjM4ZTYzM2FmYmUxNDY5N2FiYWFkZTMxNjIyMzY2ODU4YTdmMDlhZjRjMzc3ZWRiMjkwZmRlM2MxYzE5MDAwMDAwNjQ0MTcyMjk1Y2M4ZTkyMmMwYTZjMWRhODdjZjJlMmJkYThlMmEzZmNiZmY2NDliMjdjMmFjYTZmZmMyZWE1NWFkNTA2MTlmZTk5MTQ3MmM1Njg2OGNhYmQ0YzU3MWU4MTJhNzI2YmRjYWFlMjYzYmRjY2VjNjE1OWJhY2VjM2FhNzk5NDEyMTAzZTdlNTIxYzVjZmFiNDI2Njc0NDZiNmFkZTRiNjRhZGQwODUyYTI3YWM0OWFiNTIwMjEzODU2OGMzY2Y5MmNkMGZmZmZmZmZmODhjMmUxZTc1ZDMwZTBhZjFiNmI3ZWU3MGVhNTJiODA2NzA1MTcxMjYzN2NmNTI3Y2MyMTMxYjUwZTA1MjMyZjA5MDAwMDAwNjQ0MTNmOTNiZDU1YzY4NzZiYTc2MzgyMDI5MzIyZDFkZjgyNDRiM2JkM2JjZTZhOGJkOGUzNDg0MDY5M2YwM2UwNTljYTQyN2IyZmNhZjUxYTZmOGVmNzdlNTAyMjZiNzNiYjA0N2RmNzZmMDhlOTIyZDNkZGRjODMzZTUxMzZiZDVjNDEyMTAzZjM3NjU3YWJiZjg2OWI2YWY1NjM0ODkyMDFkM2RkNzg5NzFmOWMwMDM3MWM1NmZiZjk2MWMxNzU1YjA3MzI0NmZmZmZmZmZmNjAyMTliNjQzOThjZDVkZDI2NmIzYTAwNDVmN2ZmMjg3NDI4N2ZiOTJhZWJkY2ZjOGY3NGY5NDFkMmVjNTAzMTAxMDAwMDAwNjQ0MTE5ZGFiYWU2Yzg2NTlhNzcwZWQwZWIyY2Q2MjU5NGQzY2I2ZTZiMjVlMWQyNjljNDdhY2MzNTc5MzAwN2U5ZTc1ZmEwODBiN2VhZDg3YTc1OWM1N2ZhM2I2ZDdjZjU0ZjQ3NjRiM2JhM2RlZDAyYTFiZjgzNDU2NDNkYjg1NDEzNDEyMTAzMjJlNzJhODNiMTI2ZDI5NTM1ZWRiYjlkNjQ1YjE0ZmJmNmE4MTBhMWEyNDEwN2QwOGRiZTZmNmU2YTJmYmYzMWZmZmZmZmZmNzU1ZTVmNzYxMjU1ZDIyMzdlMDQwNWMwZTgzMjk1YzRmNTQ0NTQ0ODFjZmZiZWRiYjUxNGJhZjgwODVlZmIzODAzMDAwMDAwNjQ0MTdlOGI1OGJlNzI3MTA1N2I0NDVmOTkzMTI0NTAxMTliZTgzOWQ5NGM0OGU1MjA4ZGMzZTk4MGU1YzNiOTk2MzIxZjIzYmQ3NWY3YjUyZjVhZmM2MWI1Nzg4MDQwODM1OGY1MDViMjI2ZDRjYWQ4ZTU4YWQ0ODZmMGQwZDlmMzE5NDEyMTAyYTBjYzQ0MzE4NmM3NTBlMTk1MjRjYWUxNWFmZjM0M2JkNWQ5ZTcxNDAyZTkwZDY5YWMxZWNlNjhlYTBjOWJjZmZmZmZmZmZmNzU1ZTVmNzYxMjU1ZDIyMzdlMDQwNWMwZTgzMjk1YzRmNTQ0NTQ0ODFjZmZiZWRiYjUxNGJhZjgwODVlZmIzODE5MDAwMDAwNjQ0MTUwNzBmMDc5OWNiOTI0NGY2MmEwYWIxMzU5OWEwY2FiM2M0NTJiYjdkMDc4YmU4MDNmYTY0YzEwNDg5NDI4YjhiZmYyZTM4Nzk0MjdkMWUyZTBmNjJkZjUwYzQyOWQyNDU0NmFhZjRlNTg4NWEzZDQ1NTExYzYwYzFkMWY2ZmIxNDEyMTAzZGMxNDhhZmJjOTczNTE2MGQyYWUyNTFkNzk4ZjdlMGIwYjZmZTlkNmM3ZDgxNGI3MjJjNjVjNmY4ZmExZmUwY2ZmZmZmZmZmNzU1ZTVmNzYxMjU1ZDIyMzdlMDQwNWMwZTgzMjk1YzRmNTQ0NTQ0ODFjZmZiZWRiYjUxNGJhZjgwODVlZmIzODFkMDAwMDAwNjQ0MTkxMTI5MDRhNzcxOGM2ODA0NWM0YTQzNmRjYjgxZGFiODgzYmU2N2FjZmVkNGM1OWE1YWYzNDU4OTBjYmU0Mzg2NTY5MDk2OTgyMTJkNGE5ZTgyYzBhMTg1MzBlNTkyNGViZjdiNTk2ZjBiODBhZmU2NDI0OWRmMGY0MDFkZjNkNDEyMTAyZDgzZjJkZTMxZWIzZTQ4YjYzZjlmNDY1ZjU0YTExMDI3MDA2YmQyNDhkYzIzYTgwNzUwYTg3YzgyMGIzMWFkZGZmZmZmZmZmNzU1ZTVmNzYxMjU1ZDIyMzdlMDQwNWMwZTgzMjk1YzRmNTQ0NTQ0ODFjZmZiZWRiYjUxNGJhZjgwODVlZmIzODI4MDAwMDAwNjQ0MWJmOWI2YWRjNDJiNDUxYzFkOWNiNDVmMDhhYzdiMDQ5ZmQ3NjgwNjk4MDk2M2QyMDJjYzE5YmMzNTlhNjBlMmZkNWViY2EzYmYwNDAzNjY5N2Y1YWJjNDgyZjEzZTc0ZWQ1NWZhNDU3MzQwM2Y3ZjkwYTRjMjRjZjM3M2M0N2JlNDEyMTAzZmM3NmYyODAzZmY0ZWYwMmQwODEwNjNiYTQ0ZGJlZDAxZjViZDBmMzU1NjBlYTA2YWVmMTE1ZWU4MTE1MGE3N2ZmZmZmZmZmNzU1ZTVmNzYxMjU1ZDIyMzdlMDQwNWMwZTgzMjk1YzRmNTQ0NTQ0ODFjZmZiZWRiYjUxNGJhZjgwODVlZmIzODI5MDAwMDAwNjQ0MTE1OTk4MWYzMjRjMTBkZDQ1YzJhYjExZDkyYmQxN2Y0OTRkNDY5MTUwZGQ4N2RkNmExMjY0YmZhOWZkZDhmMDFjZmYwNTJiNTY2MzhkMmRkZTU1OWUwZDM4ZjczNjAzNmRkNzZmNjdlNGFjODk0OWUyMmE2YjkxODA5OGQzY2Q1NDEyMTAzMmQ0YTBkZjQ2M2Q3ZWY2N2Y0ZDE2YzhiMWQwNjM5MTVhODk3ZjZlNGEyZGIyMjAxYjNmZjkzNDg3YWM2NTc0ZmZmZmZmZmZmODYwMzY0ZjlmYmQ2MGMwMjkzOGQ2MDY1NGZiOWJiZmY2OWE2YmZlN2YwY2UyMjYwYzgwYWQ2OTNlYWU3MzIzZTNlMDAwMDAwNjQ0MWFmZjY5ZjZjYmRhYmIwNTAyZjViZTY0OTgwZjcwNjRlODI3MGE0ZmJjZTFiZjhmY2YxM2E5YTU2ZmVlZjgwNGRhODE0NjkwNWE5NDExZTk0MzBlNmZhYzE4NTc1ZDllODE5OGM5OGRjMWQwYzZiMzRjMGFiOTM5MjUyN2NkNTcxNDEyMTAyMTFhYzdlOGQ1MTZiZTZkMGJkMzMzNGQzZGI5NTU2MGFiZGM5YTBkZWI3NTZmY2QwMjhmMDA5ZmU2ZWNkYjc5ZGZmZmZmZmZmY2FkMDdjZmY3NzhkODFiMjU0NTRiODU4NTk5N2YyYTdmYzg0N2IxYjUzYmIxMTgyOGQ2ZjhhY2YwYTJhMDEzZjFmMDAwMDAwNjQ0MWM3OTg2NjhhYzllMDQ3NmNmZGJkY2Q0ZTg1Y2I5ZmI2NDJiZjllOWRlZWE2YmIyNjNjZTI1YWVmODY0YzgyZGE4MGRhYzk3NGYyODViZTViNTA1MmYzMDYwNmQwMWRhZThhMTUyOWI1ZjJkYzA0NmQzMTViZTMwMWQ2ODUzOTEwNDEyMTAzMGNmYWI1NDQ2YTQ4MmUzM2I0ZGQyMjVlNDRjZTBiZmUwMTQ5NGFiZDcyMThmOTVjNTgwMzQ5NjcxNGZjOGM0ZmZmZmZmZmZmY2FkMDdjZmY3NzhkODFiMjU0NTRiODU4NTk5N2YyYTdmYzg0N2IxYjUzYmIxMTgyOGQ2ZjhhY2YwYTJhMDEzZjI0MDAwMDAwNjQ0MTRlZTgwYTE2OGIwZDlkMGJlMDMxMzQ5NzdmOGM0MWI1ZjNkM2E0MGU4ZjlkMjhiMWMwZWJiZmEwMzgyMzc0YzY2NmQ4MWM5NWQ0ZWQ3YmZkZGMzYzc3NDBjNjZkN2ZkMjhiYTQzMTRjNzJjMmMxMjFmYTFjNWMwNzRlYzg2MzkyNDEyMTAzN2QzODMwMTEwOTM3MTIyN2RmNTQ5MjliZGVmMTRjZTZiM2RmM2MwNjM5YWRjNTE4Y2Y5NDExNWVkZWE2ZTRmNmZmZmZmZmZmY2FkMDdjZmY3NzhkODFiMjU0NTRiODU4NTk5N2YyYTdmYzg0N2IxYjUzYmIxMTgyOGQ2ZjhhY2YwYTJhMDEzZjI1MDAwMDAwNjQ0MWE2ZDZkZDNkM2RjMjliMThiOTg0OGI0YzliNDM3NDM0MzFjMGMxZTc5OGI0NjdlNzVjNGM0MjZkNDg3MGQ3MTc4MzAxNjkxY2Y3YzUzNWE5OTRkMzZlNjFkZmNiOTRkNjkzOTQwOGJjNmU4NzdlNjkwYzQyNTg5NTA1M2YxMjU2NDEyMTAyZjI4NDhhNmQzNTkyYzA0ZjYzZTM0OTZiZmYyYjkxOWQwYjJkODg2NzI2MTZmN2Y4OTI4OTNkYTkwZmQ5YzQyYWZmZmZmZmZmY2FkMDdjZmY3NzhkODFiMjU0NTRiODU4NTk5N2YyYTdmYzg0N2IxYjUzYmIxMTgyOGQ2ZjhhY2YwYTJhMDEzZjJlMDAwMDAwNjQ0MWM4YTVlYTA3ZWU2MGM5ZmMwZWE4ZDY5NzBhYjBlZTlmMmIzNDcyY2IyYjQ1ZWIyMWFhYWQ3MTI1NzZhNmU4ZGJjOTkwYTkwYTMwMjVmMmIyZTNhYTdjYzYxNjQ0OGFlMjlmNzZkZTY5NTMxMzJkNDNiYzZiNThlMTZiNGFmMDQzNDEyMTAzOTAyODcyYTQ1YWY3NmI5MDVkYjU1Y2VhMTJmZjVjOTU5MjkzY2JhYWMwNDJiYjA3MWNkNzk0NWEyN2M0NjQ4Y2ZmZmZmZmZmODgzOGRkNzkzOWUzMmQ5NmU0Yzc4YmY2OTc1OTk1YzMzOTQ5ZjBhYjU3MzBiYWIzNGVhMzFiMTgwOTUyZmI0NTEwMDAwMDAwNjQ0MTNlZGI5ZTg4OTk2ODZkMDU2NjA3ZjJjMWRmYzFlOWQ2ODAyZTYwNTY2NjU2MzIxMmEwMjY4NzZkYTM0MzYzM2U4NGY1MmYyMDJkNmRjNmIzNmUxMjY5YzkxZThlYzkyMjY5ZjhmNmE4NTFmZWE1MTUzN2Y5MmMwYWJlODI5NjY0NDEyMTAyYTNlNDhiNzFhYTA4ZTdiN2Q4YTkxMTUwODMzOTJjMmEwODkzYjM0Y2FiMDg3OTlkNDhjM2U1ZWEzMDcyNTNkMGZmZmZmZmZmODgzOGRkNzkzOWUzMmQ5NmU0Yzc4YmY2OTc1OTk1YzMzOTQ5ZjBhYjU3MzBiYWIzNGVhMzFiMTgwOTUyZmI0NTExMDAwMDAwNjQ0MTY3OTM0YTc1ZWFiNGFjY2VkN2QwNDk4OGFlNzcyZjhhMzFkYWIyZjc3ZjNjNGY1ZWE2NTFhYTEzMTFmOWIxYzRkMjU4MjgyMDk1OWExY2JlZjg0MzEyOWIwY2E0OTQ1MjI0N2YyYjM3Y2E4OTljNGEzOTg1YzliZDFhYTYxNzI1NDEyMTAzNzgyNWY3Mzk0NWFhODAwYWU1NTYxNGUyMGU1YWE0MjM1NzQwOTk5MjcwZjczMjk2YjYwYmI4MTM4NGI1OTAyOGZmZmZmZmZmODgzOGRkNzkzOWUzMmQ5NmU0Yzc4YmY2OTc1OTk1YzMzOTQ5ZjBhYjU3MzBiYWIzNGVhMzFiMTgwOTUyZmI0NTJjMDAwMDAwNjQ0MTliYjcxYTJlMjZmMjI0MzU1Mzg4MjQ5N2I3MzdkZWNiZDc1ODNjYjU4ZjQ3NjY3YTVhMGYxNTFhYjJlOWY1ODJkMGEzNTU2MTcwMTc3ZjIzYTk4MWM3MTM5NTJjZTcyYzVkMmFlMzZjNzliNDc0Nzk2NWE5ODYxOGI2OGUyYjYwNDEyMTAzYjYxM2NmZTdjN2E5NTk4ZjI5NmExMjhkZDAxZDAxY2I3OGNiNDdhZmRkN2Y4ODEzYTA2ZTgxZGVjODgyNDM4N2ZmZmZmZmZmODgzOGRkNzkzOWUzMmQ5NmU0Yzc4YmY2OTc1OTk1YzMzOTQ5ZjBhYjU3MzBiYWIzNGVhMzFiMTgwOTUyZmI0NTMwMDAwMDAwNjQ0MWIyYmRiYzhiMGRlZGYzOGQyN2VhOWJiOWE5NWI2ZmQ0NjExYWYxNDJjZjNmODM5ZmVjZmQ1OTFkN2VlOWY3Y2Y4NTEwMjNmYTUwNDczYWNlOWNkNTc3MTc2NmYxZmZlNGMwOWM5ZTE2NTY5MTk3YTg4ODE3MTY1ZTI5M2NiYzQ5NDEyMTAzZTFlOGE0ZDQxOWE4N2FjYmRlMGJmYjAzYzY5YTljNTA2NGI2MjY2NzIxNTgyY2M5MWI0YmE2MjEzYjE2ODY2N2ZmZmZmZmZmMDI2ZmE5ZGNjODVjMTVjMWMxZWEwYjhhYTEzZDNiYjk3YTFhZDRjNWY2YTg5Y2JiYmJmNTVlYmNlZTUzYjE1ZTFlMDAwMDAwNjQ0MWRiYzcxMjIwZmU1ZjRhZDg1MTEwZDcxNzY2NzViOWUyZGVmM2M0ODBkYzFmNzAxMTI5OTc3Yzk4ODFiZjNkN2NmY2YzYTA5YTA2ZDExMTNjMmViNDQ5YWUxMTVjZGFlYTBlOTNlZGZhNjM0NGFlYjU0NDQ1M2M3MjZhODQ1YTBmNDEyMTAzY2ZhMWY5YjViMDk2NDlhMDc1MTg4OGU1MjhiNDcyNTlhNDA2ZjkwYjQwZmJmNGNiYzJjOTUyYTBhZjg4ZDY4M2ZmZmZmZmZmMDI2ZmE5ZGNjODVjMTVjMWMxZWEwYjhhYTEzZDNiYjk3YTFhZDRjNWY2YTg5Y2JiYmJmNTVlYmNlZTUzYjE1ZTJjMDAwMDAwNjQ0MTYzM2QwZTAyZWFmZTkxN2UzYjkwMDczZTViNTM0OWQzZDY4ODg2ODMxMjRjMWJjNThmOWQ5ZmY3NjZmZjEwOTFiNjkwZGI2Mzc2OWRhYjI1NjVlNzQ3MWUxNGQxMWJkNjgyNDBiNTIzODZhMGZlMzI3N2RiY2I0Yzk2OTM2ZDRjNDEyMTAzNjc5ZTgyNzFjNmI0NjY5MjZkMzdmNjlhMzE3N2I0OWE2NWY4ZDI3MzFjOThkNDEwNzFkZGE2OTQ0OTMxOTQzN2ZmZmZmZmZmY2NkNzAzMWE5NGFkYTY4YjYxZTAxYjE5NjZiNjk2YTNhZTYzMzc1YTNkMjYyYzA1YjFiODc2NmUwYjA1MDM2YjAzMDAwMDAwNjQ0MTA0ZTg4YjU2ZmU1MDI3ZTExMzcwZGNiZTE1YjNmY2Q1YTQ2YmExYTIxMTdiZjExMzBmNmQ1ZDY1NTE3NTk5Njk0YmIxMjIyNmI5ZjVhNDIyYzA4NjdiNTZiZDljOWExMWQ0Yzg3NTQzZjJkMGQ4OTFlMTJiZDhkMjY3ODI0MGYyNDEyMTAzYzQ1MWM3YWZkM2E3NjVmNzY4ZjhhMmYyN2YxZWFmYWFlZGE3NjE2ZTg5MDZlY2Y0YzEwZGU3Y2IyNzc5YTVjM2ZmZmZmZmZmY2NkNzAzMWE5NGFkYTY4YjYxZTAxYjE5NjZiNjk2YTNhZTYzMzc1YTNkMjYyYzA1YjFiODc2NmUwYjA1MDM2YjBlMDAwMDAwNjQ0MTNjYTY3YTZlMzFmN2QwZmJhYWI3MWNiMzM4MzU5MjAzYWJjZDcyMWQ4M2U1OWUyMDhkOTY4YzFjM2Y4M2M2ZTk3MDQwYmJmZTg3M2QyOGU1ZGRjMmZlZDI5YjU0MmQ2YzZiM2ZmYjg5NWNiOTIzNjE0MWM2OWMzNzBkYWZkYjQ4NDEyMTAyNGY5MmRhYTAzZTk3MDY4NWIxZGM5ZTJiYmUwYWU3M2ZhMWVlZDcyNzNjNGUyOWY5NTZlYmUwMjg3YmEwM2QzNmZmZmZmZmZmY2NkNzAzMWE5NGFkYTY4YjYxZTAxYjE5NjZiNjk2YTNhZTYzMzc1YTNkMjYyYzA1YjFiODc2NmUwYjA1MDM2YjFkMDAwMDAwNjQ0MWUwMzk3MGJlZGQwZGJhM2Q2ZjVjNWU1MzUyNmE0NTllZTg1NmQwMzFlZDExN2IzNTAzNjcyYmY1NTAxOWNlMGU2YjAyNmI5YWZhOWE2ZDdmMWZjOWE4NjEyY2ZkN2QyMTQwYzVjNzVkOTZmMTRmNmE3NGVjNGZiMTk1YTk1NDg4NDEyMTAzZjBjMDZhYjBmZDk1MTViOThjODc3YWVhMGU5OWE1ODZlNWNkMzc1NTUzOTJmODI4ODdhZjgwYTk4YjQ1MTBiNmZmZmZmZmZmY2NkNzAzMWE5NGFkYTY4YjYxZTAxYjE5NjZiNjk2YTNhZTYzMzc1YTNkMjYyYzA1YjFiODc2NmUwYjA1MDM2YjI4MDAwMDAwNjQ0MTA5ZWIwNjRkZjEwOGRhZDkxNWRmYzZjNmIxYzY5OTU4YTQyNzYwMDhjZWQxM2M2YTY5Zjc0YmE2OGU1M2E1ZDI2Njc3MWI0MWUwY2M4YTgxMzk3MmM2ODM5ZmE4YWM3YTUzZmI1M2U0NjgyZGM3NDkwMTJjZjE2NTYwNWMyN2YwNDEyMTAzNzIzOTU3ZDBhMDU5MzZjYzVhOGU2MjBiYzMzMjE0MTJjYTRhZGU5Nzc1Y2JhNzllNTUzNzU1NjllMGQ5ZGRhZWZmZmZmZmZmYjZkZWNiNzZlYjhjMTZkNmQ3ZjkwNTk2N2U5YTRhY2RlYTkyMzE3ZGFkNjU5YThjOTQ1YjkwZGY0NzQ4ODI2YzAxMDAwMDAwNjQ0MTc0YzU4MDM2OTVlYmRlOWJmMDhiNWYyY2FkOGY3NzM5MTlhMjBlNjRmYmVjOWE2NTAyZWY4Y2NjY2U3NDBlOWZlOTY0ZDBlYThlMWMzMjRlZDVhY2Q3ZjVhN2I2ZjI5NjU0ZWJkOWVkNDM5MDYxMjY1OWY1MzVlYWUyYjk4NjFhNDEyMTAyY2FjZGE1ZjFhMjJjMzljY2Q3ZGRkMTJhNjZhY2QwZGIwZjRkOTY5NzNhZDMyMWM4ZWFjZDAzMTQwYzUzODEwY2ZmZmZmZmZmMmJiMmI2ZjMwM2VlOGM3YTE2MzNkMDJiMTVmMDYzNTNjNDFjNDg4M2FhMzE1M2Y0MTA5NzRjNjFkZmM1ODQ2ZjEwMDAwMDAwNjQ0MTI2NzFjYjNmZGNjNzUzZTQ3NWExN2U0YzhiNjY5NTMyYTczYzg2NmRhYWRjYTRjOTUyZTAzNTVkMmIwOWMzYzlhNjFlZGJkODRiYjQ5NzRmMzk2YjMwZGRiZDc4ODcwYWU0YmNjY2Q2MzBhOWRkYThlNjRiZGQ2MTk5OThhZDk1NDEyMTAyYmZkZTNiODJmYzRiZGRhMzM3YjkxODUyMmM3MGNhNWY2NjU0MWMwY2E4NzU1YWY4MzgwNmQ4YzU5M2ZjZDZlNmZmZmZmZmZmMmJiMmI2ZjMwM2VlOGM3YTE2MzNkMDJiMTVmMDYzNTNjNDFjNDg4M2FhMzE1M2Y0MTA5NzRjNjFkZmM1ODQ2ZjIwMDAwMDAwNjQ0MTE4MDA3MGYyYTFlMGJlYjk5MDRjYjhiMzUyZjIwMzE2NjA2YjRlNjcyOTcwMmNmYTJiNmU4MTJlNjA0NjM4MmM3NTliZDEyYWE2MDgyOTE4YWQwOTQyYmVjMjRhMzFiMTQ0ZmJjZjI2ODdkMzcyMzM1OGIyNTQ1OWVmY2UzZjEwNDEyMTAzOWI0MTZiY2QzZTBiNDM1OTI1YTlkYTQzZjQyYTg3NWFiYmRlNGRhNzdmMmM1ZTg1Mzk4MjcwM2YxMWUwYWU0NmZmZmZmZmZmMmJiMmI2ZjMwM2VlOGM3YTE2MzNkMDJiMTVmMDYzNTNjNDFjNDg4M2FhMzE1M2Y0MTA5NzRjNjFkZmM1ODQ2ZjJhMDAwMDAwNjQ0MTAyZDU4MDE0YWE3NWE3YzZiNjFlNGI3OWE1YTAxZTIzNTIxYTU4MGUxNjc4Y2Y0MTUwYTAzYzZiMTQ1NDQ0ZTRhNDViYzUzYmNlNmM0YmY4NmY5N2JmYzFhZDNiZTAzMjliZjkxOGI3MDQ5YTU2ZWFiYmQ1ZTQyZDc5NzI5NzkwNDEyMTAyNGYzN2JmYmViNjE1NTRmNzZkYjIzYjEwNWVhOWIyYjdmZWQ0MjhlZmJiYjAyYWUzNmE3YzI1M2ZjYzhiNDU5OGZmZmZmZmZmNzI4YWMwNDdjNWZkZTQxY2QzZDk2ZDRjYzI3ZTFlMWQzM2I4ZGUzZGJmYWYzODI4OTRkOGRlYjgxMjJjYzY3ODAxMDAwMDAwNjQ0MTk4ZmY2YTU0NmNkZjFiMGMyOTJjMjM5ZWY4MzU3ZjQyM2QzNzVhYjg5NzliZjZjOWY2MGNmYmYyZmQyN2ZmMDZmMjE1OWFkZDIzNjZlOGZlOWQ4ZmQzZDFhYmRhODdiNWMzZDM5MGZhNTZjYTkwN2Y0MzM2YzAzZTJjM2NiMmUyNDEyMTAzNmQ3MGUwMDIwNTVkNDg1NzE0Njk2NGQwMGZmZTczNzI0YjJmN2MwNDExOGExZmU1Zjg5OWIwOGU0MzI4YzNhYmZmZmZmZmZmNTViZGYyNjc3YWQ5NDcwNTc4MTU2YTkyYTVkOTA3MWU4MjA1ZWM4YWY3YTNkNWUzMWIyN2VhMGM2NTIwNTc3YTE0MDAwMDAwNjQ0MWRlYTlhODMzNWQxMzIzNDRkMjhlZWM0YzY1MDE0ZjgyMzYwYzkzMmQyYzQxY2ZkMTRhYjU3YWFkZDMxYjRhMzAyZDZhN2ZhNDEwNjQ3NmNjY2U5MTJmNzNjOTZhYzI5NDJkNjA1NDFhNTk0OWE3NDk1ZWMyMjU3ZWUwYzljMjlmNDEyMTAyYzQ2NTI3M2RiZWZkYmNlMjcwMDRjYmZjMTFhZjNjMzQ0YzQ1NTU2YmI4OWEzN2NlNzc4YWY0NDBiNmRiZTNlZmZmZmZmZmZmOWYzMGI3ZTg0NGE0ZDcwOGRhYzkzYWU3MWI4MmUzODgwZWNlY2UzYThkMTZkODRmYjM2MjA3ZGQzZDY2MGI5MDE1MDAwMDAwNjQ0MTU1MWMyMzA5YmE0MjY3MWM4NDYwYWI3YjIzNmYyZmRmOGMyNDUwMGI4Nzg0YmIzZGVhMGM1NmRhN2JjNDc2MjcyYzZkNzc3N2E4OGE1ZTVhN2RjZTAwNjU0MDM3MDkxOTczODZhYjNlZmI3NWIyZDdmODBmZGYyY2E4ZThmYmE3NDEyMTAyOGY5MjU5MmVkNDNiMzZmYWNjNjc1YTVlZWVlMWE5OTNkODFlYWU5YjhhMGFmOTJkNmQxYzNjZmQwZTA3NmU4NGZmZmZmZmZmYzgyZjEyNWYxMjE1ZjE0YTY4N2FlZjgyNmI5OThhZWNlMGJlZTlmNDU5YjczMmIxN2Q3NTIxY2Q0YTFlZjg5ZjA4MDAwMDAwNjQ0MWRlYTI4OWE2NWIxYTg4YjNhZjdkNDNiZGQ4M2ExOGFlNWFlMTI5N2VmOThmNzQxOWRjOTYyNDQ5ZWViZThiMDQ3MDVmZDMzZmJjNTMwMGNiYjFmMTczODhlN2FlMjA4NmQwYmU1NzI5N2NlZTdjMjU0MDk5OTc2NTFkMDg1MmZlNDEyMTAzNDU4YWJmZGVhYTM3ZTEyYzQxYWU4YzI0NzliMDE5ZmQ1ZWExNjRjNjJkMzdmZTY3NzEwYmMyNTJkZjYyOGQxOGZmZmZmZmZmNmUxZGQ0MTExYWRjYTExNjkzY2I5N2ZmNjE1YjA1NDY0M2Y0NDQ0ZTlmMjE4MWFlNjQ0ZjE2ZTE5MmM5ZWNhZTA0MDAwMDAwNjQ0MTNmMGE1NGQzYTQxZGUwZTg5NDVhNmVkNjgxZmExYzY5OWJjNWVkZDUyMTJkMjI1YWI2ZGNhY2RjN2RkZDhiZjE1N2ZlYmI2MjRkOTQ1NjlmNmI2M2YwNzYxNWMwMzFlNzJiNTNhN2NiNDM0MWFjNGFmNGYwYzNkYmUxYmU2MTk2NDEyMTAzNTE0ZGU2N2YwYmNjN2Q4ZGFjOTZkYTU5MmIwYWMxNDI5NTA0YTg2OGE5YmUxMTI0YzdjMDQ2ZGQ2ZDhiYzEzNWZmZmZmZmZmNzA5ZGRjMzIyNWRjNWQwZmE2NzMwMzVmNDgzYjg4M2EzNGNjMDQzNmU4ZmNjZDhjMjYwZmEyYTJkMmZlMTdhZjA2MDAwMDAwNjQ0MWI5YzIxODgxNmJjMzc4MzI1NTFmMjgwNTYwZmFiMGVhYzdlYmI4ZjY3NmNmMDU3MDViMjQ1ZjIzOWFhNDJlNDIyODJhYTQ5MjM1NTMxZjA5NDczZmY1OGE1ZmFlNTEzMzI0ODBiMjA0OTY3ZDgzYmZiYmZhNGNjZGY5ZTgyZGU4NDEyMTAyMWE0MThjNDBkZWFhZjUyNTk1YmNkMjU3YjkwYTRlMWYwZDFlOGRjOGNkODI2NGE1MTA3NTAwOTYyYjRkMGUxNGZmZmZmZmZmNzA5ZGRjMzIyNWRjNWQwZmE2NzMwMzVmNDgzYjg4M2EzNGNjMDQzNmU4ZmNjZDhjMjYwZmEyYTJkMmZlMTdhZjIzMDAwMDAwNjQ0MWRiNzdjMGU2OGJjYWI0MWRhYTA2NmQ0MWFkZDZkMDA0M2Y0MWVjMTI5MzMwZDkxOWEwMmM2OWUwZTk2Yjg0OWY1Mzc2OWQ0MWMxYzMzMWRiZGRmNTdlODQ0ODRmM2IwZTJmZDI3YmQzNzllNWY0MmQxNDE1ZGFmMzY2NWNiNDA2NDEyMTAzMTA5Mjk1NTNkMGRkMTIzMGE2MmJjNzA0NTMwYzg1YjBlZjJmM2ViNWZiZjU2N2MyNGQ1NDBlMzFhMWY4MDBlZWZmZmZmZmZmNzA5ZGRjMzIyNWRjNWQwZmE2NzMwMzVmNDgzYjg4M2EzNGNjMDQzNmU4ZmNjZDhjMjYwZmEyYTJkMmZlMTdhZjJlMDAwMDAwNjQ0MTgzZjAwNDMxMjJmNmQwZDI2ZDI1MDM1ZWIwZTcyNDllYzRiYjRkMWE5NzllYTNlYTc2NjY4OGZiZDcxYThlNDE4ZmI2NWZmNWFmZGNiMGUxOTQ3MTdiMjExMzhlMDM2Y2JkMGU1OWEzNDdjMzRjZTgxNjA0NDI4MjFkNWJlOThjNDEyMTAyNzRlYmJkYzhkOWU3YzQzYzU2MDEzZTA3MGY5MjQ3MzBkMjcwY2NhMzY5YjkyYTYyNGUzOTk0NTdhNWIyNWQwMWZmZmZmZmZmNzA5ZGRjMzIyNWRjNWQwZmE2NzMwMzVmNDgzYjg4M2EzNGNjMDQzNmU4ZmNjZDhjMjYwZmEyYTJkMmZlMTdhZjMxMDAwMDAwNjQ0MTUzNjU5YWFmY2NjZDczMzAxY2JhZWYzYjQwNDc3MzRmOWI3NGNkZGZiZjM4YzFiMjU5YmNlODhlZDMxMWI5OGQ4YzcxZDY5YWE4OGE0MDIxNThjMjM2YjE5ZDg1YWRiZDBlZGM0YjBkNDkyYWU1OTBlY2U2ZDc2MWUyNDIyNzZjNDEyMTAzY2MzZTY1ZThmNjYyMTZkN2YxNjI3Mjk3NDA5N2ViOTFiODU3ZGNjN2QwY2UwNTYzMjJiNjAzYjFkNzQ1MzJkMWZmZmZmZmZmZDU1Njk3ODRhMDBkYTJjOWY5MjI1Mzk3OTIwOTViYzgwY2ZjOTE5OTE3YWRkNTZkNWYzOTRmYzc5YjUwYzBiNDI0MDAwMDAwNjQ0MTgxMWY1ZThlNjgyZTUzNDBjMTVmMzM5YmU5ZTQ5Mjc0ODE5MzkwY2UxMzZmYzc3YzEyMmYyNjY4NDJlYWI1NGExN2QxZjMwZmIwMzdjNWEzZTU5YzM5OWRiNjBkNGE1MzI5ZmYzNzBiMTVmOTRhZjAzNzQxYmU1MTQ5MTcwMzY5NDEyMTAyNGFlNzFmZTFjMTk3ODhlNTQzNWRlYmQ1NmFjZDE0ZmFjMmIxNDBhNjM1ZjNjMjcwNTM5ODg1ZjVkYWZhY2Y1MGZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzAxMDAwMDAwNjQ0MTUwZmZmNmNjMDk0MzQwOTllZWZlOGFkNWE2ZWExYzc2NzhkM2E5OWI5OWI0NzljNjEzMDI4OWYwZjBiNDZmYzk3ZmM4OTg2YWNmYjIxMDgxYzAwZjFjZjcxZGI5ZGQ5M2FkMWFhYjZkNDU3ZTE2ZTVmYzQ5ZDMxNDg0NGFkNjFlNDEyMTAzODZkYTI5OGE3NGQ1ODk1YTJmNjJlOGEwZmYxZGFlNjJmNGM5ZTQ5ZmI5YmUyYjM2MTA4ZDU1N2Y2NjRhNTM0ZGZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzA0MDAwMDAwNjQ0MWQ2MGNhOTc1YWJmNjQ0ODdkNmIxOTA4MDM0ZmM4NTU4MGVlMzNlNWM1N2ZjMDcyYWQ2YzE0M2I4ZTFiNTVmZjVmZjVlYWNjNzhhNTNlNjVhZGY2ZjI2MTBiMTcyZTRjNTMyMmJiYzkwNzFlYzllNzA5MzE3ZDIzY2Q2YzlmMjA0NDEyMTAzYTdjMTFlNTgwMjBkNjdlZDNiNWVjM2JlNjM3ZGMyNjdiMzkyODE1MTdlMWRkMGM0OGU5ODNkOTg4MTU3NjI5MWZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzEzMDAwMDAwNjQ0MTdlOTAyYmFhZTUzN2VhMzFmYjI0MTE2NGZkOTM1YWI5NGI1OWQ3MWY1MjBjMzI1ZDM2ZDNhZWE4NTY1ZTdiYzk1Mzc4NTVhMGVjODYyMWE4NTVlZmZlYmI5NGRmZDVmMDVhNTY0OTZlNGQ5ZmUxNTg2ZDViMjdiZDY3YjhhYmU2NDEyMTAyZDQyZmE1MmQ1NGU1ZGQzNGM1YTM5MDg5YTQ5YmNiNmEyMTU0NTZlZTlhNjkxOTNkZTMzZmFmOTIwYWQzZjBhZmZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzE3MDAwMDAwNjQ0MTM4OGMwNzhlNGI0YmI3YzVlNjA1YmJlYjRhNDJkMWE4NDRkOTU4MTk2YmFhMGYxNTJmNmZmZDllYzM2NjBiZWFjNzc4ZmVlYjQ0YTRkNWNhMWQ5MGUxNDg1MWM5OGExZTVmNDFiMzhjNDNjODFlYzNmODE1MzdjZDQyYzc4NzkzNDEyMTAyOGZmN2RkNTc0ZTQ1MzAxYmFhYjQ2ODc2MjNiZTUyODZkNTg5MWY2MjA5NTFlZDQxMjNjZTQ1OWJjNmQyYjdjOGZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzFhMDAwMDAwNjQ0MTFjN2Y5MWE1NGMwMDAwYTIxYTE3NjIyM2FhODQzNjFjMjQ2ZDIzNDQ2YzI3MjVlNGZiNjAyYWE3YjRiYzQ1NWE3M2Y0MTIzYmYwZmNjMWQ2NGI4MWVlYjMyM2QxZTI1OWFkOTNiOWU3NjQwMTE4MDEwYjM3NDVjMDJiOTkwYjZiNDEyMTAzMmEwZjU5M2JmMmMzOWM4YmUxODVkOTdkZTMzNGQ0N2Q3MGJlNmJhYTVkNDg0NGJkZWI2N2UyN2IzZWI1MDE3YWZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzIzMDAwMDAwNjQ0MWNlNzYyMjAwN2Y4OTBjMzEwMGQ3YzBjZTkyZGFmNmViNDQ5ZmVkYjg4ZTQ0YzBjMGYxYmVhNmE5MDk3ZmJmZWY1MjgyMjVhNTM2MmM4MDUwMjhmMTQwMTdiNjJhMDMzMDAyZTFmYTE5ZTUxYzhiNDFjNWRlYmVmNmE3YTM1ZmMxNDEyMTAzMTlkOTJlZmM1OTg5YjE3NGIwOTgyYWNjYjRmNWU1MDM0YjRjZmQzYWM1YmNjODU2OWNhMDdiZGFiMzg1YTc2YmZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzI1MDAwMDAwNjQ0MWFmNjgyM2I3MjI3YmIzOGExMjIwMGJmZWE5ZDU4ZjExMTczMjM1NzU1NTdhY2JiYzRmYTc2MTEwODk3N2Y5NjA1NmEzZmZjYmVhYzg3NGY0YmExNjU0MWQyNTJmNDJhYTg3NWExMzVmZmVhOTJmZWE1MzVhMWU1ZDdkYmUyMzI2NDEyMTAzOTdjZGIyYzI0ZmYzZWZhNTQ2ZmVjYTY3YzY2YWJlNjUzNjc5NTFkNDQ5MmY5ZDUzMDhjYjFlODM2MTNhYmJhYmZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzMwMDAwMDAwNjQ0MTlmYmU3N2IzNjMyOTMxYzEzZjYwOTJmMzlkODE2Y2U3OGNjZWNmMGZmOTcyYWUyOTA5NDNiYWQ2Yzg3OTRhYzUwODUyNzU4NGJmNzYwNTlkY2E3YzdlM2RmZTQ5NTZjNmRmYjcyYzRiZGI3ZmViOGE4OTVmOTcwNjU5MzAxOThmNDEyMTAyZWEwOTYwY2FlMDQ5MzVmZDQyOTE4YzBkN2IzMDRlMTg4NWM1ZWU0YjYxZWQ3MmFhODdmYjg3MWE3NmY4YTNiY2ZmZmZmZmZmNDBlMmNjZWIxZGNmMGEzZGJjZGVhNzhiMDMwNTljNTM2NDE2YTIzODc4OGM2ZmFlNzRjNzM0MTgzOGY0OGFiYzQ1MDAwMDAwNjQ0MTk0ODdlYWJlNzZmZTI5NDM0OGZmZDEyM2RhNmQzNTcyMGEwNTU2MzhmY2Y5M2EzNGE4Njc1YjY4ZTFkNmU2M2Q5Y2M1NmNhMGNlYjhhZWNhMjE5ZmEwM2ZlZDVmNzI0NDk0Yjc5MDE5YmNiMWU0M2FjOWU2Yzg0YWYwOTc3MTNhNDEyMTAzZDYyYTYwYTI1Mjk5NTQxYTY4NDkzZjM1NmQ0MzVlYjY0YzQ2OTdmOWRhOGIxYzkxMTk3NWFjNTQxODJiZjkyMmZmZmZmZmZmMjIwZjQ5ZGE0ZjVhZDY4ZjJlYzI5MTBiY2IxZGRjZjU4ODUzZTJiYzQ2Y2Y5OGEyMDE1NjY2ZmFlMzQ5MzVjNjQzMDAwMDAwNjQ0MWI5YTJkNjIyZjUyMGJmODliMzE3YmNkYzhkNDllNWM5NzE3NDYzODVmNTA3M2MwZjg1YmM1NWNkZjk1M2E2ZDFlOTQ1YzNmODdiYzY0ODhiMDBlMTcxODZiMjczODYzMDk2MDJlNjk1ZTEzYjI3NjllNDEyM2U0NTUyMGRmMjYzNDEyMTAyZWM5YzFiZjM3ZjhiNDc2MmM5NDM0OWI2NjQ2NDM1YTI3N2Y4YmI0ODc1ODI2MTU2M2VjMDZkOTBjNTAyNWVkMWZmZmZmZmZmZGQ0Nzk0Zjg5YzMyOTU1NDBhY2I3NDEzY2IzMWY4YmU0NWQxNDlhMmYwN2RhMmY0ODk4ZmU0NzI0M2U2ZTljYzI2MDAwMDAwNjQ0MWJjZDJhZWJiOGJmZDMwNmM0MjVhMDFlZjM1MDQwOTU4NDEyMzk4MTkwMDc0MGZiMjRjNTA3ZTcwOTc1N2JhZjdjMDQzMTRkY2U0NmQ1NjkyODU0ZDIyOTQ3MTQxZWJlMDBlYjM5ZjBlYWI4Mzg1YTAxOGMxZTkzODVlODMzMjc5NDEyMTAzNjUwNDZkMDVkMThjYjg2YzE0MjNiMDIxZDlhODdmZGM3NjEyMDZjZGI1YjQ3MWY2NzU1NTIxODhjOTkzOTBiOGZmZmZmZmZmMTYxNzM0OGVjNWNhNjQzZjAzMTEwOWUwNWUwYmEzOTMxMDE0MDgzZTdhY2JlYTM1OTcxZGNlMmNjNzgwNWRkMDA5MDAwMDAwNjQ0MWFhMzU2N2U0Nzk5OGUzYWZmMjkwYWNlMmRhODBkZjIwNWJmOTRmYTMxNzBhMzgxMzgyZDc0Mjk5MTk5ZTlkOGNiOTZjMzFmZjM4MTg3OTNiYzcxNjQ3N2M3YmQzMjIyNjhlYTA5NjRmODM0NzlhZDgyN2Y4MzFiYWRjYzNlNTU1NDEyMTAzYjU1MjBhOGIzMDljOGIwODM4YjUyNmE3NDYyZTkzOTY4ODU3YWI0MGI1ZTM3ZTE5ODVjNTI5MDllMjNmMDgxN2ZmZmZmZmZmMTYxNzM0OGVjNWNhNjQzZjAzMTEwOWUwNWUwYmEzOTMxMDE0MDgzZTdhY2JlYTM1OTcxZGNlMmNjNzgwNWRkMDBhMDAwMDAwNjQ0MTZhNDgwMzZkZmIyMDViM2U0NjI5OTI5YTI1OWMyMDI3OGI3NDdkYmI0MDUxYjRiMzk5NzE3ZmI1NDA0MmY0YzI3OGU2ZWQ2ODliZjQ1YmIyMzNiYzU4YWFiZThlYzI5YTc1Y2I1NDQxMzVkZGQ1ZDg3NzIwMTVlMzZhNTRiMmJjNDEyMTAyZThhNTllNTA1YzM1NzBmOWE0MWM3YWE2NTEyNWZmOWI3ZGU5ZGJlZmZkOGY4NDc1MzgxMjFiNWZkMWU2ZGI4NmZmZmZmZmZmMTYxNzM0OGVjNWNhNjQzZjAzMTEwOWUwNWUwYmEzOTMxMDE0MDgzZTdhY2JlYTM1OTcxZGNlMmNjNzgwNWRkMDJjMDAwMDAwNjQ0MTQ0YzIwN2IzNDBhOTU4Nzk0NGJiNjk2YzhlM2JiMDQzZTNhOWJhOWJjMTdkMTFkMzU3YmQxNWMwMWMxMGM3MTA1Nzg1MTI3NzJlMjJhMjUwMTE4MjBjMmFlYzdlOGUxYzJiMDU1ODdlZmRlM2E0MGI5Mzk0ZmVjMTUwNjExNWZkNDEyMTAyMzdjZGM4ZjUxMjQ3MWY2MDgzNDZhMjI1YTE1ZTY3NmE1ZThhM2UxZGE0ZjcyZjg2MWVhMzRkZmU5NmY4NjVhOWZmZmZmZmZmMTYxNzM0OGVjNWNhNjQzZjAzMTEwOWUwNWUwYmEzOTMxMDE0MDgzZTdhY2JlYTM1OTcxZGNlMmNjNzgwNWRkMDM3MDAwMDAwNjQ0MWYzZTYwMDNmMzU4Mjg3MWQ0OGFmMTZhNGIzNzg4MDUzZTdjYTc2MmM3MzYxN2JhYjJhZTVhZTZmOTdiMTNkM2IwNGM5ZWI4MTM1ZGU4OTkwNDRkOGUyZGM3MmJhZGJkZDgxOTFiZjNiYmQ1Mjg4ZTk4NmQ0MjMxYmEzMzljNTU0NDEyMTAzZjdjNThhODFiN2QwYWFlNjdmM2MxNmY5ODgzZWUzMmUwMzgwNDk4M2NiMzM1ZDNhNTIwMDNhYjc3MTEwYjcwOGZmZmZmZmZmMTYxNzM0OGVjNWNhNjQzZjAzMTEwOWUwNWUwYmEzOTMxMDE0MDgzZTdhY2JlYTM1OTcxZGNlMmNjNzgwNWRkMDQ4MDAwMDAwNjQ0MWI3NWVmOWRiNjMwMWU4ZTRkNzM4MTVjZGE1Yjc4OTVhZGVkZTdiMTg5NzJhYmUwN2QwYTEyNmI3NGIwM2I4ZTM3OTA2NzYwMzIyZjNjYmZhZTE5Mjg0N2Y4ODAxZDI5YmI4M2JlMDJiM2FjYWEyZTZiYTMxMDUxODRhODM1MjUzNDEyMTAzYjNmZjcxN2FkZTBiZDE2ZTJiNmI3YTYxMzk4ZjFjMzk2NTI4OGZhYjdiMjNmNTc3YjA3ZDU2NmZjN2YwY2VhNGZmZmZmZmZmYWIzNWIxN2Q5OTVhNDAxODA5ZTM3ODgwN2IyYmE5OTQ1NDczYWNiOGNjNDMzZTVhY2Y1MmI2MjU4ZjQzYzJlMTMzMDAwMDAwNjQ0MWIxMWVhZTI0OTIzNjAzMWZlODI1Y2Q1YjM2MmM3YjNiZmRiNzMyODA0MTVmZDFmODUwM2NjMmMyNTAwMTU5M2RiMjMwODBkODE0MzFiYjRmYTRmMDgzNmRhMTcyZTk4NDQ1OTJiOWZlZDg0OTZkZmFlZTQ2NWE0NmExYmFkMTA0NDEyMTAyNjA1YzI3MjdhNGQxN2U1NzE3ZmUwYzI0NzkxYWNhMjc2ZDAzOWU4NmFhYzA3NmYxYzA1YzcyYjE4MWNiYjI5MGZmZmZmZmZmZGUxOWU1NmY1NjQwYjRmNDQ4Njg5YTEzMWUxMDE1ZjZhMzZkZTRjNzg5Mzk3ODM5YjViZTllNjk0MDZkYmRlNjEzMDAwMDAwNjQ0MWEwOWQzNTJiZDBiYzQzNWVjNjk2YjU0NzJjNzAzNzE2MGY2MTQ5MDZkMWI0ODBiZWJlMDdjZGQ4NmU3ZGYxZWZkNjM1MDJkMTk4NjliYmU1YTZiMDEwMTQ3OWVlYmJhYjM2ZmRjMGY3MzdiZDIwZmQyN2RiYTE0YTMwMDkxODZlNDEyMTAzMzg4ZGExZmM5N2U4OTRjNjdmNjBlNGI3MWM3YzgzMGJlYmNjNjk4ODY4MDNjM2NiM2Y4YWE0MDgxZWUyZmQ1ZGZmZmZmZmZmZGUxOWU1NmY1NjQwYjRmNDQ4Njg5YTEzMWUxMDE1ZjZhMzZkZTRjNzg5Mzk3ODM5YjViZTllNjk0MDZkYmRlNjQxMDAwMDAwNjQ0MWE2OGM5NWQyYmE3YmMyZjJiNmI4MmY0ZGQwMDJlZWE5ZTY4NjkwY2E2NjZjZDYzODk4MTU3NzRmZmQ5Njc1NzFkMmFjN2QyZDY5ODkwZmU0MTg0NzQzNmUxNzEwZDVjODMwM2U2NDVkNWY1YzM0YWVlY2VmZjIyOGY4MWQwMzQ0NDEyMTAzYWU2MDcyNTA2ZTIwOTcwMGNmMTBlYjM1MDhlODI0Nzk4MDI1Nzg0MWRiNjBlZDQ0NWYwYTQzMWU1YWQ1NWVkNmZmZmZmZmZmZGUxOWU1NmY1NjQwYjRmNDQ4Njg5YTEzMWUxMDE1ZjZhMzZkZTRjNzg5Mzk3ODM5YjViZTllNjk0MDZkYmRlNjU3MDAwMDAwNjQ0MWIxNDQ1MDczMDhkYjI0ZGZhZDJkODY3MDM2ZDc4OTI1YWJmMWFlOTk3ZjE5OThlMjExOTk5NzY3M2ZiZDFmNDFlMmRiN2ViODM4ODc3MGM0MzE5ZGJiOTE4OWRmY2Q5MzQwYTMwZjlmMzRjODQ3MDU5ZTQyMGZmMjNlZDlkOGU1NDEyMTAzYjg1Y2ExYzUwOTcxMWYwOWU5MTA4M2RiNjI4NWZlNTQ3ZjI0NmIxOWVmZmY1ZmEwYjAwM2RhOWZlY2NiYWM1ZWZmZmZmZmZmODM1NTg2YjUzZmY5YzZlOGU3YTY3NDBjOTBjYTEwNTM5NmMxNmFmNWY5ODBiMjYzMTA3MWQwNmY3ODE4YzRlNjAwMDAwMDAwNjQ0MTE5MGY5MDAzNTcyZTdlYjkxNDU4N2NlN2JjZWU3NzBmN2MxYWYzM2NkNjMzMTFkYjZkY2U0NDc4ZTcwN2JhMTk0YTAxZWEwMTBmZWM2NjkyMTQ3NWE2MTk0Mzk4MDE4MWIwMWUzMmI2MzA2MzM3MzljNGUzYTE3ZTI1YTY2ZGFhNDEyMTAzMWIyMjlmNzgxZTZhMTQzYmM1ZTY0NDY3YmZkMzJmZjU0NGQxODg5YjEzZjJmZjY0MGU0YzQ2ZjYwNzA0ZGRiNmZmZmZmZmZmNTYwMDAwMDAwMDAwMDAwMDAwMjc2YTA0NDY1NTVhMDAyMDMxZTAxZmVlOTlkYTBhMGFhZDQxZWM1ODk4ZWY2NjAxNTZkMjA3MmEwZTMyZGMxNzQ4MDFjMzUzNTUwNGQ1N2Y4OTkzMDkwMDAwMDAwMDAwMTk3NmE5MTQxY2E4MTZjYWUxNWQ1OTBhZWQzNzhkNTlmOWYyZmIxZjJkMDg0Nzk2ODhhYzRkYTUwOTAwMDAwMDAwMDAxOTc2YTkxNGQ1MTVhOTYxYTQxYmE4YzU4ZGQyYjY1ZTlmMDlhMTgwNTBiOTFjYmE4OGFjNTNhMzBiMDAwMDAwMDAwMDE5NzZhOTE0N2MyMDA0ZGVmMzhmMThkNTU0MDJmMGMyNWE4MTZlYzZhYWYwNmYwNjg4YWM2NDgwMGYwMDAwMDAwMDAwMTk3NmE5MTRlYzI5OGY0N2E1NDc1YTYzN2JhNDUyZmU0Yzc0OTE0NzVkMWUxMTQxODhhY2E3OWYxMTAwMDAwMDAwMDAxOTc2YTkxNGU1YTI2M2NlZmRhMGZiOWFiMjRkYjk1Mjg3MzU1MGQ5OWI3NDlkYzc4OGFjODAxMDFiMDAwMDAwMDAwMDE5NzZhOTE0ZDMyZGVkYWQyNjNhMDBmMGQ2YmYzMTVhNWE5N2UwN2ExMTgzOWRmNTg4YWMyOWQ3MWIwMDAwMDAwMDAwMTk3NmE5MTQ1N2UyZDcwMjA2YjFlY2ZkYjBmZjg3MzRmYjA0ODk1ZDYwMGJhNTljODhhYzkwNDIyMDAwMDAwMDAwMDAxOTc2YTkxNDI5NjdlYTYyOTg5NDM4MGVhOTA0Njk4ZGVlZGRkYmZhYTQwYmVjY2M4OGFjY2U1NTIxMDAwMDAwMDAwMDE5NzZhOTE0MGQ2OTJmNDFjNzQyZDRmYmZhM2EyYzU0NzQ5NThjYTE3ZmY4ZDg5NTg4YWM2ZjAxMzIwMDAwMDAwMDAwMTk3NmE5MTQ3MDY3M2JhMmY1MDg2OTlhNmY5ZjA5NmE2MGE3MDYwNDhmZWViOTVkODhhY2FkMDQzMjAwMDAwMDAwMDAxOTc2YTkxNDJiZmRkNWJkZWM5ZmQ5OTMzMzYzZWFjZjg5ZThkODUxMDU2NDdjY2M4OGFjZTQ4ZTMyMDAwMDAwMDAwMDE5NzZhOTE0MDgzZWEzMjRlZGU2NWU2ZGJkOGVlMjVkZmQ1YTQ5N2NjYzhjNTkzYTg4YWNlNDI1MzQwMDAwMDAwMDAwMTk3NmE5MTRiYmZlNGQzOTZjMzljZGQ3OTMzN2MzOTMzY2NhOTU1MTRlZDBkOTM1ODhhY2JjNTczNjAwMDAwMDAwMDAxOTc2YTkxNDk1OTMxYzY3YzY2NzQ4Y2ZkMGIyMmM3NTcwMWJkNGY5YTljYjc2YzI4OGFjMDdmOTM3MDAwMDAwMDAwMDE5NzZhOTE0NjA2MzNiMWViZWFmNDRjYzE2ZmFlMTk5Nzg0MTI1OTA0NjIxYWRkYTg4YWM5NTdjMzgwMDAwMDAwMDAwMTk3NmE5MTRhMmMzYTk4MWIyYjVlMWRhNTAxMmZhZTVjNDNmZmQ2NjJjMjk0NzkzODhhY2I4MTAzZjAwMDAwMDAwMDAxOTc2YTkxNDBlYjY2Njk4MGNlYmE3MjI0MDFhOTk2OWI3Y2ViMmZlYjk2MDc4MDA4OGFjMWIzZDQzMDAwMDAwMDAwMDE5NzZhOTE0ZDJiOTc4NjE5MGRhNmU5MzkzN2ViNDhhNzQyMGYzMGM1MzM2YTJlYTg4YWM0ZDNkNDQwMDAwMDAwMDAwMTk3NmE5MTRmM2FlYjI4NGI3MDNkMWY2YTEwZWFlNjY5YzExM2QyNWVjMzBmZmI5ODhhY2Y2YWY0NDAwMDAwMDAwMDAxOTc2YTkxNDU2ODMyMjg2Mzc5ZjUxNjAyYWFlY2NhOTJmNTFkYzAyY2M1MzAwMTY4OGFjMjYwODQ1MDAwMDAwMDAwMDE5NzZhOTE0MDZmYzRkODRlYzM3N2U1MDJkMTYzMGQ2MTczY2RlZDFjYTE2MDdkYzg4YWNmNWFjNGEwMDAwMDAwMDAwMTk3NmE5MTQzMGE1OTI5NDlkM2ZjYzdiZTRmMWIzM2YwY2NiYTM4YTkxZmZlZDIxODhhY2Y3Zjk0ZDAwMDAwMDAwMDAxOTc2YTkxNGUzYjIyMWJmZDVkNzcwMzQ5Y2IxZjVmODIxMDM4MGE0ZjEzZTcxMTM4OGFjZjY0NzUxMDAwMDAwMDAwMDE5NzZhOTE0MjkwZjA0OTI2YzU5MTg5NzNiMjg1NTQ1ODQyMzYwY2UyOTljYWNkZDg4YWM3MmU3NTEwMDAwMDAwMDAwMTk3NmE5MTQxODllYjgzNjkxM2RhMDM3NjBlZjE5YWM3OTAxNmFiZjRlYzNjOThkODhhYzE3MmE1NTAwMDAwMDAwMDAxOTc2YTkxNGVjZmVlN2FiODc4ZTgwZGYwYzg5NGFmZmM0MjY4YTcwNmJkZjhjOGQ4OGFjNDIxZTU2MDAwMDAwMDAwMDE5NzZhOTE0NGU2YWYxY2YyMjliZDhiMzY4Y2EwNjIyZjIwNmE0YmQzYWY4MDI0Njg4YWNjOWU4NjUwMDAwMDAwMDAwMTk3NmE5MTQ2N2QwNmFkMDcwZWRkNTYzZWQ3MTFhMjkxMjAzODJiZDA0ODk5ZDMwODhhY2ZkMDY2ODAwMDAwMDAwMDAxOTc2YTkxNDBjNGI3MDAwZjFhYTUyMmMzYTFkM2U5OTY3NWZjNDJkYWNkZDFmYmM4OGFjNzMyODZiMDAwMDAwMDAwMDE5NzZhOTE0OTA4YmQ1MjU5YjRiNDRlMTg1YjA4ZTEyODJmNjUyY2UzYzEzZmI0NTg4YWMzNDJhNmQwMDAwMDAwMDAwMTk3NmE5MTQ1MzhmZTFkNzQ2Nzc2NjFjMTIxYWZjMGE4Y2RiNTFiN2I4NzY1NDZlODhhYzE2N2E2ZjAwMDAwMDAwMDAxOTc2YTkxNDNjZTRhM2YzMzA3OTZkYTUxZDA0Y2Q1MGY2NGRjZWQ4Yzc2ZGZlN2Y4OGFjNWIyMTcxMDAwMDAwMDAwMDE5NzZhOTE0NGY1ZjE2Yzk2ZDRkMjVmMzFkY2E0ZjRhMWE2ZDVhMjAyODE2ZTBkMDg4YWNjYzI2NzMwMDAwMDAwMDAwMTk3NmE5MTRhZjBjZGUzYjNiMmQ4NGJkZWY5OGMwZTQxNTg1ZmM1MGMwNTI0NzNlODhhY2FjZGU3NTAwMDAwMDAwMDAxOTc2YTkxNDdjYWFjZDExODhhNjYwNTE4OTRkYzZhODQ4ZTFiN2Q4OTlkMTUxOWU4OGFjYjVhMjc3MDAwMDAwMDAwMDE5NzZhOTE0YzMyODMzNzU4MzEzNmQyOTQ1Nzc3MGViNjI2NzE1MTdlOTIxMDAzMzg4YWM4MGU0N2IwMDAwMDAwMDAwMTk3NmE5MTQ0NWMwZTgwM2EwNjJkMjg1YTJkYjFmOWVhZTQ5ODhkZTFmOTE1YjM0ODhhYzk4ZWE3YjAwMDAwMDAwMDAxOTc2YTkxNDA5ZjBjMjQwM2M5ZjU5OTAwMTA5Yzk1MzQ0ZjkyZjM2OTVmNTU1MDQ4OGFjMzg4NTgwMDAwMDAwMDAwMDE5NzZhOTE0NWNiZWMxZWUwNzI3MTYwNmU5YjZiY2Y3MWVjMDQ0NzlmNjMyZDMyZjg4YWNlZmY1ODEwMDAwMDAwMDAwMTk3NmE5MTRmMDIzMjc1NTY3NjUzN2ZlYjBiYmQ3OGQxZWQxYjI3NWZmYTdkZjhhODhhY2IwMTQ4ZTAwMDAwMDAwMDAxOTc2YTkxNDA0MjhiYzVjZDg0MDUxOTlmZTYxM2I0YzYzNGNlMDFhZjE1MmRiYjk4OGFjMmZiODkwMDAwMDAwMDAwMDE5NzZhOTE0ZWQ4YWMwN2IwOTFkYzJlYTU2NDMyMWVmMjgzYWMyMWIyY2RkZGEyYjg4YWMwNzgxOTEwMDAwMDAwMDAwMTk3NmE5MTRhMzBkMzcyYzNkMWIwOTA4MzA2NzE4ODVhYzQ0NGU4YWE4MjJlODkwODhhYzg2ZGU5MzAwMDAwMDAwMDAxOTc2YTkxNDUwYTYyZWJkZjgyOWExMWUzOGZjZGU2ZjI4OTgyNzE3ODBiZjUyZDM4OGFjNWFkYjk1MDAwMDAwMDAwMDE5NzZhOTE0Yjk5ZTU5NjUzZDM0Y2M2ZjJmMTAwNGEzZGQwMTVjZDJhMjhiNzljZjg4YWMxMmNlOTgwMDAwMDAwMDAwMTk3NmE5MTRiOTQxYjJkNzI0M2NhOTgyMTI1YjQyYzllNGM3ZGUwOTljNDMyMjRhODhhY2RhZmI5ODAwMDAwMDAwMDAxOTc2YTkxNDQyMTU1YjNiYjJiMDQ5YzI1YzA1MzY1NjNmMjIzYzQ5ZGJjYzQ4NmU4OGFjNGYyYzlhMDAwMDAwMDAwMDE5NzZhOTE0NDAxZjQyNzZjMGFmNGJiNTJhNTBmYTI2MTQxMWUzZDNiMGVkMTdhODg4YWM2MmRhOWEwMDAwMDAwMDAwMTk3NmE5MTRmYTI3OTkwZmI4NzM1MDkyZWViMmI4OWE1NjU0NmZmZDI1NzIwMmMyODhhYzA4NWZhMTAwMDAwMDAwMDAxOTc2YTkxNDA2NzExOGFkNjBjNTFlZmRkNTAzMWE2ODZmOWNmYjE2YzIyMWY4OWI4OGFjNDUxYWFlMDAwMDAwMDAwMDE5NzZhOTE0ZGNhZGM3OGU0ZGVmMzhiMmM1NjU1N2VhN2UxNDYwNTdkODFmYjA1Zjg4YWNlMTg3YjEwMDAwMDAwMDAwMTk3NmE5MTRiMjhjM2RkYzliMWY0ZGMxN2MyYTlkZjkwOTg5MDhmZjAwMzQ2Njc5ODhhY2IzMmViYzAwMDAwMDAwMDAxOTc2YTkxNGRiZWYyNGNiYzdjODZmZTA3NTBmNWUyNGViMDU2OTZjNTY2NzQ3OWM4OGFjM2IwY2JmMDAwMDAwMDAwMDE5NzZhOTE0NjVlODViMWE0MzYzY2QxODBjNTk0NDdhOWJjZjBkZWRjOTQzZGE2Nzg4YWNkZDBkYzUwMDAwMDAwMDAwMTk3NmE5MTQ5ZGNjYTc2MmQwYzUxMjZhZjVhYWEwNzljOTAzMzIwNWVlNTAwNzNkODhhYzQzY2RjODAwMDAwMDAwMDAxOTc2YTkxNDM0Y2FjNTc5YWJlNmY4MDlmOWE1OTJmNGU0MmI2ZTZkOWYzMjY1ZTA4OGFjZWVlNGNkMDAwMDAwMDAwMDE5NzZhOTE0MDc3OWVhMjliYzdlMmZiZTY4MWI3MTY5NDY4YzIwZDhjNjUyZGIzMDg4YWNjNTkxY2YwMDAwMDAwMDAwMTk3NmE5MTQ5Y2JhYmFlZTMyNTc1MTMxN2QzMTMyZTI0Y2E0YjQ2MzUwODg1MTU0ODhhY2Y4YTZkMjAwMDAwMDAwMDAxOTc2YTkxNDlhN2I2MTZjZjg1YzgyMzFmYTE0OWQ2OTI4NTY2MmJiZGNhOGYxNTk4OGFjMTQyZmQ2MDAwMDAwMDAwMDE5NzZhOTE0NzJmN2NlMTBiOTNhYjNjZWE3Y2M1ZWQyNGEzZDJjZjg4ZTNlZTU1MDg4YWM3OWUzZGEwMDAwMDAwMDAwMTk3NmE5MTQ4NmQzYjdjM2FmOTBiNGY5OTFiNjhlOTRjZGJkY2Q4ODVmMmZjZDU3ODhhYzZlNGNlNDAwMDAwMDAwMDAxOTc2YTkxNDFlNjJlZTJjNGFkMmNhZWY4MGQwYzhiNGJkZWJiY2U5ODI4ZDI1OTU4OGFjMzdhOGVhMDAwMDAwMDAwMDE5NzZhOTE0OTYzNTBhZGZmMGVjMDQwNGE1YTliODgwY2Y4Mzk2ZTJkZWQ1YWY1ZDg4YWM4ZmI4ZWMwMDAwMDAwMDAwMTk3NmE5MTRiYzE3ODhjYTlhNjY3NzBkNWQzNzU0YjhlZTA0YjI3ZmQyNjNlYjhkODhhY2I4MjZmMjAwMDAwMDAwMDAxOTc2YTkxNGI4MzRkNWU5MTZmODRhNjIyZDgyM2Y3ODBmMmEzMjQ0ODhlMGRmYWE4OGFjMTE0OGYzMDAwMDAwMDAwMDE5NzZhOTE0N2M3MmE2ZDk3ZjYyNDQ1MWRkYmRkYTk3MTMwMmNlZTFlMWNjMTJlODg4YWM2ZWMwMDAwMTAwMDAwMDAwMTk3NmE5MTQ4OTYwNTBjNzhiZDJmMWM4MmQ3MTdiYjNlMWUzYzBlYjYyMzBjNGI2ODhhYzVjNjIwNzAxMDAwMDAwMDAxOTc2YTkxNGZiMjk4NjJlYWU5YTQxMWQ5MWNhZTFmN2I5ZDNhYzBmYmVmYTc5NTg4OGFjNjVhMTVlMDEwMDAwMDAwMDE5NzZhOTE0ODIyZjNkNDU1YWMwNjg2MmJlYmQzMmYzOWUyZjZhNzgxYzg2MTNiZDg4YWM1ZTVlNjUwMTAwMDAwMDAwMTk3NmE5MTRkNmIwZjNkMDMwMWIzOGRiNzEzZDAxYzkwZjBiNmUxZTM3NzI3M2U4ODhhYzkwNDA3MDAxMDAwMDAwMDAxOTc2YTkxNDk4NzYwMDM5MmI4YjM5NDgwOTFlNGM0ZGVmNDJmMGZhMjc2YzBhODI4OGFjYzM0YzcwMDEwMDAwMDAwMDE5NzZhOTE0YTM0ZjBiNWE1MzdhYjE1MTVjZWI4OGQ5MzhkYjRhMGJjNjY5N2E5YTg4YWM5MjBlN2IwMTAwMDAwMDAwMTk3NmE5MTQ2NTkyYzZjMjYwZmMzNzQ3ODBhZjMyMTJhOGYwODdlM2RiM2EwZTgwODhhYzFjY2M4NjAxMDAwMDAwMDAxOTc2YTkxNDNhYzA2YjhlNjNmZjhhNWU0YzgwODZhNjkwNGZhODAwZWJmZGYxYTE4OGFjM2JmOTg4MDEwMDAwMDAwMDE5NzZhOTE0MDk5Y2QwYzJlZmJkMTRjNWQ0ZDlhODFhZjc3NTdlZDVmOWUzNmIwZTg4YWMyZGMxOTMwMTAwMDAwMDAwMTk3NmE5MTQ2ZTBlY2QyZjY4MWRmZTMxZjQ3MTI4NmIxY2NmMDI5ZGU4ODE1Y2IwODhhYzUyMzM5OTAxMDAwMDAwMDAxOTc2YTkxNGQwY2VmZDVlYzljNjc0YzY5YmVjNzlmODRjN2VmOTJhYTRlNmFiYzg4OGFjMjdkMGI1MDEwMDAwMDAwMDE5NzZhOTE0NWEzNDUyMjU2MzNmM2RkNWQ3NGZhYWJjNmU2OTRhMWUwNmZkYjlhMTg4YWMyZjMzZDAwMTAwMDAwMDAwMTk3NmE5MTRjYTE1NzNkYzBhZDQzYjI1NGM0NGI0YzhlODc3OTdjMzcyMDM0MmZhODhhYzk3ODhmNzAxMDAwMDAwMDAxOTc2YTkxNDU3OGViMGRmNjk3MzBiZDcyZTZmYzQxMzhkMTQ3ZDNiZDkwZTQ4MmE4OGFjMDZlMTQxMDIwMDAwMDAwMDE5NzZhOTE0NjI0ODBkNzg3MzkyYWVlMDU2OGUwNTYwNjEwZDhmN2E3M2NjY2M3Zjg4YWMwMjA5NmIwMjAwMDAwMDAwMTk3NmE5MTQ5NTExY2U1ZDIzZDI3M2Q3MDZhNGY3M2Y5YWMzMGE5NThmMGY4MWZiODhhYzc3MjI4ZTAyMDAwMDAwMDAxOTc2YTkxNGRjMjBhMGU2NzcyYmU1MTNjYjU3NjlmZjcxY2IwZWU2Y2M4YzJlYmE4OGFjNzBjNjliMDIwMDAwMDAwMDE5NzZhOTE0NmZjM2RmOGY5N2ExYmY1NTdiMDQyNmU4NmI1ZmFiN2QxYTBlOGRlMTg4YWM1ODNmNmYwMzAwMDAwMDAwMTk3NmE5MTRkZDZlNjlkOGVlZmRlMWMzNDkyYmJjMjMzMmY3OGQ4NWM4NTIxNGM2ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiZDhiNzljZTBiNmNlNjM4NmM5MTZiOTY5MDQ1NzM2MzljZTI1NzkzNWUxZWE3ODMyYWU4OTgyNGM3YmM3ZGIwMiIsImhhc2giOiJkOGI3OWNlMGI2Y2U2Mzg2YzkxNmI5NjkwNDU3MzYzOWNlMjU3OTM1ZTFlYTc4MzJhZTg5ODI0YzdiYzdkYjAyIiwidmVyc2lvbiI6Miwic2l6ZSI6MTY0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiZDMzNDBkZGExOWU4ZTRiZTFjODc1MDA1YTczZjBlYWIwMjA5YWQ2ZmUyMTE0ZGY0MTQ3OGYzZDI0NTFlOWY2MyIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIiwiaGV4IjoiNDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIn0sInNlcXVlbmNlIjoxfV0sInZvdXQiOlt7InZhbHVlIjowLjAwNTY4Nzk4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XSwiZmVlIjoxLjY0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTYzOWYxZTQ1ZDJmMzc4MTRmNDRkMTFlMjZmYWQwOTAyYWIwZTNmYTcwNTUwODcxY2JlZTRlODE5ZGEwZDM0ZDMwMDAwMDAwMDQ1NDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxMDEwMDAwMDAwMWRlYWQwODAwMDAwMDAwMDAyM2FhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODcwMDAwMDAwMCJ9LHsidHhpZCI6ImRiNzA2ZTlhYjA3NzM3YjU2MWQ4NzQ5Zjg4YzBkMDM4YjVmMjU5NTNkZjJmZTNlMmQ3MjkxYjU1Y2Q3MzkwYzMiLCJoYXNoIjoiZGI3MDZlOWFiMDc3MzdiNTYxZDg3NDlmODhjMGQwMzhiNWYyNTk1M2RmMmZlM2UyZDcyOTFiNTVjZDczOTBjMyIsInZlcnNpb24iOjEsInNpemUiOjQwMCwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjM3NThjNGMzM2I2MGU3Y2ZjNmYxY2Y3MjA4YjA5ODU4MGFjYmZjMmRiMjc2NzQzYTVhZDFmY2IxNzFhM2E0YTkiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAzNWVkMzA2NDE5OWExZjhiYWZiY2NhODY1MDk5OGE4NmQ0YTkxODAwNDY3YTJhOGRlNjNlYTJiNWI1NDBkOWNmMDIyMDZiYTY4OWU5MjAzZGY0MDRlNTRmMDA0YTU2YTRiOTcwMGU0MzhjODE4MDY1YWY5Y2VkZDAyMGRjMDcxNmQ3YmZbQUxMfEZPUktJRF0gMDNiMTExOGY3ZTRhYTRiYjg4NWRkNTg2MDY3OTcyM2I4ZjBmNmEyNzE4YTY3YWVmOGJkOGFhNzYzNzliY2ViYTEzIiwiaGV4IjoiNDczMDQ0MDIyMDM1ZWQzMDY0MTk5YTFmOGJhZmJjY2E4NjUwOTk4YTg2ZDRhOTE4MDA0NjdhMmE4ZGU2M2VhMmI1YjU0MGQ5Y2YwMjIwNmJhNjg5ZTkyMDNkZjQwNGU1NGYwMDRhNTZhNGI5NzAwZTQzOGM4MTgwNjVhZjljZWRkMDIwZGMwNzE2ZDdiZjQxMjEwM2IxMTE4ZjdlNGFhNGJiODg1ZGQ1ODYwNjc5NzIzYjhmMGY2YTI3MThhNjdhZWY4YmQ4YWE3NjM3OWJjZWJhMTMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMDAwNjc2MjgsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhMjdiM2Y2Y2Y0ZDJhYTVlZGFmY2U3YTkxYjg0MmNkNDY1N2YwODVhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YTI3YjNmNmNmNGQyYWE1ZWRhZmNlN2E5MWI4NDJjZDQ2NTdmMDg1YTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxejM4azBtdjduZjI1aGs2bG5uNmp4dXk5bjJ4MmxjZ3RnMHhqbHQ1azMiXX19LHsidmFsdWUiOjAuMCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX1JFVFVSTiA2YzQwNDEyZTY4NmEzNTU5NTUyMDNhNDYyMDcxMjUyZDdkMjM3MDM5NDAzODRlNTEyMDNhM2Q3ZTYxNTk3ODM5M2EzNzcxNDY1NjUxN2U2ZjQyNjM1MDMxNjg0MzM5MjA1NTU5NzgzNTcwMjUzNjcxNWEzZTMzMjM0YTRkIiwiaGV4IjoiNmEzZTZjNDA0MTJlNjg2YTM1NTk1NTIwM2E0NjIwNzEyNTJkN2QyMzcwMzk0MDM4NGU1MTIwM2EzZDdlNjE1OTc4MzkzYTM3NzE0NjU2NTE3ZTZmNDI2MzUwMzE2ODQzMzkyMDU1NTk3ODM1NzAyNTM2NzE1YTNlMzMyMzRhNGQiLCJ0eXBlIjoibnVsbGRhdGEifX0seyJ2YWx1ZSI6MC40MDE1NzE5OCwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDhjMGVhYmU2ZjExMWJkNmRhMTJlYmEwZThjOTI2ZWJkMzIxYmRhNzIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4YzBlYWJlNmYxMTFiZDZkYTEyZWJhMGU4YzkyNmViZDMyMWJkYTcyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6eHFhMmx4N3lnbTZtZHA5NmFxYXJ5amQ2N255eDc2d2d4ZjBqdjljayJdfX0seyJ2YWx1ZSI6MC4wMDAzMzgxNCwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDA2NjY4YzI2MGIyMmMxNzgyMDViZTIxOWE4ZjBkNmM4Zjc0NTZjZTUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwNjY2OGMyNjBiMjJjMTc4MjA1YmUyMTlhOGYwZDZjOGY3NDU2Y2U1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxcnhkcnB4cHYzdno3cHF0MDNwbjI4czZteTB3M3R2dTV0NzgzNjkyOCJdfX0seyJ2YWx1ZSI6MC4wMDAyOTcxNywibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDA1NDE2NGRkZGI5NTUzN2IzNjBiNWRkNmQ0YjUxZDdkMDhjYzlkNTQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwNTQxNjRkZGRiOTU1MzdiMzYwYjVkZDZkNGI1MWQ3ZDA4Y2M5ZDU0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxejV6ZXhhbXcyNHg3ZWtwZHdhZDQ5NHI0N3MzbnlhMnM5eW4ycnM1OSJdfX0seyJ2YWx1ZSI6MC4wNjc2Mjg1OCwibiI6NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGFmYjRiMzU3MzI2MmNjZmRhYzNhYjc5ZjE3Mzc4ZmJiZmIwOWUwNzYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhZmI0YjM1NzMyNjJjY2ZkYWMzYWI3OWYxNzM3OGZiYmZiMDllMDc2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6aG1mdjZoeGYzdmVsZHY4Mm1lNzllaDM3YWxrejBxd2M1cXY4NTRrYSJdfX1dLCJmZWUiOjMuNDJlLTA1LCJoZXgiOiIwMTAwMDAwMDAxYTlhNGEzNzFiMWZjZDE1YTNhNzQ3NmIyMmRmY2NiMGE1ODk4YjAwODcyY2ZmMWM2Y2ZlNzYwM2JjM2M0NTgzNzAxMDAwMDAwNmE0NzMwNDQwMjIwMzVlZDMwNjQxOTlhMWY4YmFmYmNjYTg2NTA5OThhODZkNGE5MTgwMDQ2N2EyYThkZTYzZWEyYjViNTQwZDljZjAyMjA2YmE2ODllOTIwM2RmNDA0ZTU0ZjAwNGE1NmE0Yjk3MDBlNDM4YzgxODA2NWFmOWNlZGQwMjBkYzA3MTZkN2JmNDEyMTAzYjExMThmN2U0YWE0YmI4ODVkZDU4NjA2Nzk3MjNiOGYwZjZhMjcxOGE2N2FlZjhiZDhhYTc2Mzc5YmNlYmExM2ZmZmZmZmZmMDYyYzA4MDEwMDAwMDAwMDAwMTk3NmE5MTRhMjdiM2Y2Y2Y0ZDJhYTVlZGFmY2U3YTkxYjg0MmNkNDY1N2YwODVhODhhYzAwMDAwMDAwMDAwMDAwMDA0MDZhM2U2YzQwNDEyZTY4NmEzNTU5NTUyMDNhNDYyMDcxMjUyZDdkMjM3MDM5NDAzODRlNTEyMDNhM2Q3ZTYxNTk3ODM5M2EzNzcxNDY1NjUxN2U2ZjQyNjM1MDMxNjg0MzM5MjA1NTU5NzgzNTcwMjUzNjcxNWEzZTMzMjM0YTRkMGVjMDY0MDIwMDAwMDAwMDE5NzZhOTE0OGMwZWFiZTZmMTExYmQ2ZGExMmViYTBlOGM5MjZlYmQzMjFiZGE3Mjg4YWMxNjg0MDAwMDAwMDAwMDAwMTk3NmE5MTQwNjY2OGMyNjBiMjJjMTc4MjA1YmUyMTlhOGYwZDZjOGY3NDU2Y2U1ODhhYzE1NzQwMDAwMDAwMDAwMDAxOTc2YTkxNDA1NDE2NGRkZGI5NTUzN2IzNjBiNWRkNmQ0YjUxZDdkMDhjYzlkNTQ4OGFjNmEzMTY3MDAwMDAwMDAwMDE5NzZhOTE0YWZiNGIzNTczMjYyY2NmZGFjM2FiNzlmMTczNzhmYmJmYjA5ZTA3Njg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6ImRkN2RhNWNmZjFlYmYxMmM4OWQ4MjY4MzA4YmZiMWRiYTA5M2ZlN2I3MjA3MTBiYzg2YzViYTg0OWE2ODVjOGIiLCJoYXNoIjoiZGQ3ZGE1Y2ZmMWViZjEyYzg5ZDgyNjgzMDhiZmIxZGJhMDkzZmU3YjcyMDcxMGJjODZjNWJhODQ5YTY4NWM4YiIsInZlcnNpb24iOjIsInNpemUiOjIyNSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjM5OGNjZDRhOTk0NjljNGRkOWUzZmFmYzU2ZGEwZjRhODI4NDgyMmFlNjNhMzJiNTdlNzExMmQ2ZDhhN2MzM2YiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA3ZWE1N2Y3YTNhZTFmZDJmZDdiNDQ5YzllZDEwNzgyNDllZWIyMWEwYmY0MDM1NmQwYzIzN2IyODZjZGNmMWZlMDIyMDE2ZjIzYjY2ZWY1OTIyMmUyNzU3ODYzZTkxNDBjOWQ1YmIwZWExMGIyMTIyYjBkOGI0MDUyNjYyMGZhYzQ1NjNbQUxMfEZPUktJRF0gMDJkNWRhMmY2OGM2MjAxOTJhNTVlM2M2ODI1NTMxN2I2ZjliZWIxYjU1YjYxMTgzY2IxZjcxNjM4NjgzNWQxZTNlIiwiaGV4IjoiNDczMDQ0MDIyMDdlYTU3ZjdhM2FlMWZkMmZkN2I0NDljOWVkMTA3ODI0OWVlYjIxYTBiZjQwMzU2ZDBjMjM3YjI4NmNkY2YxZmUwMjIwMTZmMjNiNjZlZjU5MjIyZTI3NTc4NjNlOTE0MGM5ZDViYjBlYTEwYjIxMjJiMGQ4YjQwNTI2NjIwZmFjNDU2MzQxMjEwMmQ1ZGEyZjY4YzYyMDE5MmE1NWUzYzY4MjU1MzE3YjZmOWJlYjFiNTViNjExODNjYjFmNzE2Mzg2ODM1ZDFlM2UifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMTQ2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTMxZmU5ZGNkM2EzZWY2ZDU1ODA2YjNiY2Y1MGI0Nzc0ZmIxMGQ5NiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDEzMWZlOWRjZDNhM2VmNmQ1NTgwNmIzYmNmNTBiNDc3NGZiMTBkOTY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFmM2w2d3U2dzM3N20yNHNwNG5objZzazNtNWx2Z2RqY3QzcGZhNzAzIl19fSx7InZhbHVlIjowLjU5NzEyNzg4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNjZmMjg3NzNjODAxZGVjNzIwYzBmNDYxMTg1YWIwOTgwZGQ4OGJiYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDY2ZjI4NzczYzgwMWRlYzcyMGMwZjQ2MTE4NWFiMDk4MGRkODhiYmM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBuMDlwbW5lcXFhYTNlcWNyNnh6eHo2a3p2cW1reXRoc3pxNTVhZ21oIl19fV0sImZlZSI6Mi4yOGUtMDYsImhleCI6IjAyMDAwMDAwMDEzZmMzYTdkOGQ2MTI3MTdlYjUzMjNhZTYyYTgyODQ4MjRhMGZkYTU2ZmNmYWUzZDk0ZDljNDY5OTRhY2Q4YzM5MDEwMDAwMDA2YTQ3MzA0NDAyMjA3ZWE1N2Y3YTNhZTFmZDJmZDdiNDQ5YzllZDEwNzgyNDllZWIyMWEwYmY0MDM1NmQwYzIzN2IyODZjZGNmMWZlMDIyMDE2ZjIzYjY2ZWY1OTIyMmUyNzU3ODYzZTkxNDBjOWQ1YmIwZWExMGIyMTIyYjBkOGI0MDUyNjYyMGZhYzQ1NjM0MTIxMDJkNWRhMmY2OGM2MjAxOTJhNTVlM2M2ODI1NTMxN2I2ZjliZWIxYjU1YjYxMTgzY2IxZjcxNjM4NjgzNWQxZTNlZmZmZmZmZmYwMjQwYzdkZTAwMDAwMDAwMDAxOTc2YTkxNDEzMWZlOWRjZDNhM2VmNmQ1NTgwNmIzYmNmNTBiNDc3NGZiMTBkOTY4OGFjMTQyNThmMDMwMDAwMDAwMDE5NzZhOTE0NjZmMjg3NzNjODAxZGVjNzIwYzBmNDYxMTg1YWIwOTgwZGQ4OGJiYzg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6ImRlODkxZjczZjc3MmQ1ZGRjYzJiZmJiMDUxNGU2NTdiYTg0ZTFlMmIzMTEwMjJhYTEzZTBmNWYzZTBkNmMzNWUiLCJoYXNoIjoiZGU4OTFmNzNmNzcyZDVkZGNjMmJmYmIwNTE0ZTY1N2JhODRlMWUyYjMxMTAyMmFhMTNlMGY1ZjNlMGQ2YzM1ZSIsInZlcnNpb24iOjIsInNpemUiOjE5MSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImE3NWVhZjhkOTJlM2EyZWRlN2U0ZDFhMjM0NWI1YjkwZTBhNjM1MTk2YTVkMjdhZjI1ZTgxOGI1NjExYWFkYTAiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjA2OTQzYmRjOGNmODJhZWZiMmZlNTk1ZjcyNWNjZjliY2ExMmU3Mzc2ZGM4ZjAyZDY2NDYwYTg3Mzk4ZTc3MTVkMDIyMDc2ODE3MTU0ZWM5NzkxMmM0NGRmOGQyZDgwMzMzY2MzZjhhNGY3NTk0YWRiM2IyOTIxOTUwODcwOWYzZDdiZTlbQUxMfEZPUktJRF0gMDIxMGM1MGU5NDMyZjQxZmUyOWU5OTRiMjBlNGU3ZjcxMTY3M2ViMjQzYjk1Mjc2M2VjZDhiN2FjYmMxNzZlNjA2IiwiaGV4IjoiNDczMDQ0MDIyMDY5NDNiZGM4Y2Y4MmFlZmIyZmU1OTVmNzI1Y2NmOWJjYTEyZTczNzZkYzhmMDJkNjY0NjBhODczOThlNzcxNWQwMjIwNzY4MTcxNTRlYzk3OTEyYzQ0ZGY4ZDJkODAzMzNjYzNmOGE0Zjc1OTRhZGIzYjI5MjE5NTA4NzA5ZjNkN2JlOTQxMjEwMjEwYzUwZTk0MzJmNDFmZTI5ZTk5NGIyMGU0ZTdmNzExNjczZWIyNDNiOTUyNzYzZWNkOGI3YWNiYzE3NmU2MDYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMDk5OTgxMiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkzOTczZjE2OGQ4ZDI2OTE2MzFhOTkzMjYxYjI5Y2UwNDk3MjIxNzQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5Mzk3M2YxNjhkOGQyNjkxNjMxYTk5MzI2MWIyOWNlMDQ5NzIyMTc0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZmV3MGNrM2t4amR5dHJyMnZueWNkam5uc3lqdTNwd3NyZ3N1cHFqMiJdfX1dLCJmZWUiOjEuODhlLTA1LCJoZXgiOiIwMjAwMDAwMDAxYTBhZDFhNjFiNTE4ZTgyNWFmMjc1ZDZhMTkzNWE2ZTA5MDViNWIzNGEyZDFlNGU3ZWRhMmUzOTI4ZGFmNWVhNzAwMDAwMDAwNmE0NzMwNDQwMjIwNjk0M2JkYzhjZjgyYWVmYjJmZTU5NWY3MjVjY2Y5YmNhMTJlNzM3NmRjOGYwMmQ2NjQ2MGE4NzM5OGU3NzE1ZDAyMjA3NjgxNzE1NGVjOTc5MTJjNDRkZjhkMmQ4MDMzM2NjM2Y4YTRmNzU5NGFkYjNiMjkyMTk1MDg3MDlmM2Q3YmU5NDEyMTAyMTBjNTBlOTQzMmY0MWZlMjllOTk0YjIwZTRlN2Y3MTE2NzNlYjI0M2I5NTI3NjNlY2Q4YjdhY2JjMTc2ZTYwNmZmZmZmZmZmMDEyODhmOTgwMDAwMDAwMDAwMTk3NmE5MTQ5Mzk3M2YxNjhkOGQyNjkxNjMxYTk5MzI2MWIyOWNlMDQ5NzIyMTc0ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsImhhc2giOiJlM2U0OWZkNDFhMzM4YTQ4ZmVmODZkNjJiZDE4YTQ5NjIzZTJkMmJjYzUxMjEzNmNiZjdkNzNmNzYzYWExZDVhIiwidmVyc2lvbiI6Miwic2l6ZSI6MjEzNCwibG9ja3RpbWUiOjQyLCJ2aW4iOlt7InR4aWQiOiI2NzczZmQ4MWMyZjZlZDI1ODdiZDYxYTZlMGM2YTg3M2Y5ZjJiNjIyZmM1ZTljMWY4NTA0ZDA2MjEwYzNjNGFmIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRkZTdhZTdiMTBkNmQ2ZDliYmMwMTcwYWIyYzJlZDQ0NTBlZDRjYjE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRkZTdhZTdiMTBkNmQ2ZDliYmMwMTcwYWIyYzJlZDQ0NTBlZDRjYjE4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjY3NzNmZDgxYzJmNmVkMjU4N2JkNjFhNmUwYzZhODczZjlmMmI2MjJmYzVlOWMxZjg1MDRkMDYyMTBjM2M0YWYiLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDcxYTcxNDllZTBiNGUxNDhkNzdhYWRiNGMwZTcxYTEyZjBmMzgyYTk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDcxYTcxNDllZTBiNGUxNDhkNzdhYWRiNGMwZTcxYTEyZjBmMzgyYTk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOjIsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZmU4MjdjNjUzODMyMDczOTc4YjdiZDU4ZWVjODQwZWU0NGJjNjcwODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI2NzczZmQ4MWMyZjZlZDI1ODdiZDYxYTZlMGM2YTg3M2Y5ZjJiNjIyZmM1ZTljMWY4NTA0ZDA2MjEwYzNjNGFmIiwidm91dCI6Mywic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTQxYjIxMzQ5YjgwMTllYzczOGE4ZGM2ZjJmYzQyZGFjYmIwNjQ0MWQwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTQxYjIxMzQ5YjgwMTllYzczOGE4ZGM2ZjJmYzQyZGFjYmIwNjQ0MWQwODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjY3NzNmZDgxYzJmNmVkMjU4N2JkNjFhNmUwYzZhODczZjlmMmI2MjJmYzVlOWMxZjg1MDRkMDYyMTBjM2M0YWYiLCJ2b3V0Ijo0LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNDJhMThlZjA2YjFiNzFiMmUzNzk3YTk2YWNlOTBhMDRiOWIyOGMxMmI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNDJhMThlZjA2YjFiNzFiMmUzNzk3YTk2YWNlOTBhMDRiOWIyOGMxMmI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOjUsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0ZDA3MzZlZTkwMTE3ZjRlZDdlOWJhZDVhZTA5YjBhYjBjODkyOTZiNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0ZDA3MzZlZTkwMTE3ZjRlZDdlOWJhZDVhZTA5YjBhYjBjODkyOTZiNDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI2NzczZmQ4MWMyZjZlZDI1ODdiZDYxYTZlMGM2YTg3M2Y5ZjJiNjIyZmM1ZTljMWY4NTA0ZDA2MjEwYzNjNGFmIiwidm91dCI6Niwic2NyaXB0U2lnIjp7ImFzbSI6Ijc0NjM3NmE5MTRjZDZkNTY3ZGM2MzRhYzhmYTg4NTYxZTg3YjI4ZjkxMThhOGFkYTdhODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgiLCJoZXgiOiI0NDc0NjM3NmE5MTRjZDZkNTY3ZGM2MzRhYzhmYTg4NTYxZTg3YjI4ZjkxMThhOGFkYTdhODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjgifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjY3NzNmZDgxYzJmNmVkMjU4N2JkNjFhNmUwYzZhODczZjlmMmI2MjJmYzVlOWMxZjg1MDRkMDYyMTBjM2M0YWYiLCJ2b3V0Ijo3LCJzY3JpcHRTaWciOnsiYXNtIjoiNzQ2Mzc2YTkxNGE3M2Q3MDg5ZTQ4YWJmMzdiNGRiMTVjZmViNGJkNDU3YjVhMTA4MWY4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCIsImhleCI6IjQ0NzQ2Mzc2YTkxNGE3M2Q3MDg5ZTQ4YWJmMzdiNGRiMTVjZmViNGJkNDU3YjVhMTA4MWY4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OCJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOjgsInNjcmlwdFNpZyI6eyJhc20iOiI3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4IiwiaGV4IjoiNDQ3NDYzNzZhOTE0YTc4N2EyNzc0Y2IxYzNiNTg3ZjkwNDhmNjZlN2RjOWY1NzQzNDQ3Mzg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4In0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiI5OGFmYjI5ZmMzMzgzMGRkM2NkYzk5NmJlNTExMDhjZjEyYThhMDY3ZGEyNTQ1NDVlZmFhYWRmMGQyNDc2NDE1Iiwidm91dCI6MjEsInNjcmlwdFNpZyI6eyJhc20iOiI3OTRhYmU5YmI2YzFiMDE0M2VkYjQ0MWFjMzEwOGMzNmQ3NDcwOWFlMjQwZTBlMGJiMmVkYThlYWI1MzczNzljNTczYjQ2NThlNDYxNjlmMmFmNTM3YjU3YmZmNDM4N2UxYzg4MDYwNjMxOGZlNGI4ZTExZWNjYTQ4NTViMWI4NFtBTEx8Rk9SS0lEfFVUWE9TXSAwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjMiLCJoZXgiOiI0MTc5NGFiZTliYjZjMWIwMTQzZWRiNDQxYWMzMTA4YzM2ZDc0NzA5YWUyNDBlMGUwYmIyZWRhOGVhYjUzNzM3OWM1NzNiNDY1OGU0NjE2OWYyYWY1MzdiNTdiZmY0Mzg3ZTFjODgwNjA2MzE4ZmU0YjhlMTFlY2NhNDg1NWIxYjg0NjEyMTAyMTY4OTIxNDE1NzQ4Mzc4OTMzMTU4NzBiZDA1MWRhNWYxNTcxOTlmYzU3YWEzODMwN2E1NzFjZjA2ODY5NDM2MyJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX0seyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOjEwLCJzY3JpcHRTaWciOnsiYXNtIjoiYjk1NzQyODRkY2FiYzhjNzlkNDBkMDRlNDUxOThjOTJjMjdkMTRjYWMyMTBlNjBmNDNhODdhNmEwZGNlZGZhOTAzN2U4OTRjMzU5ZmQzNDVjYWY1NGVjN2VhYzRjOWJjYzZiYjFjNWNhZGExYjQ3OWFmY2UxZDA5MzlhZGJjYTNbQUxMfEZPUktJRHxVVFhPU10gMDIxNjg5MjE0MTU3NDgzNzg5MzMxNTg3MGJkMDUxZGE1ZjE1NzE5OWZjNTdhYTM4MzA3YTU3MWNmMDY4Njk0MzYzIiwiaGV4IjoiNDFiOTU3NDI4NGRjYWJjOGM3OWQ0MGQwNGU0NTE5OGM5MmMyN2QxNGNhYzIxMGU2MGY0M2E4N2E2YTBkY2VkZmE5MDM3ZTg5NGMzNTlmZDM0NWNhZjU0ZWM3ZWFjNGM5YmNjNmJiMWM1Y2FkYTFiNDc5YWZjZTFkMDkzOWFkYmNhMzYxMjEwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjMifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjEwMy40MTgwNDY3NywibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NCBPUF9FUVVBTCIsImhleCI6ImFhMjA1MGZjMTZmZmIzNzdjZDRlMWQ4YjRkZWZhMzc1NmQxMjIxYmFjZmUzMzA0ZTc3MDVkZTQxNjdkNzdjMWJhMzY0ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGcwYzlobGtkbXU2bnNhM2R4N2xnbTRkNWZ6cndrMHV2Y3l1YWM5bWVxazA0bXVydzNrZ3loZjVtbjd4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDg3Mjg0MiJ9fSx7InZhbHVlIjo5Mi4xNjIxMzM4NiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMCBPUF9FUVVBTCIsImhleCI6ImFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjB3ZzlwMGx5NXRmMm5mOXY3NzNoM2Y5eGxyanB3eHFmN25oMjlzNXQ4M2N6NjVzN2t0cWFocjl5YWdkIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDM0MjQ4NyJ9fSx7InZhbHVlIjo3MS4xMDg2ODY5MywibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNiBPUF9FUVVBTCIsImhleCI6ImFhMjAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmswbjA2ZDR2ejZ5ZGZwbXNuemp3cHN5Y2hsYTNtcGdlNWdweDlteHlqMmMwYWh2c3FydmRmeGN2N3lrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzM1MDQ5MyJ9fSx7InZhbHVlIjozNi4wMzU2MjU4MSwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTY5NzkyMyJ9fSx7InZhbHVlIjoxNy40ODc2NTUwOSwibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZCBPUF9FUVVBTCIsImhleCI6ImFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnN0cWttNTRkdHZucHlxeHQ1bTVuN3Nqc240ZW5ybHhjNTI2eHl4bG5qa2F5Y2R6ZmV1NjlyZXl6bXF4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODIzOTgzIn19LHsidmFsdWUiOjExLjM0MDYwMzY0LCJuIjo1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBiMzQ3ZDY4YzhkMmNkYjMyMDY2YmU5YWY4MDRjNmZmYjhhYmJiNDQxYmRmMzZjYWYzODM0MzZiY2ZjYWEzNWNiIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGIzNDdkNjhjOGQyY2RiMzIwNjZiZTlhZjgwNGM2ZmZiOGFiYmI0NDFiZGYzNmNhZjM4MzQzNmJjZmNhYTM1Y2I4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3ZTUwNDV2MzVrZGt2c3hkMDU2bHF6dmRsYWM0d2E1Z3g3bHhtOTA4cTZyZDA4dTRnNnVrbjM3Mmdnd2wiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI1MzQzNDYifX0seyJ2YWx1ZSI6MTAuNTM2OTU0NCwibiI6Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2MyYmNjYTIwODlhOWYwZjhkYWVjMGQzYjQ1YTZjNTI1ODFjNTY3NjY1Y2M1MWViZGE5YTIwMmQ4YTMzZGNmYyBPUF9FUVVBTCIsImhleCI6ImFhMjA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZDd6aG45enB6ZGY3cnVkNG1xZDhkejZkM2Y5czh6a3dlanVjNTB0bTJkenF0djJ4MHcwYzU0eTM5aHA2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDk2NDgwIn19LHsidmFsdWUiOjUuMjEzMzU4NjUsIm4iOjcsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwZjFjMzNiYzdkYTA5OTFkMjZmOGJiZjE1MGM5MDM1NjhiMmMzZGM4NzI3MzcwODYzZjRjM2Q5Zjk2NTViMzc5MTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cDBjdXh3NzhtZ3llcjVuMDN3bDMycnlzeDQ1dDlzN3VzdW5ud3pycjducGFuN3Q5dHZtZXpyYXUzanVyOSJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjI0NTY0MyJ9fSx7InZhbHVlIjo0LjM5OTU5MjI1LCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YW15bmxlcHYzcG5nbnE2cmhxNTk1eDBsZm5nNncwd3A4NWpxM3JydjJ3amNsdmh0M3VneDRrYWo0aHUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMDczMDEifX0seyJ2YWx1ZSI6OGUtMDYsIm4iOjksInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI5MTcwIn19LHsidmFsdWUiOjM3LjMyNDY3MDM0LCJuIjoxMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZjBlZGN4bGZuemt0bTc3ZXFnODc0anVwMjh0YTZhNnU1cnk1c2Z1OSJdfX1dLCJmZWUiOjIuMTM0ZS0wNSwiaGV4IjoiMDIwMDAwMDAwYmFmYzRjMzEwNjJkMDA0ODUxZjljNWVmYzIyYjZmMmY5NzNhOGM2ZTBhNjYxYmQ4NzI1ZWRmNmMyODFmZDczNjcwMDAwMDAwMDQ1NDQ3NDYzNzZhOTE0ZGU3YWU3YjEwZDZkNmQ5YmJjMDE3MGFiMmMyZWQ0NDUwZWQ0Y2IxODg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZhZmM0YzMxMDYyZDAwNDg1MWY5YzVlZmMyMmI2ZjJmOTczYThjNmUwYTY2MWJkODcyNWVkZjZjMjgxZmQ3MzY3MDEwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDcxYTcxNDllZTBiNGUxNDhkNzdhYWRiNGMwZTcxYTEyZjBmMzgyYTk4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmYWZjNGMzMTA2MmQwMDQ4NTFmOWM1ZWZjMjJiNmYyZjk3M2E4YzZlMGE2NjFiZDg3MjVlZGY2YzI4MWZkNzM2NzAyMDAwMDAwNDU0NDc0NjM3NmE5MTRmZTgyN2M2NTM4MzIwNzM5NzhiN2JkNThlZWM4NDBlZTQ0YmM2NzA4ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZmFmYzRjMzEwNjJkMDA0ODUxZjljNWVmYzIyYjZmMmY5NzNhOGM2ZTBhNjYxYmQ4NzI1ZWRmNmMyODFmZDczNjcwMzAwMDAwMDQ1NDQ3NDYzNzZhOTE0MWIyMTM0OWI4MDE5ZWM3MzhhOGRjNmYyZmM0MmRhY2JiMDY0NDFkMDg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZhZmM0YzMxMDYyZDAwNDg1MWY5YzVlZmMyMmI2ZjJmOTczYThjNmUwYTY2MWJkODcyNWVkZjZjMjgxZmQ3MzY3MDQwMDAwMDA0NTQ0NzQ2Mzc2YTkxNDJhMThlZjA2YjFiNzFiMmUzNzk3YTk2YWNlOTBhMDRiOWIyOGMxMmI4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmYWZjNGMzMTA2MmQwMDQ4NTFmOWM1ZWZjMjJiNmYyZjk3M2E4YzZlMGE2NjFiZDg3MjVlZGY2YzI4MWZkNzM2NzA1MDAwMDAwNDU0NDc0NjM3NmE5MTRkMDczNmVlOTAxMTdmNGVkN2U5YmFkNWFlMDliMGFiMGM4OTI5NmI0ODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZmFmYzRjMzEwNjJkMDA0ODUxZjljNWVmYzIyYjZmMmY5NzNhOGM2ZTBhNjYxYmQ4NzI1ZWRmNmMyODFmZDczNjcwNjAwMDAwMDQ1NDQ3NDYzNzZhOTE0Y2Q2ZDU2N2RjNjM0YWM4ZmE4ODU2MWU4N2IyOGY5MTE4YThhZGE3YTg4YWM2N2MwZDFjMGNlODhjMjUyODhjMGNkYzBjNzg4YzBjNmMwZDA5NWMwYzZjMGNjOTQ5MDUzOTUwMmU4MDM5NmMwY2M3Yzk0YzBkMzk1N2NhMjY4ZmZmZmZmZmZhZmM0YzMxMDYyZDAwNDg1MWY5YzVlZmMyMmI2ZjJmOTczYThjNmUwYTY2MWJkODcyNWVkZjZjMjgxZmQ3MzY3MDcwMDAwMDA0NTQ0NzQ2Mzc2YTkxNGE3M2Q3MDg5ZTQ4YWJmMzdiNGRiMTVjZmViNGJkNDU3YjVhMTA4MWY4OGFjNjdjMGQxYzBjZTg4YzI1Mjg4YzBjZGMwYzc4OGMwYzZjMGQwOTVjMGM2YzBjYzk0OTA1Mzk1MDJlODAzOTZjMGNjN2M5NGMwZDM5NTdjYTI2OGZmZmZmZmZmYWZjNGMzMTA2MmQwMDQ4NTFmOWM1ZWZjMjJiNmYyZjk3M2E4YzZlMGE2NjFiZDg3MjVlZGY2YzI4MWZkNzM2NzA4MDAwMDAwNDU0NDc0NjM3NmE5MTRhNzg3YTI3NzRjYjFjM2I1ODdmOTA0OGY2NmU3ZGM5ZjU3NDM0NDczODhhYzY3YzBkMWMwY2U4OGMyNTI4OGMwY2RjMGM3ODhjMGM2YzBkMDk1YzBjNmMwY2M5NDkwNTM5NTAyZTgwMzk2YzBjYzdjOTRjMGQzOTU3Y2EyNjhmZmZmZmZmZjE1NjQ0N2QyZjBhZGFhZWY0NTQ1MjVkYTY3YTBhODEyY2YwODExZTU2Yjk5ZGMzY2RkMzAzOGMzOWZiMmFmOTgxNTAwMDAwMDY0NDE3OTRhYmU5YmI2YzFiMDE0M2VkYjQ0MWFjMzEwOGMzNmQ3NDcwOWFlMjQwZTBlMGJiMmVkYThlYWI1MzczNzljNTczYjQ2NThlNDYxNjlmMmFmNTM3YjU3YmZmNDM4N2UxYzg4MDYwNjMxOGZlNGI4ZTExZWNjYTQ4NTViMWI4NDYxMjEwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjNmZmZmZmZmZmFmYzRjMzEwNjJkMDA0ODUxZjljNWVmYzIyYjZmMmY5NzNhOGM2ZTBhNjYxYmQ4NzI1ZWRmNmMyODFmZDczNjcwYTAwMDAwMDY0NDFiOTU3NDI4NGRjYWJjOGM3OWQ0MGQwNGU0NTE5OGM5MmMyN2QxNGNhYzIxMGU2MGY0M2E4N2E2YTBkY2VkZmE5MDM3ZTg5NGMzNTlmZDM0NWNhZjU0ZWM3ZWFjNGM5YmNjNmJiMWM1Y2FkYTFiNDc5YWZjZTFkMDkzOWFkYmNhMzYxMjEwMjE2ODkyMTQxNTc0ODM3ODkzMzE1ODcwYmQwNTFkYTVmMTU3MTk5ZmM1N2FhMzgzMDdhNTcxY2YwNjg2OTQzNjNmZmZmZmZmZjBiODU2YTZiNjgwMjAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZThhNWE0YTAwYWEyMDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQ4NzhhNDE1NDI1MDIwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVkNzQyNDIwMGFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODdkNTNlZDdhNzAxMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlZGQxZjMzMDBhYTIwMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNjg3NTUwMGNhZDYwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTgzZTgxOTAwYWEyMDY3YWQxYzY0OWIwOWJhNThiYTE5ZWIzYWQzNWY0MjQxYmEwNjBlOGJiODk3YWRjZjZiZGRkMzZhODBkOGU4YTY4NzQ1MGIzYzY4MDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmVhZjkyMGMwMGFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODc0YzYzOTg0MzAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlNGEyNzA4MDBhYTIwYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYjg3ZDAxZGNlM2UwMDAwMDAwMDRhZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZTYwOTMwNzAwYWEyMDdjMmJjY2EyMDg5YTlmMGY4ZGFlYzBkM2I0NWE2YzUyNTgxYzU2NzY2NWNjNTFlYmRhOWEyMDJkOGEzM2RjZmM4NzM5ZjQxMjFmMDAwMDAwMDA0YWVmNjZmMzg3ZWQ4OTk2NzgwODM0NGY1ZDU4MjE1MDYwNzk2ZTNlODdjYjIzNmY5YTE2NWM0Y2Y4NTBmNzMzOGFiMzEwZmU4YmJmMDMwMGFhMjBmMWMzM2JjN2RhMDk5MWQyNmY4YmJmMTUwYzkwMzU2OGIyYzNkYzg3MjczNzA4NjNmNGMzZDlmOTY1NWIzNzkxODdiOTNlMzkxYTAwMDAwMDAwNGFlZjY2ZjM4N2VkODk5Njc4MDgzNDRmNWQ1ODIxNTA2MDc5NmUzZTg3Y2IyMzZmOWExNjVjNGNmODUwZjczMzhhYjMxMGZlYzUyOTAzMDBhYTIwM2JiMjRmZjkwYjIyMTlhMjYwZDBlZTBhMTY4NjdmZDMzNDY5Y2Y3MDRmNDkwMjIzMWIxNGU5NjNlY2JhZTNjNDg3MjAwMzAwMDAwMDAwMDAwMDNlZWY2NmYzODdlZDg5OTY3ODA4MzQ0ZjVkNTgyMTUwNjA3OTZlM2U4N2NiMjM2ZjlhMTY1YzRjZjg1MGY3MzM4YWIzMTBmZGQyMjM3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYzVhZWQ3OGRlMDAwMDAwMDAxOTc2YTkxNDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDc4OGFjMmEwMDAwMDAifSx7InR4aWQiOiJlNTU3OWFmYmRjZTIxMDE4NWFiZmFmYjE2MjdjZTBiOGRkYWUwY2FkYzc1MzdhNjFmNjA2NWQyNDIzMDZhNWU5IiwiaGFzaCI6ImU1NTc5YWZiZGNlMjEwMTg1YWJmYWZiMTYyN2NlMGI4ZGRhZTBjYWRjNzUzN2E2MWY2MDY1ZDI0MjMwNmE1ZTkiLCJ2ZXJzaW9uIjoyLCJzaXplIjoxNjQsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiIzZDJlMjBmMGQ3NTUyMTI5ZDIwNmM3M2FjYzk1MDM0YjJjNjRkNjVmMDI3NjgyYjg5OWFkNDNhMzVhZjdkMzkzIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEiLCJoZXgiOiI0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEifSwic2VxdWVuY2UiOjF9XSwidm91dCI6W3sidmFsdWUiOjAuMDA1NjU4NDYsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dLCJmZWUiOjEuNjRlLTA2LCJoZXgiOiIwMjAwMDAwMDAxOTNkM2Y3NWFhMzQzYWQ5OWI4ODI3NjAyNWZkNjY0MmM0YjAzOTVjYzNhYzcwNmQyMjkyMTU1ZDdmMDIwMmUzZDAwMDAwMDAwNDU0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEwMTAwMDAwMDAxNTZhMjA4MDAwMDAwMDAwMDIzYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NzAwMDAwMDAwIn0seyJ0eGlkIjoiZTkwODM5MzQxYTViMzJiZTA5ZTNiMWVkZGE2MGQwNTE1ZWZlOTU0YTBhNWIxNjc2NmU4YzAwOTk2YjQ2ZmU1MCIsImhhc2giOiJlOTA4MzkzNDFhNWIzMmJlMDllM2IxZWRkYTYwZDA1MTVlZmU5NTRhMGE1YjE2NzY2ZThjMDA5OTZiNDZmZTUwIiwidmVyc2lvbiI6Miwic2l6ZSI6MTY0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiOGI2N2YzNGFiYjM1NjA4ZDMxM2Y4NGEwMDI5MmQ3MjkxZDM5MjUyNGQ1YjRmMmE5NjFhNGUwNjdjNTI2ZmM3YiIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIiwiaGV4IjoiNDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIn0sInNlcXVlbmNlIjoxfV0sInZvdXQiOlt7InZhbHVlIjowLjAwNTczMzksIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dLCJmZWUiOjEuNjRlLTA2LCJoZXgiOiIwMjAwMDAwMDAxN2JmYzI2YzU2N2UwYTQ2MWE5ZjJiNGQ1MjQyNTM5MWQyOWQ3OTIwMmEwODQzZjMxOGQ2MDM1YmI0YWYzNjc4YjAwMDAwMDAwNDU0NDUxYjI3NWMzNTE5ZGM0NTE5ZDAyNDAwMjAyYTQwMGMwYzY3YjUyNzk5M2EwNjNjMGM3YzBjZDg4YzBjNjAyMjgxMTk1MDRjNjg1NzQ0Zjk2NmVhMDYzNzg3NzY4YzBjNmMwY2M5NDZlYTI2OTZkNjdjMGNjMDA5ZGMwY2QwMTZhODg2ODc1NTEwMTAwMDAwMDAxY2ViZjA4MDAwMDAwMDAwMDIzYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NzAwMDAwMDAwIn0seyJ0eGlkIjoiZWI2ZWE4ZDZiOTljZWQ3MGVkZWUwOWNjNjkwZWZkY2E4NDM3NDhmOGE4ZjEyMDZjZGQyMGQxNDRiNjdmM2UyMiIsImhhc2giOiJlYjZlYThkNmI5OWNlZDcwZWRlZTA5Y2M2OTBlZmRjYTg0Mzc0OGY4YThmMTIwNmNkZDIwZDE0NGI2N2YzZTIyIiwidmVyc2lvbiI6MSwic2l6ZSI6MjI2LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiNDgzZDI1OTVjNGY3YjEyOGJjOTkxNDYxNjYyZTk3N2I2NTJiNjlhMTQyNDRjOThkMDYzZDljODY1ODM0ZWMyNCIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwZDFmNTNjNjBiOGZkYmNmZjI1OWE4YTdiYTJkNTFmZTYxMDdkNDA1OWFmYzkzOWQyYzAxMTAzODlhMWJiZmFiNTAyMjA2MDgzNjEwM2NiY2E3OGQ5ZmZlOWIwYzJkZjIzZmEzOTBjYzM3ODUzNjY2MmFkODU2MGQ1ODIyYWY0NDY4NmM0W0FMTHxGT1JLSURdIDAzYTE4ZGZiZjEzMjU5ZDU2OWYwNzFhNjNjMjQ2ZTM0OTUzNTcxZWFkYWM2ZDQwZjRhZDY5ODkzMDM1MGYyNzZhZCIsImhleCI6IjQ4MzA0NTAyMjEwMGQxZjUzYzYwYjhmZGJjZmYyNTlhOGE3YmEyZDUxZmU2MTA3ZDQwNTlhZmM5MzlkMmMwMTEwMzg5YTFiYmZhYjUwMjIwNjA4MzYxMDNjYmNhNzhkOWZmZTliMGMyZGYyM2ZhMzkwY2MzNzg1MzY2NjJhZDg1NjBkNTgyMmFmNDQ2ODZjNDQxMjEwM2ExOGRmYmYxMzI1OWQ1NjlmMDcxYTYzYzI0NmUzNDk1MzU3MWVhZGFjNmQ0MGY0YWQ2OTg5MzAzNTBmMjc2YWQifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9XSwidm91dCI6W3sidmFsdWUiOjAuMTU2NDQ3OTMsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAyMjEyNWU0NDQxZGI0YTc0NWQ1MGMzNjhjZjZkN2RmZTExZTZkY2VlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MjIxMjVlNDQ0MWRiNGE3NDVkNTBjMzY4Y2Y2ZDdkZmUxMWU2ZGNlZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcTNweWhqeWc4ZDU1YXphMnJwazNubWQwaGxwcmVrdWFjc3RweTdod2YiXX19LHsidmFsdWUiOjAuMzQ2NTYyOTQsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhMzI0ZGYxMTRhMjhkM2RiNTdlYmQwYmVhZGM3YjMwNWM4ODM1YmM0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YTMyNGRmMTE0YTI4ZDNkYjU3ZWJkMGJlYWRjN2IzMDVjODgzNWJjNDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxejNqZmhjM2ZnNWQ4azZoYTBndGF0dzhrdnp1M3E2bWNzbXB2OGRndHYiXX19XSwiZmVlIjoyLjI2ZS0wNiwiaGV4IjoiMDEwMDAwMDAwMTI0ZWMzNDU4ODY5YzNkMDY4ZGM5NDQ0MmExNjkyYjY1N2I5NzJlNjY2MTE0OTliYzI4YjFmN2M0OTUyNTNkNDgwMDAwMDAwMDZiNDgzMDQ1MDIyMTAwZDFmNTNjNjBiOGZkYmNmZjI1OWE4YTdiYTJkNTFmZTYxMDdkNDA1OWFmYzkzOWQyYzAxMTAzODlhMWJiZmFiNTAyMjA2MDgzNjEwM2NiY2E3OGQ5ZmZlOWIwYzJkZjIzZmEzOTBjYzM3ODUzNjY2MmFkODU2MGQ1ODIyYWY0NDY4NmM0NDEyMTAzYTE4ZGZiZjEzMjU5ZDU2OWYwNzFhNjNjMjQ2ZTM0OTUzNTcxZWFkYWM2ZDQwZjRhZDY5ODkzMDM1MGYyNzZhZGZmZmZmZmZmMDI3OWI4ZWUwMDAwMDAwMDAwMTk3NmE5MTQyMjEyNWU0NDQxZGI0YTc0NWQ1MGMzNjhjZjZkN2RmZTExZTZkY2VlODhhYzI2ZDAxMDAyMDAwMDAwMDAxOTc2YTkxNGEzMjRkZjExNGEyOGQzZGI1N2ViZDBiZWFkYzdiMzA1Yzg4MzViYzQ4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJlZTJiYmIyM2M4OGZkZDY4YzFlMmIwMDhhNTQ2YTA5OGIyZjI5NzE4ZWI0YTZhNGUyMzhjODczOGIyZTBjNmUxIiwiaGFzaCI6ImVlMmJiYjIzYzg4ZmRkNjhjMWUyYjAwOGE1NDZhMDk4YjJmMjk3MThlYjRhNmE0ZTIzOGM4NzM4YjJlMGM2ZTEiLCJ2ZXJzaW9uIjoyLCJzaXplIjoyMjUsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI2NmUzMzZhODQ3MzE4OTE2MzUwM2ExMmFmOWYzZDE5YWE2ZGE0MDM3YzU0NmIyMzRhZmZiZDRhMmY0ZDVhZTQ4Iiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwNGM0YzBlMTgzMzBiMTdkNGE2ZGZjZmUyZTk4YWRmNGViZjA1NmFhMzg1ZTlmODgzZTNjMGNjMGNiNTdmOWYwMDAyMjAyODc1YmVkYzIwZjUwNmQ4MTViNzZlYjdiN2Y0ZmQ5NjM4ZmUzNjU1ZmIwNzZhNmVlZDMyZTk5YTFjMTA1MDVmW0FMTHxGT1JLSURdIDAyMWYxMTQ4YTcyN2RjZDgxOTNmZWFmYmMyMzYwZWQ0OTFiYzcwNzFmYzA4MGU1YjM0ZDA5ODEwMDM2M2M4YTk1ZCIsImhleCI6IjQ3MzA0NDAyMjA0YzRjMGUxODMzMGIxN2Q0YTZkZmNmZTJlOThhZGY0ZWJmMDU2YWEzODVlOWY4ODNlM2MwY2MwY2I1N2Y5ZjAwMDIyMDI4NzViZWRjMjBmNTA2ZDgxNWI3NmViN2I3ZjRmZDk2MzhmZTM2NTVmYjA3NmE2ZWVkMzJlOTlhMWMxMDUwNWY0MTIxMDIxZjExNDhhNzI3ZGNkODE5M2ZlYWZiYzIzNjBlZDQ5MWJjNzA3MWZjMDgwZTViMzRkMDk4MTAwMzYzYzhhOTVkIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjk3MSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGU4YmZhNTQ3YTJlMmJjZGY4ZjFlMWEzZGFkZTdmMDYzMmYzYzU5ZDYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRlOGJmYTU0N2EyZTJiY2RmOGYxZTFhM2RhZGU3ZjA2MzJmM2M1OWQ2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyNXRsZjI4NXQzdGVodTByY2RybXQwODdwM2o3MHplNmM1Zjg0eHl5dyJdfX0seyJ2YWx1ZSI6Ni41NTM1NDgsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhMzQ5ZDA0NDEyMWUzODg0NDI1OTZkN2E5NzJmNDJhZDQ3NTIxMTIwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YTM0OWQwNDQxMjFlMzg4NDQyNTk2ZDdhOTcyZjQyYWQ0NzUyMTEyMDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxejM1bjV6eXpnMHIzcHp6dDlraDQ5ZTBnMms1dzVzM3lxYThlamNsdGUiXX19XSwiZmVlIjoyLjI4ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMTQ4YWVkNWY0YTJkNGZiYWYzNGIyNDZjNTM3NDBkYWE2OWFkMWYzZjkyYWExMDMzNTE2ODkzMTQ3YTgzNmUzNjYwMTAwMDAwMDZhNDczMDQ0MDIyMDRjNGMwZTE4MzMwYjE3ZDRhNmRmY2ZlMmU5OGFkZjRlYmYwNTZhYTM4NWU5Zjg4M2UzYzBjYzBjYjU3ZjlmMDAwMjIwMjg3NWJlZGMyMGY1MDZkODE1Yjc2ZWI3YjdmNGZkOTYzOGZlMzY1NWZiMDc2YTZlZWQzMmU5OWExYzEwNTA1ZjQxMjEwMjFmMTE0OGE3MjdkY2Q4MTkzZmVhZmJjMjM2MGVkNDkxYmM3MDcxZmMwODBlNWIzNGQwOTgxMDAzNjNjOGE5NWRmZmZmZmZmZjAyZTBhMGM5MDUwMDAwMDAwMDE5NzZhOTE0ZThiZmE1NDdhMmUyYmNkZjhmMWUxYTNkYWRlN2YwNjMyZjNjNTlkNjg4YWNiMGViMGYyNzAwMDAwMDAwMTk3NmE5MTRhMzQ5ZDA0NDEyMWUzODg0NDI1OTZkN2E5NzJmNDJhZDQ3NTIxMTIwODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiZjM3OWYxMzI2MWNmYmNhMjBmMzI5ZGE0MWNmMTAyZjk4NzQ2M2ZiNTA3ZDhjODJmYmZlNGU5OGY1MTk3OWVjZSIsImhhc2giOiJmMzc5ZjEzMjYxY2ZiY2EyMGYzMjlkYTQxY2YxMDJmOTg3NDYzZmI1MDdkOGM4MmZiZmU0ZTk4ZjUxOTc5ZWNlIiwidmVyc2lvbiI6MSwic2l6ZSI6MjkzLCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiYzFiNTZlNWE2OWVkYWU2ZWRjNDdmOTU0OTdjNTVkMjNhM2JkOWQ4MjQ3NTdmMTBhMzIxMWM2MWExNmFjNGYwOCIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ0MDIyMDQ2NGI0MDMxOTEwMDU4YTgyZmE5OGY3NGU5NmUxODRhOTdmNWJlZGUwOGIxOWFlNTlmMDQ3YzFjYmFhYjBkNzUwMjIwNjFmNmZiNjRjNTRmOGVhNjg0YWQ3MTMyOWE0ZTJkMjA4ZGVkYWFiODQ1N2NmNGNkZTUzMTA1NzAyZTUwYWRkMltBTEx8Rk9SS0lEXSAwMjA4YzZiNmQ5N2NhM2Q2MjVmYmE1NmI2NDYwYmRjY2ViM2UwNmY1NTU1YzExODQ1OTBiMzMzOWMwZjU4NzliZTEiLCJoZXgiOiI0NzMwNDQwMjIwNDY0YjQwMzE5MTAwNThhODJmYTk4Zjc0ZTk2ZTE4NGE5N2Y1YmVkZTA4YjE5YWU1OWYwNDdjMWNiYWFiMGQ3NTAyMjA2MWY2ZmI2NGM1NGY4ZWE2ODRhZDcxMzI5YTRlMmQyMDhkZWRhYWI4NDU3Y2Y0Y2RlNTMxMDU3MDJlNTBhZGQyNDEyMTAyMDhjNmI2ZDk3Y2EzZDYyNWZiYTU2YjY0NjBiZGNjZWIzZTA2ZjU1NTVjMTE4NDU5MGIzMzM5YzBmNTg3OWJlMSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MS44NDMyODQwOSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5MGYyMjg1Mzk1MTk5MzE1NDBmMGUxZGUyNWRiYzAwOTM0OThjN2E1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZzB5MnpuajV2ZXg5MnE3cnNhdWZ3bWNxeW5meHg4NTVkODY2Y3k2dCJdfX0seyJ2YWx1ZSI6MC4xMTU4MzA5MywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDIxNTRmMTA2Njc1Mjc2OTNhYWZjNmFjOGY5OTljZDlhN2E3YWYxNDIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQyMTU0ZjEwNjY3NTI3NjkzYWFmYzZhYzhmOTk5Y2Q5YTdhN2FmMTQyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxczRmdWd4dmFmOGR5YTJsMzR2Mzd2ZWVrZDg1N2gzZ2dzNXdhNmdzMCJdfX0seyJ2YWx1ZSI6MC4xMDcyOTM4OCwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDYzYmQyZGFiZTI4MGQxYTgyZGY4NTMxYTEzMDBhMjg5MzA1NTA5MWEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2M2JkMmRhYmUyODBkMWE4MmRmODUzMWExMzAwYTI4OTMwNTUwOTFhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwM202dGR0dTJxZHIycGRscGYzNXljcTUyeW5xNGdmcmd6Y3Z5cTVjOSJdfX0seyJ2YWx1ZSI6MC4xMDM3NzI5OCwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDcxYjQ1NjQ1NTI2NDQ2YThjMmY3Y2E4ZmMzMzA4YmZiMDY2YjE0MzIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3MWI0NTY0NTUyNjQ0NmE4YzJmN2NhOGZjMzMwOGJmYjA2NmIxNDMyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwY21nNGo5MmZqeWQyeHo3bDlnbHNlczMwYXN2NmM1eGduZndjNXNndSJdfX1dLCJmZWUiOjUuOWUtMDYsImhleCI6IjAxMDAwMDAwMDEwODRmYWMxNjFhYzYxMTMyMGFmMTU3NDc4MjlkYmRhMzIzNWRjNTk3NTRmOTQ3ZGM2ZWFlZWQ2OTVhNmViNWMxMDAwMDAwMDA2YTQ3MzA0NDAyMjA0NjRiNDAzMTkxMDA1OGE4MmZhOThmNzRlOTZlMTg0YTk3ZjViZWRlMDhiMTlhZTU5ZjA0N2MxY2JhYWIwZDc1MDIyMDYxZjZmYjY0YzU0ZjhlYTY4NGFkNzEzMjlhNGUyZDIwOGRlZGFhYjg0NTdjZjRjZGU1MzEwNTcwMmU1MGFkZDI0MTIxMDIwOGM2YjZkOTdjYTNkNjI1ZmJhNTZiNjQ2MGJkY2NlYjNlMDZmNTU1NWMxMTg0NTkwYjMzMzljMGY1ODc5YmUxZmZmZmZmZmYwNGQ5YTBmYzBhMDAwMDAwMDAxOTc2YTkxNDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTU4OGFjNzViZWIwMDAwMDAwMDAwMDE5NzZhOTE0MjE1NGYxMDY2NzUyNzY5M2FhZmM2YWM4Zjk5OWNkOWE3YTdhZjE0Mjg4YWNhY2I3YTMwMDAwMDAwMDAwMTk3NmE5MTQ2M2JkMmRhYmUyODBkMWE4MmRmODUzMWExMzAwYTI4OTMwNTUwOTFhODhhYzUyNTg5ZTAwMDAwMDAwMDAxOTc2YTkxNDcxYjQ1NjQ1NTI2NDQ2YThjMmY3Y2E4ZmMzMzA4YmZiMDY2YjE0MzI4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJmM2U3MWQwYjI4Y2Y4YjgxNDNkMGJlOWMwZDI2Yzg0ZWI5Y2E1NzY0OTBiNWYxNjUyODUyODEwNmQ4MzAyYzc5IiwiaGFzaCI6ImYzZTcxZDBiMjhjZjhiODE0M2QwYmU5YzBkMjZjODRlYjljYTU3NjQ5MGI1ZjE2NTI4NTI4MTA2ZDgzMDJjNzkiLCJ2ZXJzaW9uIjoyLCJzaXplIjozOTgsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI5MTI1Y2ZkNmQwM2JlMzJhNTQ1NWIzN2MyYzEyYmNkZDdiYWFmN2NkNTAyYmE5YjM0OGJmMzU2N2E1NWVlZDFhIiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6IjI1ZjA5MGJmZDdjZWU4MzUzMDU5YmQxNGY5ZDI0ZjVlMTkwNzZiYzBhYTliMzBiYWNjODBmZjEwMDc2MGVlODJiY2JhOTc0Y2JkNjZhOGFiNDM1NWY4NTQwYzY3ZmFkOGQ1NDdjZThjMTMyODIyMWIxYWY0MDNkMTczOTZmZGUyW0FMTHxGT1JLSURdIDAzZWY1ZDZhYTQzZGU0YzliYzhhMWI3ZjJjMmUzMjVlMGI5YjI4NjY3ODI3NzkyNDgyMjRiZTRkMmMwYzYzMGY1ZCIsImhleCI6IjQxMjVmMDkwYmZkN2NlZTgzNTMwNTliZDE0ZjlkMjRmNWUxOTA3NmJjMGFhOWIzMGJhY2M4MGZmMTAwNzYwZWU4MmJjYmE5NzRjYmQ2NmE4YWI0MzU1Zjg1NDBjNjdmYWQ4ZDU0N2NlOGMxMzI4MjIxYjFhZjQwM2QxNzM5NmZkZTI0MTIxMDNlZjVkNmFhNDNkZTRjOWJjOGExYjdmMmMyZTMyNWUwYjliMjg2Njc4Mjc3OTI0ODIyNGJlNGQyYzBjNjMwZjVkIn0sInNlcXVlbmNlIjowfSx7InR4aWQiOiJkMWQxMGY4NTRlM2RjZDc2NTJlODVhOTU5OGUzZGM4MjUwZjk0ODA3ZWM3ZDM3OWRmMWE0YWMxZjgzZjNhYjE4Iiwidm91dCI6MSwic2NyaXB0U2lnIjp7ImFzbSI6ImRjODVkYjZjYmJmODc3ZmE5ODFiYmYzZGI2NzYzNWI2ZDZkNzFkZGU0NDk4MjI4NTczMDFiZTFjMGQ4MTA4ODYyMjYxYmEyMzM4ZWQ1MmE4N2JlYmU2YmYxNzAzYjM5OTUzM2Y3ZTIzMTg1NGZjNzI3ZmQ3NDFjMGJhNjY5MDlhW0FMTHxGT1JLSURdIDAzZWY1ZDZhYTQzZGU0YzliYzhhMWI3ZjJjMmUzMjVlMGI5YjI4NjY3ODI3NzkyNDgyMjRiZTRkMmMwYzYzMGY1ZCIsImhleCI6IjQxZGM4NWRiNmNiYmY4NzdmYTk4MWJiZjNkYjY3NjM1YjZkNmQ3MWRkZTQ0OTgyMjg1NzMwMWJlMWMwZDgxMDg4NjIyNjFiYTIzMzhlZDUyYTg3YmViZTZiZjE3MDNiMzk5NTMzZjdlMjMxODU0ZmM3MjdmZDc0MWMwYmE2NjkwOWE0MTIxMDNlZjVkNmFhNDNkZTRjOWJjOGExYjdmMmMyZTMyNWUwYjliMjg2Njc4Mjc3OTI0ODIyNGJlNGQyYzBjNjMwZjVkIn0sInNlcXVlbmNlIjowfV0sInZvdXQiOlt7InZhbHVlIjoxZS0wNSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDk3ODczNDUzMjI4YWNlMGM1ZGJlZjdmYjY2NjUzNzA0YzBjOTQ0ZTYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5Nzg3MzQ1MzIyOGFjZTBjNWRiZWY3ZmI2NjY1MzcwNGMwYzk0NGU2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6dGN3ZHpueTI5dnVyemFobW1sa2VuOXh1enZwajJ5dWN1bnBkMm15NiJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiNWE0ZjZiMjUyNDNjMWEyZGFiYjI0MzRlM2Q5ZTU3NGY2NWMzMTc2NGNlMGU3ZWI0MTI3YTQ2ZmE3NDY1NzY5MSIsImFtb3VudCI6IjAiLCJuZnQiOnsiY2FwYWJpbGl0eSI6Im5vbmUiLCJjb21taXRtZW50IjoiMjIwMTAyIn19fSx7InZhbHVlIjowLjA3MTM4NDE5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODY0MDM2NTdmMGM3YjE3ODllYjIzNDcyZjEwNzI1MDYxOTQwZjdiNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpyeXFkamg3cnJtejd5N2tnNjg5dWc4eTVycGpzOGhrczVtbWV4YTQzIl19fV0sImZlZSI6My45OWUtMDYsImhleCI6IjAyMDAwMDAwMDIxYWVkNWVhNTY3MzViZjQ4YjNhOTJiNTBjZGY3YWE3YmRkYmMxMjJjN2NiMzU1NTQyYWUzM2JkMGQ2Y2YyNTkxMDEwMDAwMDA2NDQxMjVmMDkwYmZkN2NlZTgzNTMwNTliZDE0ZjlkMjRmNWUxOTA3NmJjMGFhOWIzMGJhY2M4MGZmMTAwNzYwZWU4MmJjYmE5NzRjYmQ2NmE4YWI0MzU1Zjg1NDBjNjdmYWQ4ZDU0N2NlOGMxMzI4MjIxYjFhZjQwM2QxNzM5NmZkZTI0MTIxMDNlZjVkNmFhNDNkZTRjOWJjOGExYjdmMmMyZTMyNWUwYjliMjg2Njc4Mjc3OTI0ODIyNGJlNGQyYzBjNjMwZjVkMDAwMDAwMDAxOGFiZjM4MzFmYWNhNGYxOWQzNzdkZWMwNzQ4Zjk1MDgyZGNlMzk4OTU1YWU4NTI3NmNkM2Q0ZTg1MGZkMWQxMDEwMDAwMDA2NDQxZGM4NWRiNmNiYmY4NzdmYTk4MWJiZjNkYjY3NjM1YjZkNmQ3MWRkZTQ0OTgyMjg1NzMwMWJlMWMwZDgxMDg4NjIyNjFiYTIzMzhlZDUyYTg3YmViZTZiZjE3MDNiMzk5NTMzZjdlMjMxODU0ZmM3MjdmZDc0MWMwYmE2NjkwOWE0MTIxMDNlZjVkNmFhNDNkZTRjOWJjOGExYjdmMmMyZTMyNWUwYjliMjg2Njc4Mjc3OTI0ODIyNGJlNGQyYzBjNjMwZjVkMDAwMDAwMDAwMmU4MDMwMDAwMDAwMDAwMDAzZmVmOTE3NjY1NzRmYTQ2N2ExMmI0N2UwZWNlNjQxN2MzNjU0ZjU3OWUzZDRlNDNiMmFiMmQxYTNjMjQyNTZiNGY1YTYwMDMyMjAxMDI3NmE5MTQ5Nzg3MzQ1MzIyOGFjZTBjNWRiZWY3ZmI2NjY1MzcwNGMwYzk0NGU2ODhhYzczZWM2YzAwMDAwMDAwMDAxOTc2YTkxNDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQ4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJmNGM4ZThjNmFiMzNjMjlmNzQ2M2QyYmFiNDNhMDdmMDhjNzFhYWE0OWU5NzMxYTQ2M2I2YmEwNjlmZTIwYTNhIiwiaGFzaCI6ImY0YzhlOGM2YWIzM2MyOWY3NDYzZDJiYWI0M2EwN2YwOGM3MWFhYTQ5ZTk3MzFhNDYzYjZiYTA2OWZlMjBhM2EiLCJ2ZXJzaW9uIjoxLCJzaXplIjozMDYsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiJmOTE4MGEyZTNjY2I3YzEzNmE2Y2E5OTNhMTIxZGQxMGFmODNmZGE5NjkxM2IxNTM5MDA0NWE0YmM2YmI3NjQ3Iiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwNWIyNzE4ZGY1NmEzYzcwY2EzMzA2NjVkMDllNjNmZWQ1YTA3ZTc0ODExNjJiZDdlNzE0YjEyMjI3Y2M5NDc0NTAyMjAyMjMxYWJmYTM2NmQwYmVlYzZhYzJmM2JjZWE1ZDZmMGEwYjM4MTEwNmNjZDE1ZDgwYWU2NGIwMWI3M2U1NTFmW0FMTHxGT1JLSURdIDAyZjBmOTVhMTJkYjkzNTIyNDNjNDVmMjczN2U3NjJiYzc3YmIzZWIzNWJmMjg0ZDAwY2VmNDVlZTQyM2ZjZmQ5MCIsImhleCI6IjQ3MzA0NDAyMjA1YjI3MThkZjU2YTNjNzBjYTMzMDY2NWQwOWU2M2ZlZDVhMDdlNzQ4MTE2MmJkN2U3MTRiMTIyMjdjYzk0NzQ1MDIyMDIyMzFhYmZhMzY2ZDBiZWVjNmFjMmYzYmNlYTVkNmYwYTBiMzgxMTA2Y2NkMTVkODBhZTY0YjAxYjczZTU1MWY0MTIxMDJmMGY5NWExMmRiOTM1MjI0M2M0NWYyNzM3ZTc2MmJjNzdiYjNlYjM1YmYyODRkMDBjZWY0NWVlNDIzZmNmZDkwIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjowLjAsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9SRVRVUk4gNzYyZDY2NDEyODNhNmM1NTQ0MjM2NDUwNjE0NDRlNDI0MzYxMzU3MDZlNjQ0ZTJmNDE1YzJkMjU3MDM5MzYzNjMzNjM0NzU1IiwiaGV4IjoiNmEyNDc2MmQ2NjQxMjgzYTZjNTU0NDIzNjQ1MDYxNDQ0ZTQyNDM2MTM1NzA2ZTY0NGUyZjQxNWMyZDI1NzAzOTM2MzYzMzYzNDc1NSIsInR5cGUiOiJudWxsZGF0YSJ9fSx7InZhbHVlIjozLjM2OWUtMDUsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAwNjY2OGMyNjBiMjJjMTc4MjA1YmUyMTlhOGYwZDZjOGY3NDU2Y2U1IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MDY2NjhjMjYwYjIyYzE3ODIwNWJlMjE5YThmMGQ2YzhmNzQ1NmNlNTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXJ4ZHJweHB2M3Z6N3BxdDAzcG4yOHM2bXkwdzN0dnU1dDc4MzY5MjgiXX19LHsidmFsdWUiOjAuMDA2NzM5MDcsIm4iOjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAzNDRkZGY4YzNlNGRmYmNlODFlYzljYzIyMzc1NDg3NTdhZGQyMmY1IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MzQ0ZGRmOGMzZTRkZmJjZTgxZWM5Y2MyMjM3NTQ4NzU3YWRkMjJmNTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcTZ5bWh1djhleGxobjVwYWp3dnlnbTRmcDZoNGhmejc1ajJlZ3Y0ODciXX19LHsidmFsdWUiOjAuMDk0OTUyMjcsIm4iOjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2YzNhZTI1NTczNGMxYmU0OTBlYzQ3OTEwMjQ3ZDVhZjg2MGU2NjNhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NmMzYWUyNTU3MzRjMWJlNDkwZWM0NzkxMDI0N2Q1YWY4NjBlNjYzYTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGtyNGNqNHdkeHBoZXlzYTNyZXpxajg2a2hjdnJueDhnMHk5YXpjZDUiXX19XSwiZmVlIjozLjQyZS0wNSwiaGV4IjoiMDEwMDAwMDAwMTQ3NzZiYmM2NGI1YTA0OTA1M2IxMTM2OWE5ZmQ4M2FmMTBkZDIxYTE5M2E5NmM2YTEzN2NjYjNjMmUwYTE4ZjkwMDAwMDAwMDZhNDczMDQ0MDIyMDViMjcxOGRmNTZhM2M3MGNhMzMwNjY1ZDA5ZTYzZmVkNWEwN2U3NDgxMTYyYmQ3ZTcxNGIxMjIyN2NjOTQ3NDUwMjIwMjIzMWFiZmEzNjZkMGJlZWM2YWMyZjNiY2VhNWQ2ZjBhMGIzODExMDZjY2QxNWQ4MGFlNjRiMDFiNzNlNTUxZjQxMjEwMmYwZjk1YTEyZGI5MzUyMjQzYzQ1ZjI3MzdlNzYyYmM3N2JiM2ViMzViZjI4NGQwMGNlZjQ1ZWU0MjNmY2ZkOTBmZmZmZmZmZjA0MDAwMDAwMDAwMDAwMDAwMDI2NmEyNDc2MmQ2NjQxMjgzYTZjNTU0NDIzNjQ1MDYxNDQ0ZTQyNDM2MTM1NzA2ZTY0NGUyZjQxNWMyZDI1NzAzOTM2MzYzMzYzNDc1NTI5MGQwMDAwMDAwMDAwMDAxOTc2YTkxNDA2NjY4YzI2MGIyMmMxNzgyMDViZTIxOWE4ZjBkNmM4Zjc0NTZjZTU4OGFjNzM0ODBhMDAwMDAwMDAwMDE5NzZhOTE0MzQ0ZGRmOGMzZTRkZmJjZTgxZWM5Y2MyMjM3NTQ4NzU3YWRkMjJmNTg4YWNiYmUyOTAwMDAwMDAwMDAwMTk3NmE5MTQ2YzNhZTI1NTczNGMxYmU0OTBlYzQ3OTEwMjQ3ZDVhZjg2MGU2NjNhODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiZjdmMTIxYzBjMjVjNzI5ZTYxNGU1N2Y0ZjQ3NmZhNGJhNzU4M2U2ZjAyMzNkNjkyY2U0MjBjYWJjMWYzNWY4YiIsImhhc2giOiJmN2YxMjFjMGMyNWM3MjllNjE0ZTU3ZjRmNDc2ZmE0YmE3NTgzZTZmMDIzM2Q2OTJjZTQyMGNhYmMxZjM1ZjhiIiwidmVyc2lvbiI6MSwic2l6ZSI6NTU2LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiYmI4MzVmMzhhN2YxMGM3NTgyYTE5NTlkYTQ3OWU3YzQ4ZDc2NjYyNzYwOTgzNzgzOTY5ZDAwMGIzMWRhZmM0ZCIsInZvdXQiOjEsInNjcmlwdFNpZyI6eyJhc20iOiIzMDQ1MDIyMTAwYjNhMzQ4ZmU5MWRkMjk3NWRjOTU0OTM2ZWY5YzRjNmMxOWM1YzFkNTkzMTllMjJhM2YwZjEwMDFlNjBiNGM4NzAyMjAxZGQyMmIyZWQ1ZmU0NGM5NDM1MmExY2RlMTIwNDE5NGUzMzIwNDQ3NzBjNzAzZGQ5MjBlNDk2OWY0N2UxZmZkW0FMTHxGT1JLSURdIDAyOTk4MGNmNTY4ODViY2YxMmNiYTZlMTBmYzBjNTU1YTZlZDdkMzJjYjUzMjg4MTFlM2MzOGRiY2QyZWZjNjk3YiIsImhleCI6IjQ4MzA0NTAyMjEwMGIzYTM0OGZlOTFkZDI5NzVkYzk1NDkzNmVmOWM0YzZjMTljNWMxZDU5MzE5ZTIyYTNmMGYxMDAxZTYwYjRjODcwMjIwMWRkMjJiMmVkNWZlNDRjOTQzNTJhMWNkZTEyMDQxOTRlMzMyMDQ0NzcwYzcwM2RkOTIwZTQ5NjlmNDdlMWZmZDQxMjEwMjk5ODBjZjU2ODg1YmNmMTJjYmE2ZTEwZmMwYzU1NWE2ZWQ3ZDMyY2I1MzI4ODExZTNjMzhkYmNkMmVmYzY5N2IifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTV9LHsidHhpZCI6IjU4NDI3ZjA3MWEyYTcxOGEzN2JlNzlmMWU0ODdjNGU1YWFmZGM2OTkzZjM4ODllYjE2MjcyNmEzOWJhNzQ0MTkiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NTAyMjEwMDg1YzA5ZTcyYzZiZjNiMzI4Y2UxYzkxMjkwNDVhNWJlOGI2OGI1NDdlYWIzZjcxODI2YjMyMWE1NDNmMzk4YjIwMjIwNDc4ZTQ0ZWIyY2RhZmU2MThkZjAyMGM3NzFjMTlmYWU3YmYxMTNhZDJmNmM4MWY0OWRmOTQyMjRhZWVhZjY3YVtBTEx8Rk9SS0lEXSAwMzU0MzJlMmQxZTk3OWZiZjZjNzhiNjA0ZWZiZGE3MDk2ZDVjZDE2Y2UwMjk3YzZjYzMwZjlhODIzNDVhMmFiOWYiLCJoZXgiOiI0ODMwNDUwMjIxMDA4NWMwOWU3MmM2YmYzYjMyOGNlMWM5MTI5MDQ1YTViZThiNjhiNTQ3ZWFiM2Y3MTgyNmIzMjFhNTQzZjM5OGIyMDIyMDQ3OGU0NGViMmNkYWZlNjE4ZGYwMjBjNzcxYzE5ZmFlN2JmMTEzYWQyZjZjODFmNDlkZjk0MjI0YWVlYWY2N2E0MTIxMDM1NDMyZTJkMWU5NzlmYmY2Yzc4YjYwNGVmYmRhNzA5NmQ1Y2QxNmNlMDI5N2M2Y2MzMGY5YTgyMzQ1YTJhYjlmIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fSx7InR4aWQiOiJiNGIxZDYwMjI5ZDNiOGQ2M2M2NmUzY2I3YjI3ZDQyMGRiNTlhMGRjMzhmOTFiMWM4ZTY2MDllNTUzY2M2MTczIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBkNDc2ZmRkZGZmNzFmODA1YzlkOTI0ZDg4NWIxNjI4MTQxZDhmNWI5MmYyNGMyZTkxNTgzYzM0ZjZiM2IwZTc4MDIyMDY5NDUzZTQ2YjM4NDE0MGQ1MTc5ZTEwYTUxYTU4OTU2YWNiYzA3MzBhMWRmNGRhNTBiMTQzMWQ5MDZkNmZlMmNbQUxMfEZPUktJRF0gMDIzYWQxZWVjNGFlMjIzMDIwYjJkMzNmMjRlYTZjY2ZlMTU2MGUzNDk2ODY2YzM5ZDZhOGYxNzFiZjRjODc4MDBlIiwiaGV4IjoiNDgzMDQ1MDIyMTAwZDQ3NmZkZGRmZjcxZjgwNWM5ZDkyNGQ4ODViMTYyODE0MWQ4ZjViOTJmMjRjMmU5MTU4M2MzNGY2YjNiMGU3ODAyMjA2OTQ1M2U0NmIzODQxNDBkNTE3OWUxMGE1MWE1ODk1NmFjYmMwNzMwYTFkZjRkYTUwYjE0MzFkOTA2ZDZmZTJjNDEyMTAyM2FkMWVlYzRhZTIyMzAyMGIyZDMzZjI0ZWE2Y2NmZTE1NjBlMzQ5Njg2NmMzOWQ2YThmMTcxYmY0Yzg3ODAwZSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4yMDUyNDc1NiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGQ3NWJhMzAyNjFkNGEzNjFlMGFiZTg3YTE3MDY3ZjRiNWI0MTNjZmEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkNzViYTMwMjYxZDRhMzYxZTBhYmU4N2ExNzA2N2Y0YjViNDEzY2ZhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFydDRoZ2N6djgyMnhjMHE0MDU4NTljeDBhOTRrc2Z1bGd4YzYzN3p3eSJdfX0seyJ2YWx1ZSI6MC4wMDA5ODIyOCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkyZTA5MGFlMTQ1OWFmYmZkZjk2NjM0OGUxN2JmNzc3MTIyMmE2NWUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5MmUwOTBhZTE0NTlhZmJmZGY5NjYzNDhlMTdiZjc3NzEyMjJhNjVlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZndweTl3ejN2NmwwN2xqZTM1M2N0bTdhbTN5ZzR4dGNqNHVqeXZkeiJdfX0seyJ2YWx1ZSI6NS40NmUtMDYsIm4iOjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA2OWVlZDlkNTVlOWFjY2NhMDE1ZGMxYzc4OTJlZDg3NTg2NjRmZTc2IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NjllZWQ5ZDU1ZTlhY2NjYTAxNWRjMWM3ODkyZWQ4NzU4NjY0ZmU3Njg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDU3YWt3NHQ2ZHZlanNwdGhxdTB6ZndtcDZjdmU4N3djdHNjNDgza2oiXX19XSwiZmVlIjoxLjEyOGUtMDUsImhleCI6IjAxMDAwMDAwMDM0ZGZjZGEzMTBiMDA5ZDk2ODMzNzk4NjAyNzY2NzY4ZGM0ZTc3OWE0OWQ5NWExODI3NTBjZjFhNzM4NWY4M2JiMDEwMDAwMDA2YjQ4MzA0NTAyMjEwMGIzYTM0OGZlOTFkZDI5NzVkYzk1NDkzNmVmOWM0YzZjMTljNWMxZDU5MzE5ZTIyYTNmMGYxMDAxZTYwYjRjODcwMjIwMWRkMjJiMmVkNWZlNDRjOTQzNTJhMWNkZTEyMDQxOTRlMzMyMDQ0NzcwYzcwM2RkOTIwZTQ5NjlmNDdlMWZmZDQxMjEwMjk5ODBjZjU2ODg1YmNmMTJjYmE2ZTEwZmMwYzU1NWE2ZWQ3ZDMyY2I1MzI4ODExZTNjMzhkYmNkMmVmYzY5N2JmZmZmZmZmZjE5NDRhNzliYTMyNjI3MTZlYjg5MzgzZjk5YzZmZGFhZTVjNDg3ZTRmMTc5YmUzNzhhNzEyYTFhMDc3ZjQyNTgwMDAwMDAwMDZiNDgzMDQ1MDIyMTAwODVjMDllNzJjNmJmM2IzMjhjZTFjOTEyOTA0NWE1YmU4YjY4YjU0N2VhYjNmNzE4MjZiMzIxYTU0M2YzOThiMjAyMjA0NzhlNDRlYjJjZGFmZTYxOGRmMDIwYzc3MWMxOWZhZTdiZjExM2FkMmY2YzgxZjQ5ZGY5NDIyNGFlZWFmNjdhNDEyMTAzNTQzMmUyZDFlOTc5ZmJmNmM3OGI2MDRlZmJkYTcwOTZkNWNkMTZjZTAyOTdjNmNjMzBmOWE4MjM0NWEyYWI5ZmZmZmZmZmZmNzM2MWNjNTNlNTA5NjY4ZTFjMWJmOTM4ZGNhMDU5ZGIyMGQ0Mjc3YmNiZTM2NjNjZDZiOGQzMjkwMmQ2YjFiNDAwMDAwMDAwNmI0ODMwNDUwMjIxMDBkNDc2ZmRkZGZmNzFmODA1YzlkOTI0ZDg4NWIxNjI4MTQxZDhmNWI5MmYyNGMyZTkxNTgzYzM0ZjZiM2IwZTc4MDIyMDY5NDUzZTQ2YjM4NDE0MGQ1MTc5ZTEwYTUxYTU4OTU2YWNiYzA3MzBhMWRmNGRhNTBiMTQzMWQ5MDZkNmZlMmM0MTIxMDIzYWQxZWVjNGFlMjIzMDIwYjJkMzNmMjRlYTZjY2ZlMTU2MGUzNDk2ODY2YzM5ZDZhOGYxNzFiZjRjODc4MDBlZmZmZmZmZmYwM2Q0MmUzOTAxMDAwMDAwMDAxOTc2YTkxNGQ3NWJhMzAyNjFkNGEzNjFlMGFiZTg3YTE3MDY3ZjRiNWI0MTNjZmE4OGFjYjQ3ZjAxMDAwMDAwMDAwMDE5NzZhOTE0OTJlMDkwYWUxNDU5YWZiZmRmOTY2MzQ4ZTE3YmY3NzcxMjIyYTY1ZTg4YWMyMjAyMDAwMDAwMDAwMDAwMTk3NmE5MTQ2OWVlZDlkNTVlOWFjY2NhMDE1ZGMxYzc4OTJlZDg3NTg2NjRmZTc2ODhhYzAwMDAwMDAwIn0seyJ0eGlkIjoiZjhiYTIwMmJkNDJhOWVhNTQyMjYyZTMxMTNlYjg4YWI4ZGM3ZjdjMGU4NWVmNWIzMTcyMTk5YWI5ODBmZmQ5ZCIsImhhc2giOiJmOGJhMjAyYmQ0MmE5ZWE1NDIyNjJlMzExM2ViODhhYjhkYzdmN2MwZTg1ZWY1YjMxNzIxOTlhYjk4MGZmZDlkIiwidmVyc2lvbiI6Miwic2l6ZSI6MTY0LCJsb2NrdGltZSI6MCwidmluIjpbeyJ0eGlkIjoiYjg3OTk4ZGUyYmJiNDgzMTE3MzQwYzdmNjU0OWM5ODUwMTRjODhkYmRiZTBjMTk5Y2NkODUyZGQwMWQ2YjlhYSIsInZvdXQiOjAsInNjcmlwdFNpZyI6eyJhc20iOiI1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIiwiaGV4IjoiNDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxIn0sInNlcXVlbmNlIjoxfV0sInZvdXQiOlt7InZhbHVlIjowLjAwNTY1NjgyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XSwiZmVlIjoxLjY0ZS0wNiwiaGV4IjoiMDIwMDAwMDAwMWFhYjlkNjAxZGQ1MmQ4Y2M5OWMxZTBkYmRiODg0YzAxODVjOTQ5NjU3ZjBjMzQxNzMxNDhiYjJiZGU5ODc5YjgwMDAwMDAwMDQ1NDQ1MWIyNzVjMzUxOWRjNDUxOWQwMjQwMDIwMmE0MDBjMGM2N2I1Mjc5OTNhMDYzYzBjN2MwY2Q4OGMwYzYwMjI4MTE5NTA0YzY4NTc0NGY5NjZlYTA2Mzc4Nzc2OGMwYzZjMGNjOTQ2ZWEyNjk2ZDY3YzBjYzAwOWRjMGNkMDE2YTg4Njg3NTUxMDEwMDAwMDAwMWIyYTEwODAwMDAwMDAwMDAyM2FhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODcwMDAwMDAwMCJ9LHsidHhpZCI6ImY5ZjU4OWQwYjA4ODI3ZGI1NjE4N2IxM2MzNzU4MTFmMzFjY2I3NDdlY2Y5YjY3NDg3YTk0NDMzYmJjNzIyNTIiLCJoYXNoIjoiZjlmNTg5ZDBiMDg4MjdkYjU2MTg3YjEzYzM3NTgxMWYzMWNjYjc0N2VjZjliNjc0ODdhOTQ0MzNiYmM3MjI1MiIsInZlcnNpb24iOjEsInNpemUiOjE5MSwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6ImZhMGYyZjFmYTIzZmM2Yjc5YjUxYmRlOWY5YTQ3ZWNmNGUzYmUwNDJlM2UzNDYyYzFhMmE2NjNkNjg0M2NlZTciLCJ2b3V0IjoxLCJzY3JpcHRTaWciOnsiYXNtIjoiMzA0NDAyMjAwOTI1NWI4OTA2NTk1OTlhY2VmNzIxMmE5YTZmOTA5M2Y4OTc4NWQ5NjFjZDc0N2JiODliZmFkY2JhZDdmMzFjMDIyMDU3ZGIzY2FhMmI3OGU3NWE4YmZmNzQ4YmM4ZDMxYWYyYzYwYzBlOTg2YjA2NmZmOTdlYTEyNTI5MDNlM2ZlOWJbQUxMfEZPUktJRF0gMDM1OTExMzUzNWM0MmNkZGZlYjE2ZDhiZjBjYThiY2QwZTA0NzNkODdlNzFjNmRlZjJmMTU3NWY2ZjNkOWM1MWU2IiwiaGV4IjoiNDczMDQ0MDIyMDA5MjU1Yjg5MDY1OTU5OWFjZWY3MjEyYTlhNmY5MDkzZjg5Nzg1ZDk2MWNkNzQ3YmI4OWJmYWRjYmFkN2YzMWMwMjIwNTdkYjNjYWEyYjc4ZTc1YThiZmY3NDhiYzhkMzFhZjJjNjBjMGU5ODZiMDY2ZmY5N2VhMTI1MjkwM2UzZmU5YjQxMjEwMzU5MTEzNTM1YzQyY2RkZmViMTZkOGJmMGNhOGJjZDBlMDQ3M2Q4N2U3MWM2ZGVmMmYxNTc1ZjZmM2Q5YzUxZTYifSwic2VxdWVuY2UiOjQyOTQ5NjcyOTN9XSwidm91dCI6W3sidmFsdWUiOjEuMDA0OTk4MTMsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBiMWU2NDE5MDRiYTcyMDU1ZmFjNzc2Y2EyNTQxMjdlMzRhMzQ1NjY0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YjFlNjQxOTA0YmE3MjA1NWZhYzc3NmNhMjU0MTI3ZTM0YTM0NTY2NDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemM3dnN2c2Z3bmpxNDA2Y2FtdjVmMnB5bDM1NWR6a3ZzODh1c2FxdjUiXX19XSwiZmVlIjoyLjg4ZS0wNiwiaGV4IjoiMDEwMDAwMDAwMWU3Y2U0MzY4M2Q2NjJhMWEyYzQ2ZTNlMzQyZTAzYjRlY2Y3ZWE0ZjllOWJkNTE5YmI3YzYzZmEyMWYyZjBmZmEwMTAwMDAwMDZhNDczMDQ0MDIyMDA5MjU1Yjg5MDY1OTU5OWFjZWY3MjEyYTlhNmY5MDkzZjg5Nzg1ZDk2MWNkNzQ3YmI4OWJmYWRjYmFkN2YzMWMwMjIwNTdkYjNjYWEyYjc4ZTc1YThiZmY3NDhiYzhkMzFhZjJjNjBjMGU5ODZiMDY2ZmY5N2VhMTI1MjkwM2UzZmU5YjQxMjEwMzU5MTEzNTM1YzQyY2RkZmViMTZkOGJmMGNhOGJjZDBlMDQ3M2Q4N2U3MWM2ZGVmMmYxNTc1ZjZmM2Q5YzUxZTZmZGZmZmZmZjAxNjU4MWZkMDUwMDAwMDAwMDE5NzZhOTE0YjFlNjQxOTA0YmE3MjA1NWZhYzc3NmNhMjU0MTI3ZTM0YTM0NTY2NDg4YWMwMDAwMDAwMCJ9LHsidHhpZCI6ImZjNGEzMDJmOTZiOGRhYjJkN2JjYWVlYjYyMGNkNGUxODdmMDQ1OGJhNzk3MmY2MWZlMGE4MDQ2MDZiZWJhYzEiLCJoYXNoIjoiZmM0YTMwMmY5NmI4ZGFiMmQ3YmNhZWViNjIwY2Q0ZTE4N2YwNDU4YmE3OTcyZjYxZmUwYTgwNDYwNmJlYmFjMSIsInZlcnNpb24iOjIsInNpemUiOjE2NCwibG9ja3RpbWUiOjAsInZpbiI6W3sidHhpZCI6IjI5OTQ1OWQ3MDVkZjBhMzUxODM5NzUxZWI3YmE5MmVkOGMyMWJiMTM0ZTU3NWRiZWZhN2I1Nzg5ZjlmZTJiMDYiLCJ2b3V0IjowLCJzY3JpcHRTaWciOnsiYXNtIjoiNTFiMjc1YzM1MTlkYzQ1MTlkMDI0MDAyMDJhNDAwYzBjNjdiNTI3OTkzYTA2M2MwYzdjMGNkODhjMGM2MDIyODExOTUwNGM2ODU3NDRmOTY2ZWEwNjM3ODc3NjhjMGM2YzBjYzk0NmVhMjY5NmQ2N2MwY2MwMDlkYzBjZDAxNmE4ODY4NzU1MSIsImhleCI6IjQ0NTFiMjc1YzM1MTlkYzQ1MTlkMDI0MDAyMDJhNDAwYzBjNjdiNTI3OTkzYTA2M2MwYzdjMGNkODhjMGM2MDIyODExOTUwNGM2ODU3NDRmOTY2ZWEwNjM3ODc3NjhjMGM2YzBjYzk0NmVhMjY5NmQ2N2MwY2MwMDlkYzBjZDAxNmE4ODY4NzU1MSJ9LCJzZXF1ZW5jZSI6MX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4wMDU3MzIyNiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0YyBPUF9FUVVBTCIsImhleCI6ImFhMjBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3N1OGY0ZnRuc3VndW56eTh3cnVodWF5dnRwejltdDg4ZXV3dnd0cDVqdnY1OHduZDk1YzB0ZDN3cGRwIl19fV0sImZlZSI6MS42NGUtMDYsImhleCI6IjAyMDAwMDAwMDEwNjJiZmVmOTg5NTc3YmZhYmU1ZDU3NGUxM2JiMjE4Y2VkOTJiYWI3MWU3NTM5MTgzNTBhZGYwNWQ3NTk5NDI5MDAwMDAwMDA0NTQ0NTFiMjc1YzM1MTlkYzQ1MTlkMDI0MDAyMDJhNDAwYzBjNjdiNTI3OTkzYTA2M2MwYzdjMGNkODhjMGM2MDIyODExOTUwNGM2ODU3NDRmOTY2ZWEwNjM3ODc3NjhjMGM2YzBjYzk0NmVhMjY5NmQ2N2MwY2MwMDlkYzBjZDAxNmE4ODY4NzU1MTAxMDAwMDAwMDEyYWJmMDgwMDAwMDAwMDAwMjNhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3MDAwMDAwMDAifSx7InR4aWQiOiJmZGJmN2E2NGMwNmU2ODMwZmVjMjRiMmJlOGFjOTUxNWVlY2FlMGI3ZjI4OWQwMTkwNDVlYjZlNTNlNWFhNThkIiwiaGFzaCI6ImZkYmY3YTY0YzA2ZTY4MzBmZWMyNGIyYmU4YWM5NTE1ZWVjYWUwYjdmMjg5ZDAxOTA0NWViNmU1M2U1YWE1OGQiLCJ2ZXJzaW9uIjoxLCJzaXplIjoxOTEsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiI3YWQ4MWI3YzA3NzE1ZDI1Yjc4ZGM1ZmIyYmFjYzA5ZjM3Y2FmYWNjNDlhZDVhYTIyZTAyOWFmY2ZlMjA0ODJjIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDQwMjIwNWFmZTlmZmZmZTJhYzg3OTQ4NTNlZDI0ZWNlYTE2Yzg2M2Q4ZDNjNmExYzljMmZhY2Q3YTNmNjE3MjFhOTBiNjAyMjAzMDhiYTVkYjM4M2Y5MWVhOWU1OWQyMDA1ZjQwYjZkNDYyYmUyOTlhNjFmYjkxNGZkZTJjMTNlYjlmYWY4ZTY4W0FMTHxGT1JLSURdIDAyMjUzZmM1ZDc3MjFhYzgyZGY4NjY2YzA5ZTY1NWUyZjRiNjM0MTY1MGRiMmNmZTk2NTQ2ZGZmODVkN2U5YmMyMSIsImhleCI6IjQ3MzA0NDAyMjA1YWZlOWZmZmZlMmFjODc5NDg1M2VkMjRlY2VhMTZjODYzZDhkM2M2YTFjOWMyZmFjZDdhM2Y2MTcyMWE5MGI2MDIyMDMwOGJhNWRiMzgzZjkxZWE5ZTU5ZDIwMDVmNDBiNmQ0NjJiZTI5OWE2MWZiOTE0ZmRlMmMxM2ViOWZhZjhlNjg0MTIxMDIyNTNmYzVkNzcyMWFjODJkZjg2NjZjMDllNjU1ZTJmNGI2MzQxNjUwZGIyY2ZlOTY1NDZkZmY4NWQ3ZTliYzIxIn0sInNlcXVlbmNlIjo0Mjk0OTY3Mjk1fV0sInZvdXQiOlt7InZhbHVlIjo1LjQ4MDY1MTMyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODYxOTEwYWUwYmUxNjVkODkwNTdlMzczMzQ4MDA0NTdjZDRkMTQyMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg2MTkxMGFlMGJlMTY1ZDg5MDU3ZTM3MzM0ODAwNDU3Y2Q0ZDE0MjA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpycGp5OXdwMHNrdGt5czJsM2h4ZHlxcTN0dTZuZzV5cWhoM3FyczR5Il19fV0sImZlZSI6MS45M2UtMDYsImhleCI6IjAxMDAwMDAwMDEyYzQ4MjBmZWZjOWEwMjJlYTI1YWFkNDljY2ZhY2EzNzlmYzBhYzJiZmJjNThkYjcyNTVkNzEwNzdjMWJkODdhMDAwMDAwMDA2YTQ3MzA0NDAyMjA1YWZlOWZmZmZlMmFjODc5NDg1M2VkMjRlY2VhMTZjODYzZDhkM2M2YTFjOWMyZmFjZDdhM2Y2MTcyMWE5MGI2MDIyMDMwOGJhNWRiMzgzZjkxZWE5ZTU5ZDIwMDVmNDBiNmQ0NjJiZTI5OWE2MWZiOTE0ZmRlMmMxM2ViOWZhZjhlNjg0MTIxMDIyNTNmYzVkNzcyMWFjODJkZjg2NjZjMDllNjU1ZTJmNGI2MzQxNjUwZGIyY2ZlOTY1NDZkZmY4NWQ3ZTliYzIxZmZmZmZmZmYwMTZjY2ZhYTIwMDAwMDAwMDAxOTc2YTkxNDg2MTkxMGFlMGJlMTY1ZDg5MDU3ZTM3MzM0ODAwNDU3Y2Q0ZDE0MjA4OGFjMDAwMDAwMDAifSx7InR4aWQiOiJmZGU2MWJlNTg2ZDU3Nzg4MWRhN2Q3MzE0NmVmM2U1MTVmMGQ0NTc1NzZkMzVlZDViOTVkNTZmYzQxNmZlZWEwIiwiaGFzaCI6ImZkZTYxYmU1ODZkNTc3ODgxZGE3ZDczMTQ2ZWYzZTUxNWYwZDQ1NzU3NmQzNWVkNWI5NWQ1NmZjNDE2ZmVlYTAiLCJ2ZXJzaW9uIjoxLCJzaXplIjoxOTIsImxvY2t0aW1lIjowLCJ2aW4iOlt7InR4aWQiOiJhODJmYzcxZGVhYmE5ZmQ1ZTc5NTYzZjk5ODI5NmNmOWQxYzY0YjVlMDgyN2NhZmQwMWUwOGY0NGZlOGE3NTllIiwidm91dCI6MCwic2NyaXB0U2lnIjp7ImFzbSI6IjMwNDUwMjIxMDBiMWRhNmY3YmU5NWIzNGJiYmJhMTE4MTIzYWUyYTA0OGI3ZWFkZGU2MTA1YTYyOWRkMWVjYzk5MzU2NzZjMjk4MDIyMDZhNTY1YjlmNzFiNTZiMzFiMzkxY2IzMDcxMzk2NDdkMGQ2Y2NmN2Y5ZjEwNzliOTg0NWE1ZmI3NDFlYzI2NDVbQUxMfEZPUktJRF0gMDIwMmI1YzNhNWQ4ZDY1MjIyNjY1MDdmNDlhYWE1NzA5MTQ5MDI3NzBjZjMwMTI0ZWQ1NTEzM2UxMWRmN2VkZWE5IiwiaGV4IjoiNDgzMDQ1MDIyMTAwYjFkYTZmN2JlOTViMzRiYmJiYTExODEyM2FlMmEwNDhiN2VhZGRlNjEwNWE2MjlkZDFlY2M5OTM1Njc2YzI5ODAyMjA2YTU2NWI5ZjcxYjU2YjMxYjM5MWNiMzA3MTM5NjQ3ZDBkNmNjZjdmOWYxMDc5Yjk4NDVhNWZiNzQxZWMyNjQ1NDEyMTAyMDJiNWMzYTVkOGQ2NTIyMjY2NTA3ZjQ5YWFhNTcwOTE0OTAyNzcwY2YzMDEyNGVkNTUxMzNlMTFkZjdlZGVhOSJ9LCJzZXF1ZW5jZSI6NDI5NDk2NzI5NX1dLCJ2b3V0IjpbeyJ2YWx1ZSI6MC4xMzA0MDY2OSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDAxMzJlOTNiMDg3MmU1YWQ3NjAxZjYxYmYwNGI5MTFkNTBjYzJjMDMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwMTMyZTkzYjA4NzJlNWFkNzYwMWY2MWJmMDRiOTExZDUwY2MyYzAzODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxcW45NmZtcHBld3R0dGtxOG1waHV6dGp5dzRwbnB2cXZ6bXFza2U3NSJdfX1dLCJmZWUiOjAuMDAwMTQ0LCJoZXgiOiIwMTAwMDAwMDAxOWU3NThhZmU0NDhmZTAwMWZkY2EyNzA4NWU0YmM2ZDFmOTZjMjk5OGY5NjM5NWU3ZDU5ZmJhZWExZGM3MmZhODAwMDAwMDAwNmI0ODMwNDUwMjIxMDBiMWRhNmY3YmU5NWIzNGJiYmJhMTE4MTIzYWUyYTA0OGI3ZWFkZGU2MTA1YTYyOWRkMWVjYzk5MzU2NzZjMjk4MDIyMDZhNTY1YjlmNzFiNTZiMzFiMzkxY2IzMDcxMzk2NDdkMGQ2Y2NmN2Y5ZjEwNzliOTg0NWE1ZmI3NDFlYzI2NDU0MTIxMDIwMmI1YzNhNWQ4ZDY1MjIyNjY1MDdmNDlhYWE1NzA5MTQ5MDI3NzBjZjMwMTI0ZWQ1NTEzM2UxMWRmN2VkZWE5ZmZmZmZmZmYwMTFkZmNjNjAwMDAwMDAwMDAxOTc2YTkxNDAxMzJlOTNiMDg3MmU1YWQ3NjAxZjYxYmYwNGI5MTFkNTBjYzJjMDM4OGFjMDAwMDAwMDAifV0sInRpbWUiOjE3NzI2NTIzMjAsIm1lZGlhbnRpbWUiOjE3NzI2NDYxNTMsIm5vbmNlIjozNTYzOTA0MzcyLCJiaXRzIjoiMTgwMTNmMzEiLCJkaWZmaWN1bHR5Ijo4ODE4MjQxMjI1NTQuNTUyNiwiY2hhaW53b3JrIjoiMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAyY2U3ODY2NzJmM2JjOTcxZmRhYjI3ZiIsIm5UeCI6MTMyLCJwcmV2aW91c2Jsb2NraGFzaCI6IjAwMDAwMDAwMDAwMDAwMDAwMDRmZWViY2Q2ZDE4YWMxOWE4ZmU0NmYyZGZmNjAxZjhhMmM2ZWVkM2JkNDEwNGMiLCJuZXh0YmxvY2toYXNoIjoiMDAwMDAwMDAwMDAwMDAwMDAwODE1N2M1MjY3N2I5NWVhMmU5NmM2MmU3YmM1MzUxNWUxNTMzNzMxNWQyMmVhYSIsImFibGFzdGF0ZSI6eyJlcHNpbG9uIjoxNjAwMDAwMCwiYmV0YSI6MTYwMDAwMDAsImJsb2Nrc2l6ZSI6MTA1MzU1LCJibG9ja3NpemVsaW1pdCI6MzIwMDAwMDAsIm5leHRibG9ja3NpemVsaW1pdCI6MzIwMDAwMDB9fQ==",
+ "inputTransactions": [
+ {},
+ {
+ "data": [
+ "eyJ0eGlkIjoiMmIyZjI5NjFjYmE4MjNlNzcxZmIyNTM2ZWEyY2QxMDdiNjhmNWExYjM1NWI2MGRhNmI2MWI3OWJmZDRlZDJlOSIsInZvdXQiOlt7InZhbHVlIjowLjAwNTcxOTE0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZWRhN2Y5MTBjZGNiOWIyZmZkMDc2ZTRiZmU5ODliMDJiMmJiMmRmOGRmZjYzNmEzYWY2NTAyNDY1ZDYwYjlmNiIsInZvdXQiOlt7InZhbHVlIjozLjAyMzYyMjUyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjNjYmVjOTYxNDg0Y2EzNTFlZWI4MzQ3YmRjOGI2YTg2MDYxNjg4OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDIzY2JlYzk2MTQ4NGNhMzUxZWViODM0N2JkYzhiNmE4NjA2MTY4ODk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXEzdWhteWt6anp2NWRnN2F3cDUwMHdnazY1eHFjdGczeWZ3ODczNGVjIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNTI0Mzg2OTdlZjdiNDBjYWQ1NDZiOTNkMDc4ZTFmNzQxNzc4M2IwZjM4MDg1MmYxZjFmOGE5YWNkMDc5Y2MxNiIsInZvdXQiOlt7InZhbHVlIjoyLjA3NzE3MjM2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpnMHkyem5qNXZleDkycTdyc2F1ZndtY3F5bmZ4eDg1NWQ4NjZjeTZ0Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNTNjODViM2M3ZDE3YmM0ZmJlZDE5NjdlOThjOTYxZDM3MzIxYThjMTc4Y2I5NjY3ODhiYzc3ODZkNmI2OTRiZSIsInZvdXQiOlt7InZhbHVlIjozLjg4MDAyMjc4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMmU2OWEyMjNkMjI2MjA3YmUyNWI3MGRjMjdhOTA0NjIwMGVlNDE5NiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDJlNjlhMjIzZDIyNjIwN2JlMjViNzBkYzI3YTkwNDYyMDBlZTQxOTY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFoeG5nM3I2Z256cTdsenRkY2RjZmFmcTMzcXBtanBqY2V3bHFqZ3dlIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNWM0ZjkzMzdjOTZlMzFmYWRiMjA3OGEwYTYyODhhZWE4OWMxNWM0YmMxYWQ5YTJlNDZmYWQwOWJmMTI2NWVjMCIsInZvdXQiOlt7InZhbHVlIjowLjA0MDA4MywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDBmNWZjOWVkZWRlNDVhYzk3MWY0OGExY2JjOGNhMjU0YjkwOThiYzAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwZjVmYzllZGVkZTQ1YWM5NzFmNDhhMWNiYzhjYTI1NGI5MDk4YmMwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxODRsajBkYWhqOTRqdDM3ajlwZTB5djVmMnRqenZ0Y3FrZmVlMHM3NiJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNzI2YzMwZjJmNGE3ZWZlODJmN2U5MTA1M2IzOWFlYTIwMWQwMmQxNGRhMzBiZDcxZWQyYTBhNDIyYTBhZThhOSIsInZvdXQiOlt7InZhbHVlIjowLjAwMDQxNTkzLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNzM4MjI0MTY5YTZjYzk4OWM3NWEzYjFjOTU5OGZmMzYwYjZkMDlmYzk4MDliMTYzZWI0NmJhZmRhODg4NTBmZCIsInZvdXQiOlt7InZhbHVlIjoxMC43MDkwMjc2OSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDdhNTliMDliZjQyMDVkYzFmZGUxOGE1YzhhYWRlYzRiZmFmOTcxMjIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3YTU5YjA5YmY0MjA1ZGMxZmRlMThhNWM4YWFkZWM0YmZhZjk3MTIyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwYTludnltN3NzOW1zMGF1eDk5ZXo0ZGEzOWw0N3QzeWd2aDZjZGdmYSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYzA5M2FkODQzNzRlOTcyOTc3MWY2MzYwMjE4ZDEyZjY0NmQyZDlkYjY4MzI5ZjAwYmM5MDRiYTFkNjRjNmYzZSIsInZvdXQiOlt7InZhbHVlIjo2LjEyNzI5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNGY2OTc4ZmZlODZmNGIzNDBhMDY5MWI4N2NiNDEyZmYzYmZkNTcxNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDRmNjk3OGZmZTg2ZjRiMzQwYTA2OTFiODdjYjQxMmZmM2JmZDU3MTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA4a2o3OGxhcGg1a2RxMnE2Z21zbDk1enRsbmhsMmh6NTNtMzd3dGZxIl19fV19",
+ "eyJ0eGlkIjoiMDdlYmMwNTg2YWI0NWE2N2E3ZDUyNGYxM2EyZDliZGVlODg2OWNhMzg2NmNjYjdkNzFhODVhYWE3ODA0OWIyZCIsInZvdXQiOlt7InZhbHVlIjowLjA5OTk4MDAxLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNGY2OTc4ZmZlODZmNGIzNDBhMDY5MWI4N2NiNDEyZmYzYmZkNTcxNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDRmNjk3OGZmZTg2ZjRiMzQwYTA2OTFiODdjYjQxMmZmM2JmZDU3MTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA4a2o3OGxhcGg1a2RxMnE2Z21zbDk1enRsbmhsMmh6NTNtMzd3dGZxIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNjFlZjNkOTA4ZmQyYWIxOWVkZDZlYzQ1NDlhZDkzMzk0ZjM2Y2E2NTZiYzBkYTViMTA5NWEyMWZmYzFmOTExZCIsInZvdXQiOlt7InZhbHVlIjowLjAwNjAyNTMsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBkNWIxMDFiY2QxYjI1YzBkMzc5OWRkM2I3YzUyZWNjZDkyNWIzZGRhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZDViMTAxYmNkMWIyNWMwZDM3OTlkZDNiN2M1MmVjY2Q5MjViM2RkYTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcjJtenFkdTZ4ZTljcmZobjh3bmtsemphbnhleWtlYW1nOTBuNXRsdnkiXX19XX0=",
+ "eyJ0eGlkIjoiMDNkYjNmOWE4ZWQyMmRjNmM4MzAyZWJiMTI0NWQzMGRhMmM4ODY4MzcxNmRkOTRhNzBiZjM4N2E3MTljNDRlOCIsInZvdXQiOlt7InZhbHVlIjowLjAwODgxMDY3LCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjE3ZDYzZjhhMzE0NTA2ZWU0YTFkNDcxYzk0ZTA4N2Y2ODg2ODMwOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDIxN2Q2M2Y4YTMxNDUwNmVlNGExZDQ3MWM5NGUwODdmNjg4NjgzMDg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFzaDZjbGM1djI5cW1oeTU4MjhyajJ3cHBsazNwNXJwcXVuZmpxM3FqIl19fV19",
+ "eyJ0eGlkIjoiNzVhNzFmZmJmNTdjOGJhZTVkZTI5Yzc0MTdhYzllZjE3YjYxZWY5MDg4OWQ4NDNhZjlhMjEyNWE2MjBhYzEzZSIsInZvdXQiOlt7InZhbHVlIjowLjAwOTE2NDMyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZmZjM2Y2MDAwMzlhYTRmNzkzZDk4NGNlYjA0MGYyMzc4MWY1ZmM2MCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGZmYzNmNjAwMDM5YWE0Zjc5M2Q5ODRjZWIwNDBmMjM3ODFmNWZjNjA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJsdThhc3Fxd2QyZmF1bm14enZhdnpxN2dtY3JhMHV2cXkza3JlcThyIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZTg4MmI2NjRiZjExM2EzYmU3YjRhMWY5MTZlOGVkYzlkNDdjMDFlZGUxYjg2NGRhZjUxODk3NjYzZmY2ZGI3MCIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY1MTksIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZGJhZjg1Y2Y5ODFmYjYzOTlmMGVmOGY5ODE1YmZkOTg3MGEwODY0OGQxMTUyMzg3MjNjMTE4MzBjNmIyODhlZSIsInZvdXQiOlt7InZhbHVlIjoxMDMuNzU0MjE4OTIsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnMGM5aGxrZG11Nm5zYTNkeDdsZ200ZDVmenJ3azB1dmN5dWFjOW1lcWswNG11cncza2d5aGY1bW43eCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ4NTcwMDYifX1dfQ==",
+ "eyJ0eGlkIjoiZGJhZjg1Y2Y5ODFmYjYzOTlmMGVmOGY5ODE1YmZkOTg3MGEwODY0OGQxMTUyMzg3MjNjMTE4MzBjNmIyODhlZSIsInZvdXQiOlt7InZhbHVlIjo5Mi40NjE3Mjk1OSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMCBPUF9FUVVBTCIsImhleCI6ImFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjB3ZzlwMGx5NXRmMm5mOXY3NzNoM2Y5eGxyanB3eHFmN25oMjlzNXQ4M2N6NjVzN2t0cWFocjl5YWdkIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDMyODM3NCJ9fV19",
+ "eyJ0eGlkIjoiZGJhZjg1Y2Y5ODFmYjYzOTlmMGVmOGY5ODE1YmZkOTg3MGEwODY0OGQxMTUyMzg3MjNjMTE4MzBjNmIyODhlZSIsInZvdXQiOlt7InZhbHVlIjo3MS4zMzk4NDI0MywibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNiBPUF9FUVVBTCIsImhleCI6ImFhMjAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmswbjA2ZDR2ejZ5ZGZwbXNuemp3cHN5Y2hsYTNtcGdlNWdweDlteHlqMmMwYWh2c3FydmRmeGN2N3lrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzMzOTYwNCJ9fV19",
+ "eyJ0eGlkIjoiZGJhZjg1Y2Y5ODFmYjYzOTlmMGVmOGY5ODE1YmZkOTg3MGEwODY0OGQxMTUyMzg3MjNjMTE4MzBjNmIyODhlZSIsInZvdXQiOlt7InZhbHVlIjozNi4xNTI3NjM4NiwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTY5MjQwNSJ9fV19",
+ "eyJ0eGlkIjoiZGJhZjg1Y2Y5ODFmYjYzOTlmMGVmOGY5ODE1YmZkOTg3MGEwODY0OGQxMTUyMzg3MjNjMTE4MzBjNmIyODhlZSIsInZvdXQiOlt7InZhbHVlIjoxNy41NDQ1MDQ1NCwibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZCBPUF9FUVVBTCIsImhleCI6ImFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnN0cWttNTRkdHZucHlxeHQ1bTVuN3Nqc240ZW5ybHhjNTI2eHl4bG5qa2F5Y2R6ZmV1NjlyZXl6bXF4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODIxMzA1In19XX0=",
+ "eyJ0eGlkIjoiNjdlMDRlNzA1MTdiNTU1MGU1YTZlZGI0OGE3ZWVjOGNiZTQwZDQ2ODMwMWUzYmRlMWFlOWI5NDA0MGRkZmM0OCIsInZvdXQiOlt7InZhbHVlIjo4ZS0wNiwibiI6MTAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0NzU4MyJ9fV19",
+ "eyJ0eGlkIjoiZGJhZjg1Y2Y5ODFmYjYzOTlmMGVmOGY5ODE1YmZkOTg3MGEwODY0OGQxMTUyMzg3MjNjMTE4MzBjNmIyODhlZSIsInZvdXQiOlt7InZhbHVlIjozNi4xMjQyNjk4MSwibiI6Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZjBlZGN4bGZuemt0bTc3ZXFnODc0anVwMjh0YTZhNnU1cnk1c2Z1OSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZTNjNTFmZDU3NzZmZDk0YjVkOTI2ZDBjOWZkZWE2NGRiM2M2YTk1NzVkMWYxNDE1YTlkOWEzY2YwMTQxYTIxMyIsInZvdXQiOlt7InZhbHVlIjoxLjEwMTUyMDkzLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpnMHkyem5qNXZleDkycTdyc2F1ZndtY3F5bmZ4eDg1NWQ4NjZjeTZ0Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZDZmNjRmMDRjM2VjNTM0MjVjOGIyODk0ZGVlY2ZmNTYwMmVhOTY2YmYxOTMwMDRiMjIxZTkxMjRmNWQ2OTBhZCIsInZvdXQiOlt7InZhbHVlIjowLjAwMDQyNDEzLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOlt7InZhbHVlIjoxMDMuMjgyMjMzNzEsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnMGM5aGxrZG11Nm5zYTNkeDdsZ200ZDVmenJ3azB1dmN5dWFjOW1lcWswNG11cncza2d5aGY1bW43eCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ4NzkyNjkifX1dfQ==",
+ "eyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOlt7InZhbHVlIjo5Mi4wNDExMTI5NCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMCBPUF9FUVVBTCIsImhleCI6ImFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjB3ZzlwMGx5NXRmMm5mOXY3NzNoM2Y5eGxyanB3eHFmN25oMjlzNXQ4M2N6NjVzN2t0cWFocjl5YWdkIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDM0ODIxNCJ9fV19",
+ "eyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOlt7InZhbHVlIjo3MS4wMTUzMDYyMiwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNiBPUF9FUVVBTCIsImhleCI6ImFhMjAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmswbjA2ZDR2ejZ5ZGZwbXNuemp3cHN5Y2hsYTNtcGdlNWdweDlteHlqMmMwYWh2c3FydmRmeGN2N3lrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzM1NDkxMiJ9fV19",
+ "eyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOlt7InZhbHVlIjozNS45ODgyOTA5NCwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTcwMDE2MyJ9fV19",
+ "eyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOlt7InZhbHVlIjoxNy40NjQ3MDYxNCwibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZCBPUF9FUVVBTCIsImhleCI6ImFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnN0cWttNTRkdHZucHlxeHQ1bTVuN3Nqc240ZW5ybHhjNTI2eHl4bG5qa2F5Y2R6ZmV1NjlyZXl6bXF4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODI1MDY5In19XX0=",
+ "eyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOlt7InZhbHVlIjoxMS4zMjU3MDU4NSwibiI6NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYiBPUF9FUVVBTCIsImhleCI6ImFhMjBiMzQ3ZDY4YzhkMmNkYjMyMDY2YmU5YWY4MDRjNmZmYjhhYmJiNDQxYmRmMzZjYWYzODM0MzZiY2ZjYWEzNWNiODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd2U1MDQ1djM1a2RrdnN4ZDA1NmxxenZkbGFjNHdhNWd4N2x4bTkwOHE2cmQwOHU0ZzZ1a24zNzJnZ3dsIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTM1MDUxIn19XX0=",
+ "eyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOlt7InZhbHVlIjoxMC41MjMxMTMyLCJuIjo2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDdjMmJjY2EyMDg5YTlmMGY4ZGFlYzBkM2I0NWE2YzUyNTgxYzU2NzY2NWNjNTFlYmRhOWEyMDJkOGEzM2RjZmM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkN3pobjl6cHpkZjdydWQ0bXFkOGR6NmQzZjlzOHprd2VqdWM1MHRtMmR6cXR2MngwdzBjNTR5MzlocDYiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0OTcxMzUifX1dfQ==",
+ "eyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOlt7InZhbHVlIjo1LjIwNjUxMjAxLCJuIjo3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBmMWMzM2JjN2RhMDk5MWQyNmY4YmJmMTUwYzkwMzU2OGIyYzNkYzg3MjczNzA4NjNmNGMzZDlmOTY1NWIzNzkxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwY3V4dzc4bWd5ZXI1bjAzd2wzMnJ5c3g0NXQ5czd1c3Vubnd6cnI3bnBhbjd0OXR2bWV6cmF1M2p1cjkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyNDU5NjcifX1dfQ==",
+ "eyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOlt7InZhbHVlIjo0LjM5MzgyMzM0LCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YW15bmxlcHYzcG5nbnE2cmhxNTk1eDBsZm5nNncwd3A4NWpxM3JydjJ3amNsdmh0M3VneDRrYWo0aHUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMDc1NzQifX1dfQ==",
+ "eyJ0eGlkIjoiNzk5Mjk5OTRiZmRjMjY3YThlYjYxYWQ0NmI5Njc2NjYwNDc2M2U1NTZhMTQ5MTM3NTM4NjM0NDllOGM1MDE2MyIsInZvdXQiOlt7InZhbHVlIjo4ZS0wNiwibiI6MjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMTY5OTQifX1dfQ==",
+ "eyJ0eGlkIjoiYzRiNzI2ZjNjODI3Yjc0NDY1YzQxZjQxYzMwYjdhZDNlMzI5MmY1YmZiNzAxMjE3MWU3NGVjY2E1MGM1N2IyYyIsInZvdXQiOlt7InZhbHVlIjozNy45MjUzOTUxMywibiI6MTAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNGQwYTQ4NTQ5NWMxN2NhYzk0OWYwZGJiNmYzOTMwOTRmYTRiMTM2YjQ1MGRkOTQ4YTE3YzU4ZTYxZTc4NGY2MCIsInZvdXQiOlt7InZhbHVlIjoxLjg0OTE3MjQ0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNjgyYjMwMDY3NDQ0ZGRlZThmN2NmOGVlOTk3YTNkMjA4OTA2YTI5MyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDY4MmIzMDA2NzQ0NGRkZWU4ZjdjZjhlZTk5N2EzZDIwODkwNmEyOTM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA1emt2cXh3M3pkbW01MDBudXdheHQ2ODVzZ2pwNHpqdmV0OGQ3eW1rIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZWRhZjBlYjI0NjQ3YTY3OWVlODhkN2EwNDA5N2U2Zjg5YTFmOTFiM2VjZTBlMmIwZWI5ZjU2ZDk5MWQ1ODc3ZCIsInZvdXQiOlt7InZhbHVlIjowLjM3MDA2MTM5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNzI2MDJkNWIyZTFmODJlNTFiZjFiZTIyODE5NzUyNmI4ZGM4Y2YwNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDcyNjAyZDViMmUxZjgyZTUxYmYxYmUyMjgxOTc1MjZiOGRjOGNmMDU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBleHF0Mm05YzBjOWVnbTd4bHo5cXZoMmY0Y21qeDBxNW1wbDlqazM4Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZGQyNTkwNmJhNDI0MGQ5MTY4NGM0MTZkYjY1YjU1NzQ0OTBmOGNkNzM2NWE3ZGRkZTg4M2RkZTM5ZWNjYjhmMSIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY3NDg2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNjRiOTVkZTZiYzc4YjZhNjk1YmUyN2E1ODU1ZDMxNGEwZDBlOTM5NjdiMmJjZmU3MjNlYmZiNzYwZGU4MjNlYiIsInZvdXQiOlt7InZhbHVlIjozOS45OTk1OTM5NCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGY2Y2U1MmM2NjJkMmRmZWE0MWE3YzkxMzlmYzhmNWQ5ZWIzNmNjMTYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmNmNlNTJjNjYyZDJkZmVhNDFhN2M5MTM5ZmM4ZjVkOWViMzZjYzE2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFybXZ1NWt4dnRmZGw2anA1bHkzODg3ZzdodjdrZGt2emM3cm5ka2hycSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYWUyMGY3Mzk0MWI3NmIwMmJiNDYxY2FiZjQ0NGU5MzllNWY3M2JjZGUzMGQzNDU3YjkxNjQ5ZWRkMjFjNWYxNiIsInZvdXQiOlt7InZhbHVlIjo4ZS0wNiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDcyYzAzZTNlYjcwNzZhMTQ4NzdmNjM5OWYwMmNlMzJhNTJkM2UzN2QgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3MmMwM2UzZWI3MDc2YTE0ODc3ZjYzOTlmMDJjZTMyYTUyZDNlMzdkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwZXZxMDM3a3VyazU5eTgwYTNlbnVwdnV2NDk5NWxyMDV2NjdhdXpoeiJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiMDkzNjFkYzMwMWI0MDNhMjA5NjgyYmMyMDBiNjEyYTVmNTA3ODdjNGU3MzdjOTQ0Mjk2ZmQ5N2FhMDNjZmZmNiIsImFtb3VudCI6IjAiLCJuZnQiOnsiY2FwYWJpbGl0eSI6Im5vbmUiLCJjb21taXRtZW50IjoiIn19fV19",
+ "eyJ0eGlkIjoiYWUyMGY3Mzk0MWI3NmIwMmJiNDYxY2FiZjQ0NGU5MzllNWY3M2JjZGUzMGQzNDU3YjkxNjQ5ZWRkMjFjNWYxNiIsInZvdXQiOlt7InZhbHVlIjowLjAxMjg0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA4Mzc1MTY0ODZlYzg2MjVmNzYzNzhmODg0MDUyOGNmMjFlYjEyNWQ2ZGE5OTI0N2I5NDhmOGY1MGJjZjk0NGIxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDgzNzUxNjQ4NmVjODYyNWY3NjM3OGY4ODQwNTI4Y2YyMWViMTI1ZDZkYTk5MjQ3Yjk0OGY4ZjUwYmNmOTQ0YjE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3cGgyOWpnZG15eHlobWt4Nzhjc3N6ajNuZXBhdmY5Nm1kZmpmcm1qajhjNzU5dWw5enR6NHhzYzBuZTQiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6IjAxNzExZTM5ZTdiZjNiOGNhMGQ5YTZmYzZlYTMyZTM0MGNhYTFkNjRkYzdkMWRjNTFmYWUyMGZkNjY3NTU1NTgiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJtdXRhYmxlIiwiY29tbWl0bWVudCI6IjA5ODVhODY5MDAwMGU4MDM2MzAwIn19fV19",
+ "eyJ0eGlkIjoiYWUyMGY3Mzk0MWI3NmIwMmJiNDYxY2FiZjQ0NGU5MzllNWY3M2JjZGUzMGQzNDU3YjkxNjQ5ZWRkMjFjNWYxNiIsInZvdXQiOlt7InZhbHVlIjowLjg2MzMzMDUsIm4iOjIsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3MmMwM2UzZWI3MDc2YTE0ODc3ZjYzOTlmMDJjZTMyYTUyZDNlMzdkIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NzJjMDNlM2ViNzA3NmExNDg3N2Y2Mzk5ZjAyY2UzMmE1MmQzZTM3ZDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGV2cTAzN2t1cms1OXk4MGEzZW51cHZ1djQ5OTVscjA1djY3YXV6aHoiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNmEwMGM1MWViZDE5ZmJkYjg2YTFhY2Q2Mjk1MzE4NjRhYjQ3MmI0NDA5MWU0OGRiYjZiYWZmY2VlOWI4ZTg3MCIsInZvdXQiOlt7InZhbHVlIjowLjAyMzkwNTAyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2E5NmIyYmIwYWE3YmQzMzBkNzg2ZTZiNTJlZDAwYzc3NmI5OTJmNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdhOTZiMmJiMGFhN2JkMzMwZDc4NmU2YjUyZWQwMGM3NzZiOTkyZjQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBhZmR2NG1wMm5tNnZjZDBwaHhrNWhkcXJyaGR3dmo3czRkN21hNDNtIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYzgzMDllNzMxNzNiNWU2ZmM5MWJkODgzN2UzNDJlMTYzYjM1NmVlMTk4OTE2ZWI4OTk1MmFmNmIyNGExYTIzMyIsInZvdXQiOlt7InZhbHVlIjoxNjQ0MjAuNjIxNzg2MjUsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA1ZWEzY2ZmZjljNjkzNzRkOTRlOTViMDgzOGFlYTc4NTQ5ZmI1NDFhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NWVhM2NmZmY5YzY5Mzc0ZDk0ZTk1YjA4MzhhZWE3ODU0OWZiNTQxYTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDAyOG5sbG4zNW53bnY1YTlkc3N3OXc1N3o1bjc2NXJnZW5yM3N1dzYiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMzdmNWM0ZjI4Y2M2YzNhYjI5NzBmMDA2OGM1MWQ1YzZlMWIzZTAxOWQ3YjRmZDEwODVjMzdkZDlmZWE0YTI0NyIsInZvdXQiOlt7InZhbHVlIjowLjYxMSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGRmYjViOGJlODdiMjIyY2E5OTg2M2U5MjkyODI4ZmJhNjkzNDBlYWUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkZmI1YjhiZTg3YjIyMmNhOTk4NjNlOTI5MjgyOGZiYTY5MzQwZWFlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyMG10dzk3czdlejlqNWVzY2xmOXk1ejM3YXhqZHF3NGN4ZzI2eGh2ZSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiM2EyMDAyMDdkNDFkZmE1Y2YzYzA1ZDMyYTNkNTU4ZDBlZTVlMmEyZDkxYmI5MmU0MzkyYzQ0MzRkZjQ1NDc4MCIsInZvdXQiOlt7InZhbHVlIjoxLjAwODk4NDE2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2M4ZGUzYTA2MDhiMWViMjhlODhjYjRjNTRkNmVkNTQ4MWY0ZWRiOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdjOGRlM2EwNjA4YjFlYjI4ZTg4Y2I0YzU0ZDZlZDU0ODFmNGVkYjg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA3Z21jYXF2ejkzYXY1dzNyOTVjNHhrYTQyZ3JhOGRocWZqN3VkdWN4Il19fV19",
+ "eyJ0eGlkIjoiYzBiNWEyODYyNmJjOTY5N2E4MzVhMzg0NWM5YmYxMmZjNTRhNTBhMDc4MTI2ZWI4YzA0N2M1MWQ3MDk2ZGM2NiIsInZvdXQiOlt7InZhbHVlIjoxMi4wOTc3MzQyNSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDdjOGRlM2EwNjA4YjFlYjI4ZTg4Y2I0YzU0ZDZlZDU0ODFmNGVkYjggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3YzhkZTNhMDYwOGIxZWIyOGU4OGNiNGM1NGQ2ZWQ1NDgxZjRlZGI4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwN2dtY2Fxdno5M2F2NXczcjk1YzR4a2E0MmdyYThkaHFmajd1ZHVjeCJdfX1dfQ==",
+ "eyJ0eGlkIjoiMmQyNzEwMDY5OTNkYjA4MTZjOGZmMmQyNjQ4NjQxOTgyMDI3N2JiODRlNjBiYTRjMmQwNzA5MWQ1NDIyNzU5YyIsInZvdXQiOlt7InZhbHVlIjoyMC4yNjY1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2M4ZGUzYTA2MDhiMWViMjhlODhjYjRjNTRkNmVkNTQ4MWY0ZWRiOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdjOGRlM2EwNjA4YjFlYjI4ZTg4Y2I0YzU0ZDZlZDU0ODFmNGVkYjg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA3Z21jYXF2ejkzYXY1dzNyOTVjNHhrYTQyZ3JhOGRocWZqN3VkdWN4Il19fV19",
+ "eyJ0eGlkIjoiNWFiNzBiN2RhNGE1YTQzOGM2OWI0ZmJiNTVjMTJmMTM2ZjNjZGFiYTY0NTNmZGNlMmU3OGU3OGMyNzdiNGVlMSIsInZvdXQiOlt7InZhbHVlIjoxNi45OTA2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2M4ZGUzYTA2MDhiMWViMjhlODhjYjRjNTRkNmVkNTQ4MWY0ZWRiOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdjOGRlM2EwNjA4YjFlYjI4ZTg4Y2I0YzU0ZDZlZDU0ODFmNGVkYjg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA3Z21jYXF2ejkzYXY1dzNyOTVjNHhrYTQyZ3JhOGRocWZqN3VkdWN4Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiOGQ3NjEzMDY0OTM2YWUzNjM3NGFiOWYzN2Q1YjUzODJmMTM0N2YzZjBhZGZmOGE1MzJkNzk2YWJiYzEyMzAyNSIsInZvdXQiOlt7InZhbHVlIjowLjAwODM4MzA0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTViOTU1OWY0NmE5MjlmYjc1YmNlN2VkYzljYjRhOGRhNDRiOWRjNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU1Yjk1NTlmNDZhOTI5ZmI3NWJjZTdlZGM5Y2I0YThkYTQ0YjlkYzQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXAybWo0dmxnNjVqbjdtNGhubjdtand0ZjJ4NmdqdWFjc3J0aG01c2FwIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiOGI4NDU5MjkxOGU3MTYwYjZiOTQxMGIxOGQ4MWNmYTljNjlhNzliOTVmNGFiNGJmZDNiOTk1NjliYWYyODk4NiIsInZvdXQiOlt7InZhbHVlIjowLjAwMTY3NzgxLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMmZjM2QwNTI0YWY2NzAyMjk4YzU0MDViMTNmMzYwMzFhMWEwYmEyY2E4ZTc2Y2EyOTJiNTdkMDA2Y2VlMjJiYiIsInZvdXQiOlt7InZhbHVlIjo2ZS0wNSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDNiNTgwMWEzZjM5ODZkYTg2OGFlZjM5N2ZhYTIzNDgxYWM1MzcwM2MgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQzYjU4MDFhM2YzOTg2ZGE4NjhhZWYzOTdmYWEyMzQ4MWFjNTM3MDNjODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxYTRzcWRyN3d2eG0ycmc0bWVlMDc0enhqcTZjNW1zOHNwNWtsM3o5aCJdfX1dfQ==",
+ "eyJ0eGlkIjoiMzIxM2E0OTBmYzFiZTlkNDYzYjZkZjlkZWNmODcyMDU2YzIzMzAzYTk5OGEwYThlZTViMzdkNTc4ZTIxYzYxMCIsInZvdXQiOlt7InZhbHVlIjowLjAwMTQyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgM2I1ODAxYTNmMzk4NmRhODY4YWVmMzk3ZmFhMjM0ODFhYzUzNzAzYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDNiNTgwMWEzZjM5ODZkYTg2OGFlZjM5N2ZhYTIzNDgxYWM1MzcwM2M4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFhNHNxZHI3d3Z4bTJyZzRtZWUwNzR6eGpxNmM1bXM4c3A1a2wzejloIl19fV19",
+ "eyJ0eGlkIjoiZDE1ZWQ1ZjM1NDc5N2FiMjA2N2IwZDkxZjUwYzg2MjFiMzAyYzUyZWVjOWVhMjQ1NzdmNjU2Yzc2MzAyNmZkMSIsInZvdXQiOlt7InZhbHVlIjowLjAwMDQzLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgM2I1ODAxYTNmMzk4NmRhODY4YWVmMzk3ZmFhMjM0ODFhYzUzNzAzYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDNiNTgwMWEzZjM5ODZkYTg2OGFlZjM5N2ZhYTIzNDgxYWM1MzcwM2M4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFhNHNxZHI3d3Z4bTJyZzRtZWUwNzR6eGpxNmM1bXM4c3A1a2wzejloIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiY2UxOGM0ODBmZTk4MTI1MWI4MzRlOTI3Mzk5MzA3N2Y1ZDllOGFkNzRlYWRjZDcwMmM5ZWI4OWFmMTAwMDc4ZCIsInZvdXQiOlt7InZhbHVlIjoxZS0wNSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2I3MWJlZDIzYmY3ZTYwNmQyZjkxNzhkODVlMTUxNzBkNjhjNDVhMTBhOWU4NmRiNTIwYWMwOGUzZWY0MWY2YSBPUF9FUVVBTCIsImhleCI6ImFhMjAzYjcxYmVkMjNiZjdlNjA2ZDJmOTE3OGQ4NWUxNTE3MGQ2OGM0NWExMGE5ZTg2ZGI1MjBhYzA4ZTNlZjQxZjZhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmFocjBrajgwbTd2cGtqbHl0Y21wMHAyOWNkZHJ6OTV5OWZhcGttMmc5dnByMzc3czBrNTU3NGRyMndjIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiI1ZTQzNzMyNjQ0OWFiYTc4NTVkYTNmNTkyMmZkNjVjZmVlNmVhYjE3YzY4NjllMDYzNjAxNjMwMGIwZjFjM2MxIiwiYW1vdW50IjoiMCIsIm5mdCI6eyJjYXBhYmlsaXR5Ijoibm9uZSIsImNvbW1pdG1lbnQiOiIifX19XX0=",
+ "eyJ0eGlkIjoiY2UxOGM0ODBmZTk4MTI1MWI4MzRlOTI3Mzk5MzA3N2Y1ZDllOGFkNzRlYWRjZDcwMmM5ZWI4OWFmMTAwMDc4ZCIsInZvdXQiOlt7InZhbHVlIjowLjA2MDI1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBlZDY3MzA5YjM2NDI0YzFiM2YyNWUwOGYxOWQ4ZWFkNGVhZGU5MTdjNDc2ZTViMTY1MjBkZWFjNjNkNjVkNGE5IE9QX0VRVUFMIiwiaGV4IjoiYWEyMGVkNjczMDliMzY0MjRjMWIzZjI1ZTA4ZjE5ZDhlYWQ0ZWFkZTkxN2M0NzZlNWIxNjUyMGRlYWM2M2Q2NWQ0YTk4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwa2t3dnlteGVweWN4ZWx5aHNnN3h3Y2F0Mnc0aDUzMDNya3VrY2syZ3g3NDMzYXZoMjJqbHYzOXBkenQiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImQwZDQ2ZjVjYmQ4MjE4OGFjZWRlMGQzZTQ5Yzc1NzAwYzE5Y2I4MzMxYTMwMTAxZjBiYjZhMjYwMDY2YWM5NzIiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJtdXRhYmxlIiwiY29tbWl0bWVudCI6ImNkODJhODY5MDAwMGU4MDNlYmNiMTcwMDYwYjcwMDAwIn19fV19",
+ "eyJ0eGlkIjoiY2UxOGM0ODBmZTk4MTI1MWI4MzRlOTI3Mzk5MzA3N2Y1ZDllOGFkNzRlYWRjZDcwMmM5ZWI4OWFmMTAwMDc4ZCIsInZvdXQiOlt7InZhbHVlIjowLjAzMTg1MjIyLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTI1ZTliY2VjZWEwZTlhMTI5ZWRlMzkyYTA3NWRhMmVmMDNhMjJjMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkyNWU5YmNlY2VhMGU5YTEyOWVkZTM5MmEwNzVkYTJlZjAzYTIyYzE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpmOWF4N3dlNnN3bmdmZmFoM2U5Z3I0bWdoMHF3M3pjeTZrcTVhZmF1Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYzUyZGRhMzMwMjFkZDZjMjZjMmM2OGJjYWIzMWM3MmZjNWUxNmNkNDkxMDQzY2EwOTQ5NTc0MzZkMDg0M2Y1NSIsInZvdXQiOlt7InZhbHVlIjowLjExNTkxLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMGYzODA4OGQ0ZTBiZTFjNTZhMmVhNTFjMWRlOGI1OTdlYjdjNDFkZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDBmMzgwODhkNGUwYmUxYzU2YTJlYTUxYzFkZThiNTk3ZWI3YzQxZGQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXE4bnN6eWRmYzk3cjN0Mjk2ajNjODBna2t0N2tsenBtNXd0bm1ydnA5Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYzljYWUzOWEyMTc5YmZjZmI1OGMwYmRmOTYyODljNjgyYzUzZmNmYmMzMTAzNTlkMzExYzA1OTI3YWRmNjhmNiIsInZvdXQiOlt7InZhbHVlIjowLjgzOTgwMzk5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMGQ0Y2NiZmY1MzkxNmIzOWIwZTEwNzI5MDZlNzIyMGVjOTZlYTkxNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDBkNGNjYmZmNTM5MTZiMzliMGUxMDcyOTA2ZTcyMjBlYzk2ZWE5MTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF4NWVqbGwyd2dra3dkc3V5cmpqcGg4eWc4dmptNGZ6NTBuZGxwNmhlIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNmE1ZmU1MWZiN2EzMDE4NzUxNWE3Y2VmZWEzYzdlN2U0OWRhZDE4ZWRiM2YyNTIzZWMxNWYwZmIwNmUwNGRiYSIsInZvdXQiOlt7InZhbHVlIjowLjAwODU4OTI2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTViOTU1OWY0NmE5MjlmYjc1YmNlN2VkYzljYjRhOGRhNDRiOWRjNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU1Yjk1NTlmNDZhOTI5ZmI3NWJjZTdlZGM5Y2I0YThkYTQ0YjlkYzQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXAybWo0dmxnNjVqbjdtNGhubjdtand0ZjJ4NmdqdWFjc3J0aG01c2FwIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMmRlMWRlMjFjODc1ZGYwMzQxNjM5YWU2ZTQwOTZhZWUxY2ZlYTM1MmNkMWYzOTczNTE2YmUyN2Y1ZDVlNzZhYSIsInZvdXQiOlt7InZhbHVlIjoxMC42NDIyNTgzMywibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkxNzQ1Yjk2YjdjZThkZGMyZTM1M2M3MDMwOGMwODhiMmEyMmVhMTcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5MTc0NWI5NmI3Y2U4ZGRjMmUzNTNjNzAzMDhjMDg4YjJhMjJlYTE3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6Z2hna3Vra2w4Z21ocHd4NTc4cXZ5dnB6OWo1Z2gyenV3eWVsajc1NyJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMDYxMDc2ODAxYjM2YzM1NDc4MjZhZWVjNWM5ZGY5NTk3NDVjMDMyNTJiYmJiMzIwNGE5YWE0OTE2YmEzNWIyMSIsInZvdXQiOlt7InZhbHVlIjoxMC42NzEyODg3NywibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGU1NDFlN2NmM2YwM2JlOTc2M2U4OGM5MWM4ZmJjMzg1OTZkNzY5OTEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRlNTQxZTdjZjNmMDNiZTk3NjNlODhjOTFjOGZiYzM4NTk2ZDc2OTkxODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyajVyZTcwOHVwbWE5bXJhenhmcmo4bWN3emVkNG1mank4ZXpqdmM3NSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMzlkYWVkMGY5OGY3ODc5Yzc5MWY5NzJkODM4ZDExNWY1YzIzODMxYzg3ZDIzYTE2ZDUyYTE5OGY4MWU2ZGRjMSIsInZvdXQiOlt7InZhbHVlIjowLjAwMDIzNTkyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjY3NzA5MTc3NDdhOTU2NGE0YTkxMjlhMzllN2EwMWFiNTAwNjE3MSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDI2NzcwOTE3NzQ3YTk1NjRhNGE5MTI5YTM5ZTdhMDFhYjUwMDYxNzE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFuOHd6Z2h3M2FmMmU5eTR5ZmY1dzA4NXFkdDJxcnB3eXM1NGp0N2czIl19fV19",
+ "eyJ0eGlkIjoiNjM4M2JkODVhNzZhYTQ2MjQ3MTJhZTUyYzUyYjFhYzdmODQyOGFlNWZmMTdmY2UxMjczMDM0NDA4NmY3YTIxMSIsInZvdXQiOlt7InZhbHVlIjowLjE5NDEyNiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDE0MjA0Mzk4ZTNiYTgzMzU5NmYzZTA3MGE2NDM1MDU0OTZmMWI5ZTQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxNDIwNDM5OGUzYmE4MzM1OTZmM2UwNzBhNjQzNTA1NDk2ZjFiOWU0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxMnpxc3VjdXdhZ3hkdms3MHM4cGZqcjJwMmZkdWRldXNxc3RqN3FlMiJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMWRhMzNjZDE1ZjBiNGI2YTQwMGMzOTQzYjNmNGUzYzM4ZDhiM2ZkODMyMTBiNjhhMWI0ZDgwYWUxOWQ0ZTZmYiIsInZvdXQiOlt7InZhbHVlIjowLjA0OTU3MiwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDZmNzUyZGVkZjk5ZjdiYTQ3ODgzYTI1MzBjNmQyOTFkYjgzODg3NGEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2Zjc1MmRlZGY5OWY3YmE0Nzg4M2EyNTMwYzZkMjkxZGI4Mzg4NzRhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwaGgydDBkbHgwaGhmcmNzdzM5eHJyZDl5d21zd3k4ZmdsNXp0bDR6ZiJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNGU4N2NlM2I1ZTgwOTIxYzM3NDkwYzRmMWRhNTc5NjhmZTUyYmMxNjE3ZmY5Y2UzMTc0MWNhMzdmZDA4ODUwOCIsInZvdXQiOlt7InZhbHVlIjowLjI2MTIzMDYsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAwMmIwNjQyYzk3ZjVkM2VlMDliNTUwMGRmYWEzZmZhNTViZDNlMTkzIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MDJiMDY0MmM5N2Y1ZDNlZTA5YjU1MDBkZmFhM2ZmYTU1YmQzZTE5Mzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXB0cWVwdmpsNmE4bXNmazRncW03NHJsN2o0aDVscGp2MHE2ZDd5eHUiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiODljN2ZmZDFjYzkyODg3NjRkZTczOGUxYTY2YmRhNDJlYmEwMWNmNTZmMzAwMDUxODk5N2Q1MTZhYWE2MGIyYSIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY2NjY2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNmM3OTkzNzRmMTZhM2E0ZmJlZTU2MGVkMTljM2VmYmZmM2QwYWM4NTE4OWVmOGRiMWU1NmQ4N2M5NThjYjM5ZiIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY3OTc4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZmY1MWNiY2E1OGNkZDJlMmQyYWRiZGJhYTc5MmNmNGMwODhkOWZiZWMxNzIyYWNlNjE5MmU3YWQ0YzdiMWMyYiIsInZvdXQiOlt7InZhbHVlIjo5LjAyNmUtMDUsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA1NWI5NTU5ZjQ2YTkyOWZiNzViY2U3ZWRjOWNiNGE4ZGE0NGI5ZGM0IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NTViOTU1OWY0NmE5MjlmYjc1YmNlN2VkYzljYjRhOGRhNDRiOWRjNDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDJtajR2bGc2NWpuN200aG5uN21qd3RmMng2Z2p1YWNzcnRobTVzYXAiXX19XX0=",
+ "eyJ0eGlkIjoiZGNhMWJjMzM2YWFmNDdkNTQ1ZWU3MDQyOGJhN2YxYmQyOTZmNzc0YjU1ZDgwMmI5NjY1YWRmZWMyYjc3N2FhNSIsInZvdXQiOlt7InZhbHVlIjowLjAwODA0MDI5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTViOTU1OWY0NmE5MjlmYjc1YmNlN2VkYzljYjRhOGRhNDRiOWRjNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU1Yjk1NTlmNDZhOTI5ZmI3NWJjZTdlZGM5Y2I0YThkYTQ0YjlkYzQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXAybWo0dmxnNjVqbjdtNGhubjdtand0ZjJ4NmdqdWFjc3J0aG01c2FwIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiOTMzNDk1MDRiNGM0ZmQ4YWY3NjZjMzhjZWQ3OGZhOWY4ZWEyOTM0Zjc5NDdkMWQxMjNlMzY2NDE1NmI4Yzc0NCIsInZvdXQiOlt7InZhbHVlIjoxLjA3Mjc3NzIsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA4NTIzNjg2YjRjNWYzZTRhODRkZjZiNDMyZWYwOTVlNTQ4NWU2OGEwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ODUyMzY4NmI0YzVmM2U0YTg0ZGY2YjQzMmVmMDk1ZTU0ODVlNjhhMDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxenpqeDZydGYzMG51ajV5bWE0NXh0aHNqaGo1c2huZzVxNjk2bjJwcjMiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMmQyN2M5ZjU2YjU1YzYzYzI0Y2I5ZmUzOTA2MWRjYjAwYWQwNGIzZmMzN2VlNTIzNjI1ZjVkNTBkNTBjZjNlNyIsInZvdXQiOlt7InZhbHVlIjowLjQxMzQ1NjA1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjNiZmY5OTNjZmU5M2MwYjYzODBkNzNlNjllMjBhYTM2YTYwZDg3ZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGIzYmZmOTkzY2ZlOTNjMGI2MzgwZDczZTY5ZTIwYWEzNmE2MGQ4N2U4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXplbWw3dm5lbDVuY3ptcnNydG51NjB6cDIzazVjeGMwY3hudnF3eGsyIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYzU0MWZlYzdmMmNmYmZhNjk1ZDY5MmM4MTQ5YzI4MjUxOWIyZDY4OTBhNjQ1YmExMTE3MjEwMjg2ODIzYWVmNiIsInZvdXQiOlt7InZhbHVlIjowLjA0ODAxOTcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDZjMTgxMTI4NjI5ZDg3OGVjNzE2NmNhMzg2MzY5YTI5NjAzODQ2NmNlM2Y5N2Q2ODBmMzAyNWNiODQwMWFjMzMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNmMxODExMjg2MjlkODc4ZWM3MTY2Y2EzODYzNjlhMjk2MDM4NDY2Y2UzZjk3ZDY4MGYzMDI1Y2I4NDAxYWMzMzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRrcHN5Zmd2MndjMHJrOHplazI4cDNrbmc1a3F3enhkbjNsamx0Z3B1Y3p0anV5cXhrcng1OXRqNDlzNiJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYzk1YmI3MGU3MmQ3ZTFkZDg0MTViYmY2YjdhYmNjMGI1OWU3YjZkYTkxYjgwMzc2YTFlMjFkM2I0MzE4MWI2YyIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY2NjY2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMjMzZGEwYTk3Njg0N2FlM2ZiYTJmMmZjYmI5NDYxYjI0YzViMDgyNDVlYTlmOGQ0NTYzOTE0MTIzMmQ4NzQ5MyIsInZvdXQiOlt7InZhbHVlIjowLjAwMjgwMDI1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZWFlZTE1MmQ4NDRhY2NhZDFkODllNzc2NGQ0ZWQyNWZmMWFlMTdlNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGVhZWUxNTJkODQ0YWNjYWQxZDg5ZTc3NjRkNGVkMjVmZjFhZTE3ZTc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI0d3U5ZmRzMzl2ZXRnYTM4bmh2bjJ3NmYwbHJ0c2h1dXVuNXY2cW42Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMjEzMmZjMGIyZTE3ZWEzMTU0NjI0ZWI2ZTliMjY0MGI1NTBiYzgzOWRmYWE2Y2NhMGRlMWI1OWQ0ZGUxNjMyZCIsInZvdXQiOlt7InZhbHVlIjowLjg4NTU0MDg2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjgwMWNlODEzYzk3M2I3NWU3NGI3ZmEzYzEzNjA0NmI0ZGIzM2E4OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDI4MDFjZTgxM2M5NzNiNzVlNzRiN2ZhM2MxMzYwNDZiNGRiMzNhODk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXE1cXJuNXA4anRua2EwOGZkbDY4c2ZrcTM0NW12ZTYzeXFsZzVscHR5Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZWRlOGJlMDMyMzQ4OGNiYmFjOGViYzQ0MWM3OWMyOGI3M2JhZWJiOTFlNjk1MzJlNTNiMDEwMjdjMTVkOWVlZiIsInZvdXQiOlt7InZhbHVlIjowLjAxNjE4OSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGRhNmY3Yjg4MDFkNDRhZjQ5MDZlMzUwNjc5MTJkNTk1YTQ5NDc3MzIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkYTZmN2I4ODAxZDQ0YWY0OTA2ZTM1MDY3OTEyZDU5NWE0OTQ3NzMyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyZHg3N3VncTgyeTRheXNkYzZzdjdnajZrMjZmOXJoeGc2ZXBtd2c0YSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZjQyNWUwMDI3MGI0MzQyNzQ4MTNjN2YyNGE1MjJkMWUwNzhiNTc5ZWQ4NDkwOGJmMmRhMzNiZjQwZGM4ODY1YyIsInZvdXQiOlt7InZhbHVlIjowLjIwMjU0MDI5LCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBjOTNkNDU3ZDgwY2ZiOTcyZTI5NGEwZmNjMTQ0YTJkOGJhZTlkMGVhIE9QX0VRVUFMIiwiaGV4IjoiYTkxNGM5M2Q0NTdkODBjZmI5NzJlMjk0YTBmY2MxNDRhMmQ4YmFlOWQwZWE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnByeW42M3Rhc3I4bWp1aHpqanMwZXMyeTV0dnQ0NndzYWdrMzA4N2RnaCJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNGY0NTljYTA3YTE0ZTc3Y2UyMDEyZWQ4ZDVjMTA4MTE5M2ZhNjg1YzFlNzk5NTRmMmJjZmQ3ZDI4NjQ3MmMzYiIsInZvdXQiOlt7InZhbHVlIjoxNy4zMTQ1NTU0OCwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhNmVlMDRjYjc3Mjg3NTlhOGRlZTZiZWQzYTc4MTMxN2EzY2UzZDJlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6bnd1cHh0d3U1OHR4NWRhZTQ3NnduY3p2dDY4bjNhOWNtNG1yOXB0bSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiODAwNmUwYmYzNDRmYzY0ZDIyZTEzNWQzOGVjYWUwNzQ2ZTUxMzMwMjFiNDA5MDMxY2EzZjA1MTA3ZmI2MmFmYiIsInZvdXQiOlt7InZhbHVlIjowLjAwMDYyNzk2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTViOTU1OWY0NmE5MjlmYjc1YmNlN2VkYzljYjRhOGRhNDRiOWRjNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU1Yjk1NTlmNDZhOTI5ZmI3NWJjZTdlZGM5Y2I0YThkYTQ0YjlkYzQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXAybWo0dmxnNjVqbjdtNGhubjdtand0ZjJ4NmdqdWFjc3J0aG01c2FwIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMzIzMGQyNTBlMGQxZTkxYWM2MTJlMGE5YTZkNDc4MjVhZmY5M2ZjNWQ4NWQ4MTMwMDFiMWZjZWIyYzJjNzY1MiIsInZvdXQiOlt7InZhbHVlIjoxLjE4ODk4NTM3LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOWQxNWJhODcwYmFiNGRiYmZmYmZkZmIyOTU5MTQxMjdkNzAyYjkwNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDlkMTViYTg3MGJhYjRkYmJmZmJmZGZiMjk1OTE0MTI3ZDcwMmI5MDc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp3M3R3NThwdzQ1bXdsbGhsMG05OXYzZ3luYXdxNGVxdXduM255dzhmIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiOTcxNDAwNzFiODYxMjljYzNiMjRkOWJjOTdkOTE1NDE2ZjhjZWNlMTQ1NTYxMDJjOTQ5YjJlMjkzN2RhZDIwNSIsInZvdXQiOlt7InZhbHVlIjowLjAwMjA3NjAxLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDY4Nzg1NDRhNWRjYzNjOTkxZjUxYmRjZTc5NzMxN2UzMzA3ZjM5ZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDA2ODc4NTQ0YTVkY2MzYzk5MWY1MWJkY2U3OTczMTdlMzMwN2YzOWY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFyZzBwMnk1aHd2OGp2Mzc1ZGFlZXVoeDlscnhwbG5udWduc2Y4cXd0Il19fV19",
+ "eyJ0eGlkIjoiN2U1NDQyZjA3Y2E1YmIzYmVkZWI1MTFhZTMwZjQyYmIxYzQzNjg5ZmM0NTIxNGQwZmY0MThjNGU1YTExNWViYSIsInZvdXQiOlt7InZhbHVlIjowLjAwMjM0OTgyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNWIxODg1YmQ2NjhjMmRlYzQzNTVlOGIxYzI1NjUwYTBjZTg1OTRlZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDViMTg4NWJkNjY4YzJkZWM0MzU1ZThiMWMyNTY1MGEwY2U4NTk0ZWY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBkMzNwZGF2Nnh6bW16cjJoNXRyc2prMnpzdmFwdjVhdWMzc3IzZTBtIl19fV19",
+ "eyJ0eGlkIjoiMGEzYjJlZDBkMDc5NjljMDkxNzdiMmQxMTQ3NGY0MDBkN2NkMTYyMjNiZGRkZWIyYjg0NjkyYzQ1NWExM2QxYSIsInZvdXQiOlt7InZhbHVlIjowLjAwODI4NTM1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDM4MWYwMmRhOTNhZjk1NTQxMGUwNjU5MTBhYjE4Mzk0ZDQxMjczMyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDAzODFmMDJkYTkzYWY5NTU0MTBlMDY1OTEwYWIxODM5NGQ0MTI3MzM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFwY3J1cGQ0eWEwajQycHBjcjlqeTl0cnF1NTZzZjh4dnlwMDZsanFlIl19fV19",
+ "eyJ0eGlkIjoiYjcyMTZhMjM2NjRmMmNiMWJhN2Y4YTUxYjViYWNiZjRjNjY2NzUxNWI4NDg5OWE1YWI5MDhhMDEyMzg4MTU5NiIsInZvdXQiOlt7InZhbHVlIjowLjAwOTk3MDU3LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMzE3ZTg4NGE1ZWY3MWYyNGVkYWYyZTU1MWJmNDU5ODIyNzc0YTkzMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDMxN2U4ODRhNWVmNzFmMjRlZGFmMmU1NTFiZjQ1OTgyMjc3NGE5MzA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFjaGF6ejJ0bW0zN2Y4ZDR1aDkyeGw1dHhwendhOWZ4cXd4eWx5ZjA4Il19fV19",
+ "eyJ0eGlkIjoiYTVlNmUyOGJjOTQ0MTA1MDY2NTcxMjU2ZDYzNmI2ZjVjMjJkMTVjZjE2NDM5OGM1MmFhYmQ1ZjFhYTBiNTAzYyIsInZvdXQiOlt7InZhbHVlIjowLjAxMjE4OTg2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNmJhMTI1OTgzMDk5MzlhZWFkZGY1M2FkOWFhYjcwYTZjNzI3YjJkNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDZiYTEyNTk4MzA5OTM5YWVhZGRmNTNhZDlhYWI3MGE2YzcyN2IyZDU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA0NnpmdmN4enZubnQ0ZG1hZjZteDR0d3pudndmYWo2NWVycHM5M3p6Il19fV19",
+ "eyJ0eGlkIjoiNWNkNmRjMmI3YmVhZjAxM2Y1NzZkNWJiYmUzYzViYzAxZTI0MGYyMzZlNmVhMjg3NWNmMWZkMzkyMzNjNTczZiIsInZvdXQiOlt7InZhbHVlIjowLjAxMzU0NzIzLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjliMjNlMmNmZDU2NzIwMmY3MjA4YzFjNTVjMDE3OWU5MWRiY2U5MSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGI5YjIzZTJjZmQ1NjcyMDJmNzIwOGMxYzU1YzAxNzllOTFkYmNlOTE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp1bXkwM3ZsNHQ4eXFoaHl6eHBjNHdxejcwZnJrN3dqeWRleGpmZmgyIl19fV19",
+ "eyJ0eGlkIjoiMTI1YWY5ZjBkYmE3MzY0M2Y0NDBlZDU2M2MxYzA0MGU3YjhhYWRiMWUyMTQ4MmJjMmU4OTc2MGY4M2YzMTQ0YiIsInZvdXQiOlt7InZhbHVlIjowLjAxNTMwNzk5LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNDE2MjkyODIyNjc0YzYxZGFjOWJhNTQ1NmMwMjE5NDg4ZGYwZTczMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDQxNjI5MjgyMjY3NGM2MWRhYzliYTU0NTZjMDIxOTQ4OGRmMGU3MzE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBxazl5NXp5ZTZ2djhkdm53ajUybXF6cjl5Z211ODh4eXd0ZWhxem1wIl19fV19",
+ "eyJ0eGlkIjoiYjBlN2ZjZWMxNjZiNGU0NzdlOWViMTc1ZjkwY2UwMWRhMWJiODQ5ZjljZmQ0MTJlZjRiNzU1OTE5MDE4OTJkYSIsInZvdXQiOlt7InZhbHVlIjowLjAxNTg3OTc3LCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTBkMzFlZGFlYTY2NmM0Yzc0ZmIzYzM1OGJjN2I0NWQ4MzZhZjcyOSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkwZDMxZWRhZWE2NjZjNGM3NGZiM2MzNThiYzdiNDVkODM2YWY3Mjk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpnZHg4azZhZm54Y25yNWx2N3J0ejc4azN3Y3g2aGg5eXZhZjh2cWRnIl19fV19",
+ "eyJ0eGlkIjoiMjNlZTI5YzU4NzY3ODRjMTc1OTMxNWNhNzE2ZGQ1YjkyNjVjNTJhMTEzNzVlNmIxZjIyYzU4Yzc4ZGU5YzgzMiIsInZvdXQiOlt7InZhbHVlIjowLjAxOTc5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYWI2Y2M5ZmRiY2FhMDU2ZDdlNTAxN2QxZGY3ZGY2OTg4NzQxZDA2NyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGFiNmNjOWZkYmNhYTA1NmQ3ZTUwMTdkMWRmN2RmNjk4ODc0MWQwNjc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXo0a2VqMGFoajRxMm10NzJxdGFyaG1hNzZ2Z3dzd3N2dXlqYzQ3NWh6Il19fV19",
+ "eyJ0eGlkIjoiYzYxMzJhZDFkNTNiOGNlYjYxNjRmOTMwMWUzZmNiNGQ1ODYyNjgzNzFhZjY0ZjE4NDNlM2ExNjZlZTQzZWViMyIsInZvdXQiOlt7InZhbHVlIjowLjAxOTgzMjY5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYzI0ZjZkZWY0MWQ0OWMwN2IwZjc0OThlZGRlNGUxODc0OTZiMDQxNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGMyNGY2ZGVmNDFkNDljMDdiMGY3NDk4ZWRkZTRlMTg3NDk2YjA0MTQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJweTdtMDBnODJmY3BhczdheWNhaDB5dXhyNWo2Y3l6c2MzZ3p5anZsIl19fV19",
+ "eyJ0eGlkIjoiZThhNWU4YzY4MTY2MWEzOTZmZTZlMmJiNDI0NDNkNGQwNTE0MjdiMzFiZGZlMTRmNDBmZjNiNzA1ZGQ0NTNmMCIsInZvdXQiOlt7InZhbHVlIjowLjAyMTAxNzY2LCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZjRkNmNlZTg4ZGY1YWNhNTVjOGU3MjcyMThiMzc1YTExZGU3MDVlZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGY0ZDZjZWU4OGRmNWFjYTU1YzhlNzI3MjE4YjM3NWExMWRlNzA1ZWY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI2ZGRuaGczaDY2ZWYydTNlZTh5eDlud2tzM21lYzlhdXZ4MmswYWx6Il19fV19",
+ "eyJ0eGlkIjoiNTAyMWI4YTgyZDVmMDNkMTc0ODEzOTFmNzliOTkyMmRiNzE5ZTY2Yzk0MzQxNzVhNDgzOWUwM2IzZWJhMzk2OCIsInZvdXQiOlt7InZhbHVlIjowLjAyNjQ0Mjg2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMGRmNjJmNjQ3YjExNWExMzRhZDU1MWRiM2YyNmQ4N2U3YmE3ZDI0MSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDBkZjYyZjY0N2IxMTVhMTM0YWQ1NTFkYjNmMjZkODdlN2JhN2QyNDE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF4bHZ0bXkwdmc0NXk2MjY0Z2FrMGV4bXBsOGhmN2pneWxqYWVocXY2Il19fV19",
+ "eyJ0eGlkIjoiODQ0NDJjZTA5ZDA1NTZmY2QwMTUwZTgwYTkyMDYwMzliMDM5YzY0ZmMwYzgyNDBkNTFlNGY4NGZmYjBmOTdmMiIsInZvdXQiOlt7InZhbHVlIjowLjAyNjc1NjAxLCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZmM1OTY0NGVlNzg0YjQxZjUzNDNlODY4NTdiZmI2NTU0MmU2NWY0NiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGZjNTk2NDRlZTc4NGI0MWY1MzQzZTg2ODU3YmZiNjU1NDJlNjVmNDY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI3OWplend1N3p0Zzg2bmcwNXhzNGFsa2UyNTllamxnY3ZlMm1oam0zIl19fV19",
+ "eyJ0eGlkIjoiNGY0NTljYTA3YTE0ZTc3Y2UyMDEyZWQ4ZDVjMTA4MTE5M2ZhNjg1YzFlNzk5NTRmMmJjZmQ3ZDI4NjQ3MmMzYiIsInZvdXQiOlt7InZhbHVlIjowLjAzMDAzLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZjk3NzYyMjZiYWE5MGFkMDAzMDQ2NmI0YmViODFmNjcwNzU2ZGRmMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGY5Nzc2MjI2YmFhOTBhZDAwMzA0NjZiNGJlYjgxZjY3MDc1NmRkZjA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ1aHdjM3hoMjVzNDVxcnEzbnRmMDRjcmFuc3c0a2E3cWhjY3RyZjI5Il19fV19",
+ "eyJ0eGlkIjoiNjhhNGFkNDljNjI2NjM4NDA1MDhlZWUyOTgzOTk1Nzg3YjQ5YzE0NDkwOTJmODkwNjljYWZkMGRkNGQ1YjRkYSIsInZvdXQiOlt7InZhbHVlIjowLjAzMTMxODE0LCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZGM3NzU4YTBiNjM5YjY5MmNkODMyYWU2MDQ2OWMxMGIzMDc5ZjE5YSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGRjNzc1OGEwYjYzOWI2OTJjZDgzMmFlNjA0NjljMTBiMzA3OWYxOWE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ3OHdrOXFrY3VtZHlrZHN2NHd2cHJmY3k5bnE3MDNuZ3dtOWEwMzNuIl19fV19",
+ "eyJ0eGlkIjoiOTk4MTNhMjkwZmNlYmVlODMwN2I0MmRiMDYwZmI3NzE1N2ExNzdhZDU1ZjdmYWQxMDM0OTEyMDA1NGUwZGUxNSIsInZvdXQiOlt7InZhbHVlIjowLjA0MjA3OTc5LCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjdkZjNlNDZlZDVkM2Y5ZjE4MjUwYTY1YmFiZTg0MjQ1NDBlOTJiNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDI3ZGYzZTQ2ZWQ1ZDNmOWYxODI1MGE2NWJhYmU4NDI0NTQwZTkyYjU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFuYTcwanhhNHdubDhjY3k1OXh0dzQ3c3NqOWdyNWprNWRoczlyYzJwIl19fV19",
+ "eyJ0eGlkIjoiZjY1M2Q3Yjc3M2E2ZmM1Yzk1YjhmZTU5YmZjNTFjMzNjYzdlZDFiMzY1NDkwOWRhNWMzMGQ2MmQzNzQ4MzRkZSIsInZvdXQiOlt7InZhbHVlIjowLjA0NTUyNjU4LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjc2NDk0ODJmNmRhMThiNTYxMzQ1NmRhNjhkNGYyYTFjOGRiMTNiYSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGI3NjQ5NDgyZjZkYTE4YjU2MTM0NTZkYTY4ZDRmMmExYzhkYjEzYmE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpta2Y5eXo3bWRwM2R0cHgzdGQ1Nng1NzJzdTNrY25oZ3Z5bWY2Z3NsIl19fV19",
+ "eyJ0eGlkIjoiNDc0NWFiN2M4OWQ1NzBlNGRiMTBmMGYyYzkyMTJkNDQ4ZDI2ODkwMDc4ZjVjN2QwNDc4MWZjMmI2ZTU5OWVmYyIsInZvdXQiOlt7InZhbHVlIjowLjA0NzAxMDc0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTIyYjJkYWE0YTMxYTliMjk5MGI5MjBhMjBkNDAyYzE5MzI4NTJkMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkyMmIyZGFhNGEzMWE5YjI5OTBiOTIwYTIwZDQwMmMxOTMyODUyZDA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpmemt0ZDJmZ2M2bnY1ZXB3ZnE1Z3g1cXRxZXgyemo2cWswbW03cmh6Il19fV19",
+ "eyJ0eGlkIjoiMmQ5OWM1YmQ2M2VlYTA3OGJhMTJkODkxY2E2NDU2NzBiZTRhOGZjNDk5OTExMmE4MzBhNmY0ODJhMzg1YzhkYyIsInZvdXQiOlt7InZhbHVlIjowLjA0NzMzNDEyLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDEyMDgzZjE0MTA3MGZjZmI4ZGE2MTRkNGM5ZTk5YWM4OWQ1NGYxMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDAxMjA4M2YxNDEwNzBmY2ZiOGRhNjE0ZDRjOWU5OWFjODlkNTRmMTI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFxanBxbDNneXJzbG5hY21mczU2bnk3bnhrZ240MjB6ZzNsMnI4dWp4Il19fV19",
+ "eyJ0eGlkIjoiZTg1YTQ3ZjU2YmQ1YWZhMTEzNjk4ZDUwYjU2Mzg2YWY3NWIzMzAwN2I1ZTA0Mjg4MjE3YjU2NGQ4Mjc3OGY4YiIsInZvdXQiOlt7InZhbHVlIjowLjA0ODkwMjEyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNzQwZmViMDQ1YjE5Yzc1ZmU3ZWY5NTZjN2QwYTYwMmY5YjI5YzJhNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDc0MGZlYjA0NWIxOWM3NWZlN2VmOTU2YzdkMGE2MDJmOWIyOWMyYTc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA2cWw2Y3l0dnZ1d2hsOGE3MmtjbGcydnFoZWsyd3o1dXhxcTVnZHg4Il19fV19",
+ "eyJ0eGlkIjoiNzYzM2I0ODAxMTc5NGM1MDE5NjI1Zjc5ZTRmNDYxZTE3ZDY3ZGJkYWY4MDE0YzA2YWI0NzZiZGFjZTk1OTdkNyIsInZvdXQiOlt7InZhbHVlIjowLjA1MTc3NDU1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDEyMDgzZjE0MTA3MGZjZmI4ZGE2MTRkNGM5ZTk5YWM4OWQ1NGYxMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDAxMjA4M2YxNDEwNzBmY2ZiOGRhNjE0ZDRjOWU5OWFjODlkNTRmMTI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFxanBxbDNneXJzbG5hY21mczU2bnk3bnhrZ240MjB6ZzNsMnI4dWp4Il19fV19",
+ "eyJ0eGlkIjoiNGMxZmJjNDZjZWUwYjk2MWY4NzlhMjVhYjljY2RlMjFlNDM2MDI0NmMxMDY3ZGNiYWU1ZWYwZjkxODA2NDNjMiIsInZvdXQiOlt7InZhbHVlIjowLjA1MjI4NDIyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTk1YTFjZDM3ODJmNThhNjU3ZjFmYjg5Nzc0ZDI1OGQ3MGExYjgxNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDk5NWExY2QzNzgyZjU4YTY1N2YxZmI4OTc3NGQyNThkNzBhMWI4MTQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp2NDU4eG4wcWg0M2ZqaDc4YWNqYTZkeWt4aHBnZGN6czhwN3IzMnkyIl19fV19",
+ "eyJ0eGlkIjoiYWNjMWNmNjZjZDRlNWQxMTM1NzJhZjZjMDRjZDFhYzUzYjlmYThmYTkzNWI4N2ZmMWE2NjI0MzU5M2NiMzliNiIsInZvdXQiOlt7InZhbHVlIjowLjA1MjU2OTU0LCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNmQ4MmMwMTY1ZTQyNjFkZTAwODU0OTJiM2FmYTE1YWIyODM5NjM3MyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDZkODJjMDE2NWU0MjYxZGUwMDg1NDkyYjNhZmExNWFiMjgzOTYzNzM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBrYzlzcWt0ZXB4cmhzcXM0eWprd2g2ems0anN3dHJ3djBqaGxleGVuIl19fV19",
+ "eyJ0eGlkIjoiZjk4YjIwMzAxZDYyY2YyZDdhNDE5NTUzNDE5YzEzNDFmYThjZThkNzc2NjFkMWE3NWY1MjkyYWI2MDY2Y2UzOSIsInZvdXQiOlt7InZhbHVlIjowLjA1MjgzOTgsIm4iOjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBiMzQxMmQ5MzYyNTJhNDgwNDdmZTdlNjhiNDVjYTY5ZWY1NjEyOGFlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YjM0MTJkOTM2MjUyYTQ4MDQ3ZmU3ZTY4YjQ1Y2E2OWVmNTYxMjhhZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemU1enR2bnZmZjJmcXo4bGVseDNkenU1NjAwMmNmZzRjZG4zNm5rZHgiXX19XX0=",
+ "eyJ0eGlkIjoiMTY5ZmY5MDUyODFmZTIzZmFlMjdlOTRlYjZiNjQ5YmNhODZjMjk1YTMxMmU2ZTQxNTI0ODI3YTdjZjc0ZGY4YyIsInZvdXQiOlt7InZhbHVlIjowLjA1MzAxNTI2LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjIzYTNlYTY4ZTAyYjgzOGQxZGNmZDY1OTU5NGU3MTJlNmIwODVjMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDIyM2EzZWE2OGUwMmI4MzhkMWRjZmQ2NTk1OTRlNzEyZTZiMDg1YzA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXEzcjUwNHgzY3B0c3d4M21uN2t0OXY1dXVmd2R2eTljcWM5ajJqZnlqIl19fV19",
+ "eyJ0eGlkIjoiYjBlN2ZjZWMxNjZiNGU0NzdlOWViMTc1ZjkwY2UwMWRhMWJiODQ5ZjljZmQ0MTJlZjRiNzU1OTE5MDE4OTJkYSIsInZvdXQiOlt7InZhbHVlIjowLjA1NjYxMzI5LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNDYyNDA0ODQzYmZjNjgwZGFlYWM3OGExYzAzY2ExNzg4MzVmZTM0OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDQ2MjQwNDg0M2JmYzY4MGRhZWFjNzhhMWMwM2NhMTc4ODM1ZmUzNDk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXByemdweXk4MDd4c3JkdzQzdTJyc3B1NTl1Z3hobHJmeW5xejZqd2EyIl19fV19",
+ "eyJ0eGlkIjoiOTk4MTNhMjkwZmNlYmVlODMwN2I0MmRiMDYwZmI3NzE1N2ExNzdhZDU1ZjdmYWQxMDM0OTEyMDA1NGUwZGUxNSIsInZvdXQiOlt7InZhbHVlIjowLjA1ODkyOTU0LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgY2M5ODY0OTM2OWYwNzE0MmI3NzkzNjVhOWYyNzQzMjg5YTI3NTM2NCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGNjOTg2NDkzNjlmMDcxNDJiNzc5MzY1YTlmMjc0MzI4OWEyNzUzNjQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ4ZnNleW5kOGM4enM0aDB5bTk0OGU4Z3Y1ZjVmNm52czlkN200dHc4Il19fV19",
+ "eyJ0eGlkIjoiOWJmNDhhMTkwMWFhZjBkYjYyM2FiNDQ4MjgxNGE1MTNmZDQ1MGIzYjQ1MGQ5N2M5MzBhOTczYzM2ODIxN2YwNiIsInZvdXQiOlt7InZhbHVlIjowLjA2MzUzNzI1LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjY0MjJmODQ2NjZiYWEwNDViNzdiNWVkNDA0NmU4NmZiMDI0OGZjNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDI2NDIyZjg0NjY2YmFhMDQ1Yjc3YjVlZDQwNDZlODZmYjAyNDhmYzY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFueXl0dXl2ZTQ2NXB6bXc3Njc2c3p4YXBobXFmeTBjY3l0Y2czMDJuIl19fV19",
+ "eyJ0eGlkIjoiM2QwOTliMGY2ZWU4YzBmNjhmODY2YTE2YjExMjc1NzI4NGUyN2U3MjY0ODYyYmZhY2E1ZDVmYTU5ZjkxNWYxYSIsInZvdXQiOlt7InZhbHVlIjowLjA2NDA5MDkyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODAwMjQyYTBlM2IwNTdlZjIzMzI1NTNhZmU3MjkyNjVmNjMxNzlkMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDgwMDI0MmEwZTNiMDU3ZWYyMzMyNTUzYWZlNzI5MjY1ZjYzMTc5ZDA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpxcXlzNHF1d2M5MG1lcnhmMm40bG5qamZqbHZ2dGU2cWpycGtkNzl2Il19fV19",
+ "eyJ0eGlkIjoiZjQzYjU0NGY1ZThmYWY2NTE4ZTM3MjQzMGQwMjUwZmViYjg5MDI2NzE2NDI3YzhjZTkwYzVhNGVjOTcyNjAzZCIsInZvdXQiOlt7InZhbHVlIjowLjA3MzA1NTM0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgM2QwZjE0MmE1YWYxYjQ4YjQ4ZjdmOWMzYjc0ZDVhOGY2MDFkZWQ1NSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDNkMGYxNDJhNWFmMWI0OGI0OGY3ZjljM2I3NGQ1YThmNjAxZGVkNTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXE3czc5cDJ0dGNtZno2ZzdsdXU4ZDZkdDI4a3E4MGQyNXVmcXVweTZzIl19fV19",
+ "eyJ0eGlkIjoiNjU1N2JiYzdjODY3YjIyOGE0NmMzMmU1ZGIwNjJmZGFhOTE2NDJmYjQ2ZGMxNWNiMmQyOGVkZWRmZDdhYTUwNSIsInZvdXQiOlt7InZhbHVlIjowLjA4MTA1MjMyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTU0OGNhOTEyMjQ5ODZhOTkxYjdiZGQ5N2U5NjhkMDQzYTA4NWU1MCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGU1NDhjYTkxMjI0OTg2YTk5MWI3YmRkOTdlOTY4ZDA0M2EwODVlNTA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJqNTNqNTN5ZnljZDJ2M2s3N2FqbDVrMzV6cjV6ejcycTB0bjRuZGV2Il19fV19",
+ "eyJ0eGlkIjoiNzI1MzFjNDdhNjNkYzc3OTE5YzFlZTRhMTU3OWI1YzQ2YTQzZmExNTQzMzQ3YzBjMDRiNmJhYjYwOWQ1ODdiOCIsInZvdXQiOlt7InZhbHVlIjowLjA4MzA0Mjg5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYWQ3M2MxOWEwZjdjMWFjMDMzZjdhMGJjYzU3Y2FjYjQ4OGNhMTUwMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGFkNzNjMTlhMGY3YzFhYzAzM2Y3YTBiY2M1N2NhY2I0ODhjYTE1MDI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpraDhzdjZwYTdwNHNwbjc3c3RlM3R1NGo2ZzNqczRxZzk2cnd1ZDQzIl19fV19",
+ "eyJ0eGlkIjoiOWJmNDhhMTkwMWFhZjBkYjYyM2FiNDQ4MjgxNGE1MTNmZDQ1MGIzYjQ1MGQ5N2M5MzBhOTczYzM2ODIxN2YwNiIsInZvdXQiOlt7InZhbHVlIjowLjA4NDgwODA3LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTBmZmUyODQxMDQzNTk1ZTg0OGZjNTUyODdjMDRkMjA1ZjQ4ZjhhZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDEwZmZlMjg0MTA0MzU5NWU4NDhmYzU1Mjg3YzA0ZDIwNWY0OGY4YWY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFnMGxjNXl6cHA0amg1eTNsejQ5cDdxZjVzOTdqOGM0dW4wa2ZsNGNhIl19fV19",
+ "eyJ0eGlkIjoiMWIzODEyYTZjNjExMDFlOTJiMWU2ZDRjMTM1MTRlYWZmNzhkNWFjMDNjZWMwNTEzZmQ2NjkzMzJjMGIzMjQ4OSIsInZvdXQiOlt7InZhbHVlIjowLjA5MDU0NjQ1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTQ4YzI2MjEwMzVhM2ZiNGVhZDQ1ODc0YzRlNTgxMTQ2YzE0NTVjMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGU0OGMyNjIxMDM1YTNmYjRlYWQ0NTg3NGM0ZTU4MTE0NmMxNDU1YzE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJqZ2NmM3BxZGRybGQ4MjYzdjhmMzg5c3kyeGM5ejRjeWh0d3dsbGRlIl19fV19",
+ "eyJ0eGlkIjoiYzg3ZGNkOWEyZDA3OGI0YTYwZGM2YTRjOTMzNjlmNDczY2I3MDRlMzk0OWQyNTdlNTQwZTM5NWE5ODQ4ZjFkMyIsInZvdXQiOlt7InZhbHVlIjowLjA5MDcxLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMGY3YzcyMzliMGVkZGFkODQ1MWIzNGEyY2RjNzliY2NhNjM1MDBhZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDBmN2M3MjM5YjBlZGRhZDg0NTFiMzRhMmNkYzc5YmNjYTYzNTAwYWQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXE4aGN1M2VrcmthNGt6OXJ2NjI5bnc4bjB4MnZkZ3E0NWs3djA3bWVmIl19fV19",
+ "eyJ0eGlkIjoiZWU0YWY2MzAxMGM5ZjExOGVkZTZmZDc5OGE2NmNhYTYwYmJlMWU3ODFkZjk4NWE0NTg5MmRjYjg5YjMwMzcyYSIsInZvdXQiOlt7InZhbHVlIjowLjA5OTc4Nzg2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2U3MTdlM2I4MDgzMWZkN2ZmMjUzOTY2MTEwNTkyMGY5ZjI4ZjI5ZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdlNzE3ZTNiODA4MzFmZDdmZjI1Mzk2NjExMDU5MjBmOWYyOGYyOWQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBsOHpsM21zenAzbDRsbHk1dWt2eWc5amc4ZTcyOGpuNXNmaG55cnhxIl19fV19",
+ "eyJ0eGlkIjoiZmM4MGU5Y2FhN2UwMGU3MTA5ZTFjOTFkOTE5MDRlMTY0MWU3MjUzNzMzYWE2OGNjM2FkNjA4YWY3ZjAyNTU5ZCIsInZvdXQiOlt7InZhbHVlIjowLjEwMTI1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOGQzYjg5NGVjNjMyNTlkMDZlZDQ4YTBhNzIxZjM4NDQzOWFkYjMyOSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDhkM2I4OTRlYzYzMjU5ZDA2ZWQ0OGEwYTcyMWYzODQ0MzlhZGIzMjk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp4bmh6MndjY2U5bjVydzZqOXE1dXNsOHB6cm50ZG45eXNudmY4dm1wIl19fV19",
+ "eyJ0eGlkIjoiODZlM2JkMjZlYmUwODExZTQ4Y2M0Mjk1NjYyZDJkOGIxNTBmNzc4NmE2MDFjODhlZmRkZGM2ZmE2ZWZmYmMwMSIsInZvdXQiOlt7InZhbHVlIjowLjEwMTU4MDc5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOGJmM2NlZGM4Y2MxYTI4NDg0YTYwNWJiZTI4NWE5MzY2N2JjYzU5NCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDhiZjNjZWRjOGNjMWEyODQ4NGE2MDViYmUyODVhOTM2NjdiY2M1OTQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXo5bDhua3UzbnE2OXB5eTVjem1oYzU5NHlteDAweDlqc3NmbDZkbXp3Il19fV19",
+ "eyJ0eGlkIjoiZjI2Y2M2NzBmZGM1Yzk3MDdkYzk2ZDI2NTAwYjllZWVhZDNjOWM4NTgyNDE5NjZjYTg3NmY1MjVlYWFhNzIxZCIsInZvdXQiOlt7InZhbHVlIjowLjEwMjUwMjc1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTU2NDRmMzliMzY1OWI3MmRlZDU5OTI3MWMyMjFhNWIzZmQzOTVhNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDk1NjQ0ZjM5YjM2NTliNzJkZWQ1OTkyNzFjMjIxYTViM2ZkMzk1YTY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXoya2duZWVrZGpla3VrNzZrdmp3OHB6cmZkbmw1dTQ1YzZ2YzJ2MnE3Il19fV19",
+ "eyJ0eGlkIjoiYWNjMWNmNjZjZDRlNWQxMTM1NzJhZjZjMDRjZDFhYzUzYjlmYThmYTkzNWI4N2ZmMWE2NjI0MzU5M2NiMzliNiIsInZvdXQiOlt7InZhbHVlIjowLjEwMjg2NDY3LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTNhOTMwM2E4ZTkwN2UzOWE1ZjIxNjQ5NTk1OGNhMWMzYmQwYWRhYSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGEzYTkzMDNhOGU5MDdlMzlhNWYyMTY0OTU5NThjYTFjM2JkMGFkYWE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXozNmp2cDYzNmc4dXdkOTdndHlqazJjZWd3cmg1OWQ0Z20wNHd1Y3A4Il19fV19",
+ "eyJ0eGlkIjoiNDI3NmI4YTcyYmU3NTAwM2NiZTU4OGVhOGU4NzA2NzY2MGRlZWJkY2VlNzJmODljNThhZmRkMDg0ZGNjNzI2NiIsInZvdXQiOlt7InZhbHVlIjowLjEwNzA0ODA0LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTk1MzE1MzU4YTk5MmFkZGVkZmVhNGQ1ZmY2Yzc3YmVkMTM1Yzg3YyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDk5NTMxNTM1OGE5OTJhZGRlZGZlYTRkNWZmNmM3N2JlZDEzNWM4N2M4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp2NHg5ZjQzMnZqNGgwZGw2amR0bG12dzdsZHpkd2cwczYzbTBwbTR5Il19fV19",
+ "eyJ0eGlkIjoiNDI3NmI4YTcyYmU3NTAwM2NiZTU4OGVhOGU4NzA2NzY2MGRlZWJkY2VlNzJmODljNThhZmRkMDg0ZGNjNzI2NiIsInZvdXQiOlt7InZhbHVlIjowLjExNjU3NDc4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDZlNTk2NDdiNWM5ZTNmMzY2NmViMDE2YTVlZTExNjYyYTRjMWZiYSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQ2ZTU5NjQ3YjVjOWUzZjM2NjZlYjAxNmE1ZWUxMTY2MmE0YzFmYmE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ0d3Q5ajhraHk3OHVteGQ2Y3BkZjB3ejluejVucWxoZzRldGp6MDhmIl19fV19",
+ "eyJ0eGlkIjoiMDRkODc4MTMxNTg0NzRmZDE0NjM3NGY0MjUyMzI1YzM3MzJlNDA3Y2Y3MGVlOWQ1N2UwMDIzYTIzOTBjNWJjYiIsInZvdXQiOlt7InZhbHVlIjowLjEyODAwODQ3LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTNmZTE5OTAzZGJjOWIzNjQ1OTFkNWMzMjUwYTBhNWI1NThmMGY5OCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDUzZmUxOTkwM2RiYzliMzY0NTkxZDVjMzI1MGEwYTViNTU4ZjBmOTg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBmbHV4dnM4azdma2RqOWo4MnV4ZmcycGZkNHRyYzBucWx2c2RlYXE4Il19fV19",
+ "eyJ0eGlkIjoiNTZhN2QyODE5OTA1ODA3YmIxN2ZkZDFlNjA1OTYxNWFjNDk5YzY4ZjMyYTcyNmJmM2QzMDc2MGFhODE2NGU3NyIsInZvdXQiOlt7InZhbHVlIjowLjEzMzM2NTk0LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDIwNTdlZDBiNWM3N2FkNDU2ZTJmYTBkOTY5NzliODA2MDM1MTA0OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDAyMDU3ZWQwYjVjNzdhZDQ1NmUyZmEwZDk2OTc5YjgwNjAzNTEwNDk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFwcTJsa3NraHJoNDR6a3V0YXFtOTVobndxeHFkZ3NmeWMyZ3A2Y2M0Il19fV19",
+ "eyJ0eGlkIjoiZjQ4MzYzZmFlNjExMDI1ODQxNWYyM2I2OGM0NTFjZmEzNTdmMjQxODE1YjQ0MzRjODczNjc3YTc3NGM2MTM3NCIsInZvdXQiOlt7InZhbHVlIjowLjE1MjE0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjE3YjlmN2RiODYzYTdjYzZlMWU2ZmM4MzE5ZGQ1ODI1MmVmMDE2OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGIxN2I5ZjdkYjg2M2E3Y2M2ZTFlNmZjODMxOWRkNTgyNTJlZjAxNjk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpjaGg4bWFocDM2MG5yd3JlaHVzdnZhNmtwOTltY3BkeWV3bGc4bnIwIl19fV19",
+ "eyJ0eGlkIjoiZTgwNjUyN2EyOTI3OGVlOTMxNTFiMzA5Nzk3MTcwNjBjNTYyMWUwZjFkNTZkMWM5MjhiNzBjYjJjMzc2OTI2YyIsInZvdXQiOlt7InZhbHVlIjowLjE2MTE3LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYzYwNzNmOTg1MTY5MzFiMmRkYWFmMzk0ZjNmZTEwMGU5NzRmYjMwMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGM2MDczZjk4NTE2OTMxYjJkZGFhZjM5NGYzZmUxMDBlOTc0ZmIzMDA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJycXcwdWMyOTVucnZrYTR0ZWVmdWw3enE4ZnduYW5xcWx5eHV2OWp5Il19fV19",
+ "eyJ0eGlkIjoiOWM4M2E3N2M2YWY0MmQ3N2I0OTI2YjIxMDY5NTJjYzhmYjZlMTY3OTMyNzQyYjk4MWEyZmU1MzY0ZTYzYmUyMCIsInZvdXQiOlt7InZhbHVlIjowLjE3MDM3NTg5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYzk2MzZhNjA0ZDZhZDRjMGZlZmI0ZjBhZjZmMWYxMGZkOWYyZWI5YyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGM5NjM2YTYwNGQ2YWQ0YzBmZWZiNGYwYWY2ZjFmMTBmZDlmMmViOWM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ5a3g2bnFmNDRkZnM4N2xkOHM0YWgzN3k4YW51aHRuczB5Y2NucHdzIl19fV19",
+ "eyJ0eGlkIjoiY2E5ZmVjYTJkNTFmNjg1MWZlMWNkYjk3MGQ1YTA2YzE2ZWMzYjM4NjgxOGYxMWQxZTUxOWE5NmE0MGRkYzIzNCIsInZvdXQiOlt7InZhbHVlIjowLjE5OTAxNjM2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOGM3NTUyMDg2YmMwNWVkMTU4OTQ1OWNhZjEzMTNkZDVjYmNlNDY1YyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDhjNzU1MjA4NmJjMDVlZDE1ODk0NTljYWYxMzEzZGQ1Y2JjZTQ2NWM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp4ODI1c2dkMHE5YTUyY2ozdnU0dWYzOGgydWhuanh0c256czByMGYwIl19fV19",
+ "eyJ0eGlkIjoiNWE0ZWQwYjg3MjhhNzU0NzE1NTgzYzcyMDNkNmI5MTQyNmVmYzRmMjhiZmE5NTI0OWM4YWI1NTEyMDVhZjYxNyIsInZvdXQiOlt7InZhbHVlIjowLjIxMTcyOTk3LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDQ2NTgwMjg1OGVlOGYxODE0NGYyMWRjYWM5ZmI0ODQzZDJlNjFkMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQ0NjU4MDI4NThlZThmMTgxNDRmMjFkY2FjOWZiNDg0M2QyZTYxZDI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXIyeHRxcGd0cmhnN3hxNWZ1c2FldHlsa2p6cjZ0bnA2Z215Y2F2bnAyIl19fV19",
+ "eyJ0eGlkIjoiNTNhZTEzYjMwODJjZjUyYzExY2EwMDMyY2ZiNWE1ZGU1ZTdkNGQwM2ViZDZjNWEyM2Y5YTcxYWVhNWFlOWU0ZiIsInZvdXQiOlt7InZhbHVlIjowLjIxMzU2NTYsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAwMTQwYTdjYmE5NTNhZTdmZWFjYTJlNTQ1ODE4Y2I4MWEwZmUyMThjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MDE0MGE3Y2JhOTUzYWU3ZmVhY2EyZTU0NTgxOGNiODFhMGZlMjE4Yzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXE1cGY3dDQ5ZjZ1bGwyZWdoOWdrcWNld3E2cGwzcDNzcnEwZHZrM20iXX19XX0=",
+ "eyJ0eGlkIjoiZDg4MjRiMmFkZTMyNWU2ZDZjOTY1OWQzMTg0YzQwNDRmZjdlYzhmOThhY2NjNjhiZGNlNzZhNGIyZGIzMjQ3OSIsInZvdXQiOlt7InZhbHVlIjowLjIxNDY3NTY1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZjI5MWM1MTU1MWNlYTAwZTBiODZmYTYzMDQyZDQ0OWMzYmIxYjMxYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGYyOTFjNTE1NTFjZWEwMGUwYjg2ZmE2MzA0MmQ0NDljM2JiMWIzMWM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJlZnIzZzQyODgycXJzdHNtYXh4cHBkZ2p3cmh2ZG5yc2t5MGtncnBoIl19fV19",
+ "eyJ0eGlkIjoiZjg2NDkyY2RlNjc2YzY3YmJlYmFiYWVjMzUyMjQ4ZTU1YzYzZjg5ZWU2ZGNiN2I0MWE1NzA3MWY4NGU3ZDczNSIsInZvdXQiOlt7InZhbHVlIjowLjIzNDUwMzkxLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjU0Y2ExY2UyODYwNmRjNzUxODQ2MGVlM2Q1ZWQ4YzY3N2I5MWEwNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGI1NGNhMWNlMjg2MDZkYzc1MTg0NjBlZTNkNWVkOGM2NzdiOTFhMDU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXo2NWVnd3c5cHN4bTM2M3Mzc3d1MDI3bXJyODB3ZzZxNXJ6a2dwYTdzIl19fV19",
+ "eyJ0eGlkIjoiYmY4NjBkM2QzMTI1Y2Y2MTU3NmVhMmUyMjIyOGI0YzQ2M2FlZjFhZDRmYjBmZDFkZTE1MTVjMmVhMGM3YTkyMCIsInZvdXQiOlt7InZhbHVlIjowLjI1MDQzLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTM3MzIxMzAwNmNkNGJkM2IyYWU2ZDI5NWI3OWRmZmVkZDFkN2Y3ZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGEzNzMyMTMwMDZjZDRiZDNiMmFlNmQyOTViNzlkZmZlZGQxZDdmN2Q4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXozaHhnZnNxbXg1aDVhajRla2pqa21lbWxsZDY4dGwwNWg5Y2ZwbDg0Il19fV19",
+ "eyJ0eGlkIjoiZTY4MGQwNzM3Zjc2MzcyZTkwYWE0MGY4MjgwMjc0YTI3NmUwOTMzOTkxZDlkOWUzNjg1MTRmN2MyYzc2Yjg2YSIsInZvdXQiOlt7InZhbHVlIjowLjMwMzg3NTg0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODQwODE5NmNkMDVlYjE0NmJiNTA5NjIyNjcxNTkxZmFjMGE5OGY1ZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg0MDgxOTZjZDA1ZWIxNDZiYjUwOTYyMjY3MTU5MWZhYzBhOThmNWU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp6cXN4dHY2cDB0ejM0bTJ6dHp5ZWM0ajhhdnAydjB0YzQ5eWxocDhwIl19fV19",
+ "eyJ0eGlkIjoiOGFkZGIxNmM0OWEzYjMxNTQ4M2QxZmI1ZjA5MGZlMDgxMTM1Yjg3NzQxNDY3NDY0ODJlNzUwM2M0NWZiOGMzMyIsInZvdXQiOlt7InZhbHVlIjowLjM5ODYxLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODdmOTNkZjI5NWRiNmQ5N2FkZTVjYTRmNzgwYmRkYjBhMThjNTE3MSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg3ZjkzZGYyOTVkYjZkOTdhZGU1Y2E0Zjc4MGJkZGIwYTE4YzUxNzE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpybGowMGpqaGRrbTlhZHVoOXk3N3F0bWtjMnJyejN3eXVnODA3NzgwIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYTEyOGJhZDFmZjFkZWFlMDc0Zjc5ZDdiNjg0YzcwYTgyMWU5ODNjOGI3ODM3NjRiOTFmZTg3YjhkYWRiNmQ5YyIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY2NTAyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoxMDMuNTk1MjY0NjcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnMGM5aGxrZG11Nm5zYTNkeDdsZ200ZDVmenJ3azB1dmN5dWFjOW1lcWswNG11cncza2d5aGY1bW43eCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ4NjQ0ODEifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjo5Mi4zMjAwNjM2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDFlZTQxNDJmZjkyOGI0YWE2OTJiM2RlOGRlMjkyOWJlMzkwNWM2MDI3ZDNiYThiMGEyY2YxYzBiNTQ4N2FjYjA4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2MHdnOXAwbHk1dGYybmY5djc3M2gzZjl4bHJqcHd4cWY3bmgyOXM1dDgzY3o2NXM3a3RxYWhyOXlhZ2QiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0MzM1MDM2In19XX0=",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjo3MS4yMzA1NDE0NSwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNiBPUF9FUVVBTCIsImhleCI6ImFhMjAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmswbjA2ZDR2ejZ5ZGZwbXNuemp3cHN5Y2hsYTNtcGdlNWdweDlteHlqMmMwYWh2c3FydmRmeGN2N3lrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzM0NDc0NCJ9fV19",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjozNi4wOTczNjkxMSwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTY5NTAxMCJ9fV19",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoxNy41MTc2MjU4OCwibiI6Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZCBPUF9FUVVBTCIsImhleCI6ImFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnN0cWttNTRkdHZucHlxeHQ1bTVuN3Nqc240ZW5ybHhjNTI2eHl4bG5qa2F5Y2R6ZmV1NjlyZXl6bXF4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODIyNTY5In19XX0=",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoxMS4zNTkxNDg0NywibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYiBPUF9FUVVBTCIsImhleCI6ImFhMjBiMzQ3ZDY4YzhkMmNkYjMyMDY2YmU5YWY4MDRjNmZmYjhhYmJiNDQxYmRmMzZjYWYzODM0MzZiY2ZjYWEzNWNiODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd2U1MDQ1djM1a2RrdnN4ZDA1NmxxenZkbGFjNHdhNWd4N2x4bTkwOHE2cmQwOHU0ZzZ1a24zNzJnZ3dsIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTMzNDcxIn19XX0=",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoxMC41NTQxODUyLCJuIjo1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDdjMmJjY2EyMDg5YTlmMGY4ZGFlYzBkM2I0NWE2YzUyNTgxYzU2NzY2NWNjNTFlYmRhOWEyMDJkOGEzM2RjZmM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkN3pobjl6cHpkZjdydWQ0bXFkOGR6NmQzZjlzOHprd2VqdWM1MHRtMmR6cXR2MngwdzBjNTR5MzlocDYiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0OTU2NjcifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjo1LjIyMTg3ODY2LCJuIjo3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBmMWMzM2JjN2RhMDk5MWQyNmY4YmJmMTUwYzkwMzU2OGIyYzNkYzg3MjczNzA4NjNmNGMzZDlmOTY1NWIzNzkxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwY3V4dzc4bWd5ZXI1bjAzd2wzMnJ5c3g0NXQ5czd1c3Vubnd6cnI3bnBhbjd0OXR2bWV6cmF1M2p1cjkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyNDUyNDEifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjo0LjQwNjc5ODIyLCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YW15bmxlcHYzcG5nbnE2cmhxNTk1eDBsZm5nNncwd3A4NWpxM3JydjJ3amNsdmh0M3VneDRrYWo0aHUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMDY5NjEifX1dfQ==",
+ "eyJ0eGlkIjoiY2VkNjM4MDQ0Y2EyN2Y0MzJkY2ZhZjEzM2UwOWY5YzgyODc5ZmJjNjM2MzIwYjkwNmI3MDY1YjY2YWY5ZThmYiIsInZvdXQiOlt7InZhbHVlIjo4ZS0wNiwibiI6MTAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0NzMwNCJ9fV19",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjozNi43MjQ0OTUxNiwibiI6MzAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNzljNTM3YjQ4MDk5ZWRlM2U0Yzg2ZjRlODUwMGZjODMwYTg3ZjM3ODI5MzFiMzQwNGY4ZmJiYTA0ZjhhMGU0MSIsInZvdXQiOlt7InZhbHVlIjoxNy4wNjc5NDI5NiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhNmVlMDRjYjc3Mjg3NTlhOGRlZTZiZWQzYTc4MTMxN2EzY2UzZDJlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6bnd1cHh0d3U1OHR4NWRhZTQ3NnduY3p2dDY4bjNhOWNtNG1yOXB0bSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNTZlN2JhMDk0Y2Q2YjY2YWFlYmJlMzVkOTIwYjg5YjY5MTI1Mzk3MDk1ZjM2MTU0MzJmMjUzN2EzN2EzZTVkZCIsInZvdXQiOlt7InZhbHVlIjowLjAwNTYwOTU2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODhjYmZiZDQxYzZkNmI0MGFjOTk3NWRmNjU5Mzc2NDUyNTkxNTFjZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg4Y2JmYmQ0MWM2ZDZiNDBhYzk5NzVkZjY1OTM3NjQ1MjU5MTUxY2Q4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp5dmg3NzVyM2tra3M5dm45NmE3ZXZud2V6anR5MjNlNXFqZTZ2cWE0Il19fV19",
+ "eyJ0eGlkIjoiNjA0MGMwNzQxZTVlMzgzNjQ0ZWJlZmRjMWM1NzA1YTkzOTJlMGE2NmY2MzFkZjQ3ZTBjN2YyNTgwZjU5Mjk5OSIsInZvdXQiOlt7InZhbHVlIjowLjAwNDAzNzc0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMWY1MTczNDJiMzIyOGE3YTFjOTBmNGQwZWQ4NmVjNjUwODU5MTE2MyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDFmNTE3MzQyYjMyMjhhN2ExYzkwZjRkMGVkODZlYzY1MDg1OTExNjM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXEwNHp1NnprdjNnNTdzdWpyNmRwbXZ4YTNqc3NrZzN2dmc1Mm4yN3NrIl19fV19",
+ "eyJ0eGlkIjoiOWQ3MzYxYTlmMTdhZGVmNGVhNmEwNzhlNGJlZGQ0ZWUzZDRkNzA4MzdiMWNmMWZiMDRlMzczYTJjNDExNjk3ZCIsInZvdXQiOlt7InZhbHVlIjowLjAwNDAzNjI2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMWY1MTczNDJiMzIyOGE3YTFjOTBmNGQwZWQ4NmVjNjUwODU5MTE2MyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDFmNTE3MzQyYjMyMjhhN2ExYzkwZjRkMGVkODZlYzY1MDg1OTExNjM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXEwNHp1NnprdjNnNTdzdWpyNmRwbXZ4YTNqc3NrZzN2dmc1Mm4yN3NrIl19fV19",
+ "eyJ0eGlkIjoiZDQ1ZWZlNWZjODEwNjdmMGZjNzZiYWFjYzMwMjY5MWViNTRhOTI2MzcyMDljMmU0MzhmZjIzODk3NzMwZTVhYSIsInZvdXQiOlt7InZhbHVlIjowLjAwNDAzNzc0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMWY1MTczNDJiMzIyOGE3YTFjOTBmNGQwZWQ4NmVjNjUwODU5MTE2MyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDFmNTE3MzQyYjMyMjhhN2ExYzkwZjRkMGVkODZlYzY1MDg1OTExNjM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXEwNHp1NnprdjNnNTdzdWpyNmRwbXZ4YTNqc3NrZzN2dmc1Mm4yN3NrIl19fV19",
+ "eyJ0eGlkIjoiZWUyNDMzMzEyZDczMjE2NmVhMjk4YjZjNDMyY2MwYzk5MDcxMmJmZDBkZGEyOTFkMGZiYzdjYTI3NDY4ZWUzMCIsInZvdXQiOlt7InZhbHVlIjowLjAyNjE4MzcyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNjM4NzQ3ZjI0YjA1YmRjNWMxMTQ2ZDI2NjE5YjNjZmY0MWQ5NWQ4YSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDYzODc0N2YyNGIwNWJkYzVjMTE0NmQyNjYxOWIzY2ZmNDFkOTVkOGE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXAzY3czbGpmdnptbTN3cHoza2p2Y3ZtOG5sNXJrMmEzZ2N0d3A2N3hjIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYTBmYWVhN2EwNTIyNWNmNDMwOTgzNjY3OTQxZWVhZDU4ZTkxYTkzNzgxYTRmZTM4NzUzZGU2MTAzZWEyNmU5YyIsInZvdXQiOlt7InZhbHVlIjowLjAzMzY3NzcyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNmY3NTJkZWRmOTlmN2JhNDc4ODNhMjUzMGM2ZDI5MWRiODM4ODc0YSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDZmNzUyZGVkZjk5ZjdiYTQ3ODgzYTI1MzBjNmQyOTFkYjgzODg3NGE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBoaDJ0MGRseDBoaGZyY3N3Mzl4cnJkOXl3bXN3eThmZ2w1enRsNHpmIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiOTkxMmU4NjM3NDA0NmNkNDI1OTE0Mzc1NjhmZWM0ZTA3NThiOWUyMWZiZWI3ZDlmOWU0OGI4ZDlhYTJiMDkwNCIsInZvdXQiOlt7InZhbHVlIjoyLjMwOTg5MjQ1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpnMHkyem5qNXZleDkycTdyc2F1ZndtY3F5bmZ4eDg1NWQ4NjZjeTZ0Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMDQ3ZTdlMDM3ZWM4NzQ5YWJlOTc3ZjU5NGRiYzVjMTk5Nzg2NzRiMDRjMjUwMzBkODYwNzZiOTk3ZWI2MGM4NCIsInZvdXQiOlt7InZhbHVlIjo3MS4yMTQ3MjU5OCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDI5NjJmZDdiOTY1ZmQ3M2Q0YTRkYTI0YTNhZDM2MDlmOWU4YTBmZDEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQyOTYyZmQ3Yjk2NWZkNzNkNGE0ZGEyNGEzYWQzNjA5ZjllOGEwZmQxODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxNWs5bHRtamUwYXcwMjJmazN5NXdrbnZ6MGVhenMwNnlqcjVkcTZkYSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiODdlNmY5ZWY3ZWUyMmM3NTdlNThlZjgzNGFhYmM0NTgwNjljNzkzYzllOTBhNGU5ZTY0MTRmZjcyODliMDdkMCIsInZvdXQiOlt7InZhbHVlIjo0OS45NTE2NjM3MiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDhkYjEzNTI5OWE0M2M1MzY1MDcyNmRlNjRlOTY2MDUzNTk5MzFjYWYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4ZGIxMzUyOTlhNDNjNTM2NTA3MjZkZTY0ZTk2NjA1MzU5OTMxY2FmODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6eG16ZGZmbmZwdTJkanN3Zms3dm41a3ZwZjRueWN1NHV0bDlkazBrdiJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZjY1M2Q3Yjc3M2E2ZmM1Yzk1YjhmZTU5YmZjNTFjMzNjYzdlZDFiMzY1NDkwOWRhNWMzMGQ2MmQzNzQ4MzRkZSIsInZvdXQiOlt7InZhbHVlIjoxLjg4MTkwMDE1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpnMHkyem5qNXZleDkycTdyc2F1ZndtY3F5bmZ4eDg1NWQ4NjZjeTZ0Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiOGE0MzZhMTVhYzhkNDM4MTY4OTUzMzk2ZDJlNmRkYjkyMjE0NmFjZjQ5NTUwZDU3NjkzNTc1Y2JiNDQ0MjY4ZSIsInZvdXQiOlt7InZhbHVlIjozLjIwODQ1ODE2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgN2I1ODMwODRlNTE1ODUzM2IxN2IzODFjNGY2MmY5YjQxNDIzYmQ5OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDdiNTgzMDg0ZTUxNTg1MzNiMTdiMzgxYzRmNjJmOWI0MTQyM2JkOTk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBhNHN2eXl1NTJjMnZhMzB2dXBjbm16bHg2cGdnYWFueWt0ZnBwejUyIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMDMxN2Y0ZDM5N2VhODJmOWMyNGM5MGY3YTVhOTk4NDYwYjc2MzA0NzE5NWUyOTI5ZTQ2ZDZiMjRiNjA5ODdjNSIsInZvdXQiOlt7InZhbHVlIjoxLjc3OTc3MjE3LCJuIjoxMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGM1YWIyNTgyZmMyZDMzMDFjNmE1NTEwMjlkNDA4NGU5ZTBjMTViMzQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRjNWFiMjU4MmZjMmQzMzAxYzZhNTUxMDI5ZDQwODRlOWUwYzE1YjM0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyejZrZnZ6bHNrbnhxd3g1NGdzOTgycXNuNTdwczJteHMyMmtwZmY3YSJdfX1dfQ==",
+ "eyJ0eGlkIjoiMDMyMTc0Nzc4NjdkMDI0NTUyMTQ3NzBiOGI4YzE3NTkyZGFhNmZkZmViZmY1MzRiMzdiYWU1NjI5ZGE5YWIzNSIsInZvdXQiOlt7InZhbHVlIjo1Ljc3OTM3MjMyLCJuIjoyMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDgyZWM4NTE0YTgzNDJmYWRjMzQxMjc4ZTVlMmY0Y2NhM2U1Yzc1OTUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4MmVjODUxNGE4MzQyZmFkYzM0MTI3OGU1ZTJmNGNjYTNlNWM3NTk1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6cHdlcGc1NHE2emx0d3JneW5jdWgzMGZuOXJ1aHI0ajUyamZlazB1dCJdfX1dfQ==",
+ "eyJ0eGlkIjoiMDg3ZTM0OWFjNzI1MjQwZmJhYWJjY2E0MWFmOWVjZWE5MDRhNmQ4YmU1ZmIwZjU3NmYzMjhiZDllMmYzNDFkMyIsInZvdXQiOlt7InZhbHVlIjowLjAxOTIyMDE5LCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDdjNzU2MDMwNTU3ODc5MTRkYjYyMmFlMDAzZTgzNmRkMjBiNTEwNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDA3Yzc1NjAzMDU1Nzg3OTE0ZGI2MjJhZTAwM2U4MzZkZDIwYjUxMDY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFydXc0c3JxNHRjMHkyZGtjMzJ1cXA3c2RrYXl6NjNxYzVxMHl1M25lIl19fV19",
+ "eyJ0eGlkIjoiMTBmMjFjZGEyYmE4NmU1NGZjNDdhYjNmYWY3ZDU4NzAzYjI3MWQzNTQyNTFlYTk1NWFhNTU5Nzk2OWVkNzliOSIsInZvdXQiOlt7InZhbHVlIjowLjAwODQxMTE5LCJuIjoxOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDFhODUwZjJjZjI2NWI0ZTg1ZWU2OTNiMTg4OWMzMzk2M2NhZjgzMGUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxYTg1MGYyY2YyNjViNGU4NWVlNjkzYjE4ODljMzM5NjNjYWY4MzBlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZGcycmV2N2ZqbWY2ejd1NmZtcnp5dXh3dHJldHVycGM4OTVrbWttaCJdfX1dfQ==",
+ "eyJ0eGlkIjoiMmQ4MDg0NzBhZGU0NzgzZDY4ZjYxNDljODUyYTVhZWIwNmU0YzJjYTAwYmM5NTRlZTkwOGEzYTY1YzIyODUzNSIsInZvdXQiOlt7InZhbHVlIjowLjQxNDIwNjU1LCJuIjo0NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGRlOGFmN2IwNWRlNDBhN2IxZjhiZDE5NjkzNzgxMmRjNGM3OTMyMzcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkZThhZjdiMDVkZTQwYTdiMWY4YmQxOTY5Mzc4MTJkYzRjNzkzMjM3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyMGc0YWFzdGhqcTU3Y2wzMGdlZHltY3p0d3ljN2ZqeHU5cDZoNmV3ZCJdfX1dfQ==",
+ "eyJ0eGlkIjoiMzE1MGVjZDI0MWY5NzQ4ZmZjZGNlYjJhYjk3ZjI4NzQyOGZmZjc0NTAwM2E2YjI2ZGRkNThjMzk2NDliMjE2MCIsInZvdXQiOlt7InZhbHVlIjowLjA4NDU3NzIsIm4iOjMyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTMzOGJkODE3N2ExNmI2NmIzYWFmODMwMjVhMDdiMzczZGE5MDhkOSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGEzMzhiZDgxNzdhMTZiNjZiM2FhZjgzMDI1YTA3YjM3M2RhOTA4ZDk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXozbjMwdnB3N3Nra2U0bjR0dXJxZmRxMHZtbm0yZ2dteXBuZWEya2tuIl19fV19",
+ "eyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOlt7InZhbHVlIjowLjAxNzQ4MzM0LCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjA0OWQxM2MxOTQ2NmMxODYyODBlMDRhNjc4MjMxN2RhM2Q0ZTE2OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDIwNDlkMTNjMTk0NjZjMTg2MjgwZTA0YTY3ODIzMTdkYTNkNGUxNjk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFzeW41ZnVyOXJ4Y3hyenNyc3k1ZXV6eDk3Njg0OHBkeWxjZDkzdmVyIl19fV19",
+ "eyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOlt7InZhbHVlIjowLjA4NDY0NzAxLCJuIjoxMiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDYxYzMyMjIwNzU0ZmY1ODc3NzY0YzIxNDI1OGIxNDg1NzcyM2EzMTkgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2MWMzMjIyMDc1NGZmNTg3Nzc2NGMyMTQyNThiMTQ4NTc3MjNhMzE5ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwc3V4ZzNxdzQ4bHRwbWh2bnBwZ2Z2dHpqemh3Z2FycnllN2w3dXpsOCJdfX1dfQ==",
+ "eyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOlt7InZhbHVlIjowLjEwODk4OTA3LCJuIjoxNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDAzNmYzOWVhNWZlMjgwZWMzYTMwZDYxMjU5YjBjZTYzMjI3Y2M4YjUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwMzZmMzllYTVmZTI4MGVjM2EzMGQ2MTI1OWIwY2U2MzIyN2NjOGI1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxcGs3dzAydGwzZ3BtcDZ4cnRweWtkc2VlM2p5bHhnazV2MmNjbDVsdiJdfX1dfQ==",
+ "eyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOlt7InZhbHVlIjowLjQxMTMzNDg1LCJuIjozOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGI3MWY5Mzk3ZWE1Nzg0ZTdkYjBkYzg4MDE5MTY2MzMxNWI1MmY4NGYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRiNzFmOTM5N2VhNTc4NGU3ZGIwZGM4ODAxOTE2NjMzMTViNTJmODRmODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6bTNseXVoYWZ0Y2ZlN21waHlncXhna3Z2YzRrNWhjZnU4cW04enR3diJdfX1dfQ==",
+ "eyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOlt7InZhbHVlIjowLjAxMTU1MTQ5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOWE2MmM1NTNhYjk1MDQ4MzMwYWI1N2RlZDExNGU4ZGRlNjIyMmM1MCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDlhNjJjNTUzYWI5NTA0ODMzMGFiNTdkZWQxMTRlOGRkZTYyMjJjNTA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpkeDkzMm40dzJzZnFlczRkdGFhNWc1YXJ3N3ZnM3YycXJzY3lod3E1Il19fV19",
+ "eyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOlt7InZhbHVlIjowLjIwMjIzNzk4LCJuIjozMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGM5NWQyYjJhYTJiZTc2ZjZjMGI3YWQyNjdlMDlhYTVjMTEzYzAxNGIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRjOTVkMmIyYWEyYmU3NmY2YzBiN2FkMjY3ZTA5YWE1YzExM2MwMTRiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyeTQ2MmUyNTJsOGRha3FrN2tqdmxzZjRmd3B6MHFwZnZ1OTA1NWFkdCJdfX1dfQ==",
+ "eyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOlt7InZhbHVlIjowLjIxMDE5NzM5LCJuIjozNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDU1YzI3Y2YzNWQwMjI2OTE1MDQ2NzI1ZDZjNjRiMDM2YTM0ZmQ2ZTYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1NWMyN2NmMzVkMDIyNjkxNTA0NjcyNWQ2YzY0YjAzNmEzNGZkNmU2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwMnV5bDhudDVwemR5MnNnZWU5Nm1yeWtxbTJ4bjdrdWNrbGM2ZmZmMCJdfX1dfQ==",
+ "eyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOlt7InZhbHVlIjowLjU1NTYzMjU1LCJuIjo0OSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGI4OGQ5M2E2M2ZlMTdiZWQ0OTgxNTI5MmJkMmE0MGRiMGU1OTdiYjMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRiODhkOTNhNjNmZTE3YmVkNDk4MTUyOTJiZDJhNDBkYjBlNTk3YmIzODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6dWdteWF4OGxzaGhtMmZzOWZmOTBmMmdyZHN1a3Rta3Z0ZXY1amEzNyJdfX1dfQ==",
+ "eyJ0eGlkIjoiNDVmYjUyMDkxODFiYTM0ZWIzYmEzMDU3YWJmMDQ5MzljMzk1NTk5N2Y2OGJjN2U0OTYyZGUzMzk3OWRkMzg4OCIsInZvdXQiOlt7InZhbHVlIjowLjExOTAzOTA3LCJuIjoyOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDFmNzhmZDI1Y2JmYWJlNTg3MjI5YTFlOTJhYzg3MTI2NGQyMDEwN2EgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxZjc4ZmQyNWNiZmFiZTU4NzIyOWExZTkyYWM4NzEyNjRkMjAxMDdhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxMGgzbGY5ZTBhdHVrcmo5eHM3ajJrZ3d5bnk2Z3FzMGdud3NxeG0yNSJdfX1dfQ==",
+ "eyJ0eGlkIjoiNDVmYjUyMDkxODFiYTM0ZWIzYmEzMDU3YWJmMDQ5MzljMzk1NTk5N2Y2OGJjN2U0OTYyZGUzMzk3OWRkMzg4OCIsInZvdXQiOlt7InZhbHVlIjowLjE4MDY1Mjg0LCJuIjozOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDY2ZWJjZWNmMjRiZGFmZDIyMzVhMzNlN2M3MTE1MWYzMWFlYzczY2QgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2NmViY2VjZjI0YmRhZmQyMjM1YTMzZTdjNzExNTFmMzFhZWM3M2NkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwbndobmsweWo3Nmw1M3J0Z2U3MDNjMzI4ZTM0bXJuZTU3NjZkd3lqZSJdfX1dfQ==",
+ "eyJ0eGlkIjoiNDVmYjUyMDkxODFiYTM0ZWIzYmEzMDU3YWJmMDQ5MzljMzk1NTk5N2Y2OGJjN2U0OTYyZGUzMzk3OWRkMzg4OCIsInZvdXQiOlt7InZhbHVlIjowLjMyOTY3OTcyLCJuIjo0Nywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDU4YmJmZmQ5YjQ5YTZjMGU5MzE0ZThkMTllZTA1NDNiNmM3MDBkMGUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1OGJiZmZkOWI0OWE2YzBlOTMxNGU4ZDE5ZWUwNTQzYjZjNzAwZDBlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwdnRobDdla2pkeGNyNW56bjVkcjhocTJzYWtjdXFkcGMzZnZoajNwZSJdfX1dfQ==",
+ "eyJ0eGlkIjoiNDVmYjUyMDkxODFiYTM0ZWIzYmEzMDU3YWJmMDQ5MzljMzk1NTk5N2Y2OGJjN2U0OTYyZGUzMzk3OWRkMzg4OCIsInZvdXQiOlt7InZhbHVlIjowLjQ5NjE5NzUxLCJuIjo1Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDZhOTk5ZmQ2MzdkY2RkNzNhNGU2NTI1YTA4Zjg5MWYyYjIzNDllNjcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2YTk5OWZkNjM3ZGNkZDczYTRlNjUyNWEwOGY4OTFmMmIyMzQ5ZTY3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwNGZuODdreGx3ZDZ1YXl1ZWY5NXo4Y2o4ZXR5ZHk3dnVsazU1MDdlNCJdfX1dfQ==",
+ "eyJ0eGlkIjoiNThiZGNiNjNmMzBkZjZhNzRiNTgxMzI0YjBmNGRiZGQ5ZTg0MzM3YjYxZmMxZWJmMTliZjhiODhmODA3Zjc4MSIsInZvdXQiOlt7InZhbHVlIjowLjA0MDEwMjY3LCJuIjoxOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDdlMjJiMmJiYmNlYzEwNzgzY2IzYzY2ZmE2ZGQ0NzlmY2U3YmI1MzUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3ZTIyYjJiYmJjZWMxMDc4M2NiM2M2NmZhNmRkNDc5ZmNlN2JiNTM1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwbHo5djRtaG5rcHE3cHVrMHJ4bGZrYWc3MHV1N2E0eDVoOGE3Y3c0YSJdfX1dfQ==",
+ "eyJ0eGlkIjoiNWViMTUzZWViYzVlZjViYmJiOWNhOGY2YzVkNDFhN2FiOTNiM2RhMThhMGJlYWMxYzExNTVjYzhkY2E5NmYwMiIsInZvdXQiOlt7InZhbHVlIjowLjAxNTEyODY2LCJuIjo2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYzU2MDVhYWRhNTMxYjAzNWQwOGM0MWE2ODVlNDljODljODI0MjM4ZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGM1NjA1YWFkYTUzMWIwMzVkMDhjNDFhNjg1ZTQ5Yzg5YzgyNDIzOGU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ6a3FrNGQ1NWNtcWR3czMzcTZkcDB5bmp5dXNmcHIzY3luZ2tzODVuIl19fV19",
+ "eyJ0eGlkIjoiNWViMTUzZWViYzVlZjViYmJiOWNhOGY2YzVkNDFhN2FiOTNiM2RhMThhMGJlYWMxYzExNTVjYzhkY2E5NmYwMiIsInZvdXQiOlt7InZhbHVlIjowLjA0OTM0MDA5LCJuIjoxNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDQ2NmM5ZTg3YTA5OGMwMmI2ZGNmYmE1ZGNkYjBhNDdjNDkzYTE0OTcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0NjZjOWU4N2EwOThjMDJiNmRjZmJhNWRjZGIwYTQ3YzQ5M2ExNDk3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwcnhlODU4NXp2dnEybWRlN2E5bW5kczUzN3lqd3M1anUycngzYTBqaCJdfX1dfQ==",
+ "eyJ0eGlkIjoiNmY4NGM1ZGY2MTRjOTcxMGY0NTMzMWFhODM0ODFjYzQ1MzYzZjAxNTJiZDAzMzE2N2E4Y2VlMDNmM2I2YjIyYiIsInZvdXQiOlt7InZhbHVlIjowLjAwMzk1NTIsIm4iOjgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA1YTJmMDFhZDM0ODBmOTAwNWUwYzI5MmYzZWViZDZlZGVhZjZiYTZiIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NWEyZjAxYWQzNDgwZjkwMDVlMGMyOTJmM2VlYmQ2ZWRlYWY2YmE2Yjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGR6N3FkZHhqcTBqcXo3cHM1ajcwaHQ2bWs3NGE0NmR2Z2xzNzY4MngiXX19XX0=",
+ "eyJ0eGlkIjoiNmY4NGM1ZGY2MTRjOTcxMGY0NTMzMWFhODM0ODFjYzQ1MzYzZjAxNTJiZDAzMzE2N2E4Y2VlMDNmM2I2YjIyYiIsInZvdXQiOlt7InZhbHVlIjowLjAxNjc2Njk1LCJuIjoyMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGEwYjkzZTFiMmE1MDY5ZDllMTVlY2U0YjU5OGEwMjc0YzVhNThkOTYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhMGI5M2UxYjJhNTA2OWQ5ZTE1ZWNlNGI1OThhMDI3NGM1YTU4ZDk2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6c3RqMHNtOWZneG5rMHB0bTh5a2t2MnFmNnZ0ZnZkamM2bnRoODIzYyJdfX1dfQ==",
+ "eyJ0eGlkIjoiODFkMjdiNDZiYTMwMTUzYTEyZWM3OGUwNjU3Mjg5ZjE4Y2Y4MmVmYTQ0MDllNjQ0ZWQ0N2NjNDM5YTdlM2E4YiIsInZvdXQiOlt7InZhbHVlIjo0Ljg2Mjk4MzQ4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDM3N2Q5NWM5ODI0NjRiNzk4MDdhOWFlNzg2OGQwMDU0YjM2ZDQzNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQzNzdkOTVjOTgyNDY0Yjc5ODA3YTlhZTc4NjhkMDA1NGIzNmQ0MzQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJmaDBrMnVucWp4ZmR1Y3E3NTZ1N3JnNnF6NWtkazV4c3J4Nno0YzZyIl19fV19",
+ "eyJ0eGlkIjoiODdlOWNjNDAzNTJhMTgyMWEzNDdiZTljMjNlMmIzNzJlMzQzZTE5MmVkZmM1OTc2NTI2NWIxZjc5ZTFiNzVhNyIsInZvdXQiOlt7InZhbHVlIjowLjA0ODQyMDUxLCJuIjoxMiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDIyNWJjOTVjMGQzMjVmMGZlNzY0YzJkNjlmMzU5NDY2ZWU2Y2RlNzEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQyMjViYzk1YzBkMzI1ZjBmZTc2NGMyZDY5ZjM1OTQ2NmVlNmNkZTcxODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxMzloajJ1cDVlOTdybDh2bnBkZDhlNGozbnd1bXg3d3l1bnc3YzZ0NiJdfX1dfQ==",
+ "eyJ0eGlkIjoiOTAwYjY2M2RkZDA3NjJiMzRmZDgxNjhkM2FjZWNlMGU4OGUzODIxYmU3M2FjOWRhMDhkN2E0NDRlOGI3MzA5ZiIsInZvdXQiOlt7InZhbHVlIjowLjAyMjUzNjAxLCJuIjoyMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDM2N2U5OGRkYmM4MzM2NTVhNzQyMWRiMjEyOTVhYmQ1OWRkZjVhZTIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQzNjdlOThkZGJjODMzNjU1YTc0MjFkYjIxMjk1YWJkNTlkZGY1YWUyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxbThheHhhaGpwbnY0ZDhnZ3dteXk1NDQwMmVtaDY2dWc5cHdqcnpuNCJdfX1dfQ==",
+ "eyJ0eGlkIjoiOWZmODFlNGFjZDIxNzU3ZGIxMzJiNzU5ZjRlOWJlZTBlYzhhOTk2YjgyZWY3YTY4NGFmMTE1MTI1ZjEyMmZjOCIsInZvdXQiOlt7InZhbHVlIjowLjUxNzgyOTQzLCJuIjoxMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDU0ZTJmZjMzZTc2NmMzNTQ5ZjVmMWE2MmZlOTEyM2NjYjIyZTc1NzAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1NGUyZmYzM2U3NjZjMzU0OWY1ZjFhNjJmZTkxMjNjY2IyMmU3NTcwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwMnc5bGVudWFudng0eWx0dWR4OWw1M3kweHR5dG40d3FzOHM1Y2EydiJdfX1dfQ==",
+ "eyJ0eGlkIjoiOWZmODFlNGFjZDIxNzU3ZGIxMzJiNzU5ZjRlOWJlZTBlYzhhOTk2YjgyZWY3YTY4NGFmMTE1MTI1ZjEyMmZjOCIsInZvdXQiOlt7InZhbHVlIjowLjYzOTYxMjcxLCJuIjoxMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDc3MDY2OGEzMjAyMmM4NzUwOWI5NWJlMjViYzVjZWRmOTAxN2Q1OWYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3NzA2NjhhMzIwMjJjODc1MDliOTViZTI1YmM1Y2VkZjkwMTdkNTlmODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwbXN2NjlyeXEzdnNhZ2ZoOWQ3eWs3OWVtMGVxOTc0bnVhYWVldXdmZCJdfX1dfQ==",
+ "eyJ0eGlkIjoiOWZmODFlNGFjZDIxNzU3ZGIxMzJiNzU5ZjRlOWJlZTBlYzhhOTk2YjgyZWY3YTY4NGFmMTE1MTI1ZjEyMmZjOCIsInZvdXQiOlt7InZhbHVlIjoyLjEwOTU3ODE1LCJuIjoyNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDVjZTM0OTNkYjRlYmE3ZDQ2NGJkODNmMjViODk1OWNiZWJhYmZmMDYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1Y2UzNDkzZGI0ZWJhN2Q0NjRiZDgzZjI1Yjg5NTljYmViYWJmZjA2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwd3d4amZha240NjA0cnloa3BseWt1ZnQ4OTdoMmxscWMzMDc1d3c4MiJdfX1dfQ==",
+ "eyJ0eGlkIjoiYWRkZDgwNDAxNjNhMDBkZWIyMjk2NmIzZDI5OWExOGJhMTRmYTQ3MmE4YmE5ZWVkMDkyYzQyMDUwNjIzYzY4ZSIsInZvdXQiOlt7InZhbHVlIjoyLjk1NDQ2NTI2LCJuIjoyMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGE3YmJjOTk1NTAwZmYyMjU1NmY0MTg2MzVmZTQ3ZWI3MWYzMWRhODQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhN2JiYzk5NTUwMGZmMjI1NTZmNDE4NjM1ZmU0N2ViNzFmMzFkYTg0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6bm1oanY0MnE4bHlmMms3c3Z4eGhseTA2bTM3dnc2c3Nza3oya3ZwbiJdfX1dfQ==",
+ "eyJ0eGlkIjoiYjdjOTY5MDNjZWYwOGNlZGE4Zjc4MjI1MDllMjQzYjliMWRiOGRmNDUwNTUzNGY2ZDI2OGVmMGMwOGM4MzI3NSIsInZvdXQiOlt7InZhbHVlIjowLjAwNDMyMTY1LCJuIjo3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDZhMGZjOTkzZjllODQ2NTgyM2IxYWIyZDE5OTE0Yjk3NDFiYTJhOSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDA2YTBmYzk5M2Y5ZTg0NjU4MjNiMWFiMmQxOTkxNGI5NzQxYmEyYTk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFyMnBseWU4NzBnZ2V2ejh2ZHQ5NXZlemp1aGd4YXo0eXp6ZzZrcGZ1Il19fV19",
+ "eyJ0eGlkIjoiYjhlMDc2ODNmZjFlNmE0NTRhY2JmNzAwOGU4OGExMjZlZmNmMjE0ZmZhZDAzOWI5ZjA4MmY5OGM3NmExNjdlYSIsInZvdXQiOlt7InZhbHVlIjowLjEzNDkwNTk0LCJuIjoyNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGFkNDE0ODkwYzdkYTM1ZDBjYTg1NDQ5ZTRmNDE3OTQ5MzMxOTU0NzMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhZDQxNDg5MGM3ZGEzNWQwY2E4NTQ0OWU0ZjQxNzk0OTMzMTk1NDczODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6azV6anlzY2xkcnQ1eDJzNHpmdW42cDA5eW54eDI1d3Z0eHhqcWc5cyJdfX1dfQ==",
+ "eyJ0eGlkIjoiYmM2MDZhNTUwNDMwNjc2N2FhNDkxZWY3NTg5N2NjYjc0YTM4MTM0M2Y2ZjU0YTA0MDk1ZjU3ZDk4YjA0Mzc2OSIsInZvdXQiOlt7InZhbHVlIjowLjM2NTkxNjUyLCJuIjozMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDA3MDlkNzQyNmNmZGE5NzE3ZmMxNjg4YTgxMzc2M2ZmZjBjOWM4ODkgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwNzA5ZDc0MjZjZmRhOTcxN2ZjMTY4OGE4MTM3NjNmZmYwYzljODg5ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxcnNuNDZ6ZG43Nmp1dGxjOTVnNHFmaHYwbGxwandnM3lwa3FrMnlqMiJdfX1dfQ==",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjAwMTM3MzA4LCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjAzNThkZmY0ZTY3MmZlNDVhMmEyY2JhOWY2YzY5NDcyMWQ0YjkzNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDIwMzU4ZGZmNGU2NzJmZTQ1YTJhMmNiYTlmNmM2OTQ3MjFkNGI5Mzc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFzcnRyMGxmZW5qbGV6Njlna3Q0OG12ZDlyanI0OWV4dTAwdGpzajJqIl19fV19",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjAwODIzMTIxLCJuIjoxNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDJmMDFkZmY2ZTg3ZGQ2YmIwNTEwZjcyNTU2NDFiNWVhYTRiODQyNDggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQyZjAxZGZmNmU4N2RkNmJiMDUxMGY3MjU1NjQxYjVlYWE0Yjg0MjQ4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxaHNyaGxrYXA3YWR3Yzl6cm1qMjRqcGtoNDJmd3p6ZnFoYXU5YzJ5NCJdfX1dfQ==",
+ "eyJ0eGlkIjoiYzYzNTQ5ZTNmYTY2NTYwMWEyOThjZjQ2YmNlMjUzODhmNWRjMWRjYjBiOTFjMjJlOGZkNjVhNGZkYTQ5MGYyMiIsInZvdXQiOlt7InZhbHVlIjowLjA4ODExOTk2LCJuIjoyNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGI2YmY1NTdiY2JiMWIwMjBmMjcyZDNjZGI3YWQ4ZjJjNGQ3NTY3N2UgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRiNmJmNTU3YmNiYjFiMDIwZjI3MmQzY2RiN2FkOGYyYzRkNzU2NzdlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6bXQ3NHRtZXdjbXFnOGp3dGZ1bWRhZDN1a3k2YXQ4MGNzZzRsbmVuOCJdfX1dfQ==",
+ "eyJ0eGlkIjoiYzYzNTQ5ZTNmYTY2NTYwMWEyOThjZjQ2YmNlMjUzODhmNWRjMWRjYjBiOTFjMjJlOGZkNjVhNGZkYTQ5MGYyMiIsInZvdXQiOlt7InZhbHVlIjowLjEzNzE3NzQ5LCJuIjozMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGYzOWZmNmNmYzdjYzBlMGRkYzkwODBjOTg2NWJjOWEwYThlMTFlY2IgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmMzlmZjZjZmM3Y2MwZTBkZGM5MDgwYzk4NjViYzlhMGE4ZTExZWNiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyZWVsYWswY2x4cXVyd3VqenF2bnBqbWV4czIzY2c3ZXY1dDk3cG1tNiJdfX1dfQ==",
+ "eyJ0eGlkIjoiYzYzNTQ5ZTNmYTY2NTYwMWEyOThjZjQ2YmNlMjUzODhmNWRjMWRjYjBiOTFjMjJlOGZkNjVhNGZkYTQ5MGYyMiIsInZvdXQiOlt7InZhbHVlIjowLjI5MTE4OTY0LCJuIjo1Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDIxN2ZlMzliZDMwZTMyYWQ2YzZmMDEzOWUxNzY4ZDI5ZWY1NmMyYzYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQyMTdmZTM5YmQzMGUzMmFkNmM2ZjAxMzllMTc2OGQyOWVmNTZjMmM2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxc2hsY3VtNnY4cjl0dHZkdXFubmN0azM1NTc3NGt6Y2NmcDJ2ZTM4YSJdfX1dfQ==",
+ "eyJ0eGlkIjoiY2RlZjRhMTJkYTM1NGEyYjY4Yjk2OGQzOWNjMzdiMzc1ZGViNGE4ZTRlMGQzMThjNjE2NzhiY2JhMGMzYTM4YSIsInZvdXQiOlt7InZhbHVlIjowLjA4ODg1MjA3LCJuIjo1NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGRmMjVlNDRhZjZlOTkxZWU3YTJlMTk3YWNlMjhkOTc5Yzk3NTVmNTEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkZjI1ZTQ0YWY2ZTk5MWVlN2EyZTE5N2FjZTI4ZDk3OWM5NzU1ZjUxODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyMGp0ZXoyN201ZXJtbjY5Y3ZoNG4zZ205dXVqYTJsMnl0ejdnMGNlMyJdfX1dfQ==",
+ "eyJ0eGlkIjoiZDA1ZDgwYzcyY2NlMWQ5NzM1ZWFjYjdhM2UwODE0MTA5M2EzMGI1ZWUwMDkxMTAzM2Y2NGNhYzU4ZTM0MTcxNiIsInZvdXQiOlt7InZhbHVlIjowLjAyMDE4NzcsIm4iOjEyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMzgyZWE2MGMwNGEyM2YxYWRjNDg2ZTdkYzI4OTVmZjEyMWFiMzUyMyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDM4MmVhNjBjMDRhMjNmMWFkYzQ4NmU3ZGMyODk1ZmYxMjFhYjM1MjM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF1emFmc3ZxajNyN3hrdWZwaDhtczVmdGxjanIyZTR5djlycXozNjBwIl19fV19",
+ "eyJ0eGlkIjoiZDA1ZDgwYzcyY2NlMWQ5NzM1ZWFjYjdhM2UwODE0MTA5M2EzMGI1ZWUwMDkxMTAzM2Y2NGNhYzU4ZTM0MTcxNiIsInZvdXQiOlt7InZhbHVlIjowLjEwNDUyNDkzLCJuIjo0MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDk3ZjlkNzhjNjAzNzAwYmYwZDYyZjJhN2E5ZmI5YmYzYWVkM2Q0ZWIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5N2Y5ZDc4YzYwMzcwMGJmMGQ2MmYyYTdhOWZiOWJmM2FlZDNkNGViODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6dGxuNHV2dnFtc3AwY2R2dGUyMDIwbW4wZTZhNTc1YXY0NWpyYzd4eSJdfX1dfQ==",
+ "eyJ0eGlkIjoiZDZkNTg0N2NiYzk5YzcwNjZmZDJlZjgzN2Y1NjdkZGY2NjE0OGUwOGM0MTI1ZGE1NTQ5YmE4YWE3Y2I3YWY2MiIsInZvdXQiOlt7InZhbHVlIjowLjAwNTA0MywibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDU2N2NiOTI0M2VlOTc3NzU2MWI5ZjAyOTM5YzEyNzRiNjUwMTA2OTAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1NjdjYjkyNDNlZTk3Nzc1NjFiOWYwMjkzOWMxMjc0YjY1MDEwNjkwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwdDhld2Z5OG01aHdhdHBoOGN6and3cHlhOWsycWd4anEwOTM5M3BqYSJdfX1dfQ==",
+ "eyJ0eGlkIjoiZTEyMDU3ODMyYmQ1OTMwMGI2YjE1ZjFlMjA0ZmE1OTNhYmVjNmVlZDZhYTUwYjM4ZTM5M2JmZjBkMDhmYmMzMiIsInZvdXQiOlt7InZhbHVlIjowLjAzNjExNzI0LCJuIjoxOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDZjNWQ3ODIwMDY3NGEwMDYwNWE0M2Q0OTA5MTUxMzA0OGRkY2E5YTYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2YzVkNzgyMDA2NzRhMDA2MDVhNDNkNDkwOTE1MTMwNDhkZGNhOWE2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwazk2N3BxcWU2MnFwczk1czc1anpnNHp2emdtaDlmNWNjZGFudzBrOCJdfX1dfQ==",
+ "eyJ0eGlkIjoiZTEyMDU3ODMyYmQ1OTMwMGI2YjE1ZjFlMjA0ZmE1OTNhYmVjNmVlZDZhYTUwYjM4ZTM5M2JmZjBkMDhmYmMzMiIsInZvdXQiOlt7InZhbHVlIjowLjA4MjQ1Mjg5LCJuIjo0MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGYzZjc3ZjE3NDBhMWUyODQ1ODIxNmEzYjkyZDVmNWZiYTY3M2ZlMTYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmM2Y3N2YxNzQwYTFlMjg0NTgyMTZhM2I5MmQ1ZjVmYmE2NzNmZTE2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyZWx3bGNoZ3pzNzlwemN5OTRyaHlrNDdoYTZ2dWw3emNrNHg4bHdmcSJdfX1dfQ==",
+ "eyJ0eGlkIjoiZTFjMjQzOGYyNWI2NTJjZjVhM2U0M2NjYjhhYzczNTQ5NGE5MmI3YjgwNzhlMzA5MTg0MDVhOTk3ZGIxMzVhYiIsInZvdXQiOlt7InZhbHVlIjowLjAxNjczNTIxLCJuIjo5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDg4MzE0YjMxYmFhNThhNTYyZDllZmY1MDQ2YjAwYjdiZmNiYjU3NCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQ4ODMxNGIzMWJhYTU4YTU2MmQ5ZWZmNTA0NmIwMGI3YmZjYmI1NzQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ2Z3g5OW5ydzQ5M2Z0em04aGwycHJ0cXptbWxqYTR3c3U2ZXJheXpmIl19fV19",
+ "eyJ0eGlkIjoiZjgxNDU5Yzg5YTY4MTQ1ZGZhOTkwNTc0OWVhMjQ1YWJhMjk4NGY3OWZjNWM1M2M5NWE5ZGI5MmUxMjQ0ZTY0MyIsInZvdXQiOlt7InZhbHVlIjowLjIwNTY4ODI2LCJuIjoxMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkzNDA0ZWZhMDc4ZTQyYWJmOGJjOTY0NTg5NzliMTUxOGFiMGY4MjcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5MzQwNGVmYTA3OGU0MmFiZjhiYzk2NDU4OTc5YjE1MThhYjBmODI3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZjVxbmg2cTc4eTkybGNoanR5dHp0ZWs5Z2M0djhjeXVsNTJkNXJybiJdfX1dfQ==",
+ "eyJ0eGlkIjoiZjk3Yzg4Yzg1ZjIzMzEwYzhmNjE0ZThiNWEwNjU0OTM2Yjc4Zjg5OGE4NDY0YmJmMDA0OTVmZTllOGNjZTNjYyIsInZvdXQiOlt7InZhbHVlIjowLjAyNDk1MzQ0LCJuIjoxMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDkxZTZjYzhkZGQxNzVhMGIwMWEzMTRiMGNhZDAwNGQ0ZjllYjllNWIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5MWU2Y2M4ZGRkMTc1YTBiMDFhMzE0YjBjYWQwMDRkNGY5ZWI5ZTViODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZzdkbnlkbTV0NDV6Y3A1djJ0cGprc3FuMjBuNnU3dHZ6aDdqbXVlaiJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMWMyYWVmNDk0MDk4ZWY3Y2YzOGZlMzdmOGU0MzM5NGIwNjhhOGVmNzE2YjJkNWQ5ODQxMzkzYjU2MzNmNThjMSIsInZvdXQiOlt7InZhbHVlIjowLjAwMDI5NTE2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBhOTJlN2U3ZWRhMWJlNzZhZTc4MzBiYTY4MTYwYWJjMTBlOWNkY2YzIE9QX0VRVUFMIiwiaGV4IjoiYTkxNGE5MmU3ZTdlZGExYmU3NmFlNzgzMGJhNjgxNjBhYmMxMGU5Y2RjZjM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB6NWp1bG43bWdkN3c2aDhzdjk2ZHF0cTQwcXNhOHh1N3Y1Y3pqZ2xtaCJdfX1dfQ==",
+ "eyJ0eGlkIjoiNDI5MTExNWQ5Yjg5MDJiNjk3N2FiNmU5MjY4MmEwNGRmMzU2Njg3OWUzNDQzZGU5NDFiMDQwZjFiMjhmZDYyMiIsInZvdXQiOlt7InZhbHVlIjowLjAwMDIxNTcyLCJuIjoyMjA5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBkZDRhNDU1NmI3ZGZmMmJhYzRiYWY4NzcwYTYzOWUzYjdkYzVjYThmIE9QX0VRVUFMIiwiaGV4IjoiYTkxNGRkNGE0NTU2YjdkZmYyYmFjNGJhZjg3NzBhNjM5ZTNiN2RjNWNhOGY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBydzU1MzJra2wwbDl3a3lodHU4d3pucm5jYWhtM3cyM3VsOGFleWthcyJdfX1dfQ==",
+ "eyJ0eGlkIjoiNGRmOTQ5YWJlYjgzNzQ4YzA5ZGViY2YxZDRkZWMzYjI4NmQ3OTMxZGUyZDlkNzIzNWQxNDIxOTY2YWVmY2NlZSIsInZvdXQiOlt7InZhbHVlIjowLjAwMDM1NzEyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBhOTJlN2U3ZWRhMWJlNzZhZTc4MzBiYTY4MTYwYWJjMTBlOWNkY2YzIE9QX0VRVUFMIiwiaGV4IjoiYTkxNGE5MmU3ZTdlZGExYmU3NmFlNzgzMGJhNjgxNjBhYmMxMGU5Y2RjZjM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB6NWp1bG43bWdkN3c2aDhzdjk2ZHF0cTQwcXNhOHh1N3Y1Y3pqZ2xtaCJdfX1dfQ==",
+ "eyJ0eGlkIjoiNTFmMzJiMTc5YTE1N2Y4MzI3OWQyY2JkZjMzY2NjZmFkMTY5MmY5ZjlkMGU4NDdhY2E1OGZhMjg4OTUwYmM1NiIsInZvdXQiOlt7InZhbHVlIjowLjAwMDQ2NDU5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCA4MmVkMGRiNzJlNjBlN2I4ZGI3ODRkMjM0MTcxMWY5ZDg1NmNjNTk0IE9QX0VRVUFMIiwiaGV4IjoiYTkxNDgyZWQwZGI3MmU2MGU3YjhkYjc4NGQyMzQxNzExZjlkODU2Y2M1OTQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB6cHc2cmRoOWVzdzB3eG0wcHhqeHN0M3I3d2MybXg5anM5anZ0cXNxeiJdfX1dfQ==",
+ "eyJ0eGlkIjoiNTI2OGFjZTQzNmY1YjQ3N2Q2YzM1OWQyYjEyOWJhZjQxNGYyYTE1ODM4N2Q1MDU1YjU2MGIyZDJlMzY0MjRmZiIsInZvdXQiOlt7InZhbHVlIjowLjAwMDIxNzI3LCJuIjoxOTc3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBkZDRhNDU1NmI3ZGZmMmJhYzRiYWY4NzcwYTYzOWUzYjdkYzVjYThmIE9QX0VRVUFMIiwiaGV4IjoiYTkxNGRkNGE0NTU2YjdkZmYyYmFjNGJhZjg3NzBhNjM5ZTNiN2RjNWNhOGY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBydzU1MzJra2wwbDl3a3lodHU4d3pucm5jYWhtM3cyM3VsOGFleWthcyJdfX1dfQ==",
+ "eyJ0eGlkIjoiY2Y2MTEzMjc4NmQ3NjEyMjIxMjIwZWM4OTUwYTYxYjRlZDQ3MWZkNTRmMjE0ZGYxYjA5NTg4MDU2NzE1NWE5NiIsInZvdXQiOlt7InZhbHVlIjowLjAwMDIxNTk1LCJuIjoxMjg2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBkZDRhNDU1NmI3ZGZmMmJhYzRiYWY4NzcwYTYzOWUzYjdkYzVjYThmIE9QX0VRVUFMIiwiaGV4IjoiYTkxNGRkNGE0NTU2YjdkZmYyYmFjNGJhZjg3NzBhNjM5ZTNiN2RjNWNhOGY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBydzU1MzJra2wwbDl3a3lodHU4d3pucm5jYWhtM3cyM3VsOGFleWthcyJdfX1dfQ==",
+ "eyJ0eGlkIjoiZTA3ZGY0NzQyZWZlYmE5NDdkNmE4ZDQ2ZDBmNTg3MmRhODBjZTA0OWEyZGE0MmQ4ZjZjMjZhOTBlM2FmNmVhMiIsInZvdXQiOlt7InZhbHVlIjowLjAwMDQ2NDE2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCA4MmVkMGRiNzJlNjBlN2I4ZGI3ODRkMjM0MTcxMWY5ZDg1NmNjNTk0IE9QX0VRVUFMIiwiaGV4IjoiYTkxNDgyZWQwZGI3MmU2MGU3YjhkYjc4NGQyMzQxNzExZjlkODU2Y2M1OTQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB6cHc2cmRoOWVzdzB3eG0wcHhqeHN0M3I3d2MybXg5anM5anZ0cXNxeiJdfX1dfQ==",
+ "eyJ0eGlkIjoiZTczYTI1NjVlZjA5N2Y4OWYyNTI1NDY2ZWY0MTMwNmY1MzQ4NjkzMTg5YjUwMWZiZGYxMDA1NjFkNmRlNTM5OSIsInZvdXQiOlt7InZhbHVlIjowLjAwMDIwNjI4LCJuIjoyMzI5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBkZDRhNDU1NmI3ZGZmMmJhYzRiYWY4NzcwYTYzOWUzYjdkYzVjYThmIE9QX0VRVUFMIiwiaGV4IjoiYTkxNGRkNGE0NTU2YjdkZmYyYmFjNGJhZjg3NzBhNjM5ZTNiN2RjNWNhOGY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBydzU1MzJra2wwbDl3a3lodHU4d3pucm5jYWhtM3cyM3VsOGFleWthcyJdfX1dfQ==",
+ "eyJ0eGlkIjoiZTk4YzAzY2JhYWZhZDg1OWU0NDZmMjYwNmU1NDRkZDM2YTgxMWI4ODQzNzc2NzRmOWZjYzdmMzhlMzhhZDg4MSIsInZvdXQiOlt7InZhbHVlIjowLjAyMjI4NDAyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBiMjA1NDQ4YzhjMmFjN2VhZWViZWE0MThjOWM3OWEyODZhMTBkNGRhIE9QX0VRVUFMIiwiaGV4IjoiYTkxNGIyMDU0NDhjOGMyYWM3ZWFlZWJlYTQxOGM5Yzc5YTI4NmExMGQ0ZGE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB6ZXEyM3l2M3M0djA2aHdoNmpwM2p3OG5nNXg1eXg1bWdyc2xoa2VsciJdfX1dfQ==",
+ "eyJ0eGlkIjoiZmU4MDY0MmRhMmRmYjBmNWRmOTliYmI5MTcxOTI0NzIwYmE0OTc0ZWQzZDI0MzBmMjg1YzFmNmJiNTQ3N2RhMCIsInZvdXQiOlt7InZhbHVlIjowLjAwMDIxMTMsIm4iOjc4MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gxNjAgMjM2MzliYjAzYjFiZGVmNzkxZjc0N2NmMzMyNTc2NzJmN2Y2ZTY3MSBPUF9FUVVBTCIsImhleCI6ImE5MTQyMzYzOWJiMDNiMWJkZWY3OTFmNzQ3Y2YzMzI1NzY3MmY3ZjZlNjcxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwcTNrOHhhczh2ZGFhYXUzN2FydTd2ZTl3ZWUwMGFoeHd5NXF6bWZsaHIiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMGFkNWE4YTg5MWJiZTc5YzYwYjg4MjhkYTA4YjhmMjBlMzAzNWYwNDI3ZGRmYWRlM2Y1NWUwMzAwMDk1YTI3NiIsInZvdXQiOlt7InZhbHVlIjoxMDMuNjU3MzIxMDIsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnMGM5aGxrZG11Nm5zYTNkeDdsZ200ZDVmenJ3azB1dmN5dWFjOW1lcWswNG11cncza2d5aGY1bW43eCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ4NjE1NjAifX1dfQ==",
+ "eyJ0eGlkIjoiMGFkNWE4YTg5MWJiZTc5YzYwYjg4MjhkYTA4YjhmMjBlMzAzNWYwNDI3ZGRmYWRlM2Y1NWUwMzAwMDk1YTI3NiIsInZvdXQiOlt7InZhbHVlIjo5Mi4zNzUzNjQwNywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMCBPUF9FUVVBTCIsImhleCI6ImFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjB3ZzlwMGx5NXRmMm5mOXY3NzNoM2Y5eGxyanB3eHFmN25oMjlzNXQ4M2N6NjVzN2t0cWFocjl5YWdkIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDMzMjQzMyJ9fV19",
+ "eyJ0eGlkIjoiMGFkNWE4YTg5MWJiZTc5YzYwYjg4MjhkYTA4YjhmMjBlMzAzNWYwNDI3ZGRmYWRlM2Y1NWUwMzAwMDk1YTI3NiIsInZvdXQiOlt7InZhbHVlIjo3MS4yNzMyMDEyLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDJjZjliZjRkYWIwNWEyMzUyMWRjMjYyOTM4MzAyNjJmZmVjNzYxNDY2ODgwOThiYjMxMjRhYzNmYjc2NDAwMzY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2azBuMDZkNHZ6NnlkZnBtc256andwc3ljaGxhM21wZ2U1Z3B4OW14eWoyYzBhaHZzcXJ2ZGZ4Y3Y3eWsiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzMzQyNzM2In19XX0=",
+ "eyJ0eGlkIjoiMGFkNWE4YTg5MWJiZTc5YzYwYjg4MjhkYTA4YjhmMjBlMzAzNWYwNDI3ZGRmYWRlM2Y1NWUwMzAwMDk1YTI3NiIsInZvdXQiOlt7InZhbHVlIjozNi4xMTg5OTY0MSwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTY5Mzk5MiJ9fV19",
+ "eyJ0eGlkIjoiZjMzM2FiNGYxM2VjYjQzZWIxODAxMDhhMDg5NmM4NGIxYjAxM2ZlZDc1MGRhZTRlNzM0MDQ5MGM4MDNiNGUxNCIsInZvdXQiOlt7InZhbHVlIjoxMS4zNzU2NjcyNiwibiI6NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYiBPUF9FUVVBTCIsImhleCI6ImFhMjBiMzQ3ZDY4YzhkMmNkYjMyMDY2YmU5YWY4MDRjNmZmYjhhYmJiNDQxYmRmMzZjYWYzODM0MzZiY2ZjYWEzNWNiODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd2U1MDQ1djM1a2RrdnN4ZDA1NmxxenZkbGFjNHdhNWd4N2x4bTkwOHE2cmQwOHU0ZzZ1a24zNzJnZ3dsIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTMyNjk0In19XX0=",
+ "eyJ0eGlkIjoiZjMzM2FiNGYxM2VjYjQzZWIxODAxMDhhMDg5NmM4NGIxYjAxM2ZlZDc1MGRhZTRlNzM0MDQ5MGM4MDNiNGUxNCIsInZvdXQiOlt7InZhbHVlIjoxMC41Njk1MzQ3LCJuIjo2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDdjMmJjY2EyMDg5YTlmMGY4ZGFlYzBkM2I0NWE2YzUyNTgxYzU2NzY2NWNjNTFlYmRhOWEyMDJkOGEzM2RjZmM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkN3pobjl6cHpkZjdydWQ0bXFkOGR6NmQzZjlzOHprd2VqdWM1MHRtMmR6cXR2MngwdzBjNTR5MzlocDYiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0OTQ5NDUifX1dfQ==",
+ "eyJ0eGlkIjoiMGFkNWE4YTg5MWJiZTc5YzYwYjg4MjhkYTA4YjhmMjBlMzAzNWYwNDI3ZGRmYWRlM2Y1NWUwMzAwMDk1YTI3NiIsInZvdXQiOlt7InZhbHVlIjoxNy41MjgxMjA4NSwibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZCBPUF9FUVVBTCIsImhleCI6ImFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnN0cWttNTRkdHZucHlxeHQ1bTVuN3Nqc240ZW5ybHhjNTI2eHl4bG5qa2F5Y2R6ZmV1NjlyZXl6bXF4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODIyMDc1In19XX0=",
+ "eyJ0eGlkIjoiZjMzM2FiNGYxM2VjYjQzZWIxODAxMDhhMDg5NmM4NGIxYjAxM2ZlZDc1MGRhZTRlNzM0MDQ5MGM4MDNiNGUxNCIsInZvdXQiOlt7InZhbHVlIjo1LjIyOTQ2ODM1LCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBmMWMzM2JjN2RhMDk5MWQyNmY4YmJmMTUwYzkwMzU2OGIyYzNkYzg3MjczNzA4NjNmNGMzZDlmOTY1NWIzNzkxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwY3V4dzc4bWd5ZXI1bjAzd2wzMnJ5c3g0NXQ5czd1c3Vubnd6cnI3bnBhbjd0OXR2bWV6cmF1M2p1cjkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyNDQ4ODQifX1dfQ==",
+ "eyJ0eGlkIjoiZjMzM2FiNGYxM2VjYjQzZWIxODAxMDhhMDg5NmM4NGIxYjAxM2ZlZDc1MGRhZTRlNzM0MDQ5MGM4MDNiNGUxNCIsInZvdXQiOlt7InZhbHVlIjo0LjQxMzE5NzM3LCJuIjo5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YW15bmxlcHYzcG5nbnE2cmhxNTk1eDBsZm5nNncwd3A4NWpxM3JydjJ3amNsdmh0M3VneDRrYWo0aHUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMDY2NjAifX1dfQ==",
+ "eyJ0eGlkIjoiZjMzM2FiNGYxM2VjYjQzZWIxODAxMDhhMDg5NmM4NGIxYjAxM2ZlZDc1MGRhZTRlNzM0MDQ5MGM4MDNiNGUxNCIsInZvdXQiOlt7InZhbHVlIjozLjMxMTg5MTA2LCJuIjoxMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2JiMjRmZjkwYjIyMTlhMjYwZDBlZTBhMTY4NjdmZDMzNDY5Y2Y3MDRmNDkwMjIzMWIxNGU5NjNlY2JhZTNjNCBPUF9FUVVBTCIsImhleCI6ImFhMjAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmFteW5sZXB2M3BuZ25xNnJocTU5NXgwbGZuZzZ3MHdwODVqcTNycnYyd2pjbHZodDN1Z3g0a2FqNGh1Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTU1MDg4In19XX0=",
+ "eyJ0eGlkIjoiZjMzM2FiNGYxM2VjYjQzZWIxODAxMDhhMDg5NmM4NGIxYjAxM2ZlZDc1MGRhZTRlNzM0MDQ5MGM4MDNiNGUxNCIsInZvdXQiOlt7InZhbHVlIjoyLjg4NDA5MDg0LCJuIjo3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBlZmJjNTU2ZDVjZmU5MGE0NThkYmY0ZDU3YmQ3ODdmMzM0YmFmYjFhOTMzZmZjMzMyMjI2N2ViZjQ0YzM0ZmIwIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGVmYmM1NTZkNWNmZTkwYTQ1OGRiZjRkNTdiZDc4N2YzMzRiYWZiMWE5MzNmZmMzMzIyMjY3ZWJmNDRjMzRmYjA4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwaG1jNHRkdG5sZnBmemNtMDZkMjc3aHNsZW5md2htcjJmbmxscG55Z244YTA2eWNkOG1xZGE1ZXZwczIiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMzUwNTIifX1dfQ==",
+ "eyJ0eGlkIjoiODFiMjZkMDBiMWU2MTZmOWRhYTc2YTE2YzA2Yzc4NWJiMTgyZGEzYWYyZTZlYmYwNjVmM2ZkOWQ2MWIxMTNmYSIsInZvdXQiOlt7InZhbHVlIjoyLjU0NjQ4ODcsIm4iOjUsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnajY5bjJ1Mjh0MHp2ZzUwbHIzdWttdDl2eHBwZ2RjcmY5YXEzY3U1Nmxkc3B5eGw0dzVoc2Z5bThzNyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjExOTIwMyJ9fV19",
+ "eyJ0eGlkIjoiODFiMjZkMDBiMWU2MTZmOWRhYTc2YTE2YzA2Yzc4NWJiMTgyZGEzYWYyZTZlYmYwNjVmM2ZkOWQ2MWIxMTNmYSIsInZvdXQiOlt7InZhbHVlIjoyLjU0MTcwMDg5LCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkZ2o2OW4ydTI4dDB6dmc1MGxyM3VrbXQ5dnhwcGdkY3JmOWFxM2N1NTZsZHNweXhsNHc1aHNmeW04czciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMTg5NzkifX1dfQ==",
+ "eyJ0eGlkIjoiODFiMjZkMDBiMWU2MTZmOWRhYTc2YTE2YzA2Yzc4NWJiMTgyZGEzYWYyZTZlYmYwNjVmM2ZkOWQ2MWIxMTNmYSIsInZvdXQiOlt7InZhbHVlIjoyLjU0MTE5NTQxLCJuIjo3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkZ2o2OW4ydTI4dDB6dmc1MGxyM3VrbXQ5dnhwcGdkY3JmOWFxM2N1NTZsZHNweXhsNHc1aHNmeW04czciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMTg5NTYifX1dfQ==",
+ "eyJ0eGlkIjoiODFiMjZkMDBiMWU2MTZmOWRhYTc2YTE2YzA2Yzc4NWJiMTgyZGEzYWYyZTZlYmYwNjVmM2ZkOWQ2MWIxMTNmYSIsInZvdXQiOlt7InZhbHVlIjoyLjU0MTcxNzksIm4iOjYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWEgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYTg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnajY5bjJ1Mjh0MHp2ZzUwbHIzdWttdDl2eHBwZ2RjcmY5YXEzY3U1Nmxkc3B5eGw0dzVoc2Z5bThzNyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjExODk4MSJ9fV19",
+ "eyJ0eGlkIjoiODFiMjZkMDBiMWU2MTZmOWRhYTc2YTE2YzA2Yzc4NWJiMTgyZGEzYWYyZTZlYmYwNjVmM2ZkOWQ2MWIxMTNmYSIsInZvdXQiOlt7InZhbHVlIjoyLjQ5OTM4NjE0LCJuIjo5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkZ2o2OW4ydTI4dDB6dmc1MGxyM3VrbXQ5dnhwcGdkY3JmOWFxM2N1NTZsZHNweXhsNHc1aHNmeW04czciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMTY5OTkifX1dfQ==",
+ "eyJ0eGlkIjoiODFiMjZkMDBiMWU2MTZmOWRhYTc2YTE2YzA2Yzc4NWJiMTgyZGEzYWYyZTZlYmYwNjVmM2ZkOWQ2MWIxMTNmYSIsInZvdXQiOlt7InZhbHVlIjoyLjQ3MTMwMjkzLCJuIjoxMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYSBPUF9FUVVBTCIsImhleCI6ImFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGdqNjluMnUyOHQwenZnNTBscjN1a210OXZ4cHBnZGNyZjlhcTNjdTU2bGRzcHl4bDR3NWhzZnltOHM3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTE1Njg0In19XX0=",
+ "eyJ0eGlkIjoiODFiMjZkMDBiMWU2MTZmOWRhYTc2YTE2YzA2Yzc4NWJiMTgyZGEzYWYyZTZlYmYwNjVmM2ZkOWQ2MWIxMTNmYSIsInZvdXQiOlt7InZhbHVlIjoyLjM1NjA1MTk1LCJuIjoxMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYzhkZDk3N2FlOTkxMmI3MjEzNDFiYWYzMDg2ODdiZTViOWVmYjMyY2MwZmJmMjY5NjY2OTBiNjE5OGUyMDFjZiBPUF9FUVVBTCIsImhleCI6ImFhMjBjOGRkOTc3YWU5OTEyYjcyMTM0MWJhZjMwODY4N2JlNWI5ZWZiMzJjYzBmYmYyNjk2NjY5MGI2MTk4ZTIwMWNmODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMHlkbTltNmF4Z2prdXNuZ3hhMHh6cmcwMGptbm1hbjlucTBodW5mdmU1c2tjdmN1Z3F1N3J1YWd3dmVsIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTEwMjg5In19XX0=",
+ "eyJ0eGlkIjoiYWM2ZWEwMmIxMWU2NDY1YjA1OTgxY2VlYzEwMDA5ZmUwZTg4NDEyYjkzYzU3MWM2NDQ2YWZlMWM3YmE2NWQzOSIsInZvdXQiOlt7InZhbHVlIjoxLjA4MjQ4Mjk4LCJuIjoxMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2Y5OTJkMTI5MDQxMTM3NDhjNDQ0M2MxNjRlY2YxMmY0OGQxYjA3NTE4MDhlMmI1MzcxODQ5N2UxMmY5NGI0MSBPUF9FUVVBTCIsImhleCI6ImFhMjAzZjk5MmQxMjkwNDExMzc0OGM0NDQzYzE2NGVjZjEyZjQ4ZDFiMDc1MTgwOGUyYjUzNzE4NDk3ZTEyZjk0YjQxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmxlanRnampwcTN4YXl2ZzNwdXplOHY3eWg1MzVkc3c1dnEzYzQ0eHV2eWpsc2psOTk1ejdxOHcyeThrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTA2MDEifX1dfQ==",
+ "eyJ0eGlkIjoiYWM2ZWEwMmIxMWU2NDY1YjA1OTgxY2VlYzEwMDA5ZmUwZTg4NDEyYjkzYzU3MWM2NDQ2YWZlMWM3YmE2NWQzOSIsInZvdXQiOlt7InZhbHVlIjoxLjA2ODI5ODU0LCJuIjoxNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZCBPUF9FUVVBTCIsImhleCI6ImFhMjBhYWVhNjQ5ZjcxOTNhYjQzMjk5OTk5M2JkOGQyY2NjZTVmYWJkM2FmMDFlYmZlNDFhMGUwYTM1ODEwNDRkNDZkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzR3NWV5bHd4ZjZrc2Vmbnh2bmhreGplbjg5bDI3bjR1cTdobGpwNXJzMnhrcXNnbjJ4NjhnbnV2emVrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDk5MzYifX1dfQ==",
+ "eyJ0eGlkIjoiYWM2ZWEwMmIxMWU2NDY1YjA1OTgxY2VlYzEwMDA5ZmUwZTg4NDEyYjkzYzU3MWM2NDQ2YWZlMWM3YmE2NWQzOSIsInZvdXQiOlt7InZhbHVlIjoxLjA2NzUzMjU0LCJuIjoxNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2FmZDRkZThhZmZiMDdjYjY2ODMxMzRhZDU5ZTljN2E3NDNhNzBlNjE0YWQ2MjFhZmY1YTExMDM3YzAxMTVlMiBPUF9FUVVBTCIsImhleCI6ImFhMjA3YWZkNGRlOGFmZmIwN2NiNjY4MzEzNGFkNTllOWM3YTc0M2E3MGU2MTRhZDYyMWFmZjVhMTEwMzdjMDExNWUyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGEwNm4wZzRsYXMwam14c3ZmNTQ0djduM2E4Z3duc3VjMjI2Y3M2bGFkcHpxbXVxeTI3eTZqa2V1NnpnIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDk5MDEifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjoxLjI1OTMzODk5LCJuIjoyNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmRkNzA1NWM5MjYyNTMyNTJlYjQ0ZjdlYTQxMzlkYjI0NWU0OTJhYWRjMTllOWU1Y2ZlOTVhMmVjODdjYjA2MCBPUF9FUVVBTCIsImhleCI6ImFhMjAyZGQ3MDU1YzkyNjI1MzI1MmViNDRmN2VhNDEzOWRiMjQ1ZTQ5MmFhZGMxOWU5ZTVjZmU5NWEyZWM4N2NiMDYwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmthd3AydWpmMzl4ZmZ3azM4aGFmcW5ua2V5dGV5ajR0d3BuNjA5ZWw1NDV0a2cwamN4cW40OGM3enllIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTg5MTkifX1dfQ==",
+ "eyJ0eGlkIjoiYWM2ZWEwMmIxMWU2NDY1YjA1OTgxY2VlYzEwMDA5ZmUwZTg4NDEyYjkzYzU3MWM2NDQ2YWZlMWM3YmE2NWQzOSIsInZvdXQiOlt7InZhbHVlIjowLjk5Mzk5NTMyLCJuIjoxNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYTY0YTY1MzA5MTYzMzI5ODA2Mjc1YTVhOTliMjc4NTI0YjIxYTQ2NTU5ZGU0N2VmY2NlMDk4ZDNlYjViMmEwZSBPUF9FUVVBTCIsImhleCI6ImFhMjBhNjRhNjUzMDkxNjMzMjk4MDYyNzVhNWE5OWIyNzg1MjRiMjFhNDY1NTlkZTQ3ZWZjY2UwOThkM2ViNWIyYTBlODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd255NWVmc2o5M245eHF4eWFkOTR4ZGowcGZ5a2dkeXY0dmF1M2wwZW5zZjM1bHR0djRxdTJ4cnMzNndoIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDY0NjMifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjoxLjIzODYzMDI5LCJuIjoyNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZCBPUF9FUVVBTCIsImhleCI6ImFhMjBhYWVhNjQ5ZjcxOTNhYjQzMjk5OTk5M2JkOGQyY2NjZTVmYWJkM2FmMDFlYmZlNDFhMGUwYTM1ODEwNDRkNDZkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzR3NWV5bHd4ZjZrc2Vmbnh2bmhreGplbjg5bDI3bjR1cTdobGpwNXJzMnhrcXNnbjJ4NjhnbnV2emVrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTc5NDkifX1dfQ==",
+ "eyJ0eGlkIjoiYWM2ZWEwMmIxMWU2NDY1YjA1OTgxY2VlYzEwMDA5ZmUwZTg4NDEyYjkzYzU3MWM2NDQ2YWZlMWM3YmE2NWQzOSIsInZvdXQiOlt7InZhbHVlIjowLjk3OTcyNzY1LCJuIjoxNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2FmZDRkZThhZmZiMDdjYjY2ODMxMzRhZDU5ZTljN2E3NDNhNzBlNjE0YWQ2MjFhZmY1YTExMDM3YzAxMTVlMiBPUF9FUVVBTCIsImhleCI6ImFhMjA3YWZkNGRlOGFmZmIwN2NiNjY4MzEzNGFkNTllOWM3YTc0M2E3MGU2MTRhZDYyMWFmZjVhMTEwMzdjMDExNWUyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGEwNm4wZzRsYXMwam14c3ZmNTQ0djduM2E4Z3duc3VjMjI2Y3M2bGFkcHpxbXVxeTI3eTZqa2V1NnpnIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDU3OTcifX1dfQ==",
+ "eyJ0eGlkIjoiYWM2ZWEwMmIxMWU2NDY1YjA1OTgxY2VlYzEwMDA5ZmUwZTg4NDEyYjkzYzU3MWM2NDQ2YWZlMWM3YmE2NWQzOSIsInZvdXQiOlt7InZhbHVlIjowLjg2NjY4OTM2LCJuIjoxOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYzNlN2RjY2U2M2UzN2FiMmRlM2JmOGE3ZDkzMmNmYTAyOTM3NDkwMmM2MzEwZThlZGNiMzExODEyMzQ1NzY3YSBPUF9FUVVBTCIsImhleCI6ImFhMjBjM2U3ZGNjZTYzZTM3YWIyZGUzYmY4YTdkOTMyY2ZhMDI5Mzc0OTAyYzYzMTBlOGVkY2IzMTE4MTIzNDU3NjdhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMHA3MGh4d3YwM2g0dms3ODB1MjBrZmplN3N6amQ2ZnF0cnJ6cjV3bWplM3JxZnJnNG04NXgyNzBmNTZ2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDA1MTMifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjg0NTc3NjU3LCJuIjoxOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWQ3OTc3OWQ2YjgyMzYyODE5Y2MxYTVhOTUxYjlkN2M5ZjcyZTdhMWFmNDc1Nzc2ZDk0NGYzNTRkNzYzMzlmMyBPUF9FUVVBTCIsImhleCI6ImFhMjAxZDc5Nzc5ZDZiODIzNjI4MTljYzFhNWE5NTFiOWQ3YzlmNzJlN2ExYWY0NzU3NzZkOTQ0ZjM1NGQ3NjMzOWYzODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdndoamF1YWR3cHJ2MnFlZXNkOTQ5Z21uNDdmN3VoODV4aDV3NG1rbTl6MHg0eGh2dnVseGM2cjYzNGg2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzk1NzIifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjc4OTc2NTQ4LCJuIjoyMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNDYxOWYwYjI5ZDFlZTcwNTQ2Nzg2YjhiMzBjNzViYmU1YjE1YmRiOThkMDFhNmFkZDNhNDE0NzAwMGY4NmU2MSBPUF9FUVVBTCIsImhleCI6ImFhMjA0NjE5ZjBiMjlkMWVlNzA1NDY3ODZiOGIzMGM3NWJiZTViMTViZGI5OGQwMWE2YWRkM2E0MTQ3MDAwZjg2ZTYxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZHJwbnU5am41MHd3cDJ4MHA0Y2t2eDh0d2w5azlkYWh4eHNyZjRkNndqcGd1cXFscGh4emV1cHRkeDZuIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzY5NTEifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjc5MjE5MTMzLCJuIjoyMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOGRhZDcxYTk3YmMxZGM5ZTJiZWQ5ZjZhYTE0YmNlMTk4OThkM2NlODE0MmY2NWQyMTg1ODFkYWY0Mjc0NTMxYyBPUF9FUVVBTCIsImhleCI6ImFhMjA4ZGFkNzFhOTdiYzFkYzllMmJlZDlmNmFhMTRiY2UxOTg5OGQzY2U4MTQyZjY1ZDIxODU4MWRhZjQyNzQ1MzFjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3g2NnVkZjAwcWFlODN0YWswazRnMnRlY3ZjbnJmdWFxMno3ZXdqcnB2cG10Nnp3M2YzYzc1cmd6azN5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzcwNjUifX1dfQ==",
+ "eyJ0eGlkIjoiZWRiZWYwYjNmMGQ4NmEyMjVkNDVlMjdiNWU2ZjQ1ZWJlZGJiNjYyZTI3YTc0M2NhODdiOTYxNTQxMzk4YjUxZiIsInZvdXQiOlt7InZhbHVlIjo4ZS0wNiwibiI6NDUsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0Njg1NyJ9fV19",
+ "eyJ0eGlkIjoiMGFkNWE4YTg5MWJiZTc5YzYwYjg4MjhkYTA4YjhmMjBlMzAzNWYwNDI3ZGRmYWRlM2Y1NWUwMzAwMDk1YTI3NiIsInZvdXQiOlt7InZhbHVlIjozNi40MjQzMTE5OCwibiI6Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDEyZmNiNzA2ZmE2NjJiMmY3ZWY2NDA4M2ZhYjJlMDU0NzVmNzVkZDcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZjBlZGN4bGZuemt0bTc3ZXFnODc0anVwMjh0YTZhNnU1cnk1c2Z1OSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZTk4YzAzY2JhYWZhZDg1OWU0NDZmMjYwNmU1NDRkZDM2YTgxMWI4ODQzNzc2NzRmOWZjYzdmMzhlMzhhZDg4MSIsInZvdXQiOlt7InZhbHVlIjo0MjUuMCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gxNjAgMGZmYmQ3YjI5YzVjMTE0MmQwMDRlYTI5OTQyYTYwNWI1OGI5MWI3MyBPUF9FUVVBTCIsImhleCI6ImE5MTQwZmZiZDdiMjljNWMxMTQyZDAwNGVhMjk5NDJhNjA1YjU4YjkxYjczODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwcThsaDRham4zd3B6c2tzcW40em45cDJ2cGQ0M3dnbXd2a3F6ZjB4OWgiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZjRkMmFlOTk4NDFmZTE3YTQ5MGZmZWIyMzk0MGVjNmM4MzU2NGJkZjhhZmIzZGE3YWQ1MTU4OTdmMWMxNjY1MyIsInZvdXQiOlt7InZhbHVlIjoyLjA3Njc3OTAzLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYmY5M2QwNGI3NmFkZWMxY2JhODVlNmQwM2QzMWQ2NGVmMTQyOGFiNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGJmOTNkMDRiNzZhZGVjMWNiYTg1ZTZkMDNkMzFkNjRlZjE0MjhhYjc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpsZTg1enR3Nms3Yzg5NnNobmRxMGYzNmU4MHpzNTJrdW52OXh1N2ptIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiODY0YTUwZTQyOGVlOGJiMzRmZGMyYjkyNzc1ZDM1NDA4YWY2MWUyOWI5MmJkZmQ1ZjllZDFhMWQ5YjA4NWRmZCIsInZvdXQiOlt7InZhbHVlIjoxNy4xMDU3MTk4NSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhNmVlMDRjYjc3Mjg3NTlhOGRlZTZiZWQzYTc4MTMxN2EzY2UzZDJlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6bnd1cHh0d3U1OHR4NWRhZTQ3NnduY3p2dDY4bjNhOWNtNG1yOXB0bSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNjg5YjA0MDJhZDJhNjJhZjA4ZTMwYTQ2NzI4NjEzZjI4NTYwY2I5OTFhNGFjMjEwYzJlM2JlMmE1ZTJjOGJmNCIsInZvdXQiOlt7InZhbHVlIjoyLjgyOTIyMjIsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5MGYyMjg1Mzk1MTk5MzE1NDBmMGUxZGUyNWRiYzAwOTM0OThjN2E1IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxemcweTJ6bmo1dmV4OTJxN3JzYXVmd21jcXluZnh4ODU1ZDg2NmN5NnQiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZTViNmQ4YWEyN2QzYzkwZGVmYWE1N2U5OTY2YzFkZmE5ODA4Y2Q2YjVmNTFhOTg3MTczYmEwMDNkOWU3MmExMiIsInZvdXQiOlt7InZhbHVlIjowLjAwMDQxOTIxLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiN2VmMTEzYTVkNjQwMGY1Yjg3ZTUyNTRmMGZlZTVhMzE0YjEwMjZiZGEzMWViYWQwY2I1ZTFjMTUwOTAwMGUyZiIsInZvdXQiOlt7InZhbHVlIjowLjAyNDEyNTE0LCJuIjo0NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGI4MWExZTgxZjhlMTRkNzRkNGIxZWRkYzZiNGIxN2E2NTY5NWFlNTIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRiODFhMWU4MWY4ZTE0ZDc0ZDRiMWVkZGM2YjRiMTdhNjU2OTVhZTUyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6dXA1ODVwbHJzNTZheDVrOGthYzY2dHo3bjlkOWR3MmdubjlhaGQzMyJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMGRhMmE0MDFhYWMxZmJkOGZkMGU1NTRjYjdlYjZlOTg5ZWQxNWRhMGYwZGY1Y2ExNWIwMjE4ZjVmZTBlYzUzMiIsInZvdXQiOlt7InZhbHVlIjoxLjM5M2UtMDUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIGEyNmI0MTY4OTE4MGIyY2I0OTg2NTczYWI4YTVhZTA0NzQyMGRhNTMgT1BfRVFVQUwiLCJoZXgiOiJhOTE0YTI2YjQxNjg5MTgwYjJjYjQ5ODY1NzNhYjhhNWFlMDQ3NDIwZGE1Mzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHozeGtzdGdqeHF0OWo2ZnNldG40dzk5NGN6OGdneDYydjhnOHhqZ3B4Il19fV19",
+ "eyJ0eGlkIjoiMjFiNWI0YTQ1OThmZjhkODE5NGQ3ODk1ZDFkM2ZhMmYwNTU2NDU1ZGM0ZmZlOWRiNjQyYTQ4MWI4MzdmZDc0OCIsInZvdXQiOlt7InZhbHVlIjoxLjM5M2UtMDUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIGEyNmI0MTY4OTE4MGIyY2I0OTg2NTczYWI4YTVhZTA0NzQyMGRhNTMgT1BfRVFVQUwiLCJoZXgiOiJhOTE0YTI2YjQxNjg5MTgwYjJjYjQ5ODY1NzNhYjhhNWFlMDQ3NDIwZGE1Mzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHozeGtzdGdqeHF0OWo2ZnNldG40dzk5NGN6OGdneDYydjhnOHhqZ3B4Il19fV19",
+ "eyJ0eGlkIjoiNDk4MzU4NDI3YTljZjMzMjUzZmFmZWJkNTQzYmRlOTI2YzQ0Mjk2YTc3ODY2NGIwOTkwY2EzYjQ4MzkyOGQ0MCIsInZvdXQiOlt7InZhbHVlIjoxLjM5M2UtMDUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIGIzZWUwNDkzZmNjMDM1NDhkNWI5NjA3Y2MzMzQ0ZGY1NzkzMjQ2MzQgT1BfRVFVQUwiLCJoZXgiOiJhOTE0YjNlZTA0OTNmY2MwMzU0OGQ1Yjk2MDdjYzMzNDRkZjU3OTMyNDYzNDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHplN3VweW5sbnFyMmp4NGg5czhlc2U1Zmg2aGp2anh4c3kwdDhtajUyIl19fV19",
+ "eyJ0eGlkIjoiNTY0MWQ1NDY0YWYzYmRjNTU1M2QwMWYxMDU4YzJjYWY4NDE4OTY5ZWYwNjlkMzgwMmQzYTM1NTUwN2UyNDI5NSIsInZvdXQiOlt7InZhbHVlIjo2LjU1Njk2ODE4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCA1ZmYwYmJkMTQ3OTE1ZWI5MjEyYWM2YjQ3OTQ0YTBjMGZmYzcwNzY0IE9QX0VRVUFMIiwiaGV4IjoiYTkxNDVmZjBiYmQxNDc5MTVlYjkyMTJhYzZiNDc5NDRhMGMwZmZjNzA3NjQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBwMGxwdzczZzdnNGF3ZnA5dHJ0ZzcyeTVycTBsM2M4dnNmN2thczdrdiJdfX1dfQ==",
+ "eyJ0eGlkIjoiOGRjZmRmOGRkMWU2MWFlNjc4OTliZTZmMDM5NTNlYmVkYmFkMjA5ZDQ1MTc1ZjJlYjIwZWY5YzcxNWVlNDBmMyIsInZvdXQiOlt7InZhbHVlIjoxLjM5M2UtMDUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIGEyNmI0MTY4OTE4MGIyY2I0OTg2NTczYWI4YTVhZTA0NzQyMGRhNTMgT1BfRVFVQUwiLCJoZXgiOiJhOTE0YTI2YjQxNjg5MTgwYjJjYjQ5ODY1NzNhYjhhNWFlMDQ3NDIwZGE1Mzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHozeGtzdGdqeHF0OWo2ZnNldG40dzk5NGN6OGdneDYydjhnOHhqZ3B4Il19fV19",
+ "eyJ0eGlkIjoiOTY4MmUwNjM4ODRkODdhODE2NDNhZjMyMzAxNTUxMmY2YWVhNmM0YjZkMjI4NGE5ZjQ0NmY4Yzc3NDVjOWYwNiIsInZvdXQiOlt7InZhbHVlIjoxLjM5M2UtMDUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIDYzYTM0NmFjNzAwZDJhOGMxOWI3ZWY3MzRiYmQ3NzcxNDk2ZWQ0M2MgT1BfRVFVQUwiLCJoZXgiOiJhOTE0NjNhMzQ2YWM3MDBkMmE4YzE5YjdlZjczNGJiZDc3NzE0OTZlZDQzYzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHAzNngzNHZ3cXhqNHJxZWtsaGh4amFhd2FjNWptazU4c2MzeGZ1d3FyIl19fV19",
+ "eyJ0eGlkIjoiYTU5M2MwZmViNThhYWI4ZDliMDlhZDkxYWM4NzlmMjcxNTU2YjM1NWZkNDc0N2NhOGIzY2E5YjQzN2I5ZmY5OSIsInZvdXQiOlt7InZhbHVlIjoxLjM5M2UtMDUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIDYzYTM0NmFjNzAwZDJhOGMxOWI3ZWY3MzRiYmQ3NzcxNDk2ZWQ0M2MgT1BfRVFVQUwiLCJoZXgiOiJhOTE0NjNhMzQ2YWM3MDBkMmE4YzE5YjdlZjczNGJiZDc3NzE0OTZlZDQzYzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHAzNngzNHZ3cXhqNHJxZWtsaGh4amFhd2FjNWptazU4c2MzeGZ1d3FyIl19fV19",
+ "eyJ0eGlkIjoiYzU2ZTM5N2Y3MjI5YTE5ZGUyOGVlZjY3MTZiZDg1YzY5NzBlNGNmNGY3MjBmYjVlZDE3OTJhMmRkOGY5N2VmNyIsInZvdXQiOlt7InZhbHVlIjoxLjM5M2UtMDUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIGEyNmI0MTY4OTE4MGIyY2I0OTg2NTczYWI4YTVhZTA0NzQyMGRhNTMgT1BfRVFVQUwiLCJoZXgiOiJhOTE0YTI2YjQxNjg5MTgwYjJjYjQ5ODY1NzNhYjhhNWFlMDQ3NDIwZGE1Mzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHozeGtzdGdqeHF0OWo2ZnNldG40dzk5NGN6OGdneDYydjhnOHhqZ3B4Il19fV19",
+ "eyJ0eGlkIjoiZGU4NDM5ZThhYTFkYzcwYTJkNGE4NjkzNzZkNzdmZmM3MDliNWE3NDZhMjkzNzVlODA4YjAyY2Q2YTc3YzUxYiIsInZvdXQiOlt7InZhbHVlIjowLjAwMTkwMDc1LCJuIjo1NjUsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIDQ2M2FhMzc0YWUwZjhlYmQxNGEwNTA5MGUzOTIzMmNhZDIxZWY2ZDcgT1BfRVFVQUwiLCJoZXgiOiJhOTE0NDYzYWEzNzRhZTBmOGViZDE0YTA1MDkwZTM5MjMyY2FkMjFlZjZkNzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHBycjRnbTU0YzhjYTBnNTVwZ2ZwY3VqeHQ5ZHk4aGs2dXV6NmQ1Y3ZlIl19fV19",
+ "eyJ0eGlkIjoiZWZiYjBiNDc1MTc0YjNkMjEyZTMxZDQ0N2FjYzY0YmU3NzQzODcxZjUzYjU0MjlmMGFlNzI4YzJiNDcyZjQxZiIsInZvdXQiOlt7InZhbHVlIjoxLjM5M2UtMDUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIDYzYTM0NmFjNzAwZDJhOGMxOWI3ZWY3MzRiYmQ3NzcxNDk2ZWQ0M2MgT1BfRVFVQUwiLCJoZXgiOiJhOTE0NjNhMzQ2YWM3MDBkMmE4YzE5YjdlZjczNGJiZDc3NzE0OTZlZDQzYzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHAzNngzNHZ3cXhqNHJxZWtsaGh4amFhd2FjNWptazU4c2MzeGZ1d3FyIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYTA3ZTZlNGQzYzg2NTc3MTIyYmFjZGE2NDdkZGZjY2FlNDFhMmMwN2NkMGMyYzljMWE5OTQ4NTFhZTYzYWJhNCIsInZvdXQiOlt7InZhbHVlIjowLjAwNTcxMjU4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMWRlZTM3OThhNGUwNjA0YzRlM2QxZjA3NDAxNDYyZTIxZDgzMjE1ZDg1NzJhYjEwZGZlM2UyNDI2ZTEzN2NmNSIsInZvdXQiOlt7InZhbHVlIjowLjIxMjgyOTY5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODE5OTZlYmMyZmM3MmZjYTc5NjhlYzgwZjI0NTNiZmNmNGM4Y2RhZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDgxOTk2ZWJjMmZjNzJmY2E3OTY4ZWM4MGYyNDUzYmZjZjRjOGNkYWU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpxZWptNHU5bHJqbGpuZWRya2dwdWo5ODA3MGZqeGQ0Y2Z0emF4MjIzIl19fV19",
+ "eyJ0eGlkIjoiMWYwODg5NmYwMTUzOTI2NmExYTY0ZDljMmJjZDZmNTBkNmI3ZWM3NTZhNTM0YzVhZWI3NGQ2ZjMxNWY1NGE5NCIsInZvdXQiOlt7InZhbHVlIjowLjIxMTg0NDQ0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODE5OTZlYmMyZmM3MmZjYTc5NjhlYzgwZjI0NTNiZmNmNGM4Y2RhZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDgxOTk2ZWJjMmZjNzJmY2E3OTY4ZWM4MGYyNDUzYmZjZjRjOGNkYWU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpxZWptNHU5bHJqbGpuZWRya2dwdWo5ODA3MGZqeGQ0Y2Z0emF4MjIzIl19fV19",
+ "eyJ0eGlkIjoiM2E3OGJlODg1YmI4ZGVkZTE0ZWVhOWRmZWJkMDA4ZTYxZjAwZjhhMzkzZGFiOWNjYWJjMjBkMzUyODgwYWQyNiIsInZvdXQiOlt7InZhbHVlIjowLjExNTQ0NDY4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDUwMTNmNTliNDU3YWQ2MGVjODExMTZmMWJhNGM3YjBlYzg2ZjZkMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQ1MDEzZjU5YjQ1N2FkNjBlYzgxMTE2ZjFiYTRjN2IwZWM4NmY2ZDE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXIyc3owNmVrM3Q2NmM4dnN5Z2s3eGF5Yzdjd2VwaGs2eTM5ZXp3bHNnIl19fV19",
+ "eyJ0eGlkIjoiNjg1NzVmZGYzNWEwODQzMjAwZjg1MzkwNWQ2MDU5NTg0YTI4NDEzYjcxN2E1MTk0NzViMGU3NThiYmY4MWRhYSIsInZvdXQiOlt7InZhbHVlIjowLjExMzIxMTgxLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTYwNmYwZmJjYzgxMDgyMWY2NGNiNzhkOTAzNWM3ZGNiZDdmNDk4ZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU2MDZmMGZiY2M4MTA4MjFmNjRjYjc4ZDkwMzVjN2RjYmQ3ZjQ5OGY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB0cWR1OG1lanFzc2cwa2ZqbWNteXA0Y2x3dDZsNmYzdXJhY3phaHF5Il19fV19",
+ "eyJ0eGlkIjoiNmFjNWYzMjg4ZjE2NjJmMWJhNTExYjU4ODFlNmM3MDE2MzQzZTI4OTFmMWEwNDEyM2NjN2M5YjM3MGI5ODVkZiIsInZvdXQiOlt7InZhbHVlIjowLjExNzEyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMzc3MDZjZGYyNGMxY2Y5YmVlNjNiNWNmZTg2MWE4NzA3NTIyNWQ0NyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDM3NzA2Y2RmMjRjMWNmOWJlZTYzYjVjZmU4NjFhODcwNzUyMjVkNDc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFtaHFteGx5bnF1bHhsd3Z3NnVsNnJwNHBjODJnamFndW51MzVobGR2Il19fV19",
+ "eyJ0eGlkIjoiNzNkNWEzZTI0ZjJmMWQwYjg3NTExMDgwNmQ1ZDIxN2IyZDU5NDc3ZGI5ZjE0MGE3Mjg5MWZkMWYzZmZkODMxNCIsInZvdXQiOlt7InZhbHVlIjowLjIwNzkwMjUxLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODE5OTZlYmMyZmM3MmZjYTc5NjhlYzgwZjI0NTNiZmNmNGM4Y2RhZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDgxOTk2ZWJjMmZjNzJmY2E3OTY4ZWM4MGYyNDUzYmZjZjRjOGNkYWU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpxZWptNHU5bHJqbGpuZWRya2dwdWo5ODA3MGZqeGQ0Y2Z0emF4MjIzIl19fV19",
+ "eyJ0eGlkIjoiODhmNGY2MWNlNmU0NTg0OWY3ODczMGU0NGM5YmNhYTg2ZmViNzdjNmU5ZDhmNjBhN2QzZGVjYWY2NGM1YzJlOSIsInZvdXQiOlt7InZhbHVlIjowLjExMzA1ODIyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYWNjODJmN2Q0NjE5OTZkNzQxNmFiNzY0MjEwNjcwMWVkZDc3MzY5NSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGFjYzgyZjdkNDYxOTk2ZDc0MTZhYjc2NDIxMDY3MDFlZGQ3NzM2OTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXprdnN0bWFnY3ZlZDQ2cGQybWtnZ2d4d3EwZDZhZWtqNWdrNjJ3MHpoIl19fV19",
+ "eyJ0eGlkIjoiOGY3NWM4NmY4NGJlN2VkY2YyNzk5ZjNmZDFjMjgwZTdiYWQ3NGE5MzUwOGY4OTgyYWM3MzEwZDBhMGVkZGIxYyIsInZvdXQiOlt7InZhbHVlIjowLjA1NTM1MDg0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZGZkODQyMTVmY2I1ODg0ZDQ0NDRkMDU2ZTMyY2EyYjIzZDY4OWQwMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGRmZDg0MjE1ZmNiNTg4NGQ0NDQ0ZDA1NmUzMmNhMmIyM2Q2ODlkMDI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXIwYXNzczRsajZjc24yeWduZzlkY2V2NTJlcjY2eWFxZ2h4dnRodzlxIl19fV19",
+ "eyJ0eGlkIjoiYTM1MGViZmU3MDU1ODRlZTMyYTMyMWNjYWZmNWJmMjE4MTk5NTk3ZWViNjJlN2FlNDA3OTQxYzM4OWVhODk2YyIsInZvdXQiOlt7InZhbHVlIjowLjExMjY0MzA2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjI2ZTI5YjFlZTY1M2M5MzVlZmZhMmQ5NDJhYmMyODg1MTM3ZWRjNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDIyNmUyOWIxZWU2NTNjOTM1ZWZmYTJkOTQyYWJjMjg4NTEzN2VkYzY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXEzeHUyZDNhZWpuZXk2N2w3M2RqczR0YzJ5OXpkbGRjY2h2YTVkdnBkIl19fV19",
+ "eyJ0eGlkIjoiYjAxMjA0YTg5YjY0MzJjNWU0MTgxMmNmMzk5OThlZWI2ZGRlYTRkMTJhYWM0M2MzNmRjMzRiY2NlMWQ0OWE0YSIsInZvdXQiOlt7InZhbHVlIjowLjQ0NTcxNTA2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMzhmMGNiZGExZjRiZTM1NmZiNzhkNGEzMDQzMmVlNGIxNTY5OTc0NyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDM4ZjBjYmRhMWY0YmUzNTZmYjc4ZDRhMzA0MzJlZTRiMTU2OTk3NDc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF1MHBqNzZyYTk3eDRobTByMjJ4cHBqYWU5MzI2dmhndXZ1a3pmNWV1Il19fV19",
+ "eyJ0eGlkIjoiZjlhZDhlNjU1ZTIxZTA1ZTRhMjhjMGViNGRlMDk0NGRkMDAwMWJmNWY3MWJkMzY1MTI5YThiMTBkNmUxZjJmNCIsInZvdXQiOlt7InZhbHVlIjowLjIxNTU3OTc1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODE5OTZlYmMyZmM3MmZjYTc5NjhlYzgwZjI0NTNiZmNmNGM4Y2RhZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDgxOTk2ZWJjMmZjNzJmY2E3OTY4ZWM4MGYyNDUzYmZjZjRjOGNkYWU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpxZWptNHU5bHJqbGpuZWRya2dwdWo5ODA3MGZqeGQ0Y2Z0emF4MjIzIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMDRjNzg2N2I2NmU5Y2QyZmYwN2VjNzAzYzdkYjVmOWJlMDc0MTI0MTczYjkxODVhNWEwOTUzM2QwMjAzZWUyYiIsInZvdXQiOlt7InZhbHVlIjowLjk5Mjg5MTA3LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYzI5ZmIxZWM1MTlmZjYyN2QwYjYwNTg2YTQwMWZhOWQ0YjIyMjAzYiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGMyOWZiMWVjNTE5ZmY2MjdkMGI2MDU4NmE0MDFmYTlkNGIyMjIwM2I4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJwZmx2MHYyeDBsdmY3c2tjemNkZnFwbDJ3NWtnM3E4djl4cGMwZGc3Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNTZmMWU3OTFmZTZkZmQwNTBjODZlZjljMjMxOGUwYjkzYjFiYjRlODE0YTExMjU1MjQyNGE1NTIzNWU3NTU3NiIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY3OTc4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMjBiM2M5MGUyZWU3MDkzNzY5MWU0ZWNlZWIzNWM0MmM1ZDMwN2I5NTAxOGEwYjk4MzQ1NjJhNTFlNDQ0MGExOSIsInZvdXQiOlt7InZhbHVlIjoxNy4yNzk1NDU0OSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhNmVlMDRjYjc3Mjg3NTlhOGRlZTZiZWQzYTc4MTMxN2EzY2UzZDJlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6bnd1cHh0d3U1OHR4NWRhZTQ3NnduY3p2dDY4bjNhOWNtNG1yOXB0bSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNTU0MWYyODczZWI0ZWRjY2IzYmVjN2UzZDkwNjM4YTNhYWRjMzY4MmU1N2I4MjRmOTJhNjdiNmM5NWI4NjE0NyIsInZvdXQiOlt7InZhbHVlIjowLjAwMDE3MjU5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYzJhZGUyYWQzYzk1OTlkYWMzYWYxYjc3NDUxZTgzZDUwZjIwNzFlMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGMyYWRlMmFkM2M5NTk5ZGFjM2FmMWI3NzQ1MWU4M2Q1MGYyMDcxZTI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJwMm1jNGQ4ajJlbmtrcjR1ZGh3M2c3czAyczdncjN1Z3B4aDM0NnQ4Il19fV19",
+ "eyJ0eGlkIjoiZjlkZDIxOTk5NTU1NTVhNjRjMTczNzY4MDM0NTMxZDQ5MTcxMDg0ZmJhZDM5MWZkYzU3MmZkZWI3ZDUyNjQ4OSIsInZvdXQiOlt7InZhbHVlIjoxLjA0ODE1MDc2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYzJhZGUyYWQzYzk1OTlkYWMzYWYxYjc3NDUxZTgzZDUwZjIwNzFlMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGMyYWRlMmFkM2M5NTk5ZGFjM2FmMWI3NzQ1MWU4M2Q1MGYyMDcxZTI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJwMm1jNGQ4ajJlbmtrcjR1ZGh3M2c3czAyczdncjN1Z3B4aDM0NnQ4Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNjljNTI5Nzg1ZWIyNGU4ZTkxNzRjNzM1OGZlNjA5ZTQzNTJiMDkxYjA4NTg1YTYzMmMwZjAzMzYxYWVhZTllZSIsInZvdXQiOlt7InZhbHVlIjoxZS0wNSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYjZlZjI5ZGE1MjQ5N2EyNDNhNzNhNzAxMTVkZWRlNmUzODY1MzE0OGNjZGJhYWZhMjRmN2Y2NjJiOGJjNWIzNyBPUF9FUVVBTCIsImhleCI6ImFhMjBiNmVmMjlkYTUyNDk3YTI0M2E3M2E3MDExNWRlZGU2ZTM4NjUzMTQ4Y2NkYmFhZmEyNGY3ZjY2MmI4YmM1YjM3ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd213NzJ3NjJmeWg1ZnA2d3duc3o5dzdtZWhyc2VmM2ZyeGRoMmg2eW5tbHZjNGNoM2Rud3p1dGx3ZXVrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiI1YTRmNmIyNTI0M2MxYTJkYWJiMjQzNGUzZDllNTc0ZjY1YzMxNzY0Y2UwZTdlYjQxMjdhNDZmYTc0NjU3NjkxIiwiYW1vdW50IjoiMCIsIm5mdCI6eyJjYXBhYmlsaXR5IjoibWludGluZyIsImNvbW1pdG1lbnQiOiIwMGNhOWEzYjAwMDBlMWY1MDUwMDAwNWE2MjAyMDBiNTgzMGUwMGE2YmI1NGE5NzAyOTQ2MDAxZjE0ZDMzYzhmMTg3ZDlmMjQzNDMzNjgifX19XX0=",
+ "eyJ0eGlkIjoiOTdkYmQ2M2YxMzhlMTQyNTFjNjdmYmNhM2NhY2Q3MzAyNzgxYTRlNjY5ODY3NzJkNjM3MzM1MmI4OTg0OThkOCIsInZvdXQiOlt7InZhbHVlIjoxZS0wNSwibiI6NzYsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGI2ZWYyOWRhNTI0OTdhMjQzYTczYTcwMTE1ZGVkZTZlMzg2NTMxNDhjY2RiYWFmYTI0ZjdmNjYyYjhiYzViMzcgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYjZlZjI5ZGE1MjQ5N2EyNDNhNzNhNzAxMTVkZWRlNmUzODY1MzE0OGNjZGJhYWZhMjRmN2Y2NjJiOGJjNWIzNzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdtdzcydzYyZnloNWZwNnd3bnN6OXc3bWVocnNlZjNmcnhkaDJoNnlubWx2YzRjaDNkbnd6dXRsd2V1ayJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiNWE0ZjZiMjUyNDNjMWEyZGFiYjI0MzRlM2Q5ZTU3NGY2NWMzMTc2NGNlMGU3ZWI0MTI3YTQ2ZmE3NDY1NzY5MSIsImFtb3VudCI6IjAiLCJuZnQiOnsiY2FwYWJpbGl0eSI6Im5vbmUiLCJjb21taXRtZW50IjoiMjIwMSJ9fX1dfQ==",
+ "eyJ0eGlkIjoiZDFkMTBmODU0ZTNkY2Q3NjUyZTg1YTk1OThlM2RjODI1MGY5NDgwN2VjN2QzNzlkZjFhNGFjMWY4M2YzYWIxOCIsInZvdXQiOlt7InZhbHVlIjo4LjJlLTA1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODY0MDM2NTdmMGM3YjE3ODllYjIzNDcyZjEwNzI1MDYxOTQwZjdiNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpyeXFkamg3cnJtejd5N2tnNjg5dWc4eTVycGpzOGhrczVtbWV4YTQzIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiI1YTRmNmIyNTI0M2MxYTJkYWJiMjQzNGUzZDllNTc0ZjY1YzMxNzY0Y2UwZTdlYjQxMjdhNDZmYTc0NjU3NjkxIiwiYW1vdW50IjoiMCIsIm5mdCI6eyJjYXBhYmlsaXR5IjoibWludGluZyIsImNvbW1pdG1lbnQiOiIifX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMjg4OTlkNjgxMTVlOWZkYThlZWFiYmM0NWEwMDg5NjVkNzQ3MzU5NGRkZmFmMWYyYTFkNGU2Yjc5YzhlYmYyZiIsInZvdXQiOlt7InZhbHVlIjowLjAwMzk0MzYyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZmE5MDg1YTZjM2ZhMzcyNmMyMjg4NWE4YWY5ODFlZmIzYTIxNjJlNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGZhOTA4NWE2YzNmYTM3MjZjMjI4ODVhOGFmOTgxZWZiM2EyMTYyZTY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJhZnBwZHhjMGFyd2Zrejl6ejYzdHVjcm1hbjVndHp1Y3kybTcyaGdkIl19fV19",
+ "eyJ0eGlkIjoiODI0MzllM2ViMWViNjI1OWFhY2FhMThlM2E5YmU5NTY1YTJkMzdkODlhMjQ4MjUzZWUxM2Q5MGI5YTlhMDJiYiIsInZvdXQiOlt7InZhbHVlIjowLjAwNjQzMDExLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZmE5MDg1YTZjM2ZhMzcyNmMyMjg4NWE4YWY5ODFlZmIzYTIxNjJlNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGZhOTA4NWE2YzNmYTM3MjZjMjI4ODVhOGFmOTgxZWZiM2EyMTYyZTY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJhZnBwZHhjMGFyd2Zrejl6ejYzdHVjcm1hbjVndHp1Y3kybTcyaGdkIl19fV19",
+ "eyJ0eGlkIjoiYmI1ZGVjZThjNDM4MjIwZjNlYTY3YTM5YTEwMzkxMGVlZTVmNWExZGE0ODM5ZTg0ZDNhYjk5ODMzM2Q4MWNjYSIsInZvdXQiOlt7InZhbHVlIjowLjAxNTA5NjQzLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNGRkOWQ4YjQ2YmQyN2NkNGZmMTExOThkMjRjZGQ3MGRlMjM5ZWJjMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDRkZDlkOGI0NmJkMjdjZDRmZjExMTk4ZDI0Y2RkNzBkZTIzOWViYzA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB4YW5rOTVkMGY4ZTQ4bHp5dmM2ZnhkNnV4N3l3MHRjcTI1MGF5djZlIl19fV19",
+ "eyJ0eGlkIjoiNmUyMTUyMDgyYTc5YjJkYmU5N2NhZWZlODkwOTcyODZlZjY3ZWE4YTA3YWRhZDI2MTU4ZGIzYWUxNzgyMTEwZiIsInZvdXQiOlt7InZhbHVlIjowLjAyMDI3MTQ5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMzNjZmRlOTM1NDZkZTNiNWRhZmFhZTEwYjgzZDdjMjUyNTFiOWVhMyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDMzY2ZkZTkzNTQ2ZGUzYjVkYWZhYWUxMGI4M2Q3YzI1MjUxYjllYTM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFldWxoNW4yM2s3OGR3NmwyaHBwd3BhMHNqajJ4dTc1dnBscHFuZWtnIl19fV19",
+ "eyJ0eGlkIjoiODk3YzNlMWY0YjA0NDdjZTMwZjQyOTNlMDE1NDE5YTFlYjNiM2NiZmQ3ZTVhNzY2ZjE0OWE5ZWQyMTJkM2UwMSIsInZvdXQiOlt7InZhbHVlIjowLjAyMjE3MDc4LCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMmU5NmVmOTExYmQzODk2MmU0NGRmZTVhZWI1MmMyYzlhMWY1OTExMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDJlOTZlZjkxMWJkMzg5NjJlNDRkZmU1YWViNTJjMmM5YTFmNTkxMTA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFoZmRtdTNyMGZjamNoeWZobDk0NjZqY3R5NnJhdjN6cTAzc3ozaGc5Il19fV19",
+ "eyJ0eGlkIjoiYWNjMWNmNjZjZDRlNWQxMTM1NzJhZjZjMDRjZDFhYzUzYjlmYThmYTkzNWI4N2ZmMWE2NjI0MzU5M2NiMzliNiIsInZvdXQiOlt7InZhbHVlIjowLjAzNzg1ODE0LCJuIjo1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMzc4MWMzYjA2MDY1ZWFlNDY0MGE5MjYwMjI3NWQ2YWIyZmMxYzRkNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDM3ODFjM2IwNjA2NWVhZTQ2NDBhOTI2MDIyNzVkNmFiMmZjMWM0ZDY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFtY3JzYXN2cGo3NGVyeXAyZnhxZ240NjY0amxzd3k2Y2R0NDAzdmcwIl19fV19",
+ "eyJ0eGlkIjoiZWRhN2Y5MTBjZGNiOWIyZmZkMDc2ZTRiZmU5ODliMDJiMmJiMmRmOGRmZjYzNmEzYWY2NTAyNDY1ZDYwYjlmNiIsInZvdXQiOlt7InZhbHVlIjowLjA0MDU2OTE3LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgM2RlNzI0Y2U5MDNkZjY2ZDBkMTdjYzIwNjZjYTQxOWY1YTNhZjU0ZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDNkZTcyNGNlOTAzZGY2NmQwZDE3Y2MyMDY2Y2E0MTlmNWEzYWY1NGQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXE3N3dmeHdqcTdsdm1nZHpseHpxZWsyZ3gwNDV3aDRmNXgzemRmZHd2Il19fV19",
+ "eyJ0eGlkIjoiZTUyM2UyNzQ2NTJlNjg3ZDc2MzliNTJiNjNkODg3YzAyMjA5MGQ0ODNhYWJmM2Q4Yzg3Y2NjYTUwZTI5YTUyNyIsInZvdXQiOlt7InZhbHVlIjowLjA0MjUzNzgxLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOWI0ZDY3MDZiNTg3ZjNiZWQ1MzFiMDcxOGY2MzAwYjYxNjdmYTJiZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDliNGQ2NzA2YjU4N2YzYmVkNTMxYjA3MThmNjMwMGI2MTY3ZmEyYmY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpkNTZlY3hra3JsODBrNHh4Yzhycm1ycXptcHZsYXpodXAyN25mdDR0Il19fV19",
+ "eyJ0eGlkIjoiM2EyMTNhOWM5NGM0NDcwMDFiNjY5MjM4ZThmYzJlM2ZmZDIxNTIyOGI2M2E3YTA1NzJkZDU0NWFiMWQxMWZiNSIsInZvdXQiOlt7InZhbHVlIjowLjA0NDU2NTM0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjkyMTNmMDMzNGVjZDJmZDk3NjBjYzE1MWY3N2UzNmY2Y2NiNjVkNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDI5MjEzZjAzMzRlY2QyZmQ5NzYwY2MxNTFmNzdlMzZmNmNjYjY1ZDY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXE1anowY3J4bmtkOWx2aHZyeHAyOG1odWRoa2VqbTk2Y2Q3MDB2bTd1Il19fV19",
+ "eyJ0eGlkIjoiZjk4YjIwMzAxZDYyY2YyZDdhNDE5NTUzNDE5YzEzNDFmYThjZThkNzc2NjFkMWE3NWY1MjkyYWI2MDY2Y2UzOSIsInZvdXQiOlt7InZhbHVlIjowLjA1OTE4OTE3LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjkyMTNmMDMzNGVjZDJmZDk3NjBjYzE1MWY3N2UzNmY2Y2NiNjVkNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDI5MjEzZjAzMzRlY2QyZmQ5NzYwY2MxNTFmNzdlMzZmNmNjYjY1ZDY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXE1anowY3J4bmtkOWx2aHZyeHAyOG1odWRoa2VqbTk2Y2Q3MDB2bTd1Il19fV19",
+ "eyJ0eGlkIjoiYzc0YmFmMmRiYWQzNjI4NTljMzg0NTQ3ZWVkNWI4NzQ4OTFiMmU0NjIyMGYzY2EyOWMwNDljYTc4ZDI2MjkwNyIsInZvdXQiOlt7InZhbHVlIjowLjA4MjcyOTg5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMWNjNmE2MzhiYmNlZmI3OWJjNzc3YjIxMTc4NmQ3ZTJhYTYwYTQzZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDFjYzZhNjM4YmJjZWZiNzliYzc3N2IyMTE3ODZkN2UyYWE2MGE0M2Q4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF3dmRmM2NoMDgwazdkdXdhYWp6OXV4NmwzMjVjOXk4NXIybG56NjN4Il19fV19",
+ "eyJ0eGlkIjoiMjE3N2M2NzQyNGEzNjBlMzBjNzg5ZWRkOTc1OTA2Yzk3MzQzNGM1NjE3Y2EwYTU2MWUxOWU0YjlhNDI3NDRjYSIsInZvdXQiOlt7InZhbHVlIjowLjA5NzQzMDAxLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjJhNzEyNzA4NGFjZjE2YmQ4OTQxODIwYWQzMmJkYWYyMzE3MDczMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGIyYTcxMjcwODRhY2YxNmJkODk0MTgyMGFkMzJiZGFmMjMxNzA3MzE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXplMnd5bnNzamswejY3Y2pzdnpwdGZqaGtoang5Yzh4eTJzenRjbjBhIl19fV19",
+ "eyJ0eGlkIjoiMDBmNDJmOTk2MmRmZDExMGFiZGJhZTBhYTEyNTA3ZTI2OGFjODI2NDE5MTQxZGVjOTRjYmUwMzYwNjcyMTg0OCIsInZvdXQiOlt7InZhbHVlIjowLjIyMjUzMDc5LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYjRkZmQ2MjIwNGEwNzZlNGIyNWY2YjhjOGRkOGE1ZWFkNmE4MjQwYiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGI0ZGZkNjIyMDRhMDc2ZTRiMjVmNmI4YzhkZDhhNWVhZDZhODI0MGI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXo2ZGw0M3pxanM4ZGU5anRhNGNlcndjNWg0ZGQycHlwdm1wOGgwZGNqIl19fV19",
+ "eyJ0eGlkIjoiM2Q0MjExOTUyNTc4ZjE2MjZhNzIyNzViMzZmMzZjZTMzN2RlNDc1MGE2MTg2ZjY3YjE5MDRjMDkyYzFiMzNjMSIsInZvdXQiOlt7InZhbHVlIjowLjIzMDYwNTQ1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNjU4NTQ2MmM2YmI4YmExNzczOTYxNWRiMjgxNTEwMzY3ZDgzZmIyMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDY1ODU0NjJjNmJiOGJhMTc3Mzk2MTVkYjI4MTUxMDM2N2Q4M2ZiMjI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBqYzIzM3Zkd3V0NTltbmpjMmFrMnE0enFtOG1xbG15Z3R6NGxmNGZ4Il19fV19",
+ "eyJ0eGlkIjoiODljOTFjNTI3OTNlYjM2ZGVmMzFlMDNhZmVhMjYyYzIwMDIyZjg0MjNlNGJhZjk5ZjViOWE4MmYxMzMwOTk5MyIsInZvdXQiOlt7InZhbHVlIjowLjI1NTkyNjU0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTdmZmUwYWEzNGVmMjJiMzdhYWY4NmY2YjA5NWE4NDczOTIzMThmZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU3ZmZlMGFhMzRlZjIyYjM3YWFmODZmNmIwOTVhODQ3MzkyMzE4ZmY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB0bGxjOTJ4bmhqOXZtNjQ3cjBkdnk0NHBybmpnY2NsdXh1eHlndDYwIl19fV19",
+ "eyJ0eGlkIjoiMmZmMzY4ODIwMzliYjNlN2I3MmVmZDE5ZGExMGU3ODEyMzJkMWRlZDczYWU4MzczYjEwZWRmZmVhMjI3NjljOCIsInZvdXQiOlt7InZhbHVlIjowLjM2NDI2NTAyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTdmZmUwYWEzNGVmMjJiMzdhYWY4NmY2YjA5NWE4NDczOTIzMThmZiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU3ZmZlMGFhMzRlZjIyYjM3YWFmODZmNmIwOTVhODQ3MzkyMzE4ZmY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB0bGxjOTJ4bmhqOXZtNjQ3cjBkdnk0NHBybmpnY2NsdXh1eHlndDYwIl19fV19",
+ "eyJ0eGlkIjoiMTgzNGRjOWRhYjg4NDM3ZWEyZmY2MDlmNzA4MWYzOTA3YTA4ZDYyYWU4M2M4NWRmMjI1MmMxOTkwZmNkMGIyNyIsInZvdXQiOlt7InZhbHVlIjowLjQzMDE0OSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDhmNjMxYzM1M2I2ZGRiNmZkY2QxYzdlMjQ3ZTk3NmRjMmQ4MzZmYTIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4ZjYzMWMzNTNiNmRkYjZmZGNkMWM3ZTI0N2U5NzZkYzJkODM2ZmEyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6OGt4OHA0OGRrYWttN3U2OHI3eTNsZndtd3ptcW0wNWcyNG5kaG5qcSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYmE0ZWQzMjQzMWZlMDhmYTUyY2NhZWNlN2UyZjFhMDIzZTljMTdkZTBjMGUwNjllNjliY2JmNDE0OWUxYTZmZiIsInZvdXQiOlt7InZhbHVlIjoxNy40MjQxNzc3MSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDc3MTFlYWI2ZmZjYzk5ZTQzNzZhYzM5ZjQwMjJkZTQxM2UxZWEyYTkgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3NzExZWFiNmZmY2M5OWU0Mzc2YWMzOWY0MDIyZGU0MTNlMWVhMmE5ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwbTNyNjRrbGx4Zm5lcGhkdHBlN3Nwem1lcW51ODR6NHl6OTI3cWNjayJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiOTdmODVhNTE5NjYwMzAyODIwMzBhNjE3YjkzYTlkY2I2Mjg1OWJhNWJkYmE5MjdhMWNhY2E1Y2U0YmUwOTFlZCIsInZvdXQiOlt7InZhbHVlIjowLjE3MzQzMzEyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDcxZGM2OWMxNDg3ZGQ0ZTYyZjZiMmVkYzA3NTVhOTIxODYwMDZjNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDA3MWRjNjljMTQ4N2RkNGU2MmY2YjJlZGMwNzU1YTkyMTg2MDA2YzQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFyM20zNXV6anJhNm5uejc2ZXdtc3I0dDJmcHNjcXhjc3R2eXVnOTI5Il19fV19",
+ "eyJ0eGlkIjoiYjMwODAxNmFhMTMyMDAzMjUxY2I0ZTQwNDhiOGVmZmE3NTk0NDU0NDU0N2JlOTUyNWM0NTcwNjBmODhiOTVhYiIsInZvdXQiOlt7InZhbHVlIjowLjIwNDA0Nzk4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDRjOWE5ZTQ1ZTcxZDViOWM4YzFhNmYyZjMwNjM3YTU2YWVkNzRkMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDA0YzlhOWU0NWU3MWQ1YjljOGMxYTZmMmYzMDYzN2E1NmFlZDc0ZDI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF6dm4yMHl0ZWNhdHd3Z2N4bjA5dWN4eDdqazRtdDU2ZzZmYTdnMmdkIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYmQxZjUxNGQ2M2E1YzRlMTM3ZTJlNjJmZTVlMDg3YzI4NTczMDgzODBiYzAxM2M5MDgwM2VkOGFiZDgyMmFmYiIsInZvdXQiOlt7InZhbHVlIjowLjAxMTI3OTQ4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZjU3ZTAyMGY1ZDZlMzU2YTU4YzUwYzJhOTg5MDYzMGE2MjNkOTRlYTkwMDRiNzEzZWRjMzUyYWY0MjhmZjgwOCIsInZvdXQiOlt7InZhbHVlIjo2MTkuNjg1Mzk0ODMsIm4iOjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBkM2VlYjViMjEwOGViMDAwZmVmOTQwODRhZjVkM2QwNjdmMmU4YjljIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZDNlZWI1YjIxMDhlYjAwMGZlZjk0MDg0YWY1ZDNkMDY3ZjJlOGI5Yzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcmY3YWRkanp6OHRxcTg3bDlxZ2Z0NmE4NXI4N3Q1dG5zdzY0YzNzNjIiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNzM1YjVlOWFiODJkYjVmZDY2NWNiZDdmMDExMDVkOGQ3OTEwNzY1YjU3MDczYTQwYmEzZmZiZmEyNTUxZGRlNCIsInZvdXQiOlt7InZhbHVlIjoyLjIxOTE2NDUyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDJiNTc2ZjlhOGI2YjY1MGQyODcxZGU2ZTNhMmQwNjA4NGFiOGI2MCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDAyYjU3NmY5YThiNmI2NTBkMjg3MWRlNmUzYTJkMDYwODRhYjhiNjA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFwdDJhaGU0em10djV4anN1dzdkY2F6NnBzZ2YydXR2cThoZGw0aDIzIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMjg4MmEyNTdhMjhhNzg0MWU4OTZlZjZiYTM3YjlmM2Y0NjA2MzI0ODk0MjU4YzgwZjg5MGU1YjIxNmZhZGU2NCIsInZvdXQiOlt7InZhbHVlIjoxLjUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBhNDk0MTRhZGQwMTY5NTM5YTg4MDRkMTc3ODBkOGNmOTZmMTNiZTFlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0YTQ5NDE0YWRkMDE2OTUzOWE4ODA0ZDE3NzgwZDhjZjk2ZjEzYmUxZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxempmZzk5ZDZxdGYyd2Rnc3B4M3c3cWQzbnVrN3lhN3JjdHl6ODYyNHciXX19XX0=",
+ "eyJ0eGlkIjoiMjAwZmRjYTU2ZTlkOTQ1OTYzYWVhYzAyZmJiNGYzNDIxZmY4NmVhZWFmNjdkZjVjNDAxNzI5Y2UxOWE4NDNlZCIsInZvdXQiOlt7InZhbHVlIjowLjAwMDQ1MTUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxZThhNjE3ZjdhNWQyNjg0YmI3NTlhOGE4NmIwNzkwN2ZiMGY4ZjFmIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MWU4YTYxN2Y3YTVkMjY4NGJiNzU5YThhODZiMDc5MDdmYjBmOGYxZjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcTBnNWN0bDBmd2pkcDltd2tkZzRwNHMweXJsa3J1MHJ1c2U5amduZDYiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMjQ4MWMyODEwMTMzMjk2YWNiZTU0NGU1M2IwMmVmMGIyNWUxYzRiMjU3NzMzMDc3NTE5NWE4OWMwZTAzYWE2MiIsInZvdXQiOlt7InZhbHVlIjo1LjczNjkyNzUzLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCA4NzJkY2FiMzQwYjdhODUwMGIyNTg1NzgxZTUxZTkyMTdmMTFkY2VkIE9QX0VRVUFMIiwiaGV4IjoiYTkxNDg3MmRjYWIzNDBiN2E4NTAwYjI1ODU3ODFlNTFlOTIxN2YxMWRjZWQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB6cmptajRuZ3ptNnM1cXR5a3poczhqM2F5c2g3eXd1YTUyMnM1NzlkYSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNmQ5MGMyMzczOTNiMTk2NzFiNDk2MzJhZTc2ZGJkN2EzZTI4MjMzOThjN2JhZTZmZDM3NGI5NGVlNmZjMmQyMCIsInZvdXQiOlt7InZhbHVlIjozNC42NjM1Njc2MiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDZlYjlhMGExMDc1MjA0ZTNlZmUzZGM3M2M2NTdlZjViYjE2NmRmYmMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2ZWI5YTBhMTA3NTIwNGUzZWZlM2RjNzNjNjU3ZWY1YmIxNjZkZmJjODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwaHRuZzlwcWFmcWZjbDB1MHc4ODNqaGFhZG16ZWtsaHM3M2dnZnd6biJdfX1dfQ==",
+ "eyJ0eGlkIjoiODc2YzkwYjllYWY2NzFlNTY0N2I5OGQwZThhODcxNjNhYTlmNTc4YjEyODFhNTViZmM0NDI0ZmNmZmU3ZjQwNSIsInZvdXQiOlt7InZhbHVlIjo2ZS0wNiwibiI6NDU1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTljZDg4NWYyY2ZmOWNlZTU3ZDUwZjc1ZmE4MTU4YzNmZTllOTQ5MCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGU5Y2Q4ODVmMmNmZjljZWU1N2Q1MGY3NWZhODE1OGMzZmU5ZTk0OTA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI1dW16emw5bmxlZW1qaDY1OGh0NzVwdHJwbGE4NTVqcWpzazhmMjJ5Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiN2ZkOGMzMDQzYWEwZDQ2ODhkNjYzODRhMmUzZDNjMjQ4M2QwZDY0ZTMyZmVkMTM5NmZhZDNmMjU2NjMxM2Q2NiIsInZvdXQiOlt7InZhbHVlIjowLjAzNCwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGQzZTg0NzIwY2I5NzE5ZWEzMDBlOWQ2ZTVmZjRhNzJlNDJhNTQ4ZTMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkM2U4NDcyMGNiOTcxOWVhMzAwZTlkNmU1ZmY0YTcyZTQyYTU0OGUzODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyZjdzM2VxZXd0M242M3NwNndrdWhsNTV1aHk5ZjJndXZxcXhjcDJmbiJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMDMzOTJkNGY5OTQ5NTBiZDliOTM4NDZhMDQ1NjBhNzI1NjI0MTY5NzVkOTUwY2RkMzgxZWE1NjQ2MDVmMTU4OSIsInZvdXQiOlt7InZhbHVlIjowLjAwMjE0NiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDEwMDJjNjlkMjkxMTRhODgxMTFiNDc4MGNmYWUyZjQ0YmVlMTFiYTMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxMDAyYzY5ZDI5MTE0YTg4MTExYjQ3ODBjZmFlMmY0NGJlZTExYmEzODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxZ3E5MzVhOXlnNTR6cTNyZHJjcG5hdzlhenRhY2dtNXY1MmY3ZzB1OCJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMmUwZjliMzc4ZGFhYjdhYzFjOWQ1NGIyNDE0OTI2ZDdmMDk3NjEzZDI2ZTU3Yjk4NzFlZGQ2NWI0ZjIzY2RmNiIsInZvdXQiOlt7InZhbHVlIjowLjM0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZmQ3ODg1NzE0NTExNjUwYWZkZDE3NzI0ZjRmNjAxMDNjMjU2NTA4NiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGZkNzg4NTcxNDUxMTY1MGFmZGQxNzcyNGY0ZjYwMTAzYzI1NjUwODY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI3aDNwdDNnNWdrMnpoYTY5bWpmYThrcXlwdXk0anNzY3VrNXZlN2szIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZDk3ZGM4MWY3YjM1NzkxZWE0YWViNTIyZTYyMzZlY2UyZjMwNDY1MmY2N2QyOTFkNGY0MDA0ZWIzZDExNmE1MiIsInZvdXQiOlt7InZhbHVlIjowLjE4MjU4NywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGZhNmJmZWFlMGI3MTNhZTBmNTJlYzBjYWI0MDJmZGQ3MmQwMWQzMTkgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmYTZiZmVhZTBiNzEzYWUwZjUyZWMwY2FiNDAyZmRkNzJkMDFkMzE5ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyYXhobDR3cGRjbjRjODQ5bXF2NGRxemxodGo2cXducnkyeXo1Zzl4ZiJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNDc4OTZiYmNkZWMzYmE3OTgxZjBhYWRiNzdhNDVhOTEwMWI5MWQzNWEzZjA5ODEwODE3YTcyNjkxMTNjYmY1ZSIsInZvdXQiOlt7InZhbHVlIjowLjIwMDIzMjc1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTg1ZTI1NjA0ZjJlYTlhYjhmNDk5YjY3NWZmMTAyODNlYWFmYTcwNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU4NWUyNTYwNGYyZWE5YWI4ZjQ5OWI2NzVmZjEwMjgzZWFhZmE3MDQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB2OXVmdHFmdWgybjJ1MGZ4ZGt3aGwzcTJwNzR0YThxc3gybXA1OGFjIl19fV19",
+ "eyJ0eGlkIjoiYzEyNWI1NjUyMDUxYjk1YWIxMjA1M2NiOGE0NjE3OWM4NmI5ZmVmNWFlYjRlMjc0MWQyMjUyMDExYTVjZGUzYSIsInZvdXQiOlt7InZhbHVlIjo1LjQ2ZS0wNiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDQzMDMwYzJkNWVhN2M0MzdhMzc0NWRlYmVhNGNiZWVkYTY3ODBjYmEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0MzAzMGMyZDVlYTdjNDM3YTM3NDVkZWJlYTRjYmVlZGE2NzgwY2JhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwcHN4cnBkdDZudWdkYXJ3M3c3aDZqdmhtazZ2N3F2aGdlNXNnZ2FhZyJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNDMwYzY4NDJmMjA1MzljZjk1ZThkODYxMGU3Zjc1MzM5NWYzZDY0NmM5MDhlMzc4NDc4YjFlMzdiZWM0Nzc3ZSIsInZvdXQiOlt7InZhbHVlIjo4ZS0wNiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDcyYzAzZTNlYjcwNzZhMTQ4NzdmNjM5OWYwMmNlMzJhNTJkM2UzN2QgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3MmMwM2UzZWI3MDc2YTE0ODc3ZjYzOTlmMDJjZTMyYTUyZDNlMzdkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwZXZxMDM3a3VyazU5eTgwYTNlbnVwdnV2NDk5NWxyMDV2NjdhdXpoeiJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiMDkzNjFkYzMwMWI0MDNhMjA5NjgyYmMyMDBiNjEyYTVmNTA3ODdjNGU3MzdjOTQ0Mjk2ZmQ5N2FhMDNjZmZmNiIsImFtb3VudCI6IjAiLCJuZnQiOnsiY2FwYWJpbGl0eSI6Im5vbmUiLCJjb21taXRtZW50IjoiIn19fV19",
+ "eyJ0eGlkIjoiNDMwYzY4NDJmMjA1MzljZjk1ZThkODYxMGU3Zjc1MzM5NWYzZDY0NmM5MDhlMzc4NDc4YjFlMzdiZWM0Nzc3ZSIsInZvdXQiOlt7InZhbHVlIjowLjAxMjg0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA4Mzc1MTY0ODZlYzg2MjVmNzYzNzhmODg0MDUyOGNmMjFlYjEyNWQ2ZGE5OTI0N2I5NDhmOGY1MGJjZjk0NGIxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDgzNzUxNjQ4NmVjODYyNWY3NjM3OGY4ODQwNTI4Y2YyMWViMTI1ZDZkYTk5MjQ3Yjk0OGY4ZjUwYmNmOTQ0YjE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3cGgyOWpnZG15eHlobWt4Nzhjc3N6ajNuZXBhdmY5Nm1kZmpmcm1qajhjNzU5dWw5enR6NHhzYzBuZTQiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6IjAxNzExZTM5ZTdiZjNiOGNhMGQ5YTZmYzZlYTMyZTM0MGNhYTFkNjRkYzdkMWRjNTFmYWUyMGZkNjY3NTU1NTgiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJtdXRhYmxlIiwiY29tbWl0bWVudCI6IjA5ODBhODY5MDAwMGU4MDM2MzAwIn19fV19",
+ "eyJ0eGlkIjoiNDMwYzY4NDJmMjA1MzljZjk1ZThkODYxMGU3Zjc1MzM5NWYzZDY0NmM5MDhlMzc4NDc4YjFlMzdiZWM0Nzc3ZSIsInZvdXQiOlt7InZhbHVlIjowLjg2MzMzNzc2LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNzJjMDNlM2ViNzA3NmExNDg3N2Y2Mzk5ZjAyY2UzMmE1MmQzZTM3ZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDcyYzAzZTNlYjcwNzZhMTQ4NzdmNjM5OWYwMmNlMzJhNTJkM2UzN2Q4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBldnEwMzdrdXJrNTl5ODBhM2VudXB2dXY0OTk1bHIwNXY2N2F1emh6Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMjBiM2M5MGUyZWU3MDkzNzY5MWU0ZWNlZWIzNWM0MmM1ZDMwN2I5NTAxOGEwYjk4MzQ1NjJhNTFlNDQ0MGExOSIsInZvdXQiOlt7InZhbHVlIjoxNy4yNzk1NDU0OCwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGE2ZWUwNGNiNzcyODc1OWE4ZGVlNmJlZDNhNzgxMzE3YTNjZTNkMmUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhNmVlMDRjYjc3Mjg3NTlhOGRlZTZiZWQzYTc4MTMxN2EzY2UzZDJlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6bnd1cHh0d3U1OHR4NWRhZTQ3NnduY3p2dDY4bjNhOWNtNG1yOXB0bSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMTYyODhkZTdmZjg0MDA4MTc4MDkwZjFlZjE0ZGY4ZGM2Yjg5OTc5YmM4YWFiM2Q4NDE3ODk2YWU2NjA5YWFlOSIsInZvdXQiOlt7InZhbHVlIjoxMTc0LjY2NDczMjcyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCA1NjYzMWU0YzNiM2M5MGUwZDQ1ZTg2NzRiYTMyMjYyMWM2N2FhNDc1IE9QX0VRVUFMIiwiaGV4IjoiYTkxNDU2NjMxZTRjM2IzYzkwZTBkNDVlODY3NGJhMzIyNjIxYzY3YWE0NzU4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBwdHh4OGp2OHY3ZnBjeDV0NnI4Znczanljc3V2NzR5dzVhZDUwNjJ4ciJdfX1dfQ==",
+ "eyJ0eGlkIjoiMTA3MWM3NjAyZmJmZDlhNDM4YjBlYTk2Mjg4ZDUxMDUwMmMyZTNjNzQ5ZDhjMThjYzQ5YTgzOGU2MjI4MWM0NyIsInZvdXQiOlt7InZhbHVlIjowLjAwMDEsIm4iOjg3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBiODFmMWEzZjkzMDE4ZTlhYTJkMzg0ZDUxZDE1ZTBmN2U2ZDVmZTYwIE9QX0VRVUFMIiwiaGV4IjoiYTkxNGI4MWYxYTNmOTMwMThlOWFhMmQzODRkNTFkMTVlMGY3ZTZkNWZlNjA4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB6dXA3eDNsanZxY2F4NHo2d3pkMjhnNHVybTdkNDA3dnFoa3NrY2w2eSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMjcyM2EyYmMzY2Y0OTdhMmZjMWY2MzYxMzQ2Yjc3NzllNTYyNzg5NGRmY2Q0OTY4ZDVhNzhhYzNlY2NkZDlkNCIsInZvdXQiOlt7InZhbHVlIjozLjkzNTg5NDksIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMTYwIGNmNTA4ODRlNGRmZTNiMDk2YTM5NGI3MGRlODYwMTI1ZDE0ZDhjYzQgT1BfRVFVQUwiLCJoZXgiOiJhOTE0Y2Y1MDg4NGU0ZGZlM2IwOTZhMzk0YjcwZGU4NjAxMjVkMTRkOGNjNDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHI4NHB6endmaGxya3p0Mjg5OWhwaDV4cXlqYXpudnZjc2NjcjdzcTJtIl19fV19",
+ "eyJ0eGlkIjoiMmRmOTYyMGVlMTdiYTVmMzMzNWI0ZjhlODNlMjJmOGRiNTliYzEzNmY0NzBjMWJkYzBhNTFlMjA2YTYxMjI4OCIsInZvdXQiOlt7InZhbHVlIjoyNS4zODk4MDM1OCwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gxNjAgY2Y1MDg4NGU0ZGZlM2IwOTZhMzk0YjcwZGU4NjAxMjVkMTRkOGNjNCBPUF9FUVVBTCIsImhleCI6ImE5MTRjZjUwODg0ZTRkZmUzYjA5NmEzOTRiNzBkZTg2MDEyNWQxNGQ4Y2M0ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwcjg0cHp6d2ZobHJrenQyODk5aHBoNXhxeWphem52dmNzY2NyN3NxMm0iXX19XX0=",
+ "eyJ0eGlkIjoiNjJkYjA2ZTg3NWY3MjZkZTI5ZDFhNzI4MTQyODg0NTFhYzgwMDU0N2RhNzIyMDI2MjFhZjFjMGEyMGIzMzE2ZSIsInZvdXQiOlt7InZhbHVlIjo5LjkxODQxMTU4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCA2Njc0MTljOGIyN2I4M2UyMDZkNGI3ZjU3YmQxZTYzZWJlYTYxNzNlIE9QX0VRVUFMIiwiaGV4IjoiYTkxNDY2NzQxOWM4YjI3YjgzZTIwNmQ0YjdmNTdiZDFlNjNlYmVhNjE3M2U4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBwbjhneHdna2ZhYzhjc3g2am1sMjc3M3VjbHRhZnNoOGNsa3c2djU4aCJdfX1dfQ==",
+ "eyJ0eGlkIjoiNjJkYjA2ZTg3NWY3MjZkZTI5ZDFhNzI4MTQyODg0NTFhYzgwMDU0N2RhNzIyMDI2MjFhZjFjMGEyMGIzMzE2ZSIsInZvdXQiOlt7InZhbHVlIjoyMy43NTg5NDk3LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCA1MjcwMjc0ZDk0ZjQwYzFhYzNmZWYzMTYxM2MwODEyMjQ1NjBkY2IwIE9QX0VRVUFMIiwiaGV4IjoiYTkxNDUyNzAyNzRkOTRmNDBjMWFjM2ZlZjMxNjEzYzA4MTIyNDU2MGRjYjA4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBwZjhxZjZkam42cWN4a3JsbWUzdnk3cXN5M3kyY3h1a3F6YTJoenE0cCJdfX1dfQ==",
+ "eyJ0eGlkIjoiNjJkYjA2ZTg3NWY3MjZkZTI5ZDFhNzI4MTQyODg0NTFhYzgwMDU0N2RhNzIyMDI2MjFhZjFjMGEyMGIzMzE2ZSIsInZvdXQiOlt7InZhbHVlIjoyOS4yOTYzMzc3NSwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gxNjAgMmJkNjg4ZTU2YzcxOTI1OTcyYzE5OGI2ZTMyNmE3OGI0YzFhNGFlMSBPUF9FUVVBTCIsImhleCI6ImE5MTQyYmQ2ODhlNTZjNzE5MjU5NzJjMTk4YjZlMzI2YTc4YjRjMWE0YWUxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwcTRhZHo4OWQzY2V5a3RqY3h2dGRjZXg1Nzk1Y3hqMnV5anBuenh6dDMiXX19XX0=",
+ "eyJ0eGlkIjoiNjJkYjA2ZTg3NWY3MjZkZTI5ZDFhNzI4MTQyODg0NTFhYzgwMDU0N2RhNzIyMDI2MjFhZjFjMGEyMGIzMzE2ZSIsInZvdXQiOlt7InZhbHVlIjozMS40Mzg4OTgwOSwibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gxNjAgMjMwNjdkOGE1MzkyMjk2NGQ3NWE2MGU5Mzg4NGEzYTg5MWNlODg2YyBPUF9FUVVBTCIsImhleCI6ImE5MTQyMzA2N2Q4YTUzOTIyOTY0ZDc1YTYwZTkzODg0YTNhODkxY2U4ODZjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwcTNzdmx2MjJ3ZnpqZXhodGZzd2p3eXk1dzVmcm41Z2RzM3A0YTBsYXEiXX19XX0=",
+ "eyJ0eGlkIjoiZjNiYmVmNTNmNmJkMTg5MzJmMjgxN2RlNTdiYzNiZGRmYWJjMDZhNzRjMGVlNTRmN2FlMDA1MmE3NjgzOGY5MCIsInZvdXQiOlt7InZhbHVlIjoyMC4xMzI5MjUyLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDE2MCBhMzhiYThlODFkZWNjYjE4ZTRlOTc0Y2U0YjhhNTgyNmEyNTZjNGRhIE9QX0VRVUFMIiwiaGV4IjoiYTkxNGEzOGJhOGU4MWRlY2NiMThlNGU5NzRjZTRiOGE1ODI2YTI1NmM0ZGE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB6M2NoMjhncmhrdmt4OHlhOTZ2dWp1MnRxbjJ5NGt5bWc3dGozdTI2NiJdfX1dfQ==",
+ "eyJ0eGlkIjoiZjNiYmVmNTNmNmJkMTg5MzJmMjgxN2RlNTdiYzNiZGRmYWJjMDZhNzRjMGVlNTRmN2FlMDA1MmE3NjgzOGY5MCIsInZvdXQiOlt7InZhbHVlIjoyMS44NTA2Nzk4MSwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gxNjAgN2RjZmU5N2JhY2EyMDQxYmQ5ZDg5NTAzZDUwNmZlNmZkNmI1ZDY1OCBPUF9FUVVBTCIsImhleCI6ImE5MTQ3ZGNmZTk3YmFjYTIwNDFiZDlkODk1MDNkNTA2ZmU2ZmQ2YjVkNjU4ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwcDd1bDZ0bTRqM3FneDdlbXoyczg0Z3hsZWhhZGR3a3RxeDMyYXV0eDUiXX19XX0=",
+ "eyJ0eGlkIjoiZjNiYmVmNTNmNmJkMTg5MzJmMjgxN2RlNTdiYzNiZGRmYWJjMDZhNzRjMGVlNTRmN2FlMDA1MmE3NjgzOGY5MCIsInZvdXQiOlt7InZhbHVlIjoyMi4yODM1MDY1NiwibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gxNjAgZmI0NjE1OGExNWIxYzBlZDdiN2M0NzQ4Njg1MjMwYzU1ZGZjYmM5MyBPUF9FUVVBTCIsImhleCI6ImE5MTRmYjQ2MTU4YTE1YjFjMGVkN2I3YzQ3NDg2ODUyMzBjNTVkZmNiYzkzODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwcmE1djl2MnprY3VwbXRtMDNyNXM2emp4cno0bWw5dWp2bWVyZTIydnkiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMjZhNzFmNjFiYzE0Zjk2ZWY1MDdmYmY4MTAzNTljMTFhNmQ4ZmRkYzk5MGZhYTYzYjUxNTM4YzFlYTczZTdhMSIsInZvdXQiOlt7InZhbHVlIjowLjAyNjk3OTAyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTcwZTA3MWU2OTAwODYzMDc0MzJjYjVkYjIxODY2YWQxMTk4ODE3ZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDE3MGUwNzFlNjkwMDg2MzA3NDMyY2I1ZGIyMTg2NmFkMTE5ODgxN2U4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF0c3VwYzdkeXFndnZyNXh0OTRtdnNjdjZrM3J4eXAwY2VndmdqNmp5Il19fV19",
+ "eyJ0eGlkIjoiNGNlMGJjZjY5YjQ1YmNkZTlmNWZiYWE1NGI2OTA1NTE1ZTYxYjlmYzg0YTIwYWU3MjJlYWRhYjE0MzQyNzdjOCIsInZvdXQiOlt7InZhbHVlIjowLjAyNDk1Mzk0LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjE5MWE3NThkZjFkYjM3Yzk5M2QxODIxNzNkOTkzYTY0ODY5MjM4ZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDIxOTFhNzU4ZGYxZGIzN2M5OTNkMTgyMTczZDk5M2E2NDg2OTIzOGU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFzZXJmNmNtdXdteGx5ZTg1dnp6dTdlandueXM2ZnIzY3N2bXhqZHR3Il19fV19",
+ "eyJ0eGlkIjoiMjJlMmUwNGM1MGJiMmEwODAyZmY5ZTZjYmRkYzllMzQ2ZmFhMWY2ODRkOTAxY2I2NjY1Y2QxZjhlMTQ3NmFjZSIsInZvdXQiOlt7InZhbHVlIjowLjAyNDgxNywibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDY4M2Q0MDdkNGU5NzdjNDJkMjlmMjAzZmU4NzExZDU0ZTRmNWRhYmQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2ODNkNDA3ZDRlOTc3YzQyZDI5ZjIwM2ZlODcxMWQ1NGU0ZjVkYWJkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwNXI2c3JhZjZ0aGNza2pudXNybDZyM3I0MndmYXc2aDVmbnJ5Z3JrdCJdfX1dfQ==",
+ "eyJ0eGlkIjoiMzZiNWQ2MGQ2YjkxMzlkOWIwZDdmZDg1ZDM1NTMxMzFiN2I1OGQ5MWIwYTM2NmE2NDQ3ODIwYWE5ZmYxMDhjNiIsInZvdXQiOlt7InZhbHVlIjowLjAyMjU5NDY2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZGUwNWM3NjA3Yzc5NjQyNDcwNmNlNWZhZTZhMmRlOWMxZjI1NzJlZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGRlMDVjNzYwN2M3OTY0MjQ3MDZjZTVmYWU2YTJkZTljMWYyNTcyZWQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXIwcXQzbXEwM3VrZ2Zyc2Ruamw0ZTR6bTZ3cDdmdGphNXJ2enlqYzh5Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYjlkNDg0YjA5MGQyZTYzNTRiMGMxZmJiNDQwZGU1MzBlNjA3MzBkNmI5YzJiNjMzOWRiNTU1NzcxOTBiYzZjNCIsInZvdXQiOlt7InZhbHVlIjowLjE2NjEyMDQ1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDAwOTc0MDIyZjE1NDhhNTIyM2UxMGVhNGViYmFlYWY0NjRhY2Y0ZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQwMDk3NDAyMmYxNTQ4YTUyMjNlMTBlYTRlYmJhZWFmNDY0YWNmNGU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJncWphcXo5dTI1M2ZmejhjZ3c1bjRtNDZoNXZqazBmY2N4bjNhYWp4Il19fV19",
+ "eyJ0eGlkIjoiYmY4NjBkM2QzMTI1Y2Y2MTU3NmVhMmUyMjIyOGI0YzQ2M2FlZjFhZDRmYjBmZDFkZTE1MTVjMmVhMGM3YTkyMCIsInZvdXQiOlt7InZhbHVlIjowLjIsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCBkMDA5NzQwMjJmMTU0OGE1MjIzZTEwZWE0ZWJiYWVhZjQ2NGFjZjRlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0ZDAwOTc0MDIyZjE1NDhhNTIyM2UxMGVhNGViYmFlYWY0NjRhY2Y0ZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcmdxamFxejl1MjUzZmZ6OGNndzVuNG00Nmg1dmprMGZjY3huM2FhangiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiM2Q0MjExOTUyNTc4ZjE2MjZhNzIyNzViMzZmMzZjZTMzN2RlNDc1MGE2MTg2ZjY3YjE5MDRjMDkyYzFiMzNjMSIsInZvdXQiOlt7InZhbHVlIjowLjUwODczMzEyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMTc2OTJkNGI3MzBhZmIxYmJjMTNhMGU4MDNjM2QwYTA5ZTUxZDU3MSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDE3NjkyZDRiNzMwYWZiMWJiYzEzYTBlODAzYzNkMGEwOWU1MWQ1NzE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF0a2p0MnR3djkwa3hhdXp3c3dzcTdyNnpzZnU1dzR3eXBuNmU0aDdyIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZThjOTI3NTlmNjQzNTdlMjg4MGJjZGZjNmZjNTcwNGVmZjZkZTljMzRiOGQ4MGU2Nzc0NWRlNmZhNWYzOThjZSIsInZvdXQiOlt7InZhbHVlIjozLjEyOTk5NjMyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMjQ4MjEyZjg1OTdiNTFhN2QzNzFhMzUzNjI3ZDg2YWIzNjdjZjE1YiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDI0ODIxMmY4NTk3YjUxYTdkMzcxYTM1MzYyN2Q4NmFiMzY3Y2YxNWI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFqZ3l5aGN0OWE0cmY3bnd4MzR4Y25hczY0bnZsODN0dmZzZDM5Y2U0Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiM2M4OTU0MDA1ODVkZTBlMjU3MjM1NDdjNTRiYzkxNTZkMmQ5OWUwZGE3ODUzODY5ZjMxMDFiYmJiZTIyYWY2MyIsInZvdXQiOlt7InZhbHVlIjowLjIxMjk2OSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDNlOGNlMjkyN2FmZmRlMjIwZjAzZDM5Mzk0OTE1ZDExYzIwNzU5NmIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQzZThjZTI5MjdhZmZkZTIyMGYwM2QzOTM5NDkxNWQxMWMyMDc1OTZiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxbGdlYzVqMHRsYXVnczBxMGZlODl5M3Q1Z3V5cDZlZHZubGVmZXBkeSJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYmM0MDNiZTE4NDZkYWUwMDNlN2IwYTFjMWM3MWMxMTlkYzU4YTMyNzcwZTE2NDNiNWMxZjg3ZDcwNDNiODljMSIsInZvdXQiOlt7InZhbHVlIjoxLjA2OTc5NTM3LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpnMHkyem5qNXZleDkycTdyc2F1ZndtY3F5bmZ4eDg1NWQ4NjZjeTZ0Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOlt7InZhbHVlIjoxMDMuMzcwNDgxMDIsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnMGM5aGxrZG11Nm5zYTNkeDdsZ200ZDVmenJ3azB1dmN5dWFjOW1lcWswNG11cncza2d5aGY1bW43eCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ4NzUwOTEifX1dfQ==",
+ "eyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOlt7InZhbHVlIjo5Mi4xMTk3NDk3OSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMCBPUF9FUVVBTCIsImhleCI6ImFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjB3ZzlwMGx5NXRmMm5mOXY3NzNoM2Y5eGxyanB3eHFmN25oMjlzNXQ4M2N6NjVzN2t0cWFocjl5YWdkIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDM0NDQ5MSJ9fV19",
+ "eyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOlt7InZhbHVlIjo3MS4wNzU5ODk0NSwibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNiBPUF9FUVVBTCIsImhleCI6ImFhMjAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmswbjA2ZDR2ejZ5ZGZwbXNuemp3cHN5Y2hsYTNtcGdlNWdweDlteHlqMmMwYWh2c3FydmRmeGN2N3lrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzM1MjAzOSJ9fV19",
+ "eyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOlt7InZhbHVlIjozNi4wMTkwNDQ0MiwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTY5ODcwNyJ9fV19",
+ "eyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOlt7InZhbHVlIjoxNy40Nzk2MTgyLCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDIwYjA1Yjc0YWI1NmM5ODQ4MDMyZTliYTRmZDA5NDI3NWNjYzdmMzYyOGFkMTg4NmZjZTU2ZTkzMGQxMjczY2Q4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2c3Rxa201NGR0dm5weXF4dDVtNW43c2pzbjRlbnJseGM1MjZ4eXhsbmprYXljZHpmZXU2OXJleXptcXgiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI4MjQzNjMifX1dfQ==",
+ "eyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOlt7InZhbHVlIjoxMS4zMzUzNzk2NiwibiI6MTQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGIzNDdkNjhjOGQyY2RiMzIwNjZiZTlhZjgwNGM2ZmZiOGFiYmI0NDFiZGYzNmNhZjM4MzQzNmJjZmNhYTM1Y2IgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYjg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdlNTA0NXYzNWtka3ZzeGQwNTZscXp2ZGxhYzR3YTVneDdseG05MDhxNnJkMDh1NGc2dWtuMzcyZ2d3bCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjUzNDU5MyJ9fV19",
+ "eyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOlt7InZhbHVlIjoxMC41MzIxMTExMiwibiI6MTgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDdjMmJjY2EyMDg5YTlmMGY4ZGFlYzBkM2I0NWE2YzUyNTgxYzU2NzY2NWNjNTFlYmRhOWEyMDJkOGEzM2RjZmMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwN2MyYmNjYTIwODlhOWYwZjhkYWVjMGQzYjQ1YTZjNTI1ODFjNTY3NjY1Y2M1MWViZGE5YTIwMmQ4YTMzZGNmYzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGQ3emhuOXpwemRmN3J1ZDRtcWQ4ZHo2ZDNmOXM4emt3ZWp1YzUwdG0yZHpxdHYyeDB3MGM1NHkzOWhwNiJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ5NjcwOSJ9fV19",
+ "eyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOlt7InZhbHVlIjo1LjIxMDk0NzU5LCJuIjoyNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgZjFjMzNiYzdkYTA5OTFkMjZmOGJiZjE1MGM5MDM1NjhiMmMzZGM4NzI3MzcwODYzZjRjM2Q5Zjk2NTViMzc5MSBPUF9FUVVBTCIsImhleCI6ImFhMjBmMWMzM2JjN2RhMDk5MWQyNmY4YmJmMTUwYzkwMzU2OGIyYzNkYzg3MjczNzA4NjNmNGMzZDlmOTY1NWIzNzkxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMGN1eHc3OG1neWVyNW4wM3dsMzJyeXN4NDV0OXM3dXN1bm53enJyN25wYW43dDl0dm1lenJhdTNqdXI5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjQ1NzU3In19XX0=",
+ "eyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOlt7InZhbHVlIjo0LjM5NzU4MzAzLCJuIjozMiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2JiMjRmZjkwYjIyMTlhMjYwZDBlZTBhMTY4NjdmZDMzNDY5Y2Y3MDRmNDkwMjIzMWIxNGU5NjNlY2JhZTNjNCBPUF9FUVVBTCIsImhleCI6ImFhMjAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmFteW5sZXB2M3BuZ25xNnJocTU5NXgwbGZuZzZ3MHdwODVqcTNycnYyd2pjbHZodDN1Z3g0a2FqNGh1Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjA3Mzk2In19XX0=",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjo4ZS0wNiwibiI6NTQsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzMzM1NyJ9fV19",
+ "eyJ0eGlkIjoiY2RjZGY2ZDMxMjY4YjcxODhhODRlZDhjYzIyZGY1Y2M0YWZiY2VkMDY0ZmY1MWE5M2ZhOTExODU5ZDczODY2MSIsInZvdXQiOlt7InZhbHVlIjozNy42MjUzMTY1NCwibiI6NzMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiM2MzNDQ0NTVhZTJkMWEzNDlkYzM4NzZkMGYyYjc5ZTcyOWQ1NmFiNDVjYzdlNDZiZGUxYTA1YmIyYTAwYmQ1ZCIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY2NjY2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMDcxZmMwMTExYTUxMTI3YTA0NWQ5YTM4YmI1MjhlYjVkMThlOGRjMmQ0ZWNmZjE4MTYwYmRmNWNiODA5NjNkZSIsInZvdXQiOlt7InZhbHVlIjoyMjUuMTI5NTEzLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNWVhM2NmZmY5YzY5Mzc0ZDk0ZTk1YjA4MzhhZWE3ODU0OWZiNTQxYSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDVlYTNjZmZmOWM2OTM3NGQ5NGU5NWIwODM4YWVhNzg1NDlmYjU0MWE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXAwMjhubGxuMzVud252NWE5ZHNzdzl3NTd6NW43NjVyZ2VucjNzdXc2Il19fV19",
+ "eyJ0eGlkIjoiZTUxYWI4YWFlOWZmMDg4MGY4NTkxZjE0OGUxYjRhNmQxOTM4Njg0MjQzOTU3NzgzNGQ4MGE1MjE5YmZkNGRhNyIsInZvdXQiOlt7InZhbHVlIjoxNjQxOTUuNTI3MzkxMzEsIm4iOjMsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA1ZWEzY2ZmZjljNjkzNzRkOTRlOTViMDgzOGFlYTc4NTQ5ZmI1NDFhIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0NWVhM2NmZmY5YzY5Mzc0ZDk0ZTk1YjA4MzhhZWE3ODU0OWZiNTQxYTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcDAyOG5sbG4zNW53bnY1YTlkc3N3OXc1N3o1bjc2NXJnZW5yM3N1dzYiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZDRjZmU3MjRlYmY3YWQ0ODE1ZmQ0YWQ5NDI4NTQxZTdhMmEwMTdmMGUwYWUyOWJkNTQ2YTk4MDJjZTgwYTY1ZSIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY0NTM0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNmQ5NTkxYzAzYWRmNDJjZmQ5MWM1ZjJmOTJmMjI2NWZiODQ0YzI2NmUxMDhhNGRmMjE1NjZhNWExZmU2NTE4MyIsInZvdXQiOlt7InZhbHVlIjo1LjQ2ZS0wNiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDRkNjJiZWFhODQyZWU1MmFlOWRmNzIwMDUyZTUyY2FmNDYyMGZlYmUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0ZDYyYmVhYTg0MmVlNTJhZTlkZjcyMDA1MmU1MmNhZjQ2MjBmZWJlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFweGs5MDQyc3NodzIyaGZtYWVxcTVoOTlqaDV2Zzg3aGM4a244OHc1MCJdfX1dfQ==",
+ "eyJ0eGlkIjoiYWE2NzZiMzkzMzA3MGU2MjVkZWVmMzI3YzRjMGE0MzI3NGE5MWYyMDQ5ZjU1MzJlZWExYTZjMjhlYjg1MmM4OCIsInZvdXQiOlt7InZhbHVlIjowLjAwMzQ3NTcsIm4iOjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA5NDgxYTA1OTZhMTBmZmZhMjczZjVjMTQ0ZTBmZTNiNjlhZjk5OTMyIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0OTQ4MWEwNTk2YTEwZmZmYTI3M2Y1YzE0NGUwZmUzYjY5YWY5OTkzMjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxejJncmd6ZWRnZzBsNzM4OGF3cGduczB1d21mNDd2ZXhnZzdrejVxZGoiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYWNiZGQzYjhmMzBmY2EzMWM4NTEyNWE3ZjcwOTgzNTcwMzczY2EyOTE2MDU3YmQ3MTNkMzBjZDZhOTNmN2MyNCIsInZvdXQiOlt7InZhbHVlIjowLjUsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAyOTc2YzE3ODkyNWI4YTQwODk4YWE0NDViMjI1NjhlOWRiZmI5ZTcwIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0Mjk3NmMxNzg5MjViOGE0MDg5OGFhNDQ1YjIyNTY4ZTlkYmZiOWU3MDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcTVoZHN0Y2pmZGM1c3lmMzJqeXR2MzlkcjVhaDd1N3dxdnZ4NHpnbHkiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiODA3OGFjODMwNTljN2JjNTk2YmUxODJjZWQ4YTI0MzNjYzExMmJjYTQzMDkyNmJmNWQxY2IyNDJkZDU3ODg5ZCIsInZvdXQiOlt7InZhbHVlIjozMS42Nzk2NjEzNiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDRmNDIxMWM5MDcwOWVhMWIzM2M2MjQ1MzMyMWY1NmE1MDJlNzZhYjUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0ZjQyMTFjOTA3MDllYTFiMzNjNjI0NTMzMjFmNTZhNTAyZTc2YWI1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwODV5eXdmcXV5NzV4ZW5jY2o5eHZzbDI2anM5ZW0yazVjeWx2dXp0ZyJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYmUzM2RlZmRkYmM1ZTYyNGRiN2YxZTAyZTVjMmVlZmM3ZjQ4NGU3ZDU4OGMxYmZiZmFiOTM4NjQzNjNlNDRhZSIsInZvdXQiOlt7InZhbHVlIjowLjAyMDc4MDA1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNzM3ODZjOGQ4MGY4ZTg4M2JmNWI2NzdlOTZhYzg5MmE2MzliYWJhOCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDczNzg2YzhkODBmOGU4ODNiZjViNjc3ZTk2YWM4OTJhNjM5YmFiYTg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBlaHNteWRzcnV3M3FhbHRkbmhhOTR2M3k0eDh4YXQ0cXd2cno4OGtyIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOlt7InZhbHVlIjoxMDMuNDE4MDQ2NzcsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnMGM5aGxrZG11Nm5zYTNkeDdsZ200ZDVmenJ3azB1dmN5dWFjOW1lcWswNG11cncza2d5aGY1bW43eCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ4NzI4NDIifX1dfQ==",
+ "eyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOlt7InZhbHVlIjo5Mi4xNjIxMzM4NiwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMCBPUF9FUVVBTCIsImhleCI6ImFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjB3ZzlwMGx5NXRmMm5mOXY3NzNoM2Y5eGxyanB3eHFmN25oMjlzNXQ4M2N6NjVzN2t0cWFocjl5YWdkIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDM0MjQ4NyJ9fV19",
+ "eyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOlt7InZhbHVlIjo3MS4xMDg2ODY5MywibiI6Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmNmOWJmNGRhYjA1YTIzNTIxZGMyNjI5MzgzMDI2MmZmZWM3NjE0NjY4ODA5OGJiMzEyNGFjM2ZiNzY0MDAzNiBPUF9FUVVBTCIsImhleCI6ImFhMjAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmswbjA2ZDR2ejZ5ZGZwbXNuemp3cHN5Y2hsYTNtcGdlNWdweDlteHlqMmMwYWh2c3FydmRmeGN2N3lrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzM1MDQ5MyJ9fV19",
+ "eyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOlt7InZhbHVlIjozNi4wMzU2MjU4MSwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTY5NzkyMyJ9fV19",
+ "eyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOlt7InZhbHVlIjoxNy40ODc2NTUwOSwibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZCBPUF9FUVVBTCIsImhleCI6ImFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnN0cWttNTRkdHZucHlxeHQ1bTVuN3Nqc240ZW5ybHhjNTI2eHl4bG5qa2F5Y2R6ZmV1NjlyZXl6bXF4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODIzOTgzIn19XX0=",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjozLjMwNzA4NjM4LCJuIjo5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YW15bmxlcHYzcG5nbnE2cmhxNTk1eDBsZm5nNncwd3A4NWpxM3JydjJ3amNsdmh0M3VneDRrYWo0aHUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxNTUzMTQifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoyLjg3OTg2MDEzLCJuIjoxMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgZWZiYzU1NmQ1Y2ZlOTBhNDU4ZGJmNGQ1N2JkNzg3ZjMzNGJhZmIxYTkzM2ZmYzMzMjIyNjdlYmY0NGMzNGZiMCBPUF9FUVVBTCIsImhleCI6ImFhMjBlZmJjNTU2ZDVjZmU5MGE0NThkYmY0ZDU3YmQ3ODdmMzM0YmFmYjFhOTMzZmZjMzMyMjI2N2ViZjQ0YzM0ZmIwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMGhtYzR0ZHRubGZwZnpjbTA2ZDI3N2hzbGVuZndobXIyZm5sbHBueWduOGEwNnljZDhtcWRhNWV2cHMyIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTM1MjUxIn19XX0=",
+ "eyJ0eGlkIjoiNzk5Mjk5OTRiZmRjMjY3YThlYjYxYWQ0NmI5Njc2NjYwNDc2M2U1NTZhMTQ5MTM3NTM4NjM0NDllOGM1MDE2MyIsInZvdXQiOlt7InZhbHVlIjoyLjE0MDU4NjUzLCJuIjoxOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYTY0YTY1MzA5MTYzMzI5ODA2Mjc1YTVhOTliMjc4NTI0YjIxYTQ2NTU5ZGU0N2VmY2NlMDk4ZDNlYjViMmEwZSBPUF9FUVVBTCIsImhleCI6ImFhMjBhNjRhNjUzMDkxNjMzMjk4MDYyNzVhNWE5OWIyNzg1MjRiMjFhNDY1NTlkZTQ3ZWZjY2UwOThkM2ViNWIyYTBlODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd255NWVmc2o5M245eHF4eWFkOTR4ZGowcGZ5a2dkeXY0dmF1M2wwZW5zZjM1bHR0djRxdTJ4cnMzNndoIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTAwNDA2In19XX0=",
+ "eyJ0eGlkIjoiNzk5Mjk5OTRiZmRjMjY3YThlYjYxYWQ0NmI5Njc2NjYwNDc2M2U1NTZhMTQ5MTM3NTM4NjM0NDllOGM1MDE2MyIsInZvdXQiOlt7InZhbHVlIjoyLjEzNzk1MzgyLCJuIjoxOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMGVmNGZhYWU4MDliOWUzNWJhNzU1ZjE1MTNkY2ZhYTMzYTAzMGRkZGY1NjhiMzU5YTYzN2U2N2NlNWI3Mjg3NiBPUF9FUVVBTCIsImhleCI6ImFhMjAwZWY0ZmFhZTgwOWI5ZTM1YmE3NTVmMTUxM2RjZmFhMzNhMDMwZGRkZjU2OGIzNTlhNjM3ZTY3Y2U1YjcyODc2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjgwZjc0d3N6ZGV1ZGQ2dzQwMzJ5N3VsMjNuNXFjZG1oNmszdjZlNWNtN3ZsODlrdTU4djgya2hmaDk0Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTAwMjgzIn19XX0=",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoyLjUzNzUxMjA2LCJuIjoxMiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYSBPUF9FUVVBTCIsImhleCI6ImFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGdqNjluMnUyOHQwenZnNTBscjN1a210OXZ4cHBnZGNyZjlhcTNjdTU2bGRzcHl4bDR3NWhzZnltOHM3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTE5MTc2In19XX0=",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoyLjU0MjI3ODYyLCJuIjoxMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYSBPUF9FUVVBTCIsImhleCI6ImFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGdqNjluMnUyOHQwenZnNTBscjN1a210OXZ4cHBnZGNyZjlhcTNjdTU2bGRzcHl4bDR3NWhzZnltOHM3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTE5NDAxIn19XX0=",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoyLjUzNzAwNjYsIm4iOjEzLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDUxMmQxNjZhZTI4ZWI3ODk4OGEzZmUzOGYyZGI1OTU4NjA4NTBkYzBkMjVlODIzOGU1MzVmNmMwMjQzN2VhZWE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkZ2o2OW4ydTI4dDB6dmc1MGxyM3VrbXQ5dnhwcGdkY3JmOWFxM2N1NTZsZHNweXhsNHc1aHNmeW04czciXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIxMTkxNTMifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoyLjUzNzUyOTExLCJuIjoxNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYSBPUF9FUVVBTCIsImhleCI6ImFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGdqNjluMnUyOHQwenZnNTBscjN1a210OXZ4cHBnZGNyZjlhcTNjdTU2bGRzcHl4bDR3NWhzZnltOHM3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTE5MTc4In19XX0=",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoyLjQ5NTI2MTEzLCJuIjoxNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYSBPUF9FUVVBTCIsImhleCI6ImFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGdqNjluMnUyOHQwenZnNTBscjN1a210OXZ4cHBnZGNyZjlhcTNjdTU2bGRzcHl4bDR3NWhzZnltOHM3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTE3MTkzIn19XX0=",
+ "eyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOlt7InZhbHVlIjoxMS4zNDA2MDM2NCwibiI6NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYiBPUF9FUVVBTCIsImhleCI6ImFhMjBiMzQ3ZDY4YzhkMmNkYjMyMDY2YmU5YWY4MDRjNmZmYjhhYmJiNDQxYmRmMzZjYWYzODM0MzZiY2ZjYWEzNWNiODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd2U1MDQ1djM1a2RrdnN4ZDA1NmxxenZkbGFjNHdhNWd4N2x4bTkwOHE2cmQwOHU0ZzZ1a24zNzJnZ3dsIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTM0MzQ2In19XX0=",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoyLjQ2NzIyMDQ1LCJuIjoxNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTEyZDE2NmFlMjhlYjc4OTg4YTNmZTM4ZjJkYjU5NTg2MDg1MGRjMGQyNWU4MjM4ZTUzNWY2YzAyNDM3ZWFlYSBPUF9FUVVBTCIsImhleCI6ImFhMjA1MTJkMTY2YWUyOGViNzg5ODhhM2ZlMzhmMmRiNTk1ODYwODUwZGMwZDI1ZTgyMzhlNTM1ZjZjMDI0MzdlYWVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGdqNjluMnUyOHQwenZnNTBscjN1a210OXZ4cHBnZGNyZjlhcTNjdTU2bGRzcHl4bDR3NWhzZnltOHM3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTE1ODc2In19XX0=",
+ "eyJ0eGlkIjoiNzk5Mjk5OTRiZmRjMjY3YThlYjYxYWQ0NmI5Njc2NjYwNDc2M2U1NTZhMTQ5MTM3NTM4NjM0NDllOGM1MDE2MyIsInZvdXQiOlt7InZhbHVlIjoxLjc5NDg1NTksIm4iOjIwLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDIwYjA1Yjc0YWI1NmM5ODQ4MDMyZTliYTRmZDA5NDI3NWNjYzdmMzYyOGFkMTg4NmZjZTU2ZTkzMGQxMjczY2Q4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2c3Rxa201NGR0dm5weXF4dDVtNW43c2pzbjRlbnJseGM1MjZ4eXhsbmprYXljZHpmZXU2OXJleXptcXgiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI4NDE4OSJ9fV19",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoyLjM1MjE2MDgzLCJuIjoxNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYzhkZDk3N2FlOTkxMmI3MjEzNDFiYWYzMDg2ODdiZTViOWVmYjMyY2MwZmJmMjY5NjY2OTBiNjE5OGUyMDFjZiBPUF9FUVVBTCIsImhleCI6ImFhMjBjOGRkOTc3YWU5OTEyYjcyMTM0MWJhZjMwODY4N2JlNWI5ZWZiMzJjYzBmYmYyNjk2NjY5MGI2MTk4ZTIwMWNmODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMHlkbTltNmF4Z2prdXNuZ3hhMHh6cmcwMGptbm1hbjlucTBodW5mdmU1c2tjdmN1Z3F1N3J1YWd3dmVsIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTEwNDcyIn19XX0=",
+ "eyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOlt7InZhbHVlIjoxMC41MzY5NTQ0LCJuIjo2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMDdjMmJjY2EyMDg5YTlmMGY4ZGFlYzBkM2I0NWE2YzUyNTgxYzU2NzY2NWNjNTFlYmRhOWEyMDJkOGEzM2RjZmM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnBkN3pobjl6cHpkZjdydWQ0bXFkOGR6NmQzZjlzOHprd2VqdWM1MHRtMmR6cXR2MngwdzBjNTR5MzlocDYiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI0OTY0ODAifX1dfQ==",
+ "eyJ0eGlkIjoiNzk5Mjk5OTRiZmRjMjY3YThlYjYxYWQ0NmI5Njc2NjYwNDc2M2U1NTZhMTQ5MTM3NTM4NjM0NDllOGM1MDE2MyIsInZvdXQiOlt7InZhbHVlIjoxLjI0NDU1NzY1LCJuIjoyMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgY2I5NDVlMjY1MzQzNmIwNTBlYjVmZDY0YjM1NzA1MGEyM2EwZjI3YWFmZjlkMWU1Mjk2MGI4MDYwOGM5MTJkZSBPUF9FUVVBTCIsImhleCI6ImFhMjBjYjk0NWUyNjUzNDM2YjA1MGViNWZkNjRiMzU3MDUwYTIzYTBmMjdhYWZmOWQxZTUyOTYwYjgwNjA4YzkxMmRlODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMDllZ2gzeDJkcGtrcGd3a2g3a2Z2NmhxNTl6OGc4ajAyaGxuNTA5OTlzdHNwc2dleWZkdTBnZG5keWhxIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTgzNzcifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjcwMDEzOTM1LCJuIjoyMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYTIwZjE2OTc3ODk3NzFhMjdkODBjZGIyODJhZGM2NTE1NWIxYWQxNGZkY2IzMmUxYjI2ZmRkMzg2ZDg4NzM1MyBPUF9FUVVBTCIsImhleCI6ImFhMjBhMjBmMTY5Nzc4OTc3MWEyN2Q4MGNkYjI4MmFkYzY1MTU1YjFhZDE0ZmRjYjMyZTFiMjZmZGQzODZkODg3MzUzODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzNxNzk1aDB6dGhyZ25hc3J4bTlxNGRjZWc0dHZkZHpuN3Vrdmhwa2ZoYTZ3cmQzcGU0eDh5bjVqcjhxIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzI3NTkifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjY4ODA3NDY3LCJuIjoyNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMTQ1Njc3NjUxZWMxMjIxY2M3ZjBmYjFlYjk2ZTllMzRiOTEwYzNhNWIyNzI5NDJjOTVlNTNjNGRiOTliY2E3MiBPUF9FUVVBTCIsImhleCI6ImFhMjAxNDU2Nzc2NTFlYzEyMjFjYzdmMGZiMWViOTZlOWUzNGI5MTBjM2E1YjI3Mjk0MmM5NWU1M2M0ZGI5OWJjYTcyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjI5dmFtOXJtcWp5OHg4N3JhM2F3dHduYzZ0anl4cjVrZTg5OXB2amhqbmNuZGVuMDk4eWRuMnZ4YTZzIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzIxOTQifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoxLjI1Njg3MTkxLCJuIjoyMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmRkNzA1NWM5MjYyNTMyNTJlYjQ0ZjdlYTQxMzlkYjI0NWU0OTJhYWRjMTllOWU1Y2ZlOTVhMmVjODdjYjA2MCBPUF9FUVVBTCIsImhleCI6ImFhMjAyZGQ3MDU1YzkyNjI1MzI1MmViNDRmN2VhNDEzOWRiMjQ1ZTQ5MmFhZGMxOWU5ZTVjZmU5NWEyZWM4N2NiMDYwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmthd3AydWpmMzl4ZmZ3azM4aGFmcW5ua2V5dGV5ajR0d3BuNjA5ZWw1NDV0a2cwamN4cW40OGM3enllIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTkwMzUifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoxLjIzNjIwNTcsIm4iOjIzLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhYWVhNjQ5ZjcxOTNhYjQzMjk5OTk5M2JkOGQyY2NjZTVmYWJkM2FmMDFlYmZlNDFhMGUwYTM1ODEwNDRkNDZkIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGFhZWE2NDlmNzE5M2FiNDMyOTk5OTkzYmQ4ZDJjY2NlNWZhYmQzYWYwMWViZmU0MWEwZTBhMzU4MTA0NGQ0NmQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3NHc1ZXlsd3hmNmtzZWZueHZuaGt4amVuODlsMjduNHVxN2hsanA1cnMyeGtxc2duMng2OGdudXZ6ZWsiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI1ODA2MyJ9fV19",
+ "eyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOlt7InZhbHVlIjo1LjIxMzM1ODY1LCJuIjo3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBmMWMzM2JjN2RhMDk5MWQyNmY4YmJmMTUwYzkwMzU2OGIyYzNkYzg3MjczNzA4NjNmNGMzZDlmOTY1NWIzNzkxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwY3V4dzc4bWd5ZXI1bjAzd2wzMnJ5c3g0NXQ5czd1c3Vubnd6cnI3bnBhbjd0OXR2bWV6cmF1M2p1cjkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyNDU2NDMifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjU1ODU2ODQ2LCJuIjoyOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2FmZDRkZThhZmZiMDdjYjY2ODMxMzRhZDU5ZTljN2E3NDNhNzBlNjE0YWQ2MjFhZmY1YTExMDM3YzAxMTVlMiBPUF9FUVVBTCIsImhleCI6ImFhMjA3YWZkNGRlOGFmZmIwN2NiNjY4MzEzNGFkNTllOWM3YTc0M2E3MGU2MTRhZDYyMWFmZjVhMTEwMzdjMDExNWUyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGEwNm4wZzRsYXMwam14c3ZmNTQ0djduM2E4Z3duc3VjMjI2Y3M2bGFkcHpxbXVxeTI3eTZqa2V1NnpnIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjYxMzUifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjU1MzI1Nzc3LCJuIjoyOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjhkNjg0ZGUwNGI4NTAxZmI5MmQ2MWYxMjdlYWE4YTg4ZTk4NGMxNDVlMzIwNmZlY2Y4NGU3NGU0ZjhkZjk0NSBPUF9FUVVBTCIsImhleCI6ImFhMjA2OGQ2ODRkZTA0Yjg1MDFmYjkyZDYxZjEyN2VhYThhODhlOTg0YzE0NWUzMjA2ZmVjZjg0ZTc0ZTRmOGRmOTQ1ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZDVkZHB4N3FqdTlxOGFlOTRzbHpmbDI0ejVnYXh6dnozMHJ5cGg3ZTd6d3duajAzaHU1MnpwZDNtcjluIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjU4ODcifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoxLjA3OTg2NTk4LCJuIjoxOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2Y5OTJkMTI5MDQxMTM3NDhjNDQ0M2MxNjRlY2YxMmY0OGQxYjA3NTE4MDhlMmI1MzcxODQ5N2UxMmY5NGI0MSBPUF9FUVVBTCIsImhleCI6ImFhMjAzZjk5MmQxMjkwNDExMzc0OGM0NDQzYzE2NGVjZjEyZjQ4ZDFiMDc1MTgwOGUyYjUzNzE4NDk3ZTEyZjk0YjQxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmxlanRnampwcTN4YXl2ZzNwdXplOHY3eWg1MzVkc3c1dnEzYzQ0eHV2eWpsc2psOTk1ejdxOHcyeThrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTA3MjQifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoxLjA2NDkzNjc5LCJuIjoyMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2FmZDRkZThhZmZiMDdjYjY2ODMxMzRhZDU5ZTljN2E3NDNhNzBlNjE0YWQ2MjFhZmY1YTExMDM3YzAxMTVlMiBPUF9FUVVBTCIsImhleCI6ImFhMjA3YWZkNGRlOGFmZmIwN2NiNjY4MzEzNGFkNTllOWM3YTc0M2E3MGU2MTRhZDYyMWFmZjVhMTEwMzdjMDExNWUyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGEwNm4wZzRsYXMwam14c3ZmNTQ0djduM2E4Z3duc3VjMjI2Y3M2bGFkcHpxbXVxeTI3eTZqa2V1NnpnIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTAwMjMifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjoxLjA2NTY4MTUyLCJuIjoxOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZCBPUF9FUVVBTCIsImhleCI6ImFhMjBhYWVhNjQ5ZjcxOTNhYjQzMjk5OTk5M2JkOGQyY2NjZTVmYWJkM2FmMDFlYmZlNDFhMGUwYTM1ODEwNDRkNDZkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzR3NWV5bHd4ZjZrc2Vmbnh2bmhreGplbjg5bDI3bjR1cTdobGpwNXJzMnhrcXNnbjJ4NjhnbnV2emVrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTAwNTkifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjUyNzgxMDAzLCJuIjozMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOWU3MjU1NTEzODEyOGUwNWM4NDViNzBhMzA4NGIxOWRhNDE0YTUwM2RmYjc1OTZkMTFmY2Y1NDIyODAwYTE5MCBPUF9FUVVBTCIsImhleCI6ImFhMjA5ZTcyNTU1MTM4MTI4ZTA1Yzg0NWI3MGEzMDg0YjE5ZGE0MTRhNTAzZGZiNzU5NmQxMWZjZjU0MjI4MDBhMTkwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzA4eTQyMzhxZmd1cHdnZ2ttczV2eXlreHc2Zzk5OXEwMG13a3Rkejg3MDJzM2dxenNlcW5ybnZhMnlhIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjQ2OTYifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjUxODUwOTc5LCJuIjozMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWRlZDBiYzBhNTFiZTkyZmI2NDY0MjY3ZDk5NDA1NGVkMGU3MzJhYmE0NDgwZDE2ZDEzYTMwNGFkM2IwNTZlMSBPUF9FUVVBTCIsImhleCI6ImFhMjAxZGVkMGJjMGE1MWJlOTJmYjY0NjQyNjdkOTk0MDU0ZWQwZTczMmFiYTQ0ODBkMTZkMTNhMzA0YWQzYjA1NmUxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnc3Nno3cTU1ZDdqdGFrZ2VweDBrdjVxNDhkcGVlajR3anlzcmdrNnlhcnFqa25rcHR3enA3aDZ4NzI5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjQyNjEifX1dfQ==",
+ "eyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOlt7InZhbHVlIjo0LjM5OTU5MjI1LCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YW15bmxlcHYzcG5nbnE2cmhxNTk1eDBsZm5nNncwd3A4NWpxM3JydjJ3amNsdmh0M3VneDRrYWo0aHUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMDczMDEifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjowLjk5MTU2OTc4LCJuIjoyMiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYTY0YTY1MzA5MTYzMzI5ODA2Mjc1YTVhOTliMjc4NTI0YjIxYTQ2NTU5ZGU0N2VmY2NlMDk4ZDNlYjViMmEwZSBPUF9FUVVBTCIsImhleCI6ImFhMjBhNjRhNjUzMDkxNjMzMjk4MDYyNzVhNWE5OWIyNzg1MjRiMjFhNDY1NTlkZTQ3ZWZjY2UwOThkM2ViNWIyYTBlODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd255NWVmc2o5M245eHF4eWFkOTR4ZGowcGZ5a2dkeXY0dmF1M2wwZW5zZjM1bHR0djRxdTJ4cnMzNndoIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDY1NzcifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjowLjk3NzM0NDY5LCJuIjoyNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2FmZDRkZThhZmZiMDdjYjY2ODMxMzRhZDU5ZTljN2E3NDNhNzBlNjE0YWQ2MjFhZmY1YTExMDM3YzAxMTVlMiBPUF9FUVVBTCIsImhleCI6ImFhMjA3YWZkNGRlOGFmZmIwN2NiNjY4MzEzNGFkNTllOWM3YTc0M2E3MGU2MTRhZDYyMWFmZjVhMTEwMzdjMDExNWUyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGEwNm4wZzRsYXMwam14c3ZmNTQ0djduM2E4Z3duc3VjMjI2Y3M2bGFkcHpxbXVxeTI3eTZqa2V1NnpnIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDU5MDkifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjQ4NzA3NDEzLCJuIjozMiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgZmU5ZWI3MjM0NjI5YzFiOGNiNDhmYjZiMGUzYjc1ZDliNzZhNmYxODZiNjE1NjkyOGZlZTU5NDRjYzkwZDQ2NCBPUF9FUVVBTCIsImhleCI6ImFhMjBmZTllYjcyMzQ2MjljMWI4Y2I0OGZiNmIwZTNiNzVkOWI3NmE2ZjE4NmI2MTU2OTI4ZmVlNTk0NGNjOTBkNDY0ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMGxmYWRlcmdjNXVyd3h0ZnJha2tyM213aHZtdzZuMHJwNGt6NDVqM2xoOWozeHZqcjJ4Z2pxNWpsNnU4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjI3ODkifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjQ4NjI1NDU0LCJuIjozMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMzExOWYyOWU5MDNkNDY4YmYzZDYxOGRlMGM1NTBmMGIwMjE4NWJiNDZiMTNmOGNmYzRjOGJjODNhNzJjNmZiZSBPUF9FUVVBTCIsImhleCI6ImFhMjAzMTE5ZjI5ZTkwM2Q0NjhiZjNkNjE4ZGUwYzU1MGYwYjAyMTg1YmI0NmIxM2Y4Y2ZjNGM4YmM4M2E3MmM2ZmJlODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmMzbnU1N2pxNzVkemxuNmN2ZHVyejRwdTlzeXh6bWszNDM4N3gwY255dGVxYTg5M2htdXNsZXQ0MHljIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjI3NTEifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjQ4Nzc1MjUxLCJuIjozNCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYWFhYmQwNWI4YzE1YjYxNGY2ZjM3MGM5YmU0NWZhMTliOGY1YjhhOTA0ZDAyY2Y4YTY1Yzc4NjA4NWQ3MjYzMiBPUF9FUVVBTCIsImhleCI6ImFhMjBhYWFiZDA1YjhjMTViNjE0ZjZmMzcwYzliZTQ1ZmExOWI4ZjViOGE5MDRkMDJjZjhhNjVjNzg2MDg1ZDcyNjMyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzQyaDV6bTNzMm12OThrN2Rjdm4wajlsZ3ZtM2FkYzR5emRxdDhjNWV3OHNjeTk2dW5yeWU4OXlldm1lIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjI4MjIifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjQ1ODczOTk0LCJuIjozNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2Y5ZDM3MzI4ODc0ODAwOGM1NDhmZjQ1MDgyMTlmZjNkZjJlOWU0ZGFkYmYyNDNhZjFlNTdlNDZiZjJiNWNlOSBPUF9FUVVBTCIsImhleCI6ImFhMjAzZjlkMzczMjg4NzQ4MDA4YzU0OGZmNDUwODIxOWZmM2RmMmU5ZTRkYWRiZjI0M2FmMWU1N2U0NmJmMmI1Y2U5ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmxlNmRlajNwNmdxeng5ZnJsNTJ6cHBubGVhN3Q1N2Zra203ZnA2NzhqaHUzNGw5ZHd3ajJnejJra2F5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjE0NjQifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjQ0MTM5NTA4LCJuIjozNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMzdkMTZkYzAxNjhjZDA3ZWMzMmI1ZWMxZTM4MTdmMjVmYTRjZTU4OTQ3ODNlZGY5ZDFiY2QyZmFmZDEwMDJmYiBPUF9FUVVBTCIsImhleCI6ImFhMjAzN2QxNmRjMDE2OGNkMDdlYzMyYjVlYzFlMzgxN2YyNWZhNGNlNTg5NDc4M2VkZjlkMWJjZDJmYWZkMTAwMmZiODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdm1hem13cXo2eGRxbGtyOWQwdnJjdXAwdWpsNW44OTM5cmM4bTBlNng3ZDk3aGF6cXAwa2V6M3RtZWpmIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjA2NTIifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjowLjg2NDU4Mjk4LCJuIjoyNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYzNlN2RjY2U2M2UzN2FiMmRlM2JmOGE3ZDkzMmNmYTAyOTM3NDkwMmM2MzEwZThlZGNiMzExODEyMzQ1NzY3YSBPUF9FUVVBTCIsImhleCI6ImFhMjBjM2U3ZGNjZTYzZTM3YWIyZGUzYmY4YTdkOTMyY2ZhMDI5Mzc0OTAyYzYzMTBlOGVkY2IzMTE4MTIzNDU3NjdhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMHA3MGh4d3YwM2g0dms3ODB1MjBrZmplN3N6amQ2ZnF0cnJ6cjV3bWplM3JxZnJnNG04NXgyNzBmNTZ2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDA2MTIifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjowLjg0NDEzODk4LCJuIjoyNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWQ3OTc3OWQ2YjgyMzYyODE5Y2MxYTVhOTUxYjlkN2M5ZjcyZTdhMWFmNDc1Nzc2ZDk0NGYzNTRkNzYzMzlmMyBPUF9FUVVBTCIsImhleCI6ImFhMjAxZDc5Nzc5ZDZiODIzNjI4MTljYzFhNWE5NTFiOWQ3YzlmNzJlN2ExYWY0NzU3NzZkOTQ0ZjM1NGQ3NjMzOWYzODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdndoamF1YWR3cHJ2MnFlZXNkOTQ5Z21uNDdmN3VoODV4aDV3NG1rbTl6MHg0eGh2dnVseGM2cjYzNGg2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzk2NDkifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjQyMzM4MDQyLCJuIjozNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOGRhZDcxYTk3YmMxZGM5ZTJiZWQ5ZjZhYTE0YmNlMTk4OThkM2NlODE0MmY2NWQyMTg1ODFkYWY0Mjc0NTMxYyBPUF9FUVVBTCIsImhleCI6ImFhMjA4ZGFkNzFhOTdiYzFkYzllMmJlZDlmNmFhMTRiY2UxOTg5OGQzY2U4MTQyZjY1ZDIxODU4MWRhZjQyNzQ1MzFjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3g2NnVkZjAwcWFlODN0YWswazRnMnRlY3ZjbnJmdWFxMno3ZXdqcnB2cG10Nnp3M2YzYzc1cmd6azN5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTk4MDkifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjowLjc5MDY2MDA4LCJuIjoyOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOGRhZDcxYTk3YmMxZGM5ZTJiZWQ5ZjZhYTE0YmNlMTk4OThkM2NlODE0MmY2NWQyMTg1ODFkYWY0Mjc0NTMxYyBPUF9FUVVBTCIsImhleCI6ImFhMjA4ZGFkNzFhOTdiYzFkYzllMmJlZDlmNmFhMTRiY2UxOTg5OGQzY2U4MTQyZjY1ZDIxODU4MWRhZjQyNzQ1MzFjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3g2NnVkZjAwcWFlODN0YWswazRnMnRlY3ZjbnJmdWFxMno3ZXdqcnB2cG10Nnp3M2YzYzc1cmd6azN5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzcxMzcifX1dfQ==",
+ "eyJ0eGlkIjoiNzhjYjc1MmZlOTY0YWM4YWZiMzBiMDI0OTFhMTlkNzkwYTQ3ZTg4Mzk2YjEyMjAyNDNmYzRlNWY2ZDU0ODc5OSIsInZvdXQiOlt7InZhbHVlIjowLjc4ODIzNDIyLCJuIjoyNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNDYxOWYwYjI5ZDFlZTcwNTQ2Nzg2YjhiMzBjNzViYmU1YjE1YmRiOThkMDFhNmFkZDNhNDE0NzAwMGY4NmU2MSBPUF9FUVVBTCIsImhleCI6ImFhMjA0NjE5ZjBiMjlkMWVlNzA1NDY3ODZiOGIzMGM3NWJiZTViMTViZGI5OGQwMWE2YWRkM2E0MTQ3MDAwZjg2ZTYxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZHJwbnU5am41MHd3cDJ4MHA0Y2t2eDh0d2w5azlkYWh4eHNyZjRkNndqcGd1cXFscGh4emV1cHRkeDZuIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzcwMjMifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjM3MDcxMDcyLCJuIjozOCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOGRhZDcxYTk3YmMxZGM5ZTJiZWQ5ZjZhYTE0YmNlMTk4OThkM2NlODE0MmY2NWQyMTg1ODFkYWY0Mjc0NTMxYyBPUF9FUVVBTCIsImhleCI6ImFhMjA4ZGFkNzFhOTdiYzFkYzllMmJlZDlmNmFhMTRiY2UxOTg5OGQzY2U4MTQyZjY1ZDIxODU4MWRhZjQyNzQ1MzFjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3g2NnVkZjAwcWFlODN0YWswazRnMnRlY3ZjbnJmdWFxMno3ZXdqcnB2cG10Nnp3M2YzYzc1cmd6azN5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTczNDYifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjMwMTAxMjk1LCJuIjozOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYzNlN2RjY2U2M2UzN2FiMmRlM2JmOGE3ZDkzMmNmYTAyOTM3NDkwMmM2MzEwZThlZGNiMzExODEyMzQ1NzY3YSBPUF9FUVVBTCIsImhleCI6ImFhMjBjM2U3ZGNjZTYzZTM3YWIyZGUzYmY4YTdkOTMyY2ZhMDI5Mzc0OTAyYzYzMTBlOGVkY2IzMTE4MTIzNDU3NjdhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMHA3MGh4d3YwM2g0dms3ODB1MjBrZmplN3N6amQ2ZnF0cnJ6cjV3bWplM3JxZnJnNG04NXgyNzBmNTZ2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTQwODUifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjI5ODU5MTIzLCJuIjo0MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMzdkMTZkYzAxNjhjZDA3ZWMzMmI1ZWMxZTM4MTdmMjVmYTRjZTU4OTQ3ODNlZGY5ZDFiY2QyZmFmZDEwMDJmYiBPUF9FUVVBTCIsImhleCI6ImFhMjAzN2QxNmRjMDE2OGNkMDdlYzMyYjVlYzFlMzgxN2YyNWZhNGNlNTg5NDc4M2VkZjlkMWJjZDJmYWZkMTAwMmZiODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdm1hem13cXo2eGRxbGtyOWQwdnJjdXAwdWpsNW44OTM5cmM4bTBlNng3ZDk3aGF6cXAwa2V6M3RtZWpmIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTM5NzIifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjI5NDk3NDA3LCJuIjo0MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMzZiODU4ZjY3OWM1YjA5YjA4OGVmZjE5MDdmMTVkMjk4YjliNzUxMzI1NTVmZjZhMWQyMTA2MGQxNmZkODQ2OCBPUF9FUVVBTCIsImhleCI6ImFhMjAzNmI4NThmNjc5YzViMDliMDg4ZWZmMTkwN2YxNWQyOThiOWI3NTEzMjU1NWZmNmExZDIxMDYwZDE2ZmQ4NDY4ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdm10c2s4azA4em1weGNnM21sM2pwbDN0NTVjaHhtNHp2ajR0bG0ycjVzc3ZyZ2tsa3p4c3FmeDduemN0Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTM4MDIifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjI2NjQ5OTUxLCJuIjo0Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYjRjYzFjZGY2MTc3YzdkZTdiMGJhMDQyMGM0ZDg3YTlkM2QzYmFhN2ExNGYwMjI5Nzg1Y2E0ZmNlMDVlZDRhZCBPUF9FUVVBTCIsImhleCI6ImFhMjBiNGNjMWNkZjYxNzdjN2RlN2IwYmEwNDIwYzRkODdhOWQzZDNiYWE3YTE0ZjAyMjk3ODVjYTRmY2UwNWVkNGFkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzZ2Yzh4bHY5bXUwaG5tcHdzeXlyemRzNzVhODVhNjU3czU3cTNmMHB3MmZsOHF0bTIyNng0ZzM2dGU5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTI0NzAifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjI0MzE5MzIzLCJuIjo0Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZCBPUF9FUVVBTCIsImhleCI6ImFhMjBhYWVhNjQ5ZjcxOTNhYjQzMjk5OTk5M2JkOGQyY2NjZTVmYWJkM2FmMDFlYmZlNDFhMGUwYTM1ODEwNDRkNDZkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzR3NWV5bHd4ZjZrc2Vmbnh2bmhreGplbjg5bDI3bjR1cTdobGpwNXJzMnhrcXNnbjJ4NjhnbnV2emVrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTEzODAifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjIyMTI3MjE4LCJuIjo0NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMTc0ZWM4OGMzMGU5ZjM0NjkwODI1ZjRjZDVmNjcxNDA0OTU5NmNkY2I3ZGYwODU4Y2U0YjFjZjdhNTgyZWIxZSBPUF9FUVVBTCIsImhleCI6ImFhMjAxNzRlYzg4YzMwZTlmMzQ2OTA4MjVmNGNkNWY2NzE0MDQ5NTk2Y2RjYjdkZjA4NThjZTRiMWNmN2E1ODJlYjFlODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnQ1YWp5dnhyNWx4MzVzc2YwNWU0MGt3OXF5amt0dm1qbWE3enpjZWU5M2VhYTlzdDQzdWNkOWx3cmw3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTAzNTMifX1dfQ==",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjIxNzQ3MjU1LCJuIjo0NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMzZiN2I3ODk4ZGFhYTg5NWQxMTNhZjk1OTBkNmE0OWRlMDZjZTBjNDEwYTZhNDc0ZGIwMTVlMTJmMzQ2MTRiOSBPUF9FUVVBTCIsImhleCI6ImFhMjAzNmI3Yjc4OThkYWFhODk1ZDExM2FmOTU5MGQ2YTQ5ZGUwNmNlMGM0MTBhNmE0NzRkYjAxNWUxMmYzNDYxNGI5ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdm10MGR1ZjNrNDIzOXczendoZXR5eGs1anc3cW04cWNzZzJkZnI1bXZxNHV5aG5nYzJ0anQ0aGdjbGRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTAxNzUifX1dfQ==",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjEyMjk1MzQ5LCJuIjo2MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYTY0YTY1MzA5MTYzMzI5ODA2Mjc1YTVhOTliMjc4NTI0YjIxYTQ2NTU5ZGU0N2VmY2NlMDk4ZDNlYjViMmEwZSBPUF9FUVVBTCIsImhleCI6ImFhMjBhNjRhNjUzMDkxNjMzMjk4MDYyNzVhNWE5OWIyNzg1MjRiMjFhNDY1NTlkZTQ3ZWZjY2UwOThkM2ViNWIyYTBlODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd255NWVmc2o5M245eHF4eWFkOTR4ZGowcGZ5a2dkeXY0dmF1M2wwZW5zZjM1bHR0djRxdTJ4cnMzNndoIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTcyMiJ9fV19",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjIwMzcwMzc3LCJuIjo0OSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgZmE4OTA0NmMxMzRmM2JhN2Q4ZTg5NjU0YWJkOGJlZTgyMzg1NTg1M2Q3NTQ2ZmFjYjc3OTZkOGU0ZjM5MzQ4MSBPUF9FUVVBTCIsImhleCI6ImFhMjBmYTg5MDQ2YzEzNGYzYmE3ZDhlODk2NTRhYmQ4YmVlODIzODU1ODUzZDc1NDZmYWNiNzc5NmQ4ZTRmMzkzNDgxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMGFnanBydnpkOG5oZjdjYXp0OWYyN2NobTV6OHAyYzIwdDRnbWF2a2F1a21yajA4eTZnejd0anJnaHpnIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiOTUzMSJ9fV19",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjIwNjczMjY2LCJuIjo0Nywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWZhMmEwNDMwOTRiZDJiZjQ1MDZkYzM0N2QxYzZjMTg1MmExMDU1ZWUwNWFjNjIzNDI0YzFjZDVkZjUyOGI5YyBPUF9FUVVBTCIsImhleCI6ImFhMjAxZmEyYTA0MzA5NGJkMmJmNDUwNmRjMzQ3ZDFjNmMxODUyYTEwNTVlZTA1YWM2MjM0MjRjMWNkNWRmNTI4YjljODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjA2OWd6cnA5OWE5MDY5cW13cmdsZ3Vkc3Y5OWdnOXRtczk0MzNyZ2Z4cGU0d2wyMjllY3VkdnZseDM4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiOTY3NCJ9fV19",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjIwNDM2OTE3LCJuIjo0Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOWJkNTNiOTk2ZjhkOGEwYTVlYzQzMWU3Y2U5NGI4YWU1YjJiNTc1NzBkNjY4ZjZhNzIxN2UzYmYwNzNkNTUwNiBPUF9FUVVBTCIsImhleCI6ImFhMjA5YmQ1M2I5OTZmOGQ4YTBhNWVjNDMxZTdjZTk0YjhhZTViMmI1NzU3MGQ2NjhmNmE3MjE3ZTNiZjA3M2Q1NTA2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd2RhMnd1ZWQ3eGM1emo3Y3NjNzBuNTVoemg5azI2aDJ1eGtkcm0yd2d0NzgwYzg4NDJzdmVwOHB5eHQ4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiOTU2MyJ9fV19",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjIwMzI0NDg1LCJuIjo0OCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNzBmNmVkYzU1NjU3YWJjOGFlZWJkN2QxZTQ3YmFkYWZlZDY2YjQzMmQ1Yzg5OWUzODFlYjJmYjlhYjhiNzM5ZCBPUF9FUVVBTCIsImhleCI6ImFhMjA3MGY2ZWRjNTU2NTdhYmM4YWVlYmQ3ZDFlNDdiYWRhZmVkNjZiNDMyZDVjODk5ZTM4MWViMmZiOWFiOGI3MzlkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZGMwZG13OTJldDZoajl3YTB0YXJlcm00a2g3NmU0NXh0MnUzeDByczg0amx3ZHQzZGVlNnRjdG44bmF0Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiOTUxMSJ9fV19",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjE5NzIwNDI1LCJuIjo1MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2Y0NDU1NTZiYmU1YzU1NjE3OGRlODY1NWYxOTA3NTIwZjQ2ZDA0MzJmMGQxZmU4OWIzNWE4MjZlM2ZkMTM1NCBPUF9FUVVBTCIsImhleCI6ImFhMjAzZjQ0NTU1NmJiZTVjNTU2MTc4ZGU4NjU1ZjE5MDc1MjBmNDZkMDQzMmYwZDFmZTg5YjM1YTgyNmUzZmQxMzU0ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmw1ZzQya2gwanUyNHNoM2g1eDJoY2VxYWZxNzNrc2d2aHM2OGxnbnY2NnNmaHJsNWY0Z2V2azd4d3h5Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiOTIyOCJ9fV19",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjE5MzIxNzczLCJuIjo1Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOGNkZWY3YmViNzEzZDI1NjY0NjhmYWNhY2Q3NGI5YjI5MjUyODljNDJiNzRiYjM4OGRjNDFkMTNhMTFmZGZhZiBPUF9FUVVBTCIsImhleCI6ImFhMjA4Y2RlZjdiZWI3MTNkMjU2NjQ2OGZhY2FjZDc0YjliMjkyNTI4OWM0MmI3NGJiMzg4ZGM0MWQxM2ExMWZkZmFmODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd3hkYWFhN2t1ZmF5NG55ZHJhdjRudDVoeGVmeTU1ZmNzNGhmd2VjM2h6cDZ5YXBybDA2N242aHVwcmdrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiOTA0MiJ9fV19",
+ "eyJ0eGlkIjoiNzViOGNmOWFiN2I4MzU0YWU0OTBkM2E5NmNjOGNlNDExODc4N2ZjZjI0MDU2ZDhmNDRjYjhhZGVjZmFlMWIzYSIsInZvdXQiOlt7InZhbHVlIjowLjE4NjI0MjQ5LCJuIjo1Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmIwOWQ1MWVlNzVkZDYwYjc1Nzg5ZjIyZWZlOTFlNzk5ZGRlMjkxYjBjYjdjYmIwNTY2ZGI5OTgwMjdhMzYwMiBPUF9FUVVBTCIsImhleCI6ImFhMjAyYjA5ZDUxZWU3NWRkNjBiNzU3ODlmMjJlZmU5MWU3OTlkZGUyOTFiMGNiN2NiYjA1NjZkYjk5ODAyN2EzNjAyODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjRzbjRnN3Vhd2F2em00MHowajltbGZyZXVlbWgzZnJ2eHQwamFzMmVrbW54cXowZ21xeXYwYWo2ZmZnIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODcxNCJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjEwNDkxMDM0LCJuIjo2NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2VkMjY3YzdlMzM1OGNjYjhkNWIzMjY3YjJmYjJlYWZmMzc0YmE1ZGI2YWNjNjlhYzFlYWRkOTQyNzIxZWNhYyBPUF9FUVVBTCIsImhleCI6ImFhMjAzZWQyNjdjN2UzMzU4Y2NiOGQ1YjMyNjdiMmZiMmVhZmYzNzRiYTVkYjZhY2M2OWFjMWVhZGQ5NDI3MjFlY2FjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmxkeWU3OHV2NmNlanVkdHZleDB2aG05NmhseGE5NnRrbTJlMzU2Yzg0ZG05cDh5OGsyY3ZxcTJkZ2NjIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDg4MyJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjEwNDM3NzQ2LCJuIjo2Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYWFlYTY0OWY3MTkzYWI0MzI5OTk5OTNiZDhkMmNjY2U1ZmFiZDNhZjAxZWJmZTQxYTBlMGEzNTgxMDQ0ZDQ2ZCBPUF9FUVVBTCIsImhleCI6ImFhMjBhYWVhNjQ5ZjcxOTNhYjQzMjk5OTk5M2JkOGQyY2NjZTVmYWJkM2FmMDFlYmZlNDFhMGUwYTM1ODEwNDRkNDZkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzR3NWV5bHd4ZjZrc2Vmbnh2bmhreGplbjg5bDI3bjR1cTdobGpwNXJzMnhrcXNnbjJ4NjhnbnV2emVrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDg1OCJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjEwMjQwNjE5LCJuIjo2Nywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTY2NzlmZTcyOGM2YzE4NzU5Yjc5ZWE0ZjIwYjJlOThiMmNkOWEyNTcwMWM1MzMyMDRkYmFkZWViYWViYzcxNSBPUF9FUVVBTCIsImhleCI6ImFhMjA1NjY3OWZlNzI4YzZjMTg3NTliNzllYTRmMjBiMmU5OGIyY2Q5YTI1NzAxYzUzMzIwNGRiYWRlZWJhZWJjNzE1ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZHR4MDhsODlycnZycDZlazcwMmZ1c3Q5NnZ0OW52Nnk0Y3BjNWVqcW5kNm1tNDZhMHIzMjV0eWh3dHI2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDc2NiJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjA5NzM2ODg1LCJuIjo2OSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMzdkMTZkYzAxNjhjZDA3ZWMzMmI1ZWMxZTM4MTdmMjVmYTRjZTU4OTQ3ODNlZGY5ZDFiY2QyZmFmZDEwMDJmYiBPUF9FUVVBTCIsImhleCI6ImFhMjAzN2QxNmRjMDE2OGNkMDdlYzMyYjVlYzFlMzgxN2YyNWZhNGNlNTg5NDc4M2VkZjlkMWJjZDJmYWZkMTAwMmZiODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdm1hem13cXo2eGRxbGtyOWQwdnJjdXAwdWpsNW44OTM5cmM4bTBlNng3ZDk3aGF6cXAwa2V6M3RtZWpmIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDUzMSJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjA5NzcyODY5LCJuIjo2OCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2QxNDMzN2U0YWRlZjMzOTZiMWExNjdmNGM1YTBjODc4NTRlZjEyY2RjMTBkNjA3MGZmOWI4NGQ1MDRhYWUzMSBPUF9FUVVBTCIsImhleCI6ImFhMjAzZDE0MzM3ZTRhZGVmMzM5NmIxYTE2N2Y0YzVhMGM4Nzg1NGVmMTJjZGMxMGQ2MDcwZmY5Yjg0ZDUwNGFhZTMxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjczZ3ZtN2Z0MDB4d3R0cmd0ODduejZwanJjMm5oMzlud3BwNHM4cGx1bXNuMnNmMmhyejNwajljZjZqIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDU0OCJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjA3OTY3OTA3LCJuIjo3MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2QxNDMzN2U0YWRlZjMzOTZiMWExNjdmNGM1YTBjODc4NTRlZjEyY2RjMTBkNjA3MGZmOWI4NGQ1MDRhYWUzMSBPUF9FUVVBTCIsImhleCI6ImFhMjAzZDE0MzM3ZTRhZGVmMzM5NmIxYTE2N2Y0YzVhMGM4Nzg1NGVmMTJjZGMxMGQ2MDcwZmY5Yjg0ZDUwNGFhZTMxODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjczZ3ZtN2Z0MDB4d3R0cmd0ODduejZwanJjMm5oMzlud3BwNHM4cGx1bXNuMnNmMmhyejNwajljZjZqIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzcwOSJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjA3ODAyNDU4LCJuIjo2MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgOGYxNDk0MDZiZTVjN2RmZDQyOTkzYzViZmYzYWFhNWQ0YTE3MGZlMGE0MmM5OGY4MTMzOGQ4OGMxZWYyODMyNSBPUF9FUVVBTCIsImhleCI6ImFhMjA4ZjE0OTQwNmJlNWM3ZGZkNDI5OTNjNWJmZjNhYWE1ZDRhMTcwZmUwYTQyYzk4ZjgxMzM4ZDg4YzFlZjI4MzI1ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdzgzZjlxeGhldzhtbDJ6bnk3OWhsZTY0Znc1NTljMHV6anpleDhjenZ1ZDNycTc3MnBqMnVsbTBwcDc3Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzYzMSJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjA2ODM3MjkyLCJuIjo3NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNTY2NzlmZTcyOGM2YzE4NzU5Yjc5ZWE0ZjIwYjJlOThiMmNkOWEyNTcwMWM1MzMyMDRkYmFkZWViYWViYzcxNSBPUF9FUVVBTCIsImhleCI6ImFhMjA1NjY3OWZlNzI4YzZjMTg3NTliNzllYTRmMjBiMmU5OGIyY2Q5YTI1NzAxYzUzMzIwNGRiYWRlZWJhZWJjNzE1ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZHR4MDhsODlycnZycDZlazcwMmZ1c3Q5NnZ0OW52Nnk0Y3BjNWVqcW5kNm1tNDZhMHIzMjV0eWh3dHI2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMzE4MiJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjA2MjE1NTEyLCJuIjo2Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMmQzM2MwNWJjZTliOTkwNzZmYjcxZWZmZDNlNzI2YTg0ZGZlMjY2N2M1YWU3NWM3MmI1MjRmMjlkNjRjZTJlYSBPUF9FUVVBTCIsImhleCI6ImFhMjAyZDMzYzA1YmNlOWI5OTA3NmZiNzFlZmZkM2U3MjZhODRkZmUyNjY3YzVhZTc1YzcyYjUyNGYyOWQ2NGNlMmVhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmtuOHN6bWU2ZGVqcG0wa3UwMGw1bDh5NjV5bWwzeHZsejZ1YXc4OWRmeTcyd2tmbjN3NWRkNXdwazkzIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjg5MyJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjA2MDI4MDcxLCJuIjo2NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNmFhYzM4ZGJhZTFhOWMwMmFhNjRmY2I2OGMwNTBlMjZhMmY1ZWY3MDVjYjI4ZjZjYTY4ZWJlNjgyMzllOGNjZSBPUF9FUVVBTCIsImhleCI6ImFhMjA2YWFjMzhkYmFlMWE5YzAyYWE2NGZjYjY4YzA1MGUyNmEyZjVlZjcwNWNiMjhmNmNhNjhlYmU2ODIzOWU4Y2NlODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZDQyY3d4bTRjZGZjcTQydm43dGRycTlwY24yOWEwMHdwd3Q5cm12NTY4dHU2cHJuNnh2dXF4dGE5bjNxIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjgwNSJ9fV19",
+ "eyJ0eGlkIjoiNjAwMzNmMDg3MDIxNTIyZjA2Y2IyMTAxMTUyZmZjNGE2Y2VlODc4Y2FmMmRiZDYxMzY0Y2E0NDJiMjZiMmFlOCIsInZvdXQiOlt7InZhbHVlIjowLjA1Mzg3NDY2LCJuIjo2Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYzJmYThiNTJmYTM2ODU1OTVkN2NhZGIwYmIyZDkzZjFiMzNkN2RjNTdjYjE3YTAyOTI5MzhhODMxMmFkYjQ1NyBPUF9FUVVBTCIsImhleCI6ImFhMjBjMmZhOGI1MmZhMzY4NTU5NWQ3Y2FkYjBiYjJkOTNmMWIzM2Q3ZGM1N2NiMTdhMDI5MjkzOGE4MzEyYWRiNDU3ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwMHAwNHo2amxnbWcyazJhMGprbXB3ZWRqMGNteDB0YWM0N3R6N3N6ajJmYzRxY2o0azY5dzg0Z2s2OHkyIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMjUwNyJ9fV19",
+ "eyJ0eGlkIjoiOWNkYzhjYTYwM2QxY2Y1MGFhMTA2YWQwOTNjYzdiNTI1Nzg4NzVlODNmYWNkZWMyNWVkYzllNmUzMzBmNzEyZiIsInZvdXQiOlt7InZhbHVlIjo4ZS0wNiwibiI6MTEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiI2MDc1MTAifX1dfQ==",
+ "eyJ0eGlkIjoiZTNlNDlmZDQxYTMzOGE0OGZlZjg2ZDYyYmQxOGE0OTYyM2UyZDJiY2M1MTIxMzZjYmY3ZDczZjc2M2FhMWQ1YSIsInZvdXQiOlt7InZhbHVlIjozNy4zMjQ2NzAzNCwibiI6MTAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZTFiODU2NzdmNDYyZmU3NmJhMTcwZTZjMzNiNTk4MmVhNTU4NzY1MGUzY2I0M2EwNTc2NTIxOGRhZWQwNmUzYiIsInZvdXQiOlt7InZhbHVlIjoxZS0wNSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgM2I3MWJlZDIzYmY3ZTYwNmQyZjkxNzhkODVlMTUxNzBkNjhjNDVhMTBhOWU4NmRiNTIwYWMwOGUzZWY0MWY2YSBPUF9FUVVBTCIsImhleCI6ImFhMjAzYjcxYmVkMjNiZjdlNjA2ZDJmOTE3OGQ4NWUxNTE3MGQ2OGM0NWExMGE5ZTg2ZGI1MjBhYzA4ZTNlZjQxZjZhODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdmFocjBrajgwbTd2cGtqbHl0Y21wMHAyOWNkZHJ6OTV5OWZhcGttMmc5dnByMzc3czBrNTU3NGRyMndjIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiI1ZTQzNzMyNjQ0OWFiYTc4NTVkYTNmNTkyMmZkNjVjZmVlNmVhYjE3YzY4NjllMDYzNjAxNjMwMGIwZjFjM2MxIiwiYW1vdW50IjoiMCIsIm5mdCI6eyJjYXBhYmlsaXR5Ijoibm9uZSIsImNvbW1pdG1lbnQiOiIifX19XX0=",
+ "eyJ0eGlkIjoiZTFiODU2NzdmNDYyZmU3NmJhMTcwZTZjMzNiNTk4MmVhNTU4NzY1MGUzY2I0M2EwNTc2NTIxOGRhZWQwNmUzYiIsInZvdXQiOlt7InZhbHVlIjowLjA2MDI1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBlZDY3MzA5YjM2NDI0YzFiM2YyNWUwOGYxOWQ4ZWFkNGVhZGU5MTdjNDc2ZTViMTY1MjBkZWFjNjNkNjVkNGE5IE9QX0VRVUFMIiwiaGV4IjoiYWEyMGVkNjczMDliMzY0MjRjMWIzZjI1ZTA4ZjE5ZDhlYWQ0ZWFkZTkxN2M0NzZlNWIxNjUyMGRlYWM2M2Q2NWQ0YTk4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwa2t3dnlteGVweWN4ZWx5aHNnN3h3Y2F0Mnc0aDUzMDNya3VrY2syZ3g3NDMzYXZoMjJqbHYzOXBkenQiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImQwZDQ2ZjVjYmQ4MjE4OGFjZWRlMGQzZTQ5Yzc1NzAwYzE5Y2I4MzMxYTMwMTAxZjBiYjZhMjYwMDY2YWM5NzIiLCJhbW91bnQiOiIwIiwibmZ0Ijp7ImNhcGFiaWxpdHkiOiJtdXRhYmxlIiwiY29tbWl0bWVudCI6IjM5ODBhODY5MDAwMGU4MDNlMGNiMTcwMGY1YjYwMDAwIn19fV19",
+ "eyJ0eGlkIjoiZTFiODU2NzdmNDYyZmU3NmJhMTcwZTZjMzNiNTk4MmVhNTU4NzY1MGUzY2I0M2EwNTc2NTIxOGRhZWQwNmUzYiIsInZvdXQiOlt7InZhbHVlIjowLjAzMTg2MTY4LCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTI1ZTliY2VjZWEwZTlhMTI5ZWRlMzkyYTA3NWRhMmVmMDNhMjJjMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkyNWU5YmNlY2VhMGU5YTEyOWVkZTM5MmEwNzVkYTJlZjAzYTIyYzE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpmOWF4N3dlNnN3bmdmZmFoM2U5Z3I0bWdoMHF3M3pjeTZrcTVhZmF1Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNzAxOTJhNmE5YmRmMWNjMTFlYWJlOWU0NTU0MWFkM2U0NzlhZjYwMTJmN2EwMmNiMDg2Y2ZiZGRmY2RkYTdiMiIsInZvdXQiOlt7InZhbHVlIjozLjJlLTA1LCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODY0MDM2NTdmMGM3YjE3ODllYjIzNDcyZjEwNzI1MDYxOTQwZjdiNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpyeXFkamg3cnJtejd5N2tnNjg5dWc4eTVycGpzOGhrczVtbWV4YTQzIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiI1YTRmNmIyNTI0M2MxYTJkYWJiMjQzNGUzZDllNTc0ZjY1YzMxNzY0Y2UwZTdlYjQxMjdhNDZmYTc0NjU3NjkxIiwiYW1vdW50IjoiMCIsIm5mdCI6eyJjYXBhYmlsaXR5IjoibWludGluZyIsImNvbW1pdG1lbnQiOiIifX19XX0=",
+ "eyJ0eGlkIjoiNDc1YTU0MzllMjZlMDcwMTBmMzRhZjQwYzgxYjczNzcxZWIyYWIyNjI2ZTY2ZTFkYjFkNzdiMmVjNjFmN2Q3MSIsInZvdXQiOlt7InZhbHVlIjowLjA3MTQ0ODE4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODY0MDM2NTdmMGM3YjE3ODllYjIzNDcyZjEwNzI1MDYxOTQwZjdiNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpyeXFkamg3cnJtejd5N2tnNjg5dWc4eTVycGpzOGhrczVtbWV4YTQzIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMGRjMGUzMzgwNGE1ZWFmMGMwYzMwMzBiNGNmY2NkMzdlNTVhYjc4NTVmZTE3OGE2Mjk2ZDdhZTVjMDlmZGQ1MyIsInZvdXQiOlt7InZhbHVlIjowLjU5Nzg4OSwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGZkYjU3YjBkZDNjOWFhZTUxMTA0MjZmNzM4MGExODc2NzUyZTRlMWUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmZGI1N2IwZGQzYzlhYWU1MTEwNDI2ZjczODBhMTg3Njc1MmU0ZTFlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyN20yN2NkNjB5NjRlZzNxc24wd3dxMnJwbTgydGp3cmNka2xtbGx0ZyJdfX1dfQ==",
+ "eyJ0eGlkIjoiMTBmMjFjZGEyYmE4NmU1NGZjNDdhYjNmYWY3ZDU4NzAzYjI3MWQzNTQyNTFlYTk1NWFhNTU5Nzk2OWVkNzliOSIsInZvdXQiOlt7InZhbHVlIjowLjAwNTI4NjQ3LCJuIjoxMiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDQyMTdmYmVjNGM3NDY1ODJiZTczZTc1YWZiYTExNjU3ZDhmNzA5YjQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0MjE3ZmJlYzRjNzQ2NTgyYmU3M2U3NWFmYmExMTY1N2Q4ZjcwOWI0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwcHAwN2x2ZjM2eHRxNDd3MG40NDdhcHpldGEzYWNma3MyZmxnODl5OSJdfX1dfQ==",
+ "eyJ0eGlkIjoiMWMzY2RlMGYyOWRiN2UzNzRjYWYwOTdmOGE4NTY2MjM2MjMxZGVhYWFiOTc0NmUxZmIzYTYzOGVmM2FjN2RkOCIsInZvdXQiOlt7InZhbHVlIjowLjExODA1NTA3LCJuIjoyNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDdiNjExNzFlOGRlY2VkYWFkNmEyZDJlNzlkOWFiN2QyNWNlOTUxNDQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3YjYxMTcxZThkZWNlZGFhZDZhMmQyZTc5ZDlhYjdkMjVjZTk1MTQ0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwYWt6OWM3M2hrd20ya2s1dGZ3MDh2NmtsZjllNjIzZ3N3ODB3cWo4NiJdfX1dfQ==",
+ "eyJ0eGlkIjoiMmYyMzA1MGViNTMxMjFjYzI3ZjU3YzYzMTIxNzA1Njc4MDJiYTUwZWU3N2U2YjFiYWZlMDMwNWRlN2UxYzI4OCIsInZvdXQiOlt7InZhbHVlIjowLjE2OTYzMzMxLCJuIjo5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgY2ZkODJjY2YxN2RkYWY2Mzg3ZmMxNWVmNTU0MGEwNTdkOWJiYTdlNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGNmZDgyY2NmMTdkZGFmNjM4N2ZjMTVlZjU1NDBhMDU3ZDliYmE3ZTY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI4YXN0eDB6bHc2N2N1OGxzMjc3NDJxNXB0YW53YTh1Yzl2cTZhbmF0Il19fV19",
+ "eyJ0eGlkIjoiMzE1MGVjZDI0MWY5NzQ4ZmZjZGNlYjJhYjk3ZjI4NzQyOGZmZjc0NTAwM2E2YjI2ZGRkNThjMzk2NDliMjE2MCIsInZvdXQiOlt7InZhbHVlIjowLjAwMjE1NDgyLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgY2VmZjhkYTkyMDYyZjk1MGRiMmNhYzg5YTZhYzZmYmRlOWVhM2NlYSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGNlZmY4ZGE5MjA2MmY5NTBkYjJjYWM4OWE2YWM2ZmJkZTllYTNjZWE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI4MGxyZGZ5cDMwajV4bTlqa2duZjR2ZDc3N242M3VhZ3d1NmZzNXBnIl19fV19",
+ "eyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOlt7InZhbHVlIjowLjAxNjc5MjMxLCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNDU2NjU4NGVkYjYzOGY5Zjk4ZDIxMTY4MThiZTFhNGRkYWQyMmExNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDQ1NjY1ODRlZGI2MzhmOWY5OGQyMTE2ODE4YmUxYTRkZGFkMjJhMTY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB6a3ZrendtZDNjbDh1YzZnZ2tzeDk3cmZ4YTQ1MzJ6Y3c3Zzkzdjg4Il19fV19",
+ "eyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOlt7InZhbHVlIjowLjE5MjUzNDY3LCJuIjoyNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGNhNWJiYTI4ZTRiNDI5M2IzZjBjZTc3MzlkZjlmYTYyZDYxOTRkYjAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRjYTViYmEyOGU0YjQyOTNiM2YwY2U3NzM5ZGY5ZmE2MmQ2MTk0ZGIwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyOTlodzNndWo2emp3ZWxwbm5oODgwZWxmM2R2eDJka3FrOGx1azc3MCJdfX1dfQ==",
+ "eyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOlt7InZhbHVlIjowLjIzMDcwNTA0LCJuIjoyOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDNiYzQ1NDc0NWM1ZTc2ZmZmNDA0OTAwYTVhZmQyNDc3ZmE1OWMxZWEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQzYmM0NTQ3NDVjNWU3NmZmZjQwNDkwMGE1YWZkMjQ3N2ZhNTljMWVhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxYXVnNHI1dDMwOGRsbDVxamdxNWtoYXkzbWw1a3dwYWcwMjZkZHhtcyJdfX1dfQ==",
+ "eyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOlt7InZhbHVlIjowLjYwNDkxNjQ2LCJuIjo0MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDVkMTJmMjJjN2VmZWYxYjM3MDA0ZDhiNmVjZTI0YTcyOGVmNmVhMWIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ1ZDEyZjIyYzdlZmVmMWIzNzAwNGQ4YjZlY2UyNGE3MjhlZjZlYTFiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwdzM5dTN2MG1sMHJ2bXNxbnZ0ZG04emZmZWdhYWgycnZwaGd4eHZ2ZiJdfX1dfQ==",
+ "eyJ0eGlkIjoiMzhmYjVlMDhmOGJhMTRiNWRiYmVmZjFjNDg1NDQ0ZjVjNDk1MzJlOGMwMDUwNDdlMjNkMjU1MTI3NjVmNWU3NSIsInZvdXQiOlt7InZhbHVlIjowLjYyMzY4NDY2LCJuIjo0MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGRkODEwY2YxZmNhMThhYjExNDQ3Mzg5MmY1ZjhmZDI1Zjc4Yzg4ZGQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkZDgxMGNmMWZjYTE4YWIxMTQ0NzM4OTJmNWY4ZmQyNWY3OGM4OGRkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyd2N6cjgzbGpzYzR2ZzVndXVmOWEwY2w1amwwcnlnbTUzdHptNDRwbSJdfX1dfQ==",
+ "eyJ0eGlkIjoiM2UzMmU3ZWE5M2Q2MGFjODYwMjJjZWYwZTdiZmE2NjlmZmJiYjk0ZjY1NjA4ZDkzMDIwY2Q2ZmJmOTY0MDM4NiIsInZvdXQiOlt7InZhbHVlIjowLjAwMjMxOTM4LCJuIjo2Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGEyNzlmYTg5NmZkYjQyMWRmZDAyNmRjMTIyYjAwMDM3ODY2NjNjNzAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhMjc5ZmE4OTZmZGI0MjFkZmQwMjZkYzEyMmIwMDAzNzg2NjYzYzcwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6MzhuNzVmZGxkNXk4MGFxZmt1emc0c3FxbWN2ZTN1d3FyMG44Mmx1eCJdfX1dfQ==",
+ "eyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOlt7InZhbHVlIjowLjE2OTQ0MTg0LCJuIjozMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDIxYjI4NjBiYmJlNDgzYjZiMDUwZTQzOGU4MTU0ZjRmYzZmMDhkM2IgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQyMWIyODYwYmJiZTQ4M2I2YjA1MGU0MzhlODE1NGY0ZmM2ZjA4ZDNiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxc205cHN0aDBqZzhkNHMycmpyMzZxNGZhOHVkdXlkOHZhbnJzMHJ2YyJdfX1dfQ==",
+ "eyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOlt7InZhbHVlIjowLjI2ODY3MDE5LCJuIjozNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGZlOGM3NWJkNTZiMGUxMjgzZmMzMWRjYTliOGQzNzVjODNhZDAxZTQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmZThjNzViZDU2YjBlMTI4M2ZjMzFkY2E5YjhkMzc1YzgzYWQwMWU0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFybGdjYWRhMjZjd3oycGxjdnd1NHh1ZHhhd2c4dGdwdXNmOTJ6YTl4cCJdfX1dfQ==",
+ "eyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOlt7InZhbHVlIjowLjI4MTc2NTE0LCJuIjozNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGZkODRiZDNkZDhhOTJhZDY4MmUzNDdlYTk4OTI1YmE5ZDRmMTNjZTUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmZDg0YmQzZGQ4YTkyYWQ2ODJlMzQ3ZWE5ODkyNWJhOWQ0ZjEzY2U1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyN2NmMGZhbXo1ajQ0NXp1ZHI3NHh5anR3NWFmdWZ1dTU5OGx3bmNxeiJdfX1dfQ==",
+ "eyJ0eGlkIjoiM2YwMTJhMGFjZjhhNmY4ZDgyMTFiYjUzMWI3Yjg0ZmNhN2YyOTc1OTU4Yjg1NDU0YjI4MThkNzdmZjdjZDBjYSIsInZvdXQiOlt7InZhbHVlIjowLjUwNjM5NDE3LCJuIjo0Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGIzM2JlZDY5NzM3MTU3YTZjNjcyMDRkNmRlMTBiNWE3MWY4MTE0NzMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRiMzNiZWQ2OTczNzE1N2E2YzY3MjA0ZDZkZTEwYjVhNzFmODExNDczODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZW5obXRmd2RjNDBma3h3Z3pkZGhzc2trbjNscWc1d3ZzZjR4bHRjZyJdfX1dfQ==",
+ "eyJ0eGlkIjoiNDVmYjUyMDkxODFiYTM0ZWIzYmEzMDU3YWJmMDQ5MzljMzk1NTk5N2Y2OGJjN2U0OTYyZGUzMzk3OWRkMzg4OCIsInZvdXQiOlt7InZhbHVlIjowLjA1NDUwNDQzLCJuIjoxNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGY1ODNiYWE2NjAyY2I1M2Q0ZWEzNGFiMjViMTJlNDY4ZmY0YjdhZjcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRmNTgzYmFhNjYwMmNiNTNkNGVhMzRhYjI1YjEyZTQ2OGZmNGI3YWY3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyNmM4dzR4dnFrdDIwMnc1ZDl0eWtjanUzNTA3am02N3VmbnQ0cTdtcCJdfX1dfQ==",
+ "eyJ0eGlkIjoiNDVmYjUyMDkxODFiYTM0ZWIzYmEzMDU3YWJmMDQ5MzljMzk1NTk5N2Y2OGJjN2U0OTYyZGUzMzk3OWRkMzg4OCIsInZvdXQiOlt7InZhbHVlIjowLjA1NTg5Nzg4LCJuIjoxNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDY5M2M0NWJmYTQyMzE4ZTY0ZjkxYjA5NDk5ZGVhMWZiN2EzZjQ1YmUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2OTNjNDViZmE0MjMxOGU2NGY5MWIwOTQ5OWRlYTFmYjdhM2Y0NWJlODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwNW5jM2RsNXMzMzNlajBqeGNmZnh3NzU4YWg1MDY5aGNkbTh0N3A2MyJdfX1dfQ==",
+ "eyJ0eGlkIjoiNDVmYjUyMDkxODFiYTM0ZWIzYmEzMDU3YWJmMDQ5MzljMzk1NTk5N2Y2OGJjN2U0OTYyZGUzMzk3OWRkMzg4OCIsInZvdXQiOlt7InZhbHVlIjowLjI5MjI1OTEyLCJuIjo0NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDk4N2ZiZmYwNmM2YzMxM2IyYWJmYzc1MWRiNGJiMWVmNWEzYWU2MzcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5ODdmYmZmMDZjNmMzMTNiMmFiZmM3NTFkYjRiYjFlZjVhM2FlNjM3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6djhsMGxzZDNrcnp3ZTJobHI0cms2dGs4aDQ1d2h4eHV1NDVodnlodiJdfX1dfQ==",
+ "eyJ0eGlkIjoiNDVmYjUyMDkxODFiYTM0ZWIzYmEzMDU3YWJmMDQ5MzljMzk1NTk5N2Y2OGJjN2U0OTYyZGUzMzk3OWRkMzg4OCIsInZvdXQiOlt7InZhbHVlIjowLjM1NDk0NzEsIm4iOjQ4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTMzZGM2NTZiZTM4MTM3YjFjODk1ZDg1NGVkMDNlMzc4MWRmMjM3NiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkzM2RjNjU2YmUzODEzN2IxYzg5NWQ4NTRlZDAzZTM3ODFkZjIzNzY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpmbm0zamtoY3VweDdjdTM5d2MybmtzOGNtY3JoZXJ3Y3lneHJtdzB1Il19fV19",
+ "eyJ0eGlkIjoiNWViMTUzZWViYzVlZjViYmJiOWNhOGY2YzVkNDFhN2FiOTNiM2RhMThhMGJlYWMxYzExNTVjYzhkY2E5NmYwMiIsInZvdXQiOlt7InZhbHVlIjowLjE0NTcyODQyLCJuIjozMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDZiNjUwMGQwNjdjMmY1YmUyNDQ4N2QwMTE5NDRlZTc2OWMxZWNiNzQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2YjY1MDBkMDY3YzJmNWJlMjQ0ODdkMDExOTQ0ZWU3NjljMWVjYjc0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwNGsycXhzdmxwMHQwM3lmcDdzengyeWFlbWZjOGt0d3M1ZTZkdjZ5biJdfX1dfQ==",
+ "eyJ0eGlkIjoiNWViMTUzZWViYzVlZjViYmJiOWNhOGY2YzVkNDFhN2FiOTNiM2RhMThhMGJlYWMxYzExNTVjYzhkY2E5NmYwMiIsInZvdXQiOlt7InZhbHVlIjowLjM4MTk5MjQ0LCJuIjo0NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDQ4MzM4OGEwMmQxOWY1NjA2MjlkZTc4ODE0MWI1N2Y4MzQ3OWE1YzUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0ODMzODhhMDJkMTlmNTYwNjI5ZGU3ODgxNDFiNTdmODM0NzlhNWM1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFweXI4ejlxOTV2bDJjcnpuaG5jczlxbTJsdXJnN2Q5YzVxNHYwbGRoZiJdfX1dfQ==",
+ "eyJ0eGlkIjoiNmIwMzA1MGI2ZTc2YjhiMTA1MmMyNjNkNWEzNzYzYWVhMzk2YjY2NjE5MWJlMDYxOGJhNmFkOTQxYTAzZDdjYyIsInZvdXQiOlt7InZhbHVlIjowLjAwMTEwNDY3LCJuIjozLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNDljMTFiY2FiNjE2NTU3MjI2NTVlODdlNWNkN2FmY2E5OGNhMmIzNyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDQ5YzExYmNhYjYxNjU1NzIyNjU1ZTg3ZTVjZDdhZmNhOThjYTJiMzc4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXB5dXp4NzJrY3Q5MnUzeDJoNTh1aHhoNGw5ZjNqM3R4dXI4M3ljZ3QwIl19fV19",
+ "eyJ0eGlkIjoiNmIwMzA1MGI2ZTc2YjhiMTA1MmMyNjNkNWEzNzYzYWVhMzk2YjY2NjE5MWJlMDYxOGJhNmFkOTQxYTAzZDdjYyIsInZvdXQiOlt7InZhbHVlIjowLjAwNDY3ODYsIm4iOjE0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMWNmMmI5ZDgyZmVjYWNmODY4ZGI1YjJhNTQ2ZTg0YTE0Zjg1NTUzMSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDFjZjJiOWQ4MmZlY2FjZjg2OGRiNWIyYTU0NmU4NGExNGY4NTU1MzE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXF3MDl3d2M5bGsyZTdyZ21kZGo1NHJ3c2pzNWxwMjR4eWg2cWRjand3Il19fV19",
+ "eyJ0eGlkIjoiNmIwMzA1MGI2ZTc2YjhiMTA1MmMyNjNkNWEzNzYzYWVhMzk2YjY2NjE5MWJlMDYxOGJhNmFkOTQxYTAzZDdjYyIsInZvdXQiOlt7InZhbHVlIjowLjAwODA4Mzk1LCJuIjoyOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGFhNTY4ZGU2NDdhOGZlNjNlZGE0NGJmMDg2ODM0NmRjNGRmODYzOTAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhYTU2OGRlNjQ3YThmZTYzZWRhNDRiZjA4NjgzNDZkYzRkZjg2MzkwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6NDlkcjB4Zzc1MHVjbGQ1MzlscHA1cmdtd3ltN3JyanF1cDd1aGZwZyJdfX1dfQ==",
+ "eyJ0eGlkIjoiNmIwMzA1MGI2ZTc2YjhiMTA1MmMyNjNkNWEzNzYzYWVhMzk2YjY2NjE5MWJlMDYxOGJhNmFkOTQxYTAzZDdjYyIsInZvdXQiOlt7InZhbHVlIjowLjAxMjA2NTU4LCJuIjo0MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDAxZmQ3NjExNGYyZWI0NDQxNzRlNDJjZjM2NzYzYzM1OWVlNzcyODUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwMWZkNzYxMTRmMmViNDQ0MTc0ZTQyY2YzNjc2M2MzNTllZTc3Mjg1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxcWw2YXMzZnVodGczcWhmZXB2N2RuazhzNmVhZW1qczU5eGNyZWY0aCJdfX1dfQ==",
+ "eyJ0eGlkIjoiNmM4MjQ4NDdkZjkwNWI5NDhjOWE2NWFkN2QzMTkyZWFjZDRhOWE3ZTk2MDVmOWQ3ZDYxNjhjZWI3NmNiZGViNiIsInZvdXQiOlt7InZhbHVlIjowLjI3OTc5NywibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGU5ZjNjMmEwYWYyYWZmMzU5YmYyNDJjMzg4ZjUxOTk0MDVlOTEzOGEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRlOWYzYzJhMGFmMmFmZjM1OWJmMjQyYzM4OGY1MTk5NDA1ZTkxMzhhODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyNWw4czRxNHU0MDdkdm03ZnB2OHo4NHJ4MnF0NmduM2d1NDA3cDUzciJdfX1dfQ==",
+ "eyJ0eGlkIjoiNmY4NGM1ZGY2MTRjOTcxMGY0NTMzMWFhODM0ODFjYzQ1MzYzZjAxNTJiZDAzMzE2N2E4Y2VlMDNmM2I2YjIyYiIsInZvdXQiOlt7InZhbHVlIjowLjAxMDM1MjEzLCJuIjoxNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDY0MDg2YmNmYjkzYTJiZWQyNjM4Yzk2ZTI3ZDMwMzgyYTM0ZTc4MGQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2NDA4NmJjZmI5M2EyYmVkMjYzOGM5NmUyN2QzMDM4MmEzNGU3ODBkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwanFzNjcwaHlhemhtZng4cnlrdWY3bnF3cDJ4bm5jcDU3MGxxOTB4YSJdfX1dfQ==",
+ "eyJ0eGlkIjoiNmY4NGM1ZGY2MTRjOTcxMGY0NTMzMWFhODM0ODFjYzQ1MzYzZjAxNTJiZDAzMzE2N2E4Y2VlMDNmM2I2YjIyYiIsInZvdXQiOlt7InZhbHVlIjowLjAyMTk4MTAyLCJuIjozMiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGUyNWU4MDc4YzNmMmZmOGMyNzUyNmExMDRlM2ZjZDFlMTQwYmU5NmQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRlMjVlODA3OGMzZjJmZjhjMjc1MjZhMTA0ZTNmY2QxZTE0MGJlOTZkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyMzlhcXJjYzBlMGxycDgyZjRwcW4zbGU1MHBnemxmZDVxbWo3dnJjdiJdfX1dfQ==",
+ "eyJ0eGlkIjoiNmY4NGM1ZGY2MTRjOTcxMGY0NTMzMWFhODM0ODFjYzQ1MzYzZjAxNTJiZDAzMzE2N2E4Y2VlMDNmM2I2YjIyYiIsInZvdXQiOlt7InZhbHVlIjowLjAzMjcxNjE5LCJuIjo0Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGFmMmUwNzdiYTQ2YTk0YzBiMjA4MDhlNmQ2MGFmMWYyMDRjNzNhYzcgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhZjJlMDc3YmE0NmE5NGMwYjIwODA4ZTZkNjBhZjFmMjA0YzczYWM3ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6aGp1cG1tNTM0ZmZzOWpwcXl3ZDRzMjc4ZXFmM2U2Y3U2N3Z4azV3NSJdfX1dfQ==",
+ "eyJ0eGlkIjoiNzhjNjJjMTJiOGRlZDg5NDI4MzhhZmJmM2RkZWI4MzMxZDFlN2VjMjRjNmRkOWQzMWNlNGZkYzU0N2MwOGE3MiIsInZvdXQiOlt7InZhbHVlIjowLjAxMTU3LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZDc5OWUxZmE3OGJmYmQxYjdiM2UwYjg1MjNmN2Y0Yjg0ZGZmNmNlYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGQ3OTllMWZhNzhiZmJkMWI3YjNlMGI4NTIzZjdmNGI4NGRmZjZjZWM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJ0ZW5jMDYwemxtNnhtbThjOWMyZ2xoN2p1eW1sbXZhc2s4cHV0bXEzIl19fV19",
+ "eyJ0eGlkIjoiN2E1NzIwNjUwY2VhMjcxYmUzZDVhM2Y3OGFlYzA1ODIxZTA3ZDlhNTkyNmExNTc4MDU0N2Q5N2E2N2YyYmQ1NSIsInZvdXQiOlt7InZhbHVlIjowLjA4NTg1ODY0LCJuIjoyMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGQ4MTg1YmMzNzRmMzMyNGNlNWY0YTlmMjYzZmUyZjBkNWIzZThkOWQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRkODE4NWJjMzc0ZjMzMjRjZTVmNGE5ZjI2M2ZlMmYwZDViM2U4ZDlkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFydnBzazdyd25lbnluODk3ajVseWNsNzl1eDRrMDVkbjVqcGowOHYzNSJdfX1dfQ==",
+ "eyJ0eGlkIjoiOTAwYjY2M2RkZDA3NjJiMzRmZDgxNjhkM2FjZWNlMGU4OGUzODIxYmU3M2FjOWRhMDhkN2E0NDRlOGI3MzA5ZiIsInZvdXQiOlt7InZhbHVlIjowLjAyNDg2NTAxLCJuIjoyMSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGE4Y2Y2NDEzODliYjYzZDU2MjM2ZGE0YjgxOTNiZDdhNmZmZWU1ODEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhOGNmNjQxMzg5YmI2M2Q1NjIzNmRhNGI4MTkzYmQ3YTZmZmVlNTgxODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6NXY3ZXFuM3hhazg0dHp4bWR5aHF2bmg0YXhsbGg5c3l6NHVkd3hsZyJdfX1dfQ==",
+ "eyJ0eGlkIjoiOWZmODFlNGFjZDIxNzU3ZGIxMzJiNzU5ZjRlOWJlZTBlYzhhOTk2YjgyZWY3YTY4NGFmMTE1MTI1ZjEyMmZjOCIsInZvdXQiOlt7InZhbHVlIjowLjQ5MTk3MzQsIm4iOjgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMTJmODgwYjI2YzU0NTQwODNjNTYyZDdjZGQyMDYwMjU1ZGQwMDNmIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTEyZjg4MGIyNmM1NDU0MDgzYzU2MmQ3Y2RkMjA2MDI1NWRkMDAzZjg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWdqbHpxdHltejUyc3lyYzQzZDBud2pxY3A5dGhncTh1ZWxnNnFwM24iXX19XX0=",
+ "eyJ0eGlkIjoiYWVlY2M5OTJlMTE2NGY2NGFlODEyMTlmNGU0NGY0NDM0NjA1NWI2MWZmOTdjYjkzMTZhMWRjMWExMWQ0MWQ2ZSIsInZvdXQiOlt7InZhbHVlIjowLjAwMjQ0MTk1LCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgY2ZhZjNiZDNmMGQ2OTA0YmViZWZhZDc5NWVlMTY3YWE0NTIwMDk4OCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGNmYWYzYmQzZjBkNjkwNGJlYmVmYWQ3OTVlZTE2N2FhNDUyMDA5ODg4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXI4Njd3N243cnRmcWpsdGE3a2hqaGhwdjc0eTJncWYzcTQ1YXVnZWZ0Il19fV19",
+ "eyJ0eGlkIjoiYWYxN2ZlZDJhMmEyMGYyNjhjY2RmY2U4MzYwNGNjMzQzYTg4M2I0ODVmMDM3M2E2MGY1ZGRjMjUzMmRjOWQ3MCIsInZvdXQiOlt7InZhbHVlIjowLjAwNzI1NTEzLCJuIjo2LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNGFjZTQ2YWY3OTIwNmE3ZjQ5OTQ5YWVmYjgyZDgxMTUzNDAwNmY3ZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDRhY2U0NmFmNzkyMDZhN2Y0OTk0OWFlZmI4MmQ4MTE1MzQwMDZmN2U4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXA5dnUzNDAweXN4NWw2ZmpqZHdsd3Bkc3kybmdxcjAwYzlldnV5aHk4Il19fV19",
+ "eyJ0eGlkIjoiYWYxN2ZlZDJhMmEyMGYyNjhjY2RmY2U4MzYwNGNjMzQzYTg4M2I0ODVmMDM3M2E2MGY1ZGRjMjUzMmRjOWQ3MCIsInZvdXQiOlt7InZhbHVlIjowLjA0MTI2NjMxLCJuIjozNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDhkYTcxMTY2MDZmNTY4ODZjNjMxNjBhYmM5ODlkNzYyOGMyMTIxOGQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4ZGE3MTE2NjA2ZjU2ODg2YzYzMTYwYWJjOTg5ZDc2MjhjMjEyMThkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6eDZ3eXR4cW02azNwa3h4OXMyaGp2ZjZhM2djZ2ZwMzU1ZzM4OG5reSJdfX1dfQ==",
+ "eyJ0eGlkIjoiYWYxN2ZlZDJhMmEyMGYyNjhjY2RmY2U4MzYwNGNjMzQzYTg4M2I0ODVmMDM3M2E2MGY1ZGRjMjUzMmRjOWQ3MCIsInZvdXQiOlt7InZhbHVlIjowLjA1NzA3Njk4LCJuIjo0Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDYzMzI3NmFmNGQ4ODA2YTlmMTBkNjM5MjhlMDRkY2Y0MmM3MjU2NjggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2MzMyNzZhZjRkODgwNmE5ZjEwZDYzOTI4ZTA0ZGNmNDJjNzI1NjY4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwM255YTQwZmt5cWQyMDNwNDNlOXJzeW1uNnpjdWprZHFocmgwdXgzYSJdfX1dfQ==",
+ "eyJ0eGlkIjoiYWYxN2ZlZDJhMmEyMGYyNjhjY2RmY2U4MzYwNGNjMzQzYTg4M2I0ODVmMDM3M2E2MGY1ZGRjMjUzMmRjOWQ3MCIsInZvdXQiOlt7InZhbHVlIjowLjA2MjE3NzI5LCJuIjo0OSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGU2ZTc1MTM3NjJmZTdjMjJiYjhiYzExMWY5ZDQ1NTMzOTkwYzlmNzMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRlNmU3NTEzNzYyZmU3YzIyYmI4YmMxMTFmOWQ0NTUzMzk5MGM5ZjczODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFybnd3NWZodnRsOGNnNG0zMHEzcjd3NTI1ZWVqcnlsd3ZkMHdlcG1lcSJdfX1dfQ==",
+ "eyJ0eGlkIjoiYjRjMDUwOWJjNzRmMzk1ZjZkZDVhZDE3OTk5MWZjMGNjODViMDk5Mjk3NTMyMmY5YzlhMjBkYTA4NDk3NTZkNSIsInZvdXQiOlt7InZhbHVlIjowLjAwNjM3ODIyLCJuIjozNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDYyMThjY2U2ODg1Y2VkMDdlODY1MjY2ZjcxYmNkODU4NzRjZGY4YjQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2MjE4Y2NlNjg4NWNlZDA3ZTg2NTI2NmY3MWJjZDg1ODc0Y2RmOGI0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwM3Azbjh4M3B3dzZwbGd2NW54N3VkdW1wdjhmbjBja3NuZnNlOXhtdyJdfX1dfQ==",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjAwMDMyNDk4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTQxNjczZTAwYzliNzAzYTk1MTk4M2M4MjdiYWRlMGY0MzlhMzg2YSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGE0MTY3M2UwMGM5YjcwM2E5NTE5ODNjODI3YmFkZTBmNDM5YTM4NmE4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpqcHZ1bHFwamRocXc1NHJ4cHVzZmE2bWM4NTh4M2NkZ2hqcHd2ZzZuIl19fV19",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjAwMTY1MTE4LCJuIjo0LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgZTEwMDE0ODJhZmZhZDYzZTM1MzEzMDg2MWU5ZjUwNGJlZmE1NDc2NCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGUxMDAxNDgyYWZmYWQ2M2UzNTMxMzA4NjFlOWY1MDRiZWZhNTQ3NjQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXJzc3E5eXo0bGFkdjAzNHh5Y2d2ODVsMnA5N2xmMjh2c2hjc2x6ZjQ5Il19fV19",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjAwOTEyMTMzLCJuIjoxOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDM4NTA3Njk0NzcxZmM1NDExZmM1Mjg3OWYyYzY0OGVmYzQzOWFlOTUgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQzODUwNzY5NDc3MWZjNTQxMWZjNTI4NzlmMmM2NDhlZmM0MzlhZTk1ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxdTlxYTU1d3UwdTJzZ2xjNTU4bnVreGZyaHVnd2R3ajVtZzBnbjg0MiJdfX1dfQ==",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjAxNDk4OTI5LCJuIjoyMywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDRjOTc3MDNmNzBkNDk5Mzg0YWYzYTk0YTNhODNhNDZlYTgwNDY5MjMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0Yzk3NzAzZjcwZDQ5OTM4NGFmM2E5NGEzYTgzYTQ2ZWE4MDQ2OTIzODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFweGZ3dXBsd3IyZmp3ejI3dzU1NXc1cjUzaDJzcHJmeXZmY2Y3bnE1YSJdfX1dfQ==",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjAxNzE3ODI4LCJuIjoyNiwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDAyY2Y2ZWY4YWQ4ZDU0MzhjNzM0YjI0ZjEwZmM5MTg4MWMxZGNmOTYgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwMmNmNmVmOGFkOGQ1NDM4YzczNGIyNGYxMGZjOTE4ODFjMWRjZjk2ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxcHY3bWhjNGt4NGd3eDh4amV5N3k4dWp4eXBjOHcwamMwdzY1YzZyMyJdfX1dfQ==",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjAyMzQ4MTQ4LCJuIjozNSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGNjZjJmNjYwZGE4MGVlZTAyYjRmZGMyMzIyOTg0ZWY0ZGI1Y2Q5NDIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRjY2YyZjY2MGRhODBlZWUwMmI0ZmRjMjMyMjk4NGVmNGRiNWNkOTQyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFyeDA5YW5xbTJxd2FjcHRmbHd6eGc1Y2ZtNmRraHhlZ2dmOHZkNDh2MCJdfX1dfQ==",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjAyNTk2MjU4LCJuIjozNywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDAyY2E0M2U3MzA4Y2VhYmI0Mzk2MTc4NDAzYWFmN2ZmODc5Mjc5MjEgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQwMmNhNDNlNzMwOGNlYWJiNDM5NjE3ODQwM2FhZjdmZjg3OTI3OTIxODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxcHY1c2w4eHp4dzR3NnJqY3RjZ3FhMjdsbGMweW5leXllZ3d0dnVmdSJdfX1dfQ==",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjAzNDcwMywibiI6NDgsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAwNWRlZDNmOTdiODIxOTIxZDFhMDM4OWFkYzc5ZTUwZjY5MjNjYjRkIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MDVkZWQzZjk3YjgyMTkyMWQxYTAzODlhZGM3OWU1MGY2OTIzY2I0ZDg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcXphYTVsZTB3cHBqZ3czNXF1ZjRocmV1NThramc3dGY1cDNtcHNwd2YiXX19XX0=",
+ "eyJ0eGlkIjoiYmM4YWY0MzgxODM0Yzc3NGFlNmY4Yzc4MzhhMjE2NjQ1MzljMDUwMzhiYTdkZWJjM2QwYWNmMWRlYmNjZTI0MCIsInZvdXQiOlt7InZhbHVlIjowLjE0Mjk3NDA4LCJuIjo2OSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDNkZTViNjNmNzQ3NTFhMzk3OTBhYzQ0NzBlZWExMThlMjlmNjZkNTMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQzZGU1YjYzZjc0NzUxYTM5NzkwYWM0NDcwZWVhMTE4ZTI5ZjY2ZDUzODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxNzd0ZDNsdzM2MzV3dGVwdHp5d3JoMnp4OHpuYW5kMnZ6NTdhcWp6OCJdfX1dfQ==",
+ "eyJ0eGlkIjoiYzYzNTQ5ZTNmYTY2NTYwMWEyOThjZjQ2YmNlMjUzODhmNWRjMWRjYjBiOTFjMjJlOGZkNjVhNGZkYTQ5MGYyMiIsInZvdXQiOlt7InZhbHVlIjowLjY2NDAzMDM4LCJuIjo2Nywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGE4NzA0M2IyMjRlOTBhMzUzMTY2OWY3ZjVlMjg3MzRjNWQ2NWVlNmMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRhODcwNDNiMjI0ZTkwYTM1MzE2NjlmN2Y1ZTI4NzM0YzVkNjVlZTZjODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6NThxc2FqeW41czVkZjN2NjBoN2gzZ3dkeDk2ZTB3ZHM0dGFyOG1wciJdfX1dfQ==",
+ "eyJ0eGlkIjoiY2NlOWU2NDM3MmU0OGY4OWY0YTI3ZGYwYTI0OWQxNDViZWY4MzFjYjEzNzRjYjBhNTQ5NTMyOWNmODk0NDdkZCIsInZvdXQiOlt7InZhbHVlIjowLjAyODEzODIsIm4iOjM4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYmIzMGZkYzBjYTUwM2RjYTg0NWIwMWFiYzcyY2NmMDNiYzk0ZDZlNiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGJiMzBmZGMwY2E1MDNkY2E4NDViMDFhYmM3MmNjZjAzYmM5NGQ2ZTY4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXphbnBsd3FlZmdybWo1eXR2cTZoM2V2ZXVwbWU5eGt1YzB4bnJsdHcwIl19fV19",
+ "eyJ0eGlkIjoiZDA1ZDgwYzcyY2NlMWQ5NzM1ZWFjYjdhM2UwODE0MTA5M2EzMGI1ZWUwMDkxMTAzM2Y2NGNhYzU4ZTM0MTcxNiIsInZvdXQiOlt7InZhbHVlIjowLjAxMzI5Njc2LCJuIjo5LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgM2VkYmMwMjY2YTVkNWI2MjZjZDlkNDU5OWQ1YWE3M2VkYzIyYzI5OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDNlZGJjMDI2NmE1ZDViNjI2Y2Q5ZDQ1OTlkNWFhNzNlZGMyMmMyOTk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFsZGhzcHhkZnc0a2Nudm04MjluODI2NXVsZGNna3pueWV1cWZtOHI0Il19fV19",
+ "eyJ0eGlkIjoiZDA1ZDgwYzcyY2NlMWQ5NzM1ZWFjYjdhM2UwODE0MTA5M2EzMGI1ZWUwMDkxMTAzM2Y2NGNhYzU4ZTM0MTcxNiIsInZvdXQiOlt7InZhbHVlIjowLjAxMzUzOTM1LCJuIjoxMCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDk0OTg3MjAxZjM3MzRhMzJkZWExZjdjYTUwMmIzMWM0YjQ3NzU1OTggT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ5NDk4NzIwMWYzNzM0YTMyZGVhMWY3Y2E1MDJiMzFjNGI0Nzc1NTk4ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6MmZzdXNwN2RlNTV2azc1OG11NTVwdHg4enRnYTY0bnF3NzR2YzN0ciJdfX1dfQ==",
+ "eyJ0eGlkIjoiZDA1ZDgwYzcyY2NlMWQ5NzM1ZWFjYjdhM2UwODE0MTA5M2EzMGI1ZWUwMDkxMTAzM2Y2NGNhYzU4ZTM0MTcxNiIsInZvdXQiOlt7InZhbHVlIjowLjExNDQ5NTk2LCJuIjo0NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIGIyMTRlZDY0YjIzN2E3MDAyMzkyMjM5MWE5OGQ5MTA3MmU4ODYzZmIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTRiMjE0ZWQ2NGIyMzdhNzAwMjM5MjIzOTFhOThkOTEwNzJlODg2M2ZiODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6ZXBmbXR5a2dtNndxcHJqZzNlcjJ2ZGp5cmphenJybHY2aHNwNDQ0MCJdfX1dfQ==",
+ "eyJ0eGlkIjoiZDA1ZDgwYzcyY2NlMWQ5NzM1ZWFjYjdhM2UwODE0MTA5M2EzMGI1ZWUwMDkxMTAzM2Y2NGNhYzU4ZTM0MTcxNiIsInZvdXQiOlt7InZhbHVlIjowLjE3NDk5ODMxLCJuIjo1NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDE4ZTE5ZTM3ZWI4ZjQ0M2U1OWE4Y2JiZGVjM2U4MjAwMjYwZGQ5OTIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQxOGUxOWUzN2ViOGY0NDNlNTlhOGNiYmRlYzNlODIwMDI2MGRkOTkyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxdndyODNoYXc4NWcwamU0cjltbW1wN3NncXp2cndlamc1bm4ybjU1NyJdfX1dfQ==",
+ "eyJ0eGlkIjoiZDA1ZDgwYzcyY2NlMWQ5NzM1ZWFjYjdhM2UwODE0MTA5M2EzMGI1ZWUwMDkxMTAzM2Y2NGNhYzU4ZTM0MTcxNiIsInZvdXQiOlt7InZhbHVlIjowLjQyMzM2MzQ4LCJuIjo3Miwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDcxMTc4ZjViY2ZkYzJjYzU2MzQzNWUzMDgxNDY0MzU4NjM4ZGU2MmQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ3MTE3OGY1YmNmZGMyY2M1NjM0MzVlMzA4MTQ2NDM1ODYzOGRlNjJkODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwYzMwcjZtZWx3emUzdHJnZDBycHEyeGdkdng4cjB4OTVmYXlkd2hhayJdfX1dfQ==",
+ "eyJ0eGlkIjoiZTFjMjQzOGYyNWI2NTJjZjVhM2U0M2NjYjhhYzczNTQ5NGE5MmI3YjgwNzhlMzA5MTg0MDVhOTk3ZGIxMzVhYiIsInZvdXQiOlt7InZhbHVlIjowLjMyNzQ5NTY3LCJuIjo1MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDIzZjViZTZmZTMzNWI2ZjYzMGFlOWNjNjEyOTBiNGM5YTcxOWE0NjMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQyM2Y1YmU2ZmUzMzViNmY2MzBhZTljYzYxMjkwYjRjOWE3MTlhNDYzODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFxM2x0MG4wdXY2bWRhM3M0Nnd2dnk1c2tueTZ3eGR5dnY5ZnBneXI3aCJdfX1dfQ==",
+ "eyJ0eGlkIjoiZTZiZDZkNDA2OTllYmViNTM5NzgzOTg5YzdlNDZkYTNmNjE1MTAxZTEzOWE2ODQ4ZjRiNDQwNTY2ZmU1MTlkZSIsInZvdXQiOlt7InZhbHVlIjowLjAwOTQ4NTA3LCJuIjoxOSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDZmYjY3MTFkNjBmZmQ1Y2RhZTkyNDcwYjM3N2U4NmNmOWMxMjVkZTAgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2ZmI2NzExZDYwZmZkNWNkYWU5MjQ3MGIzNzdlODZjZjljMTI1ZGUwODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwaG12dWdhdnJsYXRuZHdqZnJza2RtN3NtOGVjeWphdXE2ZGs5N3h2dSJdfX1dfQ==",
+ "eyJ0eGlkIjoiZTZiZDZkNDA2OTllYmViNTM5NzgzOTg5YzdlNDZkYTNmNjE1MTAxZTEzOWE2ODQ4ZjRiNDQwNTY2ZmU1MTlkZSIsInZvdXQiOlt7InZhbHVlIjowLjA0Njc0NzcsIm4iOjY1LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgMDIwM2I3NGJkMGExOTYzOGFiYzk0NTlkMjE5YjhiZmViY2IyYTJmZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDAyMDNiNzRiZDBhMTk2MzhhYmM5NDU5ZDIxOWI4YmZlYmNiMmEyZmU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXFwcThkNnQ2enNldnc5dGU5emU2Z3ZtMzBsdGV2NHpsY3lzNW5tZHFxIl19fV19",
+ "eyJ0eGlkIjoiZTZiZDZkNDA2OTllYmViNTM5NzgzOTg5YzdlNDZkYTNmNjE1MTAxZTEzOWE2ODQ4ZjRiNDQwNTY2ZmU1MTlkZSIsInZvdXQiOlt7InZhbHVlIjowLjA5MzMzNTgzLCJuIjo4Nywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDQ0ZTIxYmViYjAxNWVmMmVkZTU0OWJiY2VkYTAzOWQyZmZiNmQ4NjIgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ0NGUyMWJlYmIwMTVlZjJlZGU1NDliYmNlZGEwMzlkMmZmYjZkODYyODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwend5eGx0a3EyNzd0azcyamRtZW1kcTg4ZjBsZGtjdmc4OTd1MzNjcyJdfX1dfQ==",
+ "eyJ0eGlkIjoiZTZjNDE4Nzg2ZmQwNzExMDYzYjI4MGY5ZjU2YWMxOTY1MzEwY2E5MDBjNzRhNmU3ZThjNmY5M2ZiNTg2NTU4MyIsInZvdXQiOlt7InZhbHVlIjoyLjQsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3YTRlYmU5ZTI4NTljOGI4YmEwMDE4YzMwYzI0N2Q2ZDFhNDQ3OWRjIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0N2E0ZWJlOWUyODU5YzhiOGJhMDAxOGMzMGMyNDdkNmQxYTQ0NzlkYzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGF5YTA1NzlwdnUzdzk2cXF2dnhycHkwNGszNTNyZW1zeXJ6Z2FyOGgiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZDMzNDBkZGExOWU4ZTRiZTFjODc1MDA1YTczZjBlYWIwMjA5YWQ2ZmUyMTE0ZGY0MTQ3OGYzZDI0NTFlOWY2MyIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY4OTYyLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMzc1OGM0YzMzYjYwZTdjZmM2ZjFjZjcyMDhiMDk4NTgwYWNiZmMyZGIyNzY3NDNhNWFkMWZjYjE3MWEzYTRhOSIsInZvdXQiOlt7InZhbHVlIjowLjQ3MDU0NjM1LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOGMwZWFiZTZmMTExYmQ2ZGExMmViYTBlOGM5MjZlYmQzMjFiZGE3MiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDhjMGVhYmU2ZjExMWJkNmRhMTJlYmEwZThjOTI2ZWJkMzIxYmRhNzI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp4cWEybHg3eWdtNm1kcDk2YXFhcnlqZDY3bnl4NzZ3Z3hmMGp2OWNrIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMzk4Y2NkNGE5OTQ2OWM0ZGQ5ZTNmYWZjNTZkYTBmNGE4Mjg0ODIyYWU2M2EzMmI1N2U3MTEyZDZkOGE3YzMzZiIsInZvdXQiOlt7InZhbHVlIjowLjc0MzEzMDE2LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNjZmMjg3NzNjODAxZGVjNzIwYzBmNDYxMTg1YWIwOTgwZGQ4OGJiYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDY2ZjI4NzczYzgwMWRlYzcyMGMwZjQ2MTE4NWFiMDk4MGRkODhiYmM4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBuMDlwbW5lcXFhYTNlcWNyNnh6eHo2a3p2cW1reXRoc3pxNTVhZ21oIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYTc1ZWFmOGQ5MmUzYTJlZGU3ZTRkMWEyMzQ1YjViOTBlMGE2MzUxOTZhNWQyN2FmMjVlODE4YjU2MTFhYWRhMCIsInZvdXQiOlt7InZhbHVlIjowLjEsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCA3ZTc4ODdjOTdhY2FhOGUyNTZjOTMzZjRlNTg3MjNlMWY3ZTg4YjdlIE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0N2U3ODg3Yzk3YWNhYThlMjU2YzkzM2Y0ZTU4NzIzZTFmN2U4OGI3ZTg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcGw4M3A3ZjB0OTIzY2prZXllbGZldjh5MHNsMDZ5dDBjODg0dGgwOW4iXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOlt7InZhbHVlIjoxMDMuNTA2MjMwMDIsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IDUwZmMxNmZmYjM3N2NkNGUxZDhiNGRlZmEzNzU2ZDEyMjFiYWNmZTMzMDRlNzcwNWRlNDE2N2Q3N2MxYmEzNjQgT1BfRVFVQUwiLCJoZXgiOiJhYTIwNTBmYzE2ZmZiMzc3Y2Q0ZTFkOGI0ZGVmYTM3NTZkMTIyMWJhY2ZlMzMwNGU3NzA1ZGU0MTY3ZDc3YzFiYTM2NDg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cGRnMGM5aGxrZG11Nm5zYTNkeDdsZ200ZDVmenJ3azB1dmN5dWFjOW1lcWswNG11cncza2d5aGY1bW43eCJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiYjM4YTMzZjc1MGY4NGM1YzE2OWE2ZjIzY2I4NzNlNmU3OTYwNTAyMTU4NWQ0ZjM0MDg3ODk2ODllZDg3ZjM2NiIsImFtb3VudCI6IjQ4Njg2NzgifX1dfQ==",
+ "eyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOlt7InZhbHVlIjo5Mi4yNDA3MjM2OSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMWVlNDE0MmZmOTI4YjRhYTY5MmIzZGU4ZGUyOTI5YmUzOTA1YzYwMjdkM2JhOGIwYTJjZjFjMGI1NDg3YWNiMCBPUF9FUVVBTCIsImhleCI6ImFhMjAxZWU0MTQyZmY5MjhiNGFhNjkyYjNkZThkZTI5MjliZTM5MDVjNjAyN2QzYmE4YjBhMmNmMWMwYjU0ODdhY2IwODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdjB3ZzlwMGx5NXRmMm5mOXY3NzNoM2Y5eGxyanB3eHFmN25oMjlzNXQ4M2N6NjVzN2t0cWFocjl5YWdkIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDMzODc3NiJ9fV19",
+ "eyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOlt7InZhbHVlIjo3MS4xNjkzMTgyLCJuIjoyLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAyY2Y5YmY0ZGFiMDVhMjM1MjFkYzI2MjkzODMwMjYyZmZlYzc2MTQ2Njg4MDk4YmIzMTI0YWMzZmI3NjQwMDM2IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDJjZjliZjRkYWIwNWEyMzUyMWRjMjYyOTM4MzAyNjJmZmVjNzYxNDY2ODgwOThiYjMxMjRhYzNmYjc2NDAwMzY4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2azBuMDZkNHZ6NnlkZnBtc256andwc3ljaGxhM21wZ2U1Z3B4OW14eWoyYzBhaHZzcXJ2ZGZ4Y3Y3eWsiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIzMzQ3NjMwIn19XX0=",
+ "eyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOlt7InZhbHVlIjozNi4wNjYzNTQ0MiwibiI6Mywic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgNjdhZDFjNjQ5YjA5YmE1OGJhMTllYjNhZDM1ZjQyNDFiYTA2MGU4YmI4OTdhZGNmNmJkZGQzNmE4MGQ4ZThhNiBPUF9FUVVBTCIsImhleCI6ImFhMjA2N2FkMWM2NDliMDliYTU4YmExOWViM2FkMzVmNDI0MWJhMDYwZThiYjg5N2FkY2Y2YmRkZDM2YTgwZDhlOGE2ODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZG42NjhyeW52eW01azk2cjg0bjQ1NmxnZnFtNXBzdzN3dWYwdHcwZDB3YXg2NXFtcjUydnBmbjI1bXRrIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiMTY5NjQ3MiJ9fV19",
+ "eyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOlt7InZhbHVlIjoxNy41MDI1NjQwNSwibiI6NCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgMjBiMDViNzRhYjU2Yzk4NDgwMzJlOWJhNGZkMDk0Mjc1Y2NjN2YzNjI4YWQxODg2ZmNlNTZlOTMwZDEyNzNjZCBPUF9FUVVBTCIsImhleCI6ImFhMjAyMGIwNWI3NGFiNTZjOTg0ODAzMmU5YmE0ZmQwOTQyNzVjY2M3ZjM2MjhhZDE4ODZmY2U1NmU5MzBkMTI3M2NkODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwdnN0cWttNTRkdHZucHlxeHQ1bTVuN3Nqc240ZW5ybHhjNTI2eHl4bG5qa2F5Y2R6ZmV1NjlyZXl6bXF4Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiODIzMjc5In19XX0=",
+ "eyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOlt7InZhbHVlIjoxMS4zNTAzNDUzNSwibiI6NSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgYjM0N2Q2OGM4ZDJjZGIzMjA2NmJlOWFmODA0YzZmZmI4YWJiYjQ0MWJkZjM2Y2FmMzgzNDM2YmNmY2FhMzVjYiBPUF9FUVVBTCIsImhleCI6ImFhMjBiMzQ3ZDY4YzhkMmNkYjMyMDY2YmU5YWY4MDRjNmZmYjhhYmJiNDQxYmRmMzZjYWYzODM0MzZiY2ZjYWEzNWNiODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwd2U1MDQ1djM1a2RrdnN4ZDA1NmxxenZkbGFjNHdhNWd4N2x4bTkwOHE2cmQwOHU0ZzZ1a24zNzJnZ3dsIl19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNTMzODg2In19XX0=",
+ "eyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOlt7InZhbHVlIjoxMC41NDYwMTg0NCwibiI6Niwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0hBU0gyNTYgN2MyYmNjYTIwODlhOWYwZjhkYWVjMGQzYjQ1YTZjNTI1ODFjNTY3NjY1Y2M1MWViZGE5YTIwMmQ4YTMzZGNmYyBPUF9FUVVBTCIsImhleCI6ImFhMjA3YzJiY2NhMjA4OWE5ZjBmOGRhZWMwZDNiNDVhNmM1MjU4MWM1Njc2NjVjYzUxZWJkYTlhMjAyZDhhMzNkY2ZjODciLCJyZXFTaWdzIjoxLCJ0eXBlIjoic2NyaXB0aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpwZDd6aG45enB6ZGY3cnVkNG1xZDhkejZkM2Y5czh6a3dlanVjNTB0bTJkenF0djJ4MHcwYzU0eTM5aHA2Il19LCJ0b2tlbkRhdGEiOnsiY2F0ZWdvcnkiOiJiMzhhMzNmNzUwZjg0YzVjMTY5YTZmMjNjYjg3M2U2ZTc5NjA1MDIxNTg1ZDRmMzQwODc4OTY4OWVkODdmMzY2IiwiYW1vdW50IjoiNDk2MDUyIn19XX0=",
+ "eyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOlt7InZhbHVlIjo1LjIxNzgyNzEyLCJuIjo3LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBmMWMzM2JjN2RhMDk5MWQyNmY4YmJmMTUwYzkwMzU2OGIyYzNkYzg3MjczNzA4NjNmNGMzZDlmOTY1NWIzNzkxIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGYxYzMzYmM3ZGEwOTkxZDI2ZjhiYmYxNTBjOTAzNTY4YjJjM2RjODcyNzM3MDg2M2Y0YzNkOWY5NjU1YjM3OTE4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnAwY3V4dzc4bWd5ZXI1bjAzd2wzMnJ5c3g0NXQ5czd1c3Vubnd6cnI3bnBhbjd0OXR2bWV6cmF1M2p1cjkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyNDU0MzIifX1dfQ==",
+ "eyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOlt7InZhbHVlIjo0LjQwMzM4MzAzLCJuIjo4LCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiAzYmIyNGZmOTBiMjIxOWEyNjBkMGVlMGExNjg2N2ZkMzM0NjljZjcwNGY0OTAyMjMxYjE0ZTk2M2VjYmFlM2M0IE9QX0VRVUFMIiwiaGV4IjoiYWEyMDNiYjI0ZmY5MGIyMjE5YTI2MGQwZWUwYTE2ODY3ZmQzMzQ2OWNmNzA0ZjQ5MDIyMzFiMTRlOTYzZWNiYWUzYzQ4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB2YW15bmxlcHYzcG5nbnE2cmhxNTk1eDBsZm5nNncwd3A4NWpxM3JydjJ3amNsdmh0M3VneDRrYWo0aHUiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMDcxMjIifX1dfQ==",
+ "eyJ0eGlkIjoiOThhZmIyOWZjMzM4MzBkZDNjZGM5OTZiZTUxMTA4Y2YxMmE4YTA2N2RhMjU0NTQ1ZWZhYWFkZjBkMjQ3NjQxNSIsInZvdXQiOlt7InZhbHVlIjo4ZS0wNiwibiI6MjEsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX0sInRva2VuRGF0YSI6eyJjYXRlZ29yeSI6ImIzOGEzM2Y3NTBmODRjNWMxNjlhNmYyM2NiODczZTZlNzk2MDUwMjE1ODVkNGYzNDA4Nzg5Njg5ZWQ4N2YzNjYiLCJhbW91bnQiOiIyMzM0MSJ9fV19",
+ "eyJ0eGlkIjoiNjc3M2ZkODFjMmY2ZWQyNTg3YmQ2MWE2ZTBjNmE4NzNmOWYyYjYyMmZjNWU5YzFmODUwNGQwNjIxMGMzYzRhZiIsInZvdXQiOlt7InZhbHVlIjozNy4wMjQ1ODQ3NiwibiI6MTAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9EVVAgT1BfSEFTSDE2MCAxMmZjYjcwNmZhNjYyYjJmN2VmNjQwODNmYWIyZTA1NDc1Zjc1ZGQ3IE9QX0VRVUFMVkVSSUZZIE9QX0NIRUNLU0lHIiwiaGV4IjoiNzZhOTE0MTJmY2I3MDZmYTY2MmIyZjdlZjY0MDgzZmFiMmUwNTQ3NWY3NWRkNzg4YWMiLCJyZXFTaWdzIjoxLCJ0eXBlIjoicHVia2V5aGFzaCIsImFkZHJlc3NlcyI6WyJiaXRjb2luY2FzaDpxcWYwZWRjeGxmbnprdG03N2VxZzg3NGp1cDI4dGE2YTZ1NXJ5NXNmdTkiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiM2QyZTIwZjBkNzU1MjEyOWQyMDZjNzNhY2M5NTAzNGIyYzY0ZDY1ZjAyNzY4MmI4OTlhZDQzYTM1YWY3ZDM5MyIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY2MDEsIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiOGI2N2YzNGFiYjM1NjA4ZDMxM2Y4NGEwMDI5MmQ3MjkxZDM5MjUyNGQ1YjRmMmE5NjFhNGUwNjdjNTI2ZmM3YiIsInZvdXQiOlt7InZhbHVlIjowLjAwNTczNTU0LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNDgzZDI1OTVjNGY3YjEyOGJjOTkxNDYxNjYyZTk3N2I2NTJiNjlhMTQyNDRjOThkMDYzZDljODY1ODM0ZWMyNCIsInZvdXQiOlt7InZhbHVlIjowLjUwMzAxMzEzLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTE3NWUzOGE4ODJiYmFkY2EyNmNlNGMwNTFmNWFhNjZmOTlmNjk2ZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkxNzVlMzhhODgyYmJhZGNhMjZjZTRjMDUxZjVhYTY2Zjk5ZjY5NmQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpnaHRjdTIzcTRtNGg5emRuanZxNTA0NGZuMG44bWZkNW14anJ3N3JuIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiNjZlMzM2YTg0NzMxODkxNjM1MDNhMTJhZjlmM2QxOWFhNmRhNDAzN2M1NDZiMjM0YWZmYmQ0YTJmNGQ1YWU0OCIsInZvdXQiOlt7InZhbHVlIjo3LjUyNDU1MDI4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTM0OWQwNDQxMjFlMzg4NDQyNTk2ZDdhOTcyZjQyYWQ0NzUyMTEyMCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGEzNDlkMDQ0MTIxZTM4ODQ0MjU5NmQ3YTk3MmY0MmFkNDc1MjExMjA4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXozNW41enl6ZzByM3B6enQ5a2g0OWUwZzJrNXc1czN5cWE4ZWpjbHRlIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYzFiNTZlNWE2OWVkYWU2ZWRjNDdmOTU0OTdjNTVkMjNhM2JkOWQ4MjQ3NTdmMTBhMzIxMWM2MWExNmFjNGYwOCIsInZvdXQiOlt7InZhbHVlIjoyLjE3MDE4Nzc4LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOTBmMjI4NTM5NTE5OTMxNTQwZjBlMWRlMjVkYmMwMDkzNDk4YzdhNSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDkwZjIyODUzOTUxOTkzMTU0MGYwZTFkZTI1ZGJjMDA5MzQ5OGM3YTU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpnMHkyem5qNXZleDkycTdyc2F1ZndtY3F5bmZ4eDg1NWQ4NjZjeTZ0Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiOTEyNWNmZDZkMDNiZTMyYTU0NTViMzdjMmMxMmJjZGQ3YmFhZjdjZDUwMmJhOWIzNDhiZjM1NjdhNTVlZWQxYSIsInZvdXQiOlt7InZhbHVlIjoxZS0wNSwibiI6MSwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ4NjQwMzY1N2YwYzdiMTc4OWViMjM0NzJmMTA3MjUwNjE5NDBmN2I0ODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnF6cnlxZGpoN3JybXo3eTdrZzY4OXVnOHk1cnBqczhoa3M1bW1leGE0MyJdfSwidG9rZW5EYXRhIjp7ImNhdGVnb3J5IjoiNWE0ZjZiMjUyNDNjMWEyZGFiYjI0MzRlM2Q5ZTU3NGY2NWMzMTc2NGNlMGU3ZWI0MTI3YTQ2ZmE3NDY1NzY5MSIsImFtb3VudCI6IjAiLCJuZnQiOnsiY2FwYWJpbGl0eSI6Im5vbmUiLCJjb21taXRtZW50IjoiMjIwMTAyIn19fV19",
+ "eyJ0eGlkIjoiZDFkMTBmODU0ZTNkY2Q3NjUyZTg1YTk1OThlM2RjODI1MGY5NDgwN2VjN2QzNzlkZjFhNGFjMWY4M2YzYWIxOCIsInZvdXQiOlt7InZhbHVlIjowLjA3MTM4ODE4LCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgODY0MDM2NTdmMGM3YjE3ODllYjIzNDcyZjEwNzI1MDYxOTQwZjdiNCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDg2NDAzNjU3ZjBjN2IxNzg5ZWIyMzQ3MmYxMDcyNTA2MTk0MGY3YjQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpyeXFkamg3cnJtejd5N2tnNjg5dWc4eTVycGpzOGhrczVtbWV4YTQzIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZjkxODBhMmUzY2NiN2MxMzZhNmNhOTkzYTEyMWRkMTBhZjgzZmRhOTY5MTNiMTUzOTAwNDVhNGJjNmJiNzY0NyIsInZvdXQiOlt7InZhbHVlIjowLjEwMTc1OTIzLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNmMzYWUyNTU3MzRjMWJlNDkwZWM0NzkxMDI0N2Q1YWY4NjBlNjYzYSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDZjM2FlMjU1NzM0YzFiZTQ5MGVjNDc5MTAyNDdkNWFmODYwZTY2M2E4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXBrcjRjajR3ZHhwaGV5c2EzcmV6cWo4NmtoY3Zybng4ZzB5OWF6Y2Q1Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYmI4MzVmMzhhN2YxMGM3NTgyYTE5NTlkYTQ3OWU3YzQ4ZDc2NjYyNzYwOTgzNzgzOTY5ZDAwMGIzMWRhZmM0ZCIsInZvdXQiOlt7InZhbHVlIjowLjAwMDk4MzExLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTJkYjRhNmMwMDc2N2E0NjY2ODhlNDEzNjIzNDdjZDY1YzVhZTMwZCBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGEyZGI0YTZjMDA3NjdhNDY2Njg4ZTQxMzYyMzQ3Y2Q2NWM1YWUzMGQ4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXozZGtqbnZxcG04NTNueDNyanB4YzM1MG50OWNraHJwNTQ0bG40NGhhIl19fV19",
+ "eyJ0eGlkIjoiNTg0MjdmMDcxYTJhNzE4YTM3YmU3OWYxZTQ4N2M0ZTVhYWZkYzY5OTNmMzg4OWViMTYyNzI2YTM5YmE3NDQxOSIsInZvdXQiOlt7InZhbHVlIjowLjIwNTI1ODAxLCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgOGM5ZWNkODQ5NmIyZDY5YjE2N2VhNmNjOTFhYzE4MTU4NjMyZTk0ZSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDhjOWVjZDg0OTZiMmQ2OWIxNjdlYTZjYzkxYWMxODE1ODYzMmU5NGU4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXp4ZmFudnlqNmVkZHhjazA2bnZleWR2cnEyY3Z2aGZmY25nYWc1aHNyIl19fV19",
+ "eyJ0eGlkIjoiYjRiMWQ2MDIyOWQzYjhkNjNjNjZlM2NiN2IyN2Q0MjBkYjU5YTBkYzM4ZjkxYjFjOGU2NjA5ZTU1M2NjNjE3MyIsInZvdXQiOlt7InZhbHVlIjo1LjQ2ZS0wNiwibiI6MCwic2NyaXB0UHViS2V5Ijp7ImFzbSI6Ik9QX0RVUCBPUF9IQVNIMTYwIDZhZGE4NDdmZDM5NWIzZTY1OGY2NzkwYmQ4OTlhYWY5NTczM2EwNjMgT1BfRVFVQUxWRVJJRlkgT1BfQ0hFQ0tTSUciLCJoZXgiOiI3NmE5MTQ2YWRhODQ3ZmQzOTViM2U2NThmNjc5MGJkODk5YWFmOTU3MzNhMDYzODhhYyIsInJlcVNpZ3MiOjEsInR5cGUiOiJwdWJrZXloYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnFwNGQ0cHJsNncybThlamM3ZXVzaGt5ZTR0dTR3dmFxdnZmOWZwN2RjdiJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYjg3OTk4ZGUyYmJiNDgzMTE3MzQwYzdmNjU0OWM5ODUwMTRjODhkYmRiZTBjMTk5Y2NkODUyZGQwMWQ2YjlhYSIsInZvdXQiOlt7InZhbHVlIjowLjAwNTY1ODQ2LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfSEFTSDI1NiBhMWMzYTZhOTVjZTFjNDcyNjIyMWRjM2U1ZjlkMjMxNjExMTc2YjM5ZjNjNzMxY2IwZDI0YzY1MGVlOWI0YjRjIE9QX0VRVUFMIiwiaGV4IjoiYWEyMGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGM4NyIsInJlcVNpZ3MiOjEsInR5cGUiOiJzY3JpcHRoYXNoIiwiYWRkcmVzc2VzIjpbImJpdGNvaW5jYXNoOnB3c3U4ZjRmdG5zdWd1bnp5OHdydWh1YXl2dHB6OW10ODhldXd2d3RwNWp2djU4d25kOTVjMHRkM3dwZHAiXX19XX0="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiZmEwZjJmMWZhMjNmYzZiNzliNTFiZGU5ZjlhNDdlY2Y0ZTNiZTA0MmUzZTM0NjJjMWEyYTY2M2Q2ODQzY2VlNyIsInZvdXQiOlt7InZhbHVlIjoxLjAwNTAwMTAxLCJuIjoxLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNTU5MmI4ZmFlMzQ5OTAwNjM5YjFkYTVhY2I5MDJmNmEwMGRlZWIzYyBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDU1OTJiOGZhZTM0OTkwMDYzOWIxZGE1YWNiOTAyZjZhMDBkZWViM2M4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXAyZTl3ODZ1ZHllcXAzZWs4ZDk0anVzOWE0cXBoaHQ4c2Q3c3U3Mzc0Il19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiMjk5NDU5ZDcwNWRmMGEzNTE4Mzk3NTFlYjdiYTkyZWQ4YzIxYmIxMzRlNTc1ZGJlZmE3YjU3ODlmOWZlMmIwNiIsInZvdXQiOlt7InZhbHVlIjowLjAwNTczMzksIm4iOjAsInNjcmlwdFB1YktleSI6eyJhc20iOiJPUF9IQVNIMjU2IGExYzNhNmE5NWNlMWM0NzI2MjIxZGMzZTVmOWQyMzE2MTExNzZiMzlmM2M3MzFjYjBkMjRjNjUwZWU5YjRiNGMgT1BfRVFVQUwiLCJoZXgiOiJhYTIwYTFjM2E2YTk1Y2UxYzQ3MjYyMjFkYzNlNWY5ZDIzMTYxMTE3NmIzOWYzYzczMWNiMGQyNGM2NTBlZTliNGI0Yzg3IiwicmVxU2lncyI6MSwidHlwZSI6InNjcmlwdGhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cHdzdThmNGZ0bnN1Z3Vuenk4d3J1aHVheXZ0cHo5bXQ4OGV1d3Z3dHA1anZ2NTh3bmQ5NWMwdGQzd3BkcCJdfX1dfQ=="
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiN2FkODFiN2MwNzcxNWQyNWI3OGRjNWZiMmJhY2MwOWYzN2NhZmFjYzQ5YWQ1YWEyMmUwMjlhZmNmZTIwNDgyYyIsInZvdXQiOlt7InZhbHVlIjo1LjQ4MDY1MzI1LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgNjM2ODJjNGJmMTkwZDVlYTFkZWRkYTExMWRkMTNlNGQ4M2FmODM1OSBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNDYzNjgyYzRiZjE5MGQ1ZWExZGVkZGExMTFkZDEzZTRkODNhZjgzNTk4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXAza3N0enQ3eGdkdDZzYWFoZHB6OHczOGV4Yzh0dXJ0eThkdnZ2Z3puIl19fV19"
+ ]
+ },
+ {
+ "data": [
+ "eyJ0eGlkIjoiYTgyZmM3MWRlYWJhOWZkNWU3OTU2M2Y5OTgyOTZjZjlkMWM2NGI1ZTA4MjdjYWZkMDFlMDhmNDRmZThhNzU5ZSIsInZvdXQiOlt7InZhbHVlIjowLjEzMDU1MDY5LCJuIjowLCJzY3JpcHRQdWJLZXkiOnsiYXNtIjoiT1BfRFVQIE9QX0hBU0gxNjAgYTVhNTE5NmFmMTNiOWIyN2IxMmNmMjg5YjViZWE5ZTI5OTc2MTFkMiBPUF9FUVVBTFZFUklGWSBPUF9DSEVDS1NJRyIsImhleCI6Ijc2YTkxNGE1YTUxOTZhZjEzYjliMjdiMTJjZjI4OWI1YmVhOWUyOTk3NjExZDI4OGFjIiwicmVxU2lncyI6MSwidHlwZSI6InB1YmtleWhhc2giLCJhZGRyZXNzZXMiOlsiYml0Y29pbmNhc2g6cXpqNjJ4dDI3eWFla2ZhMzluZWduZGQ3NDgzZmphczM2ZzJ0anNqMGdlIl19fV19"
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/internal/utils/fixtures/parser/bitcoin/dash_get_block_pubkey_case.json b/internal/utils/fixtures/parser/bitcoin/dash_get_block_pubkey_case.json
new file mode 100644
index 00000000..e4c05b10
--- /dev/null
+++ b/internal/utils/fixtures/parser/bitcoin/dash_get_block_pubkey_case.json
@@ -0,0 +1,44 @@
+{
+ "hash": "000000000000002e6ea46fd2bea0a74e0c2aeeb4fa5aca704d6a9e13db47079c",
+ "confirmations": 100,
+ "size": 215,
+ "height": 45980,
+ "version": 1,
+ "versionHex": "00000001",
+ "merkleroot": "3a3be27d4f51d1f3e5870aaa0045f9c3b823c1e7d723412cc58d2de7d48b3646",
+ "tx": [
+ {
+ "txid": "3a3be27d4f51d1f3e5870aaa0045f9c3b823c1e7d723412cc58d2de7d48b3646",
+ "version": 1,
+ "size": 134,
+ "locktime": 0,
+ "vin": [
+ {
+ "coinbase": "046f7f381c0119",
+ "sequence": 4294967295
+ }
+ ],
+ "vout": [
+ {
+ "value": 50,
+ "n": 0,
+ "scriptPubKey": {
+ "asm": "0457b9b91fc85f1cccdf728a367dc556366a3e0aec97af5c559d840df3b1fc8e6661ed0f4ab8d7473a289aea4137259431d2080300236eec4e39cc91fc71e6191d OP_CHECKSIG",
+ "hex": "410457b9b91fc85f1cccdf728a367dc556366a3e0aec97af5c559d840df3b1fc8e6661ed0f4ab8d7473a289aea4137259431d2080300236eec4e39cc91fc71e6191dac",
+ "type": "pubkey"
+ }
+ }
+ ],
+ "hex": "01000000010000000000000000000000000000000000000000000000000000000000000000ffffffff07046f7f381c0119ffffffff0100f2052a0100000043410457b9b91fc85f1cccdf728a367dc556366a3e0aec97af5c559d840df3b1fc8e6661ed0f4ab8d7473a289aea4137259431d2080300236eec4e39cc91fc71e6191dac00000000"
+ }
+ ],
+ "time": 1269036081,
+ "mediantime": 1269032765,
+ "nonce": 4655188,
+ "bits": "1c387f6f",
+ "difficulty": 4.531081750070693,
+ "chainwork": "0000000000000000000000000000000000000000000000000000f904f75a69c1",
+ "nTx": 1,
+ "previousblockhash": "0000000007b926517e87d96aba64fdfe0902945dc7a58cbe3f5f42ec15eb7a53",
+ "nextblockhash": "000000001f9af4a7491ebb542c8b95b1f36d828eef3590713e0658ebaa79c7ac"
+}
diff --git a/internal/utils/fixtures/parser/bitcoin/get_block_native.json b/internal/utils/fixtures/parser/bitcoin/get_block_native.json
index e618b804..0c2402a6 100644
--- a/internal/utils/fixtures/parser/bitcoin/get_block_native.json
+++ b/internal/utils/fixtures/parser/bitcoin/get_block_native.json
@@ -11,6 +11,7 @@
"time": 1629306034,
"median_time": 1629301309,
"nonce": 1890536004,
+ "raw_nonce": "1890536004",
"bits": "1712180b",
"difficulty": "15556093717702.55",
"chain_work": "00000000000000000000000000000000000000002093b0274937449851e42ac8",
diff --git a/internal/utils/fixtures/parser/tron/raw_block_header.json b/internal/utils/fixtures/parser/tron/raw_block_header.json
new file mode 100644
index 00000000..e3451e40
--- /dev/null
+++ b/internal/utils/fixtures/parser/tron/raw_block_header.json
@@ -0,0 +1,58 @@
+{
+ "baseFeePerGas": "0x0",
+ "difficulty": "0x0",
+ "extraData": "0x",
+ "gasLimit": "0x2b3b43dc6",
+ "gasUsed": "0xb1006d",
+ "hash": "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "miner": "0x8b0359acac03bac62cbf89c4b787cb10b3c3f513",
+ "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "nonce": "0x0000000000000000",
+ "number": "0x4034f5c",
+ "parentHash": "0x0000000004034f5b43c5934257b3d1f1a313bba4af0a4dd2f778fda9e641b615",
+ "receiptsRoot": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "sha3Uncles": "0x0000000000000000000000000000000000000000000000000000000000000000",
+ "size": "0x1a366",
+ "stateRoot": "0x",
+ "timestamp": "0x6745cb8a",
+ "totalDifficulty": "0x0",
+ "transactions": [
+ {
+ "blockHash": "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ "blockNumber": "0x4034f5c",
+ "from": "0x25a51e3e65287539b8d4eb559cbca4488a08bb00",
+ "gas": "0x1fced",
+ "gasPrice": "0xd2",
+ "hash": "0xd581afa9158fbed69fb10d6a2245ad45d912a3da03ff24d59f3d2f6df6fd9529",
+ "input": "0xa9059cbb0000000000000000000000009dc5da2b3c502661c8448ba88bacf7f0b22272ad0000000000000000000000000000000000000000000000000000000000027165",
+ "nonce": "0x0000000000000000",
+ "r": "0x8178c20b4100cdab4eadd22cefb4944504b51272d6693a4e5b4a00ae8b237313",
+ "s": "0x36acd444b8e94dc157824da1aba4325df38e2c8e806826f4c71b06148e88dd91",
+ "to": "0xa614f803b6fd780986a42c78ec9c7f77e6ded13c",
+ "transactionIndex": "0x0",
+ "type": "0x0",
+ "v": "0x1c",
+ "value": "0x0"
+ },
+ {
+ "blockHash": "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ "blockNumber": "0x4034f5c",
+ "from": "0x89ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "gas": "0x12197",
+ "gasPrice": "0xd2",
+ "hash": "0xe14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ "input": "0xaf6f48960000000000000000000000004d12f87c18a914dddbc2b27f378ad126a79b76b6000000000000000000000000000000000000000000000000000000bf9e4899ba0000000000000000000000000000000000000000000000000000000000000001",
+ "nonce": "0x0000000000000000",
+ "r": "0xe30301c81bcbdf7e69116543964366b84bd34606115cc5cae96927fb5214a6ea",
+ "s": "0x219db63879a044df44b855f6e481398942c9d5ab774a2a1fae16d3646f418e1f",
+ "to": "0xc60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "transactionIndex": "0x45",
+ "type": "0x0",
+ "v": "0x1b",
+ "value": "0x0"
+ }
+ ],
+ "transactionsRoot": "0xd270690faa58558c2b03ae600334f71f9d5a0ad42d7313852fb3742e8576eec9",
+ "uncles": []
+}
\ No newline at end of file
diff --git a/internal/utils/fixtures/parser/tron/raw_block_trace_tx_info.json b/internal/utils/fixtures/parser/tron/raw_block_trace_tx_info.json
new file mode 100644
index 00000000..99d0dffd
--- /dev/null
+++ b/internal/utils/fixtures/parser/tron/raw_block_trace_tx_info.json
@@ -0,0 +1,193 @@
+[
+ {
+ "type": "TransferContract",
+ "log": [
+ {
+ "address": "a614f803b6fd780986a42c78ec9c7f77e6ded13c",
+ "data": "0000000000000000000000000000000000000000000000000000000000027165",
+ "topics": [
+ "ddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "00000000000000000000000025a51e3e65287539b8d4eb559cbca4488a08bb00",
+ "0000000000000000000000009dc5da2b3c502661c8448ba88bacf7f0b22272ad"
+ ]
+ }
+ ],
+ "blockNumber": 67325788,
+ "contractResult": [
+ "0000000000000000000000000000000000000000000000000000000000000000"
+ ],
+ "blockTimeStamp": 1732627338000,
+ "receipt": {
+ "result": "SUCCESS",
+ "energy_penalty_total": 100635,
+ "energy_usage": 130285,
+ "energy_usage_total": 130285,
+ "net_usage": 345
+ },
+ "id": "d581afa9158fbed69fb10d6a2245ad45d912a3da03ff24d59f3d2f6df6fd9529",
+ "contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c"
+ },
+ {
+ "type": "TransferAssetContract",
+ "log": [
+ {
+ "address": "c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "data": "00000000000000000000000000000000000000000000000000000001f9873bc7000000000000000000000000000000000000000000000000093732ae413feb69000000000000000000000000000000000000000000000000093732b42dd59ebe0000000000000000000000000000000000000000000000000000801f33d9f651000000000000000000000000000000000000000000000000000000000036b158",
+ "topics": [
+ "da6e3523d5765dedff9534b488c7e508318178571c144293451989755e9379e7",
+ "0000000000000000000000000000000000000000000000000000000000000001"
+ ]
+ },
+ {
+ "address": "c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "data": "000000000000000000000000000000000000000000000000093732a856669e8f000000000000000000000000000000000000000000000000093732b42dd59ebe000000000000000000000000000000000000000000000000000000bf9e4899ba000000000000000000000000000000000000000000000000000000000000a3810000000000000000000000000000000000000000000000000000000000000000",
+ "topics": [
+ "74fed619850adf4ba83cfb92b9566b424e3de6de4d9a7adc3b1909ea58421a55",
+ "00000000000000000000000089ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "0000000000000000000000004d12f87c18a914dddbc2b27f378ad126a79b76b6",
+ "0000000000000000000000000000000000000000000000000000000000000001"
+ ]
+ },
+ {
+ "address": "c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "data": "000000000000000000000000000000000000000000000000000000bf9e4899ba",
+ "topics": [
+ "f2def54ec5eba61fd8f18d019c7beaf6a47df317fb798b3263ad69ec227c9261",
+ "00000000000000000000000089ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "0000000000000000000000004d12f87c18a914dddbc2b27f378ad126a79b76b6",
+ "0000000000000000000000000000000000000000000000000000000000000001"
+ ]
+ },
+ {
+ "address": "c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "data": "000000000000000000000000000000000000000000000000000000bf9e4899ba0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000c032ffd0000000000000000000000000000000000000000000000000000000054e4691a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000093732b42dd59ebe",
+ "topics": [
+ "f7e21d5bf17851f93ab7bda7e390841620f59dfbe9d86add32824f33bd40d3f5",
+ "00000000000000000000000089ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "0000000000000000000000004d12f87c18a914dddbc2b27f378ad126a79b76b6"
+ ]
+ }
+ ],
+ "blockNumber": 67325788,
+ "contractResult": [
+ "0000000000000000000000000000000000000000000000000000000054e4691a"
+ ],
+ "blockTimeStamp": 1732627338000,
+ "fee": 379,
+ "receipt": {
+ "result": "SUCCESS",
+ "energy_usage": 68976,
+ "energy_usage_total": 74135,
+ "origin_energy_usage": 5159,
+ "net_fee": 379
+ },
+ "id": "e14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ "contract_address": "41c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "internal_transactions": [
+ {
+ "caller_address": "41c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "note": "63616c6c",
+ "transferTo_address": "41c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "callValueInfo": [
+ {
+ "callValue": 100
+ },
+ {
+ "callValue": 100
+ }
+ ],
+ "hash": "499bdbdfaae021dd510c70b433bc48d88d8ca6e0b7aee13ce6d726114e365aaf"
+ },
+ {
+ "caller_address": "41c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "note": "63616c6c",
+ "transferTo_address": "41e8667633c747066c70672c58207cc745a9860527",
+ "callValueInfo": [
+ {
+ "tokenId": "1004777",
+ "callValue": 1000000000000000
+ },
+ {
+ "callValue": 1000
+ }
+ ],
+ "hash": "997225b56440a9bd172f05f44a663830b72093a12502551cda99b0bc7c60cbc1"
+ },
+ {
+ "caller_address": "41c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "note": "63616c6c",
+ "transferTo_address": "41e8667633c747066c70672c58207cc745a9860527",
+ "callValueInfo": [
+ {
+ "tokenId": "1004777",
+ "callValue": 1000
+ },
+ {
+ "tokenId": "1004777",
+ "callValue": 100
+ }
+ ],
+ "hash": "7ac8dd16dede5c512330f5033c8fd6f5390d742aa51b805f805098109eb54fe9"
+ },
+ {
+ "caller_address": "41c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "note": "63616c6c",
+ "transferTo_address": "41c64e69acde1c7b16c2a3efcdbbdaa96c3644c2b3",
+ "callValueInfo": [
+ {
+ "tokenId": "1004777",
+ "callValue": 100
+ },
+ {
+ "callValue": 100000
+ }
+ ],
+ "hash": "cf6f699d9bdae8aa25fae310a06bb60a29a7812548cf3c1d83c737fd1a22c0ee"
+ },
+ {
+ "caller_address": "41c64e69acde1c7b16c2a3efcdbbdaa96c3644c2b3",
+ "note": "63616c6c",
+ "transferTo_address": "41c64e69acde1c7b16c2a3efcdbbdaa96c3644c2b3",
+ "callValueInfo": [
+ {}
+ ],
+ "hash": "95787b9a6558c7b6b624d0c1bece9723a7f4c3d414010b6ac105ae5f5aebffbc"
+ },
+ {
+ "caller_address": "41c64e69acde1c7b16c2a3efcdbbdaa96c3644c2b3",
+ "note": "756e44656c65676174655265736f757263654f66456e65726779",
+ "transferTo_address": "414d12f87c18a914dddbc2b27f378ad126a79b76b6",
+ "callValueInfo": [
+ {
+ "callValue": 822994311610
+ },
+ {
+ "callValue": 2000000
+ }
+
+ ],
+ "hash": "14526162e31d969ef0dca9b902d51ecc0ffab87dc936dce62022f368119043af"
+ },
+ {
+ "caller_address": "41c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "note": "63616c6c",
+ "transferTo_address": "41e8667633c747066c70672c58207cc745a9860527",
+ "callValueInfo": [
+ {}
+ ],
+ "hash": "8e088220a26ca8d794786e78096e71259cf8744cccdc4f07a8129aa8ee29bb98"
+ },
+ {
+ "caller_address": "41c60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "note": "63616c6c",
+ "transferTo_address": "4189ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "callValueInfo": [
+ {
+ "callValue": 1424255258
+ }
+ ],
+ "hash": "83b1d41ba953aab4da6e474147f647599ea53bb3213306897127b57e85ddd1ca"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/internal/utils/fixtures/parser/tron/raw_block_tx_receipt.json b/internal/utils/fixtures/parser/tron/raw_block_tx_receipt.json
new file mode 100644
index 00000000..e5f9a5bb
--- /dev/null
+++ b/internal/utils/fixtures/parser/tron/raw_block_tx_receipt.json
@@ -0,0 +1,112 @@
+[
+ {
+ "blockHash": "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ "blockNumber": "0x4034f5c",
+ "contractAddress": null,
+ "cumulativeGasUsed": "0x1fced",
+ "effectiveGasPrice": "0xd2",
+ "from": "0x25a51e3e65287539b8d4eb559cbca4488a08bb00",
+ "gasUsed": "0x1fced",
+ "logs": [
+ {
+ "address": "0xa614f803b6fd780986a42c78ec9c7f77e6ded13c",
+ "blockHash": "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ "blockNumber": "0x4034f5c",
+ "data": "0x0000000000000000000000000000000000000000000000000000000000027165",
+ "logIndex": "0x0",
+ "removed": false,
+ "topics": [
+ "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
+ "0x00000000000000000000000025a51e3e65287539b8d4eb559cbca4488a08bb00",
+ "0x0000000000000000000000009dc5da2b3c502661c8448ba88bacf7f0b22272ad"
+ ],
+ "transactionHash": "0xd581afa9158fbed69fb10d6a2245ad45d912a3da03ff24d59f3d2f6df6fd9529",
+ "transactionIndex": "0x0"
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "status": "0x1",
+ "to": "0xa614f803b6fd780986a42c78ec9c7f77e6ded13c",
+ "transactionHash": "0xd581afa9158fbed69fb10d6a2245ad45d912a3da03ff24d59f3d2f6df6fd9529",
+ "transactionIndex": "0x0",
+ "type": "0x0"
+ },
+ {
+ "blockHash": "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ "blockNumber": "0x4034f5c",
+ "contractAddress": null,
+ "cumulativeGasUsed": "0x15dc77",
+ "effectiveGasPrice": "0xd2",
+ "from": "0x89ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "gasUsed": "0x12197",
+ "logs": [
+ {
+ "address": "0xc60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "blockHash": "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ "blockNumber": "0x4034f5c",
+ "data": "0x00000000000000000000000000000000000000000000000000000001f9873bc7000000000000000000000000000000000000000000000000093732ae413feb69000000000000000000000000000000000000000000000000093732b42dd59ebe0000000000000000000000000000000000000000000000000000801f33d9f651000000000000000000000000000000000000000000000000000000000036b158",
+ "logIndex": "0x10",
+ "removed": false,
+ "topics": [
+ "0xda6e3523d5765dedff9534b488c7e508318178571c144293451989755e9379e7",
+ "0x0000000000000000000000000000000000000000000000000000000000000001"
+ ],
+ "transactionHash": "0xe14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ "transactionIndex": "0x45"
+ },
+ {
+ "address": "0xc60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "blockHash": "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ "blockNumber": "0x4034f5c",
+ "data": "0x000000000000000000000000000000000000000000000000093732a856669e8f000000000000000000000000000000000000000000000000093732b42dd59ebe000000000000000000000000000000000000000000000000000000bf9e4899ba000000000000000000000000000000000000000000000000000000000000a3810000000000000000000000000000000000000000000000000000000000000000",
+ "logIndex": "0x11",
+ "removed": false,
+ "topics": [
+ "0x74fed619850adf4ba83cfb92b9566b424e3de6de4d9a7adc3b1909ea58421a55",
+ "0x00000000000000000000000089ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "0x0000000000000000000000004d12f87c18a914dddbc2b27f378ad126a79b76b6",
+ "0x0000000000000000000000000000000000000000000000000000000000000001"
+ ],
+ "transactionHash": "0xe14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ "transactionIndex": "0x45"
+ },
+ {
+ "address": "0xc60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "blockHash": "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ "blockNumber": "0x4034f5c",
+ "data": "0x000000000000000000000000000000000000000000000000000000bf9e4899ba",
+ "logIndex": "0x12",
+ "removed": false,
+ "topics": [
+ "0xf2def54ec5eba61fd8f18d019c7beaf6a47df317fb798b3263ad69ec227c9261",
+ "0x00000000000000000000000089ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "0x0000000000000000000000004d12f87c18a914dddbc2b27f378ad126a79b76b6",
+ "0x0000000000000000000000000000000000000000000000000000000000000001"
+ ],
+ "transactionHash": "0xe14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ "transactionIndex": "0x45"
+ },
+ {
+ "address": "0xc60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "blockHash": "0x0000000004034f5cd8946001c721db6457608ad887b3734c825d55826c3c3c87",
+ "blockNumber": "0x4034f5c",
+ "data": "0x000000000000000000000000000000000000000000000000000000bf9e4899ba0000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000c032ffd0000000000000000000000000000000000000000000000000000000054e4691a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000093732b42dd59ebe",
+ "logIndex": "0x13",
+ "removed": false,
+ "topics": [
+ "0xf7e21d5bf17851f93ab7bda7e390841620f59dfbe9d86add32824f33bd40d3f5",
+ "0x00000000000000000000000089ae01b878dffc8088222adf1fb08ebadfeea53a",
+ "0x0000000000000000000000004d12f87c18a914dddbc2b27f378ad126a79b76b6"
+ ],
+ "transactionHash": "0xe14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ "transactionIndex": "0x45"
+ }
+ ],
+ "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
+ "status": "0x1",
+ "to": "0xc60a6f5c81431c97ed01b61698b6853557f3afd4",
+ "transactionHash": "0xe14935e6144007163609bb49292897ba81bf7ee93bf28ba4cc5ebd0d6b95f4b9",
+ "transactionIndex": "0x45",
+ "type": "0x0"
+ }
+]
\ No newline at end of file
diff --git a/internal/utils/instrument/instrument.go b/internal/utils/instrument/instrument.go
index ada8e15d..cd4961da 100644
--- a/internal/utils/instrument/instrument.go
+++ b/internal/utils/instrument/instrument.go
@@ -244,7 +244,7 @@ func (i *instrumentWithResult[T]) onSuccess(logger *zap.Logger, span tracer.Span
func (i *instrumentWithResult[T]) onSuccessWithFilter(logger *zap.Logger, span tracer.Span, finishTime time.Time, err error) {
i.successWithFilter.Inc(1)
- logger.Info(i.loggerMsg, zap.Error(err))
+ logger.Debug(i.loggerMsg, zap.Error(err))
span.Finish(tracer.FinishTime(finishTime), tracer.WithError(err))
}
diff --git a/internal/utils/ratelimiter/rate_limiter.go b/internal/utils/ratelimiter/rate_limiter.go
index 1e18239b..02c3fd4e 100644
--- a/internal/utils/ratelimiter/rate_limiter.go
+++ b/internal/utils/ratelimiter/rate_limiter.go
@@ -55,6 +55,35 @@ func (l *RateLimiter) WaitN(ctx context.Context, n int) (err error) {
return l.limiter.WaitN(ctx, n)
}
+// WaitWeight blocks until it permits n events to happen. Unlike WaitN, n may
+// exceed the limiter's burst size: the wait is split into burst-sized chunks
+// so that the total number of tokens consumed is still n. A non-positive n is
+// treated as 1.
+func (l *RateLimiter) WaitWeight(ctx context.Context, n int) error {
+ if l == nil {
+ return nil
+ }
+
+ if n <= 0 {
+ n = 1
+ }
+
+ burst := l.limiter.Burst()
+ for n > 0 {
+ chunk := n
+ if chunk > burst {
+ chunk = burst
+ }
+
+ if err := l.limiter.WaitN(ctx, chunk); err != nil {
+ return err
+ }
+ n -= chunk
+ }
+
+ return nil
+}
+
// Limit returns the maximum overall event rate.
// Zero is returned when there is no limit specified.
func (l *RateLimiter) Limit() int {
diff --git a/internal/utils/ratelimiter/rate_limiter_test.go b/internal/utils/ratelimiter/rate_limiter_test.go
index 53c182e8..fcf94016 100644
--- a/internal/utils/ratelimiter/rate_limiter_test.go
+++ b/internal/utils/ratelimiter/rate_limiter_test.go
@@ -69,6 +69,38 @@ func TestWait(t *testing.T) {
require.True(duration <= 1.11)
}
+func TestWaitWeight(t *testing.T) {
+ require := testutil.Require(t)
+
+ // nil limiter is unlimited.
+ var nilLimiter *RateLimiter
+ require.NoError(nilLimiter.WaitWeight(context.Background(), 100))
+
+ // WaitN fails when n exceeds the burst, while WaitWeight waits in chunks.
+ limiter := New(20)
+ require.Error(limiter.WaitN(context.Background(), 25))
+
+ start := time.Now()
+ require.NoError(limiter.WaitWeight(context.Background(), 25))
+ // 25 tokens at 20 rps with burst 20: at least 5 tokens must be waited for.
+ require.True(time.Since(start) >= 200*time.Millisecond, "expected chunked wait, elapsed=%v", time.Since(start))
+}
+
+func TestWaitWeightClampsNonPositive(t *testing.T) {
+ require := testutil.Require(t)
+
+ for _, n := range []int{0, -5} {
+ limiter := New(1)
+ start := time.Now()
+ // A non-positive weight consumes a single token from the full bucket,
+ // so it must return immediately without error.
+ require.NoError(limiter.WaitWeight(context.Background(), n))
+ require.True(time.Since(start) < 500*time.Millisecond)
+ // The single token is spent: the next request is not allowed.
+ require.False(limiter.Allow())
+ }
+}
+
func tryLimiter(limiter *RateLimiter) bool {
for i := 0; i < 100; i++ {
if !limiter.Allow() {
diff --git a/internal/utils/utils/utils.go b/internal/utils/utils/utils.go
index daf850a7..d9c0dab5 100644
--- a/internal/utils/utils/utils.go
+++ b/internal/utils/utils/utils.go
@@ -37,6 +37,19 @@ func ToTimestamp(seconds int64) *timestamppb.Timestamp {
}
}
+// ToTimestampFromMs converts a millisecond timestamp to a protobuf Timestamp,
+// preserving the sub-second precision in the Nanos field.
+func ToTimestampFromMs(ms int64) *timestamppb.Timestamp {
+ if ms == 0 {
+ return nil
+ }
+
+ return ×tamppb.Timestamp{
+ Seconds: ms / 1000,
+ Nanos: int32((ms % 1000) * 1_000_000),
+ }
+}
+
func SinceTimestamp(timestamp *timestamppb.Timestamp) time.Duration {
var res time.Duration
if timestamp.GetSeconds() > 0 || timestamp.GetNanos() > 0 {
diff --git a/internal/workflow/activity/activity.go b/internal/workflow/activity/activity.go
index 62545d39..d49498b4 100644
--- a/internal/workflow/activity/activity.go
+++ b/internal/workflow/activity/activity.go
@@ -15,21 +15,28 @@ import (
)
const (
- ActivityExtractor = "activity.extractor"
- ActivityLoader = "activity.loader"
- ActivitySyncer = "activity.syncer"
- ActivityLivenessCheck = "activity.liveness_check"
- ActivityReader = "activity.reader"
- ActivityValidator = "activity.validator"
- ActivityStreamer = "activity.streamer"
- ActivityCrossValidator = "activity.cross_validator"
- ActivityEventReader = "activity.event_reader"
- ActivityEventReconciler = "activity.event_reconciler"
- ActivityEventLoader = "activity.event_loader"
- ActivityReplicator = "activity.replicator"
- ActivityUpdateWatermark = "activity.update_watermark"
-
- loggerMsg = "activity.request"
+ ActivityExtractor = "activity.extractor"
+ ActivityLoader = "activity.loader"
+ ActivitySyncer = "activity.syncer"
+ ActivityLivenessCheck = "activity.liveness_check"
+ ActivityReader = "activity.reader"
+ ActivityValidator = "activity.validator"
+ ActivityStreamer = "activity.streamer"
+ ActivityCrossValidator = "activity.cross_validator"
+ ActivityEventReader = "activity.event_reader"
+ ActivityEventReconciler = "activity.event_reconciler"
+ ActivityEventLoader = "activity.event_loader"
+ ActivityReplicator = "activity.replicator"
+ ActivityUpdateWatermark = "activity.update_watermark"
+ ActivityLatestBlock = "activity.latest_block"
+ ActivityBatchConsolidator = "activity.batch_consolidator"
+ ActivityBatchConsolidatorStats = "activity.batch_consolidator.stats"
+ ActivityBatchConsolidatorLatestBlock = "activity.batch_consolidator.latest_block"
+ ActivityBatchConsolidatorPlan = "activity.batch_consolidator.plan"
+ ActivityBatchConsolidatorCursor = "activity.batch_consolidator.cursor"
+ ActivitySingleBlockRetentionSelect = "activity.single_block_retention.select"
+ ActivitySingleBlockRetentionProcess = "activity.single_block_retention.process"
+ loggerMsg = "activity.request"
resultTypeTag = "result_type"
resultTypeSuccess = "success"
@@ -38,6 +45,8 @@ const (
reorgDistanceCheckMetric = "reorg_distance_check"
)
+const ActivityBatchConsolidatorRepairCandidates = "activity.batch_consolidator.repair_candidates"
+
type baseActivity struct {
name string
runtime cadence.Runtime
diff --git a/internal/workflow/activity/batch_consolidator.go b/internal/workflow/activity/batch_consolidator.go
new file mode 100644
index 00000000..ef839cbd
--- /dev/null
+++ b/internal/workflow/activity/batch_consolidator.go
@@ -0,0 +1,1125 @@
+package activity
+
+import (
+ "context"
+ "os"
+ "path/filepath"
+ "sync"
+ "time"
+
+ sdkactivity "go.temporal.io/sdk/activity"
+ "go.temporal.io/sdk/workflow"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/sync/errgroup"
+ "golang.org/x/xerrors"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ chains3 "github.com/coinbase/chainstorage/internal/s3"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage"
+ "github.com/coinbase/chainstorage/internal/storage/cscbrepair"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ metapostgres "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+ storageutils "github.com/coinbase/chainstorage/internal/storage/utils"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ BatchConsolidator struct {
+ baseActivity
+ statsActivity baseActivity
+ latestBlockActivity baseActivity
+ planActivity baseActivity
+ cursorActivity baseActivity
+ candidateActivity baseActivity
+ config *config.Config
+ metaStorage metastorage.MetaStorage
+ blobStorage blobstorage.BlobStorage
+ s3Client chains3.Client
+ repairerMu sync.Mutex
+ repairer cscbrepair.Repairer
+ }
+
+ BatchConsolidatorParams struct {
+ fx.In
+ fxparams.Params
+ Runtime cadence.Runtime
+ MetaStorage metastorage.MetaStorage
+ BlobStorage blobstorage.BlobStorage
+ S3Client chains3.Client `optional:"true"`
+ }
+
+ BatchConsolidatorRequest struct {
+ Mode config.ConsolidationMode `validate:"omitempty,oneof=shadow_dual_write auto_consolidate historical_backfill repair_existing_cscb"`
+ Tag uint32
+ StartHeight uint64
+ EndHeight uint64 `validate:"gtfield=StartHeight"`
+ MaxBlocks uint64 `validate:"required"`
+ RepairExecutionKey string
+ RepairObjectKey string
+ }
+
+ BatchConsolidatorResponse struct {
+ StartHeight uint64
+ EndHeight uint64
+ ScannedBlocks uint64
+ ConsolidatedBlocks uint64
+ PromotedBlocks uint64
+ ObjectKey string
+ OldObjectKey string
+ RepairedObjects uint64
+ }
+
+ BatchConsolidatorStatsRequest struct {
+ Mode config.ConsolidationMode `validate:"omitempty,oneof=shadow_dual_write auto_consolidate historical_backfill"`
+ Tag uint32
+ StartHeight uint64
+ EndHeight uint64 `validate:"gtfield=StartHeight"`
+ }
+
+ BatchConsolidatorStatsResponse struct {
+ StartHeight uint64
+ EndHeight uint64
+ ShadowObjects uint64
+ ShadowBlocks uint64
+ }
+
+ BatchConsolidatorLatestBlockRequest struct {
+ Tag uint32
+ }
+
+ BatchConsolidatorLatestBlockResponse struct {
+ Tag uint32
+ Height uint64
+ }
+
+ BatchConsolidatorPlanRequest struct {
+ Tag uint32
+ StartHeight uint64
+ EndHeight uint64 `validate:"gtfield=StartHeight"`
+ }
+
+ BatchConsolidatorPlanResponse struct {
+ StartHeight uint64
+ EndHeight uint64
+ LatestHeight uint64
+ SafePromotionHeight uint64
+ PromotionGateHeight uint64
+ }
+
+ BatchConsolidatorCursorRequest struct {
+ Tag uint32
+ Height uint64
+ }
+
+ BatchConsolidatorCursorResponse struct {
+ Tag uint32
+ Height uint64
+ }
+
+ BatchConsolidatorRepairCandidatesRequest struct {
+ Tag uint32
+ StartHeight uint64
+ EndHeight uint64 `validate:"gtfield=StartHeight"`
+ Limit int `validate:"required,gt=0,lte=20"`
+ }
+
+ BatchConsolidatorRepairCandidatesResponse struct {
+ ObjectKeys []string
+ }
+)
+
+func NewBatchConsolidator(params BatchConsolidatorParams) *BatchConsolidator {
+ a := &BatchConsolidator{
+ baseActivity: newBaseActivity(ActivityBatchConsolidator, params.Runtime),
+ statsActivity: newBaseActivity(ActivityBatchConsolidatorStats, params.Runtime),
+ latestBlockActivity: newBaseActivity(ActivityBatchConsolidatorLatestBlock, params.Runtime),
+ planActivity: newBaseActivity(ActivityBatchConsolidatorPlan, params.Runtime),
+ cursorActivity: newBaseActivity(ActivityBatchConsolidatorCursor, params.Runtime),
+ candidateActivity: newBaseActivity(ActivityBatchConsolidatorRepairCandidates, params.Runtime),
+ config: params.Config,
+ metaStorage: params.MetaStorage,
+ blobStorage: params.BlobStorage,
+ s3Client: params.S3Client,
+ }
+ a.register(a.execute)
+ a.statsActivity.register(a.executeStats)
+ a.latestBlockActivity.register(a.executeLatestBlock)
+ a.planActivity.register(a.executePlan)
+ a.cursorActivity.register(a.executeCursor)
+ a.candidateActivity.register(a.executeRepairCandidates)
+ return a
+}
+
+func (a *BatchConsolidator) Execute(ctx workflow.Context, request *BatchConsolidatorRequest) (*BatchConsolidatorResponse, error) {
+ var response BatchConsolidatorResponse
+ err := a.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *BatchConsolidator) GetShadowStats(ctx workflow.Context, request *BatchConsolidatorStatsRequest) (*BatchConsolidatorStatsResponse, error) {
+ var response BatchConsolidatorStatsResponse
+ err := a.statsActivity.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *BatchConsolidator) GetLatestBlock(ctx workflow.Context, request *BatchConsolidatorLatestBlockRequest) (*BatchConsolidatorLatestBlockResponse, error) {
+ var response BatchConsolidatorLatestBlockResponse
+ err := a.latestBlockActivity.executeActivity(ctx, request, &response)
+ if err != nil {
+ return nil, err
+ }
+ return &response, nil
+}
+
+func (a *BatchConsolidator) GetPromotionPlan(ctx workflow.Context, request *BatchConsolidatorPlanRequest) (*BatchConsolidatorPlanResponse, error) {
+ var response BatchConsolidatorPlanResponse
+ err := a.planActivity.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *BatchConsolidator) UpdateAutoConsolidateCursor(ctx workflow.Context, request *BatchConsolidatorCursorRequest) (*BatchConsolidatorCursorResponse, error) {
+ var response BatchConsolidatorCursorResponse
+ err := a.cursorActivity.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *BatchConsolidator) GetRepairCandidates(ctx workflow.Context, request *BatchConsolidatorRepairCandidatesRequest) (*BatchConsolidatorRepairCandidatesResponse, error) {
+ var response BatchConsolidatorRepairCandidatesResponse
+ err := a.candidateActivity.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *BatchConsolidator) executeStats(ctx context.Context, request *BatchConsolidatorStatsRequest) (*BatchConsolidatorStatsResponse, error) {
+ if err := a.statsActivity.validateRequest(request); err != nil {
+ return nil, err
+ }
+ if err := a.validateShadowStatsMode(request.Mode); err != nil {
+ return nil, err
+ }
+ logger := a.statsActivity.getLogger(ctx).With(zap.Reflect("request", request))
+ statsStart := time.Now()
+ logger.Info("started consolidation shadow stats")
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.stats.started", request.Tag, request.StartHeight, request.EndHeight)
+ stats, err := a.getConsolidationShadowStats(ctx, request.Tag, request.StartHeight, request.EndHeight)
+ if err != nil {
+ logger.Error("failed consolidation shadow stats", zap.Duration("duration", time.Since(statsStart)), zap.Error(err))
+ return nil, err
+ }
+ statsDuration := time.Since(statsStart)
+ logger.Info(
+ "finished consolidation shadow stats",
+ zap.Uint64("shadow_objects", stats.Objects),
+ zap.Uint64("shadow_blocks", stats.Blocks),
+ zap.Duration("duration", statsDuration),
+ )
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.stats.completed", request.Tag, request.StartHeight, request.EndHeight, stats.Objects, stats.Blocks)
+ return &BatchConsolidatorStatsResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ShadowObjects: stats.Objects,
+ ShadowBlocks: stats.Blocks,
+ }, nil
+}
+
+func (a *BatchConsolidator) executeLatestBlock(ctx context.Context, request *BatchConsolidatorLatestBlockRequest) (*BatchConsolidatorLatestBlockResponse, error) {
+ if err := a.latestBlockActivity.validateRequest(request); err != nil {
+ return nil, err
+ }
+ latestBlock, err := a.metaStorage.GetLatestBlock(ctx, request.Tag)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get latest block for auto_consolidate safety check: %w", err)
+ }
+ if latestBlock == nil {
+ return nil, xerrors.New("latest block not found for auto_consolidate safety check")
+ }
+ return &BatchConsolidatorLatestBlockResponse{
+ Tag: request.Tag,
+ Height: latestBlock.GetHeight(),
+ }, nil
+}
+
+func (a *BatchConsolidator) executePlan(ctx context.Context, request *BatchConsolidatorPlanRequest) (*BatchConsolidatorPlanResponse, error) {
+ if err := a.planActivity.validateRequest(request); err != nil {
+ return nil, err
+ }
+ return a.planPromoteFinalized(ctx, request.Tag, request.StartHeight, request.EndHeight)
+}
+
+func (a *BatchConsolidator) executeCursor(ctx context.Context, request *BatchConsolidatorCursorRequest) (*BatchConsolidatorCursorResponse, error) {
+ if err := a.cursorActivity.validateRequest(request); err != nil {
+ return nil, err
+ }
+ if err := a.metaStorage.SetBlockConsolidationCursor(ctx, metastorage.BatchConsolidatorAutoConsolidateCursor, request.Tag, request.Height); err != nil {
+ return nil, xerrors.Errorf("failed to update auto_consolidate cursor: %w", err)
+ }
+ return &BatchConsolidatorCursorResponse{
+ Tag: request.Tag,
+ Height: request.Height,
+ }, nil
+}
+
+func (a *BatchConsolidator) executeRepairCandidates(
+ ctx context.Context,
+ request *BatchConsolidatorRepairCandidatesRequest,
+) (*BatchConsolidatorRepairCandidatesResponse, error) {
+ if err := a.candidateActivity.validateRequest(request); err != nil {
+ return nil, err
+ }
+ if err := a.validateConsolidationEnabled(); err != nil {
+ return nil, err
+ }
+ repairer, err := a.getCSCBRepairer(ctx)
+ if err != nil {
+ return nil, err
+ }
+ objectKeys, err := repairer.ListCandidates(
+ ctx,
+ request.Tag,
+ request.StartHeight,
+ request.EndHeight,
+ request.Limit,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to list CSCB repair candidates: %w", err)
+ }
+ return &BatchConsolidatorRepairCandidatesResponse{ObjectKeys: objectKeys}, nil
+}
+
+func (a *BatchConsolidator) execute(ctx context.Context, request *BatchConsolidatorRequest) (*BatchConsolidatorResponse, error) {
+ if err := a.validateRequest(request); err != nil {
+ return nil, err
+ }
+ mode := request.Mode
+ if mode == "" {
+ mode = a.config.AWS.Storage.Consolidation.Mode
+ }
+ if mode.IsRepairExistingCSCB() {
+ return a.executeRepairExistingCSCB(ctx, request)
+ }
+ if mode.IsShadowWrite() {
+ return a.executeShadowDualWrite(ctx, request)
+ }
+ switch mode {
+ case config.ConsolidationModePromoteFinalized:
+ return a.executePromoteFinalized(ctx, request)
+ default:
+ if err := a.validateConsolidationEnabled(); err != nil {
+ return nil, err
+ }
+ return nil, xerrors.Errorf(
+ "batch consolidator requires consolidation mode %q, %q, or %q, got %q",
+ config.ConsolidationModeShadowDualWrite,
+ config.ConsolidationModeAutoConsolidate,
+ config.ConsolidationModePromoteFinalized,
+ mode,
+ )
+ }
+}
+
+func (a *BatchConsolidator) executeRepairExistingCSCB(
+ ctx context.Context,
+ request *BatchConsolidatorRequest,
+) (*BatchConsolidatorResponse, error) {
+ if err := a.validateConsolidationEnabled(); err != nil {
+ return nil, err
+ }
+ repairer, err := a.getCSCBRepairer(ctx)
+ if err != nil {
+ return nil, err
+ }
+ progress := func(stage string, completed int, total int, height uint64) {
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.repair."+stage, completed, total, height)
+ }
+ var manifest *cscbrepair.Manifest
+ if request.RepairObjectKey == "" {
+ manifest, err = repairer.PrepareNext(
+ ctx,
+ request.RepairExecutionKey,
+ request.Tag,
+ request.StartHeight,
+ request.EndHeight,
+ request.MaxBlocks,
+ progress,
+ )
+ } else {
+ manifest, err = repairer.PrepareObject(
+ ctx,
+ request.RepairExecutionKey,
+ request.Tag,
+ request.StartHeight,
+ request.EndHeight,
+ request.MaxBlocks,
+ request.RepairObjectKey,
+ progress,
+ )
+ }
+ if err != nil {
+ return nil, xerrors.Errorf("failed to prepare CSCB repair: %w", err)
+ }
+ if manifest == nil {
+ return &BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ }
+
+ logger := a.getLogger(ctx).With(
+ zap.Int64("repair_id", manifest.ID),
+ zap.String("repair_state", string(manifest.State)),
+ zap.String("old_object_key", manifest.OldConsolidatedObjectKey),
+ zap.Uint64("repair_start_height", manifest.StartHeight),
+ zap.Uint64("repair_end_height", manifest.EndHeight),
+ zap.Uint64("repair_blocks", manifest.TotalBlockCount),
+ zap.Uint64("repair_canonical_blocks", manifest.CanonicalBlockCount),
+ )
+ logger.Info("resuming CSCB repair")
+
+ if manifest.State == cscbrepair.StatePrepared && manifest.CanonicalBlockCount == 0 {
+ manifest, err = repairer.Restore(ctx, manifest.ID, progress)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to restore CSCB repair to single-block metadata: %w", err)
+ }
+ }
+ if (manifest.State == cscbrepair.StatePrepared || manifest.State == cscbrepair.StateRestored) &&
+ manifest.CanonicalBlockCount > 0 {
+ payloads, cleanup, err := a.buildPinnedRepairPayloads(ctx, repairer, manifest, progress)
+ if err != nil {
+ return nil, err
+ }
+ defer cleanup()
+ uploadCtx := blobstorage.WithConsolidatedUploadProgress(ctx, func(stage string, details ...any) {
+ heartbeatDetails := append([]any{"batch_consolidator.repair." + stage}, details...)
+ sdkactivity.RecordHeartbeat(ctx, heartbeatDetails...)
+ })
+ objectKey, uploadedPlacements, err := a.blobStorage.UploadConsolidated(uploadCtx, payloads)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to upload rebuilt normalized CSCB: %w", err)
+ }
+ placements := make([]cscbrepair.RebuiltPlacement, len(uploadedPlacements))
+ for i, placement := range uploadedPlacements {
+ placements[i] = cscbrepair.RebuiltPlacement{
+ BlockMetadataID: placement.MetadataID,
+ Height: placement.Height,
+ Hash: placement.Hash,
+ ObjectFormat: placement.ObjectFormat,
+ ByteOffset: placement.ByteOffset,
+ ByteLength: placement.ByteLength,
+ UncompressedLength: placement.UncompressedLength,
+ }
+ }
+ manifest, err = repairer.VerifyAndPromote(
+ ctx,
+ manifest.ID,
+ objectKey,
+ placements,
+ a.config.AWS.Storage.Consolidation.SingleBlockObjectRetention,
+ progress,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to verify and atomically promote rebuilt CSCB: %w", err)
+ }
+ }
+ if manifest.State == cscbrepair.StateRestored || manifest.State == cscbrepair.StateVerified {
+ manifest, err = repairer.Complete(ctx, manifest.ID, progress)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to complete CSCB repair: %w", err)
+ }
+ }
+ if manifest.State != cscbrepair.StateCompleted {
+ return nil, xerrors.Errorf("CSCB repair made no terminal progress: id=%d state=%s", manifest.ID, manifest.State)
+ }
+
+ logger.Info(
+ "processed CSCB repair",
+ zap.String("final_state", string(manifest.State)),
+ zap.String("new_object_key", manifest.NewConsolidatedObjectKey),
+ zap.String("old_object_outcome", manifest.Outcome),
+ )
+ consolidatedBlocks := manifest.TotalBlockCount
+ promotedBlocks := manifest.TotalBlockCount
+ objectKey := manifest.NewConsolidatedObjectKey
+ if objectKey == "" {
+ consolidatedBlocks = 0
+ promotedBlocks = 0
+ }
+ if manifest.Outcome == cscbrepair.OutcomeAlreadyCleanStorageNeutral {
+ consolidatedBlocks = 0
+ promotedBlocks = 0
+ objectKey = manifest.OldConsolidatedObjectKey
+ }
+ return &BatchConsolidatorResponse{
+ StartHeight: manifest.StartHeight,
+ EndHeight: manifest.EndHeight,
+ ScannedBlocks: manifest.TotalBlockCount,
+ ConsolidatedBlocks: consolidatedBlocks,
+ PromotedBlocks: promotedBlocks,
+ ObjectKey: objectKey,
+ OldObjectKey: manifest.OldConsolidatedObjectKey,
+ RepairedObjects: 1,
+ }, nil
+}
+
+func (a *BatchConsolidator) getCSCBRepairer(ctx context.Context) (cscbrepair.Repairer, error) {
+ a.repairerMu.Lock()
+ defer a.repairerMu.Unlock()
+ if a.repairer != nil {
+ return a.repairer, nil
+ }
+ if a.config.StorageType.MetaStorageType != config.MetaStorageType_POSTGRES || a.config.AWS.Postgres == nil {
+ return nil, xerrors.New("repair_existing_cscb requires Postgres meta storage")
+ }
+ if a.config.StorageType.BlobStorageType != config.BlobStorageType_UNSPECIFIED &&
+ a.config.StorageType.BlobStorageType != config.BlobStorageType_S3 {
+ return nil, xerrors.New("repair_existing_cscb requires S3 blob storage")
+ }
+ if a.s3Client == nil {
+ return nil, xerrors.New("repair_existing_cscb requires an S3 client")
+ }
+ pool, err := metapostgres.GetConnectionPool(ctx, a.config.AWS.Postgres)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get CSCB repair Postgres pool: %w", err)
+ }
+ db := pool.DB()
+ if db == nil {
+ return nil, xerrors.New("CSCB repair Postgres pool returned a nil database")
+ }
+ a.repairer = cscbrepair.NewRepairer(
+ cscbrepair.NewPostgresRepository(db),
+ retirement.NewS3ObjectStore(a.s3Client),
+ a.config.AWS.Bucket,
+ )
+ return a.repairer, nil
+}
+
+func (a *BatchConsolidator) executeShadowDualWrite(ctx context.Context, request *BatchConsolidatorRequest) (*BatchConsolidatorResponse, error) {
+ return a.executeShadowDualWriteWithExpected(ctx, request, nil)
+}
+
+func (a *BatchConsolidator) executeShadowDualWriteWithExpected(
+ ctx context.Context,
+ request *BatchConsolidatorRequest,
+ expectedRecords map[int64]cscbrepair.Block,
+) (*BatchConsolidatorResponse, error) {
+ if err := a.validateShadowWriteMode(request.Mode); err != nil {
+ return nil, err
+ }
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.started")
+
+ logger := a.getLogger(ctx).With(zap.Reflect("request", request))
+ totalStart := time.Now()
+ mode := request.Mode
+ if mode == "" {
+ mode = a.config.AWS.Storage.Consolidation.Mode
+ }
+ if mode.IsAutoConsolidate() && !isFullAutoConsolidationRequest(request) {
+ return nil, xerrors.Errorf(
+ "auto_consolidate requires exactly one full consolidation window: start_height=%d end_height=%d max_blocks=%d",
+ request.StartHeight,
+ request.EndHeight,
+ request.MaxBlocks,
+ )
+ }
+ if mode.IsAutoConsolidate() {
+ if err := a.validateFullAutoConsolidationWindow(ctx, request); err != nil {
+ return nil, err
+ }
+ }
+ var prePromotedBlocks uint64
+ if expectedRecords == nil {
+ var err error
+ prePromotedBlocks, err = a.promoteConsolidatedBlocks(ctx, mode, request)
+ if err != nil {
+ return nil, err
+ }
+ }
+ scanStart := time.Now()
+ records, err := a.metaStorage.GetBlocksMissingConsolidationShadow(ctx, request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to scan missing consolidation shadows: %w", err)
+ }
+ scanDuration := time.Since(scanStart)
+ logger.Info("scanned missing consolidation shadows", zap.Int("records", len(records)), zap.Duration("duration", scanDuration))
+ if len(records) == 0 {
+ if expectedRecords != nil {
+ postPromotedBlocks, err := a.promoteConsolidatedBlocks(ctx, mode, request)
+ if err != nil {
+ return nil, err
+ }
+ prePromotedBlocks += postPromotedBlocks
+ }
+ return &BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ PromotedBlocks: prePromotedBlocks,
+ }, nil
+ }
+ if expectedRecords != nil {
+ if err := validateExactRepairRecords(records, expectedRecords); err != nil {
+ return nil, err
+ }
+ }
+
+ buildStart := time.Now()
+ payloads, recordsByID, cleanup, err := a.buildPayloads(ctx, records)
+ if err != nil {
+ return nil, err
+ }
+ defer cleanup()
+ buildDuration := time.Since(buildStart)
+ rawBytes := consolidatedPayloadBytes(payloads)
+ logger.Info("built consolidated payloads", zap.Int("blocks", len(payloads)), zap.Uint64("raw_bytes", rawBytes), zap.Duration("duration", buildDuration))
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.payloads_built", len(records))
+
+ uploadCtx := blobstorage.WithConsolidatedUploadProgress(ctx, func(stage string, details ...any) {
+ heartbeatDetails := append([]any{"batch_consolidator." + stage}, details...)
+ sdkactivity.RecordHeartbeat(ctx, heartbeatDetails...)
+ })
+ uploadStart := time.Now()
+ objectKey, placements, err := a.blobStorage.UploadConsolidated(uploadCtx, payloads)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to upload consolidated block object: %w", err)
+ }
+ uploadDuration := time.Since(uploadStart)
+ logger.Info("uploaded consolidated block object", zap.String("object_key", objectKey), zap.Int("placements", len(placements)), zap.Duration("duration", uploadDuration))
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.object_uploaded", objectKey, len(placements))
+ shadowPlacements, err := makeShadowPlacements(recordsByID, objectKey, placements)
+ if err != nil {
+ return nil, err
+ }
+ persistStart := time.Now()
+ if err := a.metaStorage.PersistBlockConsolidationShadows(ctx, shadowPlacements); err != nil {
+ return nil, xerrors.Errorf("failed to persist consolidation shadow placements: %w", err)
+ }
+ persistDuration := time.Since(persistStart)
+ logger.Info("persisted consolidation shadow placements", zap.Int("placements", len(shadowPlacements)), zap.Duration("duration", persistDuration))
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.shadows_persisted", objectKey, len(shadowPlacements))
+ postPromotedBlocks, err := a.promoteConsolidatedBlocks(ctx, mode, request)
+ if err != nil {
+ return nil, err
+ }
+ promotedBlocks := prePromotedBlocks + postPromotedBlocks
+
+ response := &BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: uint64(len(records)),
+ ConsolidatedBlocks: uint64(len(shadowPlacements)),
+ PromotedBlocks: promotedBlocks,
+ ObjectKey: objectKey,
+ }
+ logger.Info(
+ "consolidated shadow blocks",
+ zap.Int("scanned_blocks", len(records)),
+ zap.Int("consolidated_blocks", len(shadowPlacements)),
+ zap.Uint64("promoted_blocks", promotedBlocks),
+ zap.String("object_key", objectKey),
+ zap.Uint64("raw_bytes", rawBytes),
+ zap.Duration("scan_duration", scanDuration),
+ zap.Duration("build_duration", buildDuration),
+ zap.Duration("upload_duration", uploadDuration),
+ zap.Duration("persist_duration", persistDuration),
+ zap.Duration("total_duration", time.Since(totalStart)),
+ )
+ return response, nil
+}
+
+func validateExactRepairRecords(
+ records []*metastorage.BlockMetadataRecord,
+ expected map[int64]cscbrepair.Block,
+) error {
+ if len(records) != len(expected) {
+ return xerrors.Errorf("CSCB repair rebuild row count mismatch before upload: expected=%d actual=%d", len(expected), len(records))
+ }
+ seen := make(map[int64]struct{}, len(records))
+ for _, record := range records {
+ if record == nil || record.Metadata == nil {
+ return xerrors.New("CSCB repair rebuild returned missing block metadata record")
+ }
+ block, ok := expected[record.ID]
+ if !ok {
+ return xerrors.Errorf("CSCB repair rebuild returned unexpected metadata_id=%d", record.ID)
+ }
+ if _, ok := seen[record.ID]; ok {
+ return xerrors.Errorf("CSCB repair rebuild returned duplicate metadata_id=%d", record.ID)
+ }
+ seen[record.ID] = struct{}{}
+ metadata := record.Metadata
+ if metadata.GetTag() != block.Tag || metadata.GetHeight() != block.Height || metadata.GetHash() != block.Hash {
+ return xerrors.Errorf(
+ "CSCB repair rebuild identity changed for metadata_id=%d: expected=(%d,%d,%q) actual=(%d,%d,%q)",
+ record.ID,
+ block.Tag,
+ block.Height,
+ block.Hash,
+ metadata.GetTag(),
+ metadata.GetHeight(),
+ metadata.GetHash(),
+ )
+ }
+ if metadata.GetObjectKeyMain() != block.SingleBlockObjectKey ||
+ metadata.GetObjectFormat() != api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK ||
+ metadata.GetByteOffset() != 0 || metadata.GetByteLength() != 0 || metadata.GetUncompressedLength() != 0 {
+ return xerrors.Errorf("CSCB repair rebuild metadata is not restored to the pinned single-block placement for metadata_id=%d", record.ID)
+ }
+ }
+ return nil
+}
+
+func (a *BatchConsolidator) executePromoteFinalized(ctx context.Context, request *BatchConsolidatorRequest) (*BatchConsolidatorResponse, error) {
+ if err := a.validatePromoteFinalizedMode(); err != nil {
+ return nil, err
+ }
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.promote_finalized.started")
+
+ plan, err := a.planPromoteFinalized(ctx, request.Tag, request.StartHeight, request.EndHeight)
+ if err != nil {
+ return nil, err
+ }
+ if plan.EndHeight <= request.StartHeight {
+ return &BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.StartHeight,
+ }, nil
+ }
+ result, err := a.metaStorage.PromoteBlockConsolidationShadows(
+ ctx,
+ request.Tag,
+ request.StartHeight,
+ plan.EndHeight,
+ request.MaxBlocks,
+ a.config.AWS.Storage.Consolidation.SingleBlockObjectRetention,
+ )
+ if err != nil {
+ return nil, xerrors.Errorf("failed to promote consolidation shadows: %w", err)
+ }
+ promotedBlocks := uint64(0)
+ if result != nil {
+ promotedBlocks = result.Blocks
+ }
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.promote_finalized.promoted", promotedBlocks)
+ a.getLogger(ctx).Info(
+ "promoted finalized consolidation shadows",
+ zap.Reflect("request", request),
+ zap.Uint64("promoted_blocks", promotedBlocks),
+ )
+ return &BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: plan.EndHeight,
+ ScannedBlocks: promotedBlocks,
+ ConsolidatedBlocks: promotedBlocks,
+ PromotedBlocks: promotedBlocks,
+ }, nil
+}
+
+func (a *BatchConsolidator) promoteConsolidatedBlocks(ctx context.Context, mode config.ConsolidationMode, request *BatchConsolidatorRequest) (uint64, error) {
+ if mode != config.ConsolidationModeAutoConsolidate && mode != config.ConsolidationModeHistoricalBackfill {
+ return 0, nil
+ }
+ result, err := a.metaStorage.PromoteBlockConsolidationShadows(
+ ctx,
+ request.Tag,
+ request.StartHeight,
+ request.EndHeight,
+ request.MaxBlocks,
+ a.config.AWS.Storage.Consolidation.SingleBlockObjectRetention,
+ )
+ if err != nil {
+ return 0, xerrors.Errorf("failed to promote consolidated block metadata: %w", err)
+ }
+ if result == nil {
+ return 0, nil
+ }
+ if result.Blocks > 0 {
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.metadata_promoted", result.Blocks)
+ }
+ return result.Blocks, nil
+}
+
+func (a *BatchConsolidator) validateFullAutoConsolidationWindow(ctx context.Context, request *BatchConsolidatorRequest) error {
+ blocks, err := a.metaStorage.GetBlocksByHeightRange(ctx, request.Tag, request.StartHeight, request.EndHeight)
+ if err != nil {
+ return xerrors.Errorf("failed to verify auto_consolidate canonical window: %w", err)
+ }
+ if request.MaxBlocks == 0 || uint64(len(blocks)) != request.MaxBlocks {
+ return xerrors.Errorf(
+ "auto_consolidate requires a full canonical consolidation window: start_height=%d end_height=%d max_blocks=%d canonical_blocks=%d",
+ request.StartHeight,
+ request.EndHeight,
+ request.MaxBlocks,
+ len(blocks),
+ )
+ }
+ for i, block := range blocks {
+ if block == nil {
+ return xerrors.Errorf(
+ "auto_consolidate requires a full canonical consolidation window: start_height=%d end_height=%d max_blocks=%d nil_index=%d",
+ request.StartHeight,
+ request.EndHeight,
+ request.MaxBlocks,
+ i,
+ )
+ }
+ expectedHeight := request.StartHeight + uint64(i)
+ if expectedHeight < request.StartHeight || block.GetHeight() != expectedHeight {
+ return xerrors.Errorf(
+ "auto_consolidate requires a full canonical consolidation window: start_height=%d end_height=%d max_blocks=%d expected_height=%d actual_height=%d",
+ request.StartHeight,
+ request.EndHeight,
+ request.MaxBlocks,
+ expectedHeight,
+ block.GetHeight(),
+ )
+ }
+ }
+ return nil
+}
+
+func (a *BatchConsolidator) planPromoteFinalized(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) (*BatchConsolidatorPlanResponse, error) {
+ if err := a.validatePromoteFinalizedMode(); err != nil {
+ return nil, err
+ }
+ latest, err := a.metaStorage.GetLatestBlock(ctx, tag)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get latest block for consolidation promotion: %w", err)
+ }
+
+ consolidation := a.config.AWS.Storage.Consolidation
+ safeLag := *consolidation.SafePromotionLag
+ gateHeight := endHeight
+ if consolidation.PromotionGateHeight != nil {
+ gateHeight = *consolidation.PromotionGateHeight
+ }
+ safeEnd, safeHeight, ok := promotionSafeEndHeight(latest.GetHeight(), safeLag)
+ if !ok {
+ return &BatchConsolidatorPlanResponse{
+ StartHeight: startHeight,
+ EndHeight: startHeight,
+ LatestHeight: latest.GetHeight(),
+ SafePromotionHeight: 0,
+ PromotionGateHeight: gateHeight,
+ }, nil
+ }
+
+ effectiveEndHeight := endHeight
+ if gateHeight < effectiveEndHeight {
+ effectiveEndHeight = gateHeight
+ }
+ if safeEnd < effectiveEndHeight {
+ effectiveEndHeight = safeEnd
+ }
+ if effectiveEndHeight < startHeight {
+ effectiveEndHeight = startHeight
+ }
+
+ return &BatchConsolidatorPlanResponse{
+ StartHeight: startHeight,
+ EndHeight: effectiveEndHeight,
+ LatestHeight: latest.GetHeight(),
+ SafePromotionHeight: safeHeight,
+ PromotionGateHeight: gateHeight,
+ }, nil
+}
+
+func (a *BatchConsolidator) getConsolidationShadowStats(
+ ctx context.Context,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+) (*metastorage.ConsolidationShadowStats, error) {
+ stats, err := a.metaStorage.GetBlockConsolidationShadowStats(ctx, tag, startHeight, endHeight)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get consolidation shadow stats: %w", err)
+ }
+ if stats == nil {
+ return &metastorage.ConsolidationShadowStats{}, nil
+ }
+ return stats, nil
+}
+
+func (a *BatchConsolidator) validateConsolidationEnabled() error {
+ consolidation := a.config.AWS.Storage.Consolidation
+ if !consolidation.Enabled {
+ return xerrors.New("batch consolidator requires aws.storage.consolidation.enabled=true")
+ }
+ return nil
+}
+
+func (a *BatchConsolidator) validateShadowWriteMode(mode config.ConsolidationMode) error {
+ if err := a.validateConsolidationEnabled(); err != nil {
+ return err
+ }
+ if mode == "" {
+ mode = a.config.AWS.Storage.Consolidation.Mode
+ }
+ if mode.IsShadowWrite() {
+ return nil
+ }
+ return xerrors.Errorf(
+ "batch consolidator requires consolidation mode %q, %q, or %q, got %q",
+ config.ConsolidationModeShadowDualWrite,
+ config.ConsolidationModeAutoConsolidate,
+ config.ConsolidationModeHistoricalBackfill,
+ mode,
+ )
+}
+
+func (a *BatchConsolidator) validateShadowStatsMode(mode config.ConsolidationMode) error {
+ return a.validateShadowWriteMode(mode)
+}
+
+func isFullAutoConsolidationRequest(request *BatchConsolidatorRequest) bool {
+ if request == nil || request.MaxBlocks == 0 || request.EndHeight <= request.StartHeight {
+ return false
+ }
+ return request.EndHeight-request.StartHeight == request.MaxBlocks
+}
+
+func (a *BatchConsolidator) validatePromoteFinalizedMode() error {
+ if err := a.validateConsolidationEnabled(); err != nil {
+ return err
+ }
+ consolidation := a.config.AWS.Storage.Consolidation
+ if consolidation.Mode != config.ConsolidationModePromoteFinalized {
+ return xerrors.Errorf("batch consolidator requires consolidation mode %q, got %q", config.ConsolidationModePromoteFinalized, consolidation.Mode)
+ }
+ if consolidation.SafePromotionLag == nil {
+ return xerrors.New("batch consolidator promote_finalized requires safe_promotion_lag")
+ }
+ return nil
+}
+
+func promotionSafeEndHeight(latestHeight uint64, safePromotionLag uint64) (uint64, uint64, bool) {
+ if latestHeight < safePromotionLag {
+ return 0, 0, false
+ }
+ safeHeight := latestHeight - safePromotionLag
+ if safeHeight == ^uint64(0) {
+ return safeHeight, safeHeight, true
+ }
+ return safeHeight + 1, safeHeight, true
+}
+
+func (a *BatchConsolidator) buildPinnedRepairPayloads(
+ ctx context.Context,
+ repairer cscbrepair.Repairer,
+ manifest *cscbrepair.Manifest,
+ progress cscbrepair.Progress,
+) ([]blobstorage.ConsolidatedBlockPayload, func(), error) {
+ payloads := make([]blobstorage.ConsolidatedBlockPayload, 0, manifest.TotalBlockCount)
+ tempFiles := make([]string, 0, manifest.TotalBlockCount)
+ cleanup := func() {
+ for _, path := range tempFiles {
+ if path != "" {
+ _ = os.Remove(path)
+ }
+ }
+ }
+ err := repairer.VisitPinnedPayloads(ctx, manifest.ID, progress, func(pinned cscbrepair.PinnedPayload) error {
+ source, path, length, err := writeRawBlockPayloadTempFile(
+ pinned.RawBlockPayload,
+ a.config.AWS.Storage.Consolidation.LocalSpillDir,
+ )
+ if err != nil {
+ return err
+ }
+ tempFiles = append(tempFiles, path)
+ payloads = append(payloads, blobstorage.ConsolidatedBlockPayload{
+ Metadata: pinned.Metadata,
+ MetadataID: pinned.BlockMetadataID,
+ RawBlockPayload: source,
+ UncompressedLength: length,
+ })
+ return nil
+ })
+ if err != nil {
+ cleanup()
+ return nil, nil, xerrors.Errorf("failed to build normalized CSCB from pinned versions: %w", err)
+ }
+ if uint64(len(payloads)) != manifest.TotalBlockCount {
+ cleanup()
+ return nil, nil, xerrors.Errorf(
+ "CSCB repair pinned payload count mismatch: expected=%d actual=%d",
+ manifest.TotalBlockCount,
+ len(payloads),
+ )
+ }
+ return payloads, cleanup, nil
+}
+
+func (a *BatchConsolidator) buildPayloads(
+ ctx context.Context,
+ records []*metastorage.BlockMetadataRecord,
+) ([]blobstorage.ConsolidatedBlockPayload, map[int64]*api.BlockMetadata, func(), error) {
+ payloads := make([]blobstorage.ConsolidatedBlockPayload, len(records))
+ recordsByID := make(map[int64]*api.BlockMetadata, len(records))
+ for i, record := range records {
+ if record == nil || record.Metadata == nil {
+ return nil, nil, nil, xerrors.New("missing block metadata record")
+ }
+ recordsByID[record.ID] = record.Metadata
+ payloads[i].MetadataID = record.ID
+ payloads[i].Metadata = record.Metadata
+ }
+
+ tempFiles := make([]string, len(records))
+ cleanup := func() {
+ for _, path := range tempFiles {
+ if path != "" {
+ _ = os.Remove(path)
+ }
+ }
+ }
+
+ parallelism := int(a.config.AWS.Storage.Consolidation.MaxInflightRawBlocks)
+ if parallelism <= 0 {
+ parallelism = 1
+ }
+ group, groupCtx := errgroup.WithContext(ctx)
+ group.SetLimit(parallelism)
+ for i := range records {
+ i := i
+ group.Go(func() error {
+ record := records[i]
+ metadata := record.Metadata
+ block, err := a.blobStorage.Download(groupCtx, metadata)
+ if err != nil {
+ return xerrors.Errorf("failed to download single-block object (height=%d, hash=%s): %w", metadata.GetHeight(), metadata.GetHash(), err)
+ }
+ if err := validateDownloadedBlock(metadata, block); err != nil {
+ return err
+ }
+ storageNeutralBlock := storageutils.CloneBlockWithoutStoragePlacement(block)
+ rawBlockPayload, err := proto.Marshal(storageNeutralBlock)
+ if err != nil {
+ return xerrors.Errorf("failed to marshal block (height=%d, hash=%s): %w", metadata.GetHeight(), metadata.GetHash(), err)
+ }
+ source, path, length, err := writeRawBlockPayloadTempFile(rawBlockPayload, a.config.AWS.Storage.Consolidation.LocalSpillDir)
+ if err != nil {
+ return xerrors.Errorf("failed to stage raw block payload (height=%d, hash=%s): %w", metadata.GetHeight(), metadata.GetHash(), err)
+ }
+ tempFiles[i] = path
+ payloads[i].RawBlockPayload = source
+ payloads[i].UncompressedLength = length
+ sdkactivity.RecordHeartbeat(ctx, "batch_consolidator.block_downloaded", metadata.GetHeight())
+ return nil
+ })
+ }
+ if err := group.Wait(); err != nil {
+ cleanup()
+ return nil, nil, nil, err
+ }
+ for _, payload := range payloads {
+ if payload.RawBlockPayload == nil {
+ cleanup()
+ return nil, nil, nil, xerrors.Errorf("missing consolidated payload for metadata_id=%d", payload.MetadataID)
+ }
+ }
+ return payloads, recordsByID, cleanup, nil
+}
+
+func writeRawBlockPayloadTempFile(raw []byte, dir string) (blobstorage.PayloadSource, string, uint64, error) {
+ if len(raw) == 0 {
+ return nil, "", 0, xerrors.New("raw block payload cannot be empty")
+ }
+ spillDir := os.TempDir()
+ if dir != "" {
+ spillDir = filepath.Clean(dir)
+ }
+ if err := os.MkdirAll(spillDir, 0o700); err != nil {
+ return nil, "", 0, err
+ }
+ file, err := os.CreateTemp(spillDir, "chainstorage-cscb-raw-block-*.pb")
+ if err != nil {
+ return nil, "", 0, err
+ }
+ path := file.Name()
+ success := false
+ closed := false
+ defer func() {
+ if !closed {
+ _ = file.Close()
+ }
+ if !success {
+ _ = os.Remove(path)
+ }
+ }()
+ if _, err := file.Write(raw); err != nil {
+ return nil, "", 0, err
+ }
+ if err := file.Close(); err != nil {
+ return nil, "", 0, err
+ }
+ closed = true
+ success = true
+ length := uint64(len(raw))
+ return blobstorage.NewFilePayloadSource(path, length), path, length, nil
+}
+
+func consolidatedPayloadBytes(payloads []blobstorage.ConsolidatedBlockPayload) uint64 {
+ var total uint64
+ for _, payload := range payloads {
+ total += payload.UncompressedLength
+ }
+ return total
+}
+
+func validateDownloadedBlock(expected *api.BlockMetadata, block *api.Block) error {
+ if block == nil || block.Metadata == nil {
+ return xerrors.Errorf("downloaded block is missing metadata (height=%d, hash=%s)", expected.GetHeight(), expected.GetHash())
+ }
+ actual := block.Metadata
+ if actual.GetTag() != expected.GetTag() || actual.GetHeight() != expected.GetHeight() || actual.GetHash() != expected.GetHash() {
+ return xerrors.Errorf(
+ "downloaded block metadata mismatch (expected tag=%d height=%d hash=%s, got tag=%d height=%d hash=%s)",
+ expected.GetTag(),
+ expected.GetHeight(),
+ expected.GetHash(),
+ actual.GetTag(),
+ actual.GetHeight(),
+ actual.GetHash(),
+ )
+ }
+ return nil
+}
+
+func makeShadowPlacements(
+ recordsByID map[int64]*api.BlockMetadata,
+ objectKey string,
+ placements []blobstorage.BlockPlacement,
+) ([]*metastorage.ConsolidationShadowPlacement, error) {
+ shadowPlacements := make([]*metastorage.ConsolidationShadowPlacement, 0, len(placements))
+ for _, placement := range placements {
+ metadata, ok := recordsByID[placement.MetadataID]
+ if !ok {
+ return nil, xerrors.Errorf("missing source metadata for placement metadata_id=%d", placement.MetadataID)
+ }
+ if placement.Height != metadata.GetHeight() || placement.Hash != metadata.GetHash() {
+ return nil, xerrors.Errorf(
+ "placement metadata mismatch (metadata_id=%d expected height=%d hash=%s, got height=%d hash=%s)",
+ placement.MetadataID,
+ metadata.GetHeight(),
+ metadata.GetHash(),
+ placement.Height,
+ placement.Hash,
+ )
+ }
+ shadowPlacements = append(shadowPlacements, &metastorage.ConsolidationShadowPlacement{
+ BlockMetadataID: placement.MetadataID,
+ Tag: metadata.GetTag(),
+ Height: metadata.GetHeight(),
+ Hash: metadata.GetHash(),
+ SingleBlockObjectKeyMain: metadata.GetObjectKeyMain(),
+ ConsolidatedObjectKeyMain: objectKey,
+ ObjectFormat: placement.ObjectFormat,
+ ByteOffset: placement.ByteOffset,
+ ByteLength: placement.ByteLength,
+ UncompressedLength: placement.UncompressedLength,
+ })
+ }
+ return shadowPlacements, nil
+}
diff --git a/internal/workflow/activity/batch_consolidator_repair_test.go b/internal/workflow/activity/batch_consolidator_repair_test.go
new file mode 100644
index 00000000..34c3e460
--- /dev/null
+++ b/internal/workflow/activity/batch_consolidator_repair_test.go
@@ -0,0 +1,318 @@
+package activity
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "go.uber.org/mock/gomock"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/cscbrepair"
+ storageutils "github.com/coinbase/chainstorage/internal/storage/utils"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func (s *BatchConsolidatorTestSuite) TestRepairExistingCSCBRebuildsPinnedPayloadsAndPromotesAtomically() {
+ require := testutil.Require(s.T())
+ records, blocks := makeConsolidatorFixture(1, 1000)
+ request := &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: 2,
+ StartHeight: 900,
+ EndHeight: 1200,
+ MaxBlocks: 100,
+ RepairExecutionKey: testRepairExecutionKey,
+ }
+ oldKey := "consolidated/dirty.cscb.zstd"
+ newKey := "consolidated/clean.cscb.zstd"
+ prepared := repairActivityManifest(cscbrepair.StatePrepared, oldKey, "")
+ completed := repairActivityManifest(cscbrepair.StateCompleted, oldKey, newKey)
+ fake := &repairActivityFake{
+ prepared: prepared,
+ verified: completed,
+ completed: completed,
+ pinnedPayloads: []cscbrepair.PinnedPayload{{
+ BlockMetadataID: records[0].ID,
+ Metadata: records[0].Metadata,
+ RawBlockPayload: marshalRepairBlock(s.T(), blocks[0]),
+ }},
+ }
+ s.batchConsolidator.repairer = fake
+ s.batchConsolidator.config.AWS.Storage.Consolidation.LocalSpillDir = s.T().TempDir()
+
+ s.blobStorage.EXPECT().
+ UploadConsolidated(gomock.Any(), gomock.Any()).
+ Return(newKey, makeConsolidatorPlacements(records), nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: 1000,
+ EndHeight: 1001,
+ ScannedBlocks: 1,
+ ConsolidatedBlocks: 1,
+ PromotedBlocks: 1,
+ ObjectKey: newKey,
+ OldObjectKey: oldKey,
+ RepairedObjects: 1,
+ }, response)
+ require.Equal([]string{"prepare", "visit_pinned", "verify_promote"}, fake.calls)
+ require.Equal(testRepairExecutionKey, fake.executionKey)
+}
+
+func (s *BatchConsolidatorTestSuite) TestRepairExistingCSCBRejectsMissingPinnedPayloadBeforeUpload() {
+ require := testutil.Require(s.T())
+ oldKey := "consolidated/dirty.cscb.zstd"
+ prepared := repairActivityManifest(cscbrepair.StatePrepared, oldKey, "")
+ fake := &repairActivityFake{prepared: prepared}
+ s.batchConsolidator.repairer = fake
+
+ _, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: 2,
+ StartHeight: 900,
+ EndHeight: 1200,
+ MaxBlocks: 100,
+ RepairExecutionKey: testRepairExecutionKey,
+ })
+ require.ErrorContains(err, "pinned payload count mismatch")
+ require.Equal([]string{"prepare", "visit_pinned"}, fake.calls)
+}
+
+func (s *BatchConsolidatorTestSuite) TestRepairExistingCSCBResumesRestoredManifestWithPinnedPayload() {
+ require := testutil.Require(s.T())
+ records, blocks := makeConsolidatorFixture(1, 1000)
+ oldKey := "consolidated/dirty.cscb.zstd"
+ newKey := "consolidated/clean.cscb.zstd"
+ restored := repairActivityManifest(cscbrepair.StateRestored, oldKey, "")
+ completed := repairActivityManifest(cscbrepair.StateCompleted, oldKey, newKey)
+ fake := &repairActivityFake{
+ prepared: restored,
+ verified: completed,
+ completed: completed,
+ pinnedPayloads: []cscbrepair.PinnedPayload{{
+ BlockMetadataID: records[0].ID,
+ Metadata: records[0].Metadata,
+ RawBlockPayload: marshalRepairBlock(s.T(), blocks[0]),
+ }},
+ }
+ s.batchConsolidator.repairer = fake
+ s.batchConsolidator.config.AWS.Storage.Consolidation.LocalSpillDir = s.T().TempDir()
+
+ s.blobStorage.EXPECT().
+ UploadConsolidated(gomock.Any(), gomock.Any()).
+ Return(newKey, makeConsolidatorPlacements(records), nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: 2,
+ StartHeight: 900,
+ EndHeight: 1200,
+ MaxBlocks: 100,
+ RepairExecutionKey: testRepairExecutionKey,
+ })
+ require.NoError(err)
+ require.Equal(uint64(1), response.RepairedObjects)
+ require.Equal([]string{"prepare", "visit_pinned", "verify_promote"}, fake.calls)
+}
+
+func (s *BatchConsolidatorTestSuite) TestRepairExistingCSCBCompletesAllNonCanonicalObjectWithoutRebuild() {
+ require := testutil.Require(s.T())
+ oldKey := "consolidated/dirty.cscb.zstd"
+ prepared := repairActivityManifest(cscbrepair.StatePrepared, oldKey, "")
+ prepared.CanonicalBlockCount = 0
+ restored := repairActivityManifest(cscbrepair.StateRestored, oldKey, "")
+ restored.CanonicalBlockCount = 0
+ completed := repairActivityManifest(cscbrepair.StateCompleted, oldKey, "")
+ completed.CanonicalBlockCount = 0
+ fake := &repairActivityFake{prepared: prepared, restored: restored, completed: completed}
+ s.batchConsolidator.repairer = fake
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: 2,
+ StartHeight: 900,
+ EndHeight: 1200,
+ MaxBlocks: 100,
+ RepairExecutionKey: testRepairExecutionKey,
+ })
+ require.NoError(err)
+ require.Equal(uint64(1), response.RepairedObjects)
+ require.Zero(response.ConsolidatedBlocks)
+ require.Empty(response.ObjectKey)
+ require.Equal([]string{"prepare", "restore", "complete"}, fake.calls)
+ require.Equal(testRepairExecutionKey, fake.executionKey)
+}
+
+func (s *BatchConsolidatorTestSuite) TestRepairExistingCSCBRecordsAlreadyCleanObjectWithoutRebuild() {
+ require := testutil.Require(s.T())
+ oldKey := "consolidated/already-clean.cscb.zstd"
+ completed := repairActivityManifest(cscbrepair.StateCompleted, oldKey, "")
+ completed.Outcome = cscbrepair.OutcomeAlreadyCleanStorageNeutral
+ fake := &repairActivityFake{prepared: completed}
+ s.batchConsolidator.repairer = fake
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: 2,
+ StartHeight: 900,
+ EndHeight: 1200,
+ MaxBlocks: 100,
+ RepairExecutionKey: testRepairExecutionKey,
+ })
+ require.NoError(err)
+ require.Equal(uint64(1), response.RepairedObjects)
+ require.Equal(uint64(1), response.ScannedBlocks)
+ require.Zero(response.ConsolidatedBlocks)
+ require.Zero(response.PromotedBlocks)
+ require.Equal(oldKey, response.ObjectKey)
+ require.Equal([]string{"prepare"}, fake.calls)
+}
+
+func (s *BatchConsolidatorTestSuite) TestRepairExistingCSCBPreparesAssignedObject() {
+ require := testutil.Require(s.T())
+ oldKey := "consolidated/assigned-dirty.cscb.zstd"
+ completed := repairActivityManifest(cscbrepair.StateCompleted, oldKey, "")
+ completed.Outcome = cscbrepair.OutcomeAlreadyCleanStorageNeutral
+ fake := &repairActivityFake{prepared: completed}
+ s.batchConsolidator.repairer = fake
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: 2,
+ StartHeight: 900,
+ EndHeight: 1200,
+ MaxBlocks: 100,
+ RepairExecutionKey: testRepairExecutionKey,
+ RepairObjectKey: oldKey,
+ })
+ require.NoError(err)
+ require.Equal(oldKey, response.OldObjectKey)
+ require.Equal([]string{"prepare_object"}, fake.calls)
+ require.Equal(oldKey, fake.objectKey)
+}
+
+func (s *BatchConsolidatorTestSuite) TestRepairExistingCSCBCandidateListing() {
+ require := testutil.Require(s.T())
+ objectKeys := []string{"consolidated/dirty-b.cscb.zstd", "consolidated/dirty-a.cscb.zstd"}
+ fake := &repairActivityFake{candidateObjectKeys: objectKeys}
+ s.batchConsolidator.repairer = fake
+
+ response, err := s.batchConsolidator.executeRepairCandidates(context.Background(), &BatchConsolidatorRepairCandidatesRequest{
+ Tag: 2,
+ StartHeight: 900,
+ EndHeight: 1200,
+ Limit: 2,
+ })
+ require.NoError(err)
+ require.Equal(objectKeys, response.ObjectKeys)
+ require.Equal(2, fake.candidateLimit)
+}
+
+func repairActivityManifest(state cscbrepair.State, oldKey string, newKey string) *cscbrepair.Manifest {
+ return &cscbrepair.Manifest{
+ ID: 42,
+ Tag: 2,
+ State: state,
+ StartHeight: 1000,
+ EndHeight: 1001,
+ CanonicalBlockCount: 1,
+ TotalBlockCount: 1,
+ OldConsolidatedObjectKey: oldKey,
+ NewConsolidatedObjectKey: newKey,
+ Blocks: []cscbrepair.Block{{
+ BlockMetadataID: 10,
+ Canonical: true,
+ Tag: 2,
+ Height: 1000,
+ Hash: "hash-1000",
+ SingleBlockObjectKey: "single-block/1000.gzip",
+ }},
+ }
+}
+
+type repairActivityFake struct {
+ cscbrepair.Repairer
+ prepared *cscbrepair.Manifest
+ restored *cscbrepair.Manifest
+ verified *cscbrepair.Manifest
+ completed *cscbrepair.Manifest
+ calls []string
+ executionKey string
+ objectKey string
+ candidateObjectKeys []string
+ candidateLimit int
+ pinnedPayloads []cscbrepair.PinnedPayload
+}
+
+func (f *repairActivityFake) PrepareNext(_ context.Context, executionKey string, _ uint32, _ uint64, _ uint64, _ uint64, _ cscbrepair.Progress) (*cscbrepair.Manifest, error) {
+ f.calls = append(f.calls, "prepare")
+ f.executionKey = executionKey
+ return f.prepared, nil
+}
+
+func (f *repairActivityFake) PrepareObject(_ context.Context, executionKey string, _ uint32, _ uint64, _ uint64, _ uint64, objectKey string, _ cscbrepair.Progress) (*cscbrepair.Manifest, error) {
+ f.calls = append(f.calls, "prepare_object")
+ f.executionKey = executionKey
+ f.objectKey = objectKey
+ return f.prepared, nil
+}
+
+func (f *repairActivityFake) ListCandidates(_ context.Context, _ uint32, _ uint64, _ uint64, limit int) ([]string, error) {
+ f.candidateLimit = limit
+ return f.candidateObjectKeys, nil
+}
+
+func (f *repairActivityFake) Restore(context.Context, int64, cscbrepair.Progress) (*cscbrepair.Manifest, error) {
+ f.calls = append(f.calls, "restore")
+ return f.restored, nil
+}
+
+func (f *repairActivityFake) VerifyRebuilt(context.Context, int64, cscbrepair.Progress) (*cscbrepair.Manifest, error) {
+ f.calls = append(f.calls, "verify")
+ return f.verified, nil
+}
+
+func (f *repairActivityFake) VisitPinnedPayloads(
+ _ context.Context,
+ _ int64,
+ _ cscbrepair.Progress,
+ visit func(cscbrepair.PinnedPayload) error,
+) error {
+ f.calls = append(f.calls, "visit_pinned")
+ for _, payload := range f.pinnedPayloads {
+ if err := visit(payload); err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+func (f *repairActivityFake) VerifyAndPromote(
+ context.Context,
+ int64,
+ string,
+ []cscbrepair.RebuiltPlacement,
+ time.Duration,
+ cscbrepair.Progress,
+) (*cscbrepair.Manifest, error) {
+ f.calls = append(f.calls, "verify_promote")
+ return f.verified, nil
+}
+
+func (f *repairActivityFake) Complete(context.Context, int64, cscbrepair.Progress) (*cscbrepair.Manifest, error) {
+ f.calls = append(f.calls, "complete")
+ return f.completed, nil
+}
+
+const testRepairExecutionKey = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
+
+func marshalRepairBlock(t *testing.T, block *api.Block) []byte {
+ t.Helper()
+ raw, err := proto.Marshal(storageutils.CloneBlockWithoutStoragePlacement(block))
+ testutil.Require(t).NoError(err)
+ return raw
+}
diff --git a/internal/workflow/activity/batch_consolidator_test.go b/internal/workflow/activity/batch_consolidator_test.go
new file mode 100644
index 00000000..136a270a
--- /dev/null
+++ b/internal/workflow/activity/batch_consolidator_test.go
@@ -0,0 +1,792 @@
+package activity
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "go.temporal.io/sdk/testsuite"
+ "go.uber.org/fx"
+ "go.uber.org/mock/gomock"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/dlq"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage"
+ blobstoragemocks "github.com/coinbase/chainstorage/internal/storage/blobstorage/mocks"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ metastoragemocks "github.com/coinbase/chainstorage/internal/storage/metastorage/mocks"
+ storageutils "github.com/coinbase/chainstorage/internal/storage/utils"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type BatchConsolidatorTestSuite struct {
+ suite.Suite
+ testsuite.WorkflowTestSuite
+ env *cadence.TestEnv
+ ctrl *gomock.Controller
+ metaStorage *metastoragemocks.MockMetaStorage
+ blobStorage *blobstoragemocks.MockBlobStorage
+ app testapp.TestApp
+ batchConsolidator *BatchConsolidator
+}
+
+func TestBatchConsolidatorTestSuite(t *testing.T) {
+ suite.Run(t, new(BatchConsolidatorTestSuite))
+}
+
+func (s *BatchConsolidatorTestSuite) SetupTest() {
+ require := testutil.Require(s.T())
+ cfg, err := config.New()
+ require.NoError(err)
+ cfg.AWS.Storage.Consolidation.Enabled = true
+ cfg.AWS.Storage.Consolidation.Mode = config.ConsolidationModeShadowDualWrite
+
+ s.env = cadence.NewTestActivityEnv(s)
+ s.ctrl = gomock.NewController(s.T())
+ s.metaStorage = metastoragemocks.NewMockMetaStorage(s.ctrl)
+ s.blobStorage = blobstoragemocks.NewMockBlobStorage(s.ctrl)
+ s.app = testapp.New(
+ s.T(),
+ Module,
+ testapp.WithConfig(cfg),
+ cadence.WithTestEnv(s.env),
+ fx.Provide(func() metastorage.MetaStorage {
+ return s.metaStorage
+ }),
+ fx.Provide(func() blobstorage.BlobStorage {
+ return s.blobStorage
+ }),
+ fx.Provide(dlq.NewNop),
+ fx.Populate(&s.batchConsolidator),
+ )
+}
+
+func (s *BatchConsolidatorTestSuite) TearDownTest() {
+ s.app.Close()
+ s.ctrl.Finish()
+ s.env.AssertExpectations(s.T())
+}
+
+func (s *BatchConsolidatorTestSuite) TestEmptyScanNoOps() {
+ require := testutil.Require(s.T())
+ request := &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 100,
+ }
+ s.metaStorage.EXPECT().
+ GetBlocksMissingConsolidationShadow(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks).
+ Return(nil, nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestAutoConsolidateEmptyScanNoOpsWhenShadowsAlreadyExist() {
+ require := testutil.Require(s.T())
+ request := &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 100,
+ }
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).
+ Return(&metastorage.ConsolidationPromotionResult{}, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksMissingConsolidationShadow(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks).
+ Return(nil, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksByHeightRange(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight).
+ Return(makeCanonicalWindow(100, 100), nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestAutoConsolidateEmptyScanPromotesExistingShadowsForRetryCleanup() {
+ require := testutil.Require(s.T())
+ request := &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 100,
+ }
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).
+ Return(&metastorage.ConsolidationPromotionResult{Blocks: 12}, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksMissingConsolidationShadow(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks).
+ Return(nil, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksByHeightRange(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight).
+ Return(makeCanonicalWindow(100, 100), nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ PromotedBlocks: 12,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestAutoConsolidateRejectsMissingCoverageBeforePromotion() {
+ require := testutil.Require(s.T())
+ request := &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 100,
+ }
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).
+ Times(0)
+ s.metaStorage.EXPECT().
+ GetBlocksMissingConsolidationShadow(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks).
+ Times(0)
+ s.metaStorage.EXPECT().
+ GetBlocksByHeightRange(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight).
+ Return(makeCanonicalWindow(99, 100), nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.Error(err)
+ require.Equal(&BatchConsolidatorResponse{}, response)
+ require.Contains(err.Error(), "auto_consolidate requires a full canonical consolidation window")
+}
+
+func (s *BatchConsolidatorTestSuite) TestDeprecatedHistoricalBackfillAliasRemainsAccepted() {
+ require := testutil.Require(s.T())
+ request := &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 100,
+ }
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).
+ Return(&metastorage.ConsolidationPromotionResult{}, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksMissingConsolidationShadow(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks).
+ Return(nil, nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestConsolidatesAndPersistsShadowPlacements() {
+ require := testutil.Require(s.T())
+ records, blocks := makeConsolidatorFixture(2, 1000)
+ request := &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: records[0].Metadata.GetTag(),
+ StartHeight: 1000,
+ EndHeight: 1002,
+ MaxBlocks: 100,
+ }
+ objectKey := "consolidated/v=000000000001/shard=000000001000-000000001999/000000001000-000000001002-sha.zstd"
+ capturedPayloadPaths := make([]string, 0, len(records))
+ placements := []blobstorage.BlockPlacement{
+ {
+ MetadataID: records[0].ID,
+ Height: records[0].Metadata.GetHeight(),
+ Hash: records[0].Metadata.GetHash(),
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 0,
+ ByteLength: 100,
+ UncompressedLength: 100,
+ },
+ {
+ MetadataID: records[1].ID,
+ Height: records[1].Metadata.GetHeight(),
+ Hash: records[1].Metadata.GetHash(),
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 100,
+ ByteLength: 110,
+ UncompressedLength: 110,
+ },
+ }
+ expectedShadows := []*metastorage.ConsolidationShadowPlacement{
+ {
+ BlockMetadataID: records[0].ID,
+ Tag: records[0].Metadata.GetTag(),
+ Height: records[0].Metadata.GetHeight(),
+ Hash: records[0].Metadata.GetHash(),
+ SingleBlockObjectKeyMain: records[0].Metadata.GetObjectKeyMain(),
+ ConsolidatedObjectKeyMain: objectKey,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 0,
+ ByteLength: 100,
+ UncompressedLength: 100,
+ },
+ {
+ BlockMetadataID: records[1].ID,
+ Tag: records[1].Metadata.GetTag(),
+ Height: records[1].Metadata.GetHeight(),
+ Hash: records[1].Metadata.GetHash(),
+ SingleBlockObjectKeyMain: records[1].Metadata.GetObjectKeyMain(),
+ ConsolidatedObjectKeyMain: objectKey,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: 100,
+ ByteLength: 110,
+ UncompressedLength: 110,
+ },
+ }
+ s.batchConsolidator.config.AWS.Storage.Consolidation.LocalSpillDir = s.T().TempDir()
+
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).
+ Return(&metastorage.ConsolidationPromotionResult{}, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksMissingConsolidationShadow(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks).
+ Return(records, nil)
+ for i, record := range records {
+ s.blobStorage.EXPECT().Download(gomock.Any(), record.Metadata).Return(blocks[i], nil)
+ }
+ s.blobStorage.EXPECT().
+ UploadConsolidated(gomock.Any(), consolidatedPayloadsMatchAndCapturePaths(blocks, []int64{records[0].ID, records[1].ID}, &capturedPayloadPaths)).
+ Return(objectKey, placements, nil)
+ s.metaStorage.EXPECT().
+ PersistBlockConsolidationShadows(gomock.Any(), expectedShadows).
+ Return(nil)
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).
+ Return(&metastorage.ConsolidationPromotionResult{Blocks: 2}, nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: 2,
+ ConsolidatedBlocks: 2,
+ PromotedBlocks: 2,
+ ObjectKey: objectKey,
+ }, response)
+ require.Len(capturedPayloadPaths, len(records))
+ for _, path := range capturedPayloadPaths {
+ _, statErr := os.Stat(path)
+ require.ErrorIs(statErr, os.ErrNotExist)
+ }
+}
+
+func (s *BatchConsolidatorTestSuite) TestAutoConsolidateAllowsPartialRetryWindow() {
+ require := testutil.Require(s.T())
+ records, blocks := makeConsolidatorFixture(3, 1000)
+ records = records[:2]
+ blocks = blocks[:2]
+ request := &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: records[0].Metadata.GetTag(),
+ StartHeight: 1000,
+ EndHeight: 1003,
+ MaxBlocks: 3,
+ }
+
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).Times(2).
+ Return(&metastorage.ConsolidationPromotionResult{}, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksMissingConsolidationShadow(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks).
+ Return(records, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksByHeightRange(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight).
+ Return(makeCanonicalWindow(3, 1000), nil)
+ for i := range records {
+ s.blobStorage.EXPECT().Download(gomock.Any(), records[i].Metadata).Return(blocks[i], nil)
+ }
+ s.blobStorage.EXPECT().
+ UploadConsolidated(gomock.Any(), consolidatedPayloadsMatch(blocks, []int64{records[0].ID, records[1].ID})).
+ Return("object-key", makeConsolidatorPlacements(records), nil)
+ s.metaStorage.EXPECT().
+ PersistBlockConsolidationShadows(gomock.Any(), gomock.Any()).
+ Return(nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: 2,
+ ConsolidatedBlocks: 2,
+ ObjectKey: "object-key",
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestAutoConsolidateAllowsGappedMissingShadowScanWhenCanonicalWindowIsFull() {
+ require := testutil.Require(s.T())
+ allRecords, allBlocks := makeConsolidatorFixture(3, 1000)
+ records := []*metastorage.BlockMetadataRecord{allRecords[0], allRecords[2]}
+ blocks := []*api.Block{allBlocks[0], allBlocks[2]}
+ canonicalWindow := makeCanonicalWindow(3, 1000)
+ canonicalWindow[1].Skipped = true
+ request := &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: records[0].Metadata.GetTag(),
+ StartHeight: 1000,
+ EndHeight: 1003,
+ MaxBlocks: 3,
+ }
+
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).Times(2).
+ Return(&metastorage.ConsolidationPromotionResult{}, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksMissingConsolidationShadow(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks).
+ Return(records, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksByHeightRange(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight).
+ Return(canonicalWindow, nil)
+ for i := range records {
+ s.blobStorage.EXPECT().Download(gomock.Any(), records[i].Metadata).Return(blocks[i], nil)
+ }
+ s.blobStorage.EXPECT().
+ UploadConsolidated(gomock.Any(), consolidatedPayloadsMatch(blocks, []int64{records[0].ID, records[1].ID})).
+ Return("object-key", makeConsolidatorPlacements(records), nil)
+ s.metaStorage.EXPECT().
+ PersistBlockConsolidationShadows(gomock.Any(), gomock.Any()).
+ Return(nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: 2,
+ ConsolidatedBlocks: 2,
+ ObjectKey: "object-key",
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestDownloadedBlockMetadataMismatchFailsBeforeUpload() {
+ require := testutil.Require(s.T())
+ records, blocks := makeConsolidatorFixture(1, 1000)
+ blocks[0].Metadata.Hash = "wrong-hash"
+ request := &BatchConsolidatorRequest{
+ Tag: records[0].Metadata.GetTag(),
+ StartHeight: 1000,
+ EndHeight: 1001,
+ MaxBlocks: 100,
+ }
+ s.metaStorage.EXPECT().
+ GetBlocksMissingConsolidationShadow(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks).
+ Return(records, nil)
+ s.blobStorage.EXPECT().Download(gomock.Any(), records[0].Metadata).Return(blocks[0], nil)
+
+ _, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.Error(err)
+ require.Contains(err.Error(), "downloaded block metadata mismatch")
+}
+
+func (s *BatchConsolidatorTestSuite) TestUploadFailureDoesNotPersistShadowPlacements() {
+ require := testutil.Require(s.T())
+ records, blocks := makeConsolidatorFixture(1, 1000)
+ request := &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: records[0].Metadata.GetTag(),
+ StartHeight: 1000,
+ EndHeight: 1001,
+ MaxBlocks: 1,
+ }
+
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).
+ Return(&metastorage.ConsolidationPromotionResult{}, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksMissingConsolidationShadow(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks).
+ Return(records, nil)
+ s.metaStorage.EXPECT().
+ GetBlocksByHeightRange(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight).
+ Return(makeCanonicalWindow(1, 1000), nil)
+ s.blobStorage.EXPECT().Download(gomock.Any(), records[0].Metadata).Return(blocks[0], nil)
+ s.blobStorage.EXPECT().
+ UploadConsolidated(gomock.Any(), consolidatedPayloadsMatch(blocks, []int64{records[0].ID})).
+ Return("", nil, fmt.Errorf("CSCB compressed payload length exceeds max_compressed_bytes"))
+
+ _, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.Error(err)
+ require.Contains(err.Error(), "failed to upload consolidated block object")
+}
+
+func (s *BatchConsolidatorTestSuite) TestGetShadowStatsReturnsPersistedShadowStats() {
+ require := testutil.Require(s.T())
+ request := &BatchConsolidatorStatsRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 2000,
+ }
+ s.metaStorage.EXPECT().
+ GetBlockConsolidationShadowStats(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight).
+ Return(&metastorage.ConsolidationShadowStats{Objects: 3, Blocks: 2750}, nil)
+
+ response, err := s.batchConsolidator.GetShadowStats(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorStatsResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ShadowObjects: 3,
+ ShadowBlocks: 2750,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestGetLatestBlockReturnsMetastoreLatest() {
+ require := testutil.Require(s.T())
+ request := &BatchConsolidatorLatestBlockRequest{Tag: 2}
+ s.metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), request.Tag).
+ Return(&api.BlockMetadata{Tag: request.Tag, Height: 1234}, nil)
+
+ response, err := s.batchConsolidator.GetLatestBlock(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorLatestBlockResponse{
+ Tag: request.Tag,
+ Height: 1234,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestGetLatestBlockRejectsNilMetastoreLatest() {
+ require := testutil.Require(s.T())
+ request := &BatchConsolidatorLatestBlockRequest{Tag: 2}
+ s.metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), request.Tag).
+ Return(nil, nil)
+
+ response, err := s.batchConsolidator.GetLatestBlock(s.env.BackgroundContext(), request)
+ require.Error(err)
+ require.Nil(response)
+ require.Contains(err.Error(), "latest block not found")
+}
+
+func (s *BatchConsolidatorTestSuite) TestUpdateAutoConsolidateCursorPersistsExclusiveHeight() {
+ require := testutil.Require(s.T())
+ request := &BatchConsolidatorCursorRequest{
+ Tag: 2,
+ Height: 11_000,
+ }
+ s.metaStorage.EXPECT().
+ SetBlockConsolidationCursor(gomock.Any(), metastorage.BatchConsolidatorAutoConsolidateCursor, request.Tag, request.Height).
+ Return(nil)
+
+ response, err := s.batchConsolidator.UpdateAutoConsolidateCursor(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorCursorResponse{
+ Tag: request.Tag,
+ Height: request.Height,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestPromoteFinalizedPromotesShadows() {
+ require := testutil.Require(s.T())
+ gateHeight := uint64(2_000)
+ safeLag := uint64(100)
+ s.batchConsolidator.config.AWS.Storage.Consolidation.Mode = config.ConsolidationModePromoteFinalized
+ s.batchConsolidator.config.AWS.Storage.Consolidation.PromotionGateHeight = &gateHeight
+ s.batchConsolidator.config.AWS.Storage.Consolidation.SafePromotionLag = &safeLag
+ request := &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1_000,
+ EndHeight: 1_100,
+ MaxBlocks: 25,
+ }
+ s.metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), request.Tag).
+ Return(&api.BlockMetadata{Tag: request.Tag, Height: 1_300}, nil)
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, request.EndHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).
+ Return(&metastorage.ConsolidationPromotionResult{Blocks: 12}, nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: 12,
+ ConsolidatedBlocks: 12,
+ PromotedBlocks: 12,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestPromoteFinalizedExecuteCapsAtGateAndSafeHeight() {
+ require := testutil.Require(s.T())
+ gateHeight := uint64(1_050)
+ safeLag := uint64(10)
+ s.batchConsolidator.config.AWS.Storage.Consolidation.Mode = config.ConsolidationModePromoteFinalized
+ s.batchConsolidator.config.AWS.Storage.Consolidation.PromotionGateHeight = &gateHeight
+ s.batchConsolidator.config.AWS.Storage.Consolidation.SafePromotionLag = &safeLag
+ request := &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1_000,
+ EndHeight: 1_200,
+ MaxBlocks: 25,
+ }
+ s.metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), request.Tag).
+ Return(&api.BlockMetadata{Tag: request.Tag, Height: 1_080}, nil)
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, gateHeight, request.MaxBlocks, config.DefaultSingleBlockObjectRetention).
+ Return(&metastorage.ConsolidationPromotionResult{Blocks: 12}, nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: gateHeight,
+ ScannedBlocks: 12,
+ ConsolidatedBlocks: 12,
+ PromotedBlocks: 12,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestPromoteFinalizedExecuteAllowsMissingPromotionGate() {
+ require := testutil.Require(s.T())
+ safeLag := uint64(10)
+ s.batchConsolidator.config.AWS.Storage.Consolidation.Mode = config.ConsolidationModePromoteFinalized
+ s.batchConsolidator.config.AWS.Storage.Consolidation.PromotionGateHeight = nil
+ s.batchConsolidator.config.AWS.Storage.Consolidation.SafePromotionLag = &safeLag
+ request := &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1_000,
+ EndHeight: 1_200,
+ MaxBlocks: 25,
+ }
+ s.metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), request.Tag).
+ Return(&api.BlockMetadata{Tag: request.Tag, Height: 1_080}, nil)
+ s.metaStorage.EXPECT().
+ PromoteBlockConsolidationShadows(gomock.Any(), request.Tag, request.StartHeight, uint64(1_071), request.MaxBlocks, config.DefaultSingleBlockObjectRetention).
+ Return(&metastorage.ConsolidationPromotionResult{Blocks: 12}, nil)
+
+ response, err := s.batchConsolidator.Execute(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: 1_071,
+ ScannedBlocks: 12,
+ ConsolidatedBlocks: 12,
+ PromotedBlocks: 12,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestPromoteFinalizedPlanCapsAtGateAndSafeHeight() {
+ require := testutil.Require(s.T())
+ gateHeight := uint64(1_050)
+ safeLag := uint64(10)
+ s.batchConsolidator.config.AWS.Storage.Consolidation.Mode = config.ConsolidationModePromoteFinalized
+ s.batchConsolidator.config.AWS.Storage.Consolidation.PromotionGateHeight = &gateHeight
+ s.batchConsolidator.config.AWS.Storage.Consolidation.SafePromotionLag = &safeLag
+ request := &BatchConsolidatorPlanRequest{
+ Tag: 2,
+ StartHeight: 1_000,
+ EndHeight: 1_200,
+ }
+ s.metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), request.Tag).
+ Return(&api.BlockMetadata{Tag: request.Tag, Height: 1_080}, nil)
+
+ response, err := s.batchConsolidator.GetPromotionPlan(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorPlanResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: gateHeight,
+ LatestHeight: 1_080,
+ SafePromotionHeight: 1_070,
+ PromotionGateHeight: gateHeight,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestPromoteFinalizedPlanAllowsMissingPromotionGate() {
+ require := testutil.Require(s.T())
+ safeLag := uint64(10)
+ s.batchConsolidator.config.AWS.Storage.Consolidation.Mode = config.ConsolidationModePromoteFinalized
+ s.batchConsolidator.config.AWS.Storage.Consolidation.PromotionGateHeight = nil
+ s.batchConsolidator.config.AWS.Storage.Consolidation.SafePromotionLag = &safeLag
+ request := &BatchConsolidatorPlanRequest{
+ Tag: 2,
+ StartHeight: 1_000,
+ EndHeight: 1_200,
+ }
+ s.metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), request.Tag).
+ Return(&api.BlockMetadata{Tag: request.Tag, Height: 1_080}, nil)
+
+ response, err := s.batchConsolidator.GetPromotionPlan(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorPlanResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: 1_071,
+ LatestHeight: 1_080,
+ SafePromotionHeight: 1_070,
+ PromotionGateHeight: request.EndHeight,
+ }, response)
+}
+
+func (s *BatchConsolidatorTestSuite) TestPromoteFinalizedPlanNoOpsWhenLatestBelowSafeLag() {
+ require := testutil.Require(s.T())
+ gateHeight := uint64(1_050)
+ safeLag := uint64(100)
+ s.batchConsolidator.config.AWS.Storage.Consolidation.Mode = config.ConsolidationModePromoteFinalized
+ s.batchConsolidator.config.AWS.Storage.Consolidation.PromotionGateHeight = &gateHeight
+ s.batchConsolidator.config.AWS.Storage.Consolidation.SafePromotionLag = &safeLag
+ request := &BatchConsolidatorPlanRequest{
+ Tag: 2,
+ StartHeight: 1_000,
+ EndHeight: 1_200,
+ }
+ s.metaStorage.EXPECT().
+ GetLatestBlock(gomock.Any(), request.Tag).
+ Return(&api.BlockMetadata{Tag: request.Tag, Height: 50}, nil)
+
+ response, err := s.batchConsolidator.GetPromotionPlan(s.env.BackgroundContext(), request)
+ require.NoError(err)
+ require.Equal(&BatchConsolidatorPlanResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.StartHeight,
+ LatestHeight: 50,
+ SafePromotionHeight: 0,
+ PromotionGateHeight: gateHeight,
+ }, response)
+}
+
+type consolidatedPayloadsMatcher struct {
+ blocks []*api.Block
+ metadataIDs []int64
+ capturedPaths *[]string
+}
+
+func consolidatedPayloadsMatch(blocks []*api.Block, metadataIDs []int64) gomock.Matcher {
+ return consolidatedPayloadsMatchAndCapturePaths(blocks, metadataIDs, nil)
+}
+
+func consolidatedPayloadsMatchAndCapturePaths(blocks []*api.Block, metadataIDs []int64, capturedPaths *[]string) gomock.Matcher {
+ return &consolidatedPayloadsMatcher{
+ blocks: blocks,
+ metadataIDs: metadataIDs,
+ capturedPaths: capturedPaths,
+ }
+}
+
+func (m *consolidatedPayloadsMatcher) Matches(x any) bool {
+ payloads, ok := x.([]blobstorage.ConsolidatedBlockPayload)
+ if !ok || len(payloads) != len(m.blocks) || len(payloads) != len(m.metadataIDs) {
+ return false
+ }
+ paths := make([]string, 0, len(payloads))
+ for i, payload := range payloads {
+ if payload.MetadataID != m.metadataIDs[i] {
+ return false
+ }
+ if !proto.Equal(payload.Metadata, m.blocks[i].GetMetadata()) {
+ return false
+ }
+ if m.capturedPaths != nil {
+ fileSource, ok := payload.RawBlockPayload.(blobstorage.FilePayloadSource)
+ if !ok {
+ return false
+ }
+ if _, err := os.Stat(fileSource.Path()); err != nil {
+ return false
+ }
+ paths = append(paths, fileSource.Path())
+ }
+ reader, err := payload.RawBlockPayload.Open(context.Background())
+ if err != nil {
+ return false
+ }
+ raw, err := io.ReadAll(reader)
+ _ = reader.Close()
+ if err != nil || uint64(len(raw)) != payload.UncompressedLength {
+ return false
+ }
+ var block api.Block
+ if err := proto.Unmarshal(raw, &block); err != nil {
+ return false
+ }
+ if !proto.Equal(&block, storageutils.CloneBlockWithoutStoragePlacement(m.blocks[i])) {
+ return false
+ }
+ }
+ if m.capturedPaths != nil {
+ *m.capturedPaths = paths
+ }
+ return true
+}
+
+func (m *consolidatedPayloadsMatcher) String() string {
+ return fmt.Sprintf("matches %d consolidated block payloads", len(m.blocks))
+}
+
+func makeConsolidatorFixture(count int, startHeight uint64) ([]*metastorage.BlockMetadataRecord, []*api.Block) {
+ records := make([]*metastorage.BlockMetadataRecord, count)
+ blocks := make([]*api.Block, count)
+ for i := 0; i < count; i++ {
+ height := startHeight + uint64(i)
+ metadata := &api.BlockMetadata{
+ Tag: 2,
+ Height: height,
+ Hash: fmt.Sprintf("hash-%d", height),
+ ParentHash: fmt.Sprintf("hash-%d", height-1),
+ ParentHeight: height - 1,
+ ObjectKeyMain: fmt.Sprintf("single-block/%d.gzip", height),
+ }
+ records[i] = &metastorage.BlockMetadataRecord{
+ ID: int64(10 + i),
+ Metadata: metadata,
+ }
+ blocks[i] = &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ETHEREUM,
+ Network: common.Network_NETWORK_ETHEREUM_MAINNET,
+ Metadata: proto.Clone(metadata).(*api.BlockMetadata),
+ }
+ }
+ return records, blocks
+}
+
+func makeCanonicalWindow(count int, startHeight uint64) []*api.BlockMetadata {
+ records, _ := makeConsolidatorFixture(count, startHeight)
+ blocks := make([]*api.BlockMetadata, len(records))
+ for i, record := range records {
+ blocks[i] = proto.Clone(record.Metadata).(*api.BlockMetadata)
+ }
+ return blocks
+}
+
+func makeConsolidatorPlacements(records []*metastorage.BlockMetadataRecord) []blobstorage.BlockPlacement {
+ placements := make([]blobstorage.BlockPlacement, len(records))
+ for i, record := range records {
+ placements[i] = blobstorage.BlockPlacement{
+ MetadataID: record.ID,
+ Height: record.Metadata.GetHeight(),
+ Hash: record.Metadata.GetHash(),
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ ByteOffset: uint64(i * 100),
+ ByteLength: 100,
+ UncompressedLength: 100,
+ }
+ }
+ return placements
+}
diff --git a/internal/workflow/activity/extractor.go b/internal/workflow/activity/extractor.go
index d0da8b8e..8e4fd826 100644
--- a/internal/workflow/activity/extractor.go
+++ b/internal/workflow/activity/extractor.go
@@ -3,6 +3,7 @@ package activity
import (
"context"
+ "go.temporal.io/sdk/activity"
"go.temporal.io/sdk/workflow"
"go.uber.org/fx"
"go.uber.org/zap"
@@ -21,19 +22,21 @@ import (
type (
Extractor struct {
baseActivity
- blockchainClient client.Client
- blobStorage blobstorage.BlobStorage
- metaStorage metastorage.MetaStorage
- failoverManager endpoints.FailoverManager
+ blockchainClient client.Client
+ blobStorage blobstorage.BlobStorage
+ singleBlockUploader blobstorage.SingleBlockUploader
+ metaStorage metastorage.MetaStorage
+ failoverManager endpoints.FailoverManager
}
ExtractorParams struct {
fx.In
- Runtime cadence.Runtime
- BlockchainClient client.Client `name:"slave"`
- BlobStorage blobstorage.BlobStorage
- MetaStorage metastorage.MetaStorage
- FailoverManager endpoints.FailoverManager
+ Runtime cadence.Runtime
+ BlockchainClient client.Client `name:"slave"`
+ BlobStorage blobstorage.BlobStorage
+ SingleBlockUploader blobstorage.SingleBlockUploader
+ MetaStorage metastorage.MetaStorage
+ FailoverManager endpoints.FailoverManager
}
ExtractorRequest struct {
@@ -53,11 +56,12 @@ type (
func NewExtractor(params ExtractorParams) *Extractor {
a := &Extractor{
- baseActivity: newBaseActivity(ActivityExtractor, params.Runtime),
- blockchainClient: params.BlockchainClient,
- blobStorage: params.BlobStorage,
- metaStorage: params.MetaStorage,
- failoverManager: params.FailoverManager,
+ baseActivity: newBaseActivity(ActivityExtractor, params.Runtime),
+ blockchainClient: params.BlockchainClient,
+ blobStorage: params.BlobStorage,
+ singleBlockUploader: params.SingleBlockUploader,
+ metaStorage: params.MetaStorage,
+ failoverManager: params.FailoverManager,
}
a.register(a.execute)
@@ -89,12 +93,18 @@ func (a *Extractor) execute(ctx context.Context, request *ExtractorRequest) (*Ex
ctx = failoverCtx
}
+ heartbeatOpt := client.WithHeartbeat(func(ctx context.Context, details ...any) {
+ activity.RecordHeartbeat(ctx, details...)
+ })
+
result := make([]*api.BlockMetadata, len(request.Heights))
group, ctx := syncgroup.New(ctx)
for i, height := range request.Heights {
i := i
height := height
group.Go(func() error {
+ activity.RecordHeartbeat(ctx, "extractor.block.start", height)
+
var block *api.Block
var err error
if request.UpgradeFromTag != nil {
@@ -112,7 +122,7 @@ func (a *Extractor) execute(ctx context.Context, request *ExtractorRequest) (*Ex
}
if block == nil {
- var opts []client.ClientOption
+ opts := []client.ClientOption{heartbeatOpt}
if request.WithBestEffort {
// This option is enabled while reprocessing extractors.
// The block is queried in a best-effort manner and partial data may be returned.
@@ -126,11 +136,14 @@ func (a *Extractor) execute(ctx context.Context, request *ExtractorRequest) (*Ex
}
}
- objectKey, err := a.blobStorage.Upload(ctx, block, request.DataCompression)
+ activity.RecordHeartbeat(ctx, "extractor.block.fetched", height)
+
+ objectKey, err := a.singleBlockUploader.Upload(ctx, block, request.DataCompression)
if err != nil {
logger.Error("failed to upload to blob store", zap.Error(err))
return xerrors.Errorf("failed to upload to blob store: %w", err)
}
+ activity.RecordHeartbeat(ctx, "extractor.block.uploaded", height)
block.Metadata.ObjectKeyMain = objectKey
result[i] = block.Metadata
diff --git a/internal/workflow/activity/extractor_test.go b/internal/workflow/activity/extractor_test.go
index 05f86a87..b644ca81 100644
--- a/internal/workflow/activity/extractor_test.go
+++ b/internal/workflow/activity/extractor_test.go
@@ -26,13 +26,14 @@ import (
type ExtractorTestSuite struct {
suite.Suite
testsuite.WorkflowTestSuite
- env *cadence.TestEnv
- ctrl *gomock.Controller
- blockchainClient *clientmocks.MockClient
- metaStorage *metastoragemocks.MockMetaStorage
- blobStorage *blobstoragemocks.MockBlobStorage
- app testapp.TestApp
- extractor *Extractor
+ env *cadence.TestEnv
+ ctrl *gomock.Controller
+ blockchainClient *clientmocks.MockClient
+ metaStorage *metastoragemocks.MockMetaStorage
+ blobStorage *blobstoragemocks.MockBlobStorage
+ singleBlockUploader *blobstoragemocks.MockSingleBlockUploader
+ app testapp.TestApp
+ extractor *Extractor
}
func TestExtractorTestSuite(t *testing.T) {
@@ -44,7 +45,12 @@ func (s *ExtractorTestSuite) SetupTest() {
s.ctrl = gomock.NewController(s.T())
s.blockchainClient = clientmocks.NewMockClient(s.ctrl)
s.metaStorage = metastoragemocks.NewMockMetaStorage(s.ctrl)
+ s.metaStorage.EXPECT().
+ AcquireSingleBlockUploadGuard(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+ Return(metastorage.NewUnfencedSingleBlockUploadGuard(), nil).
+ AnyTimes()
s.blobStorage = blobstoragemocks.NewMockBlobStorage(s.ctrl)
+ s.singleBlockUploader = blobstoragemocks.NewMockSingleBlockUploader(s.ctrl)
s.app = testapp.New(
s.T(),
Module,
@@ -52,6 +58,9 @@ func (s *ExtractorTestSuite) SetupTest() {
fx.Provide(func() blobstorage.BlobStorage {
return s.blobStorage
}),
+ fx.Provide(func() blobstorage.SingleBlockUploader {
+ return s.singleBlockUploader
+ }),
fx.Provide(fx.Annotated{
Name: "slave",
Target: func() client.Client {
@@ -89,8 +98,8 @@ func (s *ExtractorTestSuite) TestSuccess() {
Height: height,
},
}
- s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), tag, height).Return(block, nil)
- s.blobStorage.EXPECT().Upload(gomock.Any(), block, api.Compression_NONE).Return(objectKey, nil)
+ s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), tag, height, gomock.Any()).Return(block, nil)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), block, api.Compression_NONE).Return(objectKey, nil)
response, err := s.extractor.Execute(s.env.BackgroundContext(), &ExtractorRequest{
Tag: tag,
@@ -105,6 +114,26 @@ func (s *ExtractorTestSuite) TestSuccess() {
require.Equal(objectKey, metadata.ObjectKeyMain)
}
+func (s *ExtractorTestSuite) TestRetirementFenceFailsBeforeSingleBlockUpload() {
+ const (
+ tag uint32 = 1
+ height uint64 = 123456
+ hash = "0xabcd"
+ )
+ require := testutil.Require(s.T())
+ block := &api.Block{Metadata: &api.BlockMetadata{Tag: tag, Hash: hash, Height: height}}
+ s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), tag, height, gomock.Any()).Return(block, nil)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), block, api.Compression_NONE).
+ Return("", metastorage.ErrSingleBlockRetirementFenced)
+
+ _, err := s.extractor.Execute(s.env.BackgroundContext(), &ExtractorRequest{
+ Tag: tag,
+ Heights: []uint64{height},
+ })
+ require.Error(err)
+ require.Contains(err.Error(), metastorage.ErrSingleBlockRetirementFenced.Error())
+}
+
func (s *ExtractorTestSuite) TestMiniBatch() {
const (
tag uint32 = 1
@@ -125,8 +154,8 @@ func (s *ExtractorTestSuite) TestMiniBatch() {
Height: height + i,
},
}
- s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), tag, height+i).Return(block, nil)
- s.blobStorage.EXPECT().Upload(gomock.Any(), block, api.Compression_NONE).Return(objectKey+strconv.Itoa(int(i)), nil)
+ s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), tag, height+i, gomock.Any()).Return(block, nil)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), block, api.Compression_NONE).Return(objectKey+strconv.Itoa(int(i)), nil)
heights[i] = height + i
}
@@ -163,7 +192,7 @@ func (s *ExtractorTestSuite) TestWithBestEffort() {
},
}
s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), tag, height, gomock.Any()).Return(block, nil)
- s.blobStorage.EXPECT().Upload(gomock.Any(), block, api.Compression_NONE).Return(objectKey, nil)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), block, api.Compression_NONE).Return(objectKey, nil)
response, err := s.extractor.Execute(s.env.BackgroundContext(), &ExtractorRequest{
Tag: tag,
Heights: []uint64{height},
@@ -229,7 +258,7 @@ func (s *ExtractorTestSuite) TestUpgradeSuccess() {
s.metaStorage.EXPECT().GetBlockByHeight(gomock.Any(), oldTag, height).Return(metadata, nil)
s.blobStorage.EXPECT().Download(gomock.Any(), block.Metadata).Return(block, nil)
s.blockchainClient.EXPECT().UpgradeBlock(gomock.Any(), block, newTag).Return(newBlock, nil)
- s.blobStorage.EXPECT().Upload(gomock.Any(), newBlock, api.Compression_NONE).Return(objectKey, nil)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), newBlock, api.Compression_NONE).Return(objectKey, nil)
response, err := s.extractor.Execute(s.env.BackgroundContext(), &ExtractorRequest{
Tag: newTag,
Heights: []uint64{height},
@@ -275,7 +304,7 @@ func (s *ExtractorTestSuite) TestRehydrateSuccess() {
}
s.metaStorage.EXPECT().GetBlockByHeight(gomock.Any(), oldTag, height).Return(metadata, nil)
s.blobStorage.EXPECT().Download(gomock.Any(), block.Metadata).Return(block, nil)
- s.blobStorage.EXPECT().Upload(gomock.Any(), newBlock, api.Compression_NONE).Return(objectKey, nil)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), newBlock, api.Compression_NONE).Return(objectKey, nil)
response, err := s.extractor.Execute(s.env.BackgroundContext(), &ExtractorRequest{
Tag: newTag,
Heights: []uint64{height},
@@ -309,7 +338,7 @@ func (s *ExtractorTestSuite) TestWithDataCompression() {
},
}
s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), tag, height, gomock.Any()).Return(block, nil)
- s.blobStorage.EXPECT().Upload(gomock.Any(), block, api.Compression_GZIP).Return(objectKey, nil)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), block, api.Compression_GZIP).Return(objectKey, nil)
response, err := s.extractor.Execute(s.env.BackgroundContext(), &ExtractorRequest{
Tag: tag,
Heights: []uint64{height},
diff --git a/internal/workflow/activity/latest_block.go b/internal/workflow/activity/latest_block.go
new file mode 100644
index 00000000..99d02560
--- /dev/null
+++ b/internal/workflow/activity/latest_block.go
@@ -0,0 +1,78 @@
+package activity
+
+import (
+ "context"
+
+ "go.temporal.io/sdk/workflow"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/gateway"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ LatestBlock struct {
+ baseActivity
+ config *config.Config
+ logger *zap.Logger
+ client gateway.Client
+ }
+
+ LatestBlockParams struct {
+ fx.In
+ fxparams.Params
+ Runtime cadence.Runtime
+ Client gateway.Client
+ }
+
+ LatestBlockRequest struct {
+ }
+
+ LatestBlockResponse struct {
+ Height uint64
+ }
+)
+
+func NewLatestBlock(params LatestBlockParams) *LatestBlock {
+ r := &LatestBlock{
+ baseActivity: newBaseActivity(ActivityLatestBlock, params.Runtime),
+ config: params.Config,
+ logger: params.Logger,
+ client: params.Client,
+ }
+ r.register(r.execute)
+ return r
+}
+
+func (r *LatestBlock) Execute(ctx workflow.Context, request *LatestBlockRequest) (*LatestBlockResponse, error) {
+ var response LatestBlockResponse
+ err := r.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (r *LatestBlock) execute(ctx context.Context, request *LatestBlockRequest) (*LatestBlockResponse, error) {
+ if err := r.validateRequest(request); err != nil {
+ return nil, err
+ }
+
+ logger := r.getLogger(ctx).With(zap.Reflect("request", request))
+
+ latestBlock, err := r.client.GetLatestBlock(ctx, &api.GetLatestBlockRequest{})
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get chainstorage latest block: %w", err)
+ }
+
+ logger.Debug("GetLatestBlock",
+ zap.Uint64("height", latestBlock.GetHeight()),
+ zap.String("hash", latestBlock.GetHash()),
+ )
+
+ return &LatestBlockResponse{
+ Height: latestBlock.GetHeight(),
+ }, nil
+}
diff --git a/internal/workflow/activity/migrator.go b/internal/workflow/activity/migrator.go
new file mode 100644
index 00000000..69787254
--- /dev/null
+++ b/internal/workflow/activity/migrator.go
@@ -0,0 +1,913 @@
+package activity
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/dynamodb"
+ "github.com/uber-go/tally/v4"
+ "go.temporal.io/sdk/activity"
+ "go.temporal.io/sdk/workflow"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ dynamodb_storage "github.com/coinbase/chainstorage/internal/storage/metastorage/dynamodb"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/model"
+ postgres_storage "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ Migrator struct {
+ baseActivity
+ config *config.Config
+ awsConfig aws.Config
+ dynamoClient *dynamodb.Client
+ blockTable string
+ metrics tally.Scope
+ }
+
+ BlockToMigrate struct {
+ Height uint64
+ Hash string
+ ParentHash string
+ EventSeq int64 // Event sequence for ordering
+ Skipped bool // Whether this is a skipped block
+ }
+
+ GetLatestBlockHeightActivity struct {
+ baseActivity
+ config *config.Config
+ awsConfig aws.Config
+ dynamoClient *dynamodb.Client
+ blockTable string
+ metrics tally.Scope
+ }
+
+ GetLatestBlockFromPostgresActivity struct {
+ baseActivity
+ config *config.Config
+ metrics tally.Scope
+ }
+
+ GetLatestEventFromPostgresActivity struct {
+ baseActivity
+ config *config.Config
+ metrics tally.Scope
+ }
+
+ GetMaxEventIdActivity struct {
+ baseActivity
+ config *config.Config
+ awsConfig aws.Config
+ metrics tally.Scope
+ }
+
+ MigratorParams struct {
+ fx.In
+ fxparams.Params
+ Runtime cadence.Runtime
+ AWSConfig aws.Config
+ }
+
+ MigratorRequest struct {
+ StartEventSequence int64 // Start event sequence (no more StartHeight!)
+ EndEventSequence int64 // End event sequence (exclusive)
+ EventTag uint32
+ Tag uint32
+ Parallelism int // Number of concurrent workers
+ }
+
+ MigratorResponse struct {
+ BlocksMigrated int
+ EventsMigrated int
+ Success bool
+ Message string
+ }
+
+ MigrationData struct {
+ SourceStorage metastorage.MetaStorage
+ DestStorage metastorage.MetaStorage
+ Config *config.Config
+ DynamoClient *dynamodb.Client
+ BlockTable string
+ }
+)
+
+func NewMigrator(params MigratorParams) *Migrator {
+ a := &Migrator{
+ baseActivity: newBaseActivity(ActivityMigrator, params.Runtime),
+ config: params.Config,
+ awsConfig: params.AWSConfig,
+ dynamoClient: dynamodb.NewFromConfig(params.AWSConfig),
+ blockTable: params.Config.AWS.DynamoDB.BlockTable,
+ metrics: params.Metrics,
+ }
+ a.register(a.execute)
+ return a
+}
+
+func NewGetLatestBlockHeightActivity(params MigratorParams) *GetLatestBlockHeightActivity {
+ a := &GetLatestBlockHeightActivity{
+ baseActivity: newBaseActivity(ActivityGetLatestBlockHeight, params.Runtime),
+ config: params.Config,
+ awsConfig: params.AWSConfig,
+ dynamoClient: dynamodb.NewFromConfig(params.AWSConfig),
+ blockTable: params.Config.AWS.DynamoDB.BlockTable,
+ metrics: params.Metrics,
+ }
+ a.register(a.execute)
+ return a
+}
+
+func NewGetLatestBlockFromPostgresActivity(params MigratorParams) *GetLatestBlockFromPostgresActivity {
+ a := &GetLatestBlockFromPostgresActivity{
+ baseActivity: newBaseActivity(ActivityGetLatestBlockFromPostgres, params.Runtime),
+ config: params.Config,
+ metrics: params.Metrics,
+ }
+ a.register(a.execute)
+ return a
+}
+
+func NewGetLatestEventFromPostgresActivity(params MigratorParams) *GetLatestEventFromPostgresActivity {
+ a := &GetLatestEventFromPostgresActivity{
+ baseActivity: newBaseActivity(ActivityGetLatestEventFromPostgres, params.Runtime),
+ config: params.Config,
+ metrics: params.Metrics,
+ }
+ a.register(a.execute)
+ return a
+}
+
+func NewGetMaxEventIdActivity(params MigratorParams) *GetMaxEventIdActivity {
+ a := &GetMaxEventIdActivity{
+ baseActivity: newBaseActivity(ActivityGetMaxEventId, params.Runtime),
+ config: params.Config,
+ awsConfig: params.AWSConfig,
+ metrics: params.Metrics,
+ }
+ a.register(a.execute)
+ return a
+}
+
+func (a *Migrator) Execute(ctx workflow.Context, request *MigratorRequest) (*MigratorResponse, error) {
+ var response MigratorResponse
+ err := a.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *Migrator) execute(ctx context.Context, request *MigratorRequest) (*MigratorResponse, error) {
+ startTime := time.Now()
+ logger := a.getLogger(ctx).With(
+ zap.Int64("startEventSequence", request.StartEventSequence),
+ zap.Int64("endEventSequence", request.EndEventSequence),
+ zap.Uint32("eventTag", request.EventTag),
+ zap.Int("parallelism", request.Parallelism))
+
+ logger.Info("Starting event-driven migration")
+
+ // Validate event sequence range
+ if request.EndEventSequence < request.StartEventSequence {
+ return nil, xerrors.Errorf("invalid request: EndEventSequence (%d) cannot be less than StartEventSequence (%d)",
+ request.EndEventSequence, request.StartEventSequence)
+ }
+
+ // Add heartbeat mechanism - send heartbeat every 10 seconds to avoid timeout
+ heartbeatTicker := time.NewTicker(10 * time.Second)
+ defer heartbeatTicker.Stop()
+
+ go func() {
+ for range heartbeatTicker.C {
+ select {
+ case <-ctx.Done():
+ return
+ default:
+ activity.RecordHeartbeat(ctx, fmt.Sprintf("Processing events [%d, %d), elapsed: %v",
+ request.StartEventSequence, request.EndEventSequence, time.Since(startTime)))
+ }
+ }
+ }()
+
+ // Create storage instances
+ migrationData, err := a.createStorageInstances(ctx)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create storage instances: %w", err)
+ }
+
+ // Step 1: Fetch events by sequence (with parallelism)
+ events, err := a.fetchEventsBySequence(ctx, logger, migrationData, request)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to fetch events: %w", err)
+ }
+
+ if len(events) == 0 {
+ logger.Info("No events found in sequence range")
+ return &MigratorResponse{
+ Success: true,
+ Message: "No events to migrate",
+ }, nil
+ }
+
+ // Step 2: Split events into segments and migrate blocks
+ blocksMigrated, err := a.migrateExtractedBlocks(ctx, logger, migrationData, request, events)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to migrate blocks: %w", err)
+ }
+
+ // Step 3: Migrate events
+ eventsMigrated := 0
+ if len(events) > 0 {
+ eventsMigrated, err = a.persistEvents(ctx, logger, migrationData, request, events)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to migrate events: %w", err)
+ }
+ }
+
+ duration := time.Since(startTime)
+ logger.Info("Event-driven migration completed",
+ zap.Int("blocksMigrated", blocksMigrated),
+ zap.Int("eventsMigrated", eventsMigrated),
+ zap.Duration("duration", duration))
+
+ return &MigratorResponse{
+ BlocksMigrated: blocksMigrated,
+ EventsMigrated: eventsMigrated,
+ Success: true,
+ Message: fmt.Sprintf("Migrated %d blocks and %d events in %v",
+ blocksMigrated, eventsMigrated, duration),
+ }, nil
+}
+
+func (a *Migrator) createStorageInstances(ctx context.Context) (*MigrationData, error) {
+ logger := a.getLogger(ctx)
+
+ // Create DynamoDB storage directly
+ dynamoDBParams := dynamodb_storage.Params{
+ Params: fxparams.Params{
+ Config: a.config,
+ Logger: logger,
+ Metrics: a.metrics,
+ },
+ AWSConfig: a.awsConfig,
+ }
+ sourceResult, err := dynamodb_storage.NewMetaStorage(dynamoDBParams)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create DynamoDB storage: %w", err)
+ }
+
+ // Create PostgreSQL storage using shared connection pool
+ postgresParams := postgres_storage.Params{
+ Params: fxparams.Params{
+ Config: a.config,
+ Logger: logger,
+ Metrics: a.metrics,
+ },
+ }
+ destResult, err := postgres_storage.NewMetaStorage(postgresParams)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create PostgreSQL storage: %w", err)
+ }
+
+ return &MigrationData{
+ SourceStorage: sourceResult.MetaStorage,
+ DestStorage: destResult.MetaStorage,
+ Config: a.config,
+ DynamoClient: a.dynamoClient,
+ BlockTable: a.blockTable,
+ }, nil
+}
+
+func (a *Migrator) fetchEventsBySequence(ctx context.Context, logger *zap.Logger,
+ data *MigrationData, request *MigratorRequest) ([]*model.EventEntry, error) {
+
+ startSeq := request.StartEventSequence
+ endSeq := request.EndEventSequence
+ totalSequences := endSeq - startSeq
+
+ logger.Info("Fetching events by sequence range",
+ zap.Int64("startSeq", startSeq),
+ zap.Int64("endSeq", endSeq),
+ zap.Int64("totalSequences", totalSequences))
+
+ // Record heartbeat before starting the fetch
+ activity.RecordHeartbeat(ctx, fmt.Sprintf("Starting to fetch events [%d, %d)", startSeq, endSeq))
+
+ // GetEventsByEventIdRange already handles the fetching efficiently internally
+ // No need for additional batching/parallelism as DynamoDB Query operation
+ // is already optimized for sequential range scans
+ events, err := data.SourceStorage.GetEventsByEventIdRange(
+ ctx, request.EventTag, startSeq, endSeq)
+
+ if err != nil {
+ // Events must be continuous - any missing events indicate data integrity issues
+ return nil, xerrors.Errorf("failed to fetch events [%d,%d): %w",
+ startSeq, endSeq, err)
+ }
+
+ // Record heartbeat after fetch completes
+ activity.RecordHeartbeat(ctx, fmt.Sprintf("Fetched %d events from range [%d, %d)",
+ len(events), startSeq, endSeq))
+
+ // Validate we got the expected number of events
+ expectedCount := int(totalSequences)
+ if len(events) != expectedCount {
+ return nil, xerrors.Errorf("missing events: expected %d events but got %d for range [%d,%d)",
+ expectedCount, len(events), startSeq, endSeq)
+ }
+
+ // Sort events by EventId to ensure proper ordering for gap validation
+ // DynamoDB Query should return sorted results, but we sort to be defensive
+ sort.Slice(events, func(i, j int) bool {
+ return events[i].EventId < events[j].EventId
+ })
+
+ // Validate event IDs are continuous (no gaps)
+ for i, event := range events {
+ expectedEventId := startSeq + int64(i)
+ if event.EventId != expectedEventId {
+ return nil, xerrors.Errorf("gap in event sequence: expected event ID %d but got %d at index %d",
+ expectedEventId, event.EventId, i)
+ }
+ }
+
+ logger.Info("Fetched events successfully",
+ zap.Int("totalEvents", len(events)))
+
+ return events, nil
+}
+
+func (a *Migrator) migrateExtractedBlocks(ctx context.Context, logger *zap.Logger,
+ data *MigrationData, request *MigratorRequest,
+ events []*model.EventEntry) (int, error) {
+
+ // Build segments directly from events
+ segments := a.buildSegmentsFromEvents(logger, events)
+
+ // If no segments were created (edge case), return
+ if len(segments) == 0 {
+ logger.Warn("No segments created from blocks")
+ return 0, nil
+ }
+
+ totalBlocksPersistedCount := 0
+
+ // Process each segment
+ for segmentIdx, segment := range segments {
+ if len(segment.Blocks) == 0 {
+ continue
+ }
+
+ logger.Info("Processing block segment",
+ zap.Int("segmentIndex", segmentIdx),
+ zap.Int("blockCount", len(segment.Blocks)))
+
+ // Fetch actual block data from DynamoDB for this segment (with order preservation)
+ segmentBlocksWithInfo, err := a.fetchBlockData(ctx, logger, data, request, segment.Blocks)
+ if err != nil {
+ return totalBlocksPersistedCount, xerrors.Errorf("failed to fetch block data for segment %d: %w", segmentIdx, err)
+ }
+
+ // Convert to BlockMetadata slice
+ segmentBlocks := make([]*api.BlockMetadata, len(segmentBlocksWithInfo))
+ for i, blockWithInfo := range segmentBlocksWithInfo {
+ segmentBlocks[i] = blockWithInfo.BlockMetadata
+ }
+
+ // Get parent block for validation if not genesis
+ var lastBlock *api.BlockMetadata
+ if len(segmentBlocks) > 0 && segmentBlocks[0].Height > 0 {
+ parentHeight := segmentBlocks[0].Height - 1
+ parentBlock, err := data.DestStorage.GetBlockByHeight(ctx, request.Tag, parentHeight)
+ if err == nil {
+ lastBlock = parentBlock
+ } else if !errors.Is(err, storage.ErrItemNotFound) {
+ logger.Warn("Could not fetch parent block",
+ zap.Uint64("parentHeight", parentHeight),
+ zap.Error(err))
+ }
+ }
+
+ // Bulk persist the segment with validation
+ err = data.DestStorage.PersistBlockMetas(ctx, false, segmentBlocks, lastBlock)
+ if err != nil {
+ logger.Error("Failed to persist segment",
+ zap.Int("segmentIndex", segmentIdx),
+ zap.Int("blockCount", len(segmentBlocks)),
+ zap.Error(err))
+ return totalBlocksPersistedCount, xerrors.Errorf("failed to persist segment %d: %w", segmentIdx, err)
+ }
+
+ totalBlocksPersistedCount += len(segmentBlocks)
+
+ // Record heartbeat if in activity context
+ if activity.IsActivity(ctx) {
+ activity.RecordHeartbeat(ctx, fmt.Sprintf("Migrated segment %d/%d: %d blocks (total: %d)",
+ segmentIdx+1, len(segments), len(segmentBlocks), totalBlocksPersistedCount))
+ }
+ }
+
+ logger.Info("All segments migrated successfully",
+ zap.Int("totalSegments", len(segments)),
+ zap.Int("totalBlocksPersisted", totalBlocksPersistedCount))
+
+ return totalBlocksPersistedCount, nil
+}
+
+// BlockSegment represents a continuous segment of blocks to persist together
+type BlockSegment struct {
+ Blocks []BlockToMigrate
+}
+
+// buildSegmentsFromEvents creates block segments from events, starting new segments after BLOCK_REMOVED events
+func (a *Migrator) buildSegmentsFromEvents(logger *zap.Logger,
+ events []*model.EventEntry) []*BlockSegment {
+
+ var segments []*BlockSegment
+ var currentSegment *BlockSegment = nil
+
+ // Process events in order (already sorted by EventSeq)
+ for _, event := range events {
+ switch event.EventType {
+ case api.BlockchainEvent_BLOCK_REMOVED:
+ // End current segment
+ currentSegment = nil
+
+ case api.BlockchainEvent_BLOCK_ADDED:
+ if currentSegment == nil {
+ // Create new segment
+ currentSegment = &BlockSegment{
+ Blocks: []BlockToMigrate{},
+ }
+ segments = append(segments, currentSegment)
+ }
+ // Create block directly from event and append to current segment
+ currentSegment.Blocks = append(currentSegment.Blocks, BlockToMigrate{
+ Height: event.BlockHeight,
+ Hash: event.BlockHash,
+ ParentHash: event.ParentHash,
+ EventSeq: event.EventId,
+ Skipped: event.BlockSkipped,
+ })
+ }
+ }
+
+ // Log segments for debugging
+ for i, segment := range segments {
+ if len(segment.Blocks) > 0 {
+ logger.Debug("Block segment",
+ zap.Int("index", i),
+ zap.Int("blocks", len(segment.Blocks)),
+ zap.Uint64("firstHeight", segment.Blocks[0].Height),
+ zap.Uint64("lastHeight", segment.Blocks[len(segment.Blocks)-1].Height))
+ }
+ }
+
+ return segments
+}
+
+type BlockWithInfo struct {
+ *api.BlockMetadata
+ Height uint64
+ Hash string
+ EventSeq int64
+}
+
+// WorkItem for parallel fetching with index preservation
+type WorkItem struct {
+ Block BlockToMigrate
+ Index int
+}
+
+func (a *Migrator) fetchBlockData(ctx context.Context, logger *zap.Logger,
+ data *MigrationData, request *MigratorRequest,
+ blocksToMigrate []BlockToMigrate) ([]*BlockWithInfo, error) {
+
+ if len(blocksToMigrate) == 0 {
+ return nil, nil
+ }
+
+ // Determine parallelism
+ parallelism := request.Parallelism
+ if parallelism <= 0 {
+ parallelism = 1
+ }
+ if parallelism > len(blocksToMigrate) {
+ parallelism = 1
+ }
+
+ logger.Debug("Fetching block data in parallel",
+ zap.Int("totalBlocks", len(blocksToMigrate)),
+ zap.Int("parallelism", parallelism))
+
+ // Pre-allocate result array
+ results := make([]*BlockWithInfo, len(blocksToMigrate))
+
+ // Channel for work items (index, block)
+ workChan := make(chan WorkItem, len(blocksToMigrate))
+
+ // Send work items with their index
+ for i, block := range blocksToMigrate {
+ workChan <- WorkItem{Block: block, Index: i}
+ }
+ close(workChan)
+
+ // Worker function - fetches block and writes directly to results[index]
+ var wg sync.WaitGroup
+ for i := 0; i < parallelism; i++ {
+ wg.Add(1)
+ go func() {
+ defer wg.Done()
+ for item := range workChan {
+ var blockMeta *api.BlockMetadata
+
+ // For skipped blocks, create metadata directly
+ if item.Block.Skipped {
+ blockMeta = &api.BlockMetadata{
+ Tag: request.Tag,
+ Height: item.Block.Height,
+ Skipped: true,
+ Hash: "",
+ ParentHash: "",
+ ParentHeight: 0,
+ Timestamp: nil,
+ }
+ } else {
+ // For regular blocks, fetch from source storage
+ var err error
+ blockMeta, err = data.SourceStorage.GetBlockByHash(ctx, request.Tag, item.Block.Height, item.Block.Hash)
+ if err != nil {
+ logger.Warn("Failed to fetch block by hash",
+ zap.Uint64("height", item.Block.Height),
+ zap.String("hash", item.Block.Hash),
+ zap.Error(err))
+ continue // Skip this block
+ }
+ }
+
+ // Write directly to the correct index
+ results[item.Index] = &BlockWithInfo{
+ BlockMetadata: blockMeta,
+ Height: item.Block.Height,
+ Hash: item.Block.Hash,
+ EventSeq: item.Block.EventSeq,
+ }
+ }
+ }()
+ }
+
+ // Wait for all workers to complete
+ wg.Wait()
+
+ // Filter out nils (from failed fetches) and count skipped
+ var validResults []*BlockWithInfo
+ skippedCount := 0
+ for _, blockInfo := range results {
+ if blockInfo != nil {
+ validResults = append(validResults, blockInfo)
+ if blockInfo.BlockMetadata != nil && blockInfo.BlockMetadata.Skipped {
+ skippedCount++
+ }
+ }
+ }
+
+ logger.Debug("Completed parallel block data fetch",
+ zap.Int("requested", len(blocksToMigrate)),
+ zap.Int("fetched", len(validResults)),
+ zap.Int("skippedBlocks", skippedCount))
+
+ return validResults, nil
+}
+
+func (a *Migrator) persistEvents(ctx context.Context, logger *zap.Logger,
+ data *MigrationData, request *MigratorRequest,
+ events []*model.EventEntry) (int, error) {
+
+ if len(events) == 0 {
+ return 0, nil
+ }
+
+ // Events are already sorted by EventId from fetchEventsBySequence
+ firstId := events[0].EventId
+ lastId := events[len(events)-1].EventId
+
+ logger.Info("Persisting events batch",
+ zap.Int("totalEvents", len(events)),
+ zap.Int64("firstEventId", firstId),
+ zap.Int64("lastEventId", lastId))
+
+ // Check for gaps in event sequences (for debugging)
+ var gaps []string
+ for i := 1; i < len(events); i++ {
+ if events[i].EventId != events[i-1].EventId+1 {
+ gap := fmt.Sprintf("position=%d: %d->%d (gap=%d)",
+ i, events[i-1].EventId, events[i].EventId,
+ events[i].EventId-events[i-1].EventId-1)
+ gaps = append(gaps, gap)
+ }
+ }
+ if len(gaps) > 0 {
+ logger.Warn("Found gaps in event sequences",
+ zap.Strings("gaps", gaps))
+ }
+
+ // Persist all events in a single call
+ persistStart := time.Now()
+ if err := data.DestStorage.AddEventEntries(ctx, request.EventTag, events); err != nil {
+ return 0, xerrors.Errorf("failed to persist events: %w", err)
+ }
+
+ persistDuration := time.Since(persistStart)
+ logger.Info("Events persisted successfully",
+ zap.Int("eventCount", len(events)),
+ zap.Duration("persistDuration", persistDuration),
+ zap.Float64("eventsPerSecond", float64(len(events))/persistDuration.Seconds()))
+
+ return len(events), nil
+}
+
+type GetLatestBlockHeightRequest struct {
+ Tag uint32
+}
+
+type GetLatestBlockHeightResponse struct {
+ Height uint64
+}
+
+type GetLatestBlockFromPostgresRequest struct {
+ Tag uint32
+}
+
+type GetLatestBlockFromPostgresResponse struct {
+ Height uint64
+ Found bool // true if a block was found, false if no blocks exist yet
+}
+
+type GetLatestEventFromPostgresRequest struct {
+ EventTag uint32
+}
+
+type GetLatestEventFromPostgresResponse struct {
+ Sequence int64 // Event sequence number
+ Height uint64 // Block height (for backward compatibility)
+ Found bool // true if events were found, false if no events exist yet
+}
+
+type GetMaxEventIdRequest struct {
+ EventTag uint32
+}
+
+type GetMaxEventIdResponse struct {
+ MaxEventId int64 // Maximum event ID in DynamoDB
+ Found bool // true if events were found
+}
+
+func (a *Migrator) GetLatestBlockHeight(ctx context.Context, req *GetLatestBlockHeightRequest) (*GetLatestBlockHeightResponse, error) {
+ migrationData, err := a.createStorageInstances(ctx)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create storage instances: %w", err)
+ }
+ latestBlock, err := migrationData.SourceStorage.GetLatestBlock(ctx, req.Tag)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get latest block from DynamoDB: %w", err)
+ }
+ return &GetLatestBlockHeightResponse{Height: latestBlock.Height}, nil
+}
+
+func (a *GetLatestBlockHeightActivity) Execute(ctx workflow.Context, request *GetLatestBlockHeightRequest) (*GetLatestBlockHeightResponse, error) {
+ var response GetLatestBlockHeightResponse
+ err := a.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *GetLatestBlockHeightActivity) execute(ctx context.Context, request *GetLatestBlockHeightRequest) (*GetLatestBlockHeightResponse, error) {
+ migrationData, err := a.createStorageInstances(ctx)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create storage instances: %w", err)
+ }
+ latestBlock, err := migrationData.SourceStorage.GetLatestBlock(ctx, request.Tag)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get latest block from DynamoDB: %w", err)
+ }
+ return &GetLatestBlockHeightResponse{Height: latestBlock.Height}, nil
+}
+
+func (a *GetLatestBlockFromPostgresActivity) Execute(ctx workflow.Context, request *GetLatestBlockFromPostgresRequest) (*GetLatestBlockFromPostgresResponse, error) {
+ var response GetLatestBlockFromPostgresResponse
+ err := a.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *GetLatestBlockFromPostgresActivity) execute(ctx context.Context, request *GetLatestBlockFromPostgresRequest) (*GetLatestBlockFromPostgresResponse, error) {
+ if err := a.validateRequest(request); err != nil {
+ return nil, err
+ }
+
+ logger := a.getLogger(ctx).With(zap.Reflect("request", request))
+
+ // Create PostgreSQL storage using shared connection pool to query destination
+ postgresParams := postgres_storage.Params{
+ Params: fxparams.Params{
+ Config: a.config,
+ Logger: logger,
+ Metrics: a.metrics,
+ },
+ }
+ destResult, err := postgres_storage.NewMetaStorage(postgresParams)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create PostgreSQL storage: %w", err)
+ }
+
+ latestBlock, err := destResult.MetaStorage.GetLatestBlock(ctx, request.Tag)
+ if err != nil {
+ // Check if it's a "not found" error, which means no blocks migrated yet
+ errStr := strings.ToLower(err.Error())
+ if strings.Contains(errStr, "not found") || strings.Contains(errStr, "no rows") {
+ logger.Info("No blocks found in PostgreSQL destination - starting from beginning")
+ return &GetLatestBlockFromPostgresResponse{
+ Height: 0,
+ Found: false,
+ }, nil
+ }
+ return nil, xerrors.Errorf("failed to get latest block from PostgreSQL: %w", err)
+ }
+
+ logger.Info("Found latest block in PostgreSQL destination", zap.Uint64("height", latestBlock.Height))
+ return &GetLatestBlockFromPostgresResponse{
+ Height: latestBlock.Height,
+ Found: true,
+ }, nil
+}
+
+func (a *GetLatestEventFromPostgresActivity) Execute(ctx workflow.Context, request *GetLatestEventFromPostgresRequest) (*GetLatestEventFromPostgresResponse, error) {
+ var response GetLatestEventFromPostgresResponse
+ err := a.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *GetLatestEventFromPostgresActivity) execute(ctx context.Context, request *GetLatestEventFromPostgresRequest) (*GetLatestEventFromPostgresResponse, error) {
+ if err := a.validateRequest(request); err != nil {
+ return nil, err
+ }
+
+ logger := a.getLogger(ctx).With(zap.Reflect("request", request))
+
+ // Create PostgreSQL storage using shared connection pool to query destination
+ postgresParams := postgres_storage.Params{
+ Params: fxparams.Params{
+ Config: a.config,
+ Logger: logger,
+ Metrics: a.metrics,
+ },
+ }
+
+ destResult, err := postgres_storage.NewMetaStorage(postgresParams)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create PostgreSQL storage: %w", err)
+ }
+
+ // Get the latest event sequence from PostgreSQL
+ maxEventId, err := destResult.MetaStorage.GetMaxEventId(ctx, request.EventTag)
+ if err != nil {
+ // Check if it's a "no event history" error, which means no events migrated yet
+ errStr := strings.ToLower(err.Error())
+ if strings.Contains(errStr, "no event history") || strings.Contains(errStr, "not found") || strings.Contains(errStr, "no rows") {
+ logger.Info("No events found in PostgreSQL destination - starting from beginning")
+ return &GetLatestEventFromPostgresResponse{
+ Sequence: 0,
+ Height: 0,
+ Found: false,
+ }, nil
+ }
+ return nil, xerrors.Errorf("failed to get latest event sequence from PostgreSQL: %w", err)
+ }
+
+ // Get the event entry to also return the height for backward compatibility
+ eventEntry, err := destResult.MetaStorage.GetEventByEventId(ctx, request.EventTag, maxEventId)
+ if err != nil {
+ // If we can't get the event entry, just return the sequence
+ logger.Warn("Failed to get event entry for max sequence, returning sequence only",
+ zap.Int64("maxEventId", maxEventId), zap.Error(err))
+ return &GetLatestEventFromPostgresResponse{
+ Sequence: maxEventId,
+ Height: 0,
+ Found: true,
+ }, nil
+ }
+
+ logger.Info("Found latest event in PostgreSQL destination",
+ zap.Int64("sequence", maxEventId),
+ zap.Uint64("height", eventEntry.BlockHeight))
+ return &GetLatestEventFromPostgresResponse{
+ Sequence: maxEventId,
+ Height: eventEntry.BlockHeight,
+ Found: true,
+ }, nil
+}
+
+func (a *GetMaxEventIdActivity) Execute(ctx workflow.Context, request *GetMaxEventIdRequest) (*GetMaxEventIdResponse, error) {
+ var response GetMaxEventIdResponse
+ err := a.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *GetMaxEventIdActivity) execute(ctx context.Context, request *GetMaxEventIdRequest) (*GetMaxEventIdResponse, error) {
+ logger := a.getLogger(ctx).With(
+ zap.Uint32("eventTag", request.EventTag),
+ )
+ logger.Info("getting max event ID from DynamoDB")
+
+ // Create DynamoDB storage directly
+ dynamoDBParams := dynamodb_storage.Params{
+ Params: fxparams.Params{
+ Config: a.config,
+ Logger: logger,
+ Metrics: a.metrics,
+ },
+ AWSConfig: a.awsConfig,
+ }
+ sourceResult, err := dynamodb_storage.NewMetaStorage(dynamoDBParams)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create DynamoDB storage: %w", err)
+ }
+
+ // Get max event ID from DynamoDB
+ maxEventId, err := sourceResult.MetaStorage.GetMaxEventId(ctx, request.EventTag)
+ if err != nil {
+ if errors.Is(err, storage.ErrItemNotFound) {
+ logger.Warn("no events found in DynamoDB")
+ return &GetMaxEventIdResponse{
+ MaxEventId: 0,
+ Found: false,
+ }, nil
+ }
+ return nil, xerrors.Errorf("failed to get max event ID: %w", err)
+ }
+
+ logger.Info("found max event ID in DynamoDB",
+ zap.Int64("maxEventId", maxEventId))
+
+ return &GetMaxEventIdResponse{
+ MaxEventId: maxEventId,
+ Found: true,
+ }, nil
+}
+
+func (a *GetLatestBlockHeightActivity) createStorageInstances(ctx context.Context) (*MigrationData, error) {
+ logger := a.getLogger(ctx)
+
+ // Create DynamoDB storage directly
+ dynamoDBParams := dynamodb_storage.Params{
+ Params: fxparams.Params{
+ Config: a.config,
+ Logger: logger,
+ Metrics: a.metrics,
+ },
+ AWSConfig: a.awsConfig,
+ }
+ sourceResult, err := dynamodb_storage.NewMetaStorage(dynamoDBParams)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create DynamoDB storage: %w", err)
+ }
+
+ // Create PostgreSQL storage using shared connection pool
+ postgresParams := postgres_storage.Params{
+ Params: fxparams.Params{
+ Config: a.config,
+ Logger: logger,
+ Metrics: a.metrics,
+ },
+ }
+ destResult, err := postgres_storage.NewMetaStorage(postgresParams)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to create PostgreSQL storage: %w", err)
+ }
+
+ return &MigrationData{
+ SourceStorage: sourceResult.MetaStorage,
+ DestStorage: destResult.MetaStorage,
+ Config: a.config,
+ DynamoClient: a.dynamoClient,
+ BlockTable: a.blockTable,
+ }, nil
+}
+
+const (
+ ActivityMigrator = "activity.migrator"
+ ActivityGetLatestBlockHeight = "activity.migrator.GetLatestBlockHeight"
+ ActivityGetLatestBlockFromPostgres = "activity.migrator.GetLatestBlockFromPostgres"
+ ActivityGetLatestEventFromPostgres = "activity.migrator.GetLatestEventFromPostgres"
+ ActivityGetMaxEventId = "activity.migrator.GetMaxEventId"
+)
diff --git a/internal/workflow/activity/migrator_integration_test.go b/internal/workflow/activity/migrator_integration_test.go
new file mode 100644
index 00000000..468de8d7
--- /dev/null
+++ b/internal/workflow/activity/migrator_integration_test.go
@@ -0,0 +1,464 @@
+package activity
+
+import (
+ "context"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/mock/gomock"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ metastoragemocks "github.com/coinbase/chainstorage/internal/storage/metastorage/mocks"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/model"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type migratorIntegrationTestSuite struct {
+ suite.Suite
+ ctrl *gomock.Controller
+ sourceStorage *metastoragemocks.MockMetaStorage
+ destStorage *metastoragemocks.MockMetaStorage
+ logger *zap.Logger
+}
+
+func TestMigratorIntegrationTestSuite(t *testing.T) {
+ suite.Run(t, new(migratorIntegrationTestSuite))
+}
+
+func (s *migratorIntegrationTestSuite) SetupTest() {
+ s.ctrl = gomock.NewController(s.T())
+ s.sourceStorage = metastoragemocks.NewMockMetaStorage(s.ctrl)
+ s.destStorage = metastoragemocks.NewMockMetaStorage(s.ctrl)
+ s.logger, _ = zap.NewDevelopment()
+}
+
+func (s *migratorIntegrationTestSuite) TearDownTest() {
+ s.ctrl.Finish()
+}
+
+func (s *migratorIntegrationTestSuite) TestMigrateExtractedBlocks_NoReorg_ValidationPasses() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+
+ // Setup test data - no reorgs
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0x100"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0x101"},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0x102"},
+ }
+
+ // Events contain all the block information needed
+
+ // Mock block data fetching from source
+ block100 := &api.BlockMetadata{
+ Tag: 1, Height: 100, Hash: "0x100", ParentHash: "0x99", ParentHeight: 99,
+ }
+ block101 := &api.BlockMetadata{
+ Tag: 1, Height: 101, Hash: "0x101", ParentHash: "0x100", ParentHeight: 100,
+ }
+ block102 := &api.BlockMetadata{
+ Tag: 1, Height: 102, Hash: "0x102", ParentHash: "0x101", ParentHeight: 101,
+ }
+
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(100), "0x100").
+ Return(block100, nil)
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(101), "0x101").
+ Return(block101, nil)
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(102), "0x102").
+ Return(block102, nil)
+
+ // For validation, fetch parent block (99) from destination
+ parentBlock := &api.BlockMetadata{
+ Tag: 1, Height: 99, Hash: "0x99", ParentHash: "0x98",
+ }
+ s.destStorage.EXPECT().
+ GetBlockByHeight(ctx, uint32(1), uint64(99)).
+ Return(parentBlock, nil)
+
+ // Expect bulk persist with validation - THIS IS THE KEY TEST
+ // The blocks should be persisted with the parent block for validation
+ s.destStorage.EXPECT().
+ PersistBlockMetas(ctx, false,
+ []*api.BlockMetadata{block100, block101, block102},
+ parentBlock).
+ Return(nil)
+
+ // Create migrator with mocked storage
+ migrator := &Migrator{}
+ data := &MigrationData{
+ SourceStorage: s.sourceStorage,
+ DestStorage: s.destStorage,
+ }
+ request := &MigratorRequest{
+ Tag: 1,
+ Parallelism: 1,
+ }
+
+ // Execute
+ count, err := migrator.migrateExtractedBlocks(ctx, s.logger, data, request, events)
+
+ // Verify
+ require.NoError(err)
+ require.Equal(3, count)
+}
+
+func (s *migratorIntegrationTestSuite) TestMigrateExtractedBlocks_SingleReorg_CorrectParentValidation() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+
+ // Setup test data - reorg at height 102
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0x100"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0x101"},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0x102a"},
+ {EventId: 4, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 102, BlockHash: "0x102a"},
+ {EventId: 5, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0x102b"},
+ {EventId: 6, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 103, BlockHash: "0x103"},
+ }
+
+ // Events contain all the block information needed
+
+ // Mock block data
+ block100 := &api.BlockMetadata{
+ Tag: 1, Height: 100, Hash: "0x100", ParentHash: "0x99", ParentHeight: 99,
+ }
+ block101 := &api.BlockMetadata{
+ Tag: 1, Height: 101, Hash: "0x101", ParentHash: "0x100", ParentHeight: 100,
+ }
+ block102a := &api.BlockMetadata{
+ Tag: 1, Height: 102, Hash: "0x102a", ParentHash: "0x101", ParentHeight: 101,
+ }
+ block102b := &api.BlockMetadata{
+ Tag: 1, Height: 102, Hash: "0x102b", ParentHash: "0x101", ParentHeight: 101,
+ }
+ block103 := &api.BlockMetadata{
+ Tag: 1, Height: 103, Hash: "0x103", ParentHash: "0x102b", ParentHeight: 102,
+ }
+
+ // Mock fetching blocks from source
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(100), "0x100").
+ Return(block100, nil)
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(101), "0x101").
+ Return(block101, nil)
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(102), "0x102a").
+ Return(block102a, nil)
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(102), "0x102b").
+ Return(block102b, nil)
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(103), "0x103").
+ Return(block103, nil)
+
+ // CRITICAL TEST: Two segments with different parent validations
+
+ // Segment 1: blocks 100, 101, 102a - validate against parent height 99
+ parentBlock99 := &api.BlockMetadata{
+ Tag: 1, Height: 99, Hash: "0x99", ParentHash: "0x98",
+ }
+ s.destStorage.EXPECT().
+ GetBlockByHeight(ctx, uint32(1), uint64(99)).
+ Return(parentBlock99, nil)
+
+ s.destStorage.EXPECT().
+ PersistBlockMetas(ctx, false,
+ []*api.BlockMetadata{block100, block101, block102a},
+ parentBlock99).
+ Return(nil)
+
+ // Segment 2: blocks 102b, 103 - validate against parent height 101 (reorg point)
+ s.destStorage.EXPECT().
+ GetBlockByHeight(ctx, uint32(1), uint64(101)).
+ Return(block101, nil)
+
+ s.destStorage.EXPECT().
+ PersistBlockMetas(ctx, false,
+ []*api.BlockMetadata{block102b, block103},
+ block101).
+ Return(nil)
+
+ // Create migrator
+ migrator := &Migrator{}
+ data := &MigrationData{
+ SourceStorage: s.sourceStorage,
+ DestStorage: s.destStorage,
+ }
+ request := &MigratorRequest{
+ Tag: 1,
+ Parallelism: 1,
+ }
+
+ // Execute
+ count, err := migrator.migrateExtractedBlocks(ctx, s.logger, data, request, events)
+
+ // Verify
+ require.NoError(err)
+ require.Equal(5, count) // Total blocks persisted
+}
+
+func (s *migratorIntegrationTestSuite) TestMigrateExtractedBlocks_ValidationFailure() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+
+ // Setup test data - blocks with broken chain
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0x100"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0x101"},
+ }
+
+ // Events contain all the block information needed
+
+ block100 := &api.BlockMetadata{
+ Tag: 1, Height: 100, Hash: "0x100", ParentHash: "0x99", ParentHeight: 99,
+ }
+ block101 := &api.BlockMetadata{
+ Tag: 1, Height: 101, Hash: "0x101", ParentHash: "0xWRONG", ParentHeight: 100, // Wrong parent
+ }
+
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(100), "0x100").
+ Return(block100, nil)
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(101), "0x101").
+ Return(block101, nil)
+
+ // Get parent for validation
+ parentBlock := &api.BlockMetadata{
+ Tag: 1, Height: 99, Hash: "0x99", ParentHash: "0x98",
+ }
+ s.destStorage.EXPECT().
+ GetBlockByHeight(ctx, uint32(1), uint64(99)).
+ Return(parentBlock, nil)
+
+ // Expect PersistBlockMetas to fail due to validation error
+ s.destStorage.EXPECT().
+ PersistBlockMetas(ctx, false,
+ []*api.BlockMetadata{block100, block101},
+ parentBlock).
+ Return(xerrors.Errorf("chain is not continuous"))
+
+ // Create migrator
+ migrator := &Migrator{}
+ data := &MigrationData{
+ SourceStorage: s.sourceStorage,
+ DestStorage: s.destStorage,
+ }
+ request := &MigratorRequest{
+ Tag: 1,
+ Parallelism: 1,
+ }
+
+ // Execute - should fail
+ count, err := migrator.migrateExtractedBlocks(ctx, s.logger, data, request, events)
+
+ // Verify failure
+ require.Error(err)
+ require.Contains(err.Error(), "chain is not continuous")
+ require.Equal(0, count)
+}
+
+func (s *migratorIntegrationTestSuite) TestMigrateExtractedBlocks_ComplexReorg_MultipleSegments() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+
+ // Complex scenario: multiple reorgs
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0x100", ParentHash: "0x99"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0x101a", ParentHash: "0x100"},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 101, BlockHash: "0x101a"},
+ {EventId: 4, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0x101b", ParentHash: "0x100"},
+ {EventId: 5, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0x102a", ParentHash: "0x101b"},
+ {EventId: 6, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 102, BlockHash: "0x102a"},
+ {EventId: 7, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 101, BlockHash: "0x101b"},
+ {EventId: 8, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0x101c", ParentHash: "0x100"},
+ {EventId: 9, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0x102b", ParentHash: "0x101c"},
+ }
+
+ // Events contain all the block information needed
+
+ // Mock blocks
+ block100 := &api.BlockMetadata{Tag: 1, Height: 100, Hash: "0x100", ParentHash: "0x99", ParentHeight: 99}
+ block101a := &api.BlockMetadata{Tag: 1, Height: 101, Hash: "0x101a", ParentHash: "0x100", ParentHeight: 100}
+ block101b := &api.BlockMetadata{Tag: 1, Height: 101, Hash: "0x101b", ParentHash: "0x100", ParentHeight: 100}
+ block102a := &api.BlockMetadata{Tag: 1, Height: 102, Hash: "0x102a", ParentHash: "0x101b", ParentHeight: 101}
+ block101c := &api.BlockMetadata{Tag: 1, Height: 101, Hash: "0x101c", ParentHash: "0x100", ParentHeight: 100}
+ block102b := &api.BlockMetadata{Tag: 1, Height: 102, Hash: "0x102b", ParentHash: "0x101c", ParentHeight: 101}
+
+ // Mock fetching
+ s.sourceStorage.EXPECT().GetBlockByHash(ctx, uint32(1), uint64(100), "0x100").Return(block100, nil)
+ s.sourceStorage.EXPECT().GetBlockByHash(ctx, uint32(1), uint64(101), "0x101a").Return(block101a, nil)
+ s.sourceStorage.EXPECT().GetBlockByHash(ctx, uint32(1), uint64(101), "0x101b").Return(block101b, nil)
+ s.sourceStorage.EXPECT().GetBlockByHash(ctx, uint32(1), uint64(102), "0x102a").Return(block102a, nil)
+ s.sourceStorage.EXPECT().GetBlockByHash(ctx, uint32(1), uint64(101), "0x101c").Return(block101c, nil)
+ s.sourceStorage.EXPECT().GetBlockByHash(ctx, uint32(1), uint64(102), "0x102b").Return(block102b, nil)
+
+ // Three segments expected:
+ // Segment 1: [100, 101a] - parent 99
+ parentBlock99 := &api.BlockMetadata{Tag: 1, Height: 99, Hash: "0x99"}
+ s.destStorage.EXPECT().GetBlockByHeight(ctx, uint32(1), uint64(99)).Return(parentBlock99, nil)
+ s.destStorage.EXPECT().
+ PersistBlockMetas(ctx, false, []*api.BlockMetadata{block100, block101a}, parentBlock99).
+ Return(nil)
+
+ // Segment 2: [101b, 102a] - parent 100 (reorg at 101)
+ s.destStorage.EXPECT().GetBlockByHeight(ctx, uint32(1), uint64(100)).Return(block100, nil)
+ s.destStorage.EXPECT().
+ PersistBlockMetas(ctx, false, []*api.BlockMetadata{block101b, block102a}, block100).
+ Return(nil)
+
+ // Segment 3: [101c, 102b] - parent 100 (second reorg at 101)
+ s.destStorage.EXPECT().GetBlockByHeight(ctx, uint32(1), uint64(100)).Return(block100, nil).Times(1)
+ s.destStorage.EXPECT().
+ PersistBlockMetas(ctx, false, []*api.BlockMetadata{block101c, block102b}, block100).
+ Return(nil)
+
+ // Create migrator
+ migrator := &Migrator{}
+ data := &MigrationData{
+ SourceStorage: s.sourceStorage,
+ DestStorage: s.destStorage,
+ }
+ request := &MigratorRequest{
+ Tag: 1,
+ Parallelism: 1,
+ }
+
+ // Execute
+ count, err := migrator.migrateExtractedBlocks(ctx, s.logger, data, request, events)
+
+ // Verify
+ require.NoError(err)
+ require.Equal(6, count)
+}
+
+func (s *migratorIntegrationTestSuite) TestMigrateExtractedBlocks_SkippedBlocks() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+
+ // Test with skipped blocks
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0x100", ParentHash: "0x99", BlockSkipped: false},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "", ParentHash: "", BlockSkipped: true}, // Skipped
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0x102", ParentHash: "0x100", BlockSkipped: false},
+ }
+
+ // Events contain all the block information needed
+
+ // Regular blocks
+ block100 := &api.BlockMetadata{
+ Tag: 1, Height: 100, Hash: "0x100", ParentHash: "0x99", ParentHeight: 99,
+ }
+ block102 := &api.BlockMetadata{
+ Tag: 1, Height: 102, Hash: "0x102", ParentHash: "0x100", ParentHeight: 100,
+ }
+
+ // Skipped blocks shouldn't be fetched from source
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(100), "0x100").
+ Return(block100, nil)
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(102), "0x102").
+ Return(block102, nil)
+ // NO expectation for block 101 since it's skipped
+
+ // Get parent for validation
+ parentBlock := &api.BlockMetadata{
+ Tag: 1, Height: 99, Hash: "0x99",
+ }
+ s.destStorage.EXPECT().
+ GetBlockByHeight(ctx, uint32(1), uint64(99)).
+ Return(parentBlock, nil)
+
+ // Expect persistence including the skipped block (created inline)
+ s.destStorage.EXPECT().
+ PersistBlockMetas(ctx, false,
+ gomock.Any(), // We'll validate this in a custom matcher
+ parentBlock).
+ DoAndReturn(func(ctx context.Context, updateWatermark bool, blocks []*api.BlockMetadata, lastBlock *api.BlockMetadata) error {
+ // Verify the skipped block was created correctly
+ require.Len(blocks, 3)
+ require.Equal(uint64(101), blocks[1].Height)
+ require.True(blocks[1].Skipped)
+ require.Empty(blocks[1].Hash)
+ require.Empty(blocks[1].ParentHash)
+ return nil
+ })
+
+ // Create migrator
+ migrator := &Migrator{}
+ data := &MigrationData{
+ SourceStorage: s.sourceStorage,
+ DestStorage: s.destStorage,
+ }
+ request := &MigratorRequest{
+ Tag: 1,
+ Parallelism: 1,
+ }
+
+ // Execute
+ count, err := migrator.migrateExtractedBlocks(ctx, s.logger, data, request, events)
+
+ // Verify
+ require.NoError(err)
+ require.Equal(3, count)
+}
+
+func (s *migratorIntegrationTestSuite) TestMigrateExtractedBlocks_ParentNotFound_ContinuesWithoutValidation() {
+ require := testutil.Require(s.T())
+ ctx := context.Background()
+
+ // Genesis or missing parent scenario
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 0, BlockHash: "0x0"}, // Genesis
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 1, BlockHash: "0x1"},
+ }
+
+ // Events contain all the block information needed
+
+ block0 := &api.BlockMetadata{
+ Tag: 1, Height: 0, Hash: "0x0", ParentHash: "", ParentHeight: 0,
+ }
+ block1 := &api.BlockMetadata{
+ Tag: 1, Height: 1, Hash: "0x1", ParentHash: "0x0", ParentHeight: 0,
+ }
+
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(0), "0x0").
+ Return(block0, nil)
+ s.sourceStorage.EXPECT().
+ GetBlockByHash(ctx, uint32(1), uint64(1), "0x1").
+ Return(block1, nil)
+
+ // ParentHeight is 0, so no parent fetch attempted
+ // Persist without validation (lastBlock = nil)
+ s.destStorage.EXPECT().
+ PersistBlockMetas(ctx, false,
+ []*api.BlockMetadata{block0, block1},
+ nil). // No parent validation for genesis
+ Return(nil)
+
+ // Create migrator
+ migrator := &Migrator{}
+ data := &MigrationData{
+ SourceStorage: s.sourceStorage,
+ DestStorage: s.destStorage,
+ }
+ request := &MigratorRequest{
+ Tag: 1,
+ Parallelism: 1,
+ }
+
+ // Execute
+ count, err := migrator.migrateExtractedBlocks(ctx, s.logger, data, request, events)
+
+ // Verify
+ require.NoError(err)
+ require.Equal(2, count)
+}
diff --git a/internal/workflow/activity/migrator_test.go b/internal/workflow/activity/migrator_test.go
new file mode 100644
index 00000000..f199bf5b
--- /dev/null
+++ b/internal/workflow/activity/migrator_test.go
@@ -0,0 +1,418 @@
+package activity
+
+import (
+ "testing"
+
+ "github.com/golang/mock/gomock"
+ "github.com/stretchr/testify/suite"
+ "go.temporal.io/sdk/testsuite"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/model"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type migratorActivityTestSuite struct {
+ suite.Suite
+ testsuite.WorkflowTestSuite
+ ctrl *gomock.Controller
+ app testapp.TestApp
+ logger *zap.Logger
+ migrator *Migrator
+ getLatestHeight *GetLatestBlockHeightActivity
+ getLatestFromPostgres *GetLatestBlockFromPostgresActivity
+ getLatestEvent *GetLatestEventFromPostgresActivity
+ getMaxEventId *GetMaxEventIdActivity
+ env *cadence.TestEnv
+ cfg *config.Config
+}
+
+func TestMigratorActivityTestSuite(t *testing.T) {
+ suite.Run(t, new(migratorActivityTestSuite))
+}
+
+func (s *migratorActivityTestSuite) SetupTest() {
+ require := testutil.Require(s.T())
+
+ s.env = cadence.NewTestActivityEnv(s)
+ s.ctrl = gomock.NewController(s.T())
+
+ cfg, err := config.New(
+ config.WithBlockchain(common.Blockchain_BLOCKCHAIN_ETHEREUM),
+ config.WithNetwork(common.Network_NETWORK_ETHEREUM_MAINNET),
+ config.WithEnvironment(config.EnvLocal),
+ )
+ require.NoError(err)
+ s.cfg = cfg
+
+ var deps struct {
+ fx.In
+ Migrator *Migrator
+ GetLatestHeight *GetLatestBlockHeightActivity
+ GetLatestFromPostgres *GetLatestBlockFromPostgresActivity
+ GetLatestEventFromPostgres *GetLatestEventFromPostgresActivity
+ GetMaxEventId *GetMaxEventIdActivity
+ }
+
+ s.app = testapp.New(
+ s.T(),
+ Module,
+ cadence.WithTestEnv(s.env),
+ testapp.WithConfig(cfg),
+ fx.Populate(&deps),
+ )
+
+ s.migrator = deps.Migrator
+ s.getLatestHeight = deps.GetLatestHeight
+ s.getLatestFromPostgres = deps.GetLatestFromPostgres
+ s.getLatestEvent = deps.GetLatestEventFromPostgres
+ s.getMaxEventId = deps.GetMaxEventId
+ s.logger = s.app.Logger()
+
+}
+
+func (s *migratorActivityTestSuite) TearDownTest() {
+ s.app.Close()
+ s.ctrl.Finish()
+ s.env.AssertExpectations(s.T())
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_NoReorgs() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Test case: No reorgs - single segment
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0xa", ParentHash: "0x99"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0xb", ParentHash: "0xa"},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0xc", ParentHash: "0xb"},
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Len(segments, 1, "Should have 1 segment when no reorgs")
+ require.Len(segments[0].Blocks, 3)
+
+ // Verify blocks are in correct order
+ require.Equal(uint64(100), segments[0].Blocks[0].Height)
+ require.Equal("0xa", segments[0].Blocks[0].Hash)
+ require.Equal(uint64(101), segments[0].Blocks[1].Height)
+ require.Equal("0xb", segments[0].Blocks[1].Hash)
+ require.Equal(uint64(102), segments[0].Blocks[2].Height)
+ require.Equal("0xc", segments[0].Blocks[2].Hash)
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_SingleReorg() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Test case: Single reorg in middle
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0xa", ParentHash: "0x99"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0xb", ParentHash: "0xa"},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0xc1", ParentHash: "0xb"},
+ {EventId: 4, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 102, BlockHash: "0xc1"},
+ {EventId: 5, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0xc2", ParentHash: "0xb"},
+ {EventId: 6, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 103, BlockHash: "0xd", ParentHash: "0xc2"},
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Len(segments, 2, "Should have 2 segments with 1 reorg")
+
+ // First segment: blocks before reorg
+ require.Len(segments[0].Blocks, 3)
+ require.Equal(uint64(100), segments[0].Blocks[0].Height)
+ require.Equal(uint64(101), segments[0].Blocks[1].Height)
+ require.Equal(uint64(102), segments[0].Blocks[2].Height)
+ require.Equal("0xc1", segments[0].Blocks[2].Hash)
+
+ // Second segment: blocks after reorg
+ require.Len(segments[1].Blocks, 2)
+ require.Equal(uint64(102), segments[1].Blocks[0].Height)
+ require.Equal("0xc2", segments[1].Blocks[0].Hash)
+ require.Equal(uint64(103), segments[1].Blocks[1].Height)
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_MultipleReorgs() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Test case: Multiple reorgs
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0xa", ParentHash: "0x99"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0xb1", ParentHash: "0xa"},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 101, BlockHash: "0xb1"},
+ {EventId: 4, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0xb2", ParentHash: "0xa"},
+ {EventId: 5, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0xc1", ParentHash: "0xb2"},
+ {EventId: 6, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 102, BlockHash: "0xc1"},
+ {EventId: 7, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 101, BlockHash: "0xb2"},
+ {EventId: 8, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0xb3", ParentHash: "0xa"},
+ {EventId: 9, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0xc2", ParentHash: "0xb3"},
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Len(segments, 3, "Should have 3 segments with 2 reorgs")
+
+ // First segment
+ require.Len(segments[0].Blocks, 2)
+ require.Equal("0xa", segments[0].Blocks[0].Hash)
+ require.Equal("0xb1", segments[0].Blocks[1].Hash)
+
+ // Second segment
+ require.Len(segments[1].Blocks, 2)
+ require.Equal("0xb2", segments[1].Blocks[0].Hash)
+ require.Equal("0xc1", segments[1].Blocks[1].Hash)
+
+ // Third segment
+ require.Len(segments[2].Blocks, 2)
+ require.Equal("0xb3", segments[2].Blocks[0].Hash)
+ require.Equal("0xc2", segments[2].Blocks[1].Hash)
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_StartingWithRemoved() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Test case: Starting with BLOCK_REMOVED
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 102, BlockHash: "0xold"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 101, BlockHash: "0xold"},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0xb", ParentHash: "0xa"},
+ {EventId: 4, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0xc", ParentHash: "0xb"},
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Len(segments, 1, "Should have 1 segment when starting with REMOVED")
+ require.Len(segments[0].Blocks, 2)
+ require.Equal("0xb", segments[0].Blocks[0].Hash)
+ require.Equal("0xc", segments[0].Blocks[1].Hash)
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_EmptyEvents() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, []*model.EventEntry{})
+ require.Nil(segments, "Should return nil for empty events")
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_ComplexReorgPattern() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Complex pattern: build up chain, then deep reorg
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0x100", ParentHash: "0x99"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0x101a", ParentHash: "0x100"},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0x102a", ParentHash: "0x101a"},
+ {EventId: 4, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 103, BlockHash: "0x103a", ParentHash: "0x102a"},
+ // Deep reorg back to block 100
+ {EventId: 5, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 103, BlockHash: "0x103a"},
+ {EventId: 6, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 102, BlockHash: "0x102a"},
+ {EventId: 7, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 101, BlockHash: "0x101a"},
+ // Build new chain
+ {EventId: 8, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0x101b", ParentHash: "0x100"},
+ {EventId: 9, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0x102b", ParentHash: "0x101b"},
+ {EventId: 10, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 103, BlockHash: "0x103b", ParentHash: "0x102b"},
+ {EventId: 11, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 104, BlockHash: "0x104", ParentHash: "0x103b"},
+ {EventId: 12, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 105, BlockHash: "0x105", ParentHash: "0x104"},
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Len(segments, 2, "Should have 2 segments")
+
+ // First segment: original chain
+ require.Len(segments[0].Blocks, 4)
+ require.Equal("0x100", segments[0].Blocks[0].Hash)
+ require.Equal("0x103a", segments[0].Blocks[3].Hash)
+
+ // Second segment: new chain after reorg
+ require.Len(segments[1].Blocks, 5)
+ require.Equal("0x101b", segments[1].Blocks[0].Hash)
+ require.Equal("0x105", segments[1].Blocks[4].Hash)
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_SkippedBlocks() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Test with skipped blocks
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0xa", ParentHash: "0x99", BlockSkipped: false},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "", ParentHash: "", BlockSkipped: true},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0xc", ParentHash: "0xa", BlockSkipped: false},
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Len(segments, 1)
+ require.Len(segments[0].Blocks, 3)
+
+ // Verify skipped block is included
+ require.Equal(uint64(100), segments[0].Blocks[0].Height)
+ require.False(segments[0].Blocks[0].Skipped)
+ require.Equal(uint64(101), segments[0].Blocks[1].Height)
+ require.True(segments[0].Blocks[1].Skipped)
+ require.Equal(uint64(102), segments[0].Blocks[2].Height)
+ require.False(segments[0].Blocks[2].Skipped)
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_OnlyRemovedEvents() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Only BLOCK_REMOVED events - no blocks to migrate
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 102},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 101},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 100},
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Nil(segments, "Should return nil when only REMOVED events")
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_AlternatingAddRemove() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Alternating ADD and REMOVE pattern
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0xa", ParentHash: "0x99"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 100},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0xb", ParentHash: "0x99"},
+ {EventId: 4, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 100},
+ {EventId: 5, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0xc", ParentHash: "0x99"},
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Len(segments, 3, "Should have 3 segments")
+
+ // Each segment should have one block
+ for i, segment := range segments {
+ require.Len(segment.Blocks, 1, "Segment %d should have 1 block", i)
+ require.Equal(uint64(100), segment.Blocks[0].Height)
+ }
+ require.Equal("0xa", segments[0].Blocks[0].Hash)
+ require.Equal("0xb", segments[1].Blocks[0].Hash)
+ require.Equal("0xc", segments[2].Blocks[0].Hash)
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_ConsecutiveRemovals() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Multiple consecutive BLOCK_REMOVED events
+ events := []*model.EventEntry{
+ {EventId: 1, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0xa", ParentHash: "0x99"},
+ {EventId: 2, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0xb", ParentHash: "0xa"},
+ {EventId: 3, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 101},
+ {EventId: 4, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 100},
+ {EventId: 5, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 99}, // Removing even further back
+ {EventId: 6, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 99, BlockHash: "0xnew99", ParentHash: "0x98"},
+ {EventId: 7, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0xnew100", ParentHash: "0xnew99"},
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Len(segments, 2)
+
+ // First segment: original blocks
+ require.Len(segments[0].Blocks, 2)
+ require.Equal("0xa", segments[0].Blocks[0].Hash)
+ require.Equal("0xb", segments[0].Blocks[1].Hash)
+
+ // Second segment: new blocks after deep reorg
+ require.Len(segments[1].Blocks, 2)
+ require.Equal("0xnew99", segments[1].Blocks[0].Hash)
+ require.Equal("0xnew100", segments[1].Blocks[1].Hash)
+}
+
+func (s *migratorActivityTestSuite) TestFetchBlockData_OrderPreservation() {
+ require := testutil.Require(s.T())
+
+ // Create test blocks in specific order
+ blocksToMigrate := []BlockToMigrate{
+ {Height: 102, Hash: "0xc", EventSeq: 3},
+ {Height: 100, Hash: "0xa", EventSeq: 1},
+ {Height: 101, Hash: "0xb", EventSeq: 2},
+ }
+
+ // Test that WorkItem structure exists and has correct fields
+ workItem := WorkItem{
+ Block: blocksToMigrate[0],
+ Index: 0,
+ }
+ require.Equal(uint64(102), workItem.Block.Height)
+ require.Equal(0, workItem.Index)
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_EventSequenceOrdering() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Test that blocks maintain event sequence ordering within segments
+ events := []*model.EventEntry{
+ {EventId: 10, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 100, BlockHash: "0xa", ParentHash: "0x99"},
+ {EventId: 20, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 101, BlockHash: "0xb", ParentHash: "0xa"},
+ {EventId: 30, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0xc", ParentHash: "0xb"},
+ {EventId: 40, EventType: api.BlockchainEvent_BLOCK_REMOVED, BlockHeight: 102},
+ {EventId: 50, EventType: api.BlockchainEvent_BLOCK_ADDED, BlockHeight: 102, BlockHash: "0xd", ParentHash: "0xb"},
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Len(segments, 2)
+
+ // Verify EventSeq is preserved
+ require.Equal(int64(10), segments[0].Blocks[0].EventSeq)
+ require.Equal(int64(20), segments[0].Blocks[1].EventSeq)
+ require.Equal(int64(30), segments[0].Blocks[2].EventSeq)
+ require.Equal(int64(50), segments[1].Blocks[0].EventSeq)
+}
+
+func (s *migratorActivityTestSuite) TestBuildSegmentsFromEvents_LargeReorgChain() {
+ require := testutil.Require(s.T())
+ logger := s.app.Logger()
+
+ // Test with a large number of blocks and reorgs
+ events := make([]*model.EventEntry, 0, 1000)
+
+ // Add 100 blocks
+ for i := 0; i < 100; i++ {
+ events = append(events, &model.EventEntry{
+ EventId: int64(i + 1),
+ EventType: api.BlockchainEvent_BLOCK_ADDED,
+ BlockHeight: uint64(100 + i),
+ BlockHash: string(rune('a'+i%26)) + "orig",
+ ParentHash: string(rune('a'+(i-1)%26)) + "orig",
+ })
+ }
+
+ // Remove last 50 blocks
+ for i := 99; i >= 50; i-- {
+ events = append(events, &model.EventEntry{
+ EventId: int64(101 + (99 - i)),
+ EventType: api.BlockchainEvent_BLOCK_REMOVED,
+ BlockHeight: uint64(100 + i),
+ })
+ }
+
+ // Add 60 new blocks
+ for i := 0; i < 60; i++ {
+ events = append(events, &model.EventEntry{
+ EventId: int64(151 + i),
+ EventType: api.BlockchainEvent_BLOCK_ADDED,
+ BlockHeight: uint64(150 + i),
+ BlockHash: string(rune('a'+i%26)) + "new",
+ ParentHash: string(rune('a'+(i-1)%26)) + "new",
+ })
+ }
+
+ segments := s.migrator.buildSegmentsFromEvents(logger, events)
+ require.Len(segments, 2)
+ require.Len(segments[0].Blocks, 100)
+ require.Len(segments[1].Blocks, 60)
+}
diff --git a/internal/workflow/activity/module.go b/internal/workflow/activity/module.go
index c7391883..50b0de02 100644
--- a/internal/workflow/activity/module.go
+++ b/internal/workflow/activity/module.go
@@ -18,5 +18,13 @@ var Module = fx.Options(
fx.Provide(NewEventReconciler),
fx.Provide(NewEventLoader),
fx.Provide(NewReplicator),
+ fx.Provide(NewLatestBlock),
fx.Provide(NewUpdateWatermark),
+ fx.Provide(NewMigrator),
+ fx.Provide(NewBatchConsolidator),
+ fx.Provide(NewSingleBlockRetention),
+ fx.Provide(NewGetLatestBlockHeightActivity),
+ fx.Provide(NewGetLatestBlockFromPostgresActivity),
+ fx.Provide(NewGetLatestEventFromPostgresActivity),
+ fx.Provide(NewGetMaxEventIdActivity),
)
diff --git a/internal/workflow/activity/replicator.go b/internal/workflow/activity/replicator.go
index aea3aa69..916f903a 100644
--- a/internal/workflow/activity/replicator.go
+++ b/internal/workflow/activity/replicator.go
@@ -6,6 +6,8 @@ import (
"net/http"
"time"
+ "google.golang.org/protobuf/types/known/timestamppb"
+
"go.temporal.io/sdk/workflow"
"go.uber.org/fx"
"go.uber.org/zap"
@@ -38,24 +40,24 @@ var (
type (
Replicator struct {
baseActivity
- config *config.Config
- logger *zap.Logger
- client gateway.Client
- blockDownloader downloader.BlockDownloader
- metaStorage metastorage.MetaStorage
- blobStorage blobstorage.BlobStorage
- retry retry.RetryWithResult[[]byte]
- httpClient *http.Client
+ config *config.Config
+ logger *zap.Logger
+ client gateway.Client
+ blockDownloader downloader.BlockDownloader
+ metaStorage metastorage.MetaStorage
+ singleBlockUploader blobstorage.SingleBlockUploader
+ retry retry.RetryWithResult[[]byte]
+ httpClient *http.Client
}
ReplicatorParams struct {
fx.In
fxparams.Params
- Runtime cadence.Runtime
- Client gateway.Client
- BlockDownloader downloader.BlockDownloader
- MetaStorage metastorage.MetaStorage
- BlobStorage blobstorage.BlobStorage
+ Runtime cadence.Runtime
+ Client gateway.Client
+ BlockDownloader downloader.BlockDownloader
+ MetaStorage metastorage.MetaStorage
+ SingleBlockUploader blobstorage.SingleBlockUploader
}
ReplicatorRequest struct {
@@ -67,8 +69,10 @@ type (
}
ReplicatorResponse struct {
- StartHeight uint64
- EndHeight uint64
+ StartHeight uint64
+ EndHeight uint64
+ LatestBlockHeight uint64
+ LatestBlockTimestamp *timestamppb.Timestamp
}
)
@@ -80,15 +84,15 @@ func NewReplicator(params ReplicatorParams) *Replicator {
return "/workflow/activity/replicator"
}))
a := &Replicator{
- baseActivity: newBaseActivity(ActivityReplicator, params.Runtime),
- config: params.Config,
- logger: params.Logger,
- client: params.Client,
- blockDownloader: params.BlockDownloader,
- metaStorage: params.MetaStorage,
- blobStorage: params.BlobStorage,
- httpClient: httpClient,
- retry: retry.NewWithResult[[]byte](retry.WithLogger(params.Logger)),
+ baseActivity: newBaseActivity(ActivityReplicator, params.Runtime),
+ config: params.Config,
+ logger: params.Logger,
+ client: params.Client,
+ blockDownloader: params.BlockDownloader,
+ metaStorage: params.MetaStorage,
+ singleBlockUploader: params.SingleBlockUploader,
+ httpClient: httpClient,
+ retry: retry.NewWithResult[[]byte](retry.WithLogger(params.Logger)),
}
a.register(a.execute)
return a
@@ -109,7 +113,7 @@ func (a *Replicator) downloadBlockData(ctx context.Context, url string) ([]byte,
httpResp, err := a.httpClient.Do(req)
if err != nil {
- return nil, retry.Retryable(xerrors.Errorf("failed to download block file: %w", err))
+ return nil, retry.Retryable(xerrors.Errorf("failed to download block file: %w, url: %s", err, url))
}
finalizer := finalizer.WithCloser(httpResp.Body)
@@ -139,26 +143,25 @@ func (a *Replicator) prepareRawBlockData(ctx context.Context, blockFile *api.Blo
}
var rawBytes []byte
var compressedBytes []byte
- switch blockFile.Compression {
- case api.Compression_NONE:
- rawBytes = bodyBytes
- if compression == api.Compression_GZIP {
- compressedBytes, err = storage_utils.Compress(rawBytes, compression)
- if err != nil {
- return nil, xerrors.Errorf("failed to compress block data with type %v: %w", compression.String(), err)
- }
- }
- case api.Compression_GZIP:
+
+ sourceCompressionType := blockFile.Compression
+ if sourceCompressionType == compression {
compressedBytes = bodyBytes
- if compression == api.Compression_NONE {
- rawBytes, err = storage_utils.Decompress(rawBytes, blockFile.Compression)
- if err != nil {
- return nil, xerrors.Errorf("failed to decompress block data with type %v: %w", blockFile.Compression.String(), err)
- }
+ if sourceCompressionType == api.Compression_NONE {
+ rawBytes = bodyBytes
+ }
+
+ } else {
+ rawBytes, err = storage_utils.Decompress(bodyBytes, sourceCompressionType)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to decompress block data with type %v: %w", sourceCompressionType.String(), err)
+ }
+ compressedBytes, err = storage_utils.Compress(rawBytes, compression)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to compress block data with type %v: %w", compression.String(), err)
}
- default:
- return nil, xerrors.Errorf("unknown block file compression type %v", blockFile.Compression.String())
}
+
metadata := &api.BlockMetadata{
Tag: blockFile.Tag,
Hash: blockFile.Hash,
@@ -173,7 +176,6 @@ func (a *Replicator) prepareRawBlockData(ctx context.Context, blockFile *api.Blo
// TODO remove this after the api upgrade
if metadata.Timestamp == nil || (metadata.Timestamp.Nanos == 0 && metadata.Timestamp.Seconds == 0) {
block := new(api.Block)
- rawBytes := rawBytes
if len(rawBytes) == 0 {
rawBytes, err = storage_utils.Decompress(bodyBytes, blockFile.Compression)
if err != nil {
@@ -197,7 +199,7 @@ func (a *Replicator) prepareRawBlockData(ctx context.Context, blockFile *api.Blo
case api.Compression_NONE:
rawBlockData.BlockData = rawBytes
return rawBlockData, nil
- case api.Compression_GZIP:
+ case api.Compression_GZIP, api.Compression_ZSTD:
rawBlockData.BlockData = compressedBytes
return rawBlockData, nil
default:
@@ -229,6 +231,35 @@ func (a *Replicator) execute(ctx context.Context, request *ReplicatorRequest) (*
i := i
group.Go(func() error {
blockFile := blocks.Files[i]
+ if blockFile.GetSkipped() || blockFile.GetFileUrl() == "" {
+ if blockFile.GetSkipped() {
+ logger.Debug(
+ "block file skipped; skip download",
+ zap.Uint32("tag", blockFile.Tag),
+ zap.Uint64("height", blockFile.Height),
+ zap.Bool("skipped", blockFile.Skipped),
+ )
+ } else {
+ logger.Warn(
+ "block file url missing; skip download",
+ zap.Uint32("tag", blockFile.Tag),
+ zap.Uint64("height", blockFile.Height),
+ zap.Bool("skipped", blockFile.Skipped),
+ )
+ }
+
+ blockMetas[i] = &api.BlockMetadata{
+ Tag: blockFile.Tag,
+ Hash: blockFile.Hash,
+ ParentHash: blockFile.ParentHash,
+ Height: blockFile.Height,
+ ParentHeight: blockFile.ParentHeight,
+ Skipped: blockFile.Skipped,
+ Timestamp: blockFile.BlockTimestamp,
+ }
+ return nil
+ }
+
logger.Debug(
"downloading block",
zap.Uint32("tag", blockFile.Tag),
@@ -239,7 +270,7 @@ func (a *Replicator) execute(ctx context.Context, request *ReplicatorRequest) (*
if err != nil {
return xerrors.Errorf("failed to prepare raw block data: %w", err)
}
- objectKeyMain, err := a.blobStorage.UploadRaw(errgroupCtx, rawBlockData)
+ objectKeyMain, err := a.singleBlockUploader.UploadRaw(errgroupCtx, rawBlockData)
if err != nil {
return xerrors.Errorf("failed to upload raw block file: %w", err)
}
@@ -258,7 +289,9 @@ func (a *Replicator) execute(ctx context.Context, request *ReplicatorRequest) (*
}
return &ReplicatorResponse{
- StartHeight: request.StartHeight,
- EndHeight: request.EndHeight,
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ LatestBlockHeight: blockMetas[len(blockMetas)-1].Height,
+ LatestBlockTimestamp: blockMetas[len(blockMetas)-1].Timestamp,
}, nil
}
diff --git a/internal/workflow/activity/single_block_retention.go b/internal/workflow/activity/single_block_retention.go
new file mode 100644
index 00000000..ea4b906b
--- /dev/null
+++ b/internal/workflow/activity/single_block_retention.go
@@ -0,0 +1,655 @@
+package activity
+
+import (
+ "context"
+ "sort"
+ "strings"
+ "sync"
+ "time"
+
+ sdkactivity "go.temporal.io/sdk/activity"
+ "go.temporal.io/sdk/workflow"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ chains3 "github.com/coinbase/chainstorage/internal/s3"
+ metapostgres "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ SingleBlockRetention struct {
+ selectActivity baseActivity
+ processActivity baseActivity
+ config *config.Config
+ s3Client chains3.Client
+ componentsMu sync.Mutex
+ selector *retirement.Selector
+ planner *retirement.Planner
+ }
+
+ SingleBlockRetentionParams struct {
+ fx.In
+ fxparams.Params
+ Runtime cadence.Runtime
+ S3Client chains3.Client `optional:"true"`
+ }
+
+ SingleBlockRetentionSelectRequest struct {
+ Tag uint32
+ StartHeight uint64
+ EndHeight uint64
+ // A zero cutoff is accepted only for activity payloads scheduled by the
+ // pre-sweep workflow and is resolved to activity start time.
+ EligibilityCutoff time.Time
+ Limit int `validate:"required,gt=0,lte=250"`
+ }
+
+ SingleBlockRetentionSelectResponse struct {
+ Cohorts []retirement.RetentionCohort `json:"cohorts"`
+ HasMore bool `json:"has_more"`
+ }
+
+ SingleBlockRetentionProcessRequest struct {
+ Tag uint32
+ Cohort retirement.RetentionCohort
+ // A zero cutoff preserves compatibility with activity payloads
+ // scheduled before range sweeps froze their destructive set.
+ EligibilityCutoff time.Time
+ Execute bool
+ ProductionDeleteEnabled bool
+ DirectStorageClientsGuarded bool
+ SingleBlockWritersGuarded bool
+ FallbackReadsValidated bool
+ FallbackErrorCount uint64
+ // Approved* carry the operator's explicit deletion envelope verbatim.
+ // They are never derived from the selected cohort; execution fails
+ // closed unless they name this chain and contain the cohort.
+ ApprovedChain string
+ ApprovedStartHeight uint64
+ ApprovedEndHeight uint64
+ }
+
+ SingleBlockRetentionReasonCount struct {
+ Reason string `json:"reason"`
+ Rows uint64 `json:"rows"`
+ }
+
+ SingleBlockRetentionRangeResult struct {
+ Cohort retirement.RetentionCohort `json:"cohort"`
+ ScannedRows uint64 `json:"scanned_rows"`
+ PlannedRows uint64 `json:"planned_rows"`
+ DeletedVerifiedRows uint64 `json:"deleted_verified_rows"`
+ AlreadyRetiredRows uint64 `json:"already_retired_rows"`
+ SkippedSlots uint64 `json:"skipped_slots"`
+ DeferredRows uint64 `json:"deferred_rows"`
+ FailedRows uint64 `json:"failed_rows"`
+ DeletedVersions uint64 `json:"deleted_versions"`
+ DeletedMarkers uint64 `json:"deleted_markers"`
+ RetiredBytes uint64 `json:"retired_bytes"`
+ VerifiedThroughExclusive uint64 `json:"verified_through_exclusive"`
+ FirstIncompleteHeight *uint64 `json:"first_incomplete_height,omitempty"`
+ Terminal bool `json:"terminal"`
+ RetryAfter time.Duration `json:"retry_after,omitempty"`
+ RetryReason string `json:"retry_reason,omitempty"`
+ FailureMessage string `json:"failure_message,omitempty"`
+ Blockers []SingleBlockRetentionReasonCount `json:"blockers,omitempty"`
+ }
+)
+
+func NewSingleBlockRetention(params SingleBlockRetentionParams) *SingleBlockRetention {
+ a := &SingleBlockRetention{
+ selectActivity: newBaseActivity(ActivitySingleBlockRetentionSelect, params.Runtime),
+ processActivity: newBaseActivity(ActivitySingleBlockRetentionProcess, params.Runtime),
+ config: params.Config,
+ s3Client: params.S3Client,
+ }
+ a.selectActivity.register(a.executeSelect)
+ a.processActivity.register(a.executeProcess)
+ return a
+}
+
+func (a *SingleBlockRetention) Select(
+ ctx workflow.Context,
+ request *SingleBlockRetentionSelectRequest,
+) (*SingleBlockRetentionSelectResponse, error) {
+ var response SingleBlockRetentionSelectResponse
+ err := a.selectActivity.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *SingleBlockRetention) Process(
+ ctx workflow.Context,
+ request *SingleBlockRetentionProcessRequest,
+) (*SingleBlockRetentionRangeResult, error) {
+ var response SingleBlockRetentionRangeResult
+ err := a.processActivity.executeActivity(ctx, request, &response)
+ return &response, err
+}
+
+func (a *SingleBlockRetention) executeSelect(
+ ctx context.Context,
+ request *SingleBlockRetentionSelectRequest,
+) (*SingleBlockRetentionSelectResponse, error) {
+ if err := a.selectActivity.validateRequest(request); err != nil {
+ return nil, err
+ }
+ selector, _, err := a.getComponents(ctx)
+ if err != nil {
+ return nil, err
+ }
+ tag := a.config.GetEffectiveBlockTag(request.Tag)
+ eligibilityCutoff := resolveSingleBlockRetentionEligibilityCutoff(
+ request.EligibilityCutoff,
+ time.Now(),
+ )
+ sdkactivity.RecordHeartbeat(ctx, "single_block_retention.select.started", tag, request.Limit)
+ cohorts, hasMore, err := selector.Select(
+ ctx,
+ tag,
+ request.StartHeight,
+ request.EndHeight,
+ eligibilityCutoff,
+ request.Limit,
+ )
+ if err != nil {
+ return nil, err
+ }
+ a.selectActivity.getLogger(ctx).Info(
+ "selected single-block retention cohorts",
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", request.StartHeight),
+ zap.Uint64("end_height", request.EndHeight),
+ zap.Time("eligibility_cutoff", eligibilityCutoff),
+ zap.Int("cohorts", len(cohorts)),
+ zap.Bool("has_more", hasMore),
+ zap.Int("limit", request.Limit),
+ )
+ sdkactivity.RecordHeartbeat(ctx, "single_block_retention.select.completed", tag, len(cohorts))
+ return &SingleBlockRetentionSelectResponse{
+ Cohorts: cohorts,
+ HasMore: hasMore,
+ }, nil
+}
+
+func (a *SingleBlockRetention) executeProcess(
+ ctx context.Context,
+ request *SingleBlockRetentionProcessRequest,
+) (*SingleBlockRetentionRangeResult, error) {
+ if err := a.processActivity.validateRequest(request); err != nil {
+ return nil, err
+ }
+ if err := validateSingleBlockRetentionProcessRequest(a.config, request); err != nil {
+ return nil, err
+ }
+ _, planner, err := a.getComponents(ctx)
+ if err != nil {
+ return nil, err
+ }
+
+ req := a.planRequest(request)
+ logger := a.processActivity.getLogger(ctx).With(
+ zap.Uint32("tag", req.Tag),
+ zap.String("consolidated_object_key", request.Cohort.ConsolidatedObjectKey),
+ zap.Uint64("start_height", req.StartHeight),
+ zap.Uint64("end_height", req.EndHeight),
+ zap.Bool("execute", req.Execute),
+ )
+ sdkactivity.RecordHeartbeat(
+ ctx,
+ "single_block_retention.process.started",
+ req.Tag,
+ req.StartHeight,
+ req.EndHeight,
+ req.Execute,
+ )
+
+ if request.Execute {
+ reconcileReport, reconcileErr := planner.Reconcile(ctx, req)
+ var reconcileResult *SingleBlockRetentionRangeResult
+ if reconcileReport != nil {
+ reconcileResult, err = summarizeSingleBlockRetentionReport(request.Cohort, reconcileReport)
+ if err != nil {
+ return nil, err
+ }
+ if setSingleBlockRetentionRetry(reconcileResult) {
+ return reconcileResult, nil
+ }
+ }
+ if reconcileErr != nil {
+ if reconcileResult == nil {
+ return nil, xerrors.Errorf("failed to reconcile retention cohort: %w", reconcileErr)
+ }
+ reconcileResult.FailureMessage = reconcileErr.Error()
+ logger.Error("failed to reconcile single-block retention cohort", zap.Error(reconcileErr))
+ return reconcileResult, nil
+ }
+ }
+
+ report, err := planner.Plan(ctx, req)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to plan retention cohort: %w", err)
+ }
+ if request.Execute {
+ if err := validateSingleBlockRetentionExecutionPlan(request.Cohort, report); err != nil {
+ return nil, err
+ }
+ if applyErr := planner.Apply(ctx, req, report); applyErr != nil {
+ result, summaryErr := summarizeSingleBlockRetentionReport(request.Cohort, report)
+ if summaryErr != nil {
+ return nil, summaryErr
+ }
+ if setSingleBlockRetentionRetry(result) {
+ return result, nil
+ }
+ result.FailureMessage = applyErr.Error()
+ logger.Error("failed to apply single-block retention cohort", zap.Error(applyErr))
+ return result, nil
+ }
+ }
+
+ result, err := summarizeSingleBlockRetentionReport(request.Cohort, report)
+ if err != nil {
+ return nil, err
+ }
+ if request.Execute && setSingleBlockRetentionRetry(result) {
+ return result, nil
+ }
+ logger.Info(
+ "processed single-block retention cohort",
+ zap.Uint64("scanned_rows", result.ScannedRows),
+ zap.Uint64("deleted_verified_rows", result.DeletedVerifiedRows),
+ zap.Uint64("already_retired_rows", result.AlreadyRetiredRows),
+ zap.Uint64("deferred_rows", result.DeferredRows),
+ zap.Uint64("failed_rows", result.FailedRows),
+ zap.Bool("terminal", result.Terminal),
+ )
+ sdkactivity.RecordHeartbeat(
+ ctx,
+ "single_block_retention.process.completed",
+ req.Tag,
+ req.StartHeight,
+ req.EndHeight,
+ result.DeletedVerifiedRows,
+ result.Terminal,
+ )
+ return result, nil
+}
+
+func (a *SingleBlockRetention) getComponents(
+ ctx context.Context,
+) (*retirement.Selector, *retirement.Planner, error) {
+ a.componentsMu.Lock()
+ defer a.componentsMu.Unlock()
+ if a.selector != nil && a.planner != nil {
+ return a.selector, a.planner, nil
+ }
+ if a.config.StorageType.MetaStorageType != config.MetaStorageType_POSTGRES || a.config.AWS.Postgres == nil {
+ return nil, nil, xerrors.New("single-block retention workflow requires Postgres meta storage")
+ }
+ if a.config.StorageType.BlobStorageType != config.BlobStorageType_UNSPECIFIED &&
+ a.config.StorageType.BlobStorageType != config.BlobStorageType_S3 {
+ return nil, nil, xerrors.New("single-block retention workflow requires S3 blob storage")
+ }
+ if a.s3Client == nil {
+ return nil, nil, xerrors.New("single-block retention workflow requires an S3 client")
+ }
+ pool, err := metapostgres.GetConnectionPool(ctx, a.config.AWS.Postgres)
+ if err != nil {
+ return nil, nil, xerrors.Errorf("failed to get single-block retention Postgres pool: %w", err)
+ }
+ db := pool.DB()
+ if db == nil {
+ return nil, nil, xerrors.New("single-block retention Postgres pool returned a nil database")
+ }
+ repo := retirement.NewPostgresRepository(db)
+ a.selector = retirement.NewSelector(repo)
+ // Heartbeat from the planner's row/version loops so the configured
+ // activity heartbeat timeout bounds destructive work and Temporal can
+ // deliver cancellation between steps instead of only at the end.
+ a.planner = retirement.NewPlanner(
+ repo,
+ retirement.NewS3ObjectStore(a.s3Client),
+ retirement.WithHeartbeat(func(ctx context.Context, details ...any) {
+ sdkactivity.RecordHeartbeat(ctx, details...)
+ }),
+ )
+ return a.selector, a.planner, nil
+}
+
+func (a *SingleBlockRetention) planRequest(request *SingleBlockRetentionProcessRequest) retirement.PlanRequest {
+ blockchain, network, sidechain := singleBlockRetentionChainNames(a.config)
+ tag := a.config.GetEffectiveBlockTag(request.Tag)
+ now := time.Now().UTC()
+ eligibilityCutoff := resolveSingleBlockRetentionEligibilityCutoff(request.EligibilityCutoff, now)
+ approval := retirement.Approval{
+ Chain: request.ApprovedChain,
+ StartHeight: request.ApprovedStartHeight,
+ EndHeight: request.ApprovedEndHeight,
+ }
+ if request.ApprovedChain != "" {
+ approval.AllowContainingRange = true
+ }
+ return retirement.PlanRequest{
+ Environment: string(a.config.Env()),
+ Blockchain: blockchain,
+ Network: network,
+ Sidechain: sidechain,
+ Bucket: a.config.AWS.Bucket,
+ Tag: tag,
+ StartHeight: request.Cohort.StartHeight,
+ EndHeight: request.Cohort.EndHeight,
+ Now: now,
+ EligibilityCutoff: eligibilityCutoff,
+ Execute: request.Execute,
+ ProductionDeleteEnabled: request.ProductionDeleteEnabled,
+ DirectStorageClientsGuarded: request.DirectStorageClientsGuarded,
+ SingleBlockWritersGuarded: request.SingleBlockWritersGuarded,
+ FallbackErrorCount: request.FallbackErrorCount,
+ // The approval is the operator's assertion passed through unchanged.
+ // The planner independently re-verifies the actual chain and requires
+ // the cohort under deletion to be contained by this immutable envelope.
+ Approval: approval,
+ }
+}
+
+func resolveSingleBlockRetentionEligibilityCutoff(cutoff time.Time, now time.Time) time.Time {
+ if cutoff.IsZero() {
+ return now.UTC()
+ }
+ return cutoff.UTC()
+}
+
+func singleBlockRetentionExpectedChain(cfg *config.Config) string {
+ blockchain, network, sidechain := singleBlockRetentionChainNames(cfg)
+ parts := []string{blockchain, network}
+ if sidechain != "" {
+ parts = append(parts, sidechain)
+ }
+ return strings.Join(parts, "-")
+}
+
+func singleBlockRetentionChainNames(cfg *config.Config) (string, string, string) {
+ blockchain := cfg.Blockchain().GetName()
+ network := strings.TrimPrefix(cfg.Network().GetName(), blockchain+"-")
+ sidechain := ""
+ if cfg.Sidechain() != api.SideChain_SIDECHAIN_NONE {
+ sidechain = strings.TrimPrefix(
+ cfg.Sidechain().GetName(),
+ blockchain+"-"+network+"-",
+ )
+ }
+ return blockchain, network, sidechain
+}
+
+func validateSingleBlockRetentionProcessRequest(
+ cfg *config.Config,
+ request *SingleBlockRetentionProcessRequest,
+) error {
+ if request == nil {
+ return xerrors.New("single-block retention process request is required")
+ }
+ cohort := request.Cohort
+ if cohort.ConsolidatedObjectKey == "" || cohort.EndHeight <= cohort.StartHeight ||
+ cohort.RowCount == 0 || cohort.EligibleAt.IsZero() {
+ return xerrors.Errorf("invalid single-block retention cohort: %+v", cohort)
+ }
+ if !request.Execute {
+ return nil
+ }
+ if !request.DirectStorageClientsGuarded {
+ return xerrors.New("retention execution requires direct storage clients to be guarded or out of scope")
+ }
+ if !request.SingleBlockWritersGuarded {
+ return xerrors.New("retention execution requires every single-block writer to honor the retirement fence")
+ }
+ if !request.FallbackReadsValidated {
+ return xerrors.New("retention execution requires explicit fallback-disabled read validation")
+ }
+ if request.FallbackErrorCount != 0 {
+ return xerrors.Errorf("retention execution requires zero fallback read errors, got %d", request.FallbackErrorCount)
+ }
+ if request.ApprovedChain == "" {
+ return xerrors.New("retention execution requires an explicit operator approval chain")
+ }
+ if cfg != nil {
+ expectedChain := singleBlockRetentionExpectedChain(cfg)
+ if retirement.NormalizeApprovalChain(request.ApprovedChain) != retirement.NormalizeApprovalChain(expectedChain) {
+ return xerrors.Errorf(
+ "retention execution approval names chain %q, expected %q",
+ request.ApprovedChain,
+ expectedChain,
+ )
+ }
+ }
+ if request.ApprovedStartHeight > cohort.StartHeight || request.ApprovedEndHeight < cohort.EndHeight {
+ return xerrors.Errorf(
+ "retention execution cohort %q [%d, %d) is outside approval envelope [%d, %d)",
+ cohort.ConsolidatedObjectKey,
+ cohort.StartHeight,
+ cohort.EndHeight,
+ request.ApprovedStartHeight,
+ request.ApprovedEndHeight,
+ )
+ }
+ if cfg != nil && isProductionRetentionEnvironment(cfg.Env()) && !request.ProductionDeleteEnabled {
+ return xerrors.New("production retention execution requires explicit production-delete enablement")
+ }
+ return nil
+}
+
+func isProductionRetentionEnvironment(env config.Env) bool {
+ value := strings.ToLower(string(env))
+ return value == "production" || value == "prod"
+}
+
+func summarizeSingleBlockRetentionReport(
+ cohort retirement.RetentionCohort,
+ report *retirement.Report,
+) (*SingleBlockRetentionRangeResult, error) {
+ if report == nil {
+ return nil, xerrors.New("single-block retention report is required")
+ }
+ if report.StartHeight != cohort.StartHeight || report.EndHeight != cohort.EndHeight {
+ return nil, xerrors.Errorf(
+ "retention report range [%d, %d) does not match cohort [%d, %d)",
+ report.StartHeight,
+ report.EndHeight,
+ cohort.StartHeight,
+ cohort.EndHeight,
+ )
+ }
+ result := &SingleBlockRetentionRangeResult{
+ Cohort: cohort,
+ ScannedRows: uint64(len(report.Items)),
+ VerifiedThroughExclusive: cohort.StartHeight,
+ }
+ reasons := make(map[string]uint64)
+ items := append([]retirement.Candidate(nil), report.Items...)
+ sort.Slice(items, func(i, j int) bool {
+ return items[i].Height < items[j].Height
+ })
+ for _, item := range items {
+ if item.SkipReason != retirement.SkipSkippedBlock &&
+ item.ConsolidatedKey != cohort.ConsolidatedObjectKey {
+ return nil, xerrors.Errorf(
+ "retention report row at height %d belongs to consolidated object %q, expected %q",
+ item.Height,
+ item.ConsolidatedKey,
+ cohort.ConsolidatedObjectKey,
+ )
+ }
+ switch item.Action {
+ case retirement.ActionReportOnly, retirement.ActionDeleteObjectVersion:
+ result.PlannedRows++
+ case retirement.ActionDeletedVerified:
+ result.DeletedVerifiedRows++
+ result.DeletedVersions += uint64(len(item.VersionIDs))
+ result.DeletedMarkers += uint64(len(item.DeleteMarkerVersionIDs))
+ result.RetiredBytes += item.SingleBlockBytes
+ case retirement.ActionAlreadyDeleted:
+ result.AlreadyRetiredRows++
+ case retirement.ActionDeletedObjectVersion:
+ result.DeferredRows++
+ default:
+ if item.SkipReason == retirement.SkipSkippedBlock {
+ result.SkippedSlots++
+ } else if isDeferredRetentionReason(item.SkipReason) {
+ result.DeferredRows++
+ } else {
+ result.FailedRows++
+ }
+ }
+ if item.SkipReason != "" && item.SkipReason != retirement.SkipRetirementAlreadyFinalized &&
+ item.SkipReason != retirement.SkipSkippedBlock {
+ reasons[item.SkipReason]++
+ }
+ }
+ reasonNames := make([]string, 0, len(reasons))
+ for reason := range reasons {
+ reasonNames = append(reasonNames, reason)
+ }
+ sort.Strings(reasonNames)
+ for _, reason := range reasonNames {
+ result.Blockers = append(result.Blockers, SingleBlockRetentionReasonCount{
+ Reason: reason,
+ Rows: reasons[reason],
+ })
+ }
+
+ nextHeight := cohort.StartHeight
+ for _, item := range items {
+ if item.Height < nextHeight {
+ return nil, xerrors.Errorf("retention report contains duplicate or out-of-order height %d", item.Height)
+ }
+ if item.Height > nextHeight {
+ result.FirstIncompleteHeight = uint64Pointer(nextHeight)
+ break
+ }
+ if !terminalRetentionCandidate(item) {
+ result.FirstIncompleteHeight = uint64Pointer(item.Height)
+ break
+ }
+ nextHeight++
+ result.VerifiedThroughExclusive = nextHeight
+ }
+ if result.FirstIncompleteHeight == nil && nextHeight < cohort.EndHeight {
+ result.FirstIncompleteHeight = uint64Pointer(nextHeight)
+ }
+ result.Terminal = result.FirstIncompleteHeight == nil &&
+ result.VerifiedThroughExclusive == cohort.EndHeight &&
+ result.ScannedRows > 0
+ return result, nil
+}
+
+func validateSingleBlockRetentionExecutionPlan(
+ cohort retirement.RetentionCohort,
+ report *retirement.Report,
+) error {
+ if report == nil {
+ return xerrors.New("single-block retention execution report is required")
+ }
+ if report.StartHeight != cohort.StartHeight || report.EndHeight != cohort.EndHeight {
+ return xerrors.Errorf(
+ "retention execution report range [%d, %d) does not match cohort [%d, %d)",
+ report.StartHeight,
+ report.EndHeight,
+ cohort.StartHeight,
+ cohort.EndHeight,
+ )
+ }
+ items := append([]retirement.Candidate(nil), report.Items...)
+ sort.Slice(items, func(i, j int) bool {
+ return items[i].Height < items[j].Height
+ })
+ nextHeight := cohort.StartHeight
+ for _, item := range items {
+ if item.Height != nextHeight {
+ return xerrors.Errorf(
+ "retention execution plan has non-contiguous height: expected=%d actual=%d",
+ nextHeight,
+ item.Height,
+ )
+ }
+ if item.SkipReason == retirement.SkipSkippedBlock {
+ if item.Action != retirement.ActionSkip {
+ return xerrors.Errorf(
+ "skipped retention slot at height %d has invalid action %q",
+ item.Height,
+ item.Action,
+ )
+ }
+ } else {
+ if item.ConsolidatedKey != cohort.ConsolidatedObjectKey {
+ return xerrors.Errorf(
+ "retention execution plan row at height %d belongs to consolidated object %q, expected %q",
+ item.Height,
+ item.ConsolidatedKey,
+ cohort.ConsolidatedObjectKey,
+ )
+ }
+ if item.Action != retirement.ActionDeleteObjectVersion &&
+ item.Action != retirement.ActionAlreadyDeleted {
+ return xerrors.Errorf(
+ "retention execution plan row at height %d is not executable: action=%q reason=%q",
+ item.Height,
+ item.Action,
+ item.SkipReason,
+ )
+ }
+ }
+ nextHeight++
+ }
+ if nextHeight != cohort.EndHeight {
+ return xerrors.Errorf(
+ "retention execution plan does not cover cohort [%d, %d): verified through %d",
+ cohort.StartHeight,
+ cohort.EndHeight,
+ nextHeight,
+ )
+ }
+ return nil
+}
+
+func terminalRetentionCandidate(item retirement.Candidate) bool {
+ return item.Action == retirement.ActionDeletedVerified ||
+ item.Action == retirement.ActionAlreadyDeleted ||
+ item.Action == retirement.ActionSkip && item.SkipReason == retirement.SkipSkippedBlock
+}
+
+func isDeferredRetentionReason(reason string) bool {
+ switch reason {
+ case retirement.SkipRetentionPeriodActive,
+ retirement.SkipCSCBSafetyQuiescenceActive,
+ retirement.SkipRetirementVerificationPending,
+ retirement.SkipRetirementClaimActive:
+ return true
+ default:
+ return false
+ }
+}
+
+func setSingleBlockRetentionRetry(result *SingleBlockRetentionRangeResult) bool {
+ if result == nil || result.FailedRows != 0 || result.DeferredRows == 0 || len(result.Blockers) != 1 {
+ return false
+ }
+ switch result.Blockers[0].Reason {
+ case retirement.SkipCSCBSafetyQuiescenceActive:
+ result.RetryAfter = retirement.RetentionSafetyQuiescencePeriod
+ case retirement.SkipRetirementClaimActive:
+ result.RetryAfter = retirement.RetirementClaimLease
+ default:
+ return false
+ }
+ result.RetryReason = result.Blockers[0].Reason
+ return true
+}
+
+func uint64Pointer(value uint64) *uint64 {
+ return &value
+}
diff --git a/internal/workflow/activity/single_block_retention_test.go b/internal/workflow/activity/single_block_retention_test.go
new file mode 100644
index 00000000..1f982365
--- /dev/null
+++ b/internal/workflow/activity/single_block_retention_test.go
@@ -0,0 +1,326 @@
+package activity
+
+import (
+ "math"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+)
+
+func TestSummarizeSingleBlockRetentionReportReturnsExactCompletedRange(t *testing.T) {
+ cohort := retirement.RetentionCohort{
+ ConsolidatedObjectKey: "consolidated/100-104.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 104,
+ RowCount: 3,
+ EligibleAt: time.Now().Add(-time.Hour),
+ }
+ report := &retirement.Report{
+ StartHeight: 100,
+ EndHeight: 104,
+ Items: []retirement.Candidate{
+ {
+ Height: 100,
+ ConsolidatedKey: cohort.ConsolidatedObjectKey,
+ Action: retirement.ActionDeletedVerified,
+ VersionIDs: []string{"v1", "v2"},
+ SingleBlockBytes: 1000,
+ },
+ {
+ Height: 101,
+ ConsolidatedKey: cohort.ConsolidatedObjectKey,
+ Action: retirement.ActionAlreadyDeleted,
+ },
+ {
+ Height: 102,
+ Action: retirement.ActionSkip,
+ SkipReason: retirement.SkipSkippedBlock,
+ },
+ {
+ Height: 103,
+ ConsolidatedKey: cohort.ConsolidatedObjectKey,
+ Action: retirement.ActionDeletedVerified,
+ VersionIDs: []string{"v3"},
+ DeleteMarkerVersionIDs: []string{"dm1"},
+ SingleBlockBytes: 2000,
+ },
+ },
+ }
+
+ result, err := summarizeSingleBlockRetentionReport(cohort, report)
+ require.NoError(t, err)
+ require.True(t, result.Terminal)
+ require.Nil(t, result.FirstIncompleteHeight)
+ require.Equal(t, uint64(104), result.VerifiedThroughExclusive)
+ require.Equal(t, uint64(2), result.DeletedVerifiedRows)
+ require.Equal(t, uint64(1), result.AlreadyRetiredRows)
+ require.Equal(t, uint64(1), result.SkippedSlots)
+ require.Equal(t, uint64(3), result.DeletedVersions)
+ require.Equal(t, uint64(1), result.DeletedMarkers)
+ require.Equal(t, uint64(3000), result.RetiredBytes)
+ require.Empty(t, result.Blockers)
+}
+
+func TestSummarizeSingleBlockRetentionReportIdentifiesFirstIncompleteHeight(t *testing.T) {
+ cohort := retirement.RetentionCohort{
+ ConsolidatedObjectKey: "consolidated/100-103.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 103,
+ RowCount: 3,
+ EligibleAt: time.Now().Add(-time.Hour),
+ }
+ report := &retirement.Report{
+ StartHeight: 100,
+ EndHeight: 103,
+ Items: []retirement.Candidate{
+ {
+ Height: 100,
+ ConsolidatedKey: cohort.ConsolidatedObjectKey,
+ Action: retirement.ActionDeletedVerified,
+ },
+ {
+ Height: 101,
+ ConsolidatedKey: cohort.ConsolidatedObjectKey,
+ Action: retirement.ActionSkip,
+ SkipReason: retirement.SkipCSCBSafetyQuiescenceActive,
+ },
+ {
+ Height: 102,
+ ConsolidatedKey: cohort.ConsolidatedObjectKey,
+ Action: retirement.ActionSkip,
+ SkipReason: retirement.SkipCSCBSafetyQuiescenceActive,
+ },
+ },
+ }
+
+ result, err := summarizeSingleBlockRetentionReport(cohort, report)
+ require.NoError(t, err)
+ require.False(t, result.Terminal)
+ require.NotNil(t, result.FirstIncompleteHeight)
+ require.Equal(t, uint64(101), *result.FirstIncompleteHeight)
+ require.Equal(t, uint64(101), result.VerifiedThroughExclusive)
+ require.Equal(t, uint64(2), result.DeferredRows)
+ require.Equal(t, []SingleBlockRetentionReasonCount{{
+ Reason: retirement.SkipCSCBSafetyQuiescenceActive,
+ Rows: 2,
+ }}, result.Blockers)
+ require.True(t, setSingleBlockRetentionRetry(result))
+ require.Equal(t, retirement.RetentionSafetyQuiescencePeriod, result.RetryAfter)
+ require.Equal(t, retirement.SkipCSCBSafetyQuiescenceActive, result.RetryReason)
+}
+
+func TestSetSingleBlockRetentionRetryDefersActiveClaim(t *testing.T) {
+ result := &SingleBlockRetentionRangeResult{
+ DeferredRows: 1,
+ Blockers: []SingleBlockRetentionReasonCount{{
+ Reason: retirement.SkipRetirementClaimActive,
+ Rows: 1,
+ }},
+ }
+
+ require.True(t, setSingleBlockRetentionRetry(result))
+ require.Equal(t, retirement.RetirementClaimLease, result.RetryAfter)
+ require.Equal(t, retirement.SkipRetirementClaimActive, result.RetryReason)
+}
+
+func TestSummarizeSingleBlockRetentionReportRejectsMixedConsolidatedObjects(t *testing.T) {
+ cohort := retirement.RetentionCohort{
+ ConsolidatedObjectKey: "consolidated/a.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 101,
+ RowCount: 1,
+ EligibleAt: time.Now().Add(-time.Hour),
+ }
+ report := &retirement.Report{
+ StartHeight: 100,
+ EndHeight: 101,
+ Items: []retirement.Candidate{{
+ Height: 100,
+ ConsolidatedKey: "consolidated/b.cscb.zstd",
+ Action: retirement.ActionDeletedVerified,
+ }},
+ }
+
+ _, err := summarizeSingleBlockRetentionReport(cohort, report)
+ require.ErrorContains(t, err, "belongs to consolidated object")
+}
+
+func TestValidateSingleBlockRetentionExecutionPlanFailsClosed(t *testing.T) {
+ cohort := retirement.RetentionCohort{
+ ConsolidatedObjectKey: "consolidated/a.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 102,
+ RowCount: 1,
+ EligibleAt: time.Now().Add(-time.Hour),
+ }
+ valid := &retirement.Report{
+ StartHeight: 100,
+ EndHeight: 102,
+ Items: []retirement.Candidate{
+ {
+ Height: 100,
+ ConsolidatedKey: cohort.ConsolidatedObjectKey,
+ Action: retirement.ActionDeleteObjectVersion,
+ },
+ {
+ Height: 101,
+ ConsolidatedKey: cohort.ConsolidatedObjectKey,
+ Action: retirement.ActionAlreadyDeleted,
+ },
+ },
+ }
+ require.NoError(t, validateSingleBlockRetentionExecutionPlan(cohort, valid))
+
+ mixedObject := *valid
+ mixedObject.Items = append([]retirement.Candidate(nil), valid.Items...)
+ mixedObject.Items[1].ConsolidatedKey = "consolidated/b.cscb.zstd"
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionExecutionPlan(cohort, &mixedObject),
+ "belongs to consolidated object",
+ )
+
+ blocked := *valid
+ blocked.Items = append([]retirement.Candidate(nil), valid.Items...)
+ blocked.Items[1].Action = retirement.ActionSkip
+ blocked.Items[1].SkipReason = retirement.SkipRetentionPeriodActive
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionExecutionPlan(cohort, &blocked),
+ "is not executable",
+ )
+
+ missing := *valid
+ missing.Items = valid.Items[:1]
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionExecutionPlan(cohort, &missing),
+ "does not cover cohort",
+ )
+
+}
+
+func TestValidateSingleBlockRetentionProcessRequestRequiresExecutionGates(t *testing.T) {
+ cfg, err := config.New(config.WithEnvironment(config.EnvProduction))
+ require.NoError(t, err)
+ base := &SingleBlockRetentionProcessRequest{
+ Cohort: retirement.RetentionCohort{
+ ConsolidatedObjectKey: "consolidated/a.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 101,
+ RowCount: 1,
+ EligibleAt: time.Now().Add(-time.Hour),
+ },
+ Execute: true,
+ }
+
+ err = validateSingleBlockRetentionProcessRequest(cfg, base)
+ require.ErrorContains(t, err, "direct storage clients")
+
+ base.DirectStorageClientsGuarded = true
+ err = validateSingleBlockRetentionProcessRequest(cfg, base)
+ require.ErrorContains(t, err, "single-block writer")
+
+ base.SingleBlockWritersGuarded = true
+ err = validateSingleBlockRetentionProcessRequest(cfg, base)
+ require.ErrorContains(t, err, "explicit fallback-disabled")
+
+ base.FallbackReadsValidated = true
+ base.FallbackErrorCount = 1
+ err = validateSingleBlockRetentionProcessRequest(cfg, base)
+ require.ErrorContains(t, err, "zero fallback")
+
+ base.FallbackErrorCount = 0
+ err = validateSingleBlockRetentionProcessRequest(cfg, base)
+ require.ErrorContains(t, err, "operator approval chain")
+
+ base.ApprovedChain = "wrong-chain"
+ base.ApprovedStartHeight = 100
+ base.ApprovedEndHeight = 101
+ err = validateSingleBlockRetentionProcessRequest(cfg, base)
+ require.ErrorContains(t, err, "approval names chain")
+
+ base.ApprovedChain = singleBlockRetentionExpectedChain(cfg)
+ base.ApprovedStartHeight = 101
+ err = validateSingleBlockRetentionProcessRequest(cfg, base)
+ require.ErrorContains(t, err, "outside approval envelope")
+
+ base.ApprovedStartHeight = 99
+ base.ApprovedEndHeight = 102
+ err = validateSingleBlockRetentionProcessRequest(cfg, base)
+ require.ErrorContains(t, err, "production-delete")
+
+ base.ProductionDeleteEnabled = true
+ require.NoError(t, validateSingleBlockRetentionProcessRequest(cfg, base))
+}
+
+func TestSingleBlockRetentionPlanRequestUsesOperatorApprovalVerbatim(t *testing.T) {
+ cfg, err := config.New(
+ config.WithBlockchain(common.Blockchain_BLOCKCHAIN_SOLANA),
+ config.WithNetwork(common.Network_NETWORK_SOLANA_MAINNET),
+ )
+ require.NoError(t, err)
+ a := &SingleBlockRetention{config: cfg}
+ cohort := retirement.RetentionCohort{
+ ConsolidatedObjectKey: "consolidated/a.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 110,
+ RowCount: 10,
+ EligibleAt: time.Now().Add(-time.Hour),
+ }
+
+ executeReq := a.planRequest(&SingleBlockRetentionProcessRequest{
+ Cohort: cohort,
+ EligibilityCutoff: cohort.EligibleAt,
+ Execute: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 90,
+ ApprovedEndHeight: 120,
+ })
+ require.Equal(t, retirement.Approval{
+ Chain: "solana-mainnet",
+ StartHeight: 90,
+ EndHeight: 120,
+ AllowContainingRange: true,
+ }, executeReq.Approval)
+ require.Equal(t, cohort.EligibleAt.UTC(), executeReq.EligibilityCutoff)
+
+ tagZeroReq := a.planRequest(&SingleBlockRetentionProcessRequest{
+ Tag: math.MaxUint32,
+ Cohort: cohort,
+ })
+ require.Equal(t, uint32(0), tagZeroReq.Tag)
+
+ // An omitted operator approval stays omitted; it is never synthesized
+ // from the selected cohort.
+ dryRunReq := a.planRequest(&SingleBlockRetentionProcessRequest{Cohort: cohort})
+ require.Equal(t, retirement.Approval{}, dryRunReq.Approval)
+ require.False(t, dryRunReq.EligibilityCutoff.IsZero())
+ require.Equal(t, dryRunReq.Now, dryRunReq.EligibilityCutoff)
+}
+
+func TestResolveSingleBlockRetentionEligibilityCutoffSupportsLegacyPayload(t *testing.T) {
+ now := time.Date(2026, 7, 31, 4, 0, 0, 0, time.FixedZone("test", 8*60*60))
+ require.Equal(t, now.UTC(), resolveSingleBlockRetentionEligibilityCutoff(time.Time{}, now))
+
+ cutoff := now.Add(-time.Hour)
+ require.Equal(t, cutoff.UTC(), resolveSingleBlockRetentionEligibilityCutoff(cutoff, now.Add(time.Hour)))
+}
+
+func TestSingleBlockRetentionChainNamesUseOperatorCompatibleComponents(t *testing.T) {
+ cfg, err := config.New(
+ config.WithBlockchain(common.Blockchain_BLOCKCHAIN_SOLANA),
+ config.WithNetwork(common.Network_NETWORK_SOLANA_MAINNET),
+ )
+ require.NoError(t, err)
+
+ blockchain, network, sidechain := singleBlockRetentionChainNames(cfg)
+ require.Equal(t, "solana", blockchain)
+ require.Equal(t, "mainnet", network)
+ require.Empty(t, sidechain)
+}
diff --git a/internal/workflow/activity/syncer.go b/internal/workflow/activity/syncer.go
index 512880f4..0d41166a 100644
--- a/internal/workflow/activity/syncer.go
+++ b/internal/workflow/activity/syncer.go
@@ -49,7 +49,7 @@ type (
baseActivity
heartbeater Heartbeater
metaStorage metastorage.MetaStorage
- blobStorage blobstorage.BlobStorage
+ singleBlockUploader blobstorage.SingleBlockUploader
masterBlockchainClient client.Client // Used as the source-of-truth for resolving the canonical chain. It should be connected to a single-node cluster.
slaveBlockchainClient client.Client // Used to ingest data concurrently. It may be connected to multiple clusters for load balancing purposes.
consensusBlockchainClient client.Client // Used to run consensus validation. It may be connected to multiple clusters for load balancing purposes.
@@ -61,12 +61,12 @@ type (
SyncerParams struct {
fx.In
fxparams.Params
- Runtime cadence.Runtime
- Heartbeater Heartbeater
- MetaStorage metastorage.MetaStorage
- BlobStorage blobstorage.BlobStorage
- BlockchainClient client.ClientParams
- FailoverManager endpoints.FailoverManager
+ Runtime cadence.Runtime
+ Heartbeater Heartbeater
+ MetaStorage metastorage.MetaStorage
+ SingleBlockUploader blobstorage.SingleBlockUploader
+ BlockchainClient client.ClientParams
+ FailoverManager endpoints.FailoverManager
}
SyncerRequest struct {
@@ -119,7 +119,7 @@ func NewSyncer(params SyncerParams) *Syncer {
baseActivity: newBaseActivity(ActivitySyncer, params.Runtime),
heartbeater: params.Heartbeater,
metaStorage: params.MetaStorage,
- blobStorage: params.BlobStorage,
+ singleBlockUploader: params.SingleBlockUploader,
masterBlockchainClient: params.BlockchainClient.Master,
slaveBlockchainClient: params.BlockchainClient.Slave,
consensusBlockchainClient: params.BlockchainClient.Consensus,
@@ -218,10 +218,14 @@ func (a *Syncer) execute(ctx context.Context, request *SyncerRequest) (*SyncerRe
})
}
} else {
+ // Record heartbeat before potentially slow blockchain call
+ a.heartbeater.RecordHeartbeat(ctx)
inMetadatas, err = a.masterBlockchainClient.BatchGetBlockMetadata(ctx, request.Tag, start, end)
if err != nil {
return nil, xerrors.Errorf("failed to get metadata for blocks from %d to %d: %w", start, end-1, err)
}
+ // Record heartbeat after blockchain call
+ a.heartbeater.RecordHeartbeat(ctx)
// Check if the first block to be synced is a valid descendant of the local fork block.
// If the condition is not met, it is likely that master node experienced a block chain reorg right after
@@ -610,6 +614,9 @@ func (a *Syncer) getBlocksInParallel(
for i := 0; i < parallelism; i++ {
g.Go(func() error {
for metadata := range inputChannel {
+ // Record heartbeat before processing each block to prevent timeout
+ a.heartbeater.RecordHeartbeat(ctx)
+
block, err := a.safeGetBlock(ctx, logger, metadata, withBestEffort, dataCompression, fastSync, transactionIndexingParallelism)
if err != nil {
logger.Warn("failed to get block",
@@ -622,6 +629,9 @@ func (a *Syncer) getBlocksInParallel(
reprocessChannel <- nil
outChannel <- block
}
+
+ // Record heartbeat after processing each block
+ a.heartbeater.RecordHeartbeat(ctx)
}
return nil
})
@@ -724,7 +734,7 @@ func (a *Syncer) safeGetBlock(
}
func (a *Syncer) uploadBlockToBlobStorage(ctx context.Context, outBlock *api.Block, dataCompression api.Compression) error {
- objectKey, err := a.blobStorage.Upload(ctx, outBlock, dataCompression)
+ objectKey, err := a.singleBlockUploader.Upload(ctx, outBlock, dataCompression)
if err != nil {
return xerrors.Errorf("failed to upload block hash %s to blob storage: %w", outBlock.Metadata.Hash, err)
}
diff --git a/internal/workflow/activity/syncer_test.go b/internal/workflow/activity/syncer_test.go
index 51eee480..74161a66 100644
--- a/internal/workflow/activity/syncer_test.go
+++ b/internal/workflow/activity/syncer_test.go
@@ -41,6 +41,7 @@ type SyncerTestSuite struct {
ctrl *gomock.Controller
metaStorage *metastoragemocks.MockMetaStorage
blobStorage *blobstoragemocks.MockBlobStorage
+ singleBlockUploader *blobstoragemocks.MockSingleBlockUploader
masterBlockchainClient *clientmocks.MockClient
slaveBlockchainClient *clientmocks.MockClient
validatorBlockchainClient *clientmocks.MockClient
@@ -69,6 +70,17 @@ func (s *SyncerTestSuite) TestNoOp() {
require.Equal(uint64(100), result.canonicalChainTipHeight)
}
+func (s *SyncerTestSuite) TestRetirementFenceFailsBeforeSingleBlockUpload() {
+ require := testutil.Require(s.T())
+ block := testutil.MakeBlocksFromStartHeight(123456, 1, tag)[0]
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), block, api.Compression_NONE).
+ Return("", metastorage.ErrSingleBlockRetirementFenced)
+
+ err := s.syncer.uploadBlockToBlobStorage(context.Background(), block, api.Compression_NONE)
+ require.Error(err)
+ require.Contains(err.Error(), metastorage.ErrSingleBlockRetirementFenced.Error())
+}
+
func (s *SyncerTestSuite) TestNoReorg() {
require := testutil.Require(s.T())
@@ -99,7 +111,7 @@ func (s *SyncerTestSuite) TestNoReorg() {
require.False(s.slaveEndpointProvider.HasFailoverContext(ctx))
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
s.metaStorage.EXPECT().PersistBlockMetas(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
request := &SyncerRequest{
@@ -143,7 +155,7 @@ func (s *SyncerTestSuite) TestNoReorg_Failover() {
require.True(s.slaveEndpointProvider.HasFailoverContext(ctx))
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
s.metaStorage.EXPECT().PersistBlockMetas(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
request := &SyncerRequest{
@@ -403,7 +415,7 @@ func (s *SyncerTestSuite) TestNextMasterBlockSkipped() {
func(ctx context.Context, tag uint32, height uint64, hash string, opts ...client.ClientOption) (*api.Block, error) {
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(3)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(3)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(3)
s.metaStorage.EXPECT().PersistBlockMetas(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
request := &SyncerRequest{
@@ -445,7 +457,7 @@ func (s *SyncerTestSuite) TestLatestBlockNotSkipped() {
}
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(100)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(100)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(100)
s.metaStorage.EXPECT().PersistBlockMetas(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
request := &SyncerRequest{
@@ -473,7 +485,7 @@ func (s *SyncerTestSuite) TestFastSync() {
func(ctx context.Context, tag uint32, height uint64, opts ...client.ClientOption) (*api.Block, error) {
return testutil.MakeBlock(height, tag), nil
}).Times(100)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(100)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(100)
s.metaStorage.EXPECT().PersistBlockMetas(gomock.Any(), gomock.Any(), gomock.Any(), forkBlock).Return(nil)
request := &SyncerRequest{
@@ -504,7 +516,7 @@ func (s *SyncerTestSuite) TestFastSync_UnexpectedReorg() {
func(ctx context.Context, tag uint32, height uint64, opts ...client.ClientOption) (*api.Block, error) {
return testutil.MakeBlock(height, tag), nil
}).Times(100)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(100)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(100)
s.metaStorage.EXPECT().PersistBlockMetas(gomock.Any(), gomock.Any(), gomock.Any(), forkBlock).Return(parser.ErrInvalidChain)
request := &SyncerRequest{
@@ -776,7 +788,7 @@ func (s *SyncerTestSuite) TestAddOrUpdateTransactionsInParallel() {
}).Times(2)
s.metaStorage.EXPECT().AddTransactions(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
s.metaStorage.EXPECT().PersistBlockMetas(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
request := &SyncerRequest{
@@ -823,7 +835,7 @@ func (s *SyncerTestSuite) TestAddOrUpdateTransactionsInParallel_WithBatches() {
}).Times(2)
s.metaStorage.EXPECT().AddTransactions(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
s.metaStorage.EXPECT().PersistBlockMetas(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
request := &SyncerRequest{
@@ -879,7 +891,7 @@ func (s *SyncerTestSuite) TestAddOrUpdateTransactionsInParallel_CheckOnTransacti
return nil
})
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil)
s.metaStorage.EXPECT().PersistBlockMetas(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
request := &SyncerRequest{
@@ -931,7 +943,7 @@ func (s *SyncerTestSuite) TestAddOrUpdateTransactionsInParallel_Err() {
})
s.metaStorage.EXPECT().AddTransactions(gomock.Any(), gomock.Any(), gomock.Any()).Return(xerrors.New("failed to add or update transaction")).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
request := &SyncerRequest{
Tag: stableTag,
@@ -996,7 +1008,7 @@ func (s *SyncerTestSuite) TestConsensusValidation_Success() {
func(ctx context.Context, tag uint32, height uint64, hash string, opts ...client.ClientOption) (*api.Block, error) {
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
latestFinalizedHeight := latestHeight - irreversibleDistance + 1
s.metaStorage.EXPECT().
@@ -1049,7 +1061,7 @@ func (s *SyncerTestSuite) TestConsensusValidation_ConsensusClientFailure() {
func(ctx context.Context, tag uint32, height uint64, hash string, opts ...client.ClientOption) (*api.Block, error) {
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
latestFinalizedHeight := latestHeight - irreversibleDistance + 1
s.metaStorage.EXPECT().
@@ -1099,7 +1111,7 @@ func (s *SyncerTestSuite) TestConsensusValidation_Muted() {
func(ctx context.Context, tag uint32, height uint64, hash string, opts ...client.ClientOption) (*api.Block, error) {
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
latestFinalizedHeight := latestHeight - irreversibleDistance + 1
s.metaStorage.EXPECT().
@@ -1153,7 +1165,7 @@ func (s *SyncerTestSuite) TestConsensusValidation_BlockHashMismatch() {
func(ctx context.Context, tag uint32, height uint64, hash string, opts ...client.ClientOption) (*api.Block, error) {
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
latestFinalizedHeight := latestHeight - irreversibleDistance + 1
s.metaStorage.EXPECT().
@@ -1209,7 +1221,7 @@ func (s *SyncerTestSuite) TestConsensusValidation_NoFinalizedBlock() {
func(ctx context.Context, tag uint32, height uint64, hash string, opts ...client.ClientOption) (*api.Block, error) {
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
s.metaStorage.EXPECT().PersistBlockMetas(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil)
request := &SyncerRequest{
@@ -1260,7 +1272,7 @@ func (s *SyncerTestSuite) TestConsensusValidation_FinalizedBlock_Skipped() {
func(ctx context.Context, tag uint32, height uint64, hash string, opts ...client.ClientOption) (*api.Block, error) {
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(2)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(2)
latestFinalizedHeight := latestHeight - irreversibleDistance + 1
s.metaStorage.EXPECT().
@@ -1326,7 +1338,7 @@ func (s *SyncerTestSuite) TestConsensusValidation_NoMetadataInMetaStorage() {
func(ctx context.Context, tag uint32, height uint64, hash string, opts ...client.ClientOption) (*api.Block, error) {
return testutil.MakeBlocksFromStartHeight(height, 1, tag)[0], nil
}).Times(4)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(4)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(4)
latestFinalizedHeight := latestHeight - irreversibleDistance + 1
s.consensusBlockchainClient.EXPECT().
@@ -1393,7 +1405,7 @@ func (s *SyncerTestSuite) TestConsensusValidation_NoMetadataInMetaStorage_Skippe
}
return testutil.MakeBlocksFromStartHeight(height, 1, tag, blockSkippedOption)[0], nil
}).Times(5)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(5)
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("", nil).Times(5)
s.consensusBlockchainClient.EXPECT().
BatchGetBlockMetadata(gomock.Any(), tag, gomock.Any(), gomock.Any()).DoAndReturn(
@@ -1454,7 +1466,12 @@ func (s *SyncerTestSuite) SetupTest() {
s.ctrl = gomock.NewController(s.T())
s.blobStorage = blobstoragemocks.NewMockBlobStorage(s.ctrl)
+ s.singleBlockUploader = blobstoragemocks.NewMockSingleBlockUploader(s.ctrl)
s.metaStorage = metastoragemocks.NewMockMetaStorage(s.ctrl)
+ s.metaStorage.EXPECT().
+ AcquireSingleBlockUploadGuard(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+ Return(metastorage.NewUnfencedSingleBlockUploadGuard(), nil).
+ AnyTimes()
s.masterBlockchainClient = clientmocks.NewMockClient(s.ctrl)
s.slaveBlockchainClient = clientmocks.NewMockClient(s.ctrl)
s.validatorBlockchainClient = clientmocks.NewMockClient(s.ctrl)
@@ -1474,6 +1491,9 @@ func (s *SyncerTestSuite) SetupTest() {
fx.Provide(func() blobstorage.BlobStorage {
return s.blobStorage
}),
+ fx.Provide(func() blobstorage.SingleBlockUploader {
+ return s.singleBlockUploader
+ }),
fx.Provide(func() metastorage.MetaStorage {
return s.metaStorage
}),
diff --git a/internal/workflow/backfiller.go b/internal/workflow/backfiller.go
index 2d239069..17fc4729 100644
--- a/internal/workflow/backfiller.go
+++ b/internal/workflow/backfiller.go
@@ -78,7 +78,11 @@ func NewBackfiller(params BackfillerParams) *Backfiller {
}
func (w *Backfiller) Execute(ctx context.Context, request *BackfillerRequest) (client.WorkflowRun, error) {
- return w.startWorkflow(ctx, w.name, request)
+ workflowId := w.name
+ if override, ok := workflowIDFromContext(ctx); ok {
+ workflowId = override
+ }
+ return w.startWorkflow(ctx, workflowId, request)
}
func (w *Backfiller) execute(ctx workflow.Context, request *BackfillerRequest) error {
diff --git a/internal/workflow/backfiller_test.go b/internal/workflow/backfiller_test.go
index 03814751..d4d6ea81 100644
--- a/internal/workflow/backfiller_test.go
+++ b/internal/workflow/backfiller_test.go
@@ -17,7 +17,6 @@ import (
"github.com/coinbase/chainstorage/internal/blockchain/client"
clientmocks "github.com/coinbase/chainstorage/internal/blockchain/client/mocks"
"github.com/coinbase/chainstorage/internal/blockchain/endpoints"
- "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
"github.com/coinbase/chainstorage/internal/cadence"
"github.com/coinbase/chainstorage/internal/config"
"github.com/coinbase/chainstorage/internal/dlq"
@@ -48,6 +47,7 @@ type backfillerTestSuite struct {
blockchainClient *clientmocks.MockClient
metadataAccessor *metastoragemocks.MockMetaStorage
blobStorage *blobstoragemocks.MockBlobStorage
+ singleBlockUploader *blobstoragemocks.MockSingleBlockUploader
masterEndpointProvider endpoints.EndpointProvider
slaveEndpointProvider endpoints.EndpointProvider
backfiller *Backfiller
@@ -96,7 +96,12 @@ func (s *backfillerTestSuite) SetupTest() {
s.ctrl = gomock.NewController(s.T())
s.blockchainClient = clientmocks.NewMockClient(s.ctrl)
s.metadataAccessor = metastoragemocks.NewMockMetaStorage(s.ctrl)
+ s.metadataAccessor.EXPECT().
+ AcquireSingleBlockUploadGuard(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+ Return(nil, nil).
+ AnyTimes()
s.blobStorage = blobstoragemocks.NewMockBlobStorage(s.ctrl)
+ s.singleBlockUploader = blobstoragemocks.NewMockSingleBlockUploader(s.ctrl)
s.app = testapp.New(
s.T(),
Module,
@@ -105,6 +110,9 @@ func (s *backfillerTestSuite) SetupTest() {
fx.Provide(func() blobstorage.BlobStorage {
return s.blobStorage
}),
+ fx.Provide(func() blobstorage.SingleBlockUploader {
+ return s.singleBlockUploader
+ }),
fx.Provide(fx.Annotated{
Name: "slave",
Target: func() client.Client {
@@ -144,7 +152,7 @@ func (s *backfillerTestSuite) TestBackfiller() {
require.Equal(tag, tag_)
return s.getMockMetadata(height), nil
})
- s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), gomock.Any(), gomock.Any()).
+ s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Times(int(endHeight - startHeight)).
DoAndReturn(func(ctx context.Context, tag_ uint32, height uint64, opts ...client.ClientOption) (*api.Block, error) {
require.False(s.masterEndpointProvider.HasFailoverContext(ctx))
@@ -157,7 +165,7 @@ func (s *backfillerTestSuite) TestBackfiller() {
}, nil
})
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).
Times(int(endHeight - startHeight)).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
@@ -212,7 +220,7 @@ func (s *backfillerTestSuite) TestBackfiller_MiniBatch() {
require.Equal(tag, tag_)
return s.getMockMetadata(height), nil
})
- s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), gomock.Any(), gomock.Any()).
+ s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Times(int(endHeight - startHeight)).
DoAndReturn(func(ctx context.Context, tag_ uint32, height uint64, opts ...client.ClientOption) (*api.Block, error) {
require.False(s.masterEndpointProvider.HasFailoverContext(ctx))
@@ -225,7 +233,7 @@ func (s *backfillerTestSuite) TestBackfiller_MiniBatch() {
}, nil
})
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).
Times(int(endHeight - startHeight)).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
@@ -282,9 +290,9 @@ func (s *backfillerTestSuite) TestBackfiller_Failover() {
require.Equal(tag, tag_)
return s.getMockMetadata(height), nil
})
- s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), gomock.Any(), gomock.Any()).
+ s.blockchainClient.EXPECT().GetBlockByHeight(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Times(int(endHeight - startHeight)).
- DoAndReturn(func(ctx context.Context, tag_ uint32, height uint64, opts ...jsonrpc.Option) (*api.Block, error) {
+ DoAndReturn(func(ctx context.Context, tag_ uint32, height uint64, opts ...client.ClientOption) (*api.Block, error) {
require.True(s.masterEndpointProvider.HasFailoverContext(ctx))
require.True(s.slaveEndpointProvider.HasFailoverContext(ctx))
require.Equal(tag, tag_)
@@ -295,7 +303,7 @@ func (s *backfillerTestSuite) TestBackfiller_Failover() {
}, nil
})
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).
Times(int(endHeight - startHeight)).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
diff --git a/internal/workflow/batch_consolidator.go b/internal/workflow/batch_consolidator.go
new file mode 100644
index 00000000..d76fc9b5
--- /dev/null
+++ b/internal/workflow/batch_consolidator.go
@@ -0,0 +1,1298 @@
+package workflow
+
+import (
+ "context"
+ "crypto/sha256"
+ "encoding/hex"
+ "strconv"
+
+ "go.temporal.io/sdk/client"
+ "go.temporal.io/sdk/temporal"
+ "go.temporal.io/sdk/workflow"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ "github.com/coinbase/chainstorage/internal/workflow/activity"
+)
+
+type (
+ BatchConsolidator struct {
+ baseWorkflow
+ batchConsolidator *activity.BatchConsolidator
+ }
+
+ BatchConsolidatorParams struct {
+ fx.In
+ fxparams.Params
+ Runtime cadence.Runtime
+ BatchConsolidator *activity.BatchConsolidator
+ }
+
+ BatchConsolidatorRequest struct {
+ Mode config.ConsolidationMode `validate:"omitempty,oneof=shadow_dual_write auto_consolidate historical_backfill repair_existing_cscb"`
+ Tag uint32
+ StartHeight uint64
+ EndHeight uint64 `validate:"gtfield=StartHeight"`
+ BatchSize uint64
+ CheckpointSize uint64
+ MaxBlocks uint64
+ Parallelism int `validate:"omitempty,gt=0"`
+ }
+
+ batchConsolidatorPendingActivity struct {
+ request *activity.BatchConsolidatorRequest
+ future workflow.Future
+ }
+)
+
+var (
+ _ InstrumentedRequest = (*BatchConsolidatorRequest)(nil)
+)
+
+const (
+ batchConsolidatorHeightGauge = "workflow.batch_consolidator.height"
+ batchConsolidatorObjectCounter = "workflow.batch_consolidator.object"
+ batchConsolidatorConsolidatedBlockCounter = "workflow.batch_consolidator.consolidated_block"
+ batchConsolidatorEmptyBatchCounter = "workflow.batch_consolidator.empty_batch"
+ batchConsolidatorShadowStatsChangeID = "batch-consolidator-shadow-stats"
+ batchConsolidatorShadowStatsVersion = 1
+ batchConsolidatorHistoricalParallelismChangeID = "batch-consolidator-historical-parallelism"
+ batchConsolidatorHistoricalParallelismVersion = 1
+ batchConsolidatorAutoSerialChangeID = "batch-consolidator-auto-serial"
+ batchConsolidatorAutoSerialVersion = 1
+ batchConsolidatorAutoCursorChangeID = "batch-consolidator-auto-cursor"
+ batchConsolidatorAutoCursorVersion = 1
+ batchConsolidatorAutoParallelismChangeID = "batch-consolidator-auto-parallelism"
+ batchConsolidatorAutoParallelismVersion = 1
+ batchConsolidatorRepairParallelismChangeID = "batch-consolidator-repair-parallelism"
+ batchConsolidatorRepairParallelismVersion = 1
+ batchConsolidatorPreviousMaxParallelism = 10
+ batchConsolidatorMaxParallelism = 20
+ maxUint64 = ^uint64(0)
+)
+
+func NewBatchConsolidator(params BatchConsolidatorParams) *BatchConsolidator {
+ w := &BatchConsolidator{
+ baseWorkflow: newBaseWorkflow(¶ms.Config.Workflows.BatchConsolidator, params.Runtime),
+ batchConsolidator: params.BatchConsolidator,
+ }
+ w.registerWorkflow(w.execute)
+ return w
+}
+
+func (w *BatchConsolidator) Execute(ctx context.Context, request *BatchConsolidatorRequest) (client.WorkflowRun, error) {
+ workflowID := w.name
+ if override, ok := workflowIDFromContext(ctx); ok {
+ workflowID = override
+ }
+ return w.startWorkflow(ctx, workflowID, request)
+}
+
+func (r *BatchConsolidatorRequest) GetTags() map[string]string {
+ return map[string]string{
+ tagBlockTag: strconv.Itoa(int(r.Tag)),
+ }
+}
+
+func (w *BatchConsolidator) execute(ctx workflow.Context, request *BatchConsolidatorRequest) error {
+ return w.executeWorkflow(ctx, request, func() error {
+ var cfg config.BatchConsolidatorWorkflowConfig
+ if err := w.readConfig(ctx, &cfg); err != nil {
+ return xerrors.Errorf("failed to read config: %w", err)
+ }
+
+ batchSize := cfg.BatchSize
+ if request.BatchSize > 0 {
+ batchSize = request.BatchSize
+ }
+ checkpointSize := cfg.CheckpointSize
+ if request.CheckpointSize > 0 {
+ checkpointSize = request.CheckpointSize
+ }
+ maxBlocks := cfg.MaxBlocks
+ if request.MaxBlocks > 0 {
+ maxBlocks = request.MaxBlocks
+ }
+ parallelism := 1
+ if request.Parallelism > 0 {
+ parallelism = request.Parallelism
+ }
+ if parallelism > batchConsolidatorMaxParallelism {
+ return xerrors.Errorf("batch_consolidator parallelism(%d) exceeds max(%d)", parallelism, batchConsolidatorMaxParallelism)
+ }
+ mode, err := batchConsolidatorMode(request.Mode, cfg.Storage.Consolidation.Mode)
+ if err != nil {
+ return err
+ }
+ storageMaxBlocks := cfg.Storage.Consolidation.MaxBlocks
+ if storageMaxBlocks == 0 {
+ return xerrors.New("batch_consolidator storage consolidation max_blocks must be positive")
+ }
+ if maxBlocks == 0 || maxBlocks > storageMaxBlocks {
+ maxBlocks = storageMaxBlocks
+ }
+ shardSize := cfg.Storage.Consolidation.ShardSize
+ if batchSize == 0 {
+ return xerrors.New("batch_consolidator batch_size must be positive")
+ }
+ if checkpointSize <= batchSize {
+ return xerrors.Errorf("batch_consolidator checkpoint_size(%d) must be greater than batch_size(%d)", checkpointSize, batchSize)
+ }
+ if maxBlocks == 0 {
+ return xerrors.New("batch_consolidator max_blocks must be positive")
+ }
+ if shardSize == 0 {
+ return xerrors.New("batch_consolidator storage consolidation shard_size must be positive")
+ }
+ tag := cfg.GetEffectiveBlockTag(request.Tag)
+ metrics := w.getMetricsHandler(ctx).WithTags(map[string]string{
+ tagBlockTag: strconv.Itoa(int(tag)),
+ })
+ logger := w.getLogger(ctx).With(
+ zap.Reflect("request", request),
+ zap.Reflect("config", cfg),
+ zap.Uint32("effective_tag", tag),
+ zap.Uint64("batch_size", batchSize),
+ zap.Uint64("checkpoint_size", checkpointSize),
+ zap.Uint64("max_blocks", maxBlocks),
+ zap.Uint64("storage_max_blocks", storageMaxBlocks),
+ zap.Uint64("shard_size", shardSize),
+ zap.Int("parallelism", parallelism),
+ zap.String("mode", string(mode)),
+ )
+ logger.Info("workflow started")
+ ctx = w.withActivityOptions(ctx)
+ statsCtx := w.withShadowStatsActivityOptions(ctx, cfg)
+ cursorCtx := w.withCursorActivityOptions(ctx, cfg)
+ if mode.IsRepairExistingCSCB() {
+ repairParallelismVersion := workflow.GetVersion(
+ ctx,
+ batchConsolidatorRepairParallelismChangeID,
+ workflow.DefaultVersion,
+ batchConsolidatorRepairParallelismVersion,
+ )
+ if repairParallelismVersion == workflow.DefaultVersion {
+ if parallelism != 1 {
+ return xerrors.Errorf("legacy repair_existing_cscb execution requires parallelism=1, got %d", parallelism)
+ }
+ }
+ if err := w.validateHistoricalBackfillRange(ctx, logger, tag, request.StartHeight, request.EndHeight, cfg.IrreversibleDistance); err != nil {
+ return err
+ }
+ if repairParallelismVersion == workflow.DefaultVersion {
+ return w.executeRepairExistingCSCB(
+ ctx,
+ logger,
+ metrics,
+ request,
+ tag,
+ checkpointSize,
+ maxBlocks,
+ )
+ }
+ return w.executeRepairExistingCSCBParallel(
+ ctx,
+ logger,
+ metrics,
+ request,
+ tag,
+ checkpointSize,
+ maxBlocks,
+ parallelism,
+ )
+ }
+ usePersistedShadowStats := workflow.GetVersion(
+ ctx,
+ batchConsolidatorShadowStatsChangeID,
+ workflow.DefaultVersion,
+ batchConsolidatorShadowStatsVersion,
+ ) != workflow.DefaultVersion && mode != config.ConsolidationModePromoteFinalized
+ historicalBackfillVersion := workflow.DefaultVersion
+ if mode == config.ConsolidationModeHistoricalBackfill {
+ historicalBackfillVersion = workflow.GetVersion(
+ ctx,
+ batchConsolidatorHistoricalParallelismChangeID,
+ workflow.DefaultVersion,
+ batchConsolidatorHistoricalParallelismVersion,
+ )
+ }
+ autoConsolidateVersion := workflow.DefaultVersion
+ if mode.IsAutoConsolidate() {
+ autoConsolidateVersion = workflow.GetVersion(
+ ctx,
+ batchConsolidatorAutoSerialChangeID,
+ workflow.DefaultVersion,
+ batchConsolidatorAutoSerialVersion,
+ )
+ }
+ autoConsolidateCursorVersion := workflow.DefaultVersion
+ if mode.IsAutoConsolidate() {
+ autoConsolidateCursorVersion = workflow.GetVersion(
+ ctx,
+ batchConsolidatorAutoCursorChangeID,
+ workflow.DefaultVersion,
+ batchConsolidatorAutoCursorVersion,
+ )
+ }
+ autoConsolidateParallelismVersion := workflow.DefaultVersion
+ if mode.IsAutoConsolidate() {
+ autoConsolidateParallelismVersion = workflow.GetVersion(
+ ctx,
+ batchConsolidatorAutoParallelismChangeID,
+ workflow.DefaultVersion,
+ batchConsolidatorAutoParallelismVersion,
+ )
+ }
+
+ if historicalBackfillVersion != workflow.DefaultVersion {
+ if err := w.validateHistoricalBackfillRange(ctx, logger, tag, request.StartHeight, request.EndHeight, cfg.IrreversibleDistance); err != nil {
+ return err
+ }
+ }
+ if mode.IsAutoConsolidate() {
+ if err := w.validateAutoConsolidateRange(ctx, logger, tag, request.StartHeight, request.EndHeight, cfg.IrreversibleDistance); err != nil {
+ return err
+ }
+ if autoConsolidateCursorVersion != workflow.DefaultVersion &&
+ !batchConsolidatorAutoRangeHasOnlyFullObjectWindows(request.StartHeight, request.EndHeight, batchSize, maxBlocks, shardSize) {
+ return xerrors.Errorf(
+ "auto_consolidate range [%d, %d) must contain only full object windows of max_blocks=%d",
+ request.StartHeight,
+ request.EndHeight,
+ maxBlocks,
+ )
+ }
+ }
+
+ workflowEndHeight := request.EndHeight
+ if mode == config.ConsolidationModePromoteFinalized {
+ plan, err := w.batchConsolidator.GetPromotionPlan(ctx, &activity.BatchConsolidatorPlanRequest{
+ Tag: tag,
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to plan promote_finalized range: %w", err)
+ }
+ workflowEndHeight = plan.EndHeight
+ logger.Info(
+ "planned promote_finalized range",
+ zap.Uint64("requested_end_height", request.EndHeight),
+ zap.Uint64("effective_end_height", workflowEndHeight),
+ zap.Uint64("latest_height", plan.LatestHeight),
+ zap.Uint64("safe_promotion_height", plan.SafePromotionHeight),
+ zap.Uint64("promotion_gate_height", plan.PromotionGateHeight),
+ )
+ if workflowEndHeight <= request.StartHeight {
+ logger.Info("promote_finalized range has no currently safe heights")
+ return nil
+ }
+ }
+
+ // Keep default shadow activity payloads unchanged for existing workflow history replay.
+ activityMode := config.ConsolidationMode("")
+ if request.Mode != "" {
+ activityMode = mode
+ }
+
+ for batchStart := request.StartHeight; batchStart < workflowEndHeight; {
+ if batchStart-request.StartHeight >= checkpointSize {
+ newRequest := *request
+ newRequest.StartHeight = batchStart
+ logger.Info("checkpoint reached", zap.Reflect("newRequest", newRequest))
+ return w.continueAsNew(ctx, &newRequest)
+ }
+
+ batchEnd := batchConsolidatorWindowEnd(batchStart, workflowEndHeight, batchSize, shardSize)
+ if mode == config.ConsolidationModeHistoricalBackfill && parallelism > batchConsolidatorPreviousMaxParallelism {
+ // Parallelism above the previous hard limit cannot exist in an old history.
+ // Queue work across shards while keeping each CSCB object shard-bounded below.
+ batchEnd = batchConsolidatorBatchEnd(batchStart, workflowEndHeight, batchSize)
+ }
+ if batchEnd <= batchStart {
+ return xerrors.Errorf("batch_consolidator made no height progress from %d to %d", batchStart, batchEnd)
+ }
+
+ if historicalBackfillVersion != workflow.DefaultVersion && parallelism > 1 {
+ objectsInBatch, blocksInBatch, err := w.processHistoricalBackfillBatchParallel(
+ ctx,
+ statsCtx,
+ logger,
+ metrics,
+ tag,
+ activityMode,
+ batchStart,
+ batchEnd,
+ maxBlocks,
+ shardSize,
+ parallelism,
+ usePersistedShadowStats,
+ )
+ if err != nil {
+ return err
+ }
+ metrics.Gauge(batchConsolidatorHeightGauge).Update(float64(batchEnd - 1))
+ logger.Info(
+ "processed shadow batch",
+ zap.Uint64("batch_start", batchStart),
+ zap.Uint64("batch_end", batchEnd),
+ zap.Uint64("objects", objectsInBatch),
+ zap.Uint64("consolidated_blocks", blocksInBatch),
+ )
+ batchStart = batchEnd
+ continue
+ }
+
+ if mode.IsAutoConsolidate() &&
+ (autoConsolidateCursorVersion != workflow.DefaultVersion || autoConsolidateVersion == workflow.DefaultVersion) {
+ autoConsolidateParallelism := 1
+ if autoConsolidateCursorVersion == workflow.DefaultVersion ||
+ autoConsolidateParallelismVersion != workflow.DefaultVersion {
+ autoConsolidateParallelism = parallelism
+ }
+ objectsInBatch, blocksInBatch, err := w.processAutoConsolidateBatchParallel(
+ ctx,
+ statsCtx,
+ logger,
+ metrics,
+ tag,
+ activityMode,
+ batchStart,
+ batchEnd,
+ maxBlocks,
+ autoConsolidateParallelism,
+ usePersistedShadowStats,
+ )
+ if err != nil {
+ return err
+ }
+ metrics.Gauge(batchConsolidatorHeightGauge).Update(float64(batchEnd - 1))
+ logger.Info(
+ "processed shadow batch",
+ zap.Uint64("batch_start", batchStart),
+ zap.Uint64("batch_end", batchEnd),
+ zap.Uint64("objects", objectsInBatch),
+ zap.Uint64("consolidated_blocks", blocksInBatch),
+ )
+ batchStart = batchEnd
+ continue
+ }
+
+ objectsInBatch := uint64(0)
+ blocksInBatch := uint64(0)
+ lastShadowObjects := uint64(0)
+ lastShadowBlocks := uint64(0)
+ if usePersistedShadowStats {
+ baseline, err := w.batchConsolidator.GetShadowStats(statsCtx, &activity.BatchConsolidatorStatsRequest{
+ Mode: activityMode,
+ Tag: tag,
+ StartHeight: batchStart,
+ EndHeight: batchEnd,
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to get consolidation shadow stats for batch [%d, %d): %w", batchStart, batchEnd, err)
+ }
+ lastShadowObjects = baseline.ShadowObjects
+ lastShadowBlocks = baseline.ShadowBlocks
+ }
+ for {
+ response, err := w.batchConsolidator.Execute(ctx, &activity.BatchConsolidatorRequest{
+ Mode: activityMode,
+ Tag: tag,
+ StartHeight: batchStart,
+ EndHeight: batchEnd,
+ MaxBlocks: maxBlocks,
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to consolidate shadow batch [%d, %d): %w", batchStart, batchEnd, err)
+ }
+ newObjects := uint64(0)
+ newBlocks := uint64(0)
+ if usePersistedShadowStats {
+ stats, err := w.batchConsolidator.GetShadowStats(statsCtx, &activity.BatchConsolidatorStatsRequest{
+ Mode: activityMode,
+ Tag: tag,
+ StartHeight: batchStart,
+ EndHeight: batchEnd,
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to get consolidation shadow stats for batch [%d, %d): %w", batchStart, batchEnd, err)
+ }
+ if stats.ShadowObjects < lastShadowObjects || stats.ShadowBlocks < lastShadowBlocks {
+ return xerrors.Errorf(
+ "batch_consolidator shadow stats regressed for batch [%d, %d): objects %d -> %d, blocks %d -> %d",
+ batchStart,
+ batchEnd,
+ lastShadowObjects,
+ stats.ShadowObjects,
+ lastShadowBlocks,
+ stats.ShadowBlocks,
+ )
+ }
+ newObjects = stats.ShadowObjects - lastShadowObjects
+ newBlocks = stats.ShadowBlocks - lastShadowBlocks
+ lastShadowObjects = stats.ShadowObjects
+ lastShadowBlocks = stats.ShadowBlocks
+ }
+ if response.ScannedBlocks == 0 {
+ if newObjects > 0 {
+ objectsInBatch += newObjects
+ metrics.Counter(batchConsolidatorObjectCounter).Inc(int64(newObjects))
+ }
+ if newBlocks > 0 {
+ blocksInBatch += newBlocks
+ metrics.Counter(batchConsolidatorConsolidatedBlockCounter).Inc(int64(newBlocks))
+ }
+ metrics.Counter(batchConsolidatorEmptyBatchCounter).Inc(1)
+ break
+ }
+ if response.ConsolidatedBlocks == 0 {
+ return xerrors.Errorf("batch_consolidator made no progress for non-empty shadow scan [%d, %d)", batchStart, batchEnd)
+ }
+ if !usePersistedShadowStats {
+ newObjects = 1
+ newBlocks = response.ConsolidatedBlocks
+ }
+ if newObjects > 0 {
+ objectsInBatch += newObjects
+ metrics.Counter(batchConsolidatorObjectCounter).Inc(int64(newObjects))
+ }
+ if newBlocks > 0 {
+ blocksInBatch += newBlocks
+ metrics.Counter(batchConsolidatorConsolidatedBlockCounter).Inc(int64(newBlocks))
+ }
+ logger.Info(
+ "processed shadow object",
+ zap.Uint64("batch_start", batchStart),
+ zap.Uint64("batch_end", batchEnd),
+ zap.Uint64("scanned_blocks", response.ScannedBlocks),
+ zap.Uint64("consolidated_blocks", response.ConsolidatedBlocks),
+ zap.Uint64("new_objects", newObjects),
+ zap.Uint64("new_consolidated_blocks", newBlocks),
+ zap.Uint64("shadow_objects", lastShadowObjects),
+ zap.Uint64("shadow_blocks", lastShadowBlocks),
+ zap.String("object_key", response.ObjectKey),
+ )
+ if response.ScannedBlocks < maxBlocks {
+ break
+ }
+ }
+ metrics.Gauge(batchConsolidatorHeightGauge).Update(float64(batchEnd - 1))
+ logger.Info(
+ "processed shadow batch",
+ zap.Uint64("batch_start", batchStart),
+ zap.Uint64("batch_end", batchEnd),
+ zap.Uint64("objects", objectsInBatch),
+ zap.Uint64("consolidated_blocks", blocksInBatch),
+ )
+ batchStart = batchEnd
+ }
+
+ if mode.IsAutoConsolidate() && autoConsolidateCursorVersion != workflow.DefaultVersion {
+ if _, err := w.batchConsolidator.UpdateAutoConsolidateCursor(cursorCtx, &activity.BatchConsolidatorCursorRequest{
+ Tag: tag,
+ Height: workflowEndHeight,
+ }); err != nil {
+ return xerrors.Errorf("failed to update auto_consolidate cursor to height %d: %w", workflowEndHeight, err)
+ }
+ logger.Info("updated auto_consolidate cursor", zap.Uint32("tag", tag), zap.Uint64("height", workflowEndHeight))
+ }
+
+ logger.Info("workflow finished")
+ return nil
+ })
+}
+
+func (w *BatchConsolidator) executeRepairExistingCSCB(
+ ctx workflow.Context,
+ logger *zap.Logger,
+ metrics client.MetricsHandler,
+ request *BatchConsolidatorRequest,
+ tag uint32,
+ checkpointSize uint64,
+ maxBlocks uint64,
+) error {
+ processedBlocks := uint64(0)
+ repairIteration := uint64(0)
+ for {
+ response, err := w.batchConsolidator.Execute(ctx, &activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: tag,
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ MaxBlocks: maxBlocks,
+ RepairExecutionKey: makeRepairExecutionKey(ctx, repairIteration),
+ })
+ if err != nil {
+ return xerrors.Errorf(
+ "failed to repair existing CSCB range [%d, %d): %w",
+ request.StartHeight,
+ request.EndHeight,
+ err,
+ )
+ }
+ if response.RepairedObjects == 0 {
+ metrics.Counter(batchConsolidatorEmptyBatchCounter).Inc(1)
+ logger.Info("CSCB repair range is complete")
+ return nil
+ }
+ if response.ScannedBlocks == 0 {
+ return xerrors.Errorf(
+ "repair_existing_cscb made no block progress for old object %q",
+ response.OldObjectKey,
+ )
+ }
+ processedBlocks += response.ScannedBlocks
+ metrics.Counter(batchConsolidatorObjectCounter).Inc(int64(response.RepairedObjects))
+ metrics.Counter(batchConsolidatorConsolidatedBlockCounter).Inc(int64(response.ConsolidatedBlocks))
+ metrics.Gauge(batchConsolidatorHeightGauge).Update(float64(response.EndHeight - 1))
+ logger.Info(
+ "repaired existing CSCB object",
+ zap.String("old_object_key", response.OldObjectKey),
+ zap.String("new_object_key", response.ObjectKey),
+ zap.Uint64("start_height", response.StartHeight),
+ zap.Uint64("end_height", response.EndHeight),
+ zap.Uint64("blocks", response.ScannedBlocks),
+ zap.Uint64("canonical_blocks", response.ConsolidatedBlocks),
+ zap.Bool("rebuilt_cscb", response.ObjectKey != "" && response.ObjectKey != response.OldObjectKey),
+ )
+ repairIteration++
+ if processedBlocks >= checkpointSize {
+ newRequest := *request
+ logger.Info(
+ "CSCB repair checkpoint reached",
+ zap.Uint64("processed_blocks", processedBlocks),
+ zap.Reflect("new_request", newRequest),
+ )
+ return w.continueAsNew(ctx, &newRequest)
+ }
+ }
+}
+
+func (w *BatchConsolidator) executeRepairExistingCSCBParallel(
+ ctx workflow.Context,
+ logger *zap.Logger,
+ metrics client.MetricsHandler,
+ request *BatchConsolidatorRequest,
+ tag uint32,
+ checkpointSize uint64,
+ maxBlocks uint64,
+ parallelism int,
+) error {
+ processedBlocks := uint64(0)
+ repairIteration := uint64(0)
+ for {
+ candidates, err := w.batchConsolidator.GetRepairCandidates(ctx, &activity.BatchConsolidatorRepairCandidatesRequest{
+ Tag: tag,
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ Limit: parallelism,
+ })
+ if err != nil {
+ return xerrors.Errorf(
+ "failed to list existing CSCB repair candidates in range [%d, %d): %w",
+ request.StartHeight,
+ request.EndHeight,
+ err,
+ )
+ }
+ if len(candidates.ObjectKeys) == 0 {
+ metrics.Counter(batchConsolidatorEmptyBatchCounter).Inc(1)
+ logger.Info("CSCB repair range is complete")
+ return nil
+ }
+ if len(candidates.ObjectKeys) > parallelism {
+ return xerrors.Errorf(
+ "CSCB repair candidate list exceeds parallelism: candidates=%d parallelism=%d",
+ len(candidates.ObjectKeys),
+ parallelism,
+ )
+ }
+
+ pending := make([]batchConsolidatorPendingActivity, 0, len(candidates.ObjectKeys))
+ seen := make(map[string]struct{}, len(candidates.ObjectKeys))
+ for _, objectKey := range candidates.ObjectKeys {
+ if objectKey == "" {
+ return xerrors.New("CSCB repair candidate list contains an empty object key")
+ }
+ if _, ok := seen[objectKey]; ok {
+ return xerrors.Errorf("CSCB repair candidate list contains duplicate object key %q", objectKey)
+ }
+ seen[objectKey] = struct{}{}
+ activityRequest := &activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: tag,
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ MaxBlocks: maxBlocks,
+ RepairExecutionKey: makeRepairExecutionKey(ctx, repairIteration),
+ RepairObjectKey: objectKey,
+ }
+ pending = append(pending, batchConsolidatorPendingActivity{
+ request: activityRequest,
+ future: workflow.ExecuteActivity(ctx, activity.ActivityBatchConsolidator, activityRequest),
+ })
+ repairIteration++
+ }
+
+ var firstErr error
+ for _, pendingActivity := range pending {
+ var response activity.BatchConsolidatorResponse
+ if err := pendingActivity.future.Get(ctx, &response); err != nil {
+ if firstErr == nil {
+ firstErr = xerrors.Errorf(
+ "failed to repair existing CSCB object %q: %w",
+ pendingActivity.request.RepairObjectKey,
+ err,
+ )
+ }
+ continue
+ }
+ if response.RepairedObjects != 1 {
+ if firstErr == nil {
+ firstErr = xerrors.Errorf(
+ "repair_existing_cscb returned repaired_objects=%d for object %q",
+ response.RepairedObjects,
+ pendingActivity.request.RepairObjectKey,
+ )
+ }
+ continue
+ }
+ if response.OldObjectKey != pendingActivity.request.RepairObjectKey {
+ if firstErr == nil {
+ firstErr = xerrors.Errorf(
+ "repair_existing_cscb returned unexpected old object: expected=%q actual=%q",
+ pendingActivity.request.RepairObjectKey,
+ response.OldObjectKey,
+ )
+ }
+ continue
+ }
+ if response.ScannedBlocks == 0 {
+ if firstErr == nil {
+ firstErr = xerrors.Errorf(
+ "repair_existing_cscb made no block progress for old object %q",
+ response.OldObjectKey,
+ )
+ }
+ continue
+ }
+
+ processedBlocks += response.ScannedBlocks
+ metrics.Counter(batchConsolidatorObjectCounter).Inc(int64(response.RepairedObjects))
+ metrics.Counter(batchConsolidatorConsolidatedBlockCounter).Inc(int64(response.ConsolidatedBlocks))
+ metrics.Gauge(batchConsolidatorHeightGauge).Update(float64(response.EndHeight - 1))
+ logger.Info(
+ "repaired existing CSCB object",
+ zap.String("old_object_key", response.OldObjectKey),
+ zap.String("new_object_key", response.ObjectKey),
+ zap.Uint64("start_height", response.StartHeight),
+ zap.Uint64("end_height", response.EndHeight),
+ zap.Uint64("blocks", response.ScannedBlocks),
+ zap.Uint64("canonical_blocks", response.ConsolidatedBlocks),
+ zap.Bool("rebuilt_cscb", response.ObjectKey != "" && response.ObjectKey != response.OldObjectKey),
+ )
+ }
+ if firstErr != nil {
+ return firstErr
+ }
+ if processedBlocks >= checkpointSize {
+ newRequest := *request
+ logger.Info(
+ "CSCB repair checkpoint reached",
+ zap.Uint64("processed_blocks", processedBlocks),
+ zap.Reflect("new_request", newRequest),
+ )
+ return w.continueAsNew(ctx, &newRequest)
+ }
+ }
+}
+
+func makeRepairExecutionKey(ctx workflow.Context, iteration uint64) string {
+ info := workflow.GetInfo(ctx)
+ payload := info.WorkflowExecution.ID + "\x00" +
+ info.WorkflowExecution.RunID + "\x00" +
+ strconv.FormatUint(iteration, 10)
+ digest := sha256.Sum256([]byte(payload))
+ return hex.EncodeToString(digest[:])
+}
+
+func (w *BatchConsolidator) processAutoConsolidateBatchParallel(
+ ctx workflow.Context,
+ statsCtx workflow.Context,
+ logger *zap.Logger,
+ metrics client.MetricsHandler,
+ tag uint32,
+ activityMode config.ConsolidationMode,
+ batchStart uint64,
+ batchEnd uint64,
+ maxBlocks uint64,
+ parallelism int,
+ usePersistedShadowStats bool,
+) (uint64, uint64, error) {
+ lastShadowObjects := uint64(0)
+ lastShadowBlocks := uint64(0)
+ if usePersistedShadowStats {
+ baseline, err := w.batchConsolidator.GetShadowStats(statsCtx, &activity.BatchConsolidatorStatsRequest{
+ Mode: activityMode,
+ Tag: tag,
+ StartHeight: batchStart,
+ EndHeight: batchEnd,
+ })
+ if err != nil {
+ return 0, 0, xerrors.Errorf("failed to get consolidation shadow stats for batch [%d, %d): %w", batchStart, batchEnd, err)
+ }
+ lastShadowObjects = baseline.ShadowObjects
+ lastShadowBlocks = baseline.ShadowBlocks
+ }
+
+ objectsInBatch := uint64(0)
+ blocksInBatch := uint64(0)
+ for windowStart := batchStart; windowStart < batchEnd; {
+ pending := make([]batchConsolidatorPendingActivity, 0, parallelism)
+ for len(pending) < parallelism && windowStart < batchEnd {
+ windowEnd, fullWindow := batchConsolidatorFullObjectWindowEnd(windowStart, batchEnd, maxBlocks)
+ if !fullWindow {
+ logger.Info(
+ "auto consolidate waiting for a full object window",
+ zap.Uint64("window_start", windowStart),
+ zap.Uint64("batch_end", batchEnd),
+ zap.Uint64("max_blocks", maxBlocks),
+ )
+ break
+ }
+ request := &activity.BatchConsolidatorRequest{
+ Mode: activityMode,
+ Tag: tag,
+ StartHeight: windowStart,
+ EndHeight: windowEnd,
+ MaxBlocks: maxBlocks,
+ }
+ pending = append(pending, batchConsolidatorPendingActivity{
+ request: request,
+ future: workflow.ExecuteActivity(ctx, activity.ActivityBatchConsolidator, request),
+ })
+ windowStart = windowEnd
+ }
+ if len(pending) == 0 {
+ break
+ }
+
+ var firstErr error
+ type successResult struct {
+ request *activity.BatchConsolidatorRequest
+ response activity.BatchConsolidatorResponse
+ }
+ successes := make([]successResult, 0, len(pending))
+ for _, item := range pending {
+ var response activity.BatchConsolidatorResponse
+ request := item.request
+ if err := item.future.Get(ctx, &response); err != nil {
+ if firstErr == nil {
+ firstErr = xerrors.Errorf("failed to consolidate shadow batch [%d, %d): %w", request.StartHeight, request.EndHeight, err)
+ }
+ continue
+ }
+ successes = append(successes, successResult{request: request, response: response})
+ }
+ if firstErr != nil {
+ return 0, 0, firstErr
+ }
+
+ for _, success := range successes {
+ request := success.request
+ response := success.response
+ if response.ScannedBlocks == 0 {
+ metrics.Counter(batchConsolidatorEmptyBatchCounter).Inc(1)
+ continue
+ }
+ if response.ConsolidatedBlocks == 0 {
+ return 0, 0, xerrors.Errorf(
+ "batch_consolidator made no progress for non-empty shadow scan [%d, %d)",
+ request.StartHeight,
+ request.EndHeight,
+ )
+ }
+
+ newObjects := uint64(0)
+ newBlocks := uint64(0)
+ if !usePersistedShadowStats {
+ newObjects = 1
+ newBlocks = response.ConsolidatedBlocks
+ objectsInBatch += newObjects
+ blocksInBatch += newBlocks
+ metrics.Counter(batchConsolidatorObjectCounter).Inc(int64(newObjects))
+ metrics.Counter(batchConsolidatorConsolidatedBlockCounter).Inc(int64(newBlocks))
+ }
+ logger.Info(
+ "processed shadow object",
+ zap.Uint64("batch_start", batchStart),
+ zap.Uint64("batch_end", batchEnd),
+ zap.Uint64("window_start", request.StartHeight),
+ zap.Uint64("window_end", request.EndHeight),
+ zap.Uint64("scanned_blocks", response.ScannedBlocks),
+ zap.Uint64("consolidated_blocks", response.ConsolidatedBlocks),
+ zap.Uint64("new_objects", newObjects),
+ zap.Uint64("new_consolidated_blocks", newBlocks),
+ zap.Uint64("shadow_objects", lastShadowObjects),
+ zap.Uint64("shadow_blocks", lastShadowBlocks),
+ zap.String("object_key", response.ObjectKey),
+ )
+ }
+ }
+
+ if usePersistedShadowStats {
+ stats, err := w.batchConsolidator.GetShadowStats(statsCtx, &activity.BatchConsolidatorStatsRequest{
+ Mode: activityMode,
+ Tag: tag,
+ StartHeight: batchStart,
+ EndHeight: batchEnd,
+ })
+ if err != nil {
+ return 0, 0, xerrors.Errorf("failed to get consolidation shadow stats for batch [%d, %d): %w", batchStart, batchEnd, err)
+ }
+ if stats.ShadowObjects < lastShadowObjects || stats.ShadowBlocks < lastShadowBlocks {
+ return 0, 0, xerrors.Errorf(
+ "batch_consolidator shadow stats regressed for batch [%d, %d): objects %d -> %d, blocks %d -> %d",
+ batchStart,
+ batchEnd,
+ lastShadowObjects,
+ stats.ShadowObjects,
+ lastShadowBlocks,
+ stats.ShadowBlocks,
+ )
+ }
+ objectsInBatch = stats.ShadowObjects - lastShadowObjects
+ blocksInBatch = stats.ShadowBlocks - lastShadowBlocks
+ if objectsInBatch > 0 {
+ metrics.Counter(batchConsolidatorObjectCounter).Inc(int64(objectsInBatch))
+ }
+ if blocksInBatch > 0 {
+ metrics.Counter(batchConsolidatorConsolidatedBlockCounter).Inc(int64(blocksInBatch))
+ }
+ }
+
+ return objectsInBatch, blocksInBatch, nil
+}
+
+func (w *BatchConsolidator) processHistoricalBackfillBatchParallel(
+ ctx workflow.Context,
+ statsCtx workflow.Context,
+ logger *zap.Logger,
+ metrics client.MetricsHandler,
+ tag uint32,
+ activityMode config.ConsolidationMode,
+ batchStart uint64,
+ batchEnd uint64,
+ maxBlocks uint64,
+ shardSize uint64,
+ parallelism int,
+ usePersistedShadowStats bool,
+) (uint64, uint64, error) {
+ lastShadowObjects := uint64(0)
+ lastShadowBlocks := uint64(0)
+ if usePersistedShadowStats {
+ baseline, err := w.batchConsolidator.GetShadowStats(statsCtx, &activity.BatchConsolidatorStatsRequest{
+ Mode: activityMode,
+ Tag: tag,
+ StartHeight: batchStart,
+ EndHeight: batchEnd,
+ })
+ if err != nil {
+ return 0, 0, xerrors.Errorf("failed to get consolidation shadow stats for batch [%d, %d): %w", batchStart, batchEnd, err)
+ }
+ lastShadowObjects = baseline.ShadowObjects
+ lastShadowBlocks = baseline.ShadowBlocks
+ }
+
+ objectsInBatch := uint64(0)
+ blocksInBatch := uint64(0)
+ for windowStart := batchStart; windowStart < batchEnd; {
+ pending := make([]batchConsolidatorPendingActivity, 0, parallelism)
+ for len(pending) < parallelism && windowStart < batchEnd {
+ windowEnd := batchConsolidatorWindowEnd(windowStart, batchEnd, maxBlocks, shardSize)
+ if windowEnd <= windowStart {
+ return 0, 0, xerrors.Errorf("batch_consolidator made no parallel window progress from %d to %d", windowStart, windowEnd)
+ }
+ request := &activity.BatchConsolidatorRequest{
+ Mode: activityMode,
+ Tag: tag,
+ StartHeight: windowStart,
+ EndHeight: windowEnd,
+ MaxBlocks: maxBlocks,
+ }
+ pending = append(pending, batchConsolidatorPendingActivity{
+ request: request,
+ future: workflow.ExecuteActivity(ctx, activity.ActivityBatchConsolidator, request),
+ })
+ windowStart = windowEnd
+ }
+ if len(pending) == 0 {
+ break
+ }
+
+ var firstErr error
+ type successResult struct {
+ request *activity.BatchConsolidatorRequest
+ response activity.BatchConsolidatorResponse
+ }
+ successes := make([]successResult, 0, len(pending))
+ for _, item := range pending {
+ var response activity.BatchConsolidatorResponse
+ request := item.request
+ if err := item.future.Get(ctx, &response); err != nil {
+ if firstErr == nil {
+ firstErr = xerrors.Errorf("failed to consolidate shadow batch [%d, %d): %w", request.StartHeight, request.EndHeight, err)
+ }
+ continue
+ }
+ successes = append(successes, successResult{request: request, response: response})
+ }
+ if firstErr != nil {
+ return 0, 0, firstErr
+ }
+
+ for _, success := range successes {
+ request := success.request
+ response := success.response
+ if response.ScannedBlocks == 0 {
+ metrics.Counter(batchConsolidatorEmptyBatchCounter).Inc(1)
+ continue
+ }
+ if response.ConsolidatedBlocks == 0 {
+ return 0, 0, xerrors.Errorf(
+ "batch_consolidator made no progress for non-empty shadow scan [%d, %d)",
+ request.StartHeight,
+ request.EndHeight,
+ )
+ }
+
+ newObjects := uint64(0)
+ newBlocks := uint64(0)
+ if !usePersistedShadowStats {
+ newObjects = 1
+ newBlocks = response.ConsolidatedBlocks
+ objectsInBatch += newObjects
+ blocksInBatch += newBlocks
+ metrics.Counter(batchConsolidatorObjectCounter).Inc(int64(newObjects))
+ metrics.Counter(batchConsolidatorConsolidatedBlockCounter).Inc(int64(newBlocks))
+ }
+ logger.Info(
+ "processed shadow object",
+ zap.Uint64("batch_start", batchStart),
+ zap.Uint64("batch_end", batchEnd),
+ zap.Uint64("window_start", request.StartHeight),
+ zap.Uint64("window_end", request.EndHeight),
+ zap.Uint64("scanned_blocks", response.ScannedBlocks),
+ zap.Uint64("consolidated_blocks", response.ConsolidatedBlocks),
+ zap.Uint64("new_objects", newObjects),
+ zap.Uint64("new_consolidated_blocks", newBlocks),
+ zap.Uint64("shadow_objects", lastShadowObjects),
+ zap.Uint64("shadow_blocks", lastShadowBlocks),
+ zap.String("object_key", response.ObjectKey),
+ )
+ }
+ }
+
+ if usePersistedShadowStats {
+ stats, err := w.batchConsolidator.GetShadowStats(statsCtx, &activity.BatchConsolidatorStatsRequest{
+ Mode: activityMode,
+ Tag: tag,
+ StartHeight: batchStart,
+ EndHeight: batchEnd,
+ })
+ if err != nil {
+ return 0, 0, xerrors.Errorf("failed to get consolidation shadow stats for batch [%d, %d): %w", batchStart, batchEnd, err)
+ }
+ if stats.ShadowObjects < lastShadowObjects || stats.ShadowBlocks < lastShadowBlocks {
+ return 0, 0, xerrors.Errorf(
+ "batch_consolidator shadow stats regressed for batch [%d, %d): objects %d -> %d, blocks %d -> %d",
+ batchStart,
+ batchEnd,
+ lastShadowObjects,
+ stats.ShadowObjects,
+ lastShadowBlocks,
+ stats.ShadowBlocks,
+ )
+ }
+ objectsInBatch = stats.ShadowObjects - lastShadowObjects
+ blocksInBatch = stats.ShadowBlocks - lastShadowBlocks
+ if objectsInBatch > 0 {
+ metrics.Counter(batchConsolidatorObjectCounter).Inc(int64(objectsInBatch))
+ }
+ if blocksInBatch > 0 {
+ metrics.Counter(batchConsolidatorConsolidatedBlockCounter).Inc(int64(blocksInBatch))
+ }
+ }
+
+ return objectsInBatch, blocksInBatch, nil
+}
+
+func batchConsolidatorMode(
+ requestMode config.ConsolidationMode,
+ configMode config.ConsolidationMode,
+) (config.ConsolidationMode, error) {
+ mode := requestMode
+ if mode == "" {
+ mode = configMode
+ }
+ switch mode {
+ case config.ConsolidationModeShadowDualWrite,
+ config.ConsolidationModeAutoConsolidate,
+ config.ConsolidationModeHistoricalBackfill,
+ config.ConsolidationModePromoteFinalized,
+ config.ConsolidationModeRepairExistingCSCB:
+ return mode, nil
+ default:
+ return "", xerrors.Errorf(
+ "batch_consolidator requires mode %q, %q, %q, %q, or %q, got %q",
+ config.ConsolidationModeShadowDualWrite,
+ config.ConsolidationModeAutoConsolidate,
+ config.ConsolidationModeHistoricalBackfill,
+ config.ConsolidationModePromoteFinalized,
+ config.ConsolidationModeRepairExistingCSCB,
+ mode,
+ )
+ }
+}
+
+func (w *BatchConsolidator) validateHistoricalBackfillRange(
+ ctx workflow.Context,
+ logger *zap.Logger,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ irreversibleDistance uint64,
+) error {
+ latest, err := w.batchConsolidator.GetLatestBlock(ctx, &activity.BatchConsolidatorLatestBlockRequest{
+ Tag: tag,
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to get latest block for historical backfill range validation: %w", err)
+ }
+ if latest == nil {
+ return xerrors.New("latest block response is nil for historical backfill range validation")
+ }
+ safeEndHeight, ok := batchConsolidatorHistoricalSafeEndHeight(latest.Height, irreversibleDistance)
+ if !ok {
+ return xerrors.Errorf(
+ "historical_backfill range [%d, %d) is unsafe: latest height %d is below irreversible_distance %d",
+ startHeight,
+ endHeight,
+ latest.Height,
+ irreversibleDistance,
+ )
+ }
+ if endHeight > safeEndHeight {
+ return xerrors.Errorf(
+ "historical_backfill range [%d, %d) is unsafe: end_height must be <= %d for latest_height=%d irreversible_distance=%d",
+ startHeight,
+ endHeight,
+ safeEndHeight,
+ latest.Height,
+ irreversibleDistance,
+ )
+ }
+ logger.Info(
+ "validated historical backfill range",
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", startHeight),
+ zap.Uint64("end_height", endHeight),
+ zap.Uint64("latest_height", latest.Height),
+ zap.Uint64("irreversible_distance", irreversibleDistance),
+ zap.Uint64("safe_end_height", safeEndHeight),
+ )
+ return nil
+}
+
+func batchConsolidatorHistoricalSafeEndHeight(latestHeight uint64, irreversibleDistance uint64) (uint64, bool) {
+ return batchConsolidatorSafeEndHeight(latestHeight, irreversibleDistance)
+}
+
+func (w *BatchConsolidator) validateAutoConsolidateRange(
+ ctx workflow.Context,
+ logger *zap.Logger,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ irreversibleDistance uint64,
+) error {
+ latest, err := w.batchConsolidator.GetLatestBlock(ctx, &activity.BatchConsolidatorLatestBlockRequest{
+ Tag: tag,
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to get latest block for auto_consolidate range validation: %w", err)
+ }
+ if latest == nil {
+ return xerrors.New("latest block response is nil for auto_consolidate range validation")
+ }
+ safeEndHeight, ok := batchConsolidatorAutoConsolidateSafeEndHeight(latest.Height, irreversibleDistance)
+ if !ok {
+ return xerrors.Errorf(
+ "auto_consolidate range [%d, %d) is unsafe: latest height %d is below irreversible_distance %d",
+ startHeight,
+ endHeight,
+ latest.Height,
+ irreversibleDistance,
+ )
+ }
+ if endHeight > safeEndHeight {
+ return xerrors.Errorf(
+ "auto_consolidate range [%d, %d) is unsafe: end_height must be <= %d for latest_height=%d irreversible_distance=%d",
+ startHeight,
+ endHeight,
+ safeEndHeight,
+ latest.Height,
+ irreversibleDistance,
+ )
+ }
+ logger.Info(
+ "validated auto consolidate range",
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", startHeight),
+ zap.Uint64("end_height", endHeight),
+ zap.Uint64("latest_height", latest.Height),
+ zap.Uint64("irreversible_distance", irreversibleDistance),
+ zap.Uint64("safe_end_height", safeEndHeight),
+ )
+ return nil
+}
+
+func batchConsolidatorAutoConsolidateSafeEndHeight(latestHeight uint64, irreversibleDistance uint64) (uint64, bool) {
+ return batchConsolidatorSafeEndHeight(latestHeight, irreversibleDistance)
+}
+
+func batchConsolidatorSafeEndHeight(latestHeight uint64, irreversibleDistance uint64) (uint64, bool) {
+ if irreversibleDistance == 0 {
+ if latestHeight == maxUint64 {
+ return maxUint64, true
+ }
+ return latestHeight + 1, true
+ }
+ if latestHeight < irreversibleDistance-1 {
+ return 0, false
+ }
+ latestFinalizedHeight := latestHeight - irreversibleDistance + 1
+ if latestFinalizedHeight == maxUint64 {
+ return maxUint64, true
+ }
+ return latestFinalizedHeight + 1, true
+}
+
+func (w *BatchConsolidator) withShadowStatsActivityOptions(
+ ctx workflow.Context,
+ cfg config.BatchConsolidatorWorkflowConfig,
+) workflow.Context {
+ base := cfg.Base()
+ retryPolicy := w.getShadowStatsActivityRetryPolicy(base.ActivityRetry)
+ return workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
+ TaskQueue: base.TaskList,
+ StartToCloseTimeout: base.ActivityStartToCloseTimeout,
+ ScheduleToCloseTimeout: base.ActivityScheduleToCloseTimeout,
+ HeartbeatTimeout: base.ActivityHeartbeatTimeout,
+ RetryPolicy: retryPolicy,
+ })
+}
+
+func (w *BatchConsolidator) getShadowStatsActivityRetryPolicy(cfg *config.RetryPolicy) *temporal.RetryPolicy {
+ retryPolicy := w.getRetryPolicy(cfg)
+ if retryPolicy != nil {
+ retryPolicyCopy := *retryPolicy
+ // Stats reads are side-effect-free and cheap. Unlimited attempts prevent
+ // old workers on the shared task queue from exhausting retries during a
+ // rolling deploy before a new worker polls the new stats activity name.
+ retryPolicyCopy.MaximumAttempts = 0
+ retryPolicy = &retryPolicyCopy
+ }
+ return retryPolicy
+}
+
+func (w *BatchConsolidator) withCursorActivityOptions(
+ ctx workflow.Context,
+ cfg config.BatchConsolidatorWorkflowConfig,
+) workflow.Context {
+ base := cfg.Base()
+ retryPolicy := w.getCursorActivityRetryPolicy(base.ActivityRetry)
+ return workflow.WithActivityOptions(ctx, workflow.ActivityOptions{
+ TaskQueue: base.TaskList,
+ StartToCloseTimeout: base.ActivityStartToCloseTimeout,
+ ScheduleToCloseTimeout: base.ActivityScheduleToCloseTimeout,
+ HeartbeatTimeout: base.ActivityHeartbeatTimeout,
+ RetryPolicy: retryPolicy,
+ })
+}
+
+func (w *BatchConsolidator) getCursorActivityRetryPolicy(cfg *config.RetryPolicy) *temporal.RetryPolicy {
+ retryPolicy := w.getRetryPolicy(cfg)
+ if retryPolicy != nil {
+ retryPolicyCopy := *retryPolicy
+ // Cursor writes are monotonic and idempotent. Unlimited attempts prevent
+ // old workers on the shared task queue from exhausting retries during a
+ // rolling deploy before a new worker polls the new cursor activity name.
+ retryPolicyCopy.MaximumAttempts = 0
+ retryPolicy = &retryPolicyCopy
+ }
+ return retryPolicy
+}
+
+func batchConsolidatorWindowEnd(startHeight uint64, requestedEndHeight uint64, batchSize uint64, shardSize uint64) uint64 {
+ batchEnd := batchConsolidatorBatchEnd(startHeight, requestedEndHeight, batchSize)
+ shardEnd := batchConsolidatorShardEnd(startHeight, shardSize)
+ if shardEnd < batchEnd {
+ return shardEnd
+ }
+ return batchEnd
+}
+
+func batchConsolidatorBatchEnd(startHeight uint64, requestedEndHeight uint64, batchSize uint64) uint64 {
+ batchEnd := startHeight + batchSize
+ if batchEnd < startHeight || batchEnd > requestedEndHeight {
+ batchEnd = requestedEndHeight
+ }
+ return batchEnd
+}
+
+func batchConsolidatorObjectWindowEnd(startHeight uint64, batchEnd uint64, maxBlocks uint64) uint64 {
+ windowEnd := startHeight + maxBlocks
+ if windowEnd < startHeight || windowEnd > batchEnd {
+ return batchEnd
+ }
+ return windowEnd
+}
+
+func batchConsolidatorFullObjectWindowEnd(startHeight uint64, batchEnd uint64, maxBlocks uint64) (uint64, bool) {
+ windowEnd := startHeight + maxBlocks
+ if windowEnd < startHeight || windowEnd > batchEnd {
+ return batchEnd, false
+ }
+ return windowEnd, true
+}
+
+func batchConsolidatorAutoRangeHasOnlyFullObjectWindows(startHeight uint64, endHeight uint64, batchSize uint64, maxBlocks uint64, shardSize uint64) bool {
+ if endHeight <= startHeight || batchSize == 0 || maxBlocks == 0 || shardSize == 0 {
+ return false
+ }
+ for batchStart := startHeight; batchStart < endHeight; {
+ batchEnd := batchConsolidatorWindowEnd(batchStart, endHeight, batchSize, shardSize)
+ if batchEnd <= batchStart {
+ return false
+ }
+ for windowStart := batchStart; windowStart < batchEnd; {
+ windowEnd, fullWindow := batchConsolidatorFullObjectWindowEnd(windowStart, batchEnd, maxBlocks)
+ if !fullWindow || windowEnd <= windowStart {
+ return false
+ }
+ windowStart = windowEnd
+ }
+ batchStart = batchEnd
+ }
+ return true
+}
+
+func batchConsolidatorShardEnd(height uint64, shardSize uint64) uint64 {
+ return (height/shardSize + 1) * shardSize
+}
diff --git a/internal/workflow/batch_consolidator_repair_test.go b/internal/workflow/batch_consolidator_repair_test.go
new file mode 100644
index 00000000..7b6d7da5
--- /dev/null
+++ b/internal/workflow/batch_consolidator_repair_test.go
@@ -0,0 +1,164 @@
+package workflow
+
+import (
+ "context"
+ "encoding/hex"
+ "sync"
+ "sync/atomic"
+ "time"
+
+ "github.com/stretchr/testify/mock"
+ temporalworkflow "go.temporal.io/sdk/workflow"
+
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/internal/workflow/activity"
+)
+
+func (s *batchConsolidatorTestSuite) TestRepairExistingCSCBCompletesThenChecksForNextObject() {
+ require := testutil.Require(s.T())
+ s.env.OnGetVersion(
+ batchConsolidatorRepairParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorRepairParallelismVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.mockAutoConsolidateLatestHeight(2000)
+ var requests []*activity.BatchConsolidatorRequest
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ if len(requests) > 1 {
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ }
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: 1000,
+ EndHeight: 1100,
+ ScannedBlocks: 100,
+ ConsolidatedBlocks: 100,
+ ObjectKey: "consolidated/clean.cscb.zstd",
+ OldObjectKey: "consolidated/dirty.cscb.zstd",
+ RepairedObjects: 1,
+ }, nil
+ }).Twice()
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1100,
+ MaxBlocks: 100,
+ })
+ require.NoError(err)
+ require.Len(requests, 2)
+ for _, request := range requests {
+ require.Equal(config.ConsolidationModeRepairExistingCSCB, request.Mode)
+ require.Equal(uint32(2), request.Tag)
+ require.Equal(uint64(1000), request.StartHeight)
+ require.Equal(uint64(1100), request.EndHeight)
+ require.Equal(uint64(100), request.MaxBlocks)
+ require.True(isRepairExecutionKey(request.RepairExecutionKey))
+ }
+ require.NotEqual(requests[0].RepairExecutionKey, requests[1].RepairExecutionKey)
+}
+
+func isRepairExecutionKey(value string) bool {
+ decoded, err := hex.DecodeString(value)
+ return err == nil && len(decoded) == 32
+}
+
+func (s *batchConsolidatorTestSuite) TestRepairExistingCSCBRequiresSerialExecution() {
+ require := testutil.Require(s.T())
+ s.env.OnGetVersion(
+ batchConsolidatorRepairParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorRepairParallelismVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1100,
+ MaxBlocks: 100,
+ Parallelism: 2,
+ })
+ require.Error(err)
+ require.Contains(err.Error(), "legacy repair_existing_cscb execution requires parallelism=1")
+}
+
+func (s *batchConsolidatorTestSuite) TestRepairExistingCSCBProcessesDistinctObjectsInParallel() {
+ require := testutil.Require(s.T())
+ s.env.OnGetVersion(
+ batchConsolidatorRepairParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorRepairParallelismVersion,
+ ).Return(temporalworkflow.Version(batchConsolidatorRepairParallelismVersion))
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.mockAutoConsolidateLatestHeight(2000)
+
+ objectKeys := []string{
+ "consolidated/dirty-1100-1200.cscb.zstd",
+ "consolidated/dirty-1000-1100.cscb.zstd",
+ }
+ var candidateCalls atomic.Int32
+ s.env.OnActivity(activity.ActivityBatchConsolidatorRepairCandidates, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.BatchConsolidatorRepairCandidatesRequest) (*activity.BatchConsolidatorRepairCandidatesResponse, error) {
+ require.Equal(uint32(2), request.Tag)
+ require.Equal(uint64(1000), request.StartHeight)
+ require.Equal(uint64(1200), request.EndHeight)
+ require.Equal(2, request.Limit)
+ if candidateCalls.Add(1) == 1 {
+ return &activity.BatchConsolidatorRepairCandidatesResponse{ObjectKeys: objectKeys}, nil
+ }
+ return &activity.BatchConsolidatorRepairCandidatesResponse{}, nil
+ }).Twice()
+
+ var inFlight atomic.Int32
+ var maxInFlight atomic.Int32
+ var requestsMu sync.Mutex
+ requests := make([]*activity.BatchConsolidatorRequest, 0, len(objectKeys))
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requestsMu.Lock()
+ requests = append(requests, request)
+ requestsMu.Unlock()
+ current := inFlight.Add(1)
+ for {
+ previous := maxInFlight.Load()
+ if current <= previous || maxInFlight.CompareAndSwap(previous, current) {
+ break
+ }
+ }
+ time.Sleep(50 * time.Millisecond)
+ inFlight.Add(-1)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: 1000,
+ EndHeight: 1100,
+ ScannedBlocks: 100,
+ ConsolidatedBlocks: 100,
+ ObjectKey: request.RepairObjectKey + ".clean",
+ OldObjectKey: request.RepairObjectKey,
+ RepairedObjects: 1,
+ }, nil
+ }).Twice()
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeRepairExistingCSCB,
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1200,
+ MaxBlocks: 100,
+ Parallelism: 2,
+ })
+ require.NoError(err)
+ require.Equal(int32(2), maxInFlight.Load())
+ require.Len(requests, 2)
+ require.ElementsMatch(objectKeys, []string{requests[0].RepairObjectKey, requests[1].RepairObjectKey})
+ require.True(isRepairExecutionKey(requests[0].RepairExecutionKey))
+ require.True(isRepairExecutionKey(requests[1].RepairExecutionKey))
+ require.NotEqual(requests[0].RepairExecutionKey, requests[1].RepairExecutionKey)
+}
diff --git a/internal/workflow/batch_consolidator_test.go b/internal/workflow/batch_consolidator_test.go
new file mode 100644
index 00000000..2eb64450
--- /dev/null
+++ b/internal/workflow/batch_consolidator_test.go
@@ -0,0 +1,1242 @@
+package workflow
+
+import (
+ "context"
+ "sort"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/suite"
+ "go.temporal.io/sdk/testsuite"
+ temporalworkflow "go.temporal.io/sdk/workflow"
+ "go.uber.org/fx"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/internal/workflow/activity"
+)
+
+type batchConsolidatorTestSuite struct {
+ suite.Suite
+ testsuite.WorkflowTestSuite
+ env *cadence.TestEnv
+ batchConsolidator *BatchConsolidator
+ app testapp.TestApp
+ cfg *config.Config
+}
+
+func TestBatchConsolidatorWorkflowTestSuite(t *testing.T) {
+ suite.Run(t, new(batchConsolidatorTestSuite))
+}
+
+func (s *batchConsolidatorTestSuite) SetupTest() {
+ require := testutil.Require(s.T())
+
+ cfg, err := config.New()
+ require.NoError(err)
+ cfg.Workflows.BatchConsolidator.BatchSize = 100
+ cfg.Workflows.BatchConsolidator.CheckpointSize = 500
+ cfg.Workflows.BatchConsolidator.MaxBlocks = 25
+ cfg.Workflows.BatchConsolidator.Storage.Consolidation.Enabled = true
+ cfg.Workflows.BatchConsolidator.Storage.Consolidation.Mode = config.ConsolidationModeShadowDualWrite
+ s.cfg = cfg
+
+ s.env = cadence.NewTestEnv(s)
+ s.app = testapp.New(
+ s.T(),
+ Module,
+ testapp.WithConfig(s.cfg),
+ cadence.WithTestEnv(s.env),
+ fx.Provide(func() metastorage.MetaStorage {
+ return nil
+ }),
+ fx.Provide(func() blobstorage.BlobStorage {
+ return nil
+ }),
+ fx.Populate(&s.batchConsolidator),
+ )
+}
+
+func (s *batchConsolidatorTestSuite) TearDownTest() {
+ s.app.Close()
+ s.env.AssertExpectations(s.T())
+}
+
+func (s *batchConsolidatorTestSuite) TestBatchConsolidatorProcessesBatches() {
+ require := testutil.Require(s.T())
+
+ var requests []*activity.BatchConsolidatorRequest
+ seenByStart := make(map[uint64]int)
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ seenByStart[request.StartHeight]++
+ if seenByStart[request.StartHeight] > 1 {
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ }
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.EndHeight - request.StartHeight,
+ ConsolidatedBlocks: request.EndHeight - request.StartHeight,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1250,
+ MaxBlocks: 100,
+ })
+ require.NoError(err)
+ require.Len(requests, 5)
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 1000, EndHeight: 1100, MaxBlocks: 100}, requests[0])
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 1000, EndHeight: 1100, MaxBlocks: 100}, requests[1])
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 1100, EndHeight: 1200, MaxBlocks: 100}, requests[2])
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 1100, EndHeight: 1200, MaxBlocks: 100}, requests[3])
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 1200, EndHeight: 1250, MaxBlocks: 100}, requests[4])
+}
+
+func (s *batchConsolidatorTestSuite) TestBatchConsolidatorCheckpointContinuesAsNew() {
+ require := testutil.Require(s.T())
+
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(&activity.BatchConsolidatorResponse{
+ ConsolidatedBlocks: 100,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil)
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1700,
+ })
+ require.Error(err)
+ require.True(IsContinueAsNewError(err))
+}
+
+func (s *batchConsolidatorTestSuite) TestBatchConsolidatorRequestOverridesConfig() {
+ require := testutil.Require(s.T())
+
+ var requests []*activity.BatchConsolidatorRequest
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ return &activity.BatchConsolidatorResponse{}, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1100,
+ BatchSize: 40,
+ MaxBlocks: 10,
+ })
+ require.NoError(err)
+ require.Len(requests, 3)
+ require.Equal(uint64(10), requests[0].MaxBlocks)
+ require.Equal(uint64(1040), requests[0].EndHeight)
+ require.Equal(uint64(1080), requests[1].EndHeight)
+ require.Equal(uint64(1100), requests[2].EndHeight)
+}
+
+func (s *batchConsolidatorTestSuite) TestBatchConsolidatorCapsMaxBlocksAtStorageLimit() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.Storage.Consolidation.MaxBlocks = 10
+
+ var requests []*activity.BatchConsolidatorRequest
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1020,
+ MaxBlocks: 100,
+ })
+ require.NoError(err)
+ require.Len(requests, 1)
+ require.Equal(uint64(10), requests[0].MaxBlocks)
+}
+
+func (s *batchConsolidatorTestSuite) TestBatchConsolidatorCapsWindowAtShardBoundary() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.BatchSize = 10000
+ s.cfg.Workflows.BatchConsolidator.CheckpointSize = 20000
+ s.cfg.Workflows.BatchConsolidator.Storage.Consolidation.ShardSize = 10000
+
+ var requests []*activity.BatchConsolidatorRequest
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 9500,
+ EndHeight: 10500,
+ })
+ require.NoError(err)
+ require.Len(requests, 2)
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 9500, EndHeight: 10000, MaxBlocks: 25}, requests[0])
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 10000, EndHeight: 10500, MaxBlocks: 25}, requests[1])
+}
+
+func (s *batchConsolidatorTestSuite) TestBatchConsolidatorRepeatsHeightBatchUntilEmpty() {
+ require := testutil.Require(s.T())
+
+ var requests []*activity.BatchConsolidatorRequest
+ normalCalls := 0
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ normalCalls++
+ if normalCalls <= 2 {
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.MaxBlocks,
+ ConsolidatedBlocks: request.MaxBlocks,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ }
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1100,
+ MaxBlocks: 25,
+ })
+ require.NoError(err)
+ require.Len(requests, 3)
+ for _, request := range requests {
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 1000, EndHeight: 1100, MaxBlocks: 25}, request)
+ }
+}
+
+func (s *batchConsolidatorTestSuite) TestBatchConsolidatorIgnoresParallelismOutsideHistoricalBackfill() {
+ require := testutil.Require(s.T())
+
+ var requests []*activity.BatchConsolidatorRequest
+ normalCalls := 0
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ normalCalls++
+ if normalCalls == 1 {
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.MaxBlocks,
+ ConsolidatedBlocks: request.MaxBlocks,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ }
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1100,
+ MaxBlocks: 25,
+ Parallelism: 4,
+ })
+ require.NoError(err)
+ require.Len(requests, 2)
+ for _, request := range requests {
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 1000, EndHeight: 1100, MaxBlocks: 25}, request)
+ }
+}
+
+func (s *batchConsolidatorTestSuite) TestBatchConsolidatorRejectsExcessiveParallelism() {
+ require := testutil.Require(s.T())
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 25,
+ Parallelism: batchConsolidatorMaxParallelism + 1,
+ })
+ require.Error(err)
+ require.Contains(err.Error(), "parallelism(21) exceeds max(20)")
+}
+
+func (s *batchConsolidatorTestSuite) TestHistoricalBackfillAcceptsMaximumParallelism() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.BatchSize = 10000
+ s.cfg.Workflows.BatchConsolidator.CheckpointSize = 100000
+ s.cfg.Workflows.BatchConsolidator.MaxBlocks = 1000
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.cfg.Workflows.BatchConsolidator.Storage.Consolidation.MaxBlocks = 1000
+ s.cfg.Workflows.BatchConsolidator.Storage.Consolidation.ShardSize = 10000
+ var requests []*activity.BatchConsolidatorRequest
+ var requestsMu sync.Mutex
+ s.mockAutoConsolidateLatestHeight(120010)
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requestsMu.Lock()
+ requests = append(requests, request)
+ requestsMu.Unlock()
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.EndHeight - request.StartHeight,
+ ConsolidatedBlocks: request.EndHeight - request.StartHeight,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 100000,
+ EndHeight: 120000,
+ BatchSize: 20000,
+ MaxBlocks: 1000,
+ Parallelism: 20,
+ })
+ require.NoError(err)
+ require.Len(requests, 20)
+ sort.Slice(requests, func(i, j int) bool {
+ return requests[i].StartHeight < requests[j].StartHeight
+ })
+ for i, request := range requests {
+ startHeight := uint64(100000 + i*1000)
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: startHeight,
+ EndHeight: startHeight + 1000,
+ MaxBlocks: 1000,
+ }, request)
+ }
+}
+
+func (s *batchConsolidatorTestSuite) TestAutoConsolidateValidatesFinalizedRange() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ var requests []*activity.BatchConsolidatorRequest
+ s.mockAutoConsolidateLatestHeight(120)
+ s.mockEmptyShadowStats()
+ s.mockAutoConsolidateCursorUpdate(111)
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 111,
+ MaxBlocks: 11,
+ })
+ require.NoError(err)
+ require.Len(requests, 1)
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 111,
+ MaxBlocks: 11,
+ }, requests[0])
+}
+
+func (s *batchConsolidatorTestSuite) TestHistoricalBackfillValidatesFinalizedRange() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ var requests []*activity.BatchConsolidatorRequest
+ s.mockAutoConsolidateLatestHeight(120)
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 111,
+ MaxBlocks: 25,
+ })
+ require.NoError(err)
+ require.Len(requests, 1)
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 111,
+ MaxBlocks: 25,
+ }, requests[0])
+}
+
+func (s *batchConsolidatorTestSuite) TestHistoricalBackfillRejectsReorgUnsafeRange() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.mockAutoConsolidateLatestHeight(120)
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 113,
+ })
+ require.Error(err)
+ require.Contains(err.Error(), "historical_backfill range [100, 113) is unsafe")
+}
+
+func (s *batchConsolidatorTestSuite) TestAutoConsolidateRejectsReorgUnsafeRange() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.mockAutoConsolidateLatestHeight(120)
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 113,
+ })
+ require.Error(err)
+ require.Contains(err.Error(), "auto_consolidate range [100, 113) is unsafe")
+}
+
+func (s *batchConsolidatorTestSuite) TestAutoConsolidateDuplicateRunNoOpsWhenScanEmpty() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ var requests []*activity.BatchConsolidatorRequest
+ s.mockAutoConsolidateLatestHeight(120)
+ s.mockEmptyShadowStats()
+ s.mockAutoConsolidateCursorUpdate(111)
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 111,
+ MaxBlocks: 11,
+ })
+ require.NoError(err)
+ require.Len(requests, 1)
+ require.Equal(config.ConsolidationModeAutoConsolidate, requests[0].Mode)
+}
+
+func (s *batchConsolidatorTestSuite) TestAutoConsolidateProcessesFullObjectWindowsSerially() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ var requests []*activity.BatchConsolidatorRequest
+ s.mockAutoConsolidateLatestHeight(220)
+ s.mockEmptyShadowStats()
+ s.mockAutoConsolidateCursorUpdate(200)
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.EndHeight - request.StartHeight,
+ ConsolidatedBlocks: request.EndHeight - request.StartHeight,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 25,
+ })
+ require.NoError(err)
+ require.Equal([]*activity.BatchConsolidatorRequest{
+ {Mode: config.ConsolidationModeAutoConsolidate, Tag: 2, StartHeight: 100, EndHeight: 125, MaxBlocks: 25},
+ {Mode: config.ConsolidationModeAutoConsolidate, Tag: 2, StartHeight: 125, EndHeight: 150, MaxBlocks: 25},
+ {Mode: config.ConsolidationModeAutoConsolidate, Tag: 2, StartHeight: 150, EndHeight: 175, MaxBlocks: 25},
+ {Mode: config.ConsolidationModeAutoConsolidate, Tag: 2, StartHeight: 175, EndHeight: 200, MaxBlocks: 25},
+ }, requests)
+}
+
+func (s *batchConsolidatorTestSuite) TestAutoConsolidateRejectsPartialObjectWindowRange() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.mockAutoConsolidateLatestHeight(120)
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 111,
+ MaxBlocks: 25,
+ })
+ require.Error(err)
+ require.Contains(err.Error(), "must contain only full object windows")
+}
+
+func (s *batchConsolidatorTestSuite) TestHistoricalBackfillProcessesObjectWindowsInParallel() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.cfg.Workflows.BatchConsolidator.BatchSize = 100
+ s.cfg.Workflows.BatchConsolidator.CheckpointSize = 500
+ var requests []*activity.BatchConsolidatorRequest
+ var requestsMu sync.Mutex
+ s.mockAutoConsolidateLatestHeight(220)
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requestsMu.Lock()
+ requests = append(requests, request)
+ requestsMu.Unlock()
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.EndHeight - request.StartHeight,
+ ConsolidatedBlocks: request.EndHeight - request.StartHeight,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 25,
+ Parallelism: 4,
+ })
+ require.NoError(err)
+ require.Len(requests, 4)
+ sort.Slice(requests, func(i, j int) bool {
+ return requests[i].StartHeight < requests[j].StartHeight
+ })
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 125,
+ MaxBlocks: 25,
+ }, requests[0])
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 125,
+ EndHeight: 150,
+ MaxBlocks: 25,
+ }, requests[1])
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 150,
+ EndHeight: 175,
+ MaxBlocks: 25,
+ }, requests[2])
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 175,
+ EndHeight: 200,
+ MaxBlocks: 25,
+ }, requests[3])
+}
+
+func (s *batchConsolidatorTestSuite) TestHistoricalBackfillRunsObjectWindowsConcurrently() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.cfg.Workflows.BatchConsolidator.BatchSize = 100
+ s.cfg.Workflows.BatchConsolidator.CheckpointSize = 500
+ var inFlight int64
+ var maxInFlight int64
+ s.mockAutoConsolidateLatestHeight(220)
+ s.mockEmptyShadowStats()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ current := atomic.AddInt64(&inFlight, 1)
+ for {
+ maxSeen := atomic.LoadInt64(&maxInFlight)
+ if current <= maxSeen || atomic.CompareAndSwapInt64(&maxInFlight, maxSeen, current) {
+ break
+ }
+ }
+ time.Sleep(50 * time.Millisecond)
+ atomic.AddInt64(&inFlight, -1)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.EndHeight - request.StartHeight,
+ ConsolidatedBlocks: request.EndHeight - request.StartHeight,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeHistoricalBackfill,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 25,
+ Parallelism: 4,
+ })
+ require.NoError(err)
+ observedMaxInFlight := atomic.LoadInt64(&maxInFlight)
+ s.T().Logf("max in-flight batch_consolidator activities: %d", observedMaxInFlight)
+ require.Greater(observedMaxInFlight, int64(1))
+}
+
+func (s *batchConsolidatorTestSuite) TestAutoConsolidateIgnoresParallelism() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.cfg.Workflows.BatchConsolidator.BatchSize = 100
+ s.cfg.Workflows.BatchConsolidator.CheckpointSize = 500
+ var requests []*activity.BatchConsolidatorRequest
+ var inFlight int64
+ var maxInFlight int64
+ normalCalls := 0
+ s.mockAutoConsolidateLatestHeight(220)
+ s.mockEmptyShadowStats()
+ s.mockAutoConsolidateCursorUpdate(200)
+ s.env.OnGetVersion(
+ batchConsolidatorAutoSerialChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoSerialVersion,
+ ).Return(temporalworkflow.Version(batchConsolidatorAutoSerialVersion))
+ s.env.OnGetVersion(
+ batchConsolidatorAutoCursorChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoCursorVersion,
+ ).Return(temporalworkflow.Version(batchConsolidatorAutoCursorVersion))
+ s.env.OnGetVersion(
+ batchConsolidatorAutoParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoParallelismVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ current := atomic.AddInt64(&inFlight, 1)
+ for {
+ maxSeen := atomic.LoadInt64(&maxInFlight)
+ if current <= maxSeen || atomic.CompareAndSwapInt64(&maxInFlight, maxSeen, current) {
+ break
+ }
+ }
+ time.Sleep(50 * time.Millisecond)
+ atomic.AddInt64(&inFlight, -1)
+ requests = append(requests, request)
+ normalCalls++
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.EndHeight - request.StartHeight,
+ ConsolidatedBlocks: request.EndHeight - request.StartHeight,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 25,
+ Parallelism: 4,
+ })
+ require.NoError(err)
+ require.Equal(4, normalCalls)
+ require.Equal(int64(1), atomic.LoadInt64(&maxInFlight))
+ require.Equal([]*activity.BatchConsolidatorRequest{
+ {Mode: config.ConsolidationModeAutoConsolidate, Tag: 2, StartHeight: 100, EndHeight: 125, MaxBlocks: 25},
+ {Mode: config.ConsolidationModeAutoConsolidate, Tag: 2, StartHeight: 125, EndHeight: 150, MaxBlocks: 25},
+ {Mode: config.ConsolidationModeAutoConsolidate, Tag: 2, StartHeight: 150, EndHeight: 175, MaxBlocks: 25},
+ {Mode: config.ConsolidationModeAutoConsolidate, Tag: 2, StartHeight: 175, EndHeight: 200, MaxBlocks: 25},
+ }, requests)
+}
+
+func (s *batchConsolidatorTestSuite) TestAutoConsolidateDefaultVersionPreservesParallelObjectWindows() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.cfg.Workflows.BatchConsolidator.BatchSize = 100
+ s.cfg.Workflows.BatchConsolidator.CheckpointSize = 500
+ var requests []*activity.BatchConsolidatorRequest
+ var requestsMu sync.Mutex
+ s.mockAutoConsolidateLatestHeight(220)
+ s.mockEmptyShadowStats()
+ s.env.OnGetVersion(
+ batchConsolidatorAutoSerialChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoSerialVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+ s.env.OnGetVersion(
+ batchConsolidatorAutoCursorChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoCursorVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+ s.env.OnGetVersion(
+ batchConsolidatorAutoParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoParallelismVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requestsMu.Lock()
+ requests = append(requests, request)
+ requestsMu.Unlock()
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.EndHeight - request.StartHeight,
+ ConsolidatedBlocks: request.EndHeight - request.StartHeight,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 25,
+ Parallelism: 4,
+ })
+ require.NoError(err)
+ require.Len(requests, 4)
+ sort.Slice(requests, func(i, j int) bool {
+ return requests[i].StartHeight < requests[j].StartHeight
+ })
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 125,
+ MaxBlocks: 25,
+ }, requests[0])
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 125,
+ EndHeight: 150,
+ MaxBlocks: 25,
+ }, requests[1])
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 150,
+ EndHeight: 175,
+ MaxBlocks: 25,
+ }, requests[2])
+ require.Equal(&activity.BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 175,
+ EndHeight: 200,
+ MaxBlocks: 25,
+ }, requests[3])
+}
+
+func (s *batchConsolidatorTestSuite) TestAutoConsolidateSerialVersionWithoutCursorPreservesOldScanner() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.cfg.Workflows.BatchConsolidator.BatchSize = 100
+ s.cfg.Workflows.BatchConsolidator.CheckpointSize = 500
+ var requests []*activity.BatchConsolidatorRequest
+ s.mockAutoConsolidateLatestHeight(220)
+ s.mockEmptyShadowStats()
+ s.env.OnGetVersion(
+ batchConsolidatorAutoSerialChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoSerialVersion,
+ ).Return(temporalworkflow.Version(batchConsolidatorAutoSerialVersion))
+ s.env.OnGetVersion(
+ batchConsolidatorAutoCursorChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoCursorVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+ s.env.OnGetVersion(
+ batchConsolidatorAutoParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoParallelismVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ if len(requests) == 1 {
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.EndHeight - request.StartHeight,
+ ConsolidatedBlocks: request.EndHeight - request.StartHeight,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ }
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 25,
+ Parallelism: 4,
+ })
+ require.NoError(err)
+ require.Equal([]*activity.BatchConsolidatorRequest{
+ {Mode: config.ConsolidationModeAutoConsolidate, Tag: 2, StartHeight: 100, EndHeight: 200, MaxBlocks: 25},
+ {Mode: config.ConsolidationModeAutoConsolidate, Tag: 2, StartHeight: 100, EndHeight: 200, MaxBlocks: 25},
+ }, requests)
+}
+
+func (s *batchConsolidatorTestSuite) TestAutoConsolidateParallelismVersionRunsObjectWindowsConcurrently() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ s.cfg.Workflows.BatchConsolidator.BatchSize = 100
+ s.cfg.Workflows.BatchConsolidator.CheckpointSize = 500
+ var inFlight int64
+ var maxInFlight int64
+ s.mockAutoConsolidateLatestHeight(220)
+ s.mockEmptyShadowStats()
+ s.mockAutoConsolidateCursorUpdate(200)
+ s.env.OnGetVersion(
+ batchConsolidatorAutoSerialChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoSerialVersion,
+ ).Return(temporalworkflow.Version(batchConsolidatorAutoSerialVersion))
+ s.env.OnGetVersion(
+ batchConsolidatorAutoCursorChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoCursorVersion,
+ ).Return(temporalworkflow.Version(batchConsolidatorAutoCursorVersion))
+ s.env.OnGetVersion(
+ batchConsolidatorAutoParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ batchConsolidatorAutoParallelismVersion,
+ ).Return(temporalworkflow.Version(batchConsolidatorAutoParallelismVersion))
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ current := atomic.AddInt64(&inFlight, 1)
+ for {
+ maxSeen := atomic.LoadInt64(&maxInFlight)
+ if current <= maxSeen || atomic.CompareAndSwapInt64(&maxInFlight, maxSeen, current) {
+ break
+ }
+ }
+ time.Sleep(50 * time.Millisecond)
+ atomic.AddInt64(&inFlight, -1)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.EndHeight - request.StartHeight,
+ ConsolidatedBlocks: request.EndHeight - request.StartHeight,
+ ObjectKey: "consolidated/object.zstd",
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxBlocks: 25,
+ Parallelism: 2,
+ })
+ require.NoError(err)
+ observedMaxInFlight := atomic.LoadInt64(&maxInFlight)
+ s.T().Logf("max in-flight auto_consolidate activities: %d", observedMaxInFlight)
+ require.Greater(observedMaxInFlight, int64(1))
+ require.LessOrEqual(observedMaxInFlight, int64(2))
+}
+
+func (s *batchConsolidatorTestSuite) TestAutoConsolidateResumeAfterLostActivityCompletion() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.IrreversibleDistance = 10
+ var requests []*activity.BatchConsolidatorRequest
+ s.mockAutoConsolidateLatestHeight(120)
+ s.mockAutoConsolidateCursorUpdate(111)
+ statsCalls := 0
+ s.env.OnActivity(activity.ActivityBatchConsolidatorStats, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorStatsRequest) (*activity.BatchConsolidatorStatsResponse, error) {
+ require.Equal(config.ConsolidationModeAutoConsolidate, request.Mode)
+ statsCalls++
+ if statsCalls == 1 {
+ return &activity.BatchConsolidatorStatsResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ }
+ return &activity.BatchConsolidatorStatsResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ShadowObjects: 1,
+ ShadowBlocks: 25,
+ }, nil
+ })
+ call := 0
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ require.Equal(config.ConsolidationModeAutoConsolidate, request.Mode)
+ call++
+ if call == 1 {
+ return nil, xerrors.New("completion lost after side effects")
+ }
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Mode: config.ConsolidationModeAutoConsolidate,
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 111,
+ MaxBlocks: 11,
+ })
+ require.NoError(err)
+ require.Equal(2, statsCalls)
+ require.Len(requests, 2)
+}
+
+func TestBatchConsolidatorAutoConsolidateSafeEndHeight(t *testing.T) {
+ require := testutil.Require(t)
+
+ safeEnd, ok := batchConsolidatorAutoConsolidateSafeEndHeight(120, 10)
+ require.True(ok)
+ require.Equal(uint64(112), safeEnd)
+
+ safeEnd, ok = batchConsolidatorAutoConsolidateSafeEndHeight(120, 0)
+ require.True(ok)
+ require.Equal(uint64(121), safeEnd)
+
+ _, ok = batchConsolidatorAutoConsolidateSafeEndHeight(8, 10)
+ require.False(ok)
+}
+
+func TestBatchConsolidatorHistoricalSafeEndHeight(t *testing.T) {
+ require := testutil.Require(t)
+
+ safeEnd, ok := batchConsolidatorHistoricalSafeEndHeight(120, 10)
+ require.True(ok)
+ require.Equal(uint64(112), safeEnd)
+
+ safeEnd, ok = batchConsolidatorHistoricalSafeEndHeight(120, 0)
+ require.True(ok)
+ require.Equal(uint64(121), safeEnd)
+
+ _, ok = batchConsolidatorHistoricalSafeEndHeight(8, 10)
+ require.False(ok)
+}
+
+func (s *batchConsolidatorTestSuite) TestPromoteFinalizedProcessesOnlyPlannedRange() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.Storage.Consolidation.Mode = config.ConsolidationModePromoteFinalized
+ var requests []*activity.BatchConsolidatorRequest
+ s.env.OnActivity(activity.ActivityBatchConsolidatorPlan, mock.Anything, mock.Anything).
+ Return(&activity.BatchConsolidatorPlanResponse{
+ StartHeight: 1000,
+ EndHeight: 1100,
+ LatestHeight: 1200,
+ SafePromotionHeight: 1190,
+ PromotionGateHeight: 1100,
+ }, nil)
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.EndHeight - request.StartHeight,
+ ConsolidatedBlocks: request.EndHeight - request.StartHeight,
+ }, nil
+ }).Once()
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ }).Once()
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1300,
+ MaxBlocks: 100,
+ })
+ require.NoError(err)
+ require.Equal([]*activity.BatchConsolidatorRequest{
+ {Tag: 2, StartHeight: 1000, EndHeight: 1100, MaxBlocks: 100},
+ {Tag: 2, StartHeight: 1000, EndHeight: 1100, MaxBlocks: 100},
+ }, requests)
+}
+
+func (s *batchConsolidatorTestSuite) TestPromoteFinalizedNoOpsWhenPlanHasNoSafeRange() {
+ require := testutil.Require(s.T())
+
+ s.cfg.Workflows.BatchConsolidator.Storage.Consolidation.Mode = config.ConsolidationModePromoteFinalized
+ s.env.OnActivity(activity.ActivityBatchConsolidatorPlan, mock.Anything, mock.Anything).
+ Return(&activity.BatchConsolidatorPlanResponse{
+ StartHeight: 1000,
+ EndHeight: 1000,
+ LatestHeight: 900,
+ SafePromotionHeight: 0,
+ PromotionGateHeight: 1200,
+ }, nil)
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1300,
+ MaxBlocks: 100,
+ })
+ require.NoError(err)
+}
+
+func (s *batchConsolidatorTestSuite) TestBatchConsolidatorAccountsLostActivityCompletionFromShadowStats() {
+ require := testutil.Require(s.T())
+
+ var requests []*activity.BatchConsolidatorRequest
+ statsCalls := 0
+ s.env.OnActivity(activity.ActivityBatchConsolidatorStats, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorStatsRequest) (*activity.BatchConsolidatorStatsResponse, error) {
+ statsCalls++
+ if statsCalls == 1 {
+ return &activity.BatchConsolidatorStatsResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ }
+ return &activity.BatchConsolidatorStatsResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ShadowObjects: 2,
+ ShadowBlocks: 50,
+ }, nil
+ })
+ call := 0
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ call++
+ switch call {
+ case 1:
+ return nil, xerrors.New("completion lost after side effects")
+ case 2:
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ScannedBlocks: request.MaxBlocks,
+ ConsolidatedBlocks: request.MaxBlocks,
+ ObjectKey: "consolidated/second.cscb.zstd",
+ }, nil
+ default:
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ }
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1100,
+ MaxBlocks: 25,
+ })
+ require.NoError(err)
+
+ require.GreaterOrEqual(statsCalls, 3)
+ require.Len(requests, 3)
+ for _, request := range requests {
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 1000, EndHeight: 1100, MaxBlocks: 25}, request)
+ }
+}
+
+func (s *batchConsolidatorTestSuite) TestBatchConsolidatorAccountsLostActivityCompletionWhenRetryScanIsEmpty() {
+ require := testutil.Require(s.T())
+
+ var requests []*activity.BatchConsolidatorRequest
+ statsCalls := 0
+ s.env.OnActivity(activity.ActivityBatchConsolidatorStats, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorStatsRequest) (*activity.BatchConsolidatorStatsResponse, error) {
+ statsCalls++
+ if statsCalls == 1 {
+ return &activity.BatchConsolidatorStatsResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ }
+ return &activity.BatchConsolidatorStatsResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ ShadowObjects: 1,
+ ShadowBlocks: 25,
+ }, nil
+ })
+ call := 0
+ s.env.OnActivity(activity.ActivityBatchConsolidator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorRequest) (*activity.BatchConsolidatorResponse, error) {
+ requests = append(requests, request)
+ call++
+ if call == 1 {
+ return nil, xerrors.New("completion lost after side effects")
+ }
+ return &activity.BatchConsolidatorResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+
+ _, err := s.batchConsolidator.Execute(context.Background(), &BatchConsolidatorRequest{
+ Tag: 2,
+ StartHeight: 1000,
+ EndHeight: 1100,
+ MaxBlocks: 25,
+ })
+ require.NoError(err)
+
+ require.Equal(2, statsCalls)
+ require.Len(requests, 2)
+ for _, request := range requests {
+ require.Equal(&activity.BatchConsolidatorRequest{Tag: 2, StartHeight: 1000, EndHeight: 1100, MaxBlocks: 25}, request)
+ }
+}
+
+func (s *batchConsolidatorTestSuite) TestShadowStatsActivityRetryPolicyUsesUnlimitedAttempts() {
+ require := testutil.Require(s.T())
+
+ activityRetry := s.cfg.Workflows.BatchConsolidator.ActivityRetry
+ activityRetry.MaximumAttempts = 3
+ policy := s.batchConsolidator.getShadowStatsActivityRetryPolicy(activityRetry)
+
+ require.NotNil(policy)
+ require.Equal(int32(0), policy.MaximumAttempts)
+ require.Equal(activityRetry.BackoffCoefficient, policy.BackoffCoefficient)
+ require.Equal(activityRetry.InitialInterval, policy.InitialInterval)
+ require.Equal(activityRetry.MaximumInterval, policy.MaximumInterval)
+ require.Equal(int32(3), activityRetry.MaximumAttempts)
+}
+
+func (s *batchConsolidatorTestSuite) TestCursorActivityRetryPolicyUsesUnlimitedAttempts() {
+ require := testutil.Require(s.T())
+
+ activityRetry := s.cfg.Workflows.BatchConsolidator.ActivityRetry
+ activityRetry.MaximumAttempts = 3
+ policy := s.batchConsolidator.getCursorActivityRetryPolicy(activityRetry)
+
+ require.NotNil(policy)
+ require.Equal(int32(0), policy.MaximumAttempts)
+ require.Equal(activityRetry.BackoffCoefficient, policy.BackoffCoefficient)
+ require.Equal(activityRetry.InitialInterval, policy.InitialInterval)
+ require.Equal(activityRetry.MaximumInterval, policy.MaximumInterval)
+ require.Equal(int32(3), activityRetry.MaximumAttempts)
+}
+
+func (s *batchConsolidatorTestSuite) mockEmptyShadowStats() {
+ s.env.OnActivity(activity.ActivityBatchConsolidatorStats, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorStatsRequest) (*activity.BatchConsolidatorStatsResponse, error) {
+ return &activity.BatchConsolidatorStatsResponse{
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ }, nil
+ })
+}
+
+func (s *batchConsolidatorTestSuite) mockAutoConsolidateLatestHeight(height uint64) {
+ s.env.OnActivity(activity.ActivityBatchConsolidatorLatestBlock, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorLatestBlockRequest) (*activity.BatchConsolidatorLatestBlockResponse, error) {
+ return &activity.BatchConsolidatorLatestBlockResponse{
+ Tag: request.Tag,
+ Height: height,
+ }, nil
+ })
+}
+
+func (s *batchConsolidatorTestSuite) mockAutoConsolidateCursorUpdate(height uint64) {
+ s.env.OnActivity(activity.ActivityBatchConsolidatorCursor, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.BatchConsolidatorCursorRequest) (*activity.BatchConsolidatorCursorResponse, error) {
+ require := testutil.Require(s.T())
+ require.Equal(uint64(2), uint64(request.Tag))
+ require.Equal(height, request.Height)
+ return &activity.BatchConsolidatorCursorResponse{
+ Tag: request.Tag,
+ Height: request.Height,
+ }, nil
+ })
+}
diff --git a/internal/workflow/benchmarker_test.go b/internal/workflow/benchmarker_test.go
index 346ffd3d..1d81f396 100644
--- a/internal/workflow/benchmarker_test.go
+++ b/internal/workflow/benchmarker_test.go
@@ -46,6 +46,7 @@ func (s *benchmarkerTestSuite) SetupTest() {
blockchainClient := clientmocks.NewMockClient(s.ctrl)
metadataAccessor := metastoragemocks.NewMockMetaStorage(s.ctrl)
blobStorage := blobstoragemocks.NewMockBlobStorage(s.ctrl)
+ singleBlockUploader := blobstoragemocks.NewMockSingleBlockUploader(s.ctrl)
s.app = testapp.New(
s.T(),
Module,
@@ -53,6 +54,9 @@ func (s *benchmarkerTestSuite) SetupTest() {
fx.Provide(func() blobstorage.BlobStorage {
return blobStorage
}),
+ fx.Provide(func() blobstorage.SingleBlockUploader {
+ return singleBlockUploader
+ }),
fx.Provide(fx.Annotated{
Name: "slave",
Target: func() client.Client {
diff --git a/internal/workflow/context.go b/internal/workflow/context.go
new file mode 100644
index 00000000..559b936a
--- /dev/null
+++ b/internal/workflow/context.go
@@ -0,0 +1,18 @@
+package workflow
+
+import "context"
+
+type workflowIDContextKey struct{}
+
+// WithWorkflowID overrides the default Temporal workflow ID for manual/admin starts.
+func WithWorkflowID(ctx context.Context, workflowID string) context.Context {
+ if workflowID == "" {
+ return ctx
+ }
+ return context.WithValue(ctx, workflowIDContextKey{}, workflowID)
+}
+
+func workflowIDFromContext(ctx context.Context) (string, bool) {
+ workflowID, ok := ctx.Value(workflowIDContextKey{}).(string)
+ return workflowID, ok && workflowID != ""
+}
diff --git a/internal/workflow/integration_test/migrator_integration_test.go b/internal/workflow/integration_test/migrator_integration_test.go
new file mode 100644
index 00000000..c210c79a
--- /dev/null
+++ b/internal/workflow/integration_test/migrator_integration_test.go
@@ -0,0 +1,409 @@
+package integration
+
+import (
+ "context"
+ "fmt"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/suite"
+ "go.temporal.io/sdk/testsuite"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/client"
+ "github.com/coinbase/chainstorage/internal/blockchain/jsonrpc"
+ "github.com/coinbase/chainstorage/internal/blockchain/parser"
+ "github.com/coinbase/chainstorage/internal/blockchain/restapi"
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/dlq"
+ "github.com/coinbase/chainstorage/internal/s3"
+ "github.com/coinbase/chainstorage/internal/storage"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/model"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage/postgres"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/internal/workflow"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type MigratorIntegrationTestSuite struct {
+ suite.Suite
+ testsuite.WorkflowTestSuite
+}
+
+type testDependencies struct {
+ fx.In
+ Migrator *workflow.Migrator
+ SingleBlockUploader blobstorage.SingleBlockUploader
+ MetaStorage metastorage.MetaStorage // DynamoDB
+ MetaStoragePG metastorage.MetaStorage `name:"pg"` // PostgreSQL
+ Parser parser.Parser
+ Client client.Client `name:"slave"`
+}
+
+func TestIntegrationMigratorTestSuite(t *testing.T) {
+ suite.Run(t, new(MigratorIntegrationTestSuite))
+}
+
+// Helper function to create test app with common dependencies
+func (s *MigratorIntegrationTestSuite) createTestApp(env *cadence.TestEnv, timeout time.Duration) (testapp.TestApp, *testDependencies) {
+ var deps testDependencies
+ env.SetTestTimeout(timeout)
+
+ app := testapp.New(
+ s.T(),
+ testapp.WithFunctional(),
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_ETHEREUM, common.Network_NETWORK_ETHEREUM_MAINNET),
+ cadence.WithTestEnv(env),
+ workflow.Module,
+ client.Module,
+ jsonrpc.Module,
+ restapi.Module,
+ s3.Module,
+ storage.Module,
+ parser.Module,
+ dlq.Module,
+ // Provide PostgreSQL MetaStorage with name "pg" for migration tests
+ fx.Provide(fx.Annotated{
+ Name: "pg",
+ Target: func(params postgres.Params) (metastorage.MetaStorage, error) {
+ result, err := postgres.NewMetaStorage(params)
+ if err != nil {
+ return nil, err
+ }
+ return result.MetaStorage, nil
+ },
+ }),
+ fx.Populate(&deps),
+ )
+
+ return app, &deps
+}
+
+// Helper function to create test events with blocks
+func (s *MigratorIntegrationTestSuite) createTestEvents(
+ ctx context.Context,
+ deps *testDependencies,
+ tag uint32,
+ eventTag uint32,
+ startSequence, endSequence int64,
+ includeReorg bool,
+) error {
+ require := testutil.Require(s.T())
+
+ events := make([]*model.EventEntry, 0, endSequence-startSequence)
+
+ // Create events with some BLOCK_ADDED and BLOCK_REMOVED events
+ for seq := startSequence; seq < endSequence; seq++ {
+ var eventType api.BlockchainEvent_Type
+ var blockHeight uint64
+ var blockHash string
+ var parentHash string
+
+ // Every 10th event is a BLOCK_ADDED
+ if seq%10 == 0 {
+ eventType = api.BlockchainEvent_BLOCK_ADDED
+ blockHeight = uint64(17035140 + seq/10)
+ blockHash = generateHash(blockHeight, 0)
+ if blockHeight > 17035140 {
+ parentHash = generateHash(blockHeight-1, 0)
+ }
+ } else if includeReorg && seq%100 == 5 {
+ // Add some BLOCK_REMOVED events for reorg simulation
+ eventType = api.BlockchainEvent_BLOCK_REMOVED
+ blockHeight = uint64(17035140 + seq/10)
+ blockHash = generateHash(blockHeight, 1) // Different hash for removed block
+ parentHash = generateHash(blockHeight-1, 0)
+ } else {
+ // Most events are other types (not block-related)
+ eventType = api.BlockchainEvent_UNKNOWN
+ blockHeight = uint64(17035140 + seq/10)
+ blockHash = ""
+ parentHash = ""
+ }
+
+ event := &model.EventEntry{
+ EventId: seq,
+ EventType: eventType,
+ BlockHeight: blockHeight,
+ BlockHash: blockHash,
+ ParentHash: parentHash,
+ Tag: tag,
+ EventTag: eventTag,
+ }
+ events = append(events, event)
+
+ // Add reorg at specific height
+ if includeReorg && seq == startSequence+50 {
+ // Add another BLOCK_ADDED at same height (reorg)
+ reorgEvent := &model.EventEntry{
+ EventId: seq + 1,
+ EventType: api.BlockchainEvent_BLOCK_ADDED,
+ BlockHeight: blockHeight,
+ BlockHash: generateHash(blockHeight, 2), // Different hash for reorg
+ ParentHash: parentHash,
+ Tag: tag,
+ EventTag: eventTag,
+ }
+ events = append(events, reorgEvent)
+ seq++ // Skip next sequence since we used it
+ }
+ }
+
+ // Store events in DynamoDB
+ err := deps.MetaStorage.AddEventEntries(ctx, eventTag, events)
+ require.NoError(err, "Failed to store events in DynamoDB")
+
+ // For each BLOCK_ADDED event, fetch and store the actual block
+ for _, event := range events {
+ if event.EventType == api.BlockchainEvent_BLOCK_ADDED {
+ // Fetch block from blockchain
+ block, err := deps.Client.GetBlockByHeight(ctx, tag, event.BlockHeight)
+ if err != nil {
+ // If can't fetch from chain, create a mock block
+ block = &api.Block{
+ Metadata: &api.BlockMetadata{
+ Tag: tag,
+ Height: event.BlockHeight,
+ Hash: event.BlockHash,
+ ParentHash: event.ParentHash,
+ ParentHeight: event.BlockHeight - 1,
+ },
+ }
+ }
+
+ // Upload to blob storage
+ objectKey, err := deps.SingleBlockUploader.Upload(ctx, block, api.Compression_GZIP)
+ require.NoError(err, "Failed to upload block at height %d", event.BlockHeight)
+
+ // Update metadata with object key
+ block.Metadata.ObjectKeyMain = objectKey
+
+ // Store in DynamoDB metadata storage
+ err = deps.MetaStorage.PersistBlockMetas(ctx, true, []*api.BlockMetadata{block.Metadata}, nil)
+ require.NoError(err, "Failed to store block in DynamoDB at height %d", event.BlockHeight)
+ }
+ }
+
+ return nil
+}
+
+// Helper to generate deterministic hash for testing
+func generateHash(height uint64, variant int) string {
+ return fmt.Sprintf("0x%x", height*1000+uint64(variant))
+}
+
+// Test event-driven migration
+func (s *MigratorIntegrationTestSuite) TestMigratorIntegration_EventDriven() {
+ const (
+ tag = uint32(1)
+ eventTag = uint32(3)
+ startSequence = int64(1000)
+ endSequence = int64(1500)
+ batchSize = 100
+ )
+
+ require := testutil.Require(s.T())
+
+ // Setup
+ env := cadence.NewTestEnv(s)
+ app, deps := s.createTestApp(env, 30*time.Minute)
+ defer app.Close()
+
+ ctx := context.Background()
+
+ // Create test events with blocks
+ err := s.createTestEvents(ctx, deps, tag, eventTag, startSequence, endSequence, false)
+ require.NoError(err)
+
+ // Execute event-driven migrator workflow
+ migratorRequest := &workflow.MigratorRequest{
+ StartEventSequence: startSequence,
+ EndEventSequence: endSequence,
+ Tag: tag,
+ EventTag: eventTag,
+ BatchSize: uint64(batchSize),
+ Parallelism: 4,
+ }
+
+ migratorRun, err := deps.Migrator.Execute(ctx, migratorRequest)
+ require.NoError(err)
+
+ err = migratorRun.Get(ctx, nil)
+ require.NoError(err)
+
+ // Verify migration
+ // Check that events were migrated to PostgreSQL
+ maxEventId, err := deps.MetaStoragePG.GetMaxEventId(ctx, eventTag)
+ require.NoError(err)
+ require.GreaterOrEqual(maxEventId, endSequence-1)
+
+ app.Logger().Info("Event-driven migration test passed",
+ zap.Int64("startSequence", startSequence),
+ zap.Int64("endSequence", endSequence),
+ zap.Int64("maxEventId", maxEventId))
+}
+
+// Test event-driven migration with reorgs
+func (s *MigratorIntegrationTestSuite) TestMigratorIntegration_WithReorgs() {
+ const (
+ tag = uint32(1)
+ eventTag = uint32(3)
+ startSequence = int64(2000)
+ endSequence = int64(2200)
+ batchSize = 50
+ )
+
+ require := testutil.Require(s.T())
+
+ // Setup
+ env := cadence.NewTestEnv(s)
+ app, deps := s.createTestApp(env, 20*time.Minute)
+ defer app.Close()
+
+ ctx := context.Background()
+
+ // Create test events with reorgs
+ err := s.createTestEvents(ctx, deps, tag, eventTag, startSequence, endSequence, true)
+ require.NoError(err)
+
+ // Execute event-driven migrator workflow
+ migratorRequest := &workflow.MigratorRequest{
+ StartEventSequence: startSequence,
+ EndEventSequence: endSequence,
+ Tag: tag,
+ EventTag: eventTag,
+ BatchSize: uint64(batchSize),
+ Parallelism: 2, // Lower parallelism for reorg handling
+ }
+
+ migratorRun, err := deps.Migrator.Execute(ctx, migratorRequest)
+ require.NoError(err)
+
+ err = migratorRun.Get(ctx, nil)
+ require.NoError(err)
+
+ app.Logger().Info("Migration with reorgs test passed",
+ zap.Int64("startSequence", startSequence),
+ zap.Int64("endSequence", endSequence))
+}
+
+// Test auto-resume functionality
+func (s *MigratorIntegrationTestSuite) TestMigratorIntegration_AutoResume() {
+ const (
+ tag = uint32(1)
+ eventTag = uint32(3)
+ initialStart = int64(3000)
+ midPoint = int64(3100)
+ endSequence = int64(3200)
+ batchSize = 50
+ )
+
+ require := testutil.Require(s.T())
+
+ // Setup
+ env := cadence.NewTestEnv(s)
+ app, deps := s.createTestApp(env, 20*time.Minute)
+ defer app.Close()
+
+ ctx := context.Background()
+
+ // Create all test events
+ err := s.createTestEvents(ctx, deps, tag, eventTag, initialStart, endSequence, false)
+ require.NoError(err)
+
+ // First migration - partial
+ firstRequest := &workflow.MigratorRequest{
+ StartEventSequence: initialStart,
+ EndEventSequence: midPoint,
+ Tag: tag,
+ EventTag: eventTag,
+ BatchSize: uint64(batchSize),
+ Parallelism: 4,
+ }
+
+ migratorRun, err := deps.Migrator.Execute(ctx, firstRequest)
+ require.NoError(err)
+
+ err = migratorRun.Get(ctx, nil)
+ require.NoError(err)
+
+ // Second migration - auto-resume
+ resumeRequest := &workflow.MigratorRequest{
+ StartEventSequence: 0, // Will be auto-detected
+ EndEventSequence: endSequence,
+ Tag: tag,
+ EventTag: eventTag,
+ BatchSize: uint64(batchSize),
+ Parallelism: 4,
+ AutoResume: true,
+ }
+
+ migratorRun, err = deps.Migrator.Execute(ctx, resumeRequest)
+ require.NoError(err)
+
+ err = migratorRun.Get(ctx, nil)
+ require.NoError(err)
+
+ // Verify all events migrated
+ maxEventId, err := deps.MetaStoragePG.GetMaxEventId(ctx, eventTag)
+ require.NoError(err)
+ require.GreaterOrEqual(maxEventId, endSequence-1)
+
+ app.Logger().Info("Auto-resume migration test passed",
+ zap.Int64("finalMaxEventId", maxEventId))
+}
+
+// Test large batch migration with checkpointing
+func (s *MigratorIntegrationTestSuite) TestMigratorIntegration_LargeBatch() {
+ const (
+ tag = uint32(1)
+ eventTag = uint32(3)
+ startSequence = int64(10000)
+ endSequence = int64(15000) // 5000 events
+ batchSize = 500
+ checkpointSize = 2000
+ )
+
+ require := testutil.Require(s.T())
+
+ // Setup
+ env := cadence.NewTestEnv(s)
+ app, deps := s.createTestApp(env, 30*time.Minute)
+ defer app.Close()
+
+ ctx := context.Background()
+
+ // Create test events
+ err := s.createTestEvents(ctx, deps, tag, eventTag, startSequence, endSequence, false)
+ require.NoError(err)
+
+ // Execute with checkpoint configuration
+ migratorRequest := &workflow.MigratorRequest{
+ StartEventSequence: startSequence,
+ EndEventSequence: endSequence,
+ Tag: tag,
+ EventTag: eventTag,
+ BatchSize: uint64(batchSize),
+ CheckpointSize: uint64(checkpointSize),
+ Parallelism: 8,
+ }
+
+ migratorRun, err := deps.Migrator.Execute(ctx, migratorRequest)
+ require.NoError(err)
+
+ // This should trigger checkpoints during execution
+ err = migratorRun.Get(ctx, nil)
+ // May get continue-as-new error due to checkpointing
+ if err != nil {
+ require.Contains(err.Error(), "continue as new")
+ }
+
+ app.Logger().Info("Large batch migration test completed",
+ zap.Int64("startSequence", startSequence),
+ zap.Int64("endSequence", endSequence))
+}
diff --git a/internal/workflow/migrator.go b/internal/workflow/migrator.go
new file mode 100644
index 00000000..9e930667
--- /dev/null
+++ b/internal/workflow/migrator.go
@@ -0,0 +1,389 @@
+package workflow
+
+import (
+ "context"
+ "fmt"
+ "strconv"
+ "time"
+
+ "go.temporal.io/sdk/client"
+ "go.temporal.io/sdk/workflow"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ "github.com/coinbase/chainstorage/internal/workflow/activity"
+)
+
+type (
+ Migrator struct {
+ baseWorkflow
+ migrator *activity.Migrator
+ getLatestBlockHeight *activity.GetLatestBlockHeightActivity
+ getLatestBlockFromPostgres *activity.GetLatestBlockFromPostgresActivity
+ getLatestEventFromPostgres *activity.GetLatestEventFromPostgresActivity
+ getMaxEventId *activity.GetMaxEventIdActivity
+ }
+
+ MigratorParams struct {
+ fx.In
+ fxparams.Params
+ Runtime cadence.Runtime
+ Migrator *activity.Migrator
+ GetLatestBlockHeight *activity.GetLatestBlockHeightActivity
+ GetLatestBlockFromPostgres *activity.GetLatestBlockFromPostgresActivity
+ GetLatestEventFromPostgres *activity.GetLatestEventFromPostgresActivity
+ GetMaxEventId *activity.GetMaxEventIdActivity
+ }
+
+ MigratorRequest struct {
+ StartEventSequence int64 // Start event sequence
+ EndEventSequence int64 // End event sequence (0 = auto-detect)
+ EventTag uint32
+ Tag uint32
+ BatchSize uint64 // Optional. If not specified, it is read from the workflow config.
+ CheckpointSize uint64 // Optional. If not specified, it is read from the workflow config.
+ Parallelism int // Optional. If not specified, it is read from the workflow config.
+ BackoffInterval string // Optional. If not specified, it is read from the workflow config.
+ ContinuousSync bool // Optional. Whether to continuously sync data in infinite loop mode
+ SyncInterval string // Optional. Interval for continuous sync (e.g., "1m", "30s"). Defaults to 1 minute if not specified or invalid.
+ AutoResume bool // Optional. Automatically determine StartEventSequence from latest event in PostgreSQL destination
+ }
+)
+
+var (
+ _ InstrumentedRequest = (*MigratorRequest)(nil)
+)
+
+const (
+ // migrator metrics. need to have `workflow.migrator` as prefix
+ migratorHeightGauge = "workflow.migrator.height"
+ migratorBlocksCounter = "workflow.migrator.blocks_migrated"
+ migratorEventsCounter = "workflow.migrator.events_migrated"
+ migratorProgressGauge = "workflow.migrator.progress"
+)
+
+func NewMigrator(params MigratorParams) *Migrator {
+ w := &Migrator{
+ baseWorkflow: newBaseWorkflow(¶ms.Config.Workflows.Migrator, params.Runtime),
+ migrator: params.Migrator,
+ getLatestBlockHeight: params.GetLatestBlockHeight,
+ getLatestBlockFromPostgres: params.GetLatestBlockFromPostgres,
+ getLatestEventFromPostgres: params.GetLatestEventFromPostgres,
+ getMaxEventId: params.GetMaxEventId,
+ }
+ w.registerWorkflow(w.execute)
+ return w
+}
+
+func (w *Migrator) Execute(ctx context.Context, request *MigratorRequest) (client.WorkflowRun, error) {
+ workflowID := w.name
+ if request.Tag != 0 {
+ workflowID = fmt.Sprintf("%s/block_tag=%d", w.name, request.Tag)
+ }
+ return w.startMigratorWorkflow(ctx, workflowID, request)
+}
+
+// startMigratorWorkflow starts a migrator workflow with a custom reuse policy
+// that allows restarting failed workflows but prevents concurrent execution
+func (w *Migrator) startMigratorWorkflow(ctx context.Context, workflowID string, request *MigratorRequest) (client.WorkflowRun, error) {
+ if err := w.validateRequestCtx(ctx, request); err != nil {
+ return nil, err
+ }
+ cfg := w.config.Base()
+ workflowOptions := client.StartWorkflowOptions{
+ ID: workflowID,
+ TaskQueue: cfg.TaskList,
+ WorkflowRunTimeout: cfg.WorkflowRunTimeout,
+ WorkflowExecutionErrorWhenAlreadyStarted: true,
+ RetryPolicy: w.getRetryPolicy(cfg.WorkflowRetry),
+ }
+ execution, err := w.runtime.ExecuteWorkflow(ctx, workflowOptions, w.name, request)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to execute workflow: %w", err)
+ }
+ return execution, nil
+}
+
+func (w *Migrator) execute(ctx workflow.Context, request *MigratorRequest) error {
+ return w.executeWorkflow(ctx, request, func() error {
+ if err := w.validateRequest(request); err != nil {
+ return err
+ }
+
+ var cfg config.MigratorWorkflowConfig
+ if err := w.readConfig(ctx, &cfg); err != nil {
+ return xerrors.Errorf("failed to read config: %w", err)
+ }
+
+ // Event-driven migration always processes both blocks and events
+
+ batchSize := cfg.BatchSize
+ if request.BatchSize > 0 {
+ batchSize = request.BatchSize
+ }
+
+ checkpointSize := cfg.CheckpointSize
+ if request.CheckpointSize > 0 {
+ checkpointSize = request.CheckpointSize
+ }
+
+ parallelism := cfg.Parallelism
+ if parallelism <= 0 {
+ parallelism = 1 // Fallback default if config not set
+ }
+ if request.Parallelism > 0 {
+ parallelism = request.Parallelism
+ }
+
+ backoffInterval := cfg.BackoffInterval
+ if request.BackoffInterval != "" {
+ parsedInterval, err := time.ParseDuration(request.BackoffInterval)
+ if err != nil {
+ return xerrors.Errorf("failed to parse backoff interval: %w", err)
+ }
+ backoffInterval = parsedInterval
+ }
+
+ // Use config's continuous sync setting as default if not explicitly set in request
+ continuousSync := cfg.ContinuousSync || request.ContinuousSync
+
+ syncInterval := defaultSyncInterval
+ if cfg.SyncInterval > 0 {
+ syncInterval = cfg.SyncInterval
+ }
+ if request.SyncInterval != "" {
+ interval, err := time.ParseDuration(request.SyncInterval)
+ if err == nil {
+ syncInterval = interval
+ }
+ }
+
+ tag := cfg.GetEffectiveBlockTag(request.Tag)
+ eventTag := cfg.GetEffectiveEventTag(request.EventTag)
+ metrics := w.getMetricsHandler(ctx).WithTags(map[string]string{
+ tagBlockTag: strconv.Itoa(int(tag)),
+ })
+ logger := w.getLogger(ctx).With(
+ zap.Reflect("request", request),
+ zap.Reflect("config", cfg),
+ zap.Uint32("effectiveEventTag", eventTag),
+ )
+
+ // Set up activity options early so we can use activities
+ ctx = w.withActivityOptions(ctx)
+
+ // Handle auto-resume functionality - use latest event sequence
+ if request.AutoResume && request.StartEventSequence == 0 {
+ logger.Info("AutoResume enabled, querying PostgreSQL destination for latest migrated event")
+ postgresEventResp, err := w.getLatestEventFromPostgres.Execute(ctx, &activity.GetLatestEventFromPostgresRequest{EventTag: eventTag})
+ if err != nil {
+ return xerrors.Errorf("failed to get latest event from PostgreSQL: %w", err)
+ }
+
+ if postgresEventResp.Found {
+ // Resume from the next event sequence
+ request.StartEventSequence = postgresEventResp.Sequence + 1
+ logger.Info("Auto-resume: found latest event in PostgreSQL destination",
+ zap.Int64("latestEventSequence", postgresEventResp.Sequence),
+ zap.Int64("resumeFromSequence", request.StartEventSequence))
+ } else {
+ // No events found in destination, start from the beginning
+ request.StartEventSequence = 1 // Events start at 1
+ logger.Info("Auto-resume: no events found in PostgreSQL destination, starting from beginning")
+ }
+ }
+
+ // Handle end event sequence auto-detection if not provided
+ if request.EndEventSequence == 0 {
+ logger.Info("No end event sequence provided, fetching max event ID from DynamoDB...")
+
+ // Query DynamoDB for the actual max event ID
+ maxEventResp, err := w.getMaxEventId.Execute(ctx, &activity.GetMaxEventIdRequest{
+ EventTag: eventTag,
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to get max event ID from DynamoDB: %w", err)
+ }
+
+ if !maxEventResp.Found {
+ logger.Warn("No events found in DynamoDB")
+ if continuousSync {
+ // In continuous sync, if no events exist yet, wait and retry
+ logger.Info("No events in DynamoDB, waiting for sync interval before retry",
+ zap.Duration("syncInterval", syncInterval))
+ err := workflow.Sleep(ctx, syncInterval)
+ if err != nil {
+ return xerrors.Errorf("workflow sleep failed while waiting for events: %w", err)
+ }
+ // Continue as new to retry
+ newRequest := *request
+ return workflow.NewContinueAsNewError(ctx, w.name, &newRequest)
+ }
+ return xerrors.New("No events found in DynamoDB to migrate")
+ }
+
+ request.EndEventSequence = maxEventResp.MaxEventId
+ logger.Info("Found max event ID in DynamoDB",
+ zap.Int64("maxEventId", maxEventResp.MaxEventId),
+ zap.Int64("startEventSequence", request.StartEventSequence))
+ }
+
+ // Validate end sequence after auto-detection and auto-resume
+ if !continuousSync && request.StartEventSequence >= request.EndEventSequence {
+ return xerrors.Errorf("startEventSequence (%d) must be less than endEventSequence (%d)",
+ request.StartEventSequence, request.EndEventSequence)
+ }
+
+ // Additional handling for continuous sync:
+ // If EndEventSequence <= StartEventSequence, we are caught up.
+ if continuousSync && request.EndEventSequence != 0 && request.EndEventSequence <= request.StartEventSequence {
+ logger.Info("Continuous sync: caught up (no new events).",
+ zap.Int64("startEventSequence", request.StartEventSequence),
+ zap.Int64("endEventSequence", request.EndEventSequence))
+
+ // No special event catch-up needed - normal migration handles this
+
+ // Prepare for next cycle
+ newRequest := *request
+ newRequest.StartEventSequence = request.EndEventSequence
+ newRequest.EndEventSequence = 0 // re-detect on next cycle
+
+ // Wait for syncInterval before starting a new continuous sync workflow
+ logger.Info("waiting for sync interval before next catch-up cycle",
+ zap.Duration("syncInterval", syncInterval))
+ err := workflow.Sleep(ctx, syncInterval)
+ if err != nil {
+ return xerrors.Errorf("workflow sleep failed during caught-up continuous sync: %w", err)
+ }
+
+ logger.Info("starting next continuous sync cycle after catch-up",
+ zap.Int64("nextStartEventSequence", newRequest.StartEventSequence))
+ return workflow.NewContinueAsNewError(ctx, w.name, &newRequest)
+ }
+
+ // Special case: if auto-resume found we're already caught up
+ if request.AutoResume && request.StartEventSequence >= request.EndEventSequence {
+ logger.Info("Auto-resume detected: already caught up, no migration needed",
+ zap.Int64("startEventSequence", request.StartEventSequence),
+ zap.Int64("endEventSequence", request.EndEventSequence))
+ return nil // Successfully completed with no work to do
+ }
+
+ // Log migration mode
+ logger.Info("Starting event-driven migration workflow")
+
+ logger.Info("migrator workflow started")
+
+ totalEventRange := request.EndEventSequence - request.StartEventSequence
+ processedEvents := int64(0)
+
+ for batchStart := request.StartEventSequence; batchStart < request.EndEventSequence; batchStart += int64(batchSize) {
+ // Check for checkpoint - only check after processing at least one batch
+ processedSoFar := batchStart - request.StartEventSequence
+ if processedSoFar > 0 && processedSoFar >= int64(checkpointSize) {
+ newRequest := *request
+ newRequest.StartEventSequence = batchStart
+ logger.Info("checkpoint reached", zap.Reflect("newRequest", newRequest))
+ return workflow.NewContinueAsNewError(ctx, w.name, &newRequest)
+ }
+
+ batchEnd := batchStart + int64(batchSize)
+ if batchEnd > request.EndEventSequence {
+ batchEnd = request.EndEventSequence
+ }
+
+ logger.Info("migrating event batch",
+ zap.Int64("batchStart", batchStart),
+ zap.Int64("batchEnd", batchEnd))
+
+ // Execute a single migrator activity for the entire batch.
+ migratorRequest := &activity.MigratorRequest{
+ StartEventSequence: batchStart,
+ EndEventSequence: batchEnd,
+ EventTag: eventTag,
+ Tag: tag,
+ Parallelism: parallelism,
+ }
+
+ response, err := w.migrator.Execute(ctx, migratorRequest)
+ if err != nil {
+ logger.Error(
+ "failed to migrate batch",
+ zap.Int64("batchStart", batchStart),
+ zap.Int64("batchEnd", batchEnd),
+ zap.Error(err),
+ )
+ return xerrors.Errorf("failed to migrate batch [%v, %v): %w", batchStart, batchEnd, err)
+ }
+ if !response.Success {
+ logger.Error(
+ "migration batch failed",
+ zap.Int64("batchStart", batchStart),
+ zap.Int64("batchEnd", batchEnd),
+ zap.String("message", response.Message),
+ )
+ return xerrors.Errorf("migration batch failed [%v, %v): %s", batchStart, batchEnd, response.Message)
+ }
+
+ // Update metrics for the whole batch after all shards complete
+ processedEvents += batchEnd - batchStart
+ progress := float64(processedEvents) / float64(totalEventRange) * 100
+
+ metrics.Gauge(migratorHeightGauge).Update(float64(batchEnd - 1))
+ metrics.Counter(migratorBlocksCounter).Inc(int64(response.BlocksMigrated))
+ metrics.Counter(migratorEventsCounter).Inc(int64(response.EventsMigrated))
+ metrics.Gauge(migratorProgressGauge).Update(progress)
+
+ logger.Info(
+ "migrated batch successfully",
+ zap.Int64("batchStart", batchStart),
+ zap.Int64("batchEnd", batchEnd),
+ zap.Int("blocksMigrated", response.BlocksMigrated),
+ zap.Int("eventsMigrated", response.EventsMigrated),
+ zap.Float64("progress", progress),
+ )
+
+ // Add backoff if configured
+ if backoffInterval > 0 {
+ _ = workflow.Sleep(ctx, backoffInterval)
+ }
+ }
+
+ if continuousSync {
+ logger.Info("continuous sync enabled, preparing for next sync cycle")
+ newRequest := *request
+ newRequest.StartEventSequence = request.EndEventSequence
+ newRequest.EndEventSequence = 0 // Will be auto-detected on next cycle
+ newRequest.AutoResume = false // AutoResume should only happen on first workflow run
+
+ // Wait for syncInterval before starting a new continuous sync workflow
+ logger.Info("waiting for sync interval before next cycle",
+ zap.Duration("syncInterval", syncInterval))
+ err := workflow.Sleep(ctx, syncInterval)
+ if err != nil {
+ return xerrors.Errorf("workflow sleep failed during continuous sync: %w", err)
+ }
+
+ logger.Info("starting new continuous sync workflow",
+ zap.Int64("nextStartEventSequence", newRequest.StartEventSequence),
+ zap.Reflect("newRequest", newRequest))
+ return workflow.NewContinueAsNewError(ctx, w.name, &newRequest)
+ }
+
+ logger.Info("migrator workflow finished",
+ zap.Int64("totalEvents", totalEventRange),
+ zap.Int64("processedEvents", processedEvents))
+
+ return nil
+ })
+}
+
+func (r *MigratorRequest) GetTags() map[string]string {
+ return map[string]string{
+ tagBlockTag: strconv.Itoa(int(r.Tag)),
+ }
+}
diff --git a/internal/workflow/migrator_test.go b/internal/workflow/migrator_test.go
new file mode 100644
index 00000000..d59f51c3
--- /dev/null
+++ b/internal/workflow/migrator_test.go
@@ -0,0 +1,325 @@
+package workflow
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/suite"
+ "go.temporal.io/sdk/testsuite"
+ "go.uber.org/fx"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/internal/workflow/activity"
+)
+
+const (
+ migratorCheckpointSize = 50000
+ migratorBatchSize = 5000
+)
+
+type migratorTestSuite struct {
+ suite.Suite
+ testsuite.WorkflowTestSuite
+ env *cadence.TestEnv
+ migrator *Migrator
+ app testapp.TestApp
+ cfg *config.Config
+}
+
+func TestMigratorTestSuite(t *testing.T) {
+ suite.Run(t, new(migratorTestSuite))
+}
+
+func (s *migratorTestSuite) SetupTest() {
+ require := testutil.Require(s.T())
+
+ // Override config to speed up the test
+ cfg, err := config.New()
+ require.NoError(err)
+ cfg.Workflows.Migrator.BatchSize = migratorBatchSize
+ cfg.Workflows.Migrator.CheckpointSize = migratorCheckpointSize
+ cfg.Workflows.Migrator.BackoffInterval = time.Second
+ cfg.Workflows.Migrator.Parallelism = 8
+ s.cfg = cfg
+
+ s.env = cadence.NewTestEnv(s)
+ s.app = testapp.New(
+ s.T(),
+ Module,
+ testapp.WithConfig(s.cfg),
+ cadence.WithTestEnv(s.env),
+ fx.Populate(&s.migrator),
+ )
+}
+
+func (s *migratorTestSuite) TearDownTest() {
+ s.app.Close()
+ s.env.AssertExpectations(s.T())
+}
+
+func (s *migratorTestSuite) TestMigrator_EventDriven_Success() {
+ require := testutil.Require(s.T())
+
+ startSequence := int64(1000)
+ endSequence := int64(6000) // 5000 events, fits in one batch
+ tag := uint32(1)
+ eventTag := uint32(0)
+
+ s.env.OnActivity(activity.ActivityMigrator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.MigratorRequest) (*activity.MigratorResponse, error) {
+ require.Equal(tag, request.Tag)
+ expectedEventTag := s.cfg.Workflows.Migrator.GetEffectiveEventTag(eventTag)
+ require.Equal(expectedEventTag, request.EventTag)
+
+ // Event-driven migration processes both blocks and events
+ eventCount := request.EndEventSequence - request.StartEventSequence
+ blockCount := eventCount / 10 // Assume roughly 10% are BLOCK_ADDED events
+
+ return &activity.MigratorResponse{
+ BlocksMigrated: int(blockCount),
+ EventsMigrated: int(eventCount),
+ Success: true,
+ Message: "Event-driven migration completed successfully",
+ }, nil
+ })
+
+ _, err := s.migrator.Execute(context.Background(), &MigratorRequest{
+ StartEventSequence: startSequence,
+ EndEventSequence: endSequence,
+ Tag: tag,
+ EventTag: eventTag,
+ })
+ require.NoError(err)
+}
+
+func (s *migratorTestSuite) TestMigrator_WithCheckpoint() {
+ require := testutil.Require(s.T())
+
+ startSequence := int64(1000)
+ endSequence := startSequence + int64(migratorCheckpointSize) + 10000 // Exceed checkpoint
+ tag := uint32(1)
+ eventTag := uint32(0)
+
+ s.env.OnActivity(activity.ActivityMigrator, mock.Anything, mock.Anything).
+ Return(&activity.MigratorResponse{
+ BlocksMigrated: 500,
+ EventsMigrated: 5000,
+ Success: true,
+ Message: "Migration batch completed",
+ }, nil)
+
+ _, err := s.migrator.Execute(context.Background(), &MigratorRequest{
+ StartEventSequence: startSequence,
+ EndEventSequence: endSequence,
+ Tag: tag,
+ EventTag: eventTag,
+ })
+ require.Error(err)
+ require.True(IsContinueAsNewError(err))
+}
+
+func (s *migratorTestSuite) TestMigrator_CustomBatchSize() {
+ require := testutil.Require(s.T())
+
+ startSequence := int64(1000)
+ endSequence := int64(3000)
+ tag := uint32(1)
+ eventTag := uint32(0)
+ customBatchSize := uint64(500)
+
+ callCount := 0
+ s.env.OnActivity(activity.ActivityMigrator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.MigratorRequest) (*activity.MigratorResponse, error) {
+ callCount++
+ // Each batch should be customBatchSize
+ batchSize := request.EndEventSequence - request.StartEventSequence
+ require.LessOrEqual(batchSize, int64(customBatchSize))
+
+ return &activity.MigratorResponse{
+ BlocksMigrated: 50,
+ EventsMigrated: int(batchSize),
+ Success: true,
+ Message: "Batch completed",
+ }, nil
+ })
+
+ _, err := s.migrator.Execute(context.Background(), &MigratorRequest{
+ StartEventSequence: startSequence,
+ EndEventSequence: endSequence,
+ Tag: tag,
+ EventTag: eventTag,
+ BatchSize: customBatchSize,
+ })
+ require.NoError(err)
+ // Should have been called 4 times (2000 events / 500 per batch)
+ require.Equal(4, callCount)
+}
+
+func (s *migratorTestSuite) TestMigrator_AutoResume() {
+ require := testutil.Require(s.T())
+
+ tag := uint32(1)
+ eventTag := uint32(3)
+
+ // Mock GetLatestEventFromPostgres to return a sequence
+ s.env.OnActivity(activity.ActivityGetLatestEventFromPostgres, mock.Anything, mock.Anything).
+ Return(&activity.GetLatestEventFromPostgresResponse{
+ Sequence: int64(5000),
+ Height: uint64(1000),
+ Found: true,
+ }, nil).Once()
+
+ // The workflow should resume from sequence 5001
+ s.env.OnActivity(activity.ActivityMigrator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.MigratorRequest) (*activity.MigratorResponse, error) {
+ // Should start from next sequence after latest
+ require.Equal(int64(5001), request.StartEventSequence)
+
+ return &activity.MigratorResponse{
+ BlocksMigrated: 100,
+ EventsMigrated: 1000,
+ Success: true,
+ Message: "Resumed migration",
+ }, nil
+ })
+
+ _, err := s.migrator.Execute(context.Background(), &MigratorRequest{
+ StartEventSequence: 0, // Will be auto-detected
+ EndEventSequence: 10000,
+ Tag: tag,
+ EventTag: eventTag,
+ AutoResume: true,
+ })
+ require.NoError(err)
+}
+
+func (s *migratorTestSuite) TestMigrator_ContinuousSync() {
+ require := testutil.Require(s.T())
+
+ startSequence := int64(1000)
+ tag := uint32(1)
+ eventTag := uint32(0)
+
+ // Mock GetMaxEventId activity since EndEventSequence is 0
+ s.env.OnActivity(activity.ActivityGetMaxEventId, mock.Anything, mock.Anything).
+ Return(&activity.GetMaxEventIdResponse{
+ MaxEventId: int64(6000),
+ Found: true,
+ }, nil).Once()
+
+ callCount := 0
+ s.env.OnActivity(activity.ActivityMigrator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.MigratorRequest) (*activity.MigratorResponse, error) {
+ callCount++
+ if callCount > 2 {
+ // Stop after 2 iterations to prevent infinite loop in test
+ s.env.CancelWorkflow()
+ }
+
+ return &activity.MigratorResponse{
+ BlocksMigrated: 100,
+ EventsMigrated: 1000,
+ Success: true,
+ Message: "Batch migrated",
+ }, nil
+ })
+
+ _, err := s.migrator.Execute(context.Background(), &MigratorRequest{
+ StartEventSequence: startSequence,
+ EndEventSequence: 0, // Will be auto-detected for continuous sync
+ Tag: tag,
+ EventTag: eventTag,
+ ContinuousSync: true,
+ SyncInterval: "1s",
+ })
+
+ // Should get a continue-as-new error for continuous sync
+ if err != nil {
+ require.True(IsContinueAsNewError(err) || s.env.IsWorkflowCompleted())
+ }
+}
+
+func (s *migratorTestSuite) TestMigrator_Parallelism() {
+ require := testutil.Require(s.T())
+
+ startSequence := int64(1000)
+ endSequence := int64(6000)
+ tag := uint32(1)
+ eventTag := uint32(0)
+ parallelism := 4
+
+ s.env.OnActivity(activity.ActivityMigrator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.MigratorRequest) (*activity.MigratorResponse, error) {
+ // Verify parallelism is passed through
+ require.Equal(parallelism, request.Parallelism)
+
+ eventCount := request.EndEventSequence - request.StartEventSequence
+ return &activity.MigratorResponse{
+ BlocksMigrated: int(eventCount / 10),
+ EventsMigrated: int(eventCount),
+ Success: true,
+ Message: "Parallel migration completed",
+ }, nil
+ })
+
+ _, err := s.migrator.Execute(context.Background(), &MigratorRequest{
+ StartEventSequence: startSequence,
+ EndEventSequence: endSequence,
+ Tag: tag,
+ EventTag: eventTag,
+ Parallelism: parallelism,
+ })
+ require.NoError(err)
+}
+
+func (s *migratorTestSuite) TestMigrator_LargeMigration() {
+ require := testutil.Require(s.T())
+
+ startSequence := int64(1000000)
+ endSequence := int64(2000000) // 1 million events
+ tag := uint32(1)
+ eventTag := uint32(0)
+
+ batchCount := 0
+ s.env.OnActivity(activity.ActivityMigrator, mock.Anything, mock.Anything).
+ Return(func(ctx context.Context, request *activity.MigratorRequest) (*activity.MigratorResponse, error) {
+ batchCount++
+
+ // Each batch should be the configured batch size
+ batchSize := request.EndEventSequence - request.StartEventSequence
+ require.LessOrEqual(batchSize, int64(migratorBatchSize))
+
+ // Simulate processing
+ blockCount := batchSize / 10
+
+ return &activity.MigratorResponse{
+ BlocksMigrated: int(blockCount),
+ EventsMigrated: int(batchSize),
+ Success: true,
+ Message: "Large batch processed",
+ }, nil
+ })
+
+ // This should trigger checkpoints
+ _, err := s.migrator.Execute(context.Background(), &MigratorRequest{
+ StartEventSequence: startSequence,
+ EndEventSequence: endSequence,
+ Tag: tag,
+ EventTag: eventTag,
+ BatchSize: migratorBatchSize,
+ CheckpointSize: migratorCheckpointSize,
+ })
+
+ // Should hit checkpoint and continue-as-new
+ require.Error(err)
+ require.True(IsContinueAsNewError(err))
+
+ // Should have processed checkpoint size worth of events
+ expectedBatches := int(migratorCheckpointSize / migratorBatchSize)
+ require.Equal(expectedBatches, batchCount)
+}
diff --git a/internal/workflow/module.go b/internal/workflow/module.go
index c966fb67..5c4696b3 100644
--- a/internal/workflow/module.go
+++ b/internal/workflow/module.go
@@ -17,6 +17,9 @@ var Module = fx.Options(
fx.Provide(NewCrossValidator),
fx.Provide(NewEventBackfiller),
fx.Provide(NewReplicator),
+ fx.Provide(NewMigrator),
+ fx.Provide(NewBatchConsolidator),
+ fx.Provide(NewSingleBlockRetention),
)
const (
diff --git a/internal/workflow/poller.go b/internal/workflow/poller.go
index 68832d3f..9e938d1a 100644
--- a/internal/workflow/poller.go
+++ b/internal/workflow/poller.go
@@ -204,6 +204,19 @@ func (w *Poller) execute(ctx workflow.Context, request *PollerRequest) error {
}
sessionCtx, err = workflow.CreateSession(ctx, so)
if err != nil {
+ if IsErrSessionFailed(ctx, err) || IsScheduleToStartTimeout(err) {
+ request.RetryableErrorCount++
+ errMetricName := w.getRetryableErrorMetricName(ctx, err)
+ metrics.Counter(errMetricName).Inc(1)
+
+ if request.RetryableErrorCount <= RetryableErrorLimit {
+ if err := workflow.Sleep(ctx, backoffInterval); err != nil {
+ return err
+ }
+ return w.continueAsNew(ctx, request)
+ }
+ return xerrors.Errorf("retryable errors on session creation exceeded threshold: %w", err)
+ }
return xerrors.Errorf("failed to create workflow session: %w", err)
}
defer workflow.CompleteSession(sessionCtx)
diff --git a/internal/workflow/poller_test.go b/internal/workflow/poller_test.go
index 76dc8a84..89f86b32 100644
--- a/internal/workflow/poller_test.go
+++ b/internal/workflow/poller_test.go
@@ -44,6 +44,7 @@ type pollerTestSuite struct {
env *cadence.TestEnv
metaStorage *metastoragemocks.MockMetaStorage
blobStorage *blobstoragemocks.MockBlobStorage
+ singleBlockUploader *blobstoragemocks.MockSingleBlockUploader
masterBlockchainClient *clientmocks.MockClient
slaveBlockchainClient *clientmocks.MockClient
validatorBlockchainClient *clientmocks.MockClient
@@ -105,7 +106,12 @@ func (s *pollerTestSuite) SetupTest() {
EnableSessionWorker: true,
})
s.metaStorage = metastoragemocks.NewMockMetaStorage(s.ctrl)
+ s.metaStorage.EXPECT().
+ AcquireSingleBlockUploadGuard(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
+ Return(nil, nil).
+ AnyTimes()
s.blobStorage = blobstoragemocks.NewMockBlobStorage(s.ctrl)
+ s.singleBlockUploader = blobstoragemocks.NewMockSingleBlockUploader(s.ctrl)
s.masterBlockchainClient = clientmocks.NewMockClient(s.ctrl)
s.slaveBlockchainClient = clientmocks.NewMockClient(s.ctrl)
s.validatorBlockchainClient = clientmocks.NewMockClient(s.ctrl)
@@ -118,6 +124,9 @@ func (s *pollerTestSuite) SetupTest() {
fx.Provide(func() blobstorage.BlobStorage {
return s.blobStorage
}),
+ fx.Provide(func() blobstorage.SingleBlockUploader {
+ return s.singleBlockUploader
+ }),
fx.Provide(func() metastorage.MetaStorage {
return s.metaStorage
}),
@@ -204,7 +213,7 @@ func (s *pollerTestSuite) TestPollerSuccess() {
s.slaveBlockchainClient.EXPECT().
GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&api.Block{Metadata: &api.BlockMetadata{Height: k}}, nil)
- s.blobStorage.EXPECT().
+ s.singleBlockUploader.EXPECT().
Upload(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
@@ -271,7 +280,7 @@ func (s *pollerTestSuite) TestPollerSuccess_FastSync() {
s.slaveBlockchainClient.EXPECT().
GetBlockByHeight(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&api.Block{Metadata: &api.BlockMetadata{Height: k}}, nil)
- s.blobStorage.EXPECT().
+ s.singleBlockUploader.EXPECT().
Upload(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
@@ -369,7 +378,7 @@ func (s *pollerTestSuite) TestPollerSuccess_FailoverEnabled() {
require.True(s.slaveEndpointProvider.HasFailoverContext(ctx))
return testutil.MakeBlocksFromStartHeight(curHeight, 1, tag)[0], nil
})
- s.blobStorage.EXPECT().
+ s.singleBlockUploader.EXPECT().
Upload(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
@@ -476,7 +485,7 @@ func (s *pollerTestSuite) TestPollerSuccess_ConsensusFailover() {
require.True(s.consensusEndpointProvider.HasFailoverContext(ctx))
return testutil.MakeBlocksFromStartHeight(curHeight, 1, tag)[0], nil
})
- s.blobStorage.EXPECT().
+ s.singleBlockUploader.EXPECT().
Upload(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
@@ -555,7 +564,7 @@ func (s *pollerTestSuite) TestPollerFailure_ConsensusAutomaticFailover() {
s.slaveBlockchainClient.EXPECT().
GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(testutil.MakeBlocksFromStartHeight(curHeight, 1, tag)[0], nil)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("someObjectKey", nil).AnyTimes()
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("someObjectKey", nil).AnyTimes()
}
latestFinalizedHeight := localHeight + heightGap - s.cfg.Chain.IrreversibleDistance + 1
s.metaStorage.EXPECT().GetBlockByHeight(gomock.Any(), tag, latestFinalizedHeight).
@@ -605,7 +614,7 @@ func (s *pollerTestSuite) TestPollerFailure_ConsensusValidationFailure() {
s.slaveBlockchainClient.EXPECT().
GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(testutil.MakeBlocksFromStartHeight(curHeight, 1, tag)[0], nil)
- s.blobStorage.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("someObjectKey", nil).AnyTimes()
+ s.singleBlockUploader.EXPECT().Upload(gomock.Any(), gomock.Any(), gomock.Any()).Return("someObjectKey", nil).AnyTimes()
}
latestFinalizedHeight := localHeight + heightGap - s.cfg.Chain.IrreversibleDistance + 1
s.metaStorage.EXPECT().GetBlockByHeight(gomock.Any(), tag, latestFinalizedHeight).
@@ -672,7 +681,7 @@ func (s *pollerTestSuite) TestPollerSuccess_ConsensusEnabled() {
s.slaveBlockchainClient.EXPECT().
GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(testutil.MakeBlocksFromStartHeight(curHeight, 1, tag)[0], nil)
- s.blobStorage.EXPECT().
+ s.singleBlockUploader.EXPECT().
Upload(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
seen.blocks.LoadOrStore(block.Metadata.Height, true)
@@ -761,7 +770,7 @@ func (s *pollerTestSuite) TestPollerSuccessAfterMasterFallback() {
Return(&api.Block{Metadata: &api.BlockMetadata{Height: k}}, nil)
}
for k := start; k < end; k++ {
- s.blobStorage.EXPECT().
+ s.singleBlockUploader.EXPECT().
Upload(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
@@ -844,7 +853,7 @@ func (s *pollerTestSuite) TestPollerSuccessAfterReprocessing() {
s.masterBlockchainClient.EXPECT().
GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&api.Block{Metadata: &api.BlockMetadata{Height: k}}, nil)
- s.blobStorage.EXPECT().
+ s.singleBlockUploader.EXPECT().
Upload(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
@@ -1036,7 +1045,7 @@ func (s *pollerTestSuite) TestPollerWithTransactionIndexingSuccess() {
Transactions: []string{"0xa"},
},
}, nil)
- s.blobStorage.EXPECT().
+ s.singleBlockUploader.EXPECT().
Upload(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
@@ -1122,7 +1131,7 @@ func (s *pollerTestSuite) TestPoller_WithLivenessCheck() {
s.slaveBlockchainClient.EXPECT().
GetBlockByHash(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).
Return(&api.Block{Metadata: &api.BlockMetadata{Height: k}}, nil)
- s.blobStorage.EXPECT().
+ s.singleBlockUploader.EXPECT().
Upload(gomock.Any(), gomock.Any(), gomock.Any()).
DoAndReturn(func(ctx context.Context, block *api.Block, compression api.Compression) (string, error) {
require.Equal(api.Compression_GZIP, compression)
diff --git a/internal/workflow/replicator.go b/internal/workflow/replicator.go
index d4f9ea9f..9eede0f3 100644
--- a/internal/workflow/replicator.go
+++ b/internal/workflow/replicator.go
@@ -3,6 +3,8 @@ package workflow
import (
"context"
"strconv"
+ "strings"
+ "time"
"go.temporal.io/sdk/client"
"go.temporal.io/sdk/workflow"
@@ -10,6 +12,7 @@ import (
"go.uber.org/zap"
"golang.org/x/xerrors"
+ "github.com/coinbase/chainstorage/internal/blockchain/parser"
"github.com/coinbase/chainstorage/internal/cadence"
"github.com/coinbase/chainstorage/internal/config"
"github.com/coinbase/chainstorage/internal/utils/fxparams"
@@ -22,6 +25,7 @@ type (
Replicator struct {
baseWorkflow
replicator *activity.Replicator
+ latestBLock *activity.LatestBlock
updateWatermark *activity.UpdateWatermark
}
@@ -30,22 +34,34 @@ type (
fxparams.Params
Runtime cadence.Runtime
Replicator *activity.Replicator
+ LatestBLock *activity.LatestBlock
UpdateWatermark *activity.UpdateWatermark
}
ReplicatorRequest struct {
Tag uint32
StartHeight uint64
- EndHeight uint64 `validate:"gt=0,gtfield=StartHeight"`
+ EndHeight uint64 `validate:"eq=0|gtfield=StartHeight"`
UpdateWatermark bool
DataCompression string // Optional. If not specified, it is read from the workflow config.
BatchSize uint64 // Optional. If not specified, it is read from the workflow config.
MiniBatchSize uint64 // Optional. If not specified, it is read from the workflow config.
CheckpointSize uint64 // Optional. If not specified, it is read from the workflow config.
Parallelism int // Optional. If not specified, it is read from the workflow config.
+ ContinuousSync bool // Optional. Whether to continuously sync data
+ SyncInterval string // Optional. Interval for continuous sync
}
)
+const defaultSyncInterval = 1 * time.Minute
+
+const (
+ // Replicator metrics. need to have `workflow.replicator` as prefix
+ replicatorHeightGauge = "workflow.replicator.height"
+ replicatorGapGauge = "workflow.replicator.gap"
+ replicatorTimeSinceLastBlockGauge = "workflow.replicator.time_since_last_block"
+)
+
// GetTags implements InstrumentedRequest.
func (r *ReplicatorRequest) GetTags() map[string]string {
return map[string]string{
@@ -61,6 +77,7 @@ func NewReplicator(params ReplicatorParams) *Replicator {
w := &Replicator{
baseWorkflow: newBaseWorkflow(¶ms.Config.Workflows.Replicator, params.Runtime),
replicator: params.Replicator,
+ latestBLock: params.LatestBLock,
updateWatermark: params.UpdateWatermark,
}
w.registerWorkflow(w.execute)
@@ -121,6 +138,27 @@ func (w *Replicator) execute(ctx workflow.Context, request *ReplicatorRequest) e
logger.Info("workflow started", zap.Uint64("batchSize", batchSize))
ctx = w.withActivityOptions(ctx)
+ metrics := w.runtime.GetMetricsHandler(ctx).WithTags(map[string]string{
+ tagBlockTag: strconv.Itoa(int(request.Tag)),
+ })
+
+ syncInterval := defaultSyncInterval
+ if request.SyncInterval != "" {
+ interval, err := time.ParseDuration(request.SyncInterval)
+ if err == nil {
+ syncInterval = interval
+ }
+ }
+
+ if request.ContinuousSync && request.EndHeight == 0 {
+ latestBlockResponse, err := w.latestBLock.Execute(ctx, &activity.LatestBlockRequest{})
+ if err != nil {
+ return xerrors.Errorf("failed to get latest block through activity: %w", err)
+ }
+ var chainConfig config.ChainConfig
+ request.EndHeight = latestBlockResponse.Height - chainConfig.IrreversibleDistance
+ }
+
for startHeight := request.StartHeight; startHeight < request.EndHeight; startHeight = startHeight + batchSize {
if startHeight >= request.StartHeight+checkpointSize {
newRequest := *request
@@ -148,6 +186,9 @@ func (w *Replicator) execute(ctx workflow.Context, request *ReplicatorRequest) e
reprocessChannel := workflow.NewNamedBufferedChannel(ctx, "replicator.reprocess", miniBatchCount)
defer reprocessChannel.Close()
+ responsesChannel := workflow.NewNamedBufferedChannel(ctx, "replicator.mini-batches.response", parallelism+miniBatchCount)
+ defer responsesChannel.Close()
+
// Phase 1: running mini batches in parallel.
for i := 0; i < parallelism; i++ {
workflow.Go(ctx, func(ctx workflow.Context) {
@@ -161,7 +202,7 @@ func (w *Replicator) execute(ctx workflow.Context, request *ReplicatorRequest) e
if batchEnd > endHeight {
batchEnd = endHeight
}
- _, err := w.replicator.Execute(ctx, &activity.ReplicatorRequest{
+ replicatorResponse, err := w.replicator.Execute(ctx, &activity.ReplicatorRequest{
Tag: tag,
StartHeight: batchStart,
EndHeight: batchEnd,
@@ -176,6 +217,7 @@ func (w *Replicator) execute(ctx workflow.Context, request *ReplicatorRequest) e
zap.Error(err),
)
}
+ responsesChannel.Send(ctx, *replicatorResponse)
}
})
}
@@ -192,7 +234,7 @@ func (w *Replicator) execute(ctx workflow.Context, request *ReplicatorRequest) e
if batchEnd > endHeight {
batchEnd = endHeight
}
- _, err := w.replicator.Execute(ctx, &activity.ReplicatorRequest{
+ retryResponse, err := w.replicator.Execute(ctx, &activity.ReplicatorRequest{
Tag: tag,
StartHeight: batchStart,
EndHeight: batchEnd,
@@ -202,19 +244,89 @@ func (w *Replicator) execute(ctx workflow.Context, request *ReplicatorRequest) e
if err != nil {
return xerrors.Errorf("failed to replicate block from %d to %d: %w", batchStart, batchEnd, err)
}
+ responsesChannel.Send(ctx, *retryResponse)
}
// Phase 3: update watermark
if request.UpdateWatermark {
+ var validateStart uint64
+ if startHeight == 0 {
+ validateStart = startHeight
+ } else {
+ validateStart = startHeight - 1
+ }
_, err := w.updateWatermark.Execute(ctx, &activity.UpdateWatermarkRequest{
Tag: request.Tag,
- ValidateStart: startHeight - 1,
+ ValidateStart: validateStart,
BlockHeight: endHeight - 1,
})
if err != nil {
+ // Check if the error is due to chain discontinuity (reorg)
+ // Use string matching because Temporal error serialization breaks xerrors.Is
+ if strings.Contains(err.Error(), parser.ErrInvalidChain.Error()) {
+ // Reorg detected - restart from a safe point
+ logger.Warn("Chain discontinuity detected, likely due to reorg. Restarting from earlier height",
+ zap.Uint64("currentStartHeight", startHeight),
+ zap.Uint64("irreversibleDistance", cfg.IrreversibleDistance),
+ zap.Error(err))
+
+ // Calculate the new start height by going back by irreversible_distance
+ // Ensure we go back at least 1 block, even if IrreversibleDistance is 0
+ var newStartHeight uint64
+ reorgDistance := cfg.IrreversibleDistance
+ if reorgDistance == 0 {
+ reorgDistance = 1 // Go back at least 1 block
+ }
+
+ if startHeight > reorgDistance {
+ newStartHeight = startHeight - reorgDistance
+ } else {
+ newStartHeight = 0
+ }
+
+ // Create a new request starting from the safe point
+ newRequest := *request
+ newRequest.StartHeight = newStartHeight
+ logger.Info("Restarting replicator workflow after reorg",
+ zap.Uint64("newStartHeight", newStartHeight),
+ zap.Uint64("endHeight", request.EndHeight))
+
+ // Continue as new workflow to handle the reorg
+ return workflow.NewContinueAsNewError(ctx, w.name, &newRequest)
+ }
return xerrors.Errorf("failed to update watermark: %w", err)
}
}
+
+ var latestResp activity.ReplicatorResponse
+ for {
+ var resp activity.ReplicatorResponse
+ if ok := responsesChannel.ReceiveAsync(&resp); !ok {
+ break
+ }
+ if resp.LatestBlockHeight > latestResp.LatestBlockHeight {
+ latestResp = resp
+ }
+ }
+ if latestResp != (activity.ReplicatorResponse{}) {
+ metrics.Gauge(replicatorHeightGauge).Update(float64(latestResp.LatestBlockHeight))
+ metrics.Gauge(replicatorGapGauge).Update(float64(request.EndHeight - latestResp.LatestBlockHeight + 1))
+ metrics.Gauge(replicatorTimeSinceLastBlockGauge).Update(utils.SinceTimestamp(latestResp.LatestBlockTimestamp).Seconds())
+ }
+ }
+
+ if request.ContinuousSync {
+ logger.Info("new continuous sync workflow")
+ newRequest := *request
+ newRequest.StartHeight = request.EndHeight
+ newRequest.EndHeight = 0
+ // Wait for syncInterval minutes before starting a new continuous sync workflow.
+ err := workflow.Sleep(ctx, syncInterval)
+ if err != nil {
+ return xerrors.Errorf("workflow await failed: %w", err)
+ }
+ logger.Info("start new continuous sync workflow")
+ return workflow.NewContinueAsNewError(ctx, w.name, &newRequest)
}
logger.Info("workflow finished")
diff --git a/internal/workflow/single_block_retention.go b/internal/workflow/single_block_retention.go
new file mode 100644
index 00000000..73275ab3
--- /dev/null
+++ b/internal/workflow/single_block_retention.go
@@ -0,0 +1,1164 @@
+package workflow
+
+import (
+ "context"
+ "math"
+ "sort"
+ "strconv"
+ "time"
+
+ "go.temporal.io/sdk/client"
+ "go.temporal.io/sdk/temporal"
+ "go.temporal.io/sdk/workflow"
+ "go.uber.org/fx"
+ "go.uber.org/zap"
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+ "github.com/coinbase/chainstorage/internal/utils/fxparams"
+ "github.com/coinbase/chainstorage/internal/workflow/activity"
+)
+
+type (
+ SingleBlockRetention struct {
+ baseWorkflow
+ activity *activity.SingleBlockRetention
+ }
+
+ SingleBlockRetentionParams struct {
+ fx.In
+ fxparams.Params
+ Runtime cadence.Runtime
+ Activity *activity.SingleBlockRetention
+ }
+
+ SingleBlockRetentionRequest struct {
+ Tag uint32
+ StartHeight uint64
+ EndHeight uint64
+ // EligibilityCutoff freezes the destructive set. Dry runs may omit it
+ // and return the captured cutoff; new execute sweeps must reuse that
+ // exact dry-run cutoff across every continuation.
+ EligibilityCutoff time.Time
+ // MaxObjectRanges bounds each workflow run. Execute runs continue as new
+ // with the same approved envelope while more eligible cohorts remain.
+ MaxObjectRanges int `validate:"omitempty,gt=0,lte=250"`
+ // Parallelism bounds concurrent cohort lifecycles. Each lifecycle owns
+ // one CSCB object, including its optional safety-quiescence retry.
+ // Zero preserves the serial default for existing callers.
+ Parallelism int `validate:"omitempty,gt=0"`
+ Execute bool
+ ProductionDeleteEnabled bool
+ DirectStorageClientsGuarded bool
+ SingleBlockWritersGuarded bool
+ FallbackReadsValidated bool
+ FallbackErrorCount uint64
+ // Approved* are the operator's separate exact-envelope deletion approval.
+ // Execution requires them, requires the selection range to equal the
+ // approved envelope, and passes them through unchanged across every
+ // continuation. Selected cohorts must be contained by this envelope;
+ // approval is never derived from selector output. Read-only runs may
+ // omit them.
+ ApprovedChain string
+ ApprovedStartHeight uint64
+ ApprovedEndHeight uint64
+
+ // Checkpoint is internal continue-as-new state. Initial workflow
+ // executions reject caller-supplied checkpoints.
+ Checkpoint *SingleBlockRetentionCheckpoint
+ }
+
+ SingleBlockRetentionCheckpoint struct {
+ StartedAt time.Time
+ EligibilityCutoff time.Time
+ EffectiveTag uint32
+ ContinueAsNewCount uint64
+ SelectedObjectRanges uint64
+ ProcessedObjectRanges uint64
+ CompletedObjectRangeCount uint64
+ ScannedRows uint64
+ PlannedRows uint64
+ DeletedVerifiedRows uint64
+ AlreadyRetiredRows uint64
+ SkippedSlots uint64
+ DeferredRows uint64
+ FailedRows uint64
+ DeletedVersions uint64
+ DeletedMarkers uint64
+ RetiredBytes uint64
+ LastCompletedObjectRange *SingleBlockRetentionCompletedRange
+ }
+
+ SingleBlockRetentionCompletedRange struct {
+ ConsolidatedObjectKey string `json:"consolidated_object_key"`
+ StartHeight uint64 `json:"start_height"`
+ EndHeight uint64 `json:"end_height"`
+ EligibleRows uint64 `json:"eligible_rows"`
+ }
+
+ SingleBlockRetentionResult struct {
+ StartedAt time.Time `json:"started_at"`
+ CompletedAt time.Time `json:"completed_at"`
+ EligibilityCutoff time.Time `json:"eligibility_cutoff"`
+ Tag uint32 `json:"tag"`
+ SelectionStartHeight uint64 `json:"selection_start_height"`
+ SelectionEndHeight uint64 `json:"selection_end_height"`
+ Execute bool `json:"execute"`
+ ApprovedChain string `json:"approved_chain,omitempty"`
+ ApprovedStartHeight uint64 `json:"approved_start_height,omitempty"`
+ ApprovedEndHeight uint64 `json:"approved_end_height,omitempty"`
+ FallbackReadsValidated bool `json:"fallback_reads_validated"`
+ FallbackErrorCount uint64 `json:"fallback_error_count"`
+ Parallelism int `json:"parallelism"`
+ ContinueAsNewCount uint64 `json:"continue_as_new_count"`
+ SweepCompleted bool `json:"sweep_completed"`
+ SelectedObjectRanges uint64 `json:"selected_object_ranges"`
+ MoreEligibleRanges bool `json:"more_eligible_ranges"`
+ ProcessedObjectRanges uint64 `json:"processed_object_ranges"`
+ CompletedObjectRangeCount uint64 `json:"completed_object_range_count"`
+ LastCompletedObjectRange *SingleBlockRetentionCompletedRange `json:"last_completed_object_range,omitempty"`
+ // CompletedObjectRanges and RangeResults contain only the current
+ // Temporal run. The scalar counters above are cumulative across the
+ // full continue-as-new chain.
+ CompletedObjectRanges []SingleBlockRetentionCompletedRange `json:"completed_object_ranges,omitempty"`
+ RangeResults []*activity.SingleBlockRetentionRangeResult `json:"range_results,omitempty"`
+ ScannedRows uint64 `json:"scanned_rows"`
+ PlannedRows uint64 `json:"planned_rows"`
+ DeletedVerifiedRows uint64 `json:"deleted_verified_rows"`
+ AlreadyRetiredRows uint64 `json:"already_retired_rows"`
+ SkippedSlots uint64 `json:"skipped_slots"`
+ DeferredRows uint64 `json:"deferred_rows"`
+ FailedRows uint64 `json:"failed_rows"`
+ DeletedVersions uint64 `json:"deleted_versions"`
+ DeletedMarkers uint64 `json:"deleted_markers"`
+ RetiredBytes uint64 `json:"retired_bytes"`
+ FailureMessage string `json:"failure_message,omitempty"`
+ }
+
+ SingleBlockRetentionFailureDetails struct {
+ FailureMessage string `json:"failure_message"`
+ Parallelism int `json:"parallelism"`
+ CurrentRunSelectedObjectRanges uint64 `json:"current_run_selected_object_ranges"`
+ CurrentRunLaunchedObjectRanges uint64 `json:"current_run_launched_object_ranges"`
+ SelectedObjectRanges uint64 `json:"selected_object_ranges"`
+ ProcessedObjectRanges uint64 `json:"processed_object_ranges"`
+ CompletedObjectRangeCount uint64 `json:"completed_object_range_count"`
+ DeletedVerifiedRows uint64 `json:"deleted_verified_rows"`
+ FailedRows uint64 `json:"failed_rows"`
+ DeferredRows uint64 `json:"deferred_rows"`
+ CohortOutcomes []SingleBlockRetentionCohortFailureOutcome `json:"cohort_outcomes"`
+ }
+
+ SingleBlockRetentionCohortFailureOutcome struct {
+ Cohort retirement.RetentionCohort `json:"cohort"`
+ Result *activity.SingleBlockRetentionRangeResult `json:"result,omitempty"`
+ FailureMessage string `json:"failure_message,omitempty"`
+ }
+
+ singleBlockRetentionCohortOutcome struct {
+ cohort retirement.RetentionCohort
+ result *activity.SingleBlockRetentionRangeResult
+ err error
+ }
+)
+
+var _ InstrumentedRequest = (*SingleBlockRetentionRequest)(nil)
+
+const (
+ maxSingleBlockRetentionRetryDelay = 30 * time.Minute
+ singleBlockRetentionMaxParallelism = 20
+
+ singleBlockRetentionRangeSweepChangeID = "single_block_retention.range_sweep"
+ singleBlockRetentionRangeSweepVersion = 1
+ singleBlockRetentionParallelismChangeID = "single_block_retention.parallelism"
+ singleBlockRetentionParallelismVersion = 1
+ singleBlockRetentionPartialFailureType = "single_block_retention_partial_failure"
+)
+
+func NewSingleBlockRetention(params SingleBlockRetentionParams) *SingleBlockRetention {
+ w := &SingleBlockRetention{
+ baseWorkflow: newBaseWorkflow(¶ms.Config.Workflows.SingleBlockRetention, params.Runtime),
+ activity: params.Activity,
+ }
+ w.registerWorkflow(w.execute)
+ return w
+}
+
+func (w *SingleBlockRetention) Execute(
+ ctx context.Context,
+ request *SingleBlockRetentionRequest,
+) (client.WorkflowRun, error) {
+ workflowID := w.name
+ if override, ok := workflowIDFromContext(ctx); ok {
+ workflowID = override
+ }
+ return w.startWorkflow(ctx, workflowID, request)
+}
+
+func (r *SingleBlockRetentionRequest) GetTags() map[string]string {
+ tag := uint32(0)
+ if r != nil {
+ tag = r.Tag
+ }
+ return map[string]string{
+ tagBlockTag: strconv.Itoa(int(tag)),
+ }
+}
+
+func (w *SingleBlockRetention) execute(
+ ctx workflow.Context,
+ request *SingleBlockRetentionRequest,
+) (*SingleBlockRetentionResult, error) {
+ result := newSingleBlockRetentionResult(request, workflow.Now(ctx).UTC())
+ failureDetailsEnabled := false
+ currentRunSelectedObjectRanges := uint64(0)
+ cohortOutcomes := make([]SingleBlockRetentionCohortFailureOutcome, 0)
+ err := w.executeWorkflow(ctx, request, func() error {
+ var cfg config.SingleBlockRetentionWorkflowConfig
+ if err := w.readConfig(ctx, &cfg); err != nil {
+ return xerrors.Errorf("failed to read config: %w", err)
+ }
+ if err := validateSingleBlockRetentionExecutionRequest(request); err != nil {
+ return err
+ }
+ rangeSweepEnabled := workflow.GetVersion(
+ ctx,
+ singleBlockRetentionRangeSweepChangeID,
+ workflow.DefaultVersion,
+ singleBlockRetentionRangeSweepVersion,
+ ) != workflow.DefaultVersion
+ isContinuation := workflow.GetInfo(ctx).ContinuedExecutionRunID != ""
+ if request.Execute && rangeSweepEnabled && request.EligibilityCutoff.IsZero() {
+ return xerrors.New("single-block retention range execution requires the eligibility cutoff from its approved dry run")
+ }
+ if result.EligibilityCutoff.After(workflow.Now(ctx).UTC()) {
+ return xerrors.Errorf(
+ "single-block retention eligibility cutoff %s is in the future",
+ result.EligibilityCutoff,
+ )
+ }
+
+ maxObjectRanges := cfg.MaxObjectRanges
+ if request.MaxObjectRanges > 0 {
+ maxObjectRanges = request.MaxObjectRanges
+ }
+ if maxObjectRanges <= 0 || maxObjectRanges > retirement.MaxRetentionCohortsPerWorkflow {
+ return xerrors.Errorf(
+ "single_block_retention max_object_ranges must be between 1 and %d: %d",
+ retirement.MaxRetentionCohortsPerWorkflow,
+ maxObjectRanges,
+ )
+ }
+ parallelism := 1
+ if request.Parallelism > 0 {
+ parallelism = request.Parallelism
+ }
+ if parallelism > singleBlockRetentionMaxParallelism {
+ return xerrors.Errorf(
+ "single_block_retention parallelism(%d) exceeds max(%d)",
+ parallelism,
+ singleBlockRetentionMaxParallelism,
+ )
+ }
+ parallelismVersion := workflow.GetVersion(
+ ctx,
+ singleBlockRetentionParallelismChangeID,
+ workflow.DefaultVersion,
+ singleBlockRetentionParallelismVersion,
+ )
+ failureDetailsEnabled = parallelismVersion != workflow.DefaultVersion
+ if parallelismVersion == workflow.DefaultVersion && parallelism != 1 {
+ return xerrors.Errorf(
+ "legacy single_block_retention execution requires parallelism=1, got %d",
+ parallelism,
+ )
+ }
+ tag := cfg.GetEffectiveBlockTag(request.Tag)
+ // Preserve the deployed workflow's resolved numeric tag on the legacy
+ // version path so replayed activity commands remain compatible.
+ activityTag := tag
+ activityEligibilityCutoff := time.Time{}
+ if rangeSweepEnabled {
+ // Pin both values before crossing the workflow/activity boundary.
+ // Workers may observe a newer stable tag during a rolling deploy.
+ activityTag = encodeSingleBlockRetentionEffectiveTag(tag)
+ activityEligibilityCutoff = result.EligibilityCutoff
+ }
+ result.Tag = tag
+ result.Parallelism = parallelism
+ result.SelectionStartHeight = request.StartHeight
+ result.SelectionEndHeight = request.EndHeight
+ if err := validateSingleBlockRetentionCheckpoint(
+ request,
+ isContinuation,
+ rangeSweepEnabled,
+ tag,
+ ); err != nil {
+ return err
+ }
+
+ logger := w.getLogger(ctx).With(
+ zap.Uint32("effective_tag", tag),
+ zap.Int("max_object_ranges", maxObjectRanges),
+ zap.Int("parallelism", parallelism),
+ zap.Uint64("selection_start_height", request.StartHeight),
+ zap.Uint64("selection_end_height", request.EndHeight),
+ zap.Time("eligibility_cutoff", result.EligibilityCutoff),
+ zap.Bool("execute", request.Execute),
+ )
+ logger.Info("single-block retention workflow started")
+ activityCtx := w.withActivityOptions(ctx)
+ selected, err := w.activity.Select(activityCtx, &activity.SingleBlockRetentionSelectRequest{
+ Tag: activityTag,
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ EligibilityCutoff: activityEligibilityCutoff,
+ Limit: maxObjectRanges,
+ })
+ if err != nil {
+ return xerrors.Errorf("failed to select retention cohorts: %w", err)
+ }
+ if len(selected.Cohorts) > maxObjectRanges {
+ return xerrors.Errorf(
+ "single-block retention selector returned %d cohorts above limit %d",
+ len(selected.Cohorts),
+ maxObjectRanges,
+ )
+ }
+ if selected.HasMore && len(selected.Cohorts) == 0 {
+ return xerrors.New("single-block retention selector reported a backlog without returning a cohort")
+ }
+ result.SelectedObjectRanges += uint64(len(selected.Cohorts))
+ currentRunSelectedObjectRanges = uint64(len(selected.Cohorts))
+ result.MoreEligibleRanges = selected.HasMore
+
+ if parallelismVersion != workflow.DefaultVersion {
+ if err := validateSingleBlockRetentionCohorts(
+ selected.Cohorts,
+ request,
+ rangeSweepEnabled,
+ ); err != nil {
+ return err
+ }
+ }
+ if parallelismVersion != workflow.DefaultVersion && parallelism > 1 {
+ outcomes, err := w.processSingleBlockRetentionCohortsParallel(
+ ctx,
+ activityCtx,
+ logger,
+ request,
+ activityTag,
+ activityEligibilityCutoff,
+ selected.Cohorts,
+ parallelism,
+ rangeSweepEnabled,
+ failureDetailsEnabled,
+ result,
+ )
+ cohortOutcomes = append(cohortOutcomes, outcomes...)
+ if err != nil {
+ return err
+ }
+ } else {
+ for _, cohort := range selected.Cohorts {
+ rangeResult, err := w.processSingleBlockRetentionCohort(
+ ctx,
+ activityCtx,
+ logger,
+ request,
+ activityTag,
+ activityEligibilityCutoff,
+ cohort,
+ rangeSweepEnabled,
+ failureDetailsEnabled,
+ )
+ result.addRangeResult(rangeResult)
+ if failureDetailsEnabled {
+ cohortOutcomes = append(
+ cohortOutcomes,
+ newSingleBlockRetentionCohortFailureOutcome(cohort, rangeResult, err),
+ )
+ }
+ if err != nil {
+ return err
+ }
+ }
+ }
+ if request.Execute && rangeSweepEnabled && !result.MoreEligibleRanges && len(selected.Cohorts) > 0 {
+ remaining, err := w.activity.Select(
+ activityCtx,
+ &activity.SingleBlockRetentionSelectRequest{
+ Tag: activityTag,
+ StartHeight: request.StartHeight,
+ EndHeight: request.EndHeight,
+ EligibilityCutoff: activityEligibilityCutoff,
+ Limit: 1,
+ },
+ )
+ if err != nil {
+ return xerrors.Errorf("failed to confirm retention sweep completion: %w", err)
+ }
+ if len(remaining.Cohorts) > 1 {
+ return xerrors.Errorf(
+ "single-block retention completion selector returned %d cohorts above limit 1",
+ len(remaining.Cohorts),
+ )
+ }
+ if remaining.HasMore && len(remaining.Cohorts) == 0 {
+ return xerrors.New("single-block retention completion selector reported a backlog without returning a cohort")
+ }
+ result.MoreEligibleRanges = remaining.HasMore || len(remaining.Cohorts) > 0
+ }
+ if request.Execute && rangeSweepEnabled && result.MoreEligibleRanges {
+ nextRequest := *request
+ nextRequest.Tag = encodeSingleBlockRetentionEffectiveTag(tag)
+ nextRequest.Checkpoint = result.checkpoint()
+ nextRequest.Checkpoint.ContinueAsNewCount++
+ logger.Info(
+ "single-block retention workflow continuing as new",
+ zap.Uint64("continue_as_new_count", nextRequest.Checkpoint.ContinueAsNewCount),
+ zap.Uint64("selected_object_ranges", result.SelectedObjectRanges),
+ zap.Uint64("processed_object_ranges", result.ProcessedObjectRanges),
+ zap.Uint64("completed_object_range_count", result.CompletedObjectRangeCount),
+ zap.Uint64("deleted_verified_rows", result.DeletedVerifiedRows),
+ zap.Uint64("retired_bytes", result.RetiredBytes),
+ )
+ return w.continueAsNew(ctx, &nextRequest)
+ }
+ result.SweepCompleted = request.Execute && !result.MoreEligibleRanges
+ logger.Info(
+ "single-block retention workflow completed",
+ zap.Bool("sweep_completed", result.SweepCompleted),
+ zap.Uint64("continue_as_new_count", result.ContinueAsNewCount),
+ zap.Uint64("selected_object_ranges", result.SelectedObjectRanges),
+ zap.Bool("more_eligible_ranges", result.MoreEligibleRanges),
+ zap.Uint64("processed_object_ranges", result.ProcessedObjectRanges),
+ zap.Uint64("completed_object_range_count", result.CompletedObjectRangeCount),
+ zap.Uint64("deleted_verified_rows", result.DeletedVerifiedRows),
+ zap.Uint64("retired_bytes", result.RetiredBytes),
+ )
+ return nil
+ })
+ result.CompletedAt = workflow.Now(ctx).UTC()
+ if err != nil {
+ result.FailureMessage = err.Error()
+ if failureDetailsEnabled && len(cohortOutcomes) > 0 && !IsContinueAsNewError(err) {
+ failureDetails := SingleBlockRetentionFailureDetails{
+ FailureMessage: err.Error(),
+ Parallelism: result.Parallelism,
+ CurrentRunSelectedObjectRanges: currentRunSelectedObjectRanges,
+ CurrentRunLaunchedObjectRanges: uint64(len(cohortOutcomes)),
+ SelectedObjectRanges: result.SelectedObjectRanges,
+ ProcessedObjectRanges: result.ProcessedObjectRanges,
+ CompletedObjectRangeCount: result.CompletedObjectRangeCount,
+ DeletedVerifiedRows: result.DeletedVerifiedRows,
+ FailedRows: result.FailedRows,
+ DeferredRows: result.DeferredRows,
+ CohortOutcomes: cohortOutcomes,
+ }
+ return result, temporal.NewApplicationErrorWithOptions(
+ err.Error(),
+ singleBlockRetentionPartialFailureType,
+ temporal.ApplicationErrorOptions{
+ Cause: err,
+ Details: []interface{}{failureDetails},
+ },
+ )
+ }
+ }
+ return result, err
+}
+
+func (w *SingleBlockRetention) processSingleBlockRetentionCohortsParallel(
+ ctx workflow.Context,
+ activityCtx workflow.Context,
+ logger *zap.Logger,
+ request *SingleBlockRetentionRequest,
+ activityTag uint32,
+ activityEligibilityCutoff time.Time,
+ cohorts []retirement.RetentionCohort,
+ parallelism int,
+ rangeSweepEnabled bool,
+ resultValidationEnabled bool,
+ result *SingleBlockRetentionResult,
+) ([]SingleBlockRetentionCohortFailureOutcome, error) {
+ cohortOutcomes := make([]SingleBlockRetentionCohortFailureOutcome, 0, len(cohorts))
+ for start := 0; start < len(cohorts); start += parallelism {
+ end := start + parallelism
+ if end > len(cohorts) {
+ end = len(cohorts)
+ }
+
+ futures := make([]workflow.Future, 0, end-start)
+ for i := start; i < end; i++ {
+ cohort := cohorts[i]
+ future, settable := workflow.NewFuture(ctx)
+ workflow.Go(activityCtx, func(cohortCtx workflow.Context) {
+ rangeResult, err := w.processSingleBlockRetentionCohort(
+ cohortCtx,
+ cohortCtx,
+ logger,
+ request,
+ activityTag,
+ activityEligibilityCutoff,
+ cohort,
+ rangeSweepEnabled,
+ resultValidationEnabled,
+ )
+ settable.Set(&singleBlockRetentionCohortOutcome{
+ cohort: cohort,
+ result: rangeResult,
+ err: err,
+ }, nil)
+ })
+ futures = append(futures, future)
+ }
+
+ var firstErr error
+ for i, future := range futures {
+ cohort := cohorts[start+i]
+ var outcome *singleBlockRetentionCohortOutcome
+ if err := future.Get(ctx, &outcome); err != nil {
+ cohortOutcomes = append(
+ cohortOutcomes,
+ newSingleBlockRetentionCohortFailureOutcome(cohort, nil, err),
+ )
+ if firstErr == nil {
+ firstErr = xerrors.Errorf("failed to await parallel retention cohort: %w", err)
+ }
+ continue
+ }
+ if outcome == nil {
+ outcomeErr := xerrors.New("parallel retention cohort returned no outcome")
+ cohortOutcomes = append(
+ cohortOutcomes,
+ newSingleBlockRetentionCohortFailureOutcome(cohort, nil, outcomeErr),
+ )
+ if firstErr == nil {
+ firstErr = outcomeErr
+ }
+ continue
+ }
+ result.addRangeResult(outcome.result)
+ cohortOutcomes = append(
+ cohortOutcomes,
+ newSingleBlockRetentionCohortFailureOutcome(outcome.cohort, outcome.result, outcome.err),
+ )
+ if outcome.err != nil && firstErr == nil {
+ firstErr = outcome.err
+ }
+ }
+ if firstErr != nil {
+ return cohortOutcomes, firstErr
+ }
+ }
+ return cohortOutcomes, nil
+}
+
+func (w *SingleBlockRetention) processSingleBlockRetentionCohort(
+ ctx workflow.Context,
+ activityCtx workflow.Context,
+ logger *zap.Logger,
+ request *SingleBlockRetentionRequest,
+ activityTag uint32,
+ activityEligibilityCutoff time.Time,
+ cohort retirement.RetentionCohort,
+ rangeSweepEnabled bool,
+ resultValidationEnabled bool,
+) (*activity.SingleBlockRetentionRangeResult, error) {
+ if err := validateSelectedSingleBlockRetentionCohort(
+ cohort,
+ request.StartHeight,
+ request.EndHeight,
+ ); err != nil {
+ return nil, err
+ }
+ if request.Execute {
+ if err := validateApprovedSingleBlockRetentionCohort(
+ cohort,
+ request,
+ rangeSweepEnabled,
+ ); err != nil {
+ return nil, err
+ }
+ }
+
+ processRequest := &activity.SingleBlockRetentionProcessRequest{
+ Tag: activityTag,
+ Cohort: cohort,
+ EligibilityCutoff: activityEligibilityCutoff,
+ Execute: request.Execute,
+ ProductionDeleteEnabled: request.ProductionDeleteEnabled,
+ DirectStorageClientsGuarded: request.DirectStorageClientsGuarded,
+ SingleBlockWritersGuarded: request.SingleBlockWritersGuarded,
+ FallbackReadsValidated: request.FallbackReadsValidated,
+ FallbackErrorCount: request.FallbackErrorCount,
+ ApprovedChain: request.ApprovedChain,
+ ApprovedStartHeight: request.ApprovedStartHeight,
+ ApprovedEndHeight: request.ApprovedEndHeight,
+ }
+ rangeResult, err := w.activity.Process(activityCtx, processRequest)
+ if err != nil {
+ return nil, xerrors.Errorf(
+ "failed to process retention cohort %q [%d, %d): %w",
+ cohort.ConsolidatedObjectKey,
+ cohort.StartHeight,
+ cohort.EndHeight,
+ err,
+ )
+ }
+ if resultValidationEnabled {
+ if err := validateSingleBlockRetentionRangeResult(cohort, rangeResult); err != nil {
+ return nil, err
+ }
+ }
+ latestValidResult := rangeResult
+ if request.Execute && rangeResult.RetryAfter > 0 {
+ if rangeResult.RetryAfter > maxSingleBlockRetentionRetryDelay {
+ return latestValidResult, xerrors.Errorf(
+ "retention cohort %q requested retry delay %s above maximum %s",
+ cohort.ConsolidatedObjectKey,
+ rangeResult.RetryAfter,
+ maxSingleBlockRetentionRetryDelay,
+ )
+ }
+ logger.Info(
+ "single-block retention cohort deferred for bounded retry",
+ zap.String("consolidated_object_key", cohort.ConsolidatedObjectKey),
+ zap.Uint64("start_height", cohort.StartHeight),
+ zap.Uint64("end_height", cohort.EndHeight),
+ zap.Duration("retry_after", rangeResult.RetryAfter),
+ zap.String("retry_reason", rangeResult.RetryReason),
+ )
+ if err := workflow.Sleep(ctx, rangeResult.RetryAfter); err != nil {
+ return latestValidResult, xerrors.Errorf("failed to wait before retention retry: %w", err)
+ }
+ retryResult, err := w.activity.Process(activityCtx, processRequest)
+ if err != nil {
+ return latestValidResult, xerrors.Errorf(
+ "failed to retry retention cohort %q [%d, %d): %w",
+ cohort.ConsolidatedObjectKey,
+ cohort.StartHeight,
+ cohort.EndHeight,
+ err,
+ )
+ }
+ if resultValidationEnabled {
+ if err := validateSingleBlockRetentionRangeResult(cohort, retryResult); err != nil {
+ return latestValidResult, err
+ }
+ }
+ rangeResult = retryResult
+ }
+ if !request.Execute {
+ return rangeResult, nil
+ }
+ if rangeResult.RetryAfter > 0 {
+ return rangeResult, xerrors.Errorf(
+ "retention cohort %q remained deferred after bounded retry: %s",
+ cohort.ConsolidatedObjectKey,
+ rangeResult.RetryReason,
+ )
+ }
+ if rangeResult.FailureMessage != "" {
+ return rangeResult, xerrors.Errorf(
+ "retention cohort %q failed: %s",
+ cohort.ConsolidatedObjectKey,
+ rangeResult.FailureMessage,
+ )
+ }
+ if rangeResult.FailedRows > 0 || rangeResult.DeferredRows > 0 || !rangeResult.Terminal {
+ return rangeResult, xerrors.Errorf(
+ "retention cohort %q did not finish: failed_rows=%d deferred_rows=%d verified_through_exclusive=%d",
+ cohort.ConsolidatedObjectKey,
+ rangeResult.FailedRows,
+ rangeResult.DeferredRows,
+ rangeResult.VerifiedThroughExclusive,
+ )
+ }
+ return rangeResult, nil
+}
+
+func newSingleBlockRetentionCohortFailureOutcome(
+ cohort retirement.RetentionCohort,
+ result *activity.SingleBlockRetentionRangeResult,
+ err error,
+) SingleBlockRetentionCohortFailureOutcome {
+ outcome := SingleBlockRetentionCohortFailureOutcome{
+ Cohort: cohort,
+ Result: result,
+ }
+ if err != nil {
+ outcome.FailureMessage = err.Error()
+ }
+ return outcome
+}
+
+func validateSingleBlockRetentionCohorts(
+ cohorts []retirement.RetentionCohort,
+ request *SingleBlockRetentionRequest,
+ rangeSweepEnabled bool,
+) error {
+ ordered := append([]retirement.RetentionCohort(nil), cohorts...)
+ seenKeys := make(map[string]struct{}, len(ordered))
+ for _, cohort := range ordered {
+ if err := validateSelectedSingleBlockRetentionCohort(
+ cohort,
+ request.StartHeight,
+ request.EndHeight,
+ ); err != nil {
+ return err
+ }
+ if request.Execute {
+ if err := validateApprovedSingleBlockRetentionCohort(
+ cohort,
+ request,
+ rangeSweepEnabled,
+ ); err != nil {
+ return err
+ }
+ }
+ if _, ok := seenKeys[cohort.ConsolidatedObjectKey]; ok {
+ return xerrors.Errorf(
+ "retention selection contains duplicate CSCB object %q",
+ cohort.ConsolidatedObjectKey,
+ )
+ }
+ seenKeys[cohort.ConsolidatedObjectKey] = struct{}{}
+ }
+ sort.Slice(ordered, func(i, j int) bool {
+ if ordered[i].StartHeight != ordered[j].StartHeight {
+ return ordered[i].StartHeight < ordered[j].StartHeight
+ }
+ return ordered[i].EndHeight < ordered[j].EndHeight
+ })
+ for i := 1; i < len(ordered); i++ {
+ previous := ordered[i-1]
+ current := ordered[i]
+ if current.StartHeight < previous.EndHeight {
+ return xerrors.Errorf(
+ "retention CSCB ranges overlap: %q [%d, %d) and %q [%d, %d)",
+ previous.ConsolidatedObjectKey,
+ previous.StartHeight,
+ previous.EndHeight,
+ current.ConsolidatedObjectKey,
+ current.StartHeight,
+ current.EndHeight,
+ )
+ }
+ }
+ return nil
+}
+
+func validateSingleBlockRetentionRangeResult(
+ expected retirement.RetentionCohort,
+ result *activity.SingleBlockRetentionRangeResult,
+) error {
+ if result == nil {
+ return xerrors.Errorf(
+ "retention cohort %q [%d, %d) returned no result",
+ expected.ConsolidatedObjectKey,
+ expected.StartHeight,
+ expected.EndHeight,
+ )
+ }
+ actual := result.Cohort
+ if actual.ConsolidatedObjectKey != expected.ConsolidatedObjectKey ||
+ actual.StartHeight != expected.StartHeight ||
+ actual.EndHeight != expected.EndHeight ||
+ actual.RowCount != expected.RowCount ||
+ !actual.EligibleAt.Equal(expected.EligibleAt) ||
+ actual.Pending != expected.Pending {
+ return xerrors.Errorf(
+ "retention result cohort does not match request: expected=%+v actual=%+v",
+ expected,
+ actual,
+ )
+ }
+ if result.VerifiedThroughExclusive < expected.StartHeight ||
+ result.VerifiedThroughExclusive > expected.EndHeight {
+ return xerrors.Errorf(
+ "retention result verified-through height %d is outside cohort [%d, %d)",
+ result.VerifiedThroughExclusive,
+ expected.StartHeight,
+ expected.EndHeight,
+ )
+ }
+ if result.FirstIncompleteHeight == nil {
+ if result.VerifiedThroughExclusive != expected.EndHeight ||
+ result.ScannedRows == 0 ||
+ !result.Terminal {
+ return xerrors.Errorf(
+ "retention result has inconsistent terminal progress: terminal=%t scanned_rows=%d verified_through_exclusive=%d first_incomplete_height=nil expected_end=%d",
+ result.Terminal,
+ result.ScannedRows,
+ result.VerifiedThroughExclusive,
+ expected.EndHeight,
+ )
+ }
+ } else {
+ firstIncompleteHeight := *result.FirstIncompleteHeight
+ if firstIncompleteHeight < expected.StartHeight ||
+ firstIncompleteHeight >= expected.EndHeight ||
+ firstIncompleteHeight != result.VerifiedThroughExclusive ||
+ result.Terminal {
+ return xerrors.Errorf(
+ "retention result has inconsistent incomplete progress: terminal=%t verified_through_exclusive=%d first_incomplete_height=%d cohort=[%d, %d)",
+ result.Terminal,
+ result.VerifiedThroughExclusive,
+ firstIncompleteHeight,
+ expected.StartHeight,
+ expected.EndHeight,
+ )
+ }
+ }
+ classifiedRows := []struct {
+ name string
+ value uint64
+ }{
+ {name: "planned_rows", value: result.PlannedRows},
+ {name: "deleted_verified_rows", value: result.DeletedVerifiedRows},
+ {name: "already_retired_rows", value: result.AlreadyRetiredRows},
+ {name: "skipped_slots", value: result.SkippedSlots},
+ {name: "deferred_rows", value: result.DeferredRows},
+ {name: "failed_rows", value: result.FailedRows},
+ }
+ remainingRows := result.ScannedRows
+ for _, classified := range classifiedRows {
+ if classified.value > remainingRows {
+ return xerrors.Errorf(
+ "retention result row accounting exceeds scanned rows at %s: value=%d remaining=%d scanned_rows=%d",
+ classified.name,
+ classified.value,
+ remainingRows,
+ result.ScannedRows,
+ )
+ }
+ remainingRows -= classified.value
+ }
+ if remainingRows != 0 {
+ return xerrors.Errorf(
+ "retention result row accounting is incomplete: unclassified_rows=%d scanned_rows=%d",
+ remainingRows,
+ result.ScannedRows,
+ )
+ }
+ if result.RetryAfter < 0 || (result.RetryAfter > 0) != (result.RetryReason != "") {
+ return xerrors.Errorf(
+ "retention result has inconsistent retry state: retry_after=%s retry_reason=%q",
+ result.RetryAfter,
+ result.RetryReason,
+ )
+ }
+ if result.Terminal && (result.PlannedRows != 0 ||
+ result.DeferredRows != 0 ||
+ result.FailedRows != 0 ||
+ result.RetryAfter != 0 ||
+ result.FailureMessage != "") {
+ return xerrors.Errorf(
+ "terminal retention result contains nonterminal accounting: planned_rows=%d deferred_rows=%d failed_rows=%d retry_after=%s failure_message=%q",
+ result.PlannedRows,
+ result.DeferredRows,
+ result.FailedRows,
+ result.RetryAfter,
+ result.FailureMessage,
+ )
+ }
+ return nil
+}
+
+func encodeSingleBlockRetentionEffectiveTag(tag uint32) uint32 {
+ if tag == 0 {
+ return math.MaxUint32
+ }
+ return tag
+}
+
+func newSingleBlockRetentionResult(
+ request *SingleBlockRetentionRequest,
+ now time.Time,
+) *SingleBlockRetentionResult {
+ result := &SingleBlockRetentionResult{
+ StartedAt: now,
+ EligibilityCutoff: now,
+ }
+ if request == nil {
+ return result
+ }
+
+ result.Execute = request.Execute
+ result.ApprovedChain = request.ApprovedChain
+ result.ApprovedStartHeight = request.ApprovedStartHeight
+ result.ApprovedEndHeight = request.ApprovedEndHeight
+ result.FallbackReadsValidated = request.FallbackReadsValidated
+ result.FallbackErrorCount = request.FallbackErrorCount
+ if !request.EligibilityCutoff.IsZero() {
+ result.EligibilityCutoff = request.EligibilityCutoff.UTC()
+ }
+
+ checkpoint := request.Checkpoint
+ if checkpoint == nil {
+ return result
+ }
+ result.StartedAt = checkpoint.StartedAt
+ result.EligibilityCutoff = checkpoint.EligibilityCutoff
+ result.ContinueAsNewCount = checkpoint.ContinueAsNewCount
+ result.SelectedObjectRanges = checkpoint.SelectedObjectRanges
+ result.ProcessedObjectRanges = checkpoint.ProcessedObjectRanges
+ result.CompletedObjectRangeCount = checkpoint.CompletedObjectRangeCount
+ result.ScannedRows = checkpoint.ScannedRows
+ result.PlannedRows = checkpoint.PlannedRows
+ result.DeletedVerifiedRows = checkpoint.DeletedVerifiedRows
+ result.AlreadyRetiredRows = checkpoint.AlreadyRetiredRows
+ result.SkippedSlots = checkpoint.SkippedSlots
+ result.DeferredRows = checkpoint.DeferredRows
+ result.FailedRows = checkpoint.FailedRows
+ result.DeletedVersions = checkpoint.DeletedVersions
+ result.DeletedMarkers = checkpoint.DeletedMarkers
+ result.RetiredBytes = checkpoint.RetiredBytes
+ result.LastCompletedObjectRange = cloneSingleBlockRetentionCompletedRange(
+ checkpoint.LastCompletedObjectRange,
+ )
+ return result
+}
+
+func (r *SingleBlockRetentionResult) checkpoint() *SingleBlockRetentionCheckpoint {
+ return &SingleBlockRetentionCheckpoint{
+ StartedAt: r.StartedAt,
+ EligibilityCutoff: r.EligibilityCutoff,
+ EffectiveTag: r.Tag,
+ ContinueAsNewCount: r.ContinueAsNewCount,
+ SelectedObjectRanges: r.SelectedObjectRanges,
+ ProcessedObjectRanges: r.ProcessedObjectRanges,
+ CompletedObjectRangeCount: r.CompletedObjectRangeCount,
+ ScannedRows: r.ScannedRows,
+ PlannedRows: r.PlannedRows,
+ DeletedVerifiedRows: r.DeletedVerifiedRows,
+ AlreadyRetiredRows: r.AlreadyRetiredRows,
+ SkippedSlots: r.SkippedSlots,
+ DeferredRows: r.DeferredRows,
+ FailedRows: r.FailedRows,
+ DeletedVersions: r.DeletedVersions,
+ DeletedMarkers: r.DeletedMarkers,
+ RetiredBytes: r.RetiredBytes,
+ LastCompletedObjectRange: cloneSingleBlockRetentionCompletedRange(
+ r.LastCompletedObjectRange,
+ ),
+ }
+}
+
+func cloneSingleBlockRetentionCompletedRange(
+ completedRange *SingleBlockRetentionCompletedRange,
+) *SingleBlockRetentionCompletedRange {
+ if completedRange == nil {
+ return nil
+ }
+ cloned := *completedRange
+ return &cloned
+}
+
+func (r *SingleBlockRetentionResult) addRangeResult(result *activity.SingleBlockRetentionRangeResult) {
+ if result == nil {
+ return
+ }
+ r.ProcessedObjectRanges++
+ r.RangeResults = append(r.RangeResults, result)
+ r.ScannedRows += result.ScannedRows
+ r.PlannedRows += result.PlannedRows
+ r.DeletedVerifiedRows += result.DeletedVerifiedRows
+ r.AlreadyRetiredRows += result.AlreadyRetiredRows
+ r.SkippedSlots += result.SkippedSlots
+ r.DeferredRows += result.DeferredRows
+ r.FailedRows += result.FailedRows
+ r.DeletedVersions += result.DeletedVersions
+ r.DeletedMarkers += result.DeletedMarkers
+ r.RetiredBytes += result.RetiredBytes
+ if result.Terminal {
+ completedRange := SingleBlockRetentionCompletedRange{
+ ConsolidatedObjectKey: result.Cohort.ConsolidatedObjectKey,
+ StartHeight: result.Cohort.StartHeight,
+ EndHeight: result.Cohort.EndHeight,
+ EligibleRows: result.Cohort.RowCount,
+ }
+ r.CompletedObjectRanges = append(r.CompletedObjectRanges, completedRange)
+ r.CompletedObjectRangeCount++
+ r.LastCompletedObjectRange = &completedRange
+ }
+}
+
+func validateSingleBlockRetentionCheckpoint(
+ request *SingleBlockRetentionRequest,
+ isContinuation bool,
+ rangeSweepEnabled bool,
+ effectiveTag uint32,
+) error {
+ if request == nil || !rangeSweepEnabled {
+ return nil
+ }
+ checkpoint := request.Checkpoint
+ if !isContinuation {
+ if checkpoint != nil {
+ return xerrors.New("single-block retention checkpoint is internal and cannot be supplied on an initial execution")
+ }
+ return nil
+ }
+ if !request.Execute {
+ return xerrors.New("single-block retention continuation requires execution mode")
+ }
+ if checkpoint == nil {
+ return xerrors.New("single-block retention continuation is missing its checkpoint")
+ }
+ if checkpoint.StartedAt.IsZero() ||
+ checkpoint.EligibilityCutoff.IsZero() ||
+ checkpoint.EligibilityCutoff.After(checkpoint.StartedAt) ||
+ checkpoint.ContinueAsNewCount == 0 {
+ return xerrors.New("single-block retention continuation checkpoint is invalid")
+ }
+ if !checkpoint.EligibilityCutoff.Equal(request.EligibilityCutoff) {
+ return xerrors.Errorf(
+ "single-block retention eligibility cutoff changed across continuation: checkpoint=%s current=%s",
+ checkpoint.EligibilityCutoff,
+ request.EligibilityCutoff,
+ )
+ }
+ if checkpoint.EffectiveTag != effectiveTag {
+ return xerrors.Errorf(
+ "single-block retention effective tag changed across continuation: checkpoint=%d current=%d",
+ checkpoint.EffectiveTag,
+ effectiveTag,
+ )
+ }
+ if checkpoint.ProcessedObjectRanges > checkpoint.SelectedObjectRanges ||
+ checkpoint.CompletedObjectRangeCount > checkpoint.ProcessedObjectRanges {
+ return xerrors.Errorf(
+ "single-block retention continuation checkpoint counters are invalid: selected=%d processed=%d completed=%d",
+ checkpoint.SelectedObjectRanges,
+ checkpoint.ProcessedObjectRanges,
+ checkpoint.CompletedObjectRangeCount,
+ )
+ }
+ if checkpoint.CompletedObjectRangeCount > 0 && checkpoint.LastCompletedObjectRange == nil {
+ return xerrors.New("single-block retention continuation checkpoint is missing its last completed range")
+ }
+ return nil
+}
+
+func validateSingleBlockRetentionExecutionRequest(
+ request *SingleBlockRetentionRequest,
+) error {
+ if request == nil {
+ return xerrors.New("single-block retention request is required")
+ }
+ if !request.Execute {
+ return validateSingleBlockRetentionSelectionRange(request.StartHeight, request.EndHeight)
+ }
+ if err := validateSingleBlockRetentionSelectionRange(request.StartHeight, request.EndHeight); err != nil {
+ return err
+ }
+ if request.EndHeight == 0 {
+ return xerrors.New("retention execution requires an explicit exact selection range; unbounded execution is not allowed")
+ }
+ if request.ApprovedChain == "" {
+ return xerrors.New("retention execution requires an explicit operator approval chain")
+ }
+ if request.ApprovedEndHeight <= request.ApprovedStartHeight {
+ return xerrors.Errorf(
+ "retention execution requires a valid exact approved range, got [%d, %d)",
+ request.ApprovedStartHeight,
+ request.ApprovedEndHeight,
+ )
+ }
+ if request.ApprovedStartHeight != request.StartHeight || request.ApprovedEndHeight != request.EndHeight {
+ return xerrors.Errorf(
+ "retention execution approved range [%d, %d) must exactly match the selection range [%d, %d)",
+ request.ApprovedStartHeight,
+ request.ApprovedEndHeight,
+ request.StartHeight,
+ request.EndHeight,
+ )
+ }
+ if !request.DirectStorageClientsGuarded {
+ return xerrors.New("retention execution requires direct storage clients to be guarded or out of scope")
+ }
+ if !request.SingleBlockWritersGuarded {
+ return xerrors.New("retention execution requires every single-block writer to honor the retirement fence")
+ }
+ if !request.FallbackReadsValidated {
+ return xerrors.New("retention execution requires explicit fallback-disabled read validation")
+ }
+ if request.FallbackErrorCount != 0 {
+ return xerrors.Errorf("retention execution requires zero fallback read errors, got %d", request.FallbackErrorCount)
+ }
+ return nil
+}
+
+func validateSingleBlockRetentionSelectionRange(startHeight uint64, endHeight uint64) error {
+ if endHeight == 0 && startHeight != 0 {
+ return xerrors.New("single-block retention end height is required when start height is set")
+ }
+ if endHeight != 0 && endHeight <= startHeight {
+ return xerrors.Errorf("invalid single-block retention range [%d, %d)", startHeight, endHeight)
+ }
+ return nil
+}
+
+// validateApprovedSingleBlockRetentionCohort preserves the exact-cohort check
+// for workflows started before range sweeps were introduced. New workflows
+// allow each selected cohort only when it is fully contained by the immutable
+// operator-approved envelope.
+func validateApprovedSingleBlockRetentionCohort(
+ cohort retirement.RetentionCohort,
+ request *SingleBlockRetentionRequest,
+ rangeSweepEnabled bool,
+) error {
+ if !rangeSweepEnabled {
+ if cohort.StartHeight == request.ApprovedStartHeight &&
+ cohort.EndHeight == request.ApprovedEndHeight {
+ return nil
+ }
+ return xerrors.Errorf(
+ "selected retention cohort %q [%d, %d) does not exactly match the approved range [%d, %d); approve the exact cohort range before execution",
+ cohort.ConsolidatedObjectKey,
+ cohort.StartHeight,
+ cohort.EndHeight,
+ request.ApprovedStartHeight,
+ request.ApprovedEndHeight,
+ )
+ }
+ if cohort.StartHeight < request.ApprovedStartHeight || cohort.EndHeight > request.ApprovedEndHeight {
+ return xerrors.Errorf(
+ "selected retention cohort %q [%d, %d) is outside the approved envelope [%d, %d)",
+ cohort.ConsolidatedObjectKey,
+ cohort.StartHeight,
+ cohort.EndHeight,
+ request.ApprovedStartHeight,
+ request.ApprovedEndHeight,
+ )
+ }
+ return nil
+}
+
+func validateSelectedSingleBlockRetentionCohort(
+ cohort retirement.RetentionCohort,
+ selectionStartHeight uint64,
+ selectionEndHeight uint64,
+) error {
+ if cohort.ConsolidatedObjectKey == "" || cohort.EndHeight <= cohort.StartHeight ||
+ cohort.RowCount == 0 || cohort.EligibleAt.IsZero() {
+ return xerrors.Errorf("invalid selected single-block retention cohort: %+v", cohort)
+ }
+ if selectionEndHeight != 0 &&
+ (cohort.StartHeight < selectionStartHeight || cohort.EndHeight > selectionEndHeight) {
+ return xerrors.Errorf(
+ "selected retention cohort %q [%d, %d) is outside requested range [%d, %d)",
+ cohort.ConsolidatedObjectKey,
+ cohort.StartHeight,
+ cohort.EndHeight,
+ selectionStartHeight,
+ selectionEndHeight,
+ )
+ }
+ return nil
+}
diff --git a/internal/workflow/single_block_retention_test.go b/internal/workflow/single_block_retention_test.go
new file mode 100644
index 00000000..a4462249
--- /dev/null
+++ b/internal/workflow/single_block_retention_test.go
@@ -0,0 +1,1493 @@
+package workflow
+
+import (
+ "context"
+ "errors"
+ "math"
+ "sync"
+ "sync/atomic"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+ "github.com/stretchr/testify/suite"
+ "go.temporal.io/sdk/converter"
+ "go.temporal.io/sdk/temporal"
+ "go.temporal.io/sdk/testsuite"
+ "go.temporal.io/sdk/worker"
+ temporalworkflow "go.temporal.io/sdk/workflow"
+ "go.uber.org/fx"
+
+ "github.com/coinbase/chainstorage/internal/cadence"
+ "github.com/coinbase/chainstorage/internal/config"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage"
+ "github.com/coinbase/chainstorage/internal/storage/metastorage"
+ "github.com/coinbase/chainstorage/internal/storage/retirement"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/workflow/activity"
+)
+
+type singleBlockRetentionTestSuite struct {
+ suite.Suite
+ testsuite.WorkflowTestSuite
+ env *cadence.TestEnv
+ workflow *SingleBlockRetention
+ app testapp.TestApp
+ cfg *config.Config
+}
+
+var testSingleBlockRetentionEligibilityCutoff = time.Date(2026, 7, 23, 0, 0, 0, 0, time.UTC)
+
+func TestSingleBlockRetentionWorkflowTestSuite(t *testing.T) {
+ suite.Run(t, new(singleBlockRetentionTestSuite))
+}
+
+func TestValidateSingleBlockRetentionSelectionRange(t *testing.T) {
+ require.ErrorContains(t, validateSingleBlockRetentionSelectionRange(100, 0), "end height is required")
+ require.ErrorContains(t, validateSingleBlockRetentionSelectionRange(100, 100), "invalid")
+ require.NoError(t, validateSingleBlockRetentionSelectionRange(0, 0))
+ require.NoError(t, validateSingleBlockRetentionSelectionRange(100, 200))
+}
+
+func TestValidateSelectedSingleBlockRetentionCohort(t *testing.T) {
+ cohort := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+
+ require.NoError(t, validateSelectedSingleBlockRetentionCohort(cohort, 100, 110))
+ require.NoError(t, validateSelectedSingleBlockRetentionCohort(cohort, 0, 0))
+ require.ErrorContains(
+ t,
+ validateSelectedSingleBlockRetentionCohort(cohort, 101, 110),
+ "outside requested range",
+ )
+ require.ErrorContains(
+ t,
+ validateSelectedSingleBlockRetentionCohort(cohort, 100, 109),
+ "outside requested range",
+ )
+ cohort.EligibleAt = time.Time{}
+ require.ErrorContains(t, validateSelectedSingleBlockRetentionCohort(cohort, 0, 0), "invalid selected")
+}
+
+func (s *singleBlockRetentionTestSuite) SetupTest() {
+ cfg, err := config.New()
+ require.NoError(s.T(), err)
+ cfg.Workflows.SingleBlockRetention.MaxObjectRanges = 2
+ s.cfg = cfg
+ s.env = cadence.NewTestEnv(s)
+ s.app = testapp.New(
+ s.T(),
+ Module,
+ testapp.WithConfig(cfg),
+ cadence.WithTestEnv(s.env),
+ fx.Provide(func() metastorage.MetaStorage {
+ return nil
+ }),
+ fx.Provide(func() blobstorage.BlobStorage {
+ return nil
+ }),
+ fx.Populate(&s.workflow),
+ )
+}
+
+func (s *singleBlockRetentionTestSuite) TearDownTest() {
+ s.app.Close()
+ s.env.AssertExpectations(s.T())
+}
+
+func (s *singleBlockRetentionTestSuite) TestDryRunReturnsPlannedRangesWithoutDeleting() {
+ cohort := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ firstIncompleteHeight := cohort.StartHeight
+ var selectRequest *activity.SingleBlockRetentionSelectRequest
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionSelectRequest) (*activity.SingleBlockRetentionSelectResponse, error) {
+ selectRequest = request
+ return &activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{cohort},
+ HasMore: true,
+ }, nil
+ })
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionRangeResult{
+ Cohort: cohort,
+ ScannedRows: 10,
+ PlannedRows: 10,
+ VerifiedThroughExclusive: 100,
+ FirstIncompleteHeight: &firstIncompleteHeight,
+ }, nil)
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 110,
+ MaxObjectRanges: 1,
+ })
+ require.NoError(s.T(), err)
+ require.Equal(s.T(), uint32(2), selectRequest.Tag)
+ require.Equal(s.T(), uint64(100), selectRequest.StartHeight)
+ require.Equal(s.T(), uint64(110), selectRequest.EndHeight)
+ require.False(s.T(), selectRequest.EligibilityCutoff.IsZero())
+ require.Equal(s.T(), 1, selectRequest.Limit)
+
+ var result SingleBlockRetentionResult
+ require.NoError(s.T(), s.env.GetWorkflowResult(&result))
+ require.False(s.T(), result.Execute)
+ require.Equal(s.T(), uint32(2), result.Tag)
+ require.Equal(s.T(), selectRequest.EligibilityCutoff, result.EligibilityCutoff)
+ require.Equal(s.T(), uint64(100), result.SelectionStartHeight)
+ require.Equal(s.T(), uint64(110), result.SelectionEndHeight)
+ require.Equal(s.T(), uint64(1), result.SelectedObjectRanges)
+ require.True(s.T(), result.MoreEligibleRanges)
+ require.Equal(s.T(), uint64(1), result.ProcessedObjectRanges)
+ require.Equal(s.T(), uint64(10), result.PlannedRows)
+ require.False(s.T(), result.SweepCompleted)
+ require.Empty(s.T(), result.CompletedObjectRanges)
+ require.Empty(s.T(), result.FailureMessage)
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteReturnsExactCompletedRanges() {
+ cohort := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ s.env.OnGetVersion(
+ singleBlockRetentionParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ singleBlockRetentionParallelismVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{cohort},
+ }, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{}, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ require.True(s.T(), request.FallbackReadsValidated)
+ require.Equal(s.T(), testSingleBlockRetentionEligibilityCutoff, request.EligibilityCutoff)
+ // The operator approval must reach the activity verbatim, never
+ // rewritten to whatever cohort was selected.
+ require.Equal(s.T(), "solana-mainnet", request.ApprovedChain)
+ require.Equal(s.T(), uint64(100), request.ApprovedStartHeight)
+ require.Equal(s.T(), uint64(110), request.ApprovedEndHeight)
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: request.Cohort,
+ ScannedRows: request.Cohort.RowCount,
+ DeletedVerifiedRows: request.Cohort.RowCount,
+ DeletedVersions: request.Cohort.RowCount,
+ RetiredBytes: request.Cohort.RowCount * 100,
+ VerifiedThroughExclusive: request.Cohort.EndHeight,
+ Terminal: true,
+ }, nil
+ })
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 110,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 110,
+ })
+ require.NoError(s.T(), err)
+
+ var result SingleBlockRetentionResult
+ require.NoError(s.T(), s.env.GetWorkflowResult(&result))
+ require.True(s.T(), result.Execute)
+ require.Equal(s.T(), "solana-mainnet", result.ApprovedChain)
+ require.Equal(s.T(), uint64(100), result.ApprovedStartHeight)
+ require.Equal(s.T(), uint64(110), result.ApprovedEndHeight)
+ require.Equal(s.T(), uint64(1), result.SelectedObjectRanges)
+ require.Equal(s.T(), 1, result.Parallelism)
+ require.Equal(s.T(), uint64(1), result.ProcessedObjectRanges)
+ require.Equal(s.T(), uint64(1), result.CompletedObjectRangeCount)
+ require.Equal(s.T(), uint64(10), result.DeletedVerifiedRows)
+ require.Equal(s.T(), uint64(10), result.DeletedVersions)
+ require.Equal(s.T(), uint64(1000), result.RetiredBytes)
+ require.True(s.T(), result.SweepCompleted)
+ require.Equal(s.T(), &SingleBlockRetentionCompletedRange{
+ ConsolidatedObjectKey: cohort.ConsolidatedObjectKey,
+ StartHeight: 100,
+ EndHeight: 110,
+ EligibleRows: 10,
+ }, result.LastCompletedObjectRange)
+ require.Equal(s.T(), []SingleBlockRetentionCompletedRange{
+ {
+ ConsolidatedObjectKey: cohort.ConsolidatedObjectKey,
+ StartHeight: 100,
+ EndHeight: 110,
+ EligibleRows: 10,
+ },
+ }, result.CompletedObjectRanges)
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteProcessesCohortsInsideApprovedEnvelope() {
+ first := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ second := testRetentionCohort("consolidated/200-210.cscb.zstd", 200, 210)
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{first, second},
+ }, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{}, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ require.Equal(s.T(), uint64(100), request.ApprovedStartHeight)
+ require.Equal(s.T(), uint64(210), request.ApprovedEndHeight)
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: request.Cohort,
+ ScannedRows: request.Cohort.RowCount,
+ DeletedVerifiedRows: request.Cohort.RowCount,
+ VerifiedThroughExclusive: request.Cohort.EndHeight,
+ Terminal: true,
+ }, nil
+ }).Twice()
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 210,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 210,
+ })
+ require.NoError(s.T(), err)
+
+ var result SingleBlockRetentionResult
+ require.NoError(s.T(), s.env.GetWorkflowResult(&result))
+ require.True(s.T(), result.SweepCompleted)
+ require.Equal(s.T(), uint64(2), result.SelectedObjectRanges)
+ require.Equal(s.T(), uint64(2), result.ProcessedObjectRanges)
+ require.Equal(s.T(), uint64(2), result.CompletedObjectRangeCount)
+ require.Equal(s.T(), uint64(20), result.DeletedVerifiedRows)
+ require.Equal(s.T(), second.EndHeight, result.LastCompletedObjectRange.EndHeight)
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteProcessesCohortsInParallelBatches() {
+ cohorts := []retirement.RetentionCohort{
+ testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110),
+ testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120),
+ testRetentionCohort("consolidated/120-130.cscb.zstd", 120, 130),
+ }
+ s.env.OnGetVersion(
+ singleBlockRetentionParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ singleBlockRetentionParallelismVersion,
+ ).Return(temporalworkflow.Version(singleBlockRetentionParallelismVersion))
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{Cohorts: cohorts}, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{}, nil).
+ Once()
+
+ var active int32
+ var maxActive int32
+ var mu sync.Mutex
+ processedKeys := make([]string, 0, len(cohorts))
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ current := atomic.AddInt32(&active, 1)
+ for {
+ previous := atomic.LoadInt32(&maxActive)
+ if current <= previous || atomic.CompareAndSwapInt32(&maxActive, previous, current) {
+ break
+ }
+ }
+ time.Sleep(50 * time.Millisecond)
+ atomic.AddInt32(&active, -1)
+ mu.Lock()
+ processedKeys = append(processedKeys, request.Cohort.ConsolidatedObjectKey)
+ mu.Unlock()
+ return terminalSingleBlockRetentionRangeResult(request.Cohort), nil
+ }).
+ Times(len(cohorts))
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 130,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ MaxObjectRanges: len(cohorts),
+ Parallelism: 2,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 130,
+ })
+ require.NoError(s.T(), err)
+ require.Equal(s.T(), int32(2), atomic.LoadInt32(&maxActive))
+ require.ElementsMatch(s.T(), []string{
+ cohorts[0].ConsolidatedObjectKey,
+ cohorts[1].ConsolidatedObjectKey,
+ cohorts[2].ConsolidatedObjectKey,
+ }, processedKeys)
+
+ var result SingleBlockRetentionResult
+ require.NoError(s.T(), s.env.GetWorkflowResult(&result))
+ require.Equal(s.T(), 2, result.Parallelism)
+ require.Equal(s.T(), uint64(3), result.CompletedObjectRangeCount)
+ require.Len(s.T(), result.CompletedObjectRanges, 3)
+ for i, completed := range result.CompletedObjectRanges {
+ require.Equal(s.T(), cohorts[i].ConsolidatedObjectKey, completed.ConsolidatedObjectKey)
+ }
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteRetriesParallelCohortsConcurrently() {
+ cohorts := []retirement.RetentionCohort{
+ testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110),
+ testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120),
+ }
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{Cohorts: cohorts}, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{}, nil).
+ Once()
+
+ attempts := make(map[string]int, len(cohorts))
+ var mu sync.Mutex
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ mu.Lock()
+ attempts[request.Cohort.ConsolidatedObjectKey]++
+ attempt := attempts[request.Cohort.ConsolidatedObjectKey]
+ mu.Unlock()
+ if attempt == 1 {
+ firstIncompleteHeight := request.Cohort.StartHeight
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: request.Cohort,
+ ScannedRows: request.Cohort.RowCount,
+ DeferredRows: request.Cohort.RowCount,
+ VerifiedThroughExclusive: request.Cohort.StartHeight,
+ FirstIncompleteHeight: &firstIncompleteHeight,
+ RetryAfter: time.Minute,
+ RetryReason: retirement.SkipCSCBSafetyQuiescenceActive,
+ }, nil
+ }
+ return terminalSingleBlockRetentionRangeResult(request.Cohort), nil
+ }).
+ Times(4)
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ MaxObjectRanges: len(cohorts),
+ Parallelism: 2,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ })
+ require.NoError(s.T(), err)
+ require.Equal(s.T(), 2, attempts[cohorts[0].ConsolidatedObjectKey])
+ require.Equal(s.T(), 2, attempts[cohorts[1].ConsolidatedObjectKey])
+
+ var result SingleBlockRetentionResult
+ require.NoError(s.T(), s.env.GetWorkflowResult(&result))
+ require.Less(s.T(), result.CompletedAt.Sub(result.StartedAt), 2*time.Minute)
+ require.Equal(s.T(), uint64(2), result.CompletedObjectRangeCount)
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteRejectsOverlappingParallelCohortsBeforeProcessing() {
+ cohorts := []retirement.RetentionCohort{
+ testRetentionCohort("consolidated/100-115.cscb.zstd", 100, 115),
+ testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120),
+ }
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{Cohorts: cohorts}, nil).
+ Once()
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ MaxObjectRanges: len(cohorts),
+ Parallelism: 2,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ })
+ require.ErrorContains(s.T(), err, "retention CSCB ranges overlap")
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteRejectsDuplicateSerialCohortsBeforeProcessing() {
+ first := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ second := testRetentionCohort(first.ConsolidatedObjectKey, 110, 120)
+ s.env.OnGetVersion(
+ singleBlockRetentionParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ singleBlockRetentionParallelismVersion,
+ ).Return(temporalworkflow.Version(singleBlockRetentionParallelismVersion))
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{first, second},
+ }, nil).
+ Once()
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ MaxObjectRanges: 2,
+ Parallelism: 1,
+ })
+ require.ErrorContains(s.T(), err, "retention selection contains duplicate CSCB object")
+ s.env.AssertActivityNotCalled(
+ s.T(),
+ activity.ActivitySingleBlockRetentionProcess,
+ mock.Anything,
+ mock.Anything,
+ )
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteRejectsOverlappingSerialCohortsBeforeProcessing() {
+ first := testRetentionCohort("consolidated/100-115.cscb.zstd", 100, 115)
+ second := testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120)
+ s.env.OnGetVersion(
+ singleBlockRetentionParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ singleBlockRetentionParallelismVersion,
+ ).Return(temporalworkflow.Version(singleBlockRetentionParallelismVersion))
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{first, second},
+ }, nil).
+ Once()
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ MaxObjectRanges: 2,
+ Parallelism: 1,
+ })
+ require.ErrorContains(s.T(), err, "retention CSCB ranges overlap")
+ s.env.AssertActivityNotCalled(
+ s.T(),
+ activity.ActivitySingleBlockRetentionProcess,
+ mock.Anything,
+ mock.Anything,
+ )
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteParallelFailureStopsBeforeNextBatch() {
+ cohorts := []retirement.RetentionCohort{
+ testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110),
+ testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120),
+ testRetentionCohort("consolidated/120-130.cscb.zstd", 120, 130),
+ }
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{Cohorts: cohorts}, nil).
+ Once()
+
+ var mu sync.Mutex
+ processedKeys := make([]string, 0, 2)
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ mu.Lock()
+ processedKeys = append(processedKeys, request.Cohort.ConsolidatedObjectKey)
+ mu.Unlock()
+ if request.Cohort.ConsolidatedObjectKey == cohorts[0].ConsolidatedObjectKey {
+ firstIncompleteHeight := request.Cohort.StartHeight
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: request.Cohort,
+ ScannedRows: request.Cohort.RowCount,
+ AlreadyRetiredRows: request.Cohort.RowCount - 1,
+ FailedRows: 1,
+ VerifiedThroughExclusive: request.Cohort.StartHeight,
+ FirstIncompleteHeight: &firstIncompleteHeight,
+ }, nil
+ }
+ return terminalSingleBlockRetentionRangeResult(request.Cohort), nil
+ }).
+ Twice()
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 130,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ MaxObjectRanges: len(cohorts),
+ Parallelism: 2,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 130,
+ })
+ require.ErrorContains(s.T(), err, "did not finish")
+ require.ElementsMatch(s.T(), []string{
+ cohorts[0].ConsolidatedObjectKey,
+ cohorts[1].ConsolidatedObjectKey,
+ }, processedKeys)
+
+ var applicationErr *temporal.ApplicationError
+ require.ErrorAs(s.T(), s.env.GetWorkflowError(), &applicationErr)
+ require.Equal(s.T(), singleBlockRetentionPartialFailureType, applicationErr.Type())
+ require.True(s.T(), applicationErr.HasDetails())
+ var failureDetails SingleBlockRetentionFailureDetails
+ require.NoError(s.T(), applicationErr.Details(&failureDetails))
+ require.ErrorContains(s.T(), errors.New(failureDetails.FailureMessage), "did not finish")
+ require.Equal(s.T(), 2, failureDetails.Parallelism)
+ require.Equal(s.T(), uint64(3), failureDetails.CurrentRunSelectedObjectRanges)
+ require.Equal(s.T(), uint64(2), failureDetails.CurrentRunLaunchedObjectRanges)
+ require.Equal(s.T(), uint64(3), failureDetails.SelectedObjectRanges)
+ require.Equal(s.T(), uint64(2), failureDetails.ProcessedObjectRanges)
+ require.Equal(s.T(), uint64(1), failureDetails.CompletedObjectRangeCount)
+ require.Len(s.T(), failureDetails.CohortOutcomes, 2)
+ require.Equal(s.T(), cohorts[0], failureDetails.CohortOutcomes[0].Cohort)
+ require.ErrorContains(
+ s.T(),
+ errors.New(failureDetails.CohortOutcomes[0].FailureMessage),
+ "did not finish",
+ )
+ require.NotNil(s.T(), failureDetails.CohortOutcomes[0].Result)
+ require.Equal(s.T(), cohorts[1], failureDetails.CohortOutcomes[1].Cohort)
+ require.Empty(s.T(), failureDetails.CohortOutcomes[1].FailureMessage)
+ require.True(s.T(), failureDetails.CohortOutcomes[1].Result.Terminal)
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteParallelFailureDetailsIncludeEarlierWaves() {
+ cohorts := []retirement.RetentionCohort{
+ testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110),
+ testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120),
+ testRetentionCohort("consolidated/120-130.cscb.zstd", 120, 130),
+ }
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{Cohorts: cohorts}, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ if request.Cohort.ConsolidatedObjectKey == cohorts[2].ConsolidatedObjectKey {
+ firstIncompleteHeight := request.Cohort.StartHeight
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: request.Cohort,
+ ScannedRows: request.Cohort.RowCount,
+ AlreadyRetiredRows: request.Cohort.RowCount - 1,
+ FailedRows: 1,
+ VerifiedThroughExclusive: request.Cohort.StartHeight,
+ FirstIncompleteHeight: &firstIncompleteHeight,
+ }, nil
+ }
+ return terminalSingleBlockRetentionRangeResult(request.Cohort), nil
+ }).
+ Times(len(cohorts))
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 130,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ MaxObjectRanges: len(cohorts),
+ Parallelism: 2,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 130,
+ })
+ require.ErrorContains(s.T(), err, "did not finish")
+
+ var applicationErr *temporal.ApplicationError
+ require.ErrorAs(s.T(), s.env.GetWorkflowError(), &applicationErr)
+ var failureDetails SingleBlockRetentionFailureDetails
+ require.NoError(s.T(), applicationErr.Details(&failureDetails))
+ require.Equal(s.T(), uint64(3), failureDetails.CurrentRunSelectedObjectRanges)
+ require.Equal(s.T(), uint64(3), failureDetails.CurrentRunLaunchedObjectRanges)
+ require.Equal(s.T(), uint64(3), failureDetails.ProcessedObjectRanges)
+ require.Equal(s.T(), uint64(2), failureDetails.CompletedObjectRangeCount)
+ require.Len(s.T(), failureDetails.CohortOutcomes, 3)
+ for i, cohort := range cohorts {
+ require.Equal(s.T(), cohort, failureDetails.CohortOutcomes[i].Cohort)
+ }
+ require.Empty(s.T(), failureDetails.CohortOutcomes[0].FailureMessage)
+ require.Empty(s.T(), failureDetails.CohortOutcomes[1].FailureMessage)
+ require.ErrorContains(
+ s.T(),
+ errors.New(failureDetails.CohortOutcomes[2].FailureMessage),
+ "did not finish",
+ )
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteParallelRetryFailurePreservesLatestValidResult() {
+ cohorts := []retirement.RetentionCohort{
+ testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110),
+ testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120),
+ }
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{Cohorts: cohorts}, nil).
+ Once()
+
+ var mu sync.Mutex
+ attempts := make(map[string]int)
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ mu.Lock()
+ attempts[request.Cohort.ConsolidatedObjectKey]++
+ attempt := attempts[request.Cohort.ConsolidatedObjectKey]
+ mu.Unlock()
+ if request.Cohort.ConsolidatedObjectKey != cohorts[0].ConsolidatedObjectKey {
+ return terminalSingleBlockRetentionRangeResult(request.Cohort), nil
+ }
+ if attempt == 1 {
+ firstIncompleteHeight := request.Cohort.StartHeight
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: request.Cohort,
+ ScannedRows: request.Cohort.RowCount,
+ DeferredRows: request.Cohort.RowCount,
+ VerifiedThroughExclusive: request.Cohort.StartHeight,
+ FirstIncompleteHeight: &firstIncompleteHeight,
+ RetryAfter: time.Minute,
+ RetryReason: retirement.SkipCSCBSafetyQuiescenceActive,
+ }, nil
+ }
+ return nil, temporal.NewNonRetryableApplicationError(
+ "retry unavailable",
+ "test_retry_failure",
+ nil,
+ )
+ }).
+ Times(3)
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ MaxObjectRanges: len(cohorts),
+ Parallelism: 2,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ })
+ require.ErrorContains(s.T(), err, "failed to retry retention cohort")
+
+ var applicationErr *temporal.ApplicationError
+ require.ErrorAs(s.T(), s.env.GetWorkflowError(), &applicationErr)
+ var failureDetails SingleBlockRetentionFailureDetails
+ require.NoError(s.T(), applicationErr.Details(&failureDetails))
+ require.Equal(s.T(), uint64(2), failureDetails.CurrentRunLaunchedObjectRanges)
+ require.Equal(s.T(), uint64(2), failureDetails.ProcessedObjectRanges)
+ require.Equal(s.T(), uint64(1), failureDetails.CompletedObjectRangeCount)
+ require.Equal(s.T(), cohorts[0].RowCount, failureDetails.DeferredRows)
+ require.Len(s.T(), failureDetails.CohortOutcomes, 2)
+ require.Equal(s.T(), cohorts[0], failureDetails.CohortOutcomes[0].Cohort)
+ require.NotNil(s.T(), failureDetails.CohortOutcomes[0].Result)
+ require.Equal(s.T(), cohorts[0].RowCount, failureDetails.CohortOutcomes[0].Result.DeferredRows)
+ require.ErrorContains(
+ s.T(),
+ errors.New(failureDetails.CohortOutcomes[0].FailureMessage),
+ "failed to retry retention cohort",
+ )
+ require.Equal(s.T(), cohorts[1], failureDetails.CohortOutcomes[1].Cohort)
+ require.True(s.T(), failureDetails.CohortOutcomes[1].Result.Terminal)
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteCompletionProbeFailureIncludesProcessedCohorts() {
+ cohorts := []retirement.RetentionCohort{
+ testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110),
+ testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120),
+ }
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{Cohorts: cohorts}, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ return terminalSingleBlockRetentionRangeResult(request.Cohort), nil
+ }).
+ Twice()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return((*activity.SingleBlockRetentionSelectResponse)(nil), errors.New("completion probe unavailable"))
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ MaxObjectRanges: len(cohorts),
+ Parallelism: 2,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ })
+ require.ErrorContains(s.T(), err, "failed to confirm retention sweep completion")
+
+ var applicationErr *temporal.ApplicationError
+ require.ErrorAs(s.T(), s.env.GetWorkflowError(), &applicationErr)
+ var failureDetails SingleBlockRetentionFailureDetails
+ require.NoError(s.T(), applicationErr.Details(&failureDetails))
+ require.ErrorContains(
+ s.T(),
+ errors.New(failureDetails.FailureMessage),
+ "completion probe unavailable",
+ )
+ require.Equal(s.T(), uint64(2), failureDetails.CurrentRunSelectedObjectRanges)
+ require.Equal(s.T(), uint64(2), failureDetails.CurrentRunLaunchedObjectRanges)
+ require.Equal(s.T(), uint64(2), failureDetails.CompletedObjectRangeCount)
+ require.Len(s.T(), failureDetails.CohortOutcomes, 2)
+}
+
+func (s *singleBlockRetentionTestSuite) TestLegacyExecutionRejectsParallelism() {
+ s.env.OnGetVersion(
+ singleBlockRetentionParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ singleBlockRetentionParallelismVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ Parallelism: 2,
+ MaxObjectRanges: 2,
+ })
+ require.ErrorContains(s.T(), err, "legacy single_block_retention execution requires parallelism=1")
+}
+
+func (s *singleBlockRetentionTestSuite) TestReplayPreParallelismHistory() {
+ replayer := worker.NewWorkflowReplayer()
+ replayer.RegisterWorkflowWithOptions(s.workflow.execute, temporalworkflow.RegisterOptions{
+ Name: s.workflow.name,
+ })
+ require.NoError(
+ s.T(),
+ replayer.ReplayWorkflowHistoryFromJSONFile(
+ nil,
+ "testdata/single_block_retention_pre_parallelism_history.json",
+ ),
+ )
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecutionRejectsParallelismAboveMaximum() {
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ Parallelism: singleBlockRetentionMaxParallelism + 1,
+ MaxObjectRanges: 2,
+ })
+ require.ErrorContains(s.T(), err, "parallelism(21) exceeds max(20)")
+}
+
+func (s *singleBlockRetentionTestSuite) TestLegacyExecuteRequiresExactCohortApproval() {
+ cohort := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ var selectRequest *activity.SingleBlockRetentionSelectRequest
+ s.env.OnGetVersion(
+ singleBlockRetentionRangeSweepChangeID,
+ temporalworkflow.DefaultVersion,
+ singleBlockRetentionRangeSweepVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionSelectRequest) (*activity.SingleBlockRetentionSelectResponse, error) {
+ selectRequest = request
+ return &activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{cohort},
+ }, nil
+ })
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 0,
+ StartHeight: 100,
+ EndHeight: 210,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 210,
+ })
+ require.ErrorContains(s.T(), err, "does not exactly match the approved range")
+ require.Equal(
+ s.T(),
+ s.cfg.Workflows.SingleBlockRetention.GetEffectiveBlockTag(0),
+ selectRequest.Tag,
+ )
+ require.True(s.T(), selectRequest.EligibilityCutoff.IsZero())
+}
+
+func (s *singleBlockRetentionTestSuite) TestLegacyExecutePinsResolvedTagAcrossActivities() {
+ cohort := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ effectiveTag := s.cfg.Workflows.SingleBlockRetention.GetEffectiveBlockTag(0)
+ s.env.OnGetVersion(
+ singleBlockRetentionRangeSweepChangeID,
+ temporalworkflow.DefaultVersion,
+ singleBlockRetentionRangeSweepVersion,
+ ).Return(temporalworkflow.DefaultVersion)
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionSelectRequest) (*activity.SingleBlockRetentionSelectResponse, error) {
+ require.Equal(s.T(), effectiveTag, request.Tag)
+ require.True(s.T(), request.EligibilityCutoff.IsZero())
+ s.cfg.Workflows.SingleBlockRetention.BlockTag.Stable = effectiveTag + 1
+ return &activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{cohort},
+ }, nil
+ })
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ require.Equal(s.T(), effectiveTag, request.Tag)
+ require.True(s.T(), request.EligibilityCutoff.IsZero())
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: request.Cohort,
+ ScannedRows: request.Cohort.RowCount,
+ DeletedVerifiedRows: request.Cohort.RowCount,
+ VerifiedThroughExclusive: request.Cohort.EndHeight,
+ Terminal: true,
+ }, nil
+ })
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ StartHeight: cohort.StartHeight,
+ EndHeight: cohort.EndHeight,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: cohort.StartHeight,
+ ApprovedEndHeight: cohort.EndHeight,
+ })
+ require.NoError(s.T(), err)
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteContinuesAsNewWithCumulativeCheckpoint() {
+ cohort := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ effectiveTag := s.cfg.Workflows.SingleBlockRetention.GetEffectiveBlockTag(0)
+ var selectRequest *activity.SingleBlockRetentionSelectRequest
+ var processRequest *activity.SingleBlockRetentionProcessRequest
+ s.env.OnGetVersion(
+ singleBlockRetentionParallelismChangeID,
+ temporalworkflow.DefaultVersion,
+ singleBlockRetentionParallelismVersion,
+ ).Return(temporalworkflow.Version(singleBlockRetentionParallelismVersion))
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionSelectRequest) (*activity.SingleBlockRetentionSelectResponse, error) {
+ selectRequest = request
+ return &activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{cohort},
+ HasMore: true,
+ }, nil
+ })
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ processRequest = request
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: cohort,
+ ScannedRows: 10,
+ DeletedVerifiedRows: 10,
+ DeletedVersions: 10,
+ RetiredBytes: 1000,
+ VerifiedThroughExclusive: 110,
+ Terminal: true,
+ }, nil
+ })
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 0,
+ StartHeight: 100,
+ EndHeight: 120,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ MaxObjectRanges: 1,
+ Parallelism: 2,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ })
+ require.Error(s.T(), err)
+ require.True(s.T(), IsContinueAsNewError(err))
+
+ var continueAsNewErr *temporalworkflow.ContinueAsNewError
+ require.True(s.T(), errors.As(err, &continueAsNewErr))
+ var nextRequest SingleBlockRetentionRequest
+ require.NoError(
+ s.T(),
+ converter.GetDefaultDataConverter().FromPayloads(continueAsNewErr.Input, &nextRequest),
+ )
+ require.Equal(s.T(), uint64(100), nextRequest.StartHeight)
+ require.Equal(s.T(), uint64(120), nextRequest.EndHeight)
+ require.Equal(s.T(), uint64(100), nextRequest.ApprovedStartHeight)
+ require.Equal(s.T(), uint64(120), nextRequest.ApprovedEndHeight)
+ require.Equal(s.T(), 2, nextRequest.Parallelism)
+ require.Equal(s.T(), encodeSingleBlockRetentionEffectiveTag(effectiveTag), nextRequest.Tag)
+ require.Equal(s.T(), encodeSingleBlockRetentionEffectiveTag(effectiveTag), selectRequest.Tag)
+ require.Equal(s.T(), encodeSingleBlockRetentionEffectiveTag(effectiveTag), processRequest.Tag)
+ require.Equal(s.T(), testSingleBlockRetentionEligibilityCutoff, processRequest.EligibilityCutoff)
+ require.NotNil(s.T(), nextRequest.Checkpoint)
+ require.False(s.T(), selectRequest.EligibilityCutoff.IsZero())
+ require.Equal(s.T(), selectRequest.EligibilityCutoff, nextRequest.Checkpoint.EligibilityCutoff)
+ require.Equal(s.T(), testSingleBlockRetentionEligibilityCutoff, nextRequest.Checkpoint.EligibilityCutoff)
+ require.Equal(s.T(), effectiveTag, nextRequest.Checkpoint.EffectiveTag)
+ require.Equal(s.T(), uint64(1), nextRequest.Checkpoint.ContinueAsNewCount)
+ require.Equal(s.T(), uint64(1), nextRequest.Checkpoint.SelectedObjectRanges)
+ require.Equal(s.T(), uint64(1), nextRequest.Checkpoint.ProcessedObjectRanges)
+ require.Equal(s.T(), uint64(1), nextRequest.Checkpoint.CompletedObjectRangeCount)
+ require.Equal(s.T(), uint64(10), nextRequest.Checkpoint.DeletedVerifiedRows)
+ require.Equal(s.T(), uint64(10), nextRequest.Checkpoint.DeletedVersions)
+ require.Equal(s.T(), uint64(1000), nextRequest.Checkpoint.RetiredBytes)
+ require.Equal(s.T(), uint64(110), nextRequest.Checkpoint.LastCompletedObjectRange.EndHeight)
+}
+
+func TestEncodeSingleBlockRetentionEffectiveTag(t *testing.T) {
+ require.Equal(t, uint32(math.MaxUint32), encodeSingleBlockRetentionEffectiveTag(0))
+ require.Equal(t, uint32(2), encodeSingleBlockRetentionEffectiveTag(2))
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteContinuesAsNewWhenCompletionProbeFindsBacklog() {
+ first := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ second := testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120)
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{first},
+ }, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionRangeResult{
+ Cohort: first,
+ ScannedRows: first.RowCount,
+ DeletedVerifiedRows: first.RowCount,
+ VerifiedThroughExclusive: first.EndHeight,
+ Terminal: true,
+ }, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{second},
+ }, nil).
+ Once()
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ MaxObjectRanges: 1,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ })
+ require.Error(s.T(), err)
+ require.True(s.T(), IsContinueAsNewError(err))
+
+ var continueAsNewErr *temporalworkflow.ContinueAsNewError
+ require.True(s.T(), errors.As(err, &continueAsNewErr))
+ var nextRequest SingleBlockRetentionRequest
+ require.NoError(
+ s.T(),
+ converter.GetDefaultDataConverter().FromPayloads(continueAsNewErr.Input, &nextRequest),
+ )
+ require.NotNil(s.T(), nextRequest.Checkpoint)
+ require.Equal(s.T(), uint64(1), nextRequest.Checkpoint.ContinueAsNewCount)
+ require.Equal(s.T(), uint64(1), nextRequest.Checkpoint.CompletedObjectRangeCount)
+ require.Equal(s.T(), first.EndHeight, nextRequest.Checkpoint.LastCompletedObjectRange.EndHeight)
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecutePreservesExplicitTagZeroEncoding() {
+ cohort := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ selectCalls := 0
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionSelectRequest) (*activity.SingleBlockRetentionSelectResponse, error) {
+ require.Equal(s.T(), uint32(math.MaxUint32), request.Tag)
+ selectCalls++
+ if selectCalls == 1 {
+ return &activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{cohort},
+ }, nil
+ }
+ return &activity.SingleBlockRetentionSelectResponse{}, nil
+ }).
+ Twice()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ require.Equal(s.T(), uint32(math.MaxUint32), request.Tag)
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: request.Cohort,
+ ScannedRows: request.Cohort.RowCount,
+ DeletedVerifiedRows: request.Cohort.RowCount,
+ VerifiedThroughExclusive: request.Cohort.EndHeight,
+ Terminal: true,
+ }, nil
+ }).
+ Once()
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: math.MaxUint32,
+ StartHeight: 100,
+ EndHeight: 110,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 110,
+ })
+ require.NoError(s.T(), err)
+
+ var result SingleBlockRetentionResult
+ require.NoError(s.T(), s.env.GetWorkflowResult(&result))
+ require.Equal(s.T(), uint32(0), result.Tag)
+ require.True(s.T(), result.SweepCompleted)
+}
+
+func (s *singleBlockRetentionTestSuite) TestContinuationReturnsCumulativeFinalResult() {
+ startedAt := time.Date(2026, 7, 23, 12, 0, 0, 0, time.UTC)
+ firstCompleted := &SingleBlockRetentionCompletedRange{
+ ConsolidatedObjectKey: "consolidated/100-110.cscb.zstd",
+ StartHeight: 100,
+ EndHeight: 110,
+ EligibleRows: 10,
+ }
+ second := testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120)
+ s.env.SetContinuedExecutionRunID("previous-run")
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{second},
+ }, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{}, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionRangeResult{
+ Cohort: second,
+ ScannedRows: 10,
+ DeletedVerifiedRows: 10,
+ DeletedVersions: 10,
+ RetiredBytes: 1000,
+ VerifiedThroughExclusive: 120,
+ Terminal: true,
+ }, nil)
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ EligibilityCutoff: startedAt,
+ MaxObjectRanges: 1,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ Checkpoint: &SingleBlockRetentionCheckpoint{
+ StartedAt: startedAt,
+ EligibilityCutoff: startedAt,
+ EffectiveTag: 2,
+ ContinueAsNewCount: 1,
+ SelectedObjectRanges: 1,
+ ProcessedObjectRanges: 1,
+ CompletedObjectRangeCount: 1,
+ ScannedRows: 10,
+ DeletedVerifiedRows: 10,
+ DeletedVersions: 10,
+ RetiredBytes: 1000,
+ LastCompletedObjectRange: firstCompleted,
+ },
+ })
+ require.NoError(s.T(), err)
+
+ var result SingleBlockRetentionResult
+ require.NoError(s.T(), s.env.GetWorkflowResult(&result))
+ require.Equal(s.T(), startedAt, result.StartedAt)
+ require.Equal(s.T(), startedAt, result.EligibilityCutoff)
+ require.True(s.T(), result.SweepCompleted)
+ require.False(s.T(), result.MoreEligibleRanges)
+ require.Equal(s.T(), uint64(1), result.ContinueAsNewCount)
+ require.Equal(s.T(), uint64(2), result.SelectedObjectRanges)
+ require.Equal(s.T(), uint64(2), result.ProcessedObjectRanges)
+ require.Equal(s.T(), uint64(2), result.CompletedObjectRangeCount)
+ require.Equal(s.T(), uint64(20), result.DeletedVerifiedRows)
+ require.Equal(s.T(), uint64(20), result.DeletedVersions)
+ require.Equal(s.T(), uint64(2000), result.RetiredBytes)
+ require.Equal(s.T(), second.EndHeight, result.LastCompletedObjectRange.EndHeight)
+ require.Equal(s.T(), []SingleBlockRetentionCompletedRange{
+ {
+ ConsolidatedObjectKey: second.ConsolidatedObjectKey,
+ StartHeight: second.StartHeight,
+ EndHeight: second.EndHeight,
+ EligibleRows: second.RowCount,
+ },
+ }, result.CompletedObjectRanges)
+ require.Len(s.T(), result.RangeResults, 1)
+}
+
+func (s *singleBlockRetentionTestSuite) TestInitialExecutionRejectsCallerSuppliedCheckpoint() {
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ Checkpoint: &SingleBlockRetentionCheckpoint{
+ StartedAt: time.Now().UTC(),
+ ContinueAsNewCount: 1,
+ },
+ })
+ require.ErrorContains(s.T(), err, "checkpoint is internal")
+}
+
+func (s *singleBlockRetentionTestSuite) TestRangeExecutionRequiresApprovedDryRunCutoff() {
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ })
+ require.ErrorContains(s.T(), err, "eligibility cutoff from its approved dry run")
+}
+
+func (s *singleBlockRetentionTestSuite) TestContinuationRejectsChangedEligibilityCutoff() {
+ startedAt := testSingleBlockRetentionEligibilityCutoff.Add(time.Hour)
+ s.env.SetContinuedExecutionRunID("previous-run")
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 120,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff.Add(-time.Minute),
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ Checkpoint: &SingleBlockRetentionCheckpoint{
+ StartedAt: startedAt,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ EffectiveTag: 2,
+ ContinueAsNewCount: 1,
+ },
+ })
+ require.ErrorContains(s.T(), err, "eligibility cutoff changed across continuation")
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteRetriesSafetyQuiescenceOnce() {
+ cohort := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{cohort},
+ }, nil).
+ Once()
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{}, nil).
+ Once()
+ attempt := 0
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(func(_ context.Context, request *activity.SingleBlockRetentionProcessRequest) (*activity.SingleBlockRetentionRangeResult, error) {
+ attempt++
+ if attempt == 1 {
+ firstIncompleteHeight := request.Cohort.StartHeight
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: request.Cohort,
+ ScannedRows: request.Cohort.RowCount,
+ DeferredRows: request.Cohort.RowCount,
+ VerifiedThroughExclusive: request.Cohort.StartHeight,
+ FirstIncompleteHeight: &firstIncompleteHeight,
+ RetryAfter: time.Minute,
+ RetryReason: retirement.SkipCSCBSafetyQuiescenceActive,
+ }, nil
+ }
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: request.Cohort,
+ ScannedRows: request.Cohort.RowCount,
+ DeletedVerifiedRows: request.Cohort.RowCount,
+ VerifiedThroughExclusive: request.Cohort.EndHeight,
+ Terminal: true,
+ }, nil
+ }).Twice()
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 110,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 110,
+ })
+ require.NoError(s.T(), err)
+ require.Equal(s.T(), 2, attempt)
+}
+
+func (s *singleBlockRetentionTestSuite) TestExecuteFailsClosedWhenRangeIsIncomplete() {
+ cohort := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ firstIncompleteHeight := uint64(105)
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionSelect, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionSelectResponse{
+ Cohorts: []retirement.RetentionCohort{cohort},
+ }, nil)
+ s.env.OnActivity(activity.ActivitySingleBlockRetentionProcess, mock.Anything, mock.Anything).
+ Return(&activity.SingleBlockRetentionRangeResult{
+ Cohort: cohort,
+ ScannedRows: 10,
+ AlreadyRetiredRows: 9,
+ FailedRows: 1,
+ VerifiedThroughExclusive: 105,
+ FirstIncompleteHeight: &firstIncompleteHeight,
+ }, nil)
+
+ _, err := s.workflow.Execute(context.Background(), &SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 110,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 110,
+ })
+ require.ErrorContains(s.T(), err, "did not finish")
+}
+
+func TestValidateSingleBlockRetentionExecutionRequestGates(t *testing.T) {
+ validRequest := func() *SingleBlockRetentionRequest {
+ return &SingleBlockRetentionRequest{
+ StartHeight: 100,
+ EndHeight: 110,
+ EligibilityCutoff: testSingleBlockRetentionEligibilityCutoff,
+ Execute: true,
+ DirectStorageClientsGuarded: true,
+ SingleBlockWritersGuarded: true,
+ FallbackReadsValidated: true,
+ ApprovedChain: "solana-mainnet",
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 110,
+ }
+ }
+ require.NoError(t, validateSingleBlockRetentionExecutionRequest(validRequest()))
+
+ request := validRequest()
+ request.StartHeight = 0
+ request.EndHeight = 0
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionExecutionRequest(request),
+ "explicit exact selection range",
+ )
+
+ request = validRequest()
+ request.ApprovedChain = ""
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionExecutionRequest(request),
+ "operator approval chain",
+ )
+
+ request = validRequest()
+ request.ApprovedStartHeight = 110
+ request.ApprovedEndHeight = 110
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionExecutionRequest(request),
+ "valid exact approved range",
+ )
+
+ request = validRequest()
+ request.ApprovedEndHeight = 109
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionExecutionRequest(request),
+ "must exactly match the selection range",
+ )
+
+ request = validRequest()
+ request.FallbackReadsValidated = false
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionExecutionRequest(request),
+ "explicit fallback-disabled read validation",
+ )
+
+ request = validRequest()
+ request.FallbackErrorCount = 3
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionExecutionRequest(request),
+ "zero fallback read errors",
+ )
+}
+
+func TestValidateApprovedSingleBlockRetentionCohort(t *testing.T) {
+ request := &SingleBlockRetentionRequest{
+ ApprovedStartHeight: 100,
+ ApprovedEndHeight: 120,
+ }
+ require.NoError(t, validateApprovedSingleBlockRetentionCohort(
+ testRetentionCohort("consolidated/105-110.cscb.zstd", 105, 110),
+ request,
+ true,
+ ))
+ require.ErrorContains(
+ t,
+ validateApprovedSingleBlockRetentionCohort(
+ testRetentionCohort("consolidated/99-110.cscb.zstd", 99, 110),
+ request,
+ true,
+ ),
+ "outside the approved envelope",
+ )
+ require.NoError(t, validateApprovedSingleBlockRetentionCohort(
+ testRetentionCohort("consolidated/100-120.cscb.zstd", 100, 120),
+ request,
+ false,
+ ))
+ require.ErrorContains(
+ t,
+ validateApprovedSingleBlockRetentionCohort(
+ testRetentionCohort("consolidated/105-110.cscb.zstd", 105, 110),
+ request,
+ false,
+ ),
+ "does not exactly match the approved range",
+ )
+}
+
+func TestValidateSingleBlockRetentionCohorts(t *testing.T) {
+ request := &SingleBlockRetentionRequest{
+ StartHeight: 100,
+ EndHeight: 130,
+ }
+ first := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ second := testRetentionCohort("consolidated/110-120.cscb.zstd", 110, 120)
+ require.NoError(t, validateSingleBlockRetentionCohorts(
+ []retirement.RetentionCohort{second, first},
+ request,
+ true,
+ ))
+
+ duplicate := second
+ duplicate.ConsolidatedObjectKey = first.ConsolidatedObjectKey
+ require.ErrorContains(t, validateSingleBlockRetentionCohorts(
+ []retirement.RetentionCohort{first, duplicate},
+ request,
+ true,
+ ), "duplicate CSCB object")
+}
+
+func TestValidateSingleBlockRetentionRangeResult(t *testing.T) {
+ cohort := testRetentionCohort("consolidated/100-110.cscb.zstd", 100, 110)
+ require.NoError(t, validateSingleBlockRetentionRangeResult(
+ cohort,
+ terminalSingleBlockRetentionRangeResult(cohort),
+ ))
+ mismatch := terminalSingleBlockRetentionRangeResult(cohort)
+ mismatch.Cohort.EndHeight++
+ require.ErrorContains(t, validateSingleBlockRetentionRangeResult(cohort, mismatch), "does not match request")
+
+ incompleteTerminal := terminalSingleBlockRetentionRangeResult(cohort)
+ incompleteTerminal.VerifiedThroughExclusive--
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionRangeResult(cohort, incompleteTerminal),
+ "inconsistent terminal progress",
+ )
+
+ firstIncompleteHeight := cohort.EndHeight - 1
+ contradictoryTerminal := terminalSingleBlockRetentionRangeResult(cohort)
+ contradictoryTerminal.VerifiedThroughExclusive = firstIncompleteHeight
+ contradictoryTerminal.FirstIncompleteHeight = &firstIncompleteHeight
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionRangeResult(cohort, contradictoryTerminal),
+ "inconsistent incomplete progress",
+ )
+
+ impossibleAccounting := terminalSingleBlockRetentionRangeResult(cohort)
+ impossibleAccounting.DeletedVerifiedRows++
+ require.ErrorContains(
+ t,
+ validateSingleBlockRetentionRangeResult(cohort, impossibleAccounting),
+ "row accounting exceeds scanned rows",
+ )
+}
+
+func testRetentionCohort(key string, start uint64, end uint64) retirement.RetentionCohort {
+ return retirement.RetentionCohort{
+ ConsolidatedObjectKey: key,
+ StartHeight: start,
+ EndHeight: end,
+ RowCount: end - start,
+ EligibleAt: testSingleBlockRetentionEligibilityCutoff,
+ }
+}
+
+func terminalSingleBlockRetentionRangeResult(
+ cohort retirement.RetentionCohort,
+) *activity.SingleBlockRetentionRangeResult {
+ return &activity.SingleBlockRetentionRangeResult{
+ Cohort: cohort,
+ ScannedRows: cohort.RowCount,
+ DeletedVerifiedRows: cohort.RowCount,
+ DeletedVersions: cohort.RowCount,
+ VerifiedThroughExclusive: cohort.EndHeight,
+ Terminal: true,
+ }
+}
diff --git a/internal/workflow/testdata/single_block_retention_pre_parallelism_history.json b/internal/workflow/testdata/single_block_retention_pre_parallelism_history.json
new file mode 100644
index 00000000..efc2f53a
--- /dev/null
+++ b/internal/workflow/testdata/single_block_retention_pre_parallelism_history.json
@@ -0,0 +1,315 @@
+{
+ "events": [
+ {
+ "eventId": "1",
+ "eventTime": "2026-07-31T09:44:30.036495606Z",
+ "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_STARTED",
+ "taskId": "90177805",
+ "workflowExecutionStartedEventAttributes": {
+ "workflowType": {
+ "name": "workflow.single_block_retention"
+ },
+ "taskQueue": {
+ "name": "default",
+ "kind": "TASK_QUEUE_KIND_NORMAL"
+ },
+ "input": {
+ "payloads": [
+ {
+ "metadata": {
+ "encoding": "anNvbi9wbGFpbg=="
+ },
+ "data": "eyJUYWciOjIsIlN0YXJ0SGVpZ2h0Ijo0MjMwNTAwMDAsIkVuZEhlaWdodCI6NDIzMDYwMDAwLCJFbGlnaWJpbGl0eUN1dG9mZiI6IjAwMDEtMDEtMDFUMDA6MDA6MDBaIiwiTWF4T2JqZWN0UmFuZ2VzIjoxMCwiRXhlY3V0ZSI6ZmFsc2UsIlByb2R1Y3Rpb25EZWxldGVFbmFibGVkIjpmYWxzZSwiRGlyZWN0U3RvcmFnZUNsaWVudHNHdWFyZGVkIjpmYWxzZSwiU2luZ2xlQmxvY2tXcml0ZXJzR3VhcmRlZCI6ZmFsc2UsIkZhbGxiYWNrUmVhZHNWYWxpZGF0ZWQiOmZhbHNlLCJGYWxsYmFja0Vycm9yQ291bnQiOjAsIkFwcHJvdmVkQ2hhaW4iOiIiLCJBcHByb3ZlZFN0YXJ0SGVpZ2h0IjowLCJBcHByb3ZlZEVuZEhlaWdodCI6MCwiQ2hlY2twb2ludCI6bnVsbH0="
+ }
+ ]
+ },
+ "workflowExecutionTimeout": "0s",
+ "workflowRunTimeout": "86400s",
+ "workflowTaskTimeout": "10s",
+ "originalExecutionRunId": "019fb78f-9c54-778c-b942-62fa83bbbdaa",
+ "identity": "15@chainstorage-admin-prod-console-6996ff5857-sr6vl@",
+ "firstExecutionRunId": "019fb78f-9c54-778c-b942-62fa83bbbdaa",
+ "attempt": 1,
+ "firstWorkflowTaskBackoff": "0s",
+ "header": {},
+ "workflowId": "workflow.single_block_retention/dry_run_423050000_423060000_m10_20260731T094425Z"
+ }
+ },
+ {
+ "eventId": "2",
+ "eventTime": "2026-07-31T09:44:30.036581948Z",
+ "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED",
+ "taskId": "90177806",
+ "workflowTaskScheduledEventAttributes": {
+ "taskQueue": {
+ "name": "default",
+ "kind": "TASK_QUEUE_KIND_NORMAL"
+ },
+ "startToCloseTimeout": "10s",
+ "attempt": 1
+ }
+ },
+ {
+ "eventId": "3",
+ "eventTime": "2026-07-31T09:44:30.139796386Z",
+ "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED",
+ "taskId": "90177812",
+ "workflowTaskStartedEventAttributes": {
+ "scheduledEventId": "2",
+ "identity": "1@chainstorage-solana-mainnet-prod-worker-6bcb454886-f84qz@",
+ "requestId": "ae08caf4-b038-42fc-b89a-1b87f35794f6",
+ "historySizeBytes": "763",
+ "workerVersion": {
+ "buildId": "e5ae63fbb744056987768260864c191c"
+ }
+ }
+ },
+ {
+ "eventId": "4",
+ "eventTime": "2026-07-31T09:44:30.170040004Z",
+ "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED",
+ "taskId": "90177816",
+ "workflowTaskCompletedEventAttributes": {
+ "scheduledEventId": "2",
+ "startedEventId": "3",
+ "identity": "1@chainstorage-solana-mainnet-prod-worker-6bcb454886-f84qz@",
+ "workerVersion": {
+ "buildId": "e5ae63fbb744056987768260864c191c"
+ },
+ "sdkMetadata": {
+ "langUsedFlags": [
+ 3,
+ 1
+ ],
+ "sdkName": "temporal-go",
+ "sdkVersion": "1.35.0"
+ },
+ "meteringMetadata": {}
+ }
+ },
+ {
+ "eventId": "5",
+ "eventTime": "2026-07-31T09:44:30.170079565Z",
+ "eventType": "EVENT_TYPE_MARKER_RECORDED",
+ "taskId": "90177817",
+ "markerRecordedEventAttributes": {
+ "markerName": "SideEffect",
+ "details": {
+ "data": {
+ "payloads": [
+ {
+ "metadata": {
+ "encoding": "anNvbi9wbGFpbg=="
+ },
+ "data": "eyJXb3JrZmxvd0lkZW50aXR5Ijoid29ya2Zsb3cuc2luZ2xlX2Jsb2NrX3JldGVudGlvbiIsIkVuYWJsZWQiOmZhbHNlLCJUYXNrTGlzdCI6ImRlZmF1bHQiLCJXb3JrZmxvd1J1blRpbWVvdXQiOjg2NDAwMDAwMDAwMDAwLCJXb3JrZmxvd1JldHJ5IjpudWxsLCJBY3Rpdml0eVNjaGVkdWxlVG9DbG9zZVRpbWVvdXQiOjQzMjAwMDAwMDAwMDAwLCJBY3Rpdml0eVN0YXJ0VG9DbG9zZVRpbWVvdXQiOjIxNjAwMDAwMDAwMDAwLCJBY3Rpdml0eUhlYXJ0YmVhdFRpbWVvdXQiOjMwMDAwMDAwMDAwMCwiQWN0aXZpdHlSZXRyeSI6eyJNYXhpbXVtQXR0ZW1wdHMiOjMsIkJhY2tvZmZDb2VmZmljaWVudCI6MiwiSW5pdGlhbEludGVydmFsIjoxMDAwMDAwMDAwMCwiTWF4aW11bUludGVydmFsIjoxODAwMDAwMDAwMDB9LCJCbG9ja1RhZyI6eyJTdGFibGUiOjIsIkxhdGVzdCI6Mn0sIkV2ZW50VGFnIjp7IlN0YWJsZSI6MiwiTGF0ZXN0IjoyfSwiU3RvcmFnZSI6eyJEYXRhQ29tcHJlc3Npb24iOjIsIkNvbnNvbGlkYXRpb24iOnsiRW5hYmxlZCI6dHJ1ZSwiTW9kZSI6ImF1dG9fY29uc29saWRhdGUiLCJDb2RlYyI6MiwiQ29kZWNMZXZlbCI6MTIsIlpzdGRMb25nRGlzdGFuY2VXaW5kb3dMb2ciOjI3LCJNYXhDb21wcmVzc2VkQnl0ZXMiOjIxNDc0ODM2NDgsIk1heFVuY29tcHJlc3NlZEJ5dGVzIjoxMzc0Mzg5NTM0NzIsIk1heEJsb2NrcyI6MTAwMCwiQ29tcHJlc3Npb25DaHVua0Jsb2NrcyI6MjUsIk1heENodW5rQ29tcHJlc3NlZEJ5dGVzIjpudWxsLCJNYXhDaHVua1VuY29tcHJlc3NlZEJ5dGVzIjpudWxsLCJGbHVzaEludGVydmFsIjo2MDAwMDAwMDAwMCwiU2hhZG93VGltZW91dCI6MzAwMDAwMDAwMDAsIk1heEluZmxpZ2h0UmF3QmxvY2tzIjo0LCJNZW1vcnlCdWRnZXRCeXRlcyI6MjY4NDM1NDU2LCJMb2NhbFNwaWxsRGlyIjoiL3RtcC9jaGFpbnN0b3JhZ2UtY3NjYiIsIkxvY2FsU3BpbGxNYXhCeXRlcyI6NDI5NDk2NzI5NjAsIlNoYXJkU2l6ZSI6MTAwMDAsIk11bHRpcGFydFRocmVzaG9sZCI6MTM0MjE3NzI4LCJSZWFkU2hhZG93Rmlyc3QiOnRydWUsIlByb21vdGlvbkdhdGVIZWlnaHQiOm51bGwsIlNhZmVQcm9tb3Rpb25MYWciOm51bGwsIlNpbmdsZUJsb2NrT2JqZWN0UmV0ZW50aW9uIjoyNTkyMDAwMDAwMDAwMDAsIlNpbmdsZUJsb2NrU2hhZG93V3JpdGVBZnRlclN5bmNlckN1dG92ZXIiOmZhbHNlfX0sIklycmV2ZXJzaWJsZURpc3RhbmNlIjozMiwiRmFpbG92ZXJFbmFibGVkIjpmYWxzZSwiQ29uc2Vuc3VzRmFpbG92ZXJFbmFibGVkIjpmYWxzZSwiU0xBIjp7IlRpZXIiOjEsIkJsb2NrSGVpZ2h0RGVsdGEiOjEwLCJCbG9ja1RpbWVEZWx0YSI6MTIwMDAwMDAwMDAwLCJUaW1lU2luY2VMYXN0QmxvY2siOjEyMDAwMDAwMDAwMCwiRXZlbnRIZWlnaHREZWx0YSI6MTAsIkV2ZW50VGltZURlbHRhIjoxMjAwMDAwMDAwMDAsIlRpbWVTaW5jZUxhc3RFdmVudCI6MTIwMDAwMDAwMDAwLCJPdXRPZlN5bmNOb2RlRGlzdGFuY2UiOjEwLCJPdXRPZlN5bmNWYWxpZGF0b3JOb2RlRGlzdGFuY2UiOjAsIkV4cGVjdGVkV29ya2Zsb3dzIjpbIm1vbml0b3IiLCJwb2xsZXIiLCJzdHJlYW1lciIsImNyb3NzX3ZhbGlkYXRvciIsIm1pZ3JhdG9yIl19LCJNYXhPYmplY3RSYW5nZXMiOjF9"
+ }
+ ]
+ },
+ "side-effect-id": {
+ "payloads": [
+ {
+ "metadata": {
+ "encoding": "anNvbi9wbGFpbg=="
+ },
+ "data": "MQ=="
+ }
+ ]
+ }
+ },
+ "workflowTaskCompletedEventId": "4"
+ }
+ },
+ {
+ "eventId": "6",
+ "eventTime": "2026-07-31T09:44:30.170085285Z",
+ "eventType": "EVENT_TYPE_MARKER_RECORDED",
+ "taskId": "90177818",
+ "markerRecordedEventAttributes": {
+ "markerName": "Version",
+ "details": {
+ "change-id": {
+ "payloads": [
+ {
+ "metadata": {
+ "encoding": "anNvbi9wbGFpbg=="
+ },
+ "data": "InNpbmdsZV9ibG9ja19yZXRlbnRpb24ucmFuZ2Vfc3dlZXAi"
+ }
+ ]
+ },
+ "version": {
+ "payloads": [
+ {
+ "metadata": {
+ "encoding": "anNvbi9wbGFpbg=="
+ },
+ "data": "MQ=="
+ }
+ ]
+ }
+ },
+ "workflowTaskCompletedEventId": "4"
+ }
+ },
+ {
+ "eventId": "7",
+ "eventTime": "2026-07-31T09:44:30.170116576Z",
+ "eventType": "EVENT_TYPE_UPSERT_WORKFLOW_SEARCH_ATTRIBUTES",
+ "taskId": "90177819",
+ "upsertWorkflowSearchAttributesEventAttributes": {
+ "workflowTaskCompletedEventId": "4",
+ "searchAttributes": {
+ "indexedFields": {
+ "TemporalChangeVersion": {
+ "metadata": {
+ "encoding": "anNvbi9wbGFpbg==",
+ "type": "S2V5d29yZExpc3Q="
+ },
+ "data": "WyJzaW5nbGVfYmxvY2tfcmV0ZW50aW9uLnJhbmdlX3N3ZWVwLTEiXQ=="
+ }
+ }
+ }
+ }
+ },
+ {
+ "eventId": "8",
+ "eventTime": "2026-07-31T09:44:30.170149856Z",
+ "eventType": "EVENT_TYPE_ACTIVITY_TASK_SCHEDULED",
+ "taskId": "90177820",
+ "activityTaskScheduledEventAttributes": {
+ "activityId": "8",
+ "activityType": {
+ "name": "activity.single_block_retention.select"
+ },
+ "taskQueue": {
+ "name": "default",
+ "kind": "TASK_QUEUE_KIND_NORMAL"
+ },
+ "header": {},
+ "input": {
+ "payloads": [
+ {
+ "metadata": {
+ "encoding": "anNvbi9wbGFpbg=="
+ },
+ "data": "eyJUYWciOjIsIlN0YXJ0SGVpZ2h0Ijo0MjMwNTAwMDAsIkVuZEhlaWdodCI6NDIzMDYwMDAwLCJFbGlnaWJpbGl0eUN1dG9mZiI6IjIwMjYtMDctMzFUMDk6NDQ6MzAuMTM5Nzk2Mzg2WiIsIkxpbWl0IjoxMH0="
+ }
+ ]
+ },
+ "scheduleToCloseTimeout": "43200s",
+ "scheduleToStartTimeout": "43200s",
+ "startToCloseTimeout": "21600s",
+ "heartbeatTimeout": "300s",
+ "workflowTaskCompletedEventId": "4",
+ "retryPolicy": {
+ "initialInterval": "10s",
+ "backoffCoefficient": 2,
+ "maximumInterval": "180s",
+ "maximumAttempts": 3
+ },
+ "useWorkflowBuildId": true
+ }
+ },
+ {
+ "eventId": "9",
+ "eventTime": "2026-07-31T09:44:30.205234392Z",
+ "eventType": "EVENT_TYPE_ACTIVITY_TASK_STARTED",
+ "taskId": "90177827",
+ "activityTaskStartedEventAttributes": {
+ "scheduledEventId": "8",
+ "identity": "1@chainstorage-solana-mainnet-prod-worker-6bcb454886-f84qz@",
+ "requestId": "b0c0a3fe-894d-42d7-9c7d-def761f542fa",
+ "attempt": 1,
+ "workerVersion": {
+ "buildId": "e5ae63fbb744056987768260864c191c"
+ }
+ }
+ },
+ {
+ "eventId": "10",
+ "eventTime": "2026-07-31T09:44:30.713323092Z",
+ "eventType": "EVENT_TYPE_ACTIVITY_TASK_COMPLETED",
+ "taskId": "90177828",
+ "activityTaskCompletedEventAttributes": {
+ "result": {
+ "payloads": [
+ {
+ "metadata": {
+ "encoding": "anNvbi9wbGFpbg=="
+ },
+ "data": "eyJjb2hvcnRzIjpbXSwiaGFzX21vcmUiOmZhbHNlfQ=="
+ }
+ ]
+ },
+ "scheduledEventId": "8",
+ "startedEventId": "9",
+ "identity": "1@chainstorage-solana-mainnet-prod-worker-6bcb454886-f84qz@"
+ }
+ },
+ {
+ "eventId": "11",
+ "eventTime": "2026-07-31T09:44:30.713339823Z",
+ "eventType": "EVENT_TYPE_WORKFLOW_TASK_SCHEDULED",
+ "taskId": "90177829",
+ "workflowTaskScheduledEventAttributes": {
+ "taskQueue": {
+ "name": "chainstorage-solana-mainnet-prod-worker-6bcb454886-f84qz:48a12dd6-3ede-442b-967e-5c1512b0bdcf",
+ "kind": "TASK_QUEUE_KIND_STICKY",
+ "normalName": "default"
+ },
+ "startToCloseTimeout": "10s",
+ "attempt": 1
+ }
+ },
+ {
+ "eventId": "12",
+ "eventTime": "2026-07-31T09:44:30.809501777Z",
+ "eventType": "EVENT_TYPE_WORKFLOW_TASK_STARTED",
+ "taskId": "90177833",
+ "workflowTaskStartedEventAttributes": {
+ "scheduledEventId": "11",
+ "identity": "1@chainstorage-solana-mainnet-prod-worker-6bcb454886-f84qz@",
+ "requestId": "56b04dd7-3421-409f-b124-8eb21b5624cc",
+ "historySizeBytes": "3896",
+ "workerVersion": {
+ "buildId": "e5ae63fbb744056987768260864c191c"
+ }
+ }
+ },
+ {
+ "eventId": "13",
+ "eventTime": "2026-07-31T09:44:30.967081384Z",
+ "eventType": "EVENT_TYPE_WORKFLOW_TASK_COMPLETED",
+ "taskId": "90177837",
+ "workflowTaskCompletedEventAttributes": {
+ "scheduledEventId": "11",
+ "startedEventId": "12",
+ "identity": "1@chainstorage-solana-mainnet-prod-worker-6bcb454886-f84qz@",
+ "workerVersion": {
+ "buildId": "e5ae63fbb744056987768260864c191c"
+ },
+ "sdkMetadata": {},
+ "meteringMetadata": {}
+ }
+ },
+ {
+ "eventId": "14",
+ "eventTime": "2026-07-31T09:44:30.967116695Z",
+ "eventType": "EVENT_TYPE_WORKFLOW_EXECUTION_COMPLETED",
+ "taskId": "90177838",
+ "workflowExecutionCompletedEventAttributes": {
+ "result": {
+ "payloads": [
+ {
+ "metadata": {
+ "encoding": "anNvbi9wbGFpbg=="
+ },
+ "data": "eyJzdGFydGVkX2F0IjoiMjAyNi0wNy0zMVQwOTo0NDozMC4xMzk3OTYzODZaIiwiY29tcGxldGVkX2F0IjoiMjAyNi0wNy0zMVQwOTo0NDozMC44MDk1MDE3NzdaIiwiZWxpZ2liaWxpdHlfY3V0b2ZmIjoiMjAyNi0wNy0zMVQwOTo0NDozMC4xMzk3OTYzODZaIiwidGFnIjoyLCJzZWxlY3Rpb25fc3RhcnRfaGVpZ2h0Ijo0MjMwNTAwMDAsInNlbGVjdGlvbl9lbmRfaGVpZ2h0Ijo0MjMwNjAwMDAsImV4ZWN1dGUiOmZhbHNlLCJmYWxsYmFja19yZWFkc192YWxpZGF0ZWQiOmZhbHNlLCJmYWxsYmFja19lcnJvcl9jb3VudCI6MCwiY29udGludWVfYXNfbmV3X2NvdW50IjowLCJzd2VlcF9jb21wbGV0ZWQiOmZhbHNlLCJzZWxlY3RlZF9vYmplY3RfcmFuZ2VzIjowLCJtb3JlX2VsaWdpYmxlX3JhbmdlcyI6ZmFsc2UsInByb2Nlc3NlZF9vYmplY3RfcmFuZ2VzIjowLCJjb21wbGV0ZWRfb2JqZWN0X3JhbmdlX2NvdW50IjowLCJzY2FubmVkX3Jvd3MiOjAsInBsYW5uZWRfcm93cyI6MCwiZGVsZXRlZF92ZXJpZmllZF9yb3dzIjowLCJhbHJlYWR5X3JldGlyZWRfcm93cyI6MCwic2tpcHBlZF9zbG90cyI6MCwiZGVmZXJyZWRfcm93cyI6MCwiZmFpbGVkX3Jvd3MiOjAsImRlbGV0ZWRfdmVyc2lvbnMiOjAsImRlbGV0ZWRfbWFya2VycyI6MCwicmV0aXJlZF9ieXRlcyI6MH0="
+ }
+ ]
+ },
+ "workflowTaskCompletedEventId": "13"
+ }
+ }
+ ]
+}
diff --git a/internal/workflow/workflow.go b/internal/workflow/workflow.go
index d321ecbd..df569304 100644
--- a/internal/workflow/workflow.go
+++ b/internal/workflow/workflow.go
@@ -27,32 +27,38 @@ import (
type (
Manager struct {
- config *config.Config
- logger *zap.Logger
- runtime cadence.Runtime
- backfiller *Backfiller
- poller *Poller
- benchmarker *Benchmarker
- monitor *Monitor
- streamer *Streamer
- crossValidator *CrossValidator
- eventBackfiller *EventBackfiller
- replicator *Replicator
+ config *config.Config
+ logger *zap.Logger
+ runtime cadence.Runtime
+ backfiller *Backfiller
+ poller *Poller
+ benchmarker *Benchmarker
+ monitor *Monitor
+ streamer *Streamer
+ crossValidator *CrossValidator
+ eventBackfiller *EventBackfiller
+ replicator *Replicator
+ migrator *Migrator
+ batchConsolidator *BatchConsolidator
+ singleBlockRetention *SingleBlockRetention
}
ManagerParams struct {
fx.In
fxparams.Params
- Lifecycle fx.Lifecycle
- Runtime cadence.Runtime
- Backfiller *Backfiller
- Poller *Poller
- Benchmarker *Benchmarker
- Monitor *Monitor
- Streamer *Streamer
- CrossValidator *CrossValidator
- EventBackfiller *EventBackfiller
- Replicator *Replicator
+ Lifecycle fx.Lifecycle
+ Runtime cadence.Runtime
+ Backfiller *Backfiller
+ Poller *Poller
+ Benchmarker *Benchmarker
+ Monitor *Monitor
+ Streamer *Streamer
+ CrossValidator *CrossValidator
+ EventBackfiller *EventBackfiller
+ Replicator *Replicator
+ Migrator *Migrator
+ BatchConsolidator *BatchConsolidator
+ SingleBlockRetention *SingleBlockRetention
}
InstrumentedRequest interface {
@@ -79,17 +85,20 @@ const (
func NewManager(params ManagerParams) *Manager {
mgr := &Manager{
- config: params.Config,
- logger: log.WithPackage(params.Logger),
- runtime: params.Runtime,
- backfiller: params.Backfiller,
- poller: params.Poller,
- benchmarker: params.Benchmarker,
- monitor: params.Monitor,
- streamer: params.Streamer,
- crossValidator: params.CrossValidator,
- eventBackfiller: params.EventBackfiller,
- replicator: params.Replicator,
+ config: params.Config,
+ logger: log.WithPackage(params.Logger),
+ runtime: params.Runtime,
+ backfiller: params.Backfiller,
+ poller: params.Poller,
+ benchmarker: params.Benchmarker,
+ monitor: params.Monitor,
+ streamer: params.Streamer,
+ crossValidator: params.CrossValidator,
+ eventBackfiller: params.EventBackfiller,
+ replicator: params.Replicator,
+ migrator: params.Migrator,
+ batchConsolidator: params.BatchConsolidator,
+ singleBlockRetention: params.SingleBlockRetention,
}
params.Lifecycle.Append(fx.Hook{
diff --git a/internal/workflow/workflow_identity.go b/internal/workflow/workflow_identity.go
index 320434d2..241bf379 100644
--- a/internal/workflow/workflow_identity.go
+++ b/internal/workflow/workflow_identity.go
@@ -19,28 +19,37 @@ const (
CrossValidatorIdentity
EventBackfillerIdentity
ReplicatorIdentity
+ MigratorIdentity
+ BatchConsolidatorIdentity
+ SingleBlockRetentionIdentity
)
var workflowIdentityToString = map[WorkflowIdentity]string{
- BackfillerIdentity: "workflow.backfiller",
- BenchmarkerIdentity: "workflow.benchmarker",
- MonitorIdentity: "workflow.monitor",
- PollerIdentity: "workflow.poller",
- StreamerIdentity: "workflow.streamer",
- CrossValidatorIdentity: "workflow.cross_validator",
- EventBackfillerIdentity: "workflow.event_backfiller",
- ReplicatorIdentity: "workflow.replicator",
+ BackfillerIdentity: "workflow.backfiller",
+ BenchmarkerIdentity: "workflow.benchmarker",
+ MonitorIdentity: "workflow.monitor",
+ PollerIdentity: "workflow.poller",
+ StreamerIdentity: "workflow.streamer",
+ CrossValidatorIdentity: "workflow.cross_validator",
+ EventBackfillerIdentity: "workflow.event_backfiller",
+ ReplicatorIdentity: "workflow.replicator",
+ MigratorIdentity: "workflow.migrator",
+ BatchConsolidatorIdentity: "workflow.batch_consolidator",
+ SingleBlockRetentionIdentity: "workflow.single_block_retention",
}
var workflowIdentities = map[string]WorkflowIdentity{
- "backfiller": BackfillerIdentity,
- "benchmarker": BenchmarkerIdentity,
- "monitor": MonitorIdentity,
- "poller": PollerIdentity,
- "streamer": StreamerIdentity,
- "cross_validator": CrossValidatorIdentity,
- "event_backfiller": EventBackfillerIdentity,
- "replicator": ReplicatorIdentity,
+ "backfiller": BackfillerIdentity,
+ "benchmarker": BenchmarkerIdentity,
+ "monitor": MonitorIdentity,
+ "poller": PollerIdentity,
+ "streamer": StreamerIdentity,
+ "cross_validator": CrossValidatorIdentity,
+ "event_backfiller": EventBackfillerIdentity,
+ "replicator": ReplicatorIdentity,
+ "migrator": MigratorIdentity,
+ "batch_consolidator": BatchConsolidatorIdentity,
+ "single_block_retention": SingleBlockRetentionIdentity,
}
func GetWorkflowIdentify(name string) WorkflowIdentity {
@@ -105,6 +114,21 @@ func (w WorkflowIdentity) UnmarshalJsonStringToRequest(str string) (any, error)
if err = decoder.Decode(&req); err == nil {
return req, nil
}
+ case MigratorIdentity:
+ var req MigratorRequest
+ if err = decoder.Decode(&req); err == nil {
+ return req, nil
+ }
+ case BatchConsolidatorIdentity:
+ var req BatchConsolidatorRequest
+ if err = decoder.Decode(&req); err == nil {
+ return req, nil
+ }
+ case SingleBlockRetentionIdentity:
+ var req SingleBlockRetentionRequest
+ if err = decoder.Decode(&req); err == nil {
+ return req, nil
+ }
default:
err = xerrors.Errorf("unsupported workflow identity: %v", w)
}
diff --git a/internal/workflow/workflow_identity_test.go b/internal/workflow/workflow_identity_test.go
new file mode 100644
index 00000000..f88e5d03
--- /dev/null
+++ b/internal/workflow/workflow_identity_test.go
@@ -0,0 +1,27 @@
+package workflow
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestSingleBlockRetentionWorkflowIdentity(t *testing.T) {
+ identity := GetWorkflowIdentify("single_block_retention")
+ require.Equal(t, SingleBlockRetentionIdentity, identity)
+
+ name, err := identity.String()
+ require.NoError(t, err)
+ require.Equal(t, "workflow.single_block_retention", name)
+
+ request, err := identity.UnmarshalJsonStringToRequest(
+ `{"Tag":2,"StartHeight":100,"EndHeight":200,"MaxObjectRanges":25,"Execute":false}`,
+ )
+ require.NoError(t, err)
+ require.Equal(t, SingleBlockRetentionRequest{
+ Tag: 2,
+ StartHeight: 100,
+ EndHeight: 200,
+ MaxObjectRanges: 25,
+ }, request)
+}
diff --git a/mocks.yml b/mocks.yml
index 616d4c68..2abe6ca8 100644
--- a/mocks.yml
+++ b/mocks.yml
@@ -11,6 +11,7 @@ gomocks:
- package: internal/storage/blobstorage
interfaces:
- BlobStorage
+ - SingleBlockUploader
- package: internal/storage/blobstorage/downloader
interfaces:
- BlockDownloader
diff --git a/protos/coinbase/c3/common/common.pb.go b/protos/coinbase/c3/common/common.pb.go
index 3b82eadc..12037011 100644
--- a/protos/coinbase/c3/common/common.pb.go
+++ b/protos/coinbase/c3/common/common.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/c3/common/common.proto
package common
@@ -25,19 +25,33 @@ const (
type Blockchain int32
const (
- Blockchain_BLOCKCHAIN_UNKNOWN Blockchain = 0
- Blockchain_BLOCKCHAIN_SOLANA Blockchain = 11
- Blockchain_BLOCKCHAIN_BITCOIN Blockchain = 16
- Blockchain_BLOCKCHAIN_ETHEREUM Blockchain = 17
- Blockchain_BLOCKCHAIN_DOGECOIN Blockchain = 26
- Blockchain_BLOCKCHAIN_BSC Blockchain = 31
- Blockchain_BLOCKCHAIN_AVACCHAIN Blockchain = 32
- Blockchain_BLOCKCHAIN_POLYGON Blockchain = 35
- Blockchain_BLOCKCHAIN_OPTIMISM Blockchain = 39
- Blockchain_BLOCKCHAIN_ARBITRUM Blockchain = 41
- Blockchain_BLOCKCHAIN_APTOS Blockchain = 47 // L1 network using the Move language (originally created for Libra/Diem)
- Blockchain_BLOCKCHAIN_FANTOM Blockchain = 51
- Blockchain_BLOCKCHAIN_BASE Blockchain = 56 // Coinbase L2
+ Blockchain_BLOCKCHAIN_UNKNOWN Blockchain = 0
+ Blockchain_BLOCKCHAIN_SOLANA Blockchain = 11
+ Blockchain_BLOCKCHAIN_BITCOIN Blockchain = 16
+ Blockchain_BLOCKCHAIN_ETHEREUM Blockchain = 17
+ Blockchain_BLOCKCHAIN_BITCOINCASH Blockchain = 18
+ Blockchain_BLOCKCHAIN_LITECOIN Blockchain = 19
+ Blockchain_BLOCKCHAIN_DOGECOIN Blockchain = 26
+ Blockchain_BLOCKCHAIN_TRON Blockchain = 30
+ Blockchain_BLOCKCHAIN_BSC Blockchain = 31
+ Blockchain_BLOCKCHAIN_AVACCHAIN Blockchain = 32
+ Blockchain_BLOCKCHAIN_POLYGON Blockchain = 35
+ Blockchain_BLOCKCHAIN_OPTIMISM Blockchain = 39
+ Blockchain_BLOCKCHAIN_ARBITRUM Blockchain = 41
+ Blockchain_BLOCKCHAIN_APTOS Blockchain = 47 // L1 network using the Move language (originally created for Libra/Diem)
+ Blockchain_BLOCKCHAIN_FANTOM Blockchain = 51
+ Blockchain_BLOCKCHAIN_BASE Blockchain = 56 // Coinbase L2
+ Blockchain_BLOCKCHAIN_STORY Blockchain = 60
+ Blockchain_BLOCKCHAIN_ETHEREUMCLASSIC Blockchain = 61 // Ethereum Classic
+ Blockchain_BLOCKCHAIN_PLASMA Blockchain = 62 // Plasma
+ Blockchain_BLOCKCHAIN_MONAD Blockchain = 63 // Monad
+ Blockchain_BLOCKCHAIN_ABSTRACT Blockchain = 64 // Abstract
+ Blockchain_BLOCKCHAIN_MEGAETH Blockchain = 65 // MegaETH
+ Blockchain_BLOCKCHAIN_SEISMIC Blockchain = 66 // Seismic
+ Blockchain_BLOCKCHAIN_DASH Blockchain = 67 // Dash
+ Blockchain_BLOCKCHAIN_ZCASH Blockchain = 68 // Zcash
+ Blockchain_BLOCKCHAIN_TEMPO Blockchain = 69 // Tempo
+ Blockchain_BLOCKCHAIN_ROBINHOOD Blockchain = 70 // Robinhood Chain
)
// Enum value maps for Blockchain.
@@ -47,7 +61,10 @@ var (
11: "BLOCKCHAIN_SOLANA",
16: "BLOCKCHAIN_BITCOIN",
17: "BLOCKCHAIN_ETHEREUM",
+ 18: "BLOCKCHAIN_BITCOINCASH",
+ 19: "BLOCKCHAIN_LITECOIN",
26: "BLOCKCHAIN_DOGECOIN",
+ 30: "BLOCKCHAIN_TRON",
31: "BLOCKCHAIN_BSC",
32: "BLOCKCHAIN_AVACCHAIN",
35: "BLOCKCHAIN_POLYGON",
@@ -56,21 +73,46 @@ var (
47: "BLOCKCHAIN_APTOS",
51: "BLOCKCHAIN_FANTOM",
56: "BLOCKCHAIN_BASE",
+ 60: "BLOCKCHAIN_STORY",
+ 61: "BLOCKCHAIN_ETHEREUMCLASSIC",
+ 62: "BLOCKCHAIN_PLASMA",
+ 63: "BLOCKCHAIN_MONAD",
+ 64: "BLOCKCHAIN_ABSTRACT",
+ 65: "BLOCKCHAIN_MEGAETH",
+ 66: "BLOCKCHAIN_SEISMIC",
+ 67: "BLOCKCHAIN_DASH",
+ 68: "BLOCKCHAIN_ZCASH",
+ 69: "BLOCKCHAIN_TEMPO",
+ 70: "BLOCKCHAIN_ROBINHOOD",
}
Blockchain_value = map[string]int32{
- "BLOCKCHAIN_UNKNOWN": 0,
- "BLOCKCHAIN_SOLANA": 11,
- "BLOCKCHAIN_BITCOIN": 16,
- "BLOCKCHAIN_ETHEREUM": 17,
- "BLOCKCHAIN_DOGECOIN": 26,
- "BLOCKCHAIN_BSC": 31,
- "BLOCKCHAIN_AVACCHAIN": 32,
- "BLOCKCHAIN_POLYGON": 35,
- "BLOCKCHAIN_OPTIMISM": 39,
- "BLOCKCHAIN_ARBITRUM": 41,
- "BLOCKCHAIN_APTOS": 47,
- "BLOCKCHAIN_FANTOM": 51,
- "BLOCKCHAIN_BASE": 56,
+ "BLOCKCHAIN_UNKNOWN": 0,
+ "BLOCKCHAIN_SOLANA": 11,
+ "BLOCKCHAIN_BITCOIN": 16,
+ "BLOCKCHAIN_ETHEREUM": 17,
+ "BLOCKCHAIN_BITCOINCASH": 18,
+ "BLOCKCHAIN_LITECOIN": 19,
+ "BLOCKCHAIN_DOGECOIN": 26,
+ "BLOCKCHAIN_TRON": 30,
+ "BLOCKCHAIN_BSC": 31,
+ "BLOCKCHAIN_AVACCHAIN": 32,
+ "BLOCKCHAIN_POLYGON": 35,
+ "BLOCKCHAIN_OPTIMISM": 39,
+ "BLOCKCHAIN_ARBITRUM": 41,
+ "BLOCKCHAIN_APTOS": 47,
+ "BLOCKCHAIN_FANTOM": 51,
+ "BLOCKCHAIN_BASE": 56,
+ "BLOCKCHAIN_STORY": 60,
+ "BLOCKCHAIN_ETHEREUMCLASSIC": 61,
+ "BLOCKCHAIN_PLASMA": 62,
+ "BLOCKCHAIN_MONAD": 63,
+ "BLOCKCHAIN_ABSTRACT": 64,
+ "BLOCKCHAIN_MEGAETH": 65,
+ "BLOCKCHAIN_SEISMIC": 66,
+ "BLOCKCHAIN_DASH": 67,
+ "BLOCKCHAIN_ZCASH": 68,
+ "BLOCKCHAIN_TEMPO": 69,
+ "BLOCKCHAIN_ROBINHOOD": 70,
}
)
@@ -106,33 +148,55 @@ func (Blockchain) EnumDescriptor() ([]byte, []int) {
type Network int32
const (
- Network_NETWORK_UNKNOWN Network = 0
- Network_NETWORK_SOLANA_MAINNET Network = 22
- Network_NETWORK_SOLANA_TESTNET Network = 23
- Network_NETWORK_BITCOIN_MAINNET Network = 33
- Network_NETWORK_BITCOIN_TESTNET Network = 34
- Network_NETWORK_ETHEREUM_MAINNET Network = 35
- Network_NETWORK_ETHEREUM_TESTNET Network = 36
- Network_NETWORK_ETHEREUM_GOERLI Network = 66
- Network_NETWORK_DOGECOIN_MAINNET Network = 56
- Network_NETWORK_DOGECOIN_TESTNET Network = 57
- Network_NETWORK_BSC_MAINNET Network = 70
- Network_NETWORK_BSC_TESTNET Network = 71
- Network_NETWORK_AVACCHAIN_MAINNET Network = 72
- Network_NETWORK_AVACCHAIN_TESTNET Network = 73
- Network_NETWORK_POLYGON_MAINNET Network = 78
- Network_NETWORK_POLYGON_TESTNET Network = 79
- Network_NETWORK_OPTIMISM_MAINNET Network = 86
- Network_NETWORK_OPTIMISM_TESTNET Network = 87
- Network_NETWORK_ARBITRUM_MAINNET Network = 91
- Network_NETWORK_ARBITRUM_TESTNET Network = 92
- Network_NETWORK_APTOS_MAINNET Network = 103
- Network_NETWORK_APTOS_TESTNET Network = 104
- Network_NETWORK_FANTOM_MAINNET Network = 111
- Network_NETWORK_FANTOM_TESTNET Network = 112
- Network_NETWORK_BASE_MAINNET Network = 123 // Coinbase L2 running on Ethereum mainnet
- Network_NETWORK_BASE_GOERLI Network = 125 // Coinbase L2 running on Ethereum Goerli
- Network_NETWORK_ETHEREUM_HOLESKY Network = 136
+ Network_NETWORK_UNKNOWN Network = 0
+ Network_NETWORK_SOLANA_MAINNET Network = 22
+ Network_NETWORK_SOLANA_TESTNET Network = 23
+ Network_NETWORK_BITCOIN_MAINNET Network = 33
+ Network_NETWORK_BITCOIN_TESTNET Network = 34
+ Network_NETWORK_ETHEREUM_MAINNET Network = 35
+ Network_NETWORK_ETHEREUM_TESTNET Network = 36
+ Network_NETWORK_BITCOINCASH_MAINNET Network = 37
+ Network_NETWORK_BITCOINCASH_TESTNET Network = 38
+ Network_NETWORK_LITECOIN_MAINNET Network = 39
+ Network_NETWORK_LITECOIN_TESTNET Network = 40
+ Network_NETWORK_TRON_MAINNET Network = 64
+ Network_NETWORK_TRON_TESTNET Network = 65
+ Network_NETWORK_ETHEREUM_GOERLI Network = 66
+ Network_NETWORK_DOGECOIN_MAINNET Network = 56
+ Network_NETWORK_DOGECOIN_TESTNET Network = 57
+ Network_NETWORK_BSC_MAINNET Network = 70
+ Network_NETWORK_BSC_TESTNET Network = 71
+ Network_NETWORK_AVACCHAIN_MAINNET Network = 72
+ Network_NETWORK_AVACCHAIN_TESTNET Network = 73
+ Network_NETWORK_POLYGON_MAINNET Network = 78
+ Network_NETWORK_POLYGON_TESTNET Network = 79
+ Network_NETWORK_OPTIMISM_MAINNET Network = 86
+ Network_NETWORK_OPTIMISM_TESTNET Network = 87
+ Network_NETWORK_ARBITRUM_MAINNET Network = 91
+ Network_NETWORK_ARBITRUM_TESTNET Network = 92
+ Network_NETWORK_APTOS_MAINNET Network = 103
+ Network_NETWORK_APTOS_TESTNET Network = 104
+ Network_NETWORK_FANTOM_MAINNET Network = 111
+ Network_NETWORK_FANTOM_TESTNET Network = 112
+ Network_NETWORK_BASE_MAINNET Network = 123 // Coinbase L2 running on Ethereum mainnet
+ Network_NETWORK_BASE_GOERLI Network = 125 // Coinbase L2 running on Ethereum Goerli
+ Network_NETWORK_ETHEREUM_HOLESKY Network = 136
+ Network_NETWORK_STORY_MAINNET Network = 140
+ Network_NETWORK_ETHEREUMCLASSIC_MAINNET Network = 141
+ Network_NETWORK_PLASMA_MAINNET Network = 142
+ Network_NETWORK_MONAD_MAINNET Network = 143
+ Network_NETWORK_ABSTRACT_MAINNET Network = 144
+ Network_NETWORK_MEGAETH_MAINNET Network = 145
+ Network_NETWORK_SEISMIC_TESTNET Network = 146
+ Network_NETWORK_SEISMIC_MAINNET Network = 147
+ Network_NETWORK_DASH_TESTNET Network = 148
+ Network_NETWORK_DASH_MAINNET Network = 149
+ Network_NETWORK_ZCASH_MAINNET Network = 150
+ Network_NETWORK_ZCASH_TESTNET Network = 151
+ Network_NETWORK_TEMPO_TESTNET Network = 152
+ Network_NETWORK_TEMPO_MAINNET Network = 153
+ Network_NETWORK_ROBINHOOD_MAINNET Network = 154
+ Network_NETWORK_ROBINHOOD_TESTNET Network = 155
)
// Enum value maps for Network.
@@ -145,6 +209,12 @@ var (
34: "NETWORK_BITCOIN_TESTNET",
35: "NETWORK_ETHEREUM_MAINNET",
36: "NETWORK_ETHEREUM_TESTNET",
+ 37: "NETWORK_BITCOINCASH_MAINNET",
+ 38: "NETWORK_BITCOINCASH_TESTNET",
+ 39: "NETWORK_LITECOIN_MAINNET",
+ 40: "NETWORK_LITECOIN_TESTNET",
+ 64: "NETWORK_TRON_MAINNET",
+ 65: "NETWORK_TRON_TESTNET",
66: "NETWORK_ETHEREUM_GOERLI",
56: "NETWORK_DOGECOIN_MAINNET",
57: "NETWORK_DOGECOIN_TESTNET",
@@ -165,35 +235,73 @@ var (
123: "NETWORK_BASE_MAINNET",
125: "NETWORK_BASE_GOERLI",
136: "NETWORK_ETHEREUM_HOLESKY",
+ 140: "NETWORK_STORY_MAINNET",
+ 141: "NETWORK_ETHEREUMCLASSIC_MAINNET",
+ 142: "NETWORK_PLASMA_MAINNET",
+ 143: "NETWORK_MONAD_MAINNET",
+ 144: "NETWORK_ABSTRACT_MAINNET",
+ 145: "NETWORK_MEGAETH_MAINNET",
+ 146: "NETWORK_SEISMIC_TESTNET",
+ 147: "NETWORK_SEISMIC_MAINNET",
+ 148: "NETWORK_DASH_TESTNET",
+ 149: "NETWORK_DASH_MAINNET",
+ 150: "NETWORK_ZCASH_MAINNET",
+ 151: "NETWORK_ZCASH_TESTNET",
+ 152: "NETWORK_TEMPO_TESTNET",
+ 153: "NETWORK_TEMPO_MAINNET",
+ 154: "NETWORK_ROBINHOOD_MAINNET",
+ 155: "NETWORK_ROBINHOOD_TESTNET",
}
Network_value = map[string]int32{
- "NETWORK_UNKNOWN": 0,
- "NETWORK_SOLANA_MAINNET": 22,
- "NETWORK_SOLANA_TESTNET": 23,
- "NETWORK_BITCOIN_MAINNET": 33,
- "NETWORK_BITCOIN_TESTNET": 34,
- "NETWORK_ETHEREUM_MAINNET": 35,
- "NETWORK_ETHEREUM_TESTNET": 36,
- "NETWORK_ETHEREUM_GOERLI": 66,
- "NETWORK_DOGECOIN_MAINNET": 56,
- "NETWORK_DOGECOIN_TESTNET": 57,
- "NETWORK_BSC_MAINNET": 70,
- "NETWORK_BSC_TESTNET": 71,
- "NETWORK_AVACCHAIN_MAINNET": 72,
- "NETWORK_AVACCHAIN_TESTNET": 73,
- "NETWORK_POLYGON_MAINNET": 78,
- "NETWORK_POLYGON_TESTNET": 79,
- "NETWORK_OPTIMISM_MAINNET": 86,
- "NETWORK_OPTIMISM_TESTNET": 87,
- "NETWORK_ARBITRUM_MAINNET": 91,
- "NETWORK_ARBITRUM_TESTNET": 92,
- "NETWORK_APTOS_MAINNET": 103,
- "NETWORK_APTOS_TESTNET": 104,
- "NETWORK_FANTOM_MAINNET": 111,
- "NETWORK_FANTOM_TESTNET": 112,
- "NETWORK_BASE_MAINNET": 123,
- "NETWORK_BASE_GOERLI": 125,
- "NETWORK_ETHEREUM_HOLESKY": 136,
+ "NETWORK_UNKNOWN": 0,
+ "NETWORK_SOLANA_MAINNET": 22,
+ "NETWORK_SOLANA_TESTNET": 23,
+ "NETWORK_BITCOIN_MAINNET": 33,
+ "NETWORK_BITCOIN_TESTNET": 34,
+ "NETWORK_ETHEREUM_MAINNET": 35,
+ "NETWORK_ETHEREUM_TESTNET": 36,
+ "NETWORK_BITCOINCASH_MAINNET": 37,
+ "NETWORK_BITCOINCASH_TESTNET": 38,
+ "NETWORK_LITECOIN_MAINNET": 39,
+ "NETWORK_LITECOIN_TESTNET": 40,
+ "NETWORK_TRON_MAINNET": 64,
+ "NETWORK_TRON_TESTNET": 65,
+ "NETWORK_ETHEREUM_GOERLI": 66,
+ "NETWORK_DOGECOIN_MAINNET": 56,
+ "NETWORK_DOGECOIN_TESTNET": 57,
+ "NETWORK_BSC_MAINNET": 70,
+ "NETWORK_BSC_TESTNET": 71,
+ "NETWORK_AVACCHAIN_MAINNET": 72,
+ "NETWORK_AVACCHAIN_TESTNET": 73,
+ "NETWORK_POLYGON_MAINNET": 78,
+ "NETWORK_POLYGON_TESTNET": 79,
+ "NETWORK_OPTIMISM_MAINNET": 86,
+ "NETWORK_OPTIMISM_TESTNET": 87,
+ "NETWORK_ARBITRUM_MAINNET": 91,
+ "NETWORK_ARBITRUM_TESTNET": 92,
+ "NETWORK_APTOS_MAINNET": 103,
+ "NETWORK_APTOS_TESTNET": 104,
+ "NETWORK_FANTOM_MAINNET": 111,
+ "NETWORK_FANTOM_TESTNET": 112,
+ "NETWORK_BASE_MAINNET": 123,
+ "NETWORK_BASE_GOERLI": 125,
+ "NETWORK_ETHEREUM_HOLESKY": 136,
+ "NETWORK_STORY_MAINNET": 140,
+ "NETWORK_ETHEREUMCLASSIC_MAINNET": 141,
+ "NETWORK_PLASMA_MAINNET": 142,
+ "NETWORK_MONAD_MAINNET": 143,
+ "NETWORK_ABSTRACT_MAINNET": 144,
+ "NETWORK_MEGAETH_MAINNET": 145,
+ "NETWORK_SEISMIC_TESTNET": 146,
+ "NETWORK_SEISMIC_MAINNET": 147,
+ "NETWORK_DASH_TESTNET": 148,
+ "NETWORK_DASH_MAINNET": 149,
+ "NETWORK_ZCASH_MAINNET": 150,
+ "NETWORK_ZCASH_TESTNET": 151,
+ "NETWORK_TEMPO_TESTNET": 152,
+ "NETWORK_TEMPO_MAINNET": 153,
+ "NETWORK_ROBINHOOD_MAINNET": 154,
+ "NETWORK_ROBINHOOD_TESTNET": 155,
}
)
@@ -230,80 +338,142 @@ var file_coinbase_c3_common_common_proto_rawDesc = []byte{
0x0a, 0x1f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x33, 0x2f, 0x63, 0x6f,
0x6d, 0x6d, 0x6f, 0x6e, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x12, 0x12, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x33, 0x2e, 0x63,
- 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2a, 0xbf, 0x02, 0x0a, 0x0a, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63,
+ 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2a, 0x90, 0x05, 0x0a, 0x0a, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63,
0x68, 0x61, 0x69, 0x6e, 0x12, 0x16, 0x0a, 0x12, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41,
0x49, 0x4e, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x15, 0x0a, 0x11,
0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x53, 0x4f, 0x4c, 0x41, 0x4e,
0x41, 0x10, 0x0b, 0x12, 0x16, 0x0a, 0x12, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49,
0x4e, 0x5f, 0x42, 0x49, 0x54, 0x43, 0x4f, 0x49, 0x4e, 0x10, 0x10, 0x12, 0x17, 0x0a, 0x13, 0x42,
0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45,
- 0x55, 0x4d, 0x10, 0x11, 0x12, 0x17, 0x0a, 0x13, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41,
- 0x49, 0x4e, 0x5f, 0x44, 0x4f, 0x47, 0x45, 0x43, 0x4f, 0x49, 0x4e, 0x10, 0x1a, 0x12, 0x12, 0x0a,
- 0x0e, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x42, 0x53, 0x43, 0x10,
- 0x1f, 0x12, 0x18, 0x0a, 0x14, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f,
- 0x41, 0x56, 0x41, 0x43, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x10, 0x20, 0x12, 0x16, 0x0a, 0x12, 0x42,
- 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x50, 0x4f, 0x4c, 0x59, 0x47, 0x4f,
- 0x4e, 0x10, 0x23, 0x12, 0x17, 0x0a, 0x13, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49,
- 0x4e, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4d, 0x49, 0x53, 0x4d, 0x10, 0x27, 0x12, 0x17, 0x0a, 0x13,
- 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x41, 0x52, 0x42, 0x49, 0x54,
- 0x52, 0x55, 0x4d, 0x10, 0x29, 0x12, 0x14, 0x0a, 0x10, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48,
- 0x41, 0x49, 0x4e, 0x5f, 0x41, 0x50, 0x54, 0x4f, 0x53, 0x10, 0x2f, 0x12, 0x15, 0x0a, 0x11, 0x42,
- 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x46, 0x41, 0x4e, 0x54, 0x4f, 0x4d,
- 0x10, 0x33, 0x12, 0x13, 0x0a, 0x0f, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e,
- 0x5f, 0x42, 0x41, 0x53, 0x45, 0x10, 0x38, 0x2a, 0x87, 0x06, 0x0a, 0x07, 0x4e, 0x65, 0x74, 0x77,
- 0x6f, 0x72, 0x6b, 0x12, 0x13, 0x0a, 0x0f, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x55,
- 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x4e, 0x45, 0x54, 0x57,
- 0x4f, 0x52, 0x4b, 0x5f, 0x53, 0x4f, 0x4c, 0x41, 0x4e, 0x41, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e,
- 0x45, 0x54, 0x10, 0x16, 0x12, 0x1a, 0x0a, 0x16, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f,
- 0x53, 0x4f, 0x4c, 0x41, 0x4e, 0x41, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x17,
- 0x12, 0x1b, 0x0a, 0x17, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x49, 0x54, 0x43,
- 0x4f, 0x49, 0x4e, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x21, 0x12, 0x1b, 0x0a,
- 0x17, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x49, 0x54, 0x43, 0x4f, 0x49, 0x4e,
- 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x22, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45,
- 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f, 0x4d,
- 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x23, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57,
- 0x4f, 0x52, 0x4b, 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f, 0x54, 0x45, 0x53,
- 0x54, 0x4e, 0x45, 0x54, 0x10, 0x24, 0x12, 0x1b, 0x0a, 0x17, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52,
- 0x4b, 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f, 0x47, 0x4f, 0x45, 0x52, 0x4c,
- 0x49, 0x10, 0x42, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x44,
- 0x4f, 0x47, 0x45, 0x43, 0x4f, 0x49, 0x4e, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10,
- 0x38, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x44, 0x4f, 0x47,
- 0x45, 0x43, 0x4f, 0x49, 0x4e, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x39, 0x12,
- 0x17, 0x0a, 0x13, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x53, 0x43, 0x5f, 0x4d,
- 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x46, 0x12, 0x17, 0x0a, 0x13, 0x4e, 0x45, 0x54, 0x57,
- 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x53, 0x43, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10,
- 0x47, 0x12, 0x1d, 0x0a, 0x19, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x41, 0x56, 0x41,
- 0x43, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x48,
- 0x12, 0x1d, 0x0a, 0x19, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x41, 0x56, 0x41, 0x43,
- 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x49, 0x12,
+ 0x55, 0x4d, 0x10, 0x11, 0x12, 0x1a, 0x0a, 0x16, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41,
+ 0x49, 0x4e, 0x5f, 0x42, 0x49, 0x54, 0x43, 0x4f, 0x49, 0x4e, 0x43, 0x41, 0x53, 0x48, 0x10, 0x12,
+ 0x12, 0x17, 0x0a, 0x13, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x4c,
+ 0x49, 0x54, 0x45, 0x43, 0x4f, 0x49, 0x4e, 0x10, 0x13, 0x12, 0x17, 0x0a, 0x13, 0x42, 0x4c, 0x4f,
+ 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x44, 0x4f, 0x47, 0x45, 0x43, 0x4f, 0x49, 0x4e,
+ 0x10, 0x1a, 0x12, 0x13, 0x0a, 0x0f, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e,
+ 0x5f, 0x54, 0x52, 0x4f, 0x4e, 0x10, 0x1e, 0x12, 0x12, 0x0a, 0x0e, 0x42, 0x4c, 0x4f, 0x43, 0x4b,
+ 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x42, 0x53, 0x43, 0x10, 0x1f, 0x12, 0x18, 0x0a, 0x14, 0x42,
+ 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x41, 0x56, 0x41, 0x43, 0x43, 0x48,
+ 0x41, 0x49, 0x4e, 0x10, 0x20, 0x12, 0x16, 0x0a, 0x12, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48,
+ 0x41, 0x49, 0x4e, 0x5f, 0x50, 0x4f, 0x4c, 0x59, 0x47, 0x4f, 0x4e, 0x10, 0x23, 0x12, 0x17, 0x0a,
+ 0x13, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x4f, 0x50, 0x54, 0x49,
+ 0x4d, 0x49, 0x53, 0x4d, 0x10, 0x27, 0x12, 0x17, 0x0a, 0x13, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43,
+ 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x41, 0x52, 0x42, 0x49, 0x54, 0x52, 0x55, 0x4d, 0x10, 0x29, 0x12,
+ 0x14, 0x0a, 0x10, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x41, 0x50,
+ 0x54, 0x4f, 0x53, 0x10, 0x2f, 0x12, 0x15, 0x0a, 0x11, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48,
+ 0x41, 0x49, 0x4e, 0x5f, 0x46, 0x41, 0x4e, 0x54, 0x4f, 0x4d, 0x10, 0x33, 0x12, 0x13, 0x0a, 0x0f,
+ 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x42, 0x41, 0x53, 0x45, 0x10,
+ 0x38, 0x12, 0x14, 0x0a, 0x10, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f,
+ 0x53, 0x54, 0x4f, 0x52, 0x59, 0x10, 0x3c, 0x12, 0x1e, 0x0a, 0x1a, 0x42, 0x4c, 0x4f, 0x43, 0x4b,
+ 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x43, 0x4c,
+ 0x41, 0x53, 0x53, 0x49, 0x43, 0x10, 0x3d, 0x12, 0x15, 0x0a, 0x11, 0x42, 0x4c, 0x4f, 0x43, 0x4b,
+ 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x50, 0x4c, 0x41, 0x53, 0x4d, 0x41, 0x10, 0x3e, 0x12, 0x14,
+ 0x0a, 0x10, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x4d, 0x4f, 0x4e,
+ 0x41, 0x44, 0x10, 0x3f, 0x12, 0x17, 0x0a, 0x13, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41,
+ 0x49, 0x4e, 0x5f, 0x41, 0x42, 0x53, 0x54, 0x52, 0x41, 0x43, 0x54, 0x10, 0x40, 0x12, 0x16, 0x0a,
+ 0x12, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x4d, 0x45, 0x47, 0x41,
+ 0x45, 0x54, 0x48, 0x10, 0x41, 0x12, 0x16, 0x0a, 0x12, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48,
+ 0x41, 0x49, 0x4e, 0x5f, 0x53, 0x45, 0x49, 0x53, 0x4d, 0x49, 0x43, 0x10, 0x42, 0x12, 0x13, 0x0a,
+ 0x0f, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x44, 0x41, 0x53, 0x48,
+ 0x10, 0x43, 0x12, 0x14, 0x0a, 0x10, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e,
+ 0x5f, 0x5a, 0x43, 0x41, 0x53, 0x48, 0x10, 0x44, 0x12, 0x14, 0x0a, 0x10, 0x42, 0x4c, 0x4f, 0x43,
+ 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x54, 0x45, 0x4d, 0x50, 0x4f, 0x10, 0x45, 0x12, 0x18,
+ 0x0a, 0x14, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x52, 0x4f, 0x42,
+ 0x49, 0x4e, 0x48, 0x4f, 0x4f, 0x44, 0x10, 0x46, 0x2a, 0x93, 0x0b, 0x0a, 0x07, 0x4e, 0x65, 0x74,
+ 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x13, 0x0a, 0x0f, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f,
+ 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x1a, 0x0a, 0x16, 0x4e, 0x45, 0x54,
+ 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x53, 0x4f, 0x4c, 0x41, 0x4e, 0x41, 0x5f, 0x4d, 0x41, 0x49, 0x4e,
+ 0x4e, 0x45, 0x54, 0x10, 0x16, 0x12, 0x1a, 0x0a, 0x16, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b,
+ 0x5f, 0x53, 0x4f, 0x4c, 0x41, 0x4e, 0x41, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10,
+ 0x17, 0x12, 0x1b, 0x0a, 0x17, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x49, 0x54,
+ 0x43, 0x4f, 0x49, 0x4e, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x21, 0x12, 0x1b,
+ 0x0a, 0x17, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x49, 0x54, 0x43, 0x4f, 0x49,
+ 0x4e, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x22, 0x12, 0x1c, 0x0a, 0x18, 0x4e,
+ 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f,
+ 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x23, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54,
+ 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f, 0x54, 0x45,
+ 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x24, 0x12, 0x1f, 0x0a, 0x1b, 0x4e, 0x45, 0x54, 0x57, 0x4f,
+ 0x52, 0x4b, 0x5f, 0x42, 0x49, 0x54, 0x43, 0x4f, 0x49, 0x4e, 0x43, 0x41, 0x53, 0x48, 0x5f, 0x4d,
+ 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x25, 0x12, 0x1f, 0x0a, 0x1b, 0x4e, 0x45, 0x54, 0x57,
+ 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x49, 0x54, 0x43, 0x4f, 0x49, 0x4e, 0x43, 0x41, 0x53, 0x48, 0x5f,
+ 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x26, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54,
+ 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x4c, 0x49, 0x54, 0x45, 0x43, 0x4f, 0x49, 0x4e, 0x5f, 0x4d, 0x41,
+ 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x27, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f,
+ 0x52, 0x4b, 0x5f, 0x4c, 0x49, 0x54, 0x45, 0x43, 0x4f, 0x49, 0x4e, 0x5f, 0x54, 0x45, 0x53, 0x54,
+ 0x4e, 0x45, 0x54, 0x10, 0x28, 0x12, 0x18, 0x0a, 0x14, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b,
+ 0x5f, 0x54, 0x52, 0x4f, 0x4e, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x40, 0x12,
+ 0x18, 0x0a, 0x14, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x54, 0x52, 0x4f, 0x4e, 0x5f,
+ 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x41, 0x12, 0x1b, 0x0a, 0x17, 0x4e, 0x45, 0x54,
+ 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f, 0x47, 0x4f,
+ 0x45, 0x52, 0x4c, 0x49, 0x10, 0x42, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52,
+ 0x4b, 0x5f, 0x44, 0x4f, 0x47, 0x45, 0x43, 0x4f, 0x49, 0x4e, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e,
+ 0x45, 0x54, 0x10, 0x38, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f,
+ 0x44, 0x4f, 0x47, 0x45, 0x43, 0x4f, 0x49, 0x4e, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54,
+ 0x10, 0x39, 0x12, 0x17, 0x0a, 0x13, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x53,
+ 0x43, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x46, 0x12, 0x17, 0x0a, 0x13, 0x4e,
+ 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x53, 0x43, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e,
+ 0x45, 0x54, 0x10, 0x47, 0x12, 0x1d, 0x0a, 0x19, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f,
+ 0x41, 0x56, 0x41, 0x43, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45,
+ 0x54, 0x10, 0x48, 0x12, 0x1d, 0x0a, 0x19, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x41,
+ 0x56, 0x41, 0x43, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54,
+ 0x10, 0x49, 0x12, 0x1b, 0x0a, 0x17, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x4f,
+ 0x4c, 0x59, 0x47, 0x4f, 0x4e, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x4e, 0x12,
0x1b, 0x0a, 0x17, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x59, 0x47,
- 0x4f, 0x4e, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x4e, 0x12, 0x1b, 0x0a, 0x17,
- 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x50, 0x4f, 0x4c, 0x59, 0x47, 0x4f, 0x4e, 0x5f,
- 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x4f, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54,
- 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4d, 0x49, 0x53, 0x4d, 0x5f, 0x4d, 0x41,
- 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x56, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f,
- 0x52, 0x4b, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4d, 0x49, 0x53, 0x4d, 0x5f, 0x54, 0x45, 0x53, 0x54,
- 0x4e, 0x45, 0x54, 0x10, 0x57, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b,
- 0x5f, 0x41, 0x52, 0x42, 0x49, 0x54, 0x52, 0x55, 0x4d, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45,
- 0x54, 0x10, 0x5b, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x41,
- 0x52, 0x42, 0x49, 0x54, 0x52, 0x55, 0x4d, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10,
- 0x5c, 0x12, 0x19, 0x0a, 0x15, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x41, 0x50, 0x54,
- 0x4f, 0x53, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x67, 0x12, 0x19, 0x0a, 0x15,
- 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x41, 0x50, 0x54, 0x4f, 0x53, 0x5f, 0x54, 0x45,
- 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x4e, 0x45, 0x54, 0x57, 0x4f,
- 0x52, 0x4b, 0x5f, 0x46, 0x41, 0x4e, 0x54, 0x4f, 0x4d, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45,
- 0x54, 0x10, 0x6f, 0x12, 0x1a, 0x0a, 0x16, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x46,
- 0x41, 0x4e, 0x54, 0x4f, 0x4d, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x70, 0x12,
- 0x18, 0x0a, 0x14, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x41, 0x53, 0x45, 0x5f,
- 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x7b, 0x12, 0x17, 0x0a, 0x13, 0x4e, 0x45, 0x54,
- 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x41, 0x53, 0x45, 0x5f, 0x47, 0x4f, 0x45, 0x52, 0x4c, 0x49,
- 0x10, 0x7d, 0x12, 0x1d, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x45, 0x54,
- 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f, 0x48, 0x4f, 0x4c, 0x45, 0x53, 0x4b, 0x59, 0x10, 0x88,
- 0x01, 0x42, 0x3c, 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
- 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74,
- 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x69,
- 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x33, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x62,
- 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x4f, 0x4e, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x4f, 0x12, 0x1c, 0x0a, 0x18,
+ 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4d, 0x49, 0x53, 0x4d,
+ 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x56, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45,
+ 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x4f, 0x50, 0x54, 0x49, 0x4d, 0x49, 0x53, 0x4d, 0x5f, 0x54,
+ 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x57, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57,
+ 0x4f, 0x52, 0x4b, 0x5f, 0x41, 0x52, 0x42, 0x49, 0x54, 0x52, 0x55, 0x4d, 0x5f, 0x4d, 0x41, 0x49,
+ 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x5b, 0x12, 0x1c, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52,
+ 0x4b, 0x5f, 0x41, 0x52, 0x42, 0x49, 0x54, 0x52, 0x55, 0x4d, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e,
+ 0x45, 0x54, 0x10, 0x5c, 0x12, 0x19, 0x0a, 0x15, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f,
+ 0x41, 0x50, 0x54, 0x4f, 0x53, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x67, 0x12,
+ 0x19, 0x0a, 0x15, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x41, 0x50, 0x54, 0x4f, 0x53,
+ 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x68, 0x12, 0x1a, 0x0a, 0x16, 0x4e, 0x45,
+ 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x46, 0x41, 0x4e, 0x54, 0x4f, 0x4d, 0x5f, 0x4d, 0x41, 0x49,
+ 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x6f, 0x12, 0x1a, 0x0a, 0x16, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52,
+ 0x4b, 0x5f, 0x46, 0x41, 0x4e, 0x54, 0x4f, 0x4d, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54,
+ 0x10, 0x70, 0x12, 0x18, 0x0a, 0x14, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x41,
+ 0x53, 0x45, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x7b, 0x12, 0x17, 0x0a, 0x13,
+ 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x42, 0x41, 0x53, 0x45, 0x5f, 0x47, 0x4f, 0x45,
+ 0x52, 0x4c, 0x49, 0x10, 0x7d, 0x12, 0x1d, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b,
+ 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f, 0x48, 0x4f, 0x4c, 0x45, 0x53, 0x4b,
+ 0x59, 0x10, 0x88, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f,
+ 0x53, 0x54, 0x4f, 0x52, 0x59, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x8c, 0x01,
+ 0x12, 0x24, 0x0a, 0x1f, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x45, 0x54, 0x48, 0x45,
+ 0x52, 0x45, 0x55, 0x4d, 0x43, 0x4c, 0x41, 0x53, 0x53, 0x49, 0x43, 0x5f, 0x4d, 0x41, 0x49, 0x4e,
+ 0x4e, 0x45, 0x54, 0x10, 0x8d, 0x01, 0x12, 0x1b, 0x0a, 0x16, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52,
+ 0x4b, 0x5f, 0x50, 0x4c, 0x41, 0x53, 0x4d, 0x41, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54,
+ 0x10, 0x8e, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x4d,
+ 0x4f, 0x4e, 0x41, 0x44, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x8f, 0x01, 0x12,
+ 0x1d, 0x0a, 0x18, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x41, 0x42, 0x53, 0x54, 0x52,
+ 0x41, 0x43, 0x54, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x90, 0x01, 0x12, 0x1c,
+ 0x0a, 0x17, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x4d, 0x45, 0x47, 0x41, 0x45, 0x54,
+ 0x48, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x91, 0x01, 0x12, 0x1c, 0x0a, 0x17,
+ 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x53, 0x45, 0x49, 0x53, 0x4d, 0x49, 0x43, 0x5f,
+ 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x92, 0x01, 0x12, 0x1c, 0x0a, 0x17, 0x4e, 0x45,
+ 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x53, 0x45, 0x49, 0x53, 0x4d, 0x49, 0x43, 0x5f, 0x4d, 0x41,
+ 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x93, 0x01, 0x12, 0x19, 0x0a, 0x14, 0x4e, 0x45, 0x54, 0x57,
+ 0x4f, 0x52, 0x4b, 0x5f, 0x44, 0x41, 0x53, 0x48, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54,
+ 0x10, 0x94, 0x01, 0x12, 0x19, 0x0a, 0x14, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x44,
+ 0x41, 0x53, 0x48, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x95, 0x01, 0x12, 0x1a,
+ 0x0a, 0x15, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x5a, 0x43, 0x41, 0x53, 0x48, 0x5f,
+ 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x96, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x4e, 0x45,
+ 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x5a, 0x43, 0x41, 0x53, 0x48, 0x5f, 0x54, 0x45, 0x53, 0x54,
+ 0x4e, 0x45, 0x54, 0x10, 0x97, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52,
+ 0x4b, 0x5f, 0x54, 0x45, 0x4d, 0x50, 0x4f, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10,
+ 0x98, 0x01, 0x12, 0x1a, 0x0a, 0x15, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x54, 0x45,
+ 0x4d, 0x50, 0x4f, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x99, 0x01, 0x12, 0x1e,
+ 0x0a, 0x19, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x52, 0x4f, 0x42, 0x49, 0x4e, 0x48,
+ 0x4f, 0x4f, 0x44, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x10, 0x9a, 0x01, 0x12, 0x1e,
+ 0x0a, 0x19, 0x4e, 0x45, 0x54, 0x57, 0x4f, 0x52, 0x4b, 0x5f, 0x52, 0x4f, 0x42, 0x49, 0x4e, 0x48,
+ 0x4f, 0x4f, 0x44, 0x5f, 0x54, 0x45, 0x53, 0x54, 0x4e, 0x45, 0x54, 0x10, 0x9b, 0x01, 0x42, 0x3c,
+ 0x5a, 0x3a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x69,
+ 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61,
+ 0x67, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
+ 0x73, 0x65, 0x2f, 0x63, 0x33, 0x2f, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x62, 0x06, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x33,
}
var (
diff --git a/protos/coinbase/c3/common/common.proto b/protos/coinbase/c3/common/common.proto
index 9bf93d02..f26c3421 100644
--- a/protos/coinbase/c3/common/common.proto
+++ b/protos/coinbase/c3/common/common.proto
@@ -11,7 +11,10 @@ enum Blockchain {
BLOCKCHAIN_SOLANA = 11;
BLOCKCHAIN_BITCOIN = 16;
BLOCKCHAIN_ETHEREUM = 17;
+ BLOCKCHAIN_BITCOINCASH = 18;
+ BLOCKCHAIN_LITECOIN = 19;
BLOCKCHAIN_DOGECOIN = 26;
+ BLOCKCHAIN_TRON = 30;
BLOCKCHAIN_BSC = 31;
BLOCKCHAIN_AVACCHAIN = 32;
BLOCKCHAIN_POLYGON = 35;
@@ -20,6 +23,17 @@ enum Blockchain {
BLOCKCHAIN_APTOS = 47; // L1 network using the Move language (originally created for Libra/Diem)
BLOCKCHAIN_FANTOM = 51;
BLOCKCHAIN_BASE = 56; // Coinbase L2
+ BLOCKCHAIN_STORY = 60;
+ BLOCKCHAIN_ETHEREUMCLASSIC = 61; // Ethereum Classic
+ BLOCKCHAIN_PLASMA = 62; // Plasma
+ BLOCKCHAIN_MONAD = 63; // Monad
+ BLOCKCHAIN_ABSTRACT = 64; // Abstract
+ BLOCKCHAIN_MEGAETH = 65; // MegaETH
+ BLOCKCHAIN_SEISMIC = 66; // Seismic
+ BLOCKCHAIN_DASH = 67; // Dash
+ BLOCKCHAIN_ZCASH = 68; // Zcash
+ BLOCKCHAIN_TEMPO = 69; // Tempo
+ BLOCKCHAIN_ROBINHOOD = 70; // Robinhood Chain
}
// Network defines an enumeration of supported networks.
@@ -35,6 +49,16 @@ enum Network {
NETWORK_ETHEREUM_MAINNET = 35;
NETWORK_ETHEREUM_TESTNET = 36;
+
+ NETWORK_BITCOINCASH_MAINNET = 37;
+ NETWORK_BITCOINCASH_TESTNET = 38;
+
+ NETWORK_LITECOIN_MAINNET = 39;
+ NETWORK_LITECOIN_TESTNET = 40;
+
+ NETWORK_TRON_MAINNET = 64;
+ NETWORK_TRON_TESTNET = 65;
+
NETWORK_ETHEREUM_GOERLI = 66;
NETWORK_DOGECOIN_MAINNET = 56;
@@ -65,4 +89,32 @@ enum Network {
NETWORK_BASE_GOERLI = 125; // Coinbase L2 running on Ethereum Goerli
NETWORK_ETHEREUM_HOLESKY = 136;
+
+ NETWORK_STORY_MAINNET = 140;
+
+ NETWORK_ETHEREUMCLASSIC_MAINNET = 141;
+
+ NETWORK_PLASMA_MAINNET = 142;
+
+ NETWORK_MONAD_MAINNET = 143;
+
+ NETWORK_ABSTRACT_MAINNET = 144;
+
+ NETWORK_MEGAETH_MAINNET = 145;
+
+ NETWORK_SEISMIC_TESTNET = 146;
+ NETWORK_SEISMIC_MAINNET = 147;
+
+ NETWORK_DASH_TESTNET = 148;
+ NETWORK_DASH_MAINNET = 149;
+
+ NETWORK_ZCASH_MAINNET = 150;
+ NETWORK_ZCASH_TESTNET = 151;
+
+ NETWORK_TEMPO_TESTNET = 152;
+ NETWORK_TEMPO_MAINNET = 153;
+
+ NETWORK_ROBINHOOD_MAINNET = 154;
+ NETWORK_ROBINHOOD_TESTNET = 155;
+
}
diff --git a/protos/coinbase/chainstorage/api.pb.go b/protos/coinbase/chainstorage/api.pb.go
index f18d1ef0..95659b02 100644
--- a/protos/coinbase/chainstorage/api.pb.go
+++ b/protos/coinbase/chainstorage/api.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/chainstorage/api.proto
package chainstorage
@@ -28,6 +28,7 @@ const (
Compression_NONE Compression = 0
// Compressed using gzip.
Compression_GZIP Compression = 1
+ Compression_ZSTD Compression = 2
)
// Enum value maps for Compression.
@@ -35,10 +36,12 @@ var (
Compression_name = map[int32]string{
0: "NONE",
1: "GZIP",
+ 2: "ZSTD",
}
Compression_value = map[string]int32{
"NONE": 0,
"GZIP": 1,
+ "ZSTD": 2,
}
)
@@ -115,6 +118,60 @@ func (InitialPosition) EnumDescriptor() ([]byte, []int) {
return file_coinbase_chainstorage_api_proto_rawDescGZIP(), []int{1}
}
+type BlockReadSource int32
+
+const (
+ BlockReadSource_BLOCK_READ_SOURCE_DEFAULT BlockReadSource = 0
+ // Alias order controls the protojson spelling. Keep the pre-rename name first
+ // until mixed-version REST clients and servers no longer coexist.
+ BlockReadSource_BLOCK_READ_SOURCE_LEGACY BlockReadSource = 1
+ BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK BlockReadSource = 1
+ BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED BlockReadSource = 2
+)
+
+// Enum value maps for BlockReadSource.
+var (
+ BlockReadSource_name = map[int32]string{
+ 0: "BLOCK_READ_SOURCE_DEFAULT",
+ 1: "BLOCK_READ_SOURCE_LEGACY",
+ // Duplicate value: 1: "BLOCK_READ_SOURCE_SINGLE_BLOCK",
+ 2: "BLOCK_READ_SOURCE_CONSOLIDATED",
+ }
+ BlockReadSource_value = map[string]int32{
+ "BLOCK_READ_SOURCE_DEFAULT": 0,
+ "BLOCK_READ_SOURCE_LEGACY": 1,
+ "BLOCK_READ_SOURCE_SINGLE_BLOCK": 1,
+ "BLOCK_READ_SOURCE_CONSOLIDATED": 2,
+ }
+)
+
+func (x BlockReadSource) Enum() *BlockReadSource {
+ p := new(BlockReadSource)
+ *p = x
+ return p
+}
+
+func (x BlockReadSource) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (BlockReadSource) Descriptor() protoreflect.EnumDescriptor {
+ return file_coinbase_chainstorage_api_proto_enumTypes[2].Descriptor()
+}
+
+func (BlockReadSource) Type() protoreflect.EnumType {
+ return &file_coinbase_chainstorage_api_proto_enumTypes[2]
+}
+
+func (x BlockReadSource) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use BlockReadSource.Descriptor instead.
+func (BlockReadSource) EnumDescriptor() ([]byte, []int) {
+ return file_coinbase_chainstorage_api_proto_rawDescGZIP(), []int{2}
+}
+
type BlockchainEvent_Type int32
const (
@@ -151,11 +208,11 @@ func (x BlockchainEvent_Type) String() string {
}
func (BlockchainEvent_Type) Descriptor() protoreflect.EnumDescriptor {
- return file_coinbase_chainstorage_api_proto_enumTypes[2].Descriptor()
+ return file_coinbase_chainstorage_api_proto_enumTypes[3].Descriptor()
}
func (BlockchainEvent_Type) Type() protoreflect.EnumType {
- return &file_coinbase_chainstorage_api_proto_enumTypes[2]
+ return &file_coinbase_chainstorage_api_proto_enumTypes[3]
}
func (x BlockchainEvent_Type) Number() protoreflect.EnumNumber {
@@ -172,15 +229,19 @@ type BlockFile struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
- Hash string `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"`
- ParentHash string `protobuf:"bytes,3,opt,name=parent_hash,json=parentHash,proto3" json:"parent_hash,omitempty"`
- Height uint64 `protobuf:"varint,4,opt,name=height,proto3" json:"height,omitempty"`
- FileUrl string `protobuf:"bytes,5,opt,name=file_url,json=fileUrl,proto3" json:"file_url,omitempty"`
- ParentHeight uint64 `protobuf:"varint,6,opt,name=parent_height,json=parentHeight,proto3" json:"parent_height,omitempty"`
- Skipped bool `protobuf:"varint,7,opt,name=skipped,proto3" json:"skipped,omitempty"`
- Compression Compression `protobuf:"varint,8,opt,name=compression,proto3,enum=coinbase.chainstorage.Compression" json:"compression,omitempty"`
- BlockTimestamp *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=block_timestamp,json=blockTimestamp,proto3" json:"block_timestamp,omitempty"`
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ Hash string `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"`
+ ParentHash string `protobuf:"bytes,3,opt,name=parent_hash,json=parentHash,proto3" json:"parent_hash,omitempty"`
+ Height uint64 `protobuf:"varint,4,opt,name=height,proto3" json:"height,omitempty"`
+ FileUrl string `protobuf:"bytes,5,opt,name=file_url,json=fileUrl,proto3" json:"file_url,omitempty"`
+ ParentHeight uint64 `protobuf:"varint,6,opt,name=parent_height,json=parentHeight,proto3" json:"parent_height,omitempty"`
+ Skipped bool `protobuf:"varint,7,opt,name=skipped,proto3" json:"skipped,omitempty"`
+ Compression Compression `protobuf:"varint,8,opt,name=compression,proto3,enum=coinbase.chainstorage.Compression" json:"compression,omitempty"`
+ BlockTimestamp *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=block_timestamp,json=blockTimestamp,proto3" json:"block_timestamp,omitempty"`
+ ByteOffset uint64 `protobuf:"varint,10,opt,name=byte_offset,json=byteOffset,proto3" json:"byte_offset,omitempty"`
+ ByteLength uint64 `protobuf:"varint,11,opt,name=byte_length,json=byteLength,proto3" json:"byte_length,omitempty"`
+ UncompressedLength uint64 `protobuf:"varint,12,opt,name=uncompressed_length,json=uncompressedLength,proto3" json:"uncompressed_length,omitempty"`
+ ObjectFormat BlockObjectFormat `protobuf:"varint,13,opt,name=object_format,json=objectFormat,proto3,enum=coinbase.chainstorage.BlockObjectFormat" json:"object_format,omitempty"`
}
func (x *BlockFile) Reset() {
@@ -278,6 +339,34 @@ func (x *BlockFile) GetBlockTimestamp() *timestamppb.Timestamp {
return nil
}
+func (x *BlockFile) GetByteOffset() uint64 {
+ if x != nil {
+ return x.ByteOffset
+ }
+ return 0
+}
+
+func (x *BlockFile) GetByteLength() uint64 {
+ if x != nil {
+ return x.ByteLength
+ }
+ return 0
+}
+
+func (x *BlockFile) GetUncompressedLength() uint64 {
+ if x != nil {
+ return x.UncompressedLength
+ }
+ return 0
+}
+
+func (x *BlockFile) GetObjectFormat() BlockObjectFormat {
+ if x != nil {
+ return x.ObjectFormat
+ }
+ return BlockObjectFormat_BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK
+}
+
type BlockchainEvent struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -498,9 +587,10 @@ type GetBlockFileRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
- Height uint64 `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"`
- Hash string `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"`
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ Height uint64 `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"`
+ Hash string `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"`
+ ReadSource BlockReadSource `protobuf:"varint,4,opt,name=read_source,json=readSource,proto3,enum=coinbase.chainstorage.BlockReadSource" json:"read_source,omitempty"`
}
func (x *GetBlockFileRequest) Reset() {
@@ -556,6 +646,13 @@ func (x *GetBlockFileRequest) GetHash() string {
return ""
}
+func (x *GetBlockFileRequest) GetReadSource() BlockReadSource {
+ if x != nil {
+ return x.ReadSource
+ }
+ return BlockReadSource_BLOCK_READ_SOURCE_DEFAULT
+}
+
type GetBlockFileResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -608,9 +705,10 @@ type GetBlockFilesByRangeRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
- StartHeight uint64 `protobuf:"varint,2,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"`
- EndHeight uint64 `protobuf:"varint,3,opt,name=end_height,json=endHeight,proto3" json:"end_height,omitempty"`
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ StartHeight uint64 `protobuf:"varint,2,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"`
+ EndHeight uint64 `protobuf:"varint,3,opt,name=end_height,json=endHeight,proto3" json:"end_height,omitempty"`
+ ReadSource BlockReadSource `protobuf:"varint,4,opt,name=read_source,json=readSource,proto3,enum=coinbase.chainstorage.BlockReadSource" json:"read_source,omitempty"`
}
func (x *GetBlockFilesByRangeRequest) Reset() {
@@ -666,6 +764,13 @@ func (x *GetBlockFilesByRangeRequest) GetEndHeight() uint64 {
return 0
}
+func (x *GetBlockFilesByRangeRequest) GetReadSource() BlockReadSource {
+ if x != nil {
+ return x.ReadSource
+ }
+ return BlockReadSource_BLOCK_READ_SOURCE_DEFAULT
+}
+
type GetBlockFilesByRangeResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -718,9 +823,10 @@ type GetRawBlockRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
- Height uint64 `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"`
- Hash string `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"`
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ Height uint64 `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"`
+ Hash string `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"`
+ ReadSource BlockReadSource `protobuf:"varint,4,opt,name=read_source,json=readSource,proto3,enum=coinbase.chainstorage.BlockReadSource" json:"read_source,omitempty"`
}
func (x *GetRawBlockRequest) Reset() {
@@ -776,6 +882,13 @@ func (x *GetRawBlockRequest) GetHash() string {
return ""
}
+func (x *GetRawBlockRequest) GetReadSource() BlockReadSource {
+ if x != nil {
+ return x.ReadSource
+ }
+ return BlockReadSource_BLOCK_READ_SOURCE_DEFAULT
+}
+
type GetRawBlockResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -828,9 +941,10 @@ type GetRawBlocksByRangeRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
- StartHeight uint64 `protobuf:"varint,2,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"`
- EndHeight uint64 `protobuf:"varint,3,opt,name=end_height,json=endHeight,proto3" json:"end_height,omitempty"`
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ StartHeight uint64 `protobuf:"varint,2,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"`
+ EndHeight uint64 `protobuf:"varint,3,opt,name=end_height,json=endHeight,proto3" json:"end_height,omitempty"`
+ ReadSource BlockReadSource `protobuf:"varint,4,opt,name=read_source,json=readSource,proto3,enum=coinbase.chainstorage.BlockReadSource" json:"read_source,omitempty"`
}
func (x *GetRawBlocksByRangeRequest) Reset() {
@@ -886,6 +1000,13 @@ func (x *GetRawBlocksByRangeRequest) GetEndHeight() uint64 {
return 0
}
+func (x *GetRawBlocksByRangeRequest) GetReadSource() BlockReadSource {
+ if x != nil {
+ return x.ReadSource
+ }
+ return BlockReadSource_BLOCK_READ_SOURCE_DEFAULT
+}
+
type GetRawBlocksByRangeResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -938,9 +1059,10 @@ type GetNativeBlockRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
- Height uint64 `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"`
- Hash string `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"`
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ Height uint64 `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"`
+ Hash string `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"`
+ ReadSource BlockReadSource `protobuf:"varint,4,opt,name=read_source,json=readSource,proto3,enum=coinbase.chainstorage.BlockReadSource" json:"read_source,omitempty"`
}
func (x *GetNativeBlockRequest) Reset() {
@@ -996,6 +1118,13 @@ func (x *GetNativeBlockRequest) GetHash() string {
return ""
}
+func (x *GetNativeBlockRequest) GetReadSource() BlockReadSource {
+ if x != nil {
+ return x.ReadSource
+ }
+ return BlockReadSource_BLOCK_READ_SOURCE_DEFAULT
+}
+
type GetNativeBlockResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1048,9 +1177,10 @@ type GetNativeBlocksByRangeRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
- StartHeight uint64 `protobuf:"varint,2,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"`
- EndHeight uint64 `protobuf:"varint,3,opt,name=end_height,json=endHeight,proto3" json:"end_height,omitempty"`
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ StartHeight uint64 `protobuf:"varint,2,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"`
+ EndHeight uint64 `protobuf:"varint,3,opt,name=end_height,json=endHeight,proto3" json:"end_height,omitempty"`
+ ReadSource BlockReadSource `protobuf:"varint,4,opt,name=read_source,json=readSource,proto3,enum=coinbase.chainstorage.BlockReadSource" json:"read_source,omitempty"`
}
func (x *GetNativeBlocksByRangeRequest) Reset() {
@@ -1106,6 +1236,13 @@ func (x *GetNativeBlocksByRangeRequest) GetEndHeight() uint64 {
return 0
}
+func (x *GetNativeBlocksByRangeRequest) GetReadSource() BlockReadSource {
+ if x != nil {
+ return x.ReadSource
+ }
+ return BlockReadSource_BLOCK_READ_SOURCE_DEFAULT
+}
+
type GetNativeBlocksByRangeResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1158,9 +1295,10 @@ type GetRosettaBlockRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
- Height uint64 `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"`
- Hash string `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"`
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ Height uint64 `protobuf:"varint,2,opt,name=height,proto3" json:"height,omitempty"`
+ Hash string `protobuf:"bytes,3,opt,name=hash,proto3" json:"hash,omitempty"`
+ ReadSource BlockReadSource `protobuf:"varint,4,opt,name=read_source,json=readSource,proto3,enum=coinbase.chainstorage.BlockReadSource" json:"read_source,omitempty"`
}
func (x *GetRosettaBlockRequest) Reset() {
@@ -1216,6 +1354,13 @@ func (x *GetRosettaBlockRequest) GetHash() string {
return ""
}
+func (x *GetRosettaBlockRequest) GetReadSource() BlockReadSource {
+ if x != nil {
+ return x.ReadSource
+ }
+ return BlockReadSource_BLOCK_READ_SOURCE_DEFAULT
+}
+
type GetRosettaBlockResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1268,9 +1413,10 @@ type GetRosettaBlocksByRangeRequest struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
- StartHeight uint64 `protobuf:"varint,2,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"`
- EndHeight uint64 `protobuf:"varint,3,opt,name=end_height,json=endHeight,proto3" json:"end_height,omitempty"`
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ StartHeight uint64 `protobuf:"varint,2,opt,name=start_height,json=startHeight,proto3" json:"start_height,omitempty"`
+ EndHeight uint64 `protobuf:"varint,3,opt,name=end_height,json=endHeight,proto3" json:"end_height,omitempty"`
+ ReadSource BlockReadSource `protobuf:"varint,4,opt,name=read_source,json=readSource,proto3,enum=coinbase.chainstorage.BlockReadSource" json:"read_source,omitempty"`
}
func (x *GetRosettaBlocksByRangeRequest) Reset() {
@@ -1326,6 +1472,13 @@ func (x *GetRosettaBlocksByRangeRequest) GetEndHeight() uint64 {
return 0
}
+func (x *GetRosettaBlocksByRangeRequest) GetReadSource() BlockReadSource {
+ if x != nil {
+ return x.ReadSource
+ }
+ return BlockReadSource_BLOCK_READ_SOURCE_DEFAULT
+}
+
type GetRosettaBlocksByRangeResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -2194,6 +2347,140 @@ func (x *GetVerifiedAccountStateResponse) GetResponse() *ValidateAccountStateRes
return nil
}
+type GetBlockByTimestampRequest struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ Timestamp uint64 `protobuf:"varint,2,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Unix timestamp in seconds
+}
+
+func (x *GetBlockByTimestampRequest) Reset() {
+ *x = GetBlockByTimestampRequest{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_coinbase_chainstorage_api_proto_msgTypes[34]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetBlockByTimestampRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBlockByTimestampRequest) ProtoMessage() {}
+
+func (x *GetBlockByTimestampRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_coinbase_chainstorage_api_proto_msgTypes[34]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBlockByTimestampRequest.ProtoReflect.Descriptor instead.
+func (*GetBlockByTimestampRequest) Descriptor() ([]byte, []int) {
+ return file_coinbase_chainstorage_api_proto_rawDescGZIP(), []int{34}
+}
+
+func (x *GetBlockByTimestampRequest) GetTag() uint32 {
+ if x != nil {
+ return x.Tag
+ }
+ return 0
+}
+
+func (x *GetBlockByTimestampRequest) GetTimestamp() uint64 {
+ if x != nil {
+ return x.Timestamp
+ }
+ return 0
+}
+
+type GetBlockByTimestampResponse struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ Tag uint32 `protobuf:"varint,1,opt,name=tag,proto3" json:"tag,omitempty"`
+ Hash string `protobuf:"bytes,2,opt,name=hash,proto3" json:"hash,omitempty"`
+ ParentHash string `protobuf:"bytes,3,opt,name=parent_hash,json=parentHash,proto3" json:"parent_hash,omitempty"`
+ Height uint64 `protobuf:"varint,4,opt,name=height,proto3" json:"height,omitempty"`
+ Timestamp uint64 `protobuf:"varint,5,opt,name=timestamp,proto3" json:"timestamp,omitempty"` // Unix timestamp in seconds
+}
+
+func (x *GetBlockByTimestampResponse) Reset() {
+ *x = GetBlockByTimestampResponse{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_coinbase_chainstorage_api_proto_msgTypes[35]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *GetBlockByTimestampResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetBlockByTimestampResponse) ProtoMessage() {}
+
+func (x *GetBlockByTimestampResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_coinbase_chainstorage_api_proto_msgTypes[35]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetBlockByTimestampResponse.ProtoReflect.Descriptor instead.
+func (*GetBlockByTimestampResponse) Descriptor() ([]byte, []int) {
+ return file_coinbase_chainstorage_api_proto_rawDescGZIP(), []int{35}
+}
+
+func (x *GetBlockByTimestampResponse) GetTag() uint32 {
+ if x != nil {
+ return x.Tag
+ }
+ return 0
+}
+
+func (x *GetBlockByTimestampResponse) GetHash() string {
+ if x != nil {
+ return x.Hash
+ }
+ return ""
+}
+
+func (x *GetBlockByTimestampResponse) GetParentHash() string {
+ if x != nil {
+ return x.ParentHash
+ }
+ return ""
+}
+
+func (x *GetBlockByTimestampResponse) GetHeight() uint64 {
+ if x != nil {
+ return x.Height
+ }
+ return 0
+}
+
+func (x *GetBlockByTimestampResponse) GetTimestamp() uint64 {
+ if x != nil {
+ return x.Timestamp
+ }
+ return 0
+}
+
var File_coinbase_chainstorage_api_proto protoreflect.FileDescriptor
var file_coinbase_chainstorage_api_proto_rawDesc = []byte{
@@ -2205,7 +2492,7 @@ var file_coinbase_chainstorage_api_proto_rawDesc = []byte{
0x61, 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x26, 0x63, 0x6f, 0x69, 0x6e, 0x62,
0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
0x2f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74,
- 0x6f, 0x22, 0xcf, 0x02, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x12,
+ 0x6f, 0x22, 0x91, 0x04, 0x0a, 0x09, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x12,
0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61,
0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f,
@@ -2226,380 +2513,462 @@ var file_coinbase_chainstorage_api_proto_rawDesc = []byte{
0x70, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
0x61, 0x6d, 0x70, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
- 0x61, 0x6d, 0x70, 0x22, 0xa9, 0x02, 0x0a, 0x0f, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61,
- 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65,
- 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x08, 0x73,
- 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x3f, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
- 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c,
- 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e, 0x54, 0x79,
- 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x3c, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63,
- 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
+ 0x61, 0x6d, 0x70, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x5f, 0x6f, 0x66, 0x66, 0x73,
+ 0x65, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x4f, 0x66,
+ 0x66, 0x73, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x5f, 0x6c, 0x65, 0x6e,
+ 0x67, 0x74, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x4c,
+ 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x2f, 0x0a, 0x13, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72,
+ 0x65, 0x73, 0x73, 0x65, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x0c, 0x20, 0x01,
+ 0x28, 0x04, 0x52, 0x12, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64,
+ 0x4c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x4d, 0x0a, 0x0d, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74,
+ 0x5f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e,
+ 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74,
+ 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63,
+ 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x0c, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x46,
+ 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0xa9, 0x02, 0x0a, 0x0f, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63,
+ 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x1e, 0x0a, 0x08, 0x73, 0x65, 0x71,
+ 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52,
+ 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x3f, 0x0a, 0x04, 0x74, 0x79, 0x70,
+ 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2b, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
- 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52,
- 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f,
- 0x74, 0x61, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x65, 0x76, 0x65, 0x6e, 0x74,
- 0x54, 0x61, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x5f,
- 0x6e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73, 0x65, 0x71, 0x75, 0x65,
- 0x6e, 0x63, 0x65, 0x4e, 0x75, 0x6d, 0x22, 0x37, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0b,
- 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a, 0x0b, 0x42,
- 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11, 0x0a, 0x0d,
- 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x44, 0x10, 0x02, 0x22,
- 0x29, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63,
- 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18,
- 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0xb1, 0x01, 0x0a, 0x16, 0x47,
- 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x70,
- 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x0a, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06,
- 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65,
- 0x69, 0x67, 0x68, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
- 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
- 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
- 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x53,
- 0x0a, 0x13, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68,
- 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12,
- 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68,
- 0x61, 0x73, 0x68, 0x22, 0x4c, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46,
- 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x04, 0x66,
- 0x69, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x2e,
+ 0x54, 0x79, 0x70, 0x65, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x3c, 0x0a, 0x05, 0x62, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
- 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x04, 0x66, 0x69, 0x6c,
- 0x65, 0x22, 0x71, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c,
- 0x65, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74,
- 0x61, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67,
- 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48,
- 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69,
- 0x67, 0x68, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65,
- 0x69, 0x67, 0x68, 0x74, 0x22, 0x56, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
- 0x46, 0x69, 0x6c, 0x65, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20,
- 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63,
- 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63,
- 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x22, 0x52, 0x0a, 0x12,
- 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52,
- 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02,
- 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x12, 0x0a, 0x04,
- 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68,
+ 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65,
+ 0x72, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e,
+ 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x65, 0x76, 0x65,
+ 0x6e, 0x74, 0x54, 0x61, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63,
+ 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73, 0x65, 0x71,
+ 0x75, 0x65, 0x6e, 0x63, 0x65, 0x4e, 0x75, 0x6d, 0x22, 0x37, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65,
+ 0x12, 0x0b, 0x0a, 0x07, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x0f, 0x0a,
+ 0x0b, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x41, 0x44, 0x44, 0x45, 0x44, 0x10, 0x01, 0x12, 0x11,
+ 0x0a, 0x0d, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x52, 0x45, 0x4d, 0x4f, 0x56, 0x45, 0x44, 0x10,
+ 0x02, 0x22, 0x29, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61,
+ 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x22, 0xb1, 0x01, 0x0a,
+ 0x16, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73,
+ 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a,
+ 0x0b, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16,
+ 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06,
+ 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
+ 0x61, 0x6d, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+ 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
+ 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
+ 0x22, 0x9c, 0x01, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c,
+ 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65,
+ 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67,
+ 0x68, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x47, 0x0a, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x73,
+ 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x63, 0x6f,
+ 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72,
+ 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22,
+ 0x4c, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
+ 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x04, 0x66, 0x69, 0x6c, 0x65, 0x22, 0xba, 0x01,
+ 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x42,
+ 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a,
+ 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12,
+ 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x69, 0x67,
+ 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74,
+ 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65, 0x69, 0x67, 0x68,
+ 0x74, 0x12, 0x47, 0x0a, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
+ 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0a,
+ 0x72, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x56, 0x0a, 0x1c, 0x47, 0x65,
+ 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e,
+ 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x05, 0x66, 0x69,
+ 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+ 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x05, 0x66, 0x69, 0x6c,
+ 0x65, 0x73, 0x22, 0x9b, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x68,
+ 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69,
+ 0x67, 0x68, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x47, 0x0a, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x5f,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x63,
+ 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
+ 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x61, 0x64, 0x53, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65,
0x22, 0x49, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x32, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42,
- 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x70, 0x0a, 0x1a, 0x47,
- 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0xb9, 0x01, 0x0a, 0x1a,
+ 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61,
+ 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61,
+ 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x21, 0x0a, 0x0c,
+ 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x04, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12,
+ 0x1d, 0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x04, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x47,
+ 0x0a, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20,
+ 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63,
+ 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63,
+ 0x6b, 0x52, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x61,
+ 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x53, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x52, 0x61,
+ 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73,
+ 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
+ 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x22, 0x9e, 0x01, 0x0a,
+ 0x15, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67,
+ 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74,
+ 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
+ 0x68, 0x61, 0x73, 0x68, 0x12, 0x47, 0x0a, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x73, 0x6f, 0x75,
+ 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+ 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63,
+ 0x65, 0x52, 0x0a, 0x72, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x52, 0x0a,
+ 0x16, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
+ 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x4e,
+ 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63,
+ 0x6b, 0x22, 0xbc, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
+ 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68,
+ 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x73, 0x74, 0x61,
+ 0x72, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f,
+ 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x65, 0x6e,
+ 0x64, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x47, 0x0a, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x5f,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x63,
+ 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
+ 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x61, 0x64, 0x53, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x22, 0x5c, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x03,
+ 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
+ 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x4e, 0x61, 0x74, 0x69, 0x76,
+ 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x22, 0x9f,
+ 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67,
+ 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x68,
+ 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69,
+ 0x67, 0x68, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x47, 0x0a, 0x0b, 0x72, 0x65, 0x61, 0x64, 0x5f,
+ 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x63,
+ 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
+ 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x61, 0x64, 0x53, 0x6f,
+ 0x75, 0x72, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65,
+ 0x22, 0x54, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x05, 0x62,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x69,
+ 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61,
+ 0x67, 0x65, 0x2e, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52,
+ 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0xbd, 0x01, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x52, 0x6f,
+ 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e,
0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67,
0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x73,
0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28,
0x04, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d,
0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x03, 0x20, 0x01,
- 0x28, 0x04, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x22, 0x53, 0x0a,
- 0x1b, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52,
- 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x06,
- 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x1c, 0x2e, 0x63,
- 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
- 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x06, 0x62, 0x6c, 0x6f, 0x63,
- 0x6b, 0x73, 0x22, 0x55, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42,
- 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74,
- 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a,
- 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68,
- 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x52, 0x0a, 0x16, 0x47, 0x65, 0x74,
- 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x38, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
- 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x4e, 0x61, 0x74, 0x69, 0x76,
- 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x73, 0x0a,
- 0x1d, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73,
- 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10,
- 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67,
- 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x69,
- 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68,
- 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65, 0x69, 0x67,
- 0x68, 0x74, 0x22, 0x5c, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42,
- 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3a, 0x0a, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x01,
- 0x20, 0x03, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
- 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x4e, 0x61, 0x74,
- 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73,
- 0x22, 0x56, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c,
- 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61,
- 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06,
- 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65,
- 0x69, 0x67, 0x68, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x22, 0x54, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x52,
- 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x39, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
- 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x52, 0x6f, 0x73, 0x65, 0x74,
- 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0x74,
- 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63,
- 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74,
- 0x61, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67,
- 0x68, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x73, 0x74, 0x61, 0x72, 0x74, 0x48,
- 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x65, 0x6e, 0x64, 0x5f, 0x68, 0x65, 0x69,
- 0x67, 0x68, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65,
- 0x69, 0x67, 0x68, 0x74, 0x22, 0x5e, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74,
- 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52,
- 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
- 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
- 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
- 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x06, 0x62, 0x6c,
- 0x6f, 0x63, 0x6b, 0x73, 0x22, 0xb1, 0x01, 0x0a, 0x12, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76,
- 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a, 0x1a, 0x69,
- 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
- 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x17, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e,
- 0x49, 0x6e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1e, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75,
- 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x08,
- 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e,
- 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x65, 0x76, 0x65,
- 0x6e, 0x74, 0x54, 0x61, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63,
- 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73, 0x65, 0x71,
- 0x75, 0x65, 0x6e, 0x63, 0x65, 0x4e, 0x75, 0x6d, 0x22, 0x53, 0x0a, 0x13, 0x43, 0x68, 0x61, 0x69,
- 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
- 0x3c, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26,
- 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73,
- 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69,
- 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0xda, 0x01,
- 0x0a, 0x15, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73,
- 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65,
- 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x08, 0x73,
- 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x3b, 0x0a, 0x1a, 0x69, 0x6e, 0x69, 0x74, 0x69,
- 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x73,
- 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x69, 0x6e, 0x69,
- 0x74, 0x69, 0x61, 0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x53, 0x74,
- 0x72, 0x65, 0x61, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x6e, 0x75, 0x6d, 0x5f,
- 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x6d, 0x61,
- 0x78, 0x4e, 0x75, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x76,
- 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x65,
+ 0x28, 0x04, 0x52, 0x09, 0x65, 0x6e, 0x64, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x47, 0x0a,
+ 0x0b, 0x72, 0x65, 0x61, 0x64, 0x5f, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x18, 0x04, 0x20, 0x01,
+ 0x28, 0x0e, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
+ 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x52, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x52, 0x0a, 0x72, 0x65, 0x61, 0x64,
+ 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x5e, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73,
+ 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67,
+ 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3b, 0x0a, 0x06, 0x62, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+ 0x65, 0x2e, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x06,
+ 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x22, 0xb1, 0x01, 0x0a, 0x12, 0x43, 0x68, 0x61, 0x69, 0x6e,
+ 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x3b, 0x0a,
+ 0x1a, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f,
+ 0x6e, 0x5f, 0x69, 0x6e, 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x17, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69,
+ 0x6f, 0x6e, 0x49, 0x6e, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x1e, 0x0a, 0x08, 0x73, 0x65,
+ 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01,
+ 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x65, 0x76,
+ 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x08, 0x65,
0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x71, 0x75, 0x65,
- 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73,
- 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x4e, 0x75, 0x6d, 0x22, 0x58, 0x0a, 0x16, 0x47, 0x65,
- 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01,
- 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
- 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f,
- 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06, 0x65, 0x76,
- 0x65, 0x6e, 0x74, 0x73, 0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e,
- 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x22,
- 0xc4, 0x02, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61,
- 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28, 0x0a, 0x10,
- 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x61, 0x67,
- 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c,
- 0x6f, 0x63, 0x6b, 0x54, 0x61, 0x67, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65,
- 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x09, 0x20, 0x01, 0x28, 0x0d,
- 0x52, 0x0e, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x61, 0x67,
- 0x12, 0x28, 0x0a, 0x10, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74,
- 0x5f, 0x74, 0x61, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x6c, 0x61, 0x74, 0x65,
- 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x67, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x74,
- 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x0b,
- 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76, 0x65, 0x6e,
- 0x74, 0x54, 0x61, 0x67, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x73, 0x74,
- 0x61, 0x72, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04,
- 0x52, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65, 0x69, 0x67,
- 0x68, 0x74, 0x12, 0x33, 0x0a, 0x15, 0x69, 0x72, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x69, 0x62,
- 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28,
- 0x04, 0x52, 0x14, 0x69, 0x72, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x69, 0x62, 0x6c, 0x65, 0x44,
- 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
- 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x6c, 0x6f,
- 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xbc, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x56, 0x65,
- 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e,
- 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x66, 0x72, 0x6f, 0x6d,
- 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d,
- 0x52, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x67, 0x12, 0x27,
- 0x0a, 0x0d, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18,
- 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x53,
- 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x5f, 0x65, 0x76,
- 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0a, 0x74,
- 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x67, 0x12, 0x2a, 0x0a, 0x11, 0x66, 0x72, 0x6f,
- 0x6d, 0x5f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x04,
- 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x66, 0x72, 0x6f, 0x6d, 0x53, 0x65, 0x71, 0x75, 0x65, 0x6e,
- 0x63, 0x65, 0x4e, 0x75, 0x6d, 0x22, 0x5e, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73,
- 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52,
- 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74,
- 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
+ 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x73,
+ 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x4e, 0x75, 0x6d, 0x22, 0x53, 0x0a, 0x13, 0x43, 0x68,
+ 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x12, 0x3c, 0x0a, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69,
+ 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68,
+ 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22,
+ 0xda, 0x01, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e,
+ 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1e, 0x0a, 0x08, 0x73, 0x65, 0x71,
+ 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52,
+ 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x3b, 0x0a, 0x1a, 0x69, 0x6e, 0x69,
+ 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e,
+ 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x17, 0x69,
+ 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e,
+ 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x24, 0x0a, 0x0e, 0x6d, 0x61, 0x78, 0x5f, 0x6e, 0x75,
+ 0x6d, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c,
+ 0x6d, 0x61, 0x78, 0x4e, 0x75, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09,
+ 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52,
+ 0x08, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x67, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x65, 0x71,
+ 0x75, 0x65, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52,
+ 0x0b, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x4e, 0x75, 0x6d, 0x22, 0x58, 0x0a, 0x16,
+ 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73,
+ 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42,
- 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x05,
- 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x5b, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63,
- 0x6b, 0x42, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73,
- 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x61,
- 0x73, 0x68, 0x22, 0x5f, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x42, 0x79,
- 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18, 0x01, 0x20,
- 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63,
- 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63,
- 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x06, 0x62, 0x6c, 0x6f,
- 0x63, 0x6b, 0x73, 0x22, 0x5a, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65,
- 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52,
- 0x03, 0x74, 0x61, 0x67, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74,
- 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f,
- 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x61, 0x73, 0x68, 0x22,
- 0x6c, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e,
- 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
- 0x4c, 0x0a, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18,
- 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
- 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x4e, 0x61,
- 0x74, 0x69, 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52,
- 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x71, 0x0a,
- 0x1e, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f,
- 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12,
- 0x4f, 0x0a, 0x03, 0x72, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x63,
- 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
- 0x72, 0x61, 0x67, 0x65, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x65, 0x74,
- 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53,
- 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x03, 0x72, 0x65, 0x71,
- 0x22, 0x72, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41,
- 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x18,
- 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
- 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x56, 0x61,
- 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61,
- 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x08, 0x72, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x2a, 0x21, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73,
- 0x69, 0x6f, 0x6e, 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a,
- 0x04, 0x47, 0x5a, 0x49, 0x50, 0x10, 0x01, 0x2a, 0x2b, 0x0a, 0x0f, 0x49, 0x6e, 0x69, 0x74, 0x69,
- 0x61, 0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x0c, 0x0a, 0x08, 0x45, 0x41,
- 0x52, 0x4c, 0x49, 0x45, 0x53, 0x54, 0x10, 0x00, 0x12, 0x0a, 0x0a, 0x06, 0x4c, 0x41, 0x54, 0x45,
- 0x53, 0x54, 0x10, 0x01, 0x32, 0xaa, 0x0f, 0x0a, 0x0c, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x53, 0x74,
- 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x6d, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65,
- 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x2c, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x06,
+ 0x65, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x22, 0x19, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61,
+ 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x22, 0xc4, 0x02, 0x0a, 0x18, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65,
+ 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x28,
+ 0x0a, 0x10, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74,
+ 0x61, 0x67, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74,
+ 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x61, 0x67, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x74, 0x61, 0x62,
+ 0x6c, 0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x09, 0x20, 0x01,
+ 0x28, 0x0d, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x54,
+ 0x61, 0x67, 0x12, 0x28, 0x0a, 0x10, 0x6c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x5f, 0x65, 0x76, 0x65,
+ 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x6c, 0x61,
+ 0x74, 0x65, 0x73, 0x74, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x67, 0x12, 0x28, 0x0a, 0x10,
+ 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x67,
+ 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0e, 0x73, 0x74, 0x61, 0x62, 0x6c, 0x65, 0x45, 0x76,
+ 0x65, 0x6e, 0x74, 0x54, 0x61, 0x67, 0x12, 0x2c, 0x0a, 0x12, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f,
+ 0x73, 0x74, 0x61, 0x72, 0x74, 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x0c, 0x20, 0x01,
+ 0x28, 0x04, 0x52, 0x10, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x53, 0x74, 0x61, 0x72, 0x74, 0x48, 0x65,
+ 0x69, 0x67, 0x68, 0x74, 0x12, 0x33, 0x0a, 0x15, 0x69, 0x72, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73,
+ 0x69, 0x62, 0x6c, 0x65, 0x5f, 0x64, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x0d, 0x20,
+ 0x01, 0x28, 0x04, 0x52, 0x14, 0x69, 0x72, 0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x69, 0x62, 0x6c,
+ 0x65, 0x44, 0x69, 0x73, 0x74, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x22, 0xbc, 0x01, 0x0a, 0x1d, 0x47, 0x65, 0x74,
+ 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76,
+ 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x66, 0x72,
+ 0x6f, 0x6d, 0x5f, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x0d, 0x52, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x67,
+ 0x12, 0x27, 0x0a, 0x0d, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63,
+ 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x02, 0x18, 0x01, 0x52, 0x0c, 0x66, 0x72, 0x6f,
+ 0x6d, 0x53, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x20, 0x0a, 0x0c, 0x74, 0x6f, 0x5f,
+ 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x74, 0x61, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52,
+ 0x0a, 0x74, 0x6f, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x54, 0x61, 0x67, 0x12, 0x2a, 0x0a, 0x11, 0x66,
+ 0x72, 0x6f, 0x6d, 0x5f, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x5f, 0x6e, 0x75, 0x6d,
+ 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x66, 0x72, 0x6f, 0x6d, 0x53, 0x65, 0x71, 0x75,
+ 0x65, 0x6e, 0x63, 0x65, 0x4e, 0x75, 0x6d, 0x22, 0x5e, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x56, 0x65,
+ 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e,
+ 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3c, 0x0a, 0x05, 0x65, 0x76, 0x65,
+ 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62,
+ 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+ 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74,
+ 0x52, 0x05, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x22, 0x5b, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x42, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x42, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+ 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01,
+ 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72, 0x61,
+ 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+ 0x48, 0x61, 0x73, 0x68, 0x22, 0x5f, 0x0a, 0x1d, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x42, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3e, 0x0a, 0x06, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x18,
+ 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x26, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
+ 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x49, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x66, 0x69, 0x65, 0x72, 0x52, 0x06, 0x62,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x22, 0x5a, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69,
+ 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61,
+ 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x61, 0x73,
+ 0x68, 0x22, 0x6c, 0x0a, 0x1c, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x54, 0x72,
+ 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x12, 0x4c, 0x0a, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+ 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
- 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65,
- 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
- 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65,
- 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
- 0x46, 0x69, 0x6c, 0x65, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
+ 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
+ 0x6e, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22,
+ 0x71, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63,
+ 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
+ 0x74, 0x12, 0x4f, 0x0a, 0x03, 0x72, 0x65, 0x71, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3d,
+ 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73,
+ 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47,
+ 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e,
+ 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x52, 0x03, 0x72,
+ 0x65, 0x71, 0x22, 0x72, 0x0a, 0x1f, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65,
+ 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4f, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
+ 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
+ 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53,
+ 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x08, 0x72, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x4c, 0x0a, 0x1a, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x42, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
+ 0x61, 0x6d, 0x70, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73,
+ 0x74, 0x61, 0x6d, 0x70, 0x22, 0x9a, 0x01, 0x0a, 0x1b, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63,
+ 0x6b, 0x42, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28,
+ 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61,
+ 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x0a, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68,
+ 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69,
+ 0x67, 0x68, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
+ 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
+ 0x70, 0x2a, 0x2b, 0x0a, 0x0b, 0x43, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
+ 0x12, 0x08, 0x0a, 0x04, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x47, 0x5a,
+ 0x49, 0x50, 0x10, 0x01, 0x12, 0x08, 0x0a, 0x04, 0x5a, 0x53, 0x54, 0x44, 0x10, 0x02, 0x2a, 0x2b,
+ 0x0a, 0x0f, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x6c, 0x50, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x6f,
+ 0x6e, 0x12, 0x0c, 0x0a, 0x08, 0x45, 0x41, 0x52, 0x4c, 0x49, 0x45, 0x53, 0x54, 0x10, 0x00, 0x12,
+ 0x0a, 0x0a, 0x06, 0x4c, 0x41, 0x54, 0x45, 0x53, 0x54, 0x10, 0x01, 0x2a, 0x9a, 0x01, 0x0a, 0x0f,
+ 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x61, 0x64, 0x53, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x12,
+ 0x1d, 0x0a, 0x19, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x53, 0x4f,
+ 0x55, 0x52, 0x43, 0x45, 0x5f, 0x44, 0x45, 0x46, 0x41, 0x55, 0x4c, 0x54, 0x10, 0x00, 0x12, 0x1c,
+ 0x0a, 0x18, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x53, 0x4f, 0x55,
+ 0x52, 0x43, 0x45, 0x5f, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x10, 0x01, 0x12, 0x22, 0x0a, 0x1e,
+ 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x53, 0x4f, 0x55, 0x52, 0x43,
+ 0x45, 0x5f, 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x5f, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x10, 0x01,
+ 0x12, 0x22, 0x0a, 0x1e, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x52, 0x45, 0x41, 0x44, 0x5f, 0x53,
+ 0x4f, 0x55, 0x52, 0x43, 0x45, 0x5f, 0x43, 0x4f, 0x4e, 0x53, 0x4f, 0x4c, 0x49, 0x44, 0x41, 0x54,
+ 0x45, 0x44, 0x10, 0x02, 0x1a, 0x02, 0x10, 0x01, 0x32, 0xa8, 0x10, 0x0a, 0x0c, 0x43, 0x68, 0x61,
+ 0x69, 0x6e, 0x53, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x12, 0x6d, 0x0a, 0x0e, 0x47, 0x65, 0x74,
+ 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x2c, 0x2e, 0x63, 0x6f,
+ 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72,
+ 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+ 0x65, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x67, 0x0a, 0x0c, 0x47, 0x65, 0x74, 0x42,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x12, 0x2a, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62,
+ 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+ 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74,
- 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
- 0x1a, 0x2b, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69,
- 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63,
- 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7f, 0x0a,
- 0x14, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x42, 0x79,
- 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
- 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65,
- 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e,
- 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x12, 0x7f, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c,
+ 0x65, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69, 0x6c, 0x65, 0x73, 0x42,
- 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x64,
- 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x29, 0x2e,
+ 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e,
0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74,
- 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63,
- 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62,
- 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
- 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70,
- 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c,
- 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x31, 0x2e, 0x63, 0x6f,
- 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72,
- 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73,
- 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32,
- 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73,
- 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f,
- 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42,
- 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x2c, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
- 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74,
- 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
- 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61,
- 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
- 0x65, 0x12, 0x85, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42,
- 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x34, 0x2e, 0x63,
+ 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x46, 0x69,
+ 0x6c, 0x65, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
+ 0x73, 0x65, 0x12, 0x64, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63,
+ 0x6b, 0x12, 0x29, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61,
+ 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77,
+ 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x63,
0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
- 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c,
+ 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x52,
+ 0x61, 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12,
+ 0x31, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e,
+ 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61, 0x77, 0x42, 0x6c,
0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
- 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61,
- 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67,
- 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a, 0x0f, 0x47, 0x65, 0x74,
- 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x2d, 0x2e, 0x63,
- 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
- 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42,
- 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x63, 0x6f,
+ 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
+ 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x61,
+ 0x77, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6d, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74,
+ 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x2c, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62,
+ 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+ 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
+ 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47,
+ 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74,
+ 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65,
+ 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69,
+ 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69,
+ 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52,
+ 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
+ 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47,
+ 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79,
+ 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x70, 0x0a,
+ 0x0f, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x12, 0x2d, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69,
+ 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65,
+ 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x2e, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e,
+ 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74,
+ 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+ 0x88, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x35, 0x2e, 0x63, 0x6f,
0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72,
0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c,
- 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x17,
- 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73,
- 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
- 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
- 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73,
- 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36,
- 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73,
- 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74,
- 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65,
- 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x11, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d,
- 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x29, 0x2e, 0x63, 0x6f,
+ 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
+ 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
+ 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x52, 0x6f,
+ 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x73, 0x42, 0x79, 0x52, 0x61, 0x6e,
+ 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x6c, 0x0a, 0x11, 0x53, 0x74,
+ 0x72, 0x65, 0x61, 0x6d, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12,
+ 0x29, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e,
+ 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65,
+ 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x63, 0x6f, 0x69,
+ 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61,
+ 0x67, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x30, 0x01, 0x12, 0x6d, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43,
+ 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2c, 0x2e, 0x63, 0x6f, 0x69,
+ 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61,
+ 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74,
+ 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62,
+ 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+ 0x2e, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52,
+ 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x73, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x68,
+ 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2e, 0x2e, 0x63, 0x6f,
0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72,
- 0x61, 0x67, 0x65, 0x2e, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52,
- 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
- 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x43,
- 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x30, 0x01, 0x12, 0x6d, 0x0a, 0x0e, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e,
- 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x2c, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
- 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47,
- 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71,
- 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2d, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
- 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74,
- 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x73, 0x0a, 0x10, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d,
- 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2e, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
- 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
- 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
- 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
+ 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61,
+ 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2f, 0x2e, 0x63, 0x6f,
+ 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72,
+ 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61,
+ 0x64, 0x61, 0x74, 0x61, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a,
+ 0x16, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61,
+ 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
- 0x47, 0x65, 0x74, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61,
- 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x85, 0x01, 0x0a, 0x16, 0x47, 0x65, 0x74,
- 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76,
- 0x65, 0x6e, 0x74, 0x12, 0x34, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63,
- 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x56,
- 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65,
- 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
- 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
- 0x65, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x68,
- 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
- 0x12, 0x82, 0x01, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x42, 0x79, 0x54,
- 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33, 0x2e, 0x63, 0x6f, 0x69,
- 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61,
- 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x42, 0x79, 0x54, 0x72, 0x61,
- 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
- 0x34, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e,
- 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
- 0x42, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73,
- 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7f, 0x0a, 0x14, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69,
- 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x32, 0x2e,
+ 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x65, 0x64, 0x43, 0x68, 0x61, 0x69,
+ 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e,
0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74,
- 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x54,
- 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
- 0x74, 0x1a, 0x33, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61,
+ 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e,
+ 0x65, 0x64, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63,
+ 0x6b, 0x42, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x33,
+ 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73,
+ 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x42,
+ 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63,
+ 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x42, 0x79, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
+ 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7f, 0x0a, 0x14, 0x47, 0x65, 0x74,
+ 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
+ 0x6e, 0x12, 0x32, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61,
0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x4e, 0x61, 0x74,
0x69, 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
- 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x56, 0x65,
- 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61,
- 0x74, 0x65, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
- 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x65,
- 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61,
- 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
- 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
- 0x65, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63,
- 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
- 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
- 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74,
- 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x69,
- 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61,
- 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
+ 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65,
+ 0x74, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69,
+ 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x17, 0x47,
+ 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e,
+ 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x35, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
+ 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47,
+ 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e,
+ 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e,
+ 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74,
+ 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65,
+ 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73,
+ 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63,
+ 0x6b, 0x42, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x31, 0x2e, 0x63,
+ 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
+ 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x42, 0x79, 0x54,
+ 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
+ 0x32, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e,
+ 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x42, 0x79, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x65, 0x73, 0x70, 0x6f,
+ 0x6e, 0x73, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
+ 0x6d, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e,
+ 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x63,
+ 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
+ 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -2614,113 +2983,128 @@ func file_coinbase_chainstorage_api_proto_rawDescGZIP() []byte {
return file_coinbase_chainstorage_api_proto_rawDescData
}
-var file_coinbase_chainstorage_api_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
-var file_coinbase_chainstorage_api_proto_msgTypes = make([]protoimpl.MessageInfo, 34)
+var file_coinbase_chainstorage_api_proto_enumTypes = make([]protoimpl.EnumInfo, 4)
+var file_coinbase_chainstorage_api_proto_msgTypes = make([]protoimpl.MessageInfo, 36)
var file_coinbase_chainstorage_api_proto_goTypes = []interface{}{
(Compression)(0), // 0: coinbase.chainstorage.Compression
(InitialPosition)(0), // 1: coinbase.chainstorage.InitialPosition
- (BlockchainEvent_Type)(0), // 2: coinbase.chainstorage.BlockchainEvent.Type
- (*BlockFile)(nil), // 3: coinbase.chainstorage.BlockFile
- (*BlockchainEvent)(nil), // 4: coinbase.chainstorage.BlockchainEvent
- (*GetLatestBlockRequest)(nil), // 5: coinbase.chainstorage.GetLatestBlockRequest
- (*GetLatestBlockResponse)(nil), // 6: coinbase.chainstorage.GetLatestBlockResponse
- (*GetBlockFileRequest)(nil), // 7: coinbase.chainstorage.GetBlockFileRequest
- (*GetBlockFileResponse)(nil), // 8: coinbase.chainstorage.GetBlockFileResponse
- (*GetBlockFilesByRangeRequest)(nil), // 9: coinbase.chainstorage.GetBlockFilesByRangeRequest
- (*GetBlockFilesByRangeResponse)(nil), // 10: coinbase.chainstorage.GetBlockFilesByRangeResponse
- (*GetRawBlockRequest)(nil), // 11: coinbase.chainstorage.GetRawBlockRequest
- (*GetRawBlockResponse)(nil), // 12: coinbase.chainstorage.GetRawBlockResponse
- (*GetRawBlocksByRangeRequest)(nil), // 13: coinbase.chainstorage.GetRawBlocksByRangeRequest
- (*GetRawBlocksByRangeResponse)(nil), // 14: coinbase.chainstorage.GetRawBlocksByRangeResponse
- (*GetNativeBlockRequest)(nil), // 15: coinbase.chainstorage.GetNativeBlockRequest
- (*GetNativeBlockResponse)(nil), // 16: coinbase.chainstorage.GetNativeBlockResponse
- (*GetNativeBlocksByRangeRequest)(nil), // 17: coinbase.chainstorage.GetNativeBlocksByRangeRequest
- (*GetNativeBlocksByRangeResponse)(nil), // 18: coinbase.chainstorage.GetNativeBlocksByRangeResponse
- (*GetRosettaBlockRequest)(nil), // 19: coinbase.chainstorage.GetRosettaBlockRequest
- (*GetRosettaBlockResponse)(nil), // 20: coinbase.chainstorage.GetRosettaBlockResponse
- (*GetRosettaBlocksByRangeRequest)(nil), // 21: coinbase.chainstorage.GetRosettaBlocksByRangeRequest
- (*GetRosettaBlocksByRangeResponse)(nil), // 22: coinbase.chainstorage.GetRosettaBlocksByRangeResponse
- (*ChainEventsRequest)(nil), // 23: coinbase.chainstorage.ChainEventsRequest
- (*ChainEventsResponse)(nil), // 24: coinbase.chainstorage.ChainEventsResponse
- (*GetChainEventsRequest)(nil), // 25: coinbase.chainstorage.GetChainEventsRequest
- (*GetChainEventsResponse)(nil), // 26: coinbase.chainstorage.GetChainEventsResponse
- (*GetChainMetadataRequest)(nil), // 27: coinbase.chainstorage.GetChainMetadataRequest
- (*GetChainMetadataResponse)(nil), // 28: coinbase.chainstorage.GetChainMetadataResponse
- (*GetVersionedChainEventRequest)(nil), // 29: coinbase.chainstorage.GetVersionedChainEventRequest
- (*GetVersionedChainEventResponse)(nil), // 30: coinbase.chainstorage.GetVersionedChainEventResponse
- (*GetBlockByTransactionRequest)(nil), // 31: coinbase.chainstorage.GetBlockByTransactionRequest
- (*GetBlockByTransactionResponse)(nil), // 32: coinbase.chainstorage.GetBlockByTransactionResponse
- (*GetNativeTransactionRequest)(nil), // 33: coinbase.chainstorage.GetNativeTransactionRequest
- (*GetNativeTransactionResponse)(nil), // 34: coinbase.chainstorage.GetNativeTransactionResponse
- (*GetVerifiedAccountStateRequest)(nil), // 35: coinbase.chainstorage.GetVerifiedAccountStateRequest
- (*GetVerifiedAccountStateResponse)(nil), // 36: coinbase.chainstorage.GetVerifiedAccountStateResponse
- (*timestamppb.Timestamp)(nil), // 37: google.protobuf.Timestamp
- (*BlockIdentifier)(nil), // 38: coinbase.chainstorage.BlockIdentifier
- (*Block)(nil), // 39: coinbase.chainstorage.Block
- (*NativeBlock)(nil), // 40: coinbase.chainstorage.NativeBlock
- (*RosettaBlock)(nil), // 41: coinbase.chainstorage.RosettaBlock
- (*NativeTransaction)(nil), // 42: coinbase.chainstorage.NativeTransaction
- (*InternalGetVerifiedAccountStateRequest)(nil), // 43: coinbase.chainstorage.InternalGetVerifiedAccountStateRequest
- (*ValidateAccountStateResponse)(nil), // 44: coinbase.chainstorage.ValidateAccountStateResponse
+ (BlockReadSource)(0), // 2: coinbase.chainstorage.BlockReadSource
+ (BlockchainEvent_Type)(0), // 3: coinbase.chainstorage.BlockchainEvent.Type
+ (*BlockFile)(nil), // 4: coinbase.chainstorage.BlockFile
+ (*BlockchainEvent)(nil), // 5: coinbase.chainstorage.BlockchainEvent
+ (*GetLatestBlockRequest)(nil), // 6: coinbase.chainstorage.GetLatestBlockRequest
+ (*GetLatestBlockResponse)(nil), // 7: coinbase.chainstorage.GetLatestBlockResponse
+ (*GetBlockFileRequest)(nil), // 8: coinbase.chainstorage.GetBlockFileRequest
+ (*GetBlockFileResponse)(nil), // 9: coinbase.chainstorage.GetBlockFileResponse
+ (*GetBlockFilesByRangeRequest)(nil), // 10: coinbase.chainstorage.GetBlockFilesByRangeRequest
+ (*GetBlockFilesByRangeResponse)(nil), // 11: coinbase.chainstorage.GetBlockFilesByRangeResponse
+ (*GetRawBlockRequest)(nil), // 12: coinbase.chainstorage.GetRawBlockRequest
+ (*GetRawBlockResponse)(nil), // 13: coinbase.chainstorage.GetRawBlockResponse
+ (*GetRawBlocksByRangeRequest)(nil), // 14: coinbase.chainstorage.GetRawBlocksByRangeRequest
+ (*GetRawBlocksByRangeResponse)(nil), // 15: coinbase.chainstorage.GetRawBlocksByRangeResponse
+ (*GetNativeBlockRequest)(nil), // 16: coinbase.chainstorage.GetNativeBlockRequest
+ (*GetNativeBlockResponse)(nil), // 17: coinbase.chainstorage.GetNativeBlockResponse
+ (*GetNativeBlocksByRangeRequest)(nil), // 18: coinbase.chainstorage.GetNativeBlocksByRangeRequest
+ (*GetNativeBlocksByRangeResponse)(nil), // 19: coinbase.chainstorage.GetNativeBlocksByRangeResponse
+ (*GetRosettaBlockRequest)(nil), // 20: coinbase.chainstorage.GetRosettaBlockRequest
+ (*GetRosettaBlockResponse)(nil), // 21: coinbase.chainstorage.GetRosettaBlockResponse
+ (*GetRosettaBlocksByRangeRequest)(nil), // 22: coinbase.chainstorage.GetRosettaBlocksByRangeRequest
+ (*GetRosettaBlocksByRangeResponse)(nil), // 23: coinbase.chainstorage.GetRosettaBlocksByRangeResponse
+ (*ChainEventsRequest)(nil), // 24: coinbase.chainstorage.ChainEventsRequest
+ (*ChainEventsResponse)(nil), // 25: coinbase.chainstorage.ChainEventsResponse
+ (*GetChainEventsRequest)(nil), // 26: coinbase.chainstorage.GetChainEventsRequest
+ (*GetChainEventsResponse)(nil), // 27: coinbase.chainstorage.GetChainEventsResponse
+ (*GetChainMetadataRequest)(nil), // 28: coinbase.chainstorage.GetChainMetadataRequest
+ (*GetChainMetadataResponse)(nil), // 29: coinbase.chainstorage.GetChainMetadataResponse
+ (*GetVersionedChainEventRequest)(nil), // 30: coinbase.chainstorage.GetVersionedChainEventRequest
+ (*GetVersionedChainEventResponse)(nil), // 31: coinbase.chainstorage.GetVersionedChainEventResponse
+ (*GetBlockByTransactionRequest)(nil), // 32: coinbase.chainstorage.GetBlockByTransactionRequest
+ (*GetBlockByTransactionResponse)(nil), // 33: coinbase.chainstorage.GetBlockByTransactionResponse
+ (*GetNativeTransactionRequest)(nil), // 34: coinbase.chainstorage.GetNativeTransactionRequest
+ (*GetNativeTransactionResponse)(nil), // 35: coinbase.chainstorage.GetNativeTransactionResponse
+ (*GetVerifiedAccountStateRequest)(nil), // 36: coinbase.chainstorage.GetVerifiedAccountStateRequest
+ (*GetVerifiedAccountStateResponse)(nil), // 37: coinbase.chainstorage.GetVerifiedAccountStateResponse
+ (*GetBlockByTimestampRequest)(nil), // 38: coinbase.chainstorage.GetBlockByTimestampRequest
+ (*GetBlockByTimestampResponse)(nil), // 39: coinbase.chainstorage.GetBlockByTimestampResponse
+ (*timestamppb.Timestamp)(nil), // 40: google.protobuf.Timestamp
+ (BlockObjectFormat)(0), // 41: coinbase.chainstorage.BlockObjectFormat
+ (*BlockIdentifier)(nil), // 42: coinbase.chainstorage.BlockIdentifier
+ (*Block)(nil), // 43: coinbase.chainstorage.Block
+ (*NativeBlock)(nil), // 44: coinbase.chainstorage.NativeBlock
+ (*RosettaBlock)(nil), // 45: coinbase.chainstorage.RosettaBlock
+ (*NativeTransaction)(nil), // 46: coinbase.chainstorage.NativeTransaction
+ (*InternalGetVerifiedAccountStateRequest)(nil), // 47: coinbase.chainstorage.InternalGetVerifiedAccountStateRequest
+ (*ValidateAccountStateResponse)(nil), // 48: coinbase.chainstorage.ValidateAccountStateResponse
}
var file_coinbase_chainstorage_api_proto_depIdxs = []int32{
0, // 0: coinbase.chainstorage.BlockFile.compression:type_name -> coinbase.chainstorage.Compression
- 37, // 1: coinbase.chainstorage.BlockFile.block_timestamp:type_name -> google.protobuf.Timestamp
- 2, // 2: coinbase.chainstorage.BlockchainEvent.type:type_name -> coinbase.chainstorage.BlockchainEvent.Type
- 38, // 3: coinbase.chainstorage.BlockchainEvent.block:type_name -> coinbase.chainstorage.BlockIdentifier
- 37, // 4: coinbase.chainstorage.GetLatestBlockResponse.timestamp:type_name -> google.protobuf.Timestamp
- 3, // 5: coinbase.chainstorage.GetBlockFileResponse.file:type_name -> coinbase.chainstorage.BlockFile
- 3, // 6: coinbase.chainstorage.GetBlockFilesByRangeResponse.files:type_name -> coinbase.chainstorage.BlockFile
- 39, // 7: coinbase.chainstorage.GetRawBlockResponse.block:type_name -> coinbase.chainstorage.Block
- 39, // 8: coinbase.chainstorage.GetRawBlocksByRangeResponse.blocks:type_name -> coinbase.chainstorage.Block
- 40, // 9: coinbase.chainstorage.GetNativeBlockResponse.block:type_name -> coinbase.chainstorage.NativeBlock
- 40, // 10: coinbase.chainstorage.GetNativeBlocksByRangeResponse.blocks:type_name -> coinbase.chainstorage.NativeBlock
- 41, // 11: coinbase.chainstorage.GetRosettaBlockResponse.block:type_name -> coinbase.chainstorage.RosettaBlock
- 41, // 12: coinbase.chainstorage.GetRosettaBlocksByRangeResponse.blocks:type_name -> coinbase.chainstorage.RosettaBlock
- 4, // 13: coinbase.chainstorage.ChainEventsResponse.event:type_name -> coinbase.chainstorage.BlockchainEvent
- 4, // 14: coinbase.chainstorage.GetChainEventsResponse.events:type_name -> coinbase.chainstorage.BlockchainEvent
- 4, // 15: coinbase.chainstorage.GetVersionedChainEventResponse.event:type_name -> coinbase.chainstorage.BlockchainEvent
- 38, // 16: coinbase.chainstorage.GetBlockByTransactionResponse.blocks:type_name -> coinbase.chainstorage.BlockIdentifier
- 42, // 17: coinbase.chainstorage.GetNativeTransactionResponse.transactions:type_name -> coinbase.chainstorage.NativeTransaction
- 43, // 18: coinbase.chainstorage.GetVerifiedAccountStateRequest.req:type_name -> coinbase.chainstorage.InternalGetVerifiedAccountStateRequest
- 44, // 19: coinbase.chainstorage.GetVerifiedAccountStateResponse.response:type_name -> coinbase.chainstorage.ValidateAccountStateResponse
- 5, // 20: coinbase.chainstorage.ChainStorage.GetLatestBlock:input_type -> coinbase.chainstorage.GetLatestBlockRequest
- 7, // 21: coinbase.chainstorage.ChainStorage.GetBlockFile:input_type -> coinbase.chainstorage.GetBlockFileRequest
- 9, // 22: coinbase.chainstorage.ChainStorage.GetBlockFilesByRange:input_type -> coinbase.chainstorage.GetBlockFilesByRangeRequest
- 11, // 23: coinbase.chainstorage.ChainStorage.GetRawBlock:input_type -> coinbase.chainstorage.GetRawBlockRequest
- 13, // 24: coinbase.chainstorage.ChainStorage.GetRawBlocksByRange:input_type -> coinbase.chainstorage.GetRawBlocksByRangeRequest
- 15, // 25: coinbase.chainstorage.ChainStorage.GetNativeBlock:input_type -> coinbase.chainstorage.GetNativeBlockRequest
- 17, // 26: coinbase.chainstorage.ChainStorage.GetNativeBlocksByRange:input_type -> coinbase.chainstorage.GetNativeBlocksByRangeRequest
- 19, // 27: coinbase.chainstorage.ChainStorage.GetRosettaBlock:input_type -> coinbase.chainstorage.GetRosettaBlockRequest
- 21, // 28: coinbase.chainstorage.ChainStorage.GetRosettaBlocksByRange:input_type -> coinbase.chainstorage.GetRosettaBlocksByRangeRequest
- 23, // 29: coinbase.chainstorage.ChainStorage.StreamChainEvents:input_type -> coinbase.chainstorage.ChainEventsRequest
- 25, // 30: coinbase.chainstorage.ChainStorage.GetChainEvents:input_type -> coinbase.chainstorage.GetChainEventsRequest
- 27, // 31: coinbase.chainstorage.ChainStorage.GetChainMetadata:input_type -> coinbase.chainstorage.GetChainMetadataRequest
- 29, // 32: coinbase.chainstorage.ChainStorage.GetVersionedChainEvent:input_type -> coinbase.chainstorage.GetVersionedChainEventRequest
- 31, // 33: coinbase.chainstorage.ChainStorage.GetBlockByTransaction:input_type -> coinbase.chainstorage.GetBlockByTransactionRequest
- 33, // 34: coinbase.chainstorage.ChainStorage.GetNativeTransaction:input_type -> coinbase.chainstorage.GetNativeTransactionRequest
- 35, // 35: coinbase.chainstorage.ChainStorage.GetVerifiedAccountState:input_type -> coinbase.chainstorage.GetVerifiedAccountStateRequest
- 6, // 36: coinbase.chainstorage.ChainStorage.GetLatestBlock:output_type -> coinbase.chainstorage.GetLatestBlockResponse
- 8, // 37: coinbase.chainstorage.ChainStorage.GetBlockFile:output_type -> coinbase.chainstorage.GetBlockFileResponse
- 10, // 38: coinbase.chainstorage.ChainStorage.GetBlockFilesByRange:output_type -> coinbase.chainstorage.GetBlockFilesByRangeResponse
- 12, // 39: coinbase.chainstorage.ChainStorage.GetRawBlock:output_type -> coinbase.chainstorage.GetRawBlockResponse
- 14, // 40: coinbase.chainstorage.ChainStorage.GetRawBlocksByRange:output_type -> coinbase.chainstorage.GetRawBlocksByRangeResponse
- 16, // 41: coinbase.chainstorage.ChainStorage.GetNativeBlock:output_type -> coinbase.chainstorage.GetNativeBlockResponse
- 18, // 42: coinbase.chainstorage.ChainStorage.GetNativeBlocksByRange:output_type -> coinbase.chainstorage.GetNativeBlocksByRangeResponse
- 20, // 43: coinbase.chainstorage.ChainStorage.GetRosettaBlock:output_type -> coinbase.chainstorage.GetRosettaBlockResponse
- 22, // 44: coinbase.chainstorage.ChainStorage.GetRosettaBlocksByRange:output_type -> coinbase.chainstorage.GetRosettaBlocksByRangeResponse
- 24, // 45: coinbase.chainstorage.ChainStorage.StreamChainEvents:output_type -> coinbase.chainstorage.ChainEventsResponse
- 26, // 46: coinbase.chainstorage.ChainStorage.GetChainEvents:output_type -> coinbase.chainstorage.GetChainEventsResponse
- 28, // 47: coinbase.chainstorage.ChainStorage.GetChainMetadata:output_type -> coinbase.chainstorage.GetChainMetadataResponse
- 30, // 48: coinbase.chainstorage.ChainStorage.GetVersionedChainEvent:output_type -> coinbase.chainstorage.GetVersionedChainEventResponse
- 32, // 49: coinbase.chainstorage.ChainStorage.GetBlockByTransaction:output_type -> coinbase.chainstorage.GetBlockByTransactionResponse
- 34, // 50: coinbase.chainstorage.ChainStorage.GetNativeTransaction:output_type -> coinbase.chainstorage.GetNativeTransactionResponse
- 36, // 51: coinbase.chainstorage.ChainStorage.GetVerifiedAccountState:output_type -> coinbase.chainstorage.GetVerifiedAccountStateResponse
- 36, // [36:52] is the sub-list for method output_type
- 20, // [20:36] is the sub-list for method input_type
- 20, // [20:20] is the sub-list for extension type_name
- 20, // [20:20] is the sub-list for extension extendee
- 0, // [0:20] is the sub-list for field type_name
+ 40, // 1: coinbase.chainstorage.BlockFile.block_timestamp:type_name -> google.protobuf.Timestamp
+ 41, // 2: coinbase.chainstorage.BlockFile.object_format:type_name -> coinbase.chainstorage.BlockObjectFormat
+ 3, // 3: coinbase.chainstorage.BlockchainEvent.type:type_name -> coinbase.chainstorage.BlockchainEvent.Type
+ 42, // 4: coinbase.chainstorage.BlockchainEvent.block:type_name -> coinbase.chainstorage.BlockIdentifier
+ 40, // 5: coinbase.chainstorage.GetLatestBlockResponse.timestamp:type_name -> google.protobuf.Timestamp
+ 2, // 6: coinbase.chainstorage.GetBlockFileRequest.read_source:type_name -> coinbase.chainstorage.BlockReadSource
+ 4, // 7: coinbase.chainstorage.GetBlockFileResponse.file:type_name -> coinbase.chainstorage.BlockFile
+ 2, // 8: coinbase.chainstorage.GetBlockFilesByRangeRequest.read_source:type_name -> coinbase.chainstorage.BlockReadSource
+ 4, // 9: coinbase.chainstorage.GetBlockFilesByRangeResponse.files:type_name -> coinbase.chainstorage.BlockFile
+ 2, // 10: coinbase.chainstorage.GetRawBlockRequest.read_source:type_name -> coinbase.chainstorage.BlockReadSource
+ 43, // 11: coinbase.chainstorage.GetRawBlockResponse.block:type_name -> coinbase.chainstorage.Block
+ 2, // 12: coinbase.chainstorage.GetRawBlocksByRangeRequest.read_source:type_name -> coinbase.chainstorage.BlockReadSource
+ 43, // 13: coinbase.chainstorage.GetRawBlocksByRangeResponse.blocks:type_name -> coinbase.chainstorage.Block
+ 2, // 14: coinbase.chainstorage.GetNativeBlockRequest.read_source:type_name -> coinbase.chainstorage.BlockReadSource
+ 44, // 15: coinbase.chainstorage.GetNativeBlockResponse.block:type_name -> coinbase.chainstorage.NativeBlock
+ 2, // 16: coinbase.chainstorage.GetNativeBlocksByRangeRequest.read_source:type_name -> coinbase.chainstorage.BlockReadSource
+ 44, // 17: coinbase.chainstorage.GetNativeBlocksByRangeResponse.blocks:type_name -> coinbase.chainstorage.NativeBlock
+ 2, // 18: coinbase.chainstorage.GetRosettaBlockRequest.read_source:type_name -> coinbase.chainstorage.BlockReadSource
+ 45, // 19: coinbase.chainstorage.GetRosettaBlockResponse.block:type_name -> coinbase.chainstorage.RosettaBlock
+ 2, // 20: coinbase.chainstorage.GetRosettaBlocksByRangeRequest.read_source:type_name -> coinbase.chainstorage.BlockReadSource
+ 45, // 21: coinbase.chainstorage.GetRosettaBlocksByRangeResponse.blocks:type_name -> coinbase.chainstorage.RosettaBlock
+ 5, // 22: coinbase.chainstorage.ChainEventsResponse.event:type_name -> coinbase.chainstorage.BlockchainEvent
+ 5, // 23: coinbase.chainstorage.GetChainEventsResponse.events:type_name -> coinbase.chainstorage.BlockchainEvent
+ 5, // 24: coinbase.chainstorage.GetVersionedChainEventResponse.event:type_name -> coinbase.chainstorage.BlockchainEvent
+ 42, // 25: coinbase.chainstorage.GetBlockByTransactionResponse.blocks:type_name -> coinbase.chainstorage.BlockIdentifier
+ 46, // 26: coinbase.chainstorage.GetNativeTransactionResponse.transactions:type_name -> coinbase.chainstorage.NativeTransaction
+ 47, // 27: coinbase.chainstorage.GetVerifiedAccountStateRequest.req:type_name -> coinbase.chainstorage.InternalGetVerifiedAccountStateRequest
+ 48, // 28: coinbase.chainstorage.GetVerifiedAccountStateResponse.response:type_name -> coinbase.chainstorage.ValidateAccountStateResponse
+ 6, // 29: coinbase.chainstorage.ChainStorage.GetLatestBlock:input_type -> coinbase.chainstorage.GetLatestBlockRequest
+ 8, // 30: coinbase.chainstorage.ChainStorage.GetBlockFile:input_type -> coinbase.chainstorage.GetBlockFileRequest
+ 10, // 31: coinbase.chainstorage.ChainStorage.GetBlockFilesByRange:input_type -> coinbase.chainstorage.GetBlockFilesByRangeRequest
+ 12, // 32: coinbase.chainstorage.ChainStorage.GetRawBlock:input_type -> coinbase.chainstorage.GetRawBlockRequest
+ 14, // 33: coinbase.chainstorage.ChainStorage.GetRawBlocksByRange:input_type -> coinbase.chainstorage.GetRawBlocksByRangeRequest
+ 16, // 34: coinbase.chainstorage.ChainStorage.GetNativeBlock:input_type -> coinbase.chainstorage.GetNativeBlockRequest
+ 18, // 35: coinbase.chainstorage.ChainStorage.GetNativeBlocksByRange:input_type -> coinbase.chainstorage.GetNativeBlocksByRangeRequest
+ 20, // 36: coinbase.chainstorage.ChainStorage.GetRosettaBlock:input_type -> coinbase.chainstorage.GetRosettaBlockRequest
+ 22, // 37: coinbase.chainstorage.ChainStorage.GetRosettaBlocksByRange:input_type -> coinbase.chainstorage.GetRosettaBlocksByRangeRequest
+ 24, // 38: coinbase.chainstorage.ChainStorage.StreamChainEvents:input_type -> coinbase.chainstorage.ChainEventsRequest
+ 26, // 39: coinbase.chainstorage.ChainStorage.GetChainEvents:input_type -> coinbase.chainstorage.GetChainEventsRequest
+ 28, // 40: coinbase.chainstorage.ChainStorage.GetChainMetadata:input_type -> coinbase.chainstorage.GetChainMetadataRequest
+ 30, // 41: coinbase.chainstorage.ChainStorage.GetVersionedChainEvent:input_type -> coinbase.chainstorage.GetVersionedChainEventRequest
+ 32, // 42: coinbase.chainstorage.ChainStorage.GetBlockByTransaction:input_type -> coinbase.chainstorage.GetBlockByTransactionRequest
+ 34, // 43: coinbase.chainstorage.ChainStorage.GetNativeTransaction:input_type -> coinbase.chainstorage.GetNativeTransactionRequest
+ 36, // 44: coinbase.chainstorage.ChainStorage.GetVerifiedAccountState:input_type -> coinbase.chainstorage.GetVerifiedAccountStateRequest
+ 38, // 45: coinbase.chainstorage.ChainStorage.GetBlockByTimestamp:input_type -> coinbase.chainstorage.GetBlockByTimestampRequest
+ 7, // 46: coinbase.chainstorage.ChainStorage.GetLatestBlock:output_type -> coinbase.chainstorage.GetLatestBlockResponse
+ 9, // 47: coinbase.chainstorage.ChainStorage.GetBlockFile:output_type -> coinbase.chainstorage.GetBlockFileResponse
+ 11, // 48: coinbase.chainstorage.ChainStorage.GetBlockFilesByRange:output_type -> coinbase.chainstorage.GetBlockFilesByRangeResponse
+ 13, // 49: coinbase.chainstorage.ChainStorage.GetRawBlock:output_type -> coinbase.chainstorage.GetRawBlockResponse
+ 15, // 50: coinbase.chainstorage.ChainStorage.GetRawBlocksByRange:output_type -> coinbase.chainstorage.GetRawBlocksByRangeResponse
+ 17, // 51: coinbase.chainstorage.ChainStorage.GetNativeBlock:output_type -> coinbase.chainstorage.GetNativeBlockResponse
+ 19, // 52: coinbase.chainstorage.ChainStorage.GetNativeBlocksByRange:output_type -> coinbase.chainstorage.GetNativeBlocksByRangeResponse
+ 21, // 53: coinbase.chainstorage.ChainStorage.GetRosettaBlock:output_type -> coinbase.chainstorage.GetRosettaBlockResponse
+ 23, // 54: coinbase.chainstorage.ChainStorage.GetRosettaBlocksByRange:output_type -> coinbase.chainstorage.GetRosettaBlocksByRangeResponse
+ 25, // 55: coinbase.chainstorage.ChainStorage.StreamChainEvents:output_type -> coinbase.chainstorage.ChainEventsResponse
+ 27, // 56: coinbase.chainstorage.ChainStorage.GetChainEvents:output_type -> coinbase.chainstorage.GetChainEventsResponse
+ 29, // 57: coinbase.chainstorage.ChainStorage.GetChainMetadata:output_type -> coinbase.chainstorage.GetChainMetadataResponse
+ 31, // 58: coinbase.chainstorage.ChainStorage.GetVersionedChainEvent:output_type -> coinbase.chainstorage.GetVersionedChainEventResponse
+ 33, // 59: coinbase.chainstorage.ChainStorage.GetBlockByTransaction:output_type -> coinbase.chainstorage.GetBlockByTransactionResponse
+ 35, // 60: coinbase.chainstorage.ChainStorage.GetNativeTransaction:output_type -> coinbase.chainstorage.GetNativeTransactionResponse
+ 37, // 61: coinbase.chainstorage.ChainStorage.GetVerifiedAccountState:output_type -> coinbase.chainstorage.GetVerifiedAccountStateResponse
+ 39, // 62: coinbase.chainstorage.ChainStorage.GetBlockByTimestamp:output_type -> coinbase.chainstorage.GetBlockByTimestampResponse
+ 46, // [46:63] is the sub-list for method output_type
+ 29, // [29:46] is the sub-list for method input_type
+ 29, // [29:29] is the sub-list for extension type_name
+ 29, // [29:29] is the sub-list for extension extendee
+ 0, // [0:29] is the sub-list for field type_name
}
func init() { file_coinbase_chainstorage_api_proto_init() }
@@ -3138,14 +3522,38 @@ func file_coinbase_chainstorage_api_proto_init() {
return nil
}
}
+ file_coinbase_chainstorage_api_proto_msgTypes[34].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetBlockByTimestampRequest); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_coinbase_chainstorage_api_proto_msgTypes[35].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*GetBlockByTimestampResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_coinbase_chainstorage_api_proto_rawDesc,
- NumEnums: 3,
- NumMessages: 34,
+ NumEnums: 4,
+ NumMessages: 36,
NumExtensions: 0,
NumServices: 1,
},
diff --git a/protos/coinbase/chainstorage/api.proto b/protos/coinbase/chainstorage/api.proto
index 9ead69de..09f50e30 100644
--- a/protos/coinbase/chainstorage/api.proto
+++ b/protos/coinbase/chainstorage/api.proto
@@ -12,6 +12,7 @@ enum Compression {
NONE = 0;
// Compressed using gzip.
GZIP = 1;
+ ZSTD = 2;
}
enum InitialPosition {
@@ -19,6 +20,17 @@ enum InitialPosition {
LATEST = 1;
}
+enum BlockReadSource {
+ option allow_alias = true;
+
+ BLOCK_READ_SOURCE_DEFAULT = 0;
+ // Alias order controls the protojson spelling. Keep the pre-rename name first
+ // until mixed-version REST clients and servers no longer coexist.
+ BLOCK_READ_SOURCE_LEGACY = 1;
+ BLOCK_READ_SOURCE_SINGLE_BLOCK = 1;
+ BLOCK_READ_SOURCE_CONSOLIDATED = 2;
+}
+
message BlockFile {
uint32 tag = 1;
string hash = 2;
@@ -29,6 +41,10 @@ message BlockFile {
bool skipped = 7;
Compression compression = 8;
google.protobuf.Timestamp block_timestamp = 9;
+ uint64 byte_offset = 10;
+ uint64 byte_length = 11;
+ uint64 uncompressed_length = 12;
+ BlockObjectFormat object_format = 13;
}
message BlockchainEvent {
@@ -75,6 +91,7 @@ message GetBlockFileRequest {
uint32 tag = 1;
uint64 height = 2;
string hash = 3;
+ BlockReadSource read_source = 4;
}
message GetBlockFileResponse {
@@ -85,6 +102,7 @@ message GetBlockFilesByRangeRequest {
uint32 tag = 1;
uint64 start_height = 2;
uint64 end_height = 3;
+ BlockReadSource read_source = 4;
}
message GetBlockFilesByRangeResponse {
@@ -95,6 +113,7 @@ message GetRawBlockRequest {
uint32 tag = 1;
uint64 height = 2;
string hash = 3;
+ BlockReadSource read_source = 4;
}
message GetRawBlockResponse {
@@ -105,6 +124,7 @@ message GetRawBlocksByRangeRequest {
uint32 tag = 1;
uint64 start_height = 2;
uint64 end_height = 3;
+ BlockReadSource read_source = 4;
}
message GetRawBlocksByRangeResponse {
@@ -115,6 +135,7 @@ message GetNativeBlockRequest {
uint32 tag = 1;
uint64 height = 2;
string hash = 3;
+ BlockReadSource read_source = 4;
}
message GetNativeBlockResponse {
@@ -125,6 +146,7 @@ message GetNativeBlocksByRangeRequest {
uint32 tag = 1;
uint64 start_height = 2;
uint64 end_height = 3;
+ BlockReadSource read_source = 4;
}
message GetNativeBlocksByRangeResponse {
@@ -135,6 +157,7 @@ message GetRosettaBlockRequest {
uint32 tag = 1;
uint64 height = 2;
string hash = 3;
+ BlockReadSource read_source = 4;
}
message GetRosettaBlockResponse {
@@ -145,6 +168,7 @@ message GetRosettaBlocksByRangeRequest {
uint32 tag = 1;
uint64 start_height = 2;
uint64 end_height = 3;
+ BlockReadSource read_source = 4;
}
message GetRosettaBlocksByRangeResponse {
@@ -249,6 +273,19 @@ message GetVerifiedAccountStateResponse {
ValidateAccountStateResponse response = 1;
}
+message GetBlockByTimestampRequest {
+ uint32 tag = 1;
+ uint64 timestamp = 2; // Unix timestamp in seconds
+}
+
+message GetBlockByTimestampResponse {
+ uint32 tag = 1;
+ string hash = 2;
+ string parent_hash = 3;
+ uint64 height = 4;
+ uint64 timestamp = 5; // Unix timestamp in seconds
+}
+
service ChainStorage {
rpc GetLatestBlock (GetLatestBlockRequest) returns (GetLatestBlockResponse);
rpc GetBlockFile(GetBlockFileRequest) returns (GetBlockFileResponse);
@@ -266,4 +303,5 @@ service ChainStorage {
rpc GetBlockByTransaction(GetBlockByTransactionRequest) returns (GetBlockByTransactionResponse);
rpc GetNativeTransaction (GetNativeTransactionRequest) returns (GetNativeTransactionResponse);
rpc GetVerifiedAccountState (GetVerifiedAccountStateRequest) returns (GetVerifiedAccountStateResponse);
+ rpc GetBlockByTimestamp (GetBlockByTimestampRequest) returns(GetBlockByTimestampResponse);
}
diff --git a/protos/coinbase/chainstorage/api_grpc.pb.go b/protos/coinbase/chainstorage/api_grpc.pb.go
index d386c986..d768df19 100644
--- a/protos/coinbase/chainstorage/api_grpc.pb.go
+++ b/protos/coinbase/chainstorage/api_grpc.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.3.0
-// - protoc v4.25.2
+// - protoc v5.29.4
// source: coinbase/chainstorage/api.proto
package chainstorage
@@ -35,6 +35,7 @@ const (
ChainStorage_GetBlockByTransaction_FullMethodName = "/coinbase.chainstorage.ChainStorage/GetBlockByTransaction"
ChainStorage_GetNativeTransaction_FullMethodName = "/coinbase.chainstorage.ChainStorage/GetNativeTransaction"
ChainStorage_GetVerifiedAccountState_FullMethodName = "/coinbase.chainstorage.ChainStorage/GetVerifiedAccountState"
+ ChainStorage_GetBlockByTimestamp_FullMethodName = "/coinbase.chainstorage.ChainStorage/GetBlockByTimestamp"
)
// ChainStorageClient is the client API for ChainStorage service.
@@ -57,6 +58,7 @@ type ChainStorageClient interface {
GetBlockByTransaction(ctx context.Context, in *GetBlockByTransactionRequest, opts ...grpc.CallOption) (*GetBlockByTransactionResponse, error)
GetNativeTransaction(ctx context.Context, in *GetNativeTransactionRequest, opts ...grpc.CallOption) (*GetNativeTransactionResponse, error)
GetVerifiedAccountState(ctx context.Context, in *GetVerifiedAccountStateRequest, opts ...grpc.CallOption) (*GetVerifiedAccountStateResponse, error)
+ GetBlockByTimestamp(ctx context.Context, in *GetBlockByTimestampRequest, opts ...grpc.CallOption) (*GetBlockByTimestampResponse, error)
}
type chainStorageClient struct {
@@ -234,6 +236,15 @@ func (c *chainStorageClient) GetVerifiedAccountState(ctx context.Context, in *Ge
return out, nil
}
+func (c *chainStorageClient) GetBlockByTimestamp(ctx context.Context, in *GetBlockByTimestampRequest, opts ...grpc.CallOption) (*GetBlockByTimestampResponse, error) {
+ out := new(GetBlockByTimestampResponse)
+ err := c.cc.Invoke(ctx, ChainStorage_GetBlockByTimestamp_FullMethodName, in, out, opts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
// ChainStorageServer is the server API for ChainStorage service.
// All implementations should embed UnimplementedChainStorageServer
// for forward compatibility
@@ -254,6 +265,7 @@ type ChainStorageServer interface {
GetBlockByTransaction(context.Context, *GetBlockByTransactionRequest) (*GetBlockByTransactionResponse, error)
GetNativeTransaction(context.Context, *GetNativeTransactionRequest) (*GetNativeTransactionResponse, error)
GetVerifiedAccountState(context.Context, *GetVerifiedAccountStateRequest) (*GetVerifiedAccountStateResponse, error)
+ GetBlockByTimestamp(context.Context, *GetBlockByTimestampRequest) (*GetBlockByTimestampResponse, error)
}
// UnimplementedChainStorageServer should be embedded to have forward compatible implementations.
@@ -308,6 +320,9 @@ func (UnimplementedChainStorageServer) GetNativeTransaction(context.Context, *Ge
func (UnimplementedChainStorageServer) GetVerifiedAccountState(context.Context, *GetVerifiedAccountStateRequest) (*GetVerifiedAccountStateResponse, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetVerifiedAccountState not implemented")
}
+func (UnimplementedChainStorageServer) GetBlockByTimestamp(context.Context, *GetBlockByTimestampRequest) (*GetBlockByTimestampResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetBlockByTimestamp not implemented")
+}
// UnsafeChainStorageServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to ChainStorageServer will
@@ -611,6 +626,24 @@ func _ChainStorage_GetVerifiedAccountState_Handler(srv interface{}, ctx context.
return interceptor(ctx, in, info, handler)
}
+func _ChainStorage_GetBlockByTimestamp_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetBlockByTimestampRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(ChainStorageServer).GetBlockByTimestamp(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: ChainStorage_GetBlockByTimestamp_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(ChainStorageServer).GetBlockByTimestamp(ctx, req.(*GetBlockByTimestampRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
// ChainStorage_ServiceDesc is the grpc.ServiceDesc for ChainStorage service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
@@ -678,6 +711,10 @@ var ChainStorage_ServiceDesc = grpc.ServiceDesc{
MethodName: "GetVerifiedAccountState",
Handler: _ChainStorage_GetVerifiedAccountState_Handler,
},
+ {
+ MethodName: "GetBlockByTimestamp",
+ Handler: _ChainStorage_GetBlockByTimestamp_Handler,
+ },
},
Streams: []grpc.StreamDesc{
{
diff --git a/protos/coinbase/chainstorage/block_stream.go b/protos/coinbase/chainstorage/block_stream.go
new file mode 100644
index 00000000..ee6652b0
--- /dev/null
+++ b/protos/coinbase/chainstorage/block_stream.go
@@ -0,0 +1,509 @@
+package chainstorage
+
+// Generic chain-agnostic wire walker for a proto-encoded api.Block.
+//
+// Motivation: the Download path today does `ioutil.ReadAll(dec)` +
+// `proto.Unmarshal(...)`, which holds the full decompressed block AND
+// the fully-populated api.Block in memory simultaneously. Peak RAM is
+// ~2× block size.
+//
+// WalkBlockEnvelope uses a streaming bufio.Reader and protoreflect to
+// walk the wire format recursively, allocating destination fields
+// directly (no intermediate []byte of the whole block). Peak RAM
+// drops to ~1× block size.
+//
+// This variant is bitcoin-agnostic and populates the returned *Block
+// fully (including BitcoinBlobdata.Header as a []byte, not as an
+// offset). For the bitcoin streaming path where Header is exposed as
+// an io.Reader instead of materialized, use WalkBitcoinEnvelope.
+
+import (
+ "bufio"
+ "io"
+ "math"
+
+ "golang.org/x/xerrors"
+ "google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// WalkBlockEnvelope reads a proto-encoded Block from r and populates
+// the full *Block without materializing a complete []byte copy of the
+// decompressed proto bytes.
+//
+// Compared to ioutil.ReadAll + proto.Unmarshal, peak RAM during this
+// call drops by roughly one block size. The effect is chain-agnostic:
+// ethereum / solana / aptos / rosetta / ethereum_beacon all benefit
+// proportionally to their blobdata size.
+//
+// Unknown fields on the wire return an error rather than being
+// silently skipped. The proto descriptor is the source of truth: new
+// fields are accepted automatically as long as their kind is a
+// standard proto scalar, enum, message, repeated, or map.
+func WalkBlockEnvelope(r io.Reader) (*Block, error) {
+ block := &Block{}
+ w := &walker{br: bufio.NewReaderSize(r, 64*1024)}
+ if err := w.walkMessage(block.ProtoReflect(), -1); err != nil {
+ return nil, xerrors.Errorf("walk Block: %w", err)
+ }
+ return block, nil
+}
+
+type walker struct {
+ br *bufio.Reader
+ pos int64
+
+ // scratch is a reusable buffer for discardFast. Sized so that
+ // skipping large fields doesn't trigger io.Copy's default 32 KB
+ // per-call allocation or a fresh LimitedReader wrapper. The
+ // bufio.Reader's internal buffer handles the actual read
+ // amplification to the underlying stream.
+ scratch [64 * 1024]byte
+}
+
+// discardFast reads and discards n bytes from the bufio reader without
+// the io.CopyN allocation (io.CopyN wraps with io.LimitReader and
+// allocates a fresh buffer per call; over thousands of calls this
+// dominates allocator pressure). Uses the walker's pre-allocated
+// scratch buffer instead.
+func (w *walker) discardFast(n int64) error {
+ for n > 0 {
+ chunk := int64(len(w.scratch))
+ if chunk > n {
+ chunk = n
+ }
+ got, err := io.ReadFull(w.br, w.scratch[:chunk])
+ w.pos += int64(got)
+ n -= int64(got)
+ if err != nil {
+ return err
+ }
+ }
+ return nil
+}
+
+// walkMessage reads wire-format fields into dst until endPos (absolute
+// byte offset in the underlying stream). endPos == -1 means read until
+// EOF, used for the top-level Block.
+func (w *walker) walkMessage(dst protoreflect.Message, endPos int64) error {
+ desc := dst.Descriptor()
+
+ for {
+ if endPos >= 0 && w.pos >= endPos {
+ break
+ }
+ if endPos < 0 {
+ if _, err := w.br.Peek(1); err != nil {
+ if err == io.EOF {
+ break
+ }
+ return xerrors.Errorf("peek: %w", err)
+ }
+ }
+
+ tag, err := w.readVarint()
+ if err != nil {
+ return xerrors.Errorf("read tag on %s: %w", desc.FullName(), err)
+ }
+ fieldNum := protoreflect.FieldNumber(tag >> 3)
+ wireType := int(tag & 0x7)
+
+ fd := desc.Fields().ByNumber(fieldNum)
+ if fd == nil {
+ return xerrors.Errorf("unknown field %d (wt=%d) on %s: proto changed; update walker or the field's proto definition", fieldNum, wireType, desc.FullName())
+ }
+
+ if err := w.decodeField(dst, fd, wireType); err != nil {
+ return xerrors.Errorf("decode %s.%s: %w", desc.FullName(), fd.Name(), err)
+ }
+ }
+
+ if endPos >= 0 && w.pos != endPos {
+ return xerrors.Errorf("sub-message %s consumed %d bytes, expected %d", desc.FullName(), w.pos, endPos)
+ }
+ return nil
+}
+
+func (w *walker) decodeField(dst protoreflect.Message, fd protoreflect.FieldDescriptor, wireType int) error {
+ switch {
+ case fd.IsMap():
+ return w.decodeMapEntry(dst, fd)
+ case fd.IsList():
+ return w.decodeListElement(dst, fd, wireType)
+ default:
+ return w.decodeSingular(dst, fd, wireType)
+ }
+}
+
+func (w *walker) decodeSingular(dst protoreflect.Message, fd protoreflect.FieldDescriptor, wireType int) error {
+ switch fd.Kind() {
+ case protoreflect.MessageKind, protoreflect.GroupKind:
+ length, err := w.readVarint()
+ if err != nil {
+ return err
+ }
+ end := w.pos + int64(length)
+ inner := dst.Mutable(fd).Message()
+ return w.walkMessage(inner, end)
+ case protoreflect.BytesKind:
+ b, err := w.readBytes()
+ if err != nil {
+ return err
+ }
+ dst.Set(fd, protoreflect.ValueOfBytes(b))
+ return nil
+ case protoreflect.StringKind:
+ b, err := w.readBytes()
+ if err != nil {
+ return err
+ }
+ dst.Set(fd, protoreflect.ValueOfString(string(b)))
+ return nil
+ default:
+ v, err := w.readScalarValue(fd.Kind(), wireType)
+ if err != nil {
+ return err
+ }
+ dst.Set(fd, v)
+ return nil
+ }
+}
+
+func (w *walker) decodeListElement(dst protoreflect.Message, fd protoreflect.FieldDescriptor, wireType int) error {
+ list := dst.Mutable(fd).List()
+ switch fd.Kind() {
+ case protoreflect.MessageKind, protoreflect.GroupKind:
+ length, err := w.readVarint()
+ if err != nil {
+ return err
+ }
+ end := w.pos + int64(length)
+ newElem := list.NewElement()
+ if err := w.walkMessage(newElem.Message(), end); err != nil {
+ return err
+ }
+ list.Append(newElem)
+ return nil
+ case protoreflect.BytesKind:
+ b, err := w.readBytes()
+ if err != nil {
+ return err
+ }
+ list.Append(protoreflect.ValueOfBytes(b))
+ return nil
+ case protoreflect.StringKind:
+ b, err := w.readBytes()
+ if err != nil {
+ return err
+ }
+ list.Append(protoreflect.ValueOfString(string(b)))
+ return nil
+ default:
+ // Packed repeated scalars come as a single length-delimited
+ // run; non-packed is one tag per value. Both are supported.
+ if wireType == wireBytes {
+ length, err := w.readVarint()
+ if err != nil {
+ return err
+ }
+ end := w.pos + int64(length)
+ for w.pos < end {
+ v, err := w.readScalarValue(fd.Kind(), wireTypeFor(fd.Kind()))
+ if err != nil {
+ return err
+ }
+ list.Append(v)
+ }
+ return nil
+ }
+ v, err := w.readScalarValue(fd.Kind(), wireType)
+ if err != nil {
+ return err
+ }
+ list.Append(v)
+ return nil
+ }
+}
+
+// decodeMapEntry reads one length-delimited map entry message and sets
+// it on the map. Map entries follow the synthetic "MapEntry" message
+// pattern: field 1 = key, field 2 = value.
+func (w *walker) decodeMapEntry(dst protoreflect.Message, fd protoreflect.FieldDescriptor) error {
+ length, err := w.readVarint()
+ if err != nil {
+ return err
+ }
+ end := w.pos + int64(length)
+ mp := dst.Mutable(fd).Map()
+
+ keyDesc := fd.MapKey()
+ valDesc := fd.MapValue()
+
+ var keyVal protoreflect.Value
+ valVal := mp.NewValue() // empty value of the right shape (for message types)
+
+ for w.pos < end {
+ tag, err := w.readVarint()
+ if err != nil {
+ return err
+ }
+ inner := protoreflect.FieldNumber(tag >> 3)
+ innerWT := int(tag & 0x7)
+ switch inner {
+ case 1: // key
+ switch keyDesc.Kind() {
+ case protoreflect.StringKind:
+ b, err := w.readBytes()
+ if err != nil {
+ return err
+ }
+ keyVal = protoreflect.ValueOfString(string(b))
+ case protoreflect.BytesKind:
+ b, err := w.readBytes()
+ if err != nil {
+ return err
+ }
+ keyVal = protoreflect.ValueOfBytes(b)
+ default:
+ v, err := w.readScalarValue(keyDesc.Kind(), innerWT)
+ if err != nil {
+ return err
+ }
+ keyVal = v
+ }
+ case 2: // value
+ switch valDesc.Kind() {
+ case protoreflect.MessageKind, protoreflect.GroupKind:
+ mlen, err := w.readVarint()
+ if err != nil {
+ return err
+ }
+ mend := w.pos + int64(mlen)
+ if err := w.walkMessage(valVal.Message(), mend); err != nil {
+ return err
+ }
+ case protoreflect.BytesKind:
+ b, err := w.readBytes()
+ if err != nil {
+ return err
+ }
+ valVal = protoreflect.ValueOfBytes(b)
+ case protoreflect.StringKind:
+ b, err := w.readBytes()
+ if err != nil {
+ return err
+ }
+ valVal = protoreflect.ValueOfString(string(b))
+ default:
+ v, err := w.readScalarValue(valDesc.Kind(), innerWT)
+ if err != nil {
+ return err
+ }
+ valVal = v
+ }
+ default:
+ if err := w.skipValue(innerWT); err != nil {
+ return err
+ }
+ }
+ }
+
+ if !keyVal.IsValid() {
+ return xerrors.Errorf("map entry on %s has no key", dst.Descriptor().FullName())
+ }
+ mp.Set(keyVal.MapKey(), valVal)
+ return nil
+}
+
+// readBytes reads a length-delimited byte slice. Allocates a single
+// []byte of exactly the right size and reads into it — no source-side
+// intermediate buffer is retained.
+func (w *walker) readBytes() ([]byte, error) {
+ length, err := w.readVarint()
+ if err != nil {
+ return nil, err
+ }
+ if length > (1 << 30) {
+ return nil, xerrors.Errorf("length-delimited payload too large: %d", length)
+ }
+ b := make([]byte, length)
+ n, err := io.ReadFull(w.br, b)
+ w.pos += int64(n)
+ if err != nil {
+ return nil, xerrors.Errorf("readBytes: %w", err)
+ }
+ return b, nil
+}
+
+func (w *walker) readScalarValue(kind protoreflect.Kind, wireType int) (protoreflect.Value, error) {
+ switch kind {
+ case protoreflect.BoolKind:
+ v, err := w.readVarint()
+ if err != nil {
+ return protoreflect.Value{}, err
+ }
+ return protoreflect.ValueOfBool(v != 0), nil
+ case protoreflect.EnumKind:
+ v, err := w.readVarint()
+ if err != nil {
+ return protoreflect.Value{}, err
+ }
+ return protoreflect.ValueOfEnum(protoreflect.EnumNumber(int32(v))), nil
+ case protoreflect.Int32Kind:
+ v, err := w.readVarint()
+ if err != nil {
+ return protoreflect.Value{}, err
+ }
+ return protoreflect.ValueOfInt32(int32(v)), nil
+ case protoreflect.Sint32Kind:
+ v, err := w.readVarint()
+ if err != nil {
+ return protoreflect.Value{}, err
+ }
+ return protoreflect.ValueOfInt32(int32(decodeZigZag32(uint32(v)))), nil
+ case protoreflect.Uint32Kind:
+ v, err := w.readVarint()
+ if err != nil {
+ return protoreflect.Value{}, err
+ }
+ return protoreflect.ValueOfUint32(uint32(v)), nil
+ case protoreflect.Int64Kind:
+ v, err := w.readVarint()
+ if err != nil {
+ return protoreflect.Value{}, err
+ }
+ return protoreflect.ValueOfInt64(int64(v)), nil
+ case protoreflect.Sint64Kind:
+ v, err := w.readVarint()
+ if err != nil {
+ return protoreflect.Value{}, err
+ }
+ return protoreflect.ValueOfInt64(decodeZigZag64(v)), nil
+ case protoreflect.Uint64Kind:
+ v, err := w.readVarint()
+ if err != nil {
+ return protoreflect.Value{}, err
+ }
+ return protoreflect.ValueOfUint64(v), nil
+ case protoreflect.Sfixed32Kind, protoreflect.Fixed32Kind, protoreflect.FloatKind:
+ v, err := w.readFixed32()
+ if err != nil {
+ return protoreflect.Value{}, err
+ }
+ switch kind {
+ case protoreflect.Sfixed32Kind:
+ return protoreflect.ValueOfInt32(int32(v)), nil
+ case protoreflect.Fixed32Kind:
+ return protoreflect.ValueOfUint32(v), nil
+ default: // FloatKind
+ return protoreflect.ValueOfFloat32(float32FromBits(v)), nil
+ }
+ case protoreflect.Sfixed64Kind, protoreflect.Fixed64Kind, protoreflect.DoubleKind:
+ v, err := w.readFixed64()
+ if err != nil {
+ return protoreflect.Value{}, err
+ }
+ switch kind {
+ case protoreflect.Sfixed64Kind:
+ return protoreflect.ValueOfInt64(int64(v)), nil
+ case protoreflect.Fixed64Kind:
+ return protoreflect.ValueOfUint64(v), nil
+ default: // DoubleKind
+ return protoreflect.ValueOfFloat64(float64FromBits(v)), nil
+ }
+ }
+ _ = wireType
+ return protoreflect.Value{}, xerrors.Errorf("unsupported scalar kind %s", kind)
+}
+
+func (w *walker) readFixed32() (uint32, error) {
+ var buf [4]byte
+ if _, err := io.ReadFull(w.br, buf[:]); err != nil {
+ return 0, err
+ }
+ w.pos += 4
+ return uint32(buf[0]) | uint32(buf[1])<<8 | uint32(buf[2])<<16 | uint32(buf[3])<<24, nil
+}
+
+func (w *walker) readFixed64() (uint64, error) {
+ var buf [8]byte
+ if _, err := io.ReadFull(w.br, buf[:]); err != nil {
+ return 0, err
+ }
+ w.pos += 8
+ return uint64(buf[0]) | uint64(buf[1])<<8 | uint64(buf[2])<<16 | uint64(buf[3])<<24 |
+ uint64(buf[4])<<32 | uint64(buf[5])<<40 | uint64(buf[6])<<48 | uint64(buf[7])<<56, nil
+}
+
+func (w *walker) readVarint() (uint64, error) {
+ var v uint64
+ var shift uint
+ for i := 0; i < 10; i++ {
+ b, err := w.br.ReadByte()
+ if err != nil {
+ if err == io.EOF && i > 0 {
+ return 0, io.ErrUnexpectedEOF
+ }
+ return 0, err
+ }
+ w.pos++
+ v |= uint64(b&0x7f) << shift
+ if b < 0x80 {
+ return v, nil
+ }
+ shift += 7
+ }
+ return 0, xerrors.New("varint too long")
+}
+
+func (w *walker) skipValue(wireType int) error {
+ switch wireType {
+ case wireVarint:
+ _, err := w.readVarint()
+ return err
+ case wireFixed64:
+ n, err := io.CopyN(io.Discard, w.br, 8)
+ w.pos += n
+ return err
+ case wireBytes:
+ length, err := w.readVarint()
+ if err != nil {
+ return err
+ }
+ n, err := io.CopyN(io.Discard, w.br, int64(length))
+ w.pos += n
+ return err
+ case wireFixed32:
+ n, err := io.CopyN(io.Discard, w.br, 4)
+ w.pos += n
+ return err
+ }
+ return xerrors.Errorf("cannot skip wire type %d", wireType)
+}
+
+// wireTypeFor returns the wire type used for the given kind in a
+// non-packed repeated encoding, used when decoding packed repeated
+// values inside a length-delimited run.
+func wireTypeFor(k protoreflect.Kind) int {
+ switch k {
+ case protoreflect.BoolKind, protoreflect.EnumKind,
+ protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Uint32Kind,
+ protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Uint64Kind:
+ return wireVarint
+ case protoreflect.Sfixed32Kind, protoreflect.Fixed32Kind, protoreflect.FloatKind:
+ return wireFixed32
+ case protoreflect.Sfixed64Kind, protoreflect.Fixed64Kind, protoreflect.DoubleKind:
+ return wireFixed64
+ }
+ return wireBytes
+}
+
+func decodeZigZag32(v uint32) int32 {
+ return int32((v >> 1) ^ -(v & 1))
+}
+
+func decodeZigZag64(v uint64) int64 {
+ return int64((v >> 1) ^ -(v & 1))
+}
+
+func float32FromBits(b uint32) float32 { return math.Float32frombits(b) }
+func float64FromBits(b uint64) float64 { return math.Float64frombits(b) }
diff --git a/protos/coinbase/chainstorage/block_stream_test.go b/protos/coinbase/chainstorage/block_stream_test.go
new file mode 100644
index 00000000..3ad3b730
--- /dev/null
+++ b/protos/coinbase/chainstorage/block_stream_test.go
@@ -0,0 +1,159 @@
+package chainstorage_test
+
+// Parity + drift tests for the generic WalkBlockEnvelope.
+//
+// For each chain, construct a Block with a representative blobdata
+// populated, marshal it, walk it, and assert the walked Block is
+// byte-for-byte equivalent (via proto.Equal) to the original.
+//
+// These tests serve two purposes:
+// 1. Correctness of the walker across every blobdata variant.
+// 2. Drift defense. Adding a new field to any Blobdata message
+// doesn't require walker code changes (the walker is reflection-
+// based), but the parity test exercises the full round-trip.
+// Kind-level drift (e.g. adding a new proto Kind the walker
+// doesn't support) fails here with a clear error.
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+func TestWalkBlockEnvelope_Bitcoin(t *testing.T) {
+ original := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_BITCOIN,
+ Network: common.Network_NETWORK_BITCOIN_MAINNET,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 696402, Hash: "abc"},
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: []byte(`{"hash":"abc","tx":[]}`),
+ InputTransactions: []*api.RepeatedBytes{
+ {Data: [][]byte{[]byte("prev1"), []byte("prev2")}},
+ },
+ RawInputTransactions: map[string][]byte{
+ "tx1": []byte("rawtx1"),
+ "tx2": []byte("rawtx2"),
+ },
+ },
+ },
+ }
+ assertParity(t, original)
+}
+
+func TestWalkBlockEnvelope_Ethereum(t *testing.T) {
+ original := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ETHEREUM,
+ Network: common.Network_NETWORK_ETHEREUM_MAINNET,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 19_000_000},
+ Blobdata: &api.Block_Ethereum{
+ Ethereum: &api.EthereumBlobdata{
+ Header: []byte(`{"hash":"0x123","number":"0x1"}`),
+ TransactionReceipts: [][]byte{[]byte(`{"r":1}`), []byte(`{"r":2}`)},
+ TransactionTraces: [][]byte{[]byte(`{"t":1}`)},
+ Uncles: [][]byte{[]byte(`{"u":1}`)},
+ },
+ },
+ }
+ assertParity(t, original)
+}
+
+func TestWalkBlockEnvelope_EthereumBeacon(t *testing.T) {
+ original := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_ETHEREUM,
+ SideChain: api.SideChain_SIDECHAIN_ETHEREUM_MAINNET_BEACON,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 1},
+ Blobdata: &api.Block_EthereumBeacon{
+ EthereumBeacon: &api.EthereumBeaconBlobdata{
+ Header: []byte(`{"slot":"1"}`),
+ Block: []byte(`{"body":{}}`),
+ Blobs: []byte(`[]`),
+ },
+ },
+ }
+ assertParity(t, original)
+}
+
+func TestWalkBlockEnvelope_Solana(t *testing.T) {
+ original := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_SOLANA,
+ Network: common.Network_NETWORK_SOLANA_MAINNET,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 123456},
+ Blobdata: &api.Block_Solana{
+ Solana: &api.SolanaBlobdata{
+ Header: []byte(`{"blockHeight":123456}`),
+ },
+ },
+ }
+ assertParity(t, original)
+}
+
+func TestWalkBlockEnvelope_Aptos(t *testing.T) {
+ original := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_APTOS,
+ Network: common.Network_NETWORK_APTOS_MAINNET,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 100},
+ Blobdata: &api.Block_Aptos{
+ Aptos: &api.AptosBlobdata{
+ Block: []byte(`{"block_height":"100"}`),
+ },
+ },
+ }
+ assertParity(t, original)
+}
+
+func TestWalkBlockEnvelope_Rosetta(t *testing.T) {
+ original := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_TRON,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 42},
+ Blobdata: &api.Block_Rosetta{
+ Rosetta: &api.RosettaBlobdata{
+ Header: []byte(`{"block_identifier":{}}`),
+ OtherTransactions: [][]byte{[]byte(`{"ot":1}`)},
+ RawBlock: []byte(`{"raw":"data"}`),
+ },
+ },
+ }
+ assertParity(t, original)
+}
+
+// TestWalkBlockEnvelope_Empty covers a minimal Block (no blobdata).
+func TestWalkBlockEnvelope_Empty(t *testing.T) {
+ original := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_BITCOIN,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 0},
+ }
+ assertParity(t, original)
+}
+
+// TestWalkBlockEnvelope_UnknownField confirms strict behavior: a field
+// number not present in the api.Block descriptor produces an error.
+func TestWalkBlockEnvelope_UnknownField(t *testing.T) {
+ require := require.New(t)
+ // field 77, wire type 0 varint, value 1
+ raw := []byte{0xE8, 0x04, 0x01}
+ _, err := api.WalkBlockEnvelope(bytes.NewReader(raw))
+ require.Error(err)
+ require.Contains(err.Error(), "unknown field 77")
+}
+
+// assertParity round-trips `original` through proto.Marshal +
+// WalkBlockEnvelope and asserts the walked result equals the input.
+func assertParity(t *testing.T, original *api.Block) {
+ t.Helper()
+ raw, err := proto.Marshal(original)
+ require.NoError(t, err)
+
+ walked, err := api.WalkBlockEnvelope(bytes.NewReader(raw))
+ require.NoError(t, err)
+
+ // proto.Equal handles oneof + map/repeated equivalence correctly.
+ require.Truef(t, proto.Equal(original, walked),
+ "walker output does not equal original\noriginal: %+v\nwalked: %+v",
+ original, walked)
+}
diff --git a/protos/coinbase/chainstorage/blockchain.pb.go b/protos/coinbase/chainstorage/blockchain.pb.go
index 8c52307e..3025f53b 100644
--- a/protos/coinbase/chainstorage/blockchain.pb.go
+++ b/protos/coinbase/chainstorage/blockchain.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/chainstorage/blockchain.proto
package chainstorage
@@ -23,6 +23,57 @@ const (
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
+type BlockObjectFormat int32
+
+const (
+ // Alias order controls the protojson spelling. Keep the pre-rename name first
+ // until mixed-version REST clients and servers no longer coexist.
+ BlockObjectFormat_BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK BlockObjectFormat = 0
+ BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK BlockObjectFormat = 0
+ BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH BlockObjectFormat = 1
+)
+
+// Enum value maps for BlockObjectFormat.
+var (
+ BlockObjectFormat_name = map[int32]string{
+ 0: "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK",
+ // Duplicate value: 0: "BLOCK_OBJECT_FORMAT_SINGLE_BLOCK",
+ 1: "BLOCK_OBJECT_FORMAT_CSCB_BATCH",
+ }
+ BlockObjectFormat_value = map[string]int32{
+ "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK": 0,
+ "BLOCK_OBJECT_FORMAT_SINGLE_BLOCK": 0,
+ "BLOCK_OBJECT_FORMAT_CSCB_BATCH": 1,
+ }
+)
+
+func (x BlockObjectFormat) Enum() *BlockObjectFormat {
+ p := new(BlockObjectFormat)
+ *p = x
+ return p
+}
+
+func (x BlockObjectFormat) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (BlockObjectFormat) Descriptor() protoreflect.EnumDescriptor {
+ return file_coinbase_chainstorage_blockchain_proto_enumTypes[0].Descriptor()
+}
+
+func (BlockObjectFormat) Type() protoreflect.EnumType {
+ return &file_coinbase_chainstorage_blockchain_proto_enumTypes[0]
+}
+
+func (x BlockObjectFormat) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use BlockObjectFormat.Descriptor instead.
+func (BlockObjectFormat) EnumDescriptor() ([]byte, []int) {
+ return file_coinbase_chainstorage_blockchain_proto_rawDescGZIP(), []int{0}
+}
+
type SideChain int32
const (
@@ -57,11 +108,11 @@ func (x SideChain) String() string {
}
func (SideChain) Descriptor() protoreflect.EnumDescriptor {
- return file_coinbase_chainstorage_blockchain_proto_enumTypes[0].Descriptor()
+ return file_coinbase_chainstorage_blockchain_proto_enumTypes[1].Descriptor()
}
func (SideChain) Type() protoreflect.EnumType {
- return &file_coinbase_chainstorage_blockchain_proto_enumTypes[0]
+ return &file_coinbase_chainstorage_blockchain_proto_enumTypes[1]
}
func (x SideChain) Number() protoreflect.EnumNumber {
@@ -70,7 +121,7 @@ func (x SideChain) Number() protoreflect.EnumNumber {
// Deprecated: Use SideChain.Descriptor instead.
func (SideChain) EnumDescriptor() ([]byte, []int) {
- return file_coinbase_chainstorage_blockchain_proto_rawDescGZIP(), []int{0}
+ return file_coinbase_chainstorage_blockchain_proto_rawDescGZIP(), []int{1}
}
type Block struct {
@@ -350,6 +401,14 @@ type BlockMetadata struct {
Skipped bool `protobuf:"varint,7,opt,name=skipped,proto3" json:"skipped,omitempty"`
// Block timestamp. Note that this attribute is only available in recent blocks.
Timestamp *timestamppb.Timestamp `protobuf:"bytes,8,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ // Offset of this block's raw protobuf payload inside the decoded object payload.
+ ByteOffset uint64 `protobuf:"varint,9,opt,name=byte_offset,json=byteOffset,proto3" json:"byte_offset,omitempty"`
+ // Length of this block's raw protobuf payload. Zero means single-block whole-object reads.
+ ByteLength uint64 `protobuf:"varint,10,opt,name=byte_length,json=byteLength,proto3" json:"byte_length,omitempty"`
+ // Length of the decoded payload region that contains this block.
+ UncompressedLength uint64 `protobuf:"varint,11,opt,name=uncompressed_length,json=uncompressedLength,proto3" json:"uncompressed_length,omitempty"`
+ // Object format for object_key_main. Zero value is the single-block whole-object format.
+ ObjectFormat BlockObjectFormat `protobuf:"varint,12,opt,name=object_format,json=objectFormat,proto3,enum=coinbase.chainstorage.BlockObjectFormat" json:"object_format,omitempty"`
}
func (x *BlockMetadata) Reset() {
@@ -440,6 +499,34 @@ func (x *BlockMetadata) GetTimestamp() *timestamppb.Timestamp {
return nil
}
+func (x *BlockMetadata) GetByteOffset() uint64 {
+ if x != nil {
+ return x.ByteOffset
+ }
+ return 0
+}
+
+func (x *BlockMetadata) GetByteLength() uint64 {
+ if x != nil {
+ return x.ByteLength
+ }
+ return 0
+}
+
+func (x *BlockMetadata) GetUncompressedLength() uint64 {
+ if x != nil {
+ return x.UncompressedLength
+ }
+ return 0
+}
+
+func (x *BlockMetadata) GetObjectFormat() BlockObjectFormat {
+ if x != nil {
+ return x.ObjectFormat
+ }
+ return BlockObjectFormat_BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK
+}
+
type TransactionMetadata struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1399,7 +1486,7 @@ var file_coinbase_chainstorage_blockchain_proto_rawDesc = []byte{
0x61, 0x6d, 0x70, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
- 0x22, 0x8f, 0x02, 0x0a, 0x0d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
+ 0x22, 0xd1, 0x03, 0x0a, 0x0d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61,
0x74, 0x61, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52,
0x03, 0x74, 0x61, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01,
0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65,
@@ -1416,183 +1503,205 @@ var file_coinbase_chainstorage_blockchain_proto_rawDesc = []byte{
0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f,
0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69,
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
- 0x6d, 0x70, 0x22, 0x39, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
- 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0c, 0x74, 0x72, 0x61,
- 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x09, 0x52,
- 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4a, 0x0a,
- 0x0c, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x3a, 0x0a,
- 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63,
+ 0x6d, 0x70, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x5f, 0x6f, 0x66, 0x66, 0x73, 0x65,
+ 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x4f, 0x66, 0x66,
+ 0x73, 0x65, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x62, 0x79, 0x74, 0x65, 0x5f, 0x6c, 0x65, 0x6e, 0x67,
+ 0x74, 0x68, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x62, 0x79, 0x74, 0x65, 0x4c, 0x65,
+ 0x6e, 0x67, 0x74, 0x68, 0x12, 0x2f, 0x0a, 0x13, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65,
+ 0x73, 0x73, 0x65, 0x64, 0x5f, 0x6c, 0x65, 0x6e, 0x67, 0x74, 0x68, 0x18, 0x0b, 0x20, 0x01, 0x28,
+ 0x04, 0x52, 0x12, 0x75, 0x6e, 0x63, 0x6f, 0x6d, 0x70, 0x72, 0x65, 0x73, 0x73, 0x65, 0x64, 0x4c,
+ 0x65, 0x6e, 0x67, 0x74, 0x68, 0x12, 0x4d, 0x0a, 0x0d, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x5f,
+ 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x63,
+ 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
+ 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74,
+ 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x52, 0x0c, 0x6f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x46, 0x6f,
+ 0x72, 0x6d, 0x61, 0x74, 0x22, 0x39, 0x0a, 0x13, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74,
+ 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x22, 0x0a, 0x0c, 0x74,
+ 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28,
+ 0x09, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22,
+ 0x4a, 0x0a, 0x0c, 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12,
+ 0x3a, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24,
+ 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f,
+ 0x2e, 0x72, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0xad, 0x07, 0x0a, 0x0b,
+ 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x3e, 0x0a, 0x0a, 0x62,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32,
+ 0x1e, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x33, 0x2e, 0x63, 0x6f,
+ 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x52,
+ 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x35, 0x0a, 0x07, 0x6e,
+ 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x63,
+ 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x33, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f,
+ 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f,
+ 0x72, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52,
+ 0x03, 0x74, 0x61, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65,
+ 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70,
+ 0x61, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69,
+ 0x67, 0x68, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68,
+ 0x74, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72,
+ 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
+ 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x29, 0x0a, 0x10, 0x6e,
+ 0x75, 0x6d, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18,
+ 0x08, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x6e, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61,
+ 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74,
+ 0x5f, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x70,
+ 0x61, 0x72, 0x65, 0x6e, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73,
+ 0x6b, 0x69, 0x70, 0x70, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x6b,
+ 0x69, 0x70, 0x70, 0x65, 0x64, 0x12, 0x3f, 0x0a, 0x0a, 0x73, 0x69, 0x64, 0x65, 0x5f, 0x63, 0x68,
+ 0x61, 0x69, 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+ 0x65, 0x2e, 0x53, 0x69, 0x64, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x09, 0x73, 0x69, 0x64,
+ 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x42, 0x0a, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65,
+ 0x75, 0x6d, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62,
+ 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+ 0x2e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x00,
+ 0x52, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x12, 0x3f, 0x0a, 0x07, 0x62, 0x69,
+ 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f,
+ 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72,
+ 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x48, 0x00, 0x52, 0x07, 0x62, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x12, 0x40, 0x0a, 0x07, 0x72,
+ 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63,
0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x72,
0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x6c, 0x6f,
- 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0xad, 0x07, 0x0a, 0x0b, 0x4e, 0x61,
- 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x3e, 0x0a, 0x0a, 0x62, 0x6c, 0x6f,
- 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e,
- 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x33, 0x2e, 0x63, 0x6f, 0x6d, 0x6d,
- 0x6f, 0x6e, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x0a, 0x62,
- 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x35, 0x0a, 0x07, 0x6e, 0x65, 0x74,
- 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x69,
- 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x33, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e,
- 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b,
- 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74,
- 0x61, 0x67, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74,
- 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, 0x72,
- 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68,
- 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12,
- 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
- 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09,
- 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x29, 0x0a, 0x10, 0x6e, 0x75, 0x6d,
- 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x08, 0x20,
- 0x01, 0x28, 0x04, 0x52, 0x0f, 0x6e, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74,
- 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x70, 0x61, 0x72, 0x65, 0x6e, 0x74, 0x5f, 0x68,
- 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x70, 0x61, 0x72,
- 0x65, 0x6e, 0x74, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x73, 0x6b, 0x69,
- 0x70, 0x70, 0x65, 0x64, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x6b, 0x69, 0x70,
- 0x70, 0x65, 0x64, 0x12, 0x3f, 0x0a, 0x0a, 0x73, 0x69, 0x64, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x69,
- 0x6e, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x20, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
- 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
- 0x53, 0x69, 0x64, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x09, 0x73, 0x69, 0x64, 0x65, 0x43,
- 0x68, 0x61, 0x69, 0x6e, 0x12, 0x42, 0x0a, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d,
- 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
- 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45,
- 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x08,
- 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x12, 0x3f, 0x0a, 0x07, 0x62, 0x69, 0x74, 0x63,
- 0x6f, 0x69, 0x6e, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x63, 0x6b, 0x48, 0x00, 0x52, 0x07, 0x72, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x12, 0x3c, 0x0a,
+ 0x06, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e,
+ 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74,
+ 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x53, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x42, 0x6c, 0x6f, 0x63,
+ 0x6b, 0x48, 0x00, 0x52, 0x06, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x12, 0x39, 0x0a, 0x05, 0x61,
+ 0x70, 0x74, 0x6f, 0x73, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x69,
+ 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61,
+ 0x67, 0x65, 0x2e, 0x41, 0x70, 0x74, 0x6f, 0x73, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x00, 0x52,
+ 0x05, 0x61, 0x70, 0x74, 0x6f, 0x73, 0x12, 0x43, 0x0a, 0x09, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61,
+ 0x5f, 0x76, 0x32, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
- 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x00,
- 0x52, 0x07, 0x62, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x12, 0x40, 0x0a, 0x07, 0x72, 0x6f, 0x73,
- 0x65, 0x74, 0x74, 0x61, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x69,
- 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x72, 0x6f, 0x73,
- 0x65, 0x74, 0x74, 0x61, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
- 0x48, 0x00, 0x52, 0x07, 0x72, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x12, 0x3c, 0x0a, 0x06, 0x73,
- 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x18, 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f,
- 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72,
- 0x61, 0x67, 0x65, 0x2e, 0x53, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48,
- 0x00, 0x52, 0x06, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x12, 0x39, 0x0a, 0x05, 0x61, 0x70, 0x74,
- 0x6f, 0x73, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62,
- 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
- 0x2e, 0x41, 0x70, 0x74, 0x6f, 0x73, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x00, 0x52, 0x05, 0x61,
- 0x70, 0x74, 0x6f, 0x73, 0x12, 0x43, 0x0a, 0x09, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x5f, 0x76,
- 0x32, 0x18, 0x69, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
+ 0x65, 0x2e, 0x53, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x56, 0x32, 0x48,
+ 0x00, 0x52, 0x08, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x56, 0x32, 0x12, 0x55, 0x0a, 0x0f, 0x65,
+ 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5f, 0x62, 0x65, 0x61, 0x63, 0x6f, 0x6e, 0x18, 0x6a,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
+ 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x74, 0x68,
+ 0x65, 0x72, 0x65, 0x75, 0x6d, 0x42, 0x65, 0x61, 0x63, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x48, 0x00, 0x52, 0x0e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x42, 0x65, 0x61, 0x63,
+ 0x6f, 0x6e, 0x42, 0x07, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0xbb, 0x05, 0x0a, 0x11,
+ 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
+ 0x6e, 0x12, 0x3e, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
+ 0x2e, 0x63, 0x33, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x63, 0x68, 0x61, 0x69, 0x6e, 0x52, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69,
+ 0x6e, 0x12, 0x35, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x0e, 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x33,
+ 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52,
+ 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18,
+ 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72,
+ 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
+ 0x6e, 0x48, 0x61, 0x73, 0x68, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68,
+ 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63,
+ 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x43, 0x0a, 0x0f, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b,
+ 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62,
+ 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x62, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x48, 0x0a, 0x08,
+ 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a,
+ 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73,
+ 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x54,
+ 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x08, 0x65, 0x74,
+ 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x12, 0x45, 0x0a, 0x07, 0x62, 0x69, 0x74, 0x63, 0x6f, 0x69,
+ 0x6e, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
- 0x53, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x56, 0x32, 0x48, 0x00, 0x52,
- 0x08, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x56, 0x32, 0x12, 0x55, 0x0a, 0x0f, 0x65, 0x74, 0x68,
- 0x65, 0x72, 0x65, 0x75, 0x6d, 0x5f, 0x62, 0x65, 0x61, 0x63, 0x6f, 0x6e, 0x18, 0x6a, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
- 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x74, 0x68, 0x65, 0x72,
- 0x65, 0x75, 0x6d, 0x42, 0x65, 0x61, 0x63, 0x6f, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x00,
- 0x52, 0x0e, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x42, 0x65, 0x61, 0x63, 0x6f, 0x6e,
- 0x42, 0x07, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x22, 0xbb, 0x05, 0x0a, 0x11, 0x4e, 0x61,
- 0x74, 0x69, 0x76, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12,
- 0x3e, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x0e, 0x32, 0x1e, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63,
- 0x33, 0x2e, 0x63, 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68,
- 0x61, 0x69, 0x6e, 0x52, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x12,
- 0x35, 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e,
- 0x32, 0x1b, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x33, 0x2e, 0x63,
- 0x6f, 0x6d, 0x6d, 0x6f, 0x6e, 0x2e, 0x4e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x52, 0x07, 0x6e,
- 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x03, 0x20,
- 0x01, 0x28, 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72, 0x61, 0x6e,
- 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48,
- 0x61, 0x73, 0x68, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x65, 0x69,
- 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
- 0x48, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f,
- 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63,
- 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x43, 0x0a, 0x0f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x74,
- 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a,
- 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
- 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0e, 0x62, 0x6c, 0x6f, 0x63,
- 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x48, 0x0a, 0x08, 0x65, 0x74,
- 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63,
- 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
- 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x54, 0x72, 0x61,
- 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x08, 0x65, 0x74, 0x68, 0x65,
- 0x72, 0x65, 0x75, 0x6d, 0x12, 0x45, 0x0a, 0x07, 0x62, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x18,
- 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
- 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69,
- 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
- 0x48, 0x00, 0x52, 0x07, 0x62, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x12, 0x46, 0x0a, 0x07, 0x72,
- 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63,
- 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f, 0x2e, 0x72,
- 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x54, 0x72, 0x61,
- 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x07, 0x72, 0x6f, 0x73, 0x65,
- 0x74, 0x74, 0x61, 0x12, 0x42, 0x0a, 0x06, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x18, 0x67, 0x20,
- 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63,
- 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x53, 0x6f, 0x6c, 0x61,
- 0x6e, 0x61, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52,
- 0x06, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x12, 0x3f, 0x0a, 0x05, 0x61, 0x70, 0x74, 0x6f, 0x73,
- 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
- 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x41,
- 0x70, 0x74, 0x6f, 0x73, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48,
- 0x00, 0x52, 0x05, 0x61, 0x70, 0x74, 0x6f, 0x73, 0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x72, 0x61, 0x6e,
- 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x75, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x41, 0x63,
- 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x18, 0x64,
- 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
+ 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69,
+ 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x07, 0x62, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x12, 0x46, 0x0a,
+ 0x07, 0x72, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x18, 0x66, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a,
+ 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x72, 0x79, 0x70, 0x74, 0x6f,
+ 0x2e, 0x72, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x2e, 0x74, 0x79, 0x70, 0x65, 0x73, 0x2e, 0x54,
+ 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x00, 0x52, 0x07, 0x72, 0x6f,
+ 0x73, 0x65, 0x74, 0x74, 0x61, 0x12, 0x42, 0x0a, 0x06, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x18,
+ 0x67, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x28, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
+ 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x53, 0x6f,
+ 0x6c, 0x61, 0x6e, 0x61, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48,
+ 0x00, 0x52, 0x06, 0x73, 0x6f, 0x6c, 0x61, 0x6e, 0x61, 0x12, 0x3f, 0x0a, 0x05, 0x61, 0x70, 0x74,
+ 0x6f, 0x73, 0x18, 0x68, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62,
+ 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+ 0x2e, 0x41, 0x70, 0x74, 0x6f, 0x73, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
+ 0x6e, 0x48, 0x00, 0x52, 0x05, 0x61, 0x70, 0x74, 0x6f, 0x73, 0x42, 0x0d, 0x0a, 0x0b, 0x74, 0x72,
+ 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x75, 0x0a, 0x17, 0x47, 0x65, 0x74,
+ 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4e, 0x0a, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d,
+ 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
+ 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45,
+ 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74,
+ 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x48, 0x00, 0x52, 0x08, 0x65, 0x74, 0x68, 0x65,
+ 0x72, 0x65, 0x75, 0x6d, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
+ 0x22, 0x8c, 0x02, 0x0a, 0x1b, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63,
+ 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
+ 0x12, 0x5e, 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
+ 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x49, 0x6e,
+ 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65,
+ 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
+ 0x75, 0x65, 0x73, 0x74, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71,
+ 0x12, 0x38, 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32,
+ 0x22, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e,
+ 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x53, 0x0a, 0x0d, 0x61, 0x63,
+ 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x0b, 0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61,
+ 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63,
+ 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+ 0x65, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x22,
+ 0xd8, 0x01, 0x0a, 0x26, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x65, 0x74, 0x56,
+ 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74,
+ 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63,
+ 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x63,
+ 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x0d, 0x52, 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74,
+ 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x12,
+ 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61,
+ 0x73, 0x68, 0x12, 0x47, 0x0a, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x18, 0x64,
+ 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x74, 0x68,
- 0x65, 0x72, 0x65, 0x75, 0x6d, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74,
- 0x65, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x48, 0x00, 0x52, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65,
- 0x75, 0x6d, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8c,
- 0x02, 0x0a, 0x1b, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75,
- 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x5e,
- 0x0a, 0x0b, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x71, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x0b, 0x32, 0x3d, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63,
- 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x49, 0x6e, 0x74, 0x65,
- 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72, 0x69, 0x66, 0x69, 0x65, 0x64, 0x41,
- 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
- 0x73, 0x74, 0x52, 0x0a, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x12, 0x38,
- 0x0a, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e,
- 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74,
- 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63,
- 0x6b, 0x52, 0x05, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x53, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x6f,
- 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32,
- 0x2e, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e,
- 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x47, 0x65, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75,
- 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52,
- 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x22, 0xd8, 0x01,
- 0x0a, 0x26, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x65, 0x74, 0x56, 0x65, 0x72,
- 0x69, 0x66, 0x69, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74,
- 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f,
- 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75,
- 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x74, 0x61, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0d, 0x52,
- 0x03, 0x74, 0x61, 0x67, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x03,
- 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x12, 0x0a, 0x04,
- 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73, 0x68,
- 0x12, 0x47, 0x0a, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x18, 0x64, 0x20, 0x01,
- 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
- 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x74, 0x68, 0x65, 0x72,
- 0x65, 0x75, 0x6d, 0x45, 0x78, 0x74, 0x72, 0x61, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x48, 0x00, 0x52,
- 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x42, 0x0d, 0x0a, 0x0b, 0x65, 0x78, 0x74,
- 0x72, 0x61, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x22, 0x97, 0x01, 0x0a, 0x1c, 0x56, 0x61, 0x6c,
- 0x69, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74,
- 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62, 0x61, 0x6c,
- 0x61, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61, 0x6c, 0x61,
- 0x6e, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x18,
- 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
- 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x74,
- 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61,
- 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x08, 0x65, 0x74,
- 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e,
- 0x73, 0x65, 0x22, 0x64, 0x0a, 0x1b, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x52, 0x6f,
- 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
- 0x74, 0x12, 0x45, 0x0a, 0x0c, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x62, 0x6c, 0x6f, 0x63,
- 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
+ 0x65, 0x72, 0x65, 0x75, 0x6d, 0x45, 0x78, 0x74, 0x72, 0x61, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x48,
+ 0x00, 0x52, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x42, 0x0d, 0x0a, 0x0b, 0x65,
+ 0x78, 0x74, 0x72, 0x61, 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x22, 0x97, 0x01, 0x0a, 0x1c, 0x56,
+ 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74,
+ 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x62,
+ 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x62, 0x61,
+ 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x51, 0x0a, 0x08, 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75,
+ 0x6d, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x33, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
- 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x0b, 0x6e, 0x61, 0x74,
- 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x2a, 0x6d, 0x0a, 0x09, 0x53, 0x69, 0x64, 0x65,
- 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x49, 0x44, 0x45, 0x43, 0x48, 0x41,
- 0x49, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x25, 0x0a, 0x21, 0x53, 0x49, 0x44,
- 0x45, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f,
- 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x5f, 0x42, 0x45, 0x41, 0x43, 0x4f, 0x4e, 0x10, 0x01,
- 0x12, 0x25, 0x0a, 0x21, 0x53, 0x49, 0x44, 0x45, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x45, 0x54,
- 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f, 0x48, 0x4f, 0x4c, 0x45, 0x53, 0x4b, 0x59, 0x5f, 0x42,
- 0x45, 0x41, 0x43, 0x4f, 0x4e, 0x10, 0x02, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75,
- 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63,
- 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
- 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69,
- 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53,
+ 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x48, 0x00, 0x52, 0x08,
+ 0x65, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x42, 0x0a, 0x0a, 0x08, 0x72, 0x65, 0x73, 0x70,
+ 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x64, 0x0a, 0x1b, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65,
+ 0x52, 0x6f, 0x73, 0x65, 0x74, 0x74, 0x61, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x65, 0x71, 0x75,
+ 0x65, 0x73, 0x74, 0x12, 0x45, 0x0a, 0x0c, 0x6e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, 0x62, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x22, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+ 0x65, 0x2e, 0x4e, 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x52, 0x0b, 0x6e,
+ 0x61, 0x74, 0x69, 0x76, 0x65, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x2a, 0x8e, 0x01, 0x0a, 0x11, 0x42,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x4f, 0x62, 0x6a, 0x65, 0x63, 0x74, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74,
+ 0x12, 0x2b, 0x0a, 0x27, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54,
+ 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x4c, 0x45, 0x47, 0x41, 0x43, 0x59, 0x5f, 0x53,
+ 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x5f, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x10, 0x00, 0x12, 0x24, 0x0a,
+ 0x20, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x5f, 0x46, 0x4f,
+ 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x53, 0x49, 0x4e, 0x47, 0x4c, 0x45, 0x5f, 0x42, 0x4c, 0x4f, 0x43,
+ 0x4b, 0x10, 0x00, 0x12, 0x22, 0x0a, 0x1e, 0x42, 0x4c, 0x4f, 0x43, 0x4b, 0x5f, 0x4f, 0x42, 0x4a,
+ 0x45, 0x43, 0x54, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x43, 0x53, 0x43, 0x42, 0x5f,
+ 0x42, 0x41, 0x54, 0x43, 0x48, 0x10, 0x01, 0x1a, 0x02, 0x10, 0x01, 0x2a, 0x6d, 0x0a, 0x09, 0x53,
+ 0x69, 0x64, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x49, 0x44, 0x45,
+ 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x25, 0x0a, 0x21,
+ 0x53, 0x49, 0x44, 0x45, 0x43, 0x48, 0x41, 0x49, 0x4e, 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45,
+ 0x55, 0x4d, 0x5f, 0x4d, 0x41, 0x49, 0x4e, 0x4e, 0x45, 0x54, 0x5f, 0x42, 0x45, 0x41, 0x43, 0x4f,
+ 0x4e, 0x10, 0x01, 0x12, 0x25, 0x0a, 0x21, 0x53, 0x49, 0x44, 0x45, 0x43, 0x48, 0x41, 0x49, 0x4e,
+ 0x5f, 0x45, 0x54, 0x48, 0x45, 0x52, 0x45, 0x55, 0x4d, 0x5f, 0x48, 0x4f, 0x4c, 0x45, 0x53, 0x4b,
+ 0x59, 0x5f, 0x42, 0x45, 0x41, 0x43, 0x4f, 0x4e, 0x10, 0x02, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69,
+ 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
+ 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70,
+ 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63,
+ 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f,
+ 0x74, 0x6f, 0x33,
}
var (
@@ -1607,93 +1716,95 @@ func file_coinbase_chainstorage_blockchain_proto_rawDescGZIP() []byte {
return file_coinbase_chainstorage_blockchain_proto_rawDescData
}
-var file_coinbase_chainstorage_blockchain_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_coinbase_chainstorage_blockchain_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_coinbase_chainstorage_blockchain_proto_msgTypes = make([]protoimpl.MessageInfo, 12)
var file_coinbase_chainstorage_blockchain_proto_goTypes = []interface{}{
- (SideChain)(0), // 0: coinbase.chainstorage.SideChain
- (*Block)(nil), // 1: coinbase.chainstorage.Block
- (*BlockIdentifier)(nil), // 2: coinbase.chainstorage.BlockIdentifier
- (*BlockMetadata)(nil), // 3: coinbase.chainstorage.BlockMetadata
- (*TransactionMetadata)(nil), // 4: coinbase.chainstorage.TransactionMetadata
- (*RosettaBlock)(nil), // 5: coinbase.chainstorage.RosettaBlock
- (*NativeBlock)(nil), // 6: coinbase.chainstorage.NativeBlock
- (*NativeTransaction)(nil), // 7: coinbase.chainstorage.NativeTransaction
- (*GetAccountProofResponse)(nil), // 8: coinbase.chainstorage.GetAccountProofResponse
- (*ValidateAccountStateRequest)(nil), // 9: coinbase.chainstorage.ValidateAccountStateRequest
- (*InternalGetVerifiedAccountStateRequest)(nil), // 10: coinbase.chainstorage.InternalGetVerifiedAccountStateRequest
- (*ValidateAccountStateResponse)(nil), // 11: coinbase.chainstorage.ValidateAccountStateResponse
- (*ValidateRosettaBlockRequest)(nil), // 12: coinbase.chainstorage.ValidateRosettaBlockRequest
- (common.Blockchain)(0), // 13: coinbase.c3.common.Blockchain
- (common.Network)(0), // 14: coinbase.c3.common.Network
- (*EthereumBlobdata)(nil), // 15: coinbase.chainstorage.EthereumBlobdata
- (*BitcoinBlobdata)(nil), // 16: coinbase.chainstorage.BitcoinBlobdata
- (*RosettaBlobdata)(nil), // 17: coinbase.chainstorage.RosettaBlobdata
- (*SolanaBlobdata)(nil), // 18: coinbase.chainstorage.SolanaBlobdata
- (*AptosBlobdata)(nil), // 19: coinbase.chainstorage.AptosBlobdata
- (*EthereumBeaconBlobdata)(nil), // 20: coinbase.chainstorage.EthereumBeaconBlobdata
- (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp
- (*types.Block)(nil), // 22: coinbase.crypto.rosetta.types.Block
- (*EthereumBlock)(nil), // 23: coinbase.chainstorage.EthereumBlock
- (*BitcoinBlock)(nil), // 24: coinbase.chainstorage.BitcoinBlock
- (*SolanaBlock)(nil), // 25: coinbase.chainstorage.SolanaBlock
- (*AptosBlock)(nil), // 26: coinbase.chainstorage.AptosBlock
- (*SolanaBlockV2)(nil), // 27: coinbase.chainstorage.SolanaBlockV2
- (*EthereumBeaconBlock)(nil), // 28: coinbase.chainstorage.EthereumBeaconBlock
- (*EthereumTransaction)(nil), // 29: coinbase.chainstorage.EthereumTransaction
- (*BitcoinTransaction)(nil), // 30: coinbase.chainstorage.BitcoinTransaction
- (*types.Transaction)(nil), // 31: coinbase.crypto.rosetta.types.Transaction
- (*SolanaTransaction)(nil), // 32: coinbase.chainstorage.SolanaTransaction
- (*AptosTransaction)(nil), // 33: coinbase.chainstorage.AptosTransaction
- (*EthereumAccountStateProof)(nil), // 34: coinbase.chainstorage.EthereumAccountStateProof
- (*EthereumExtraInput)(nil), // 35: coinbase.chainstorage.EthereumExtraInput
- (*EthereumAccountStateResponse)(nil), // 36: coinbase.chainstorage.EthereumAccountStateResponse
+ (BlockObjectFormat)(0), // 0: coinbase.chainstorage.BlockObjectFormat
+ (SideChain)(0), // 1: coinbase.chainstorage.SideChain
+ (*Block)(nil), // 2: coinbase.chainstorage.Block
+ (*BlockIdentifier)(nil), // 3: coinbase.chainstorage.BlockIdentifier
+ (*BlockMetadata)(nil), // 4: coinbase.chainstorage.BlockMetadata
+ (*TransactionMetadata)(nil), // 5: coinbase.chainstorage.TransactionMetadata
+ (*RosettaBlock)(nil), // 6: coinbase.chainstorage.RosettaBlock
+ (*NativeBlock)(nil), // 7: coinbase.chainstorage.NativeBlock
+ (*NativeTransaction)(nil), // 8: coinbase.chainstorage.NativeTransaction
+ (*GetAccountProofResponse)(nil), // 9: coinbase.chainstorage.GetAccountProofResponse
+ (*ValidateAccountStateRequest)(nil), // 10: coinbase.chainstorage.ValidateAccountStateRequest
+ (*InternalGetVerifiedAccountStateRequest)(nil), // 11: coinbase.chainstorage.InternalGetVerifiedAccountStateRequest
+ (*ValidateAccountStateResponse)(nil), // 12: coinbase.chainstorage.ValidateAccountStateResponse
+ (*ValidateRosettaBlockRequest)(nil), // 13: coinbase.chainstorage.ValidateRosettaBlockRequest
+ (common.Blockchain)(0), // 14: coinbase.c3.common.Blockchain
+ (common.Network)(0), // 15: coinbase.c3.common.Network
+ (*EthereumBlobdata)(nil), // 16: coinbase.chainstorage.EthereumBlobdata
+ (*BitcoinBlobdata)(nil), // 17: coinbase.chainstorage.BitcoinBlobdata
+ (*RosettaBlobdata)(nil), // 18: coinbase.chainstorage.RosettaBlobdata
+ (*SolanaBlobdata)(nil), // 19: coinbase.chainstorage.SolanaBlobdata
+ (*AptosBlobdata)(nil), // 20: coinbase.chainstorage.AptosBlobdata
+ (*EthereumBeaconBlobdata)(nil), // 21: coinbase.chainstorage.EthereumBeaconBlobdata
+ (*timestamppb.Timestamp)(nil), // 22: google.protobuf.Timestamp
+ (*types.Block)(nil), // 23: coinbase.crypto.rosetta.types.Block
+ (*EthereumBlock)(nil), // 24: coinbase.chainstorage.EthereumBlock
+ (*BitcoinBlock)(nil), // 25: coinbase.chainstorage.BitcoinBlock
+ (*SolanaBlock)(nil), // 26: coinbase.chainstorage.SolanaBlock
+ (*AptosBlock)(nil), // 27: coinbase.chainstorage.AptosBlock
+ (*SolanaBlockV2)(nil), // 28: coinbase.chainstorage.SolanaBlockV2
+ (*EthereumBeaconBlock)(nil), // 29: coinbase.chainstorage.EthereumBeaconBlock
+ (*EthereumTransaction)(nil), // 30: coinbase.chainstorage.EthereumTransaction
+ (*BitcoinTransaction)(nil), // 31: coinbase.chainstorage.BitcoinTransaction
+ (*types.Transaction)(nil), // 32: coinbase.crypto.rosetta.types.Transaction
+ (*SolanaTransaction)(nil), // 33: coinbase.chainstorage.SolanaTransaction
+ (*AptosTransaction)(nil), // 34: coinbase.chainstorage.AptosTransaction
+ (*EthereumAccountStateProof)(nil), // 35: coinbase.chainstorage.EthereumAccountStateProof
+ (*EthereumExtraInput)(nil), // 36: coinbase.chainstorage.EthereumExtraInput
+ (*EthereumAccountStateResponse)(nil), // 37: coinbase.chainstorage.EthereumAccountStateResponse
}
var file_coinbase_chainstorage_blockchain_proto_depIdxs = []int32{
- 13, // 0: coinbase.chainstorage.Block.blockchain:type_name -> coinbase.c3.common.Blockchain
- 14, // 1: coinbase.chainstorage.Block.network:type_name -> coinbase.c3.common.Network
- 3, // 2: coinbase.chainstorage.Block.metadata:type_name -> coinbase.chainstorage.BlockMetadata
- 4, // 3: coinbase.chainstorage.Block.transaction_metadata:type_name -> coinbase.chainstorage.TransactionMetadata
- 0, // 4: coinbase.chainstorage.Block.side_chain:type_name -> coinbase.chainstorage.SideChain
- 15, // 5: coinbase.chainstorage.Block.ethereum:type_name -> coinbase.chainstorage.EthereumBlobdata
- 16, // 6: coinbase.chainstorage.Block.bitcoin:type_name -> coinbase.chainstorage.BitcoinBlobdata
- 17, // 7: coinbase.chainstorage.Block.rosetta:type_name -> coinbase.chainstorage.RosettaBlobdata
- 18, // 8: coinbase.chainstorage.Block.solana:type_name -> coinbase.chainstorage.SolanaBlobdata
- 19, // 9: coinbase.chainstorage.Block.aptos:type_name -> coinbase.chainstorage.AptosBlobdata
- 20, // 10: coinbase.chainstorage.Block.ethereum_beacon:type_name -> coinbase.chainstorage.EthereumBeaconBlobdata
- 21, // 11: coinbase.chainstorage.BlockIdentifier.timestamp:type_name -> google.protobuf.Timestamp
- 21, // 12: coinbase.chainstorage.BlockMetadata.timestamp:type_name -> google.protobuf.Timestamp
- 22, // 13: coinbase.chainstorage.RosettaBlock.block:type_name -> coinbase.crypto.rosetta.types.Block
- 13, // 14: coinbase.chainstorage.NativeBlock.blockchain:type_name -> coinbase.c3.common.Blockchain
- 14, // 15: coinbase.chainstorage.NativeBlock.network:type_name -> coinbase.c3.common.Network
- 21, // 16: coinbase.chainstorage.NativeBlock.timestamp:type_name -> google.protobuf.Timestamp
- 0, // 17: coinbase.chainstorage.NativeBlock.side_chain:type_name -> coinbase.chainstorage.SideChain
- 23, // 18: coinbase.chainstorage.NativeBlock.ethereum:type_name -> coinbase.chainstorage.EthereumBlock
- 24, // 19: coinbase.chainstorage.NativeBlock.bitcoin:type_name -> coinbase.chainstorage.BitcoinBlock
- 22, // 20: coinbase.chainstorage.NativeBlock.rosetta:type_name -> coinbase.crypto.rosetta.types.Block
- 25, // 21: coinbase.chainstorage.NativeBlock.solana:type_name -> coinbase.chainstorage.SolanaBlock
- 26, // 22: coinbase.chainstorage.NativeBlock.aptos:type_name -> coinbase.chainstorage.AptosBlock
- 27, // 23: coinbase.chainstorage.NativeBlock.solana_v2:type_name -> coinbase.chainstorage.SolanaBlockV2
- 28, // 24: coinbase.chainstorage.NativeBlock.ethereum_beacon:type_name -> coinbase.chainstorage.EthereumBeaconBlock
- 13, // 25: coinbase.chainstorage.NativeTransaction.blockchain:type_name -> coinbase.c3.common.Blockchain
- 14, // 26: coinbase.chainstorage.NativeTransaction.network:type_name -> coinbase.c3.common.Network
- 21, // 27: coinbase.chainstorage.NativeTransaction.block_timestamp:type_name -> google.protobuf.Timestamp
- 29, // 28: coinbase.chainstorage.NativeTransaction.ethereum:type_name -> coinbase.chainstorage.EthereumTransaction
- 30, // 29: coinbase.chainstorage.NativeTransaction.bitcoin:type_name -> coinbase.chainstorage.BitcoinTransaction
- 31, // 30: coinbase.chainstorage.NativeTransaction.rosetta:type_name -> coinbase.crypto.rosetta.types.Transaction
- 32, // 31: coinbase.chainstorage.NativeTransaction.solana:type_name -> coinbase.chainstorage.SolanaTransaction
- 33, // 32: coinbase.chainstorage.NativeTransaction.aptos:type_name -> coinbase.chainstorage.AptosTransaction
- 34, // 33: coinbase.chainstorage.GetAccountProofResponse.ethereum:type_name -> coinbase.chainstorage.EthereumAccountStateProof
- 10, // 34: coinbase.chainstorage.ValidateAccountStateRequest.account_req:type_name -> coinbase.chainstorage.InternalGetVerifiedAccountStateRequest
- 6, // 35: coinbase.chainstorage.ValidateAccountStateRequest.block:type_name -> coinbase.chainstorage.NativeBlock
- 8, // 36: coinbase.chainstorage.ValidateAccountStateRequest.account_proof:type_name -> coinbase.chainstorage.GetAccountProofResponse
- 35, // 37: coinbase.chainstorage.InternalGetVerifiedAccountStateRequest.ethereum:type_name -> coinbase.chainstorage.EthereumExtraInput
- 36, // 38: coinbase.chainstorage.ValidateAccountStateResponse.ethereum:type_name -> coinbase.chainstorage.EthereumAccountStateResponse
- 6, // 39: coinbase.chainstorage.ValidateRosettaBlockRequest.native_block:type_name -> coinbase.chainstorage.NativeBlock
- 40, // [40:40] is the sub-list for method output_type
- 40, // [40:40] is the sub-list for method input_type
- 40, // [40:40] is the sub-list for extension type_name
- 40, // [40:40] is the sub-list for extension extendee
- 0, // [0:40] is the sub-list for field type_name
+ 14, // 0: coinbase.chainstorage.Block.blockchain:type_name -> coinbase.c3.common.Blockchain
+ 15, // 1: coinbase.chainstorage.Block.network:type_name -> coinbase.c3.common.Network
+ 4, // 2: coinbase.chainstorage.Block.metadata:type_name -> coinbase.chainstorage.BlockMetadata
+ 5, // 3: coinbase.chainstorage.Block.transaction_metadata:type_name -> coinbase.chainstorage.TransactionMetadata
+ 1, // 4: coinbase.chainstorage.Block.side_chain:type_name -> coinbase.chainstorage.SideChain
+ 16, // 5: coinbase.chainstorage.Block.ethereum:type_name -> coinbase.chainstorage.EthereumBlobdata
+ 17, // 6: coinbase.chainstorage.Block.bitcoin:type_name -> coinbase.chainstorage.BitcoinBlobdata
+ 18, // 7: coinbase.chainstorage.Block.rosetta:type_name -> coinbase.chainstorage.RosettaBlobdata
+ 19, // 8: coinbase.chainstorage.Block.solana:type_name -> coinbase.chainstorage.SolanaBlobdata
+ 20, // 9: coinbase.chainstorage.Block.aptos:type_name -> coinbase.chainstorage.AptosBlobdata
+ 21, // 10: coinbase.chainstorage.Block.ethereum_beacon:type_name -> coinbase.chainstorage.EthereumBeaconBlobdata
+ 22, // 11: coinbase.chainstorage.BlockIdentifier.timestamp:type_name -> google.protobuf.Timestamp
+ 22, // 12: coinbase.chainstorage.BlockMetadata.timestamp:type_name -> google.protobuf.Timestamp
+ 0, // 13: coinbase.chainstorage.BlockMetadata.object_format:type_name -> coinbase.chainstorage.BlockObjectFormat
+ 23, // 14: coinbase.chainstorage.RosettaBlock.block:type_name -> coinbase.crypto.rosetta.types.Block
+ 14, // 15: coinbase.chainstorage.NativeBlock.blockchain:type_name -> coinbase.c3.common.Blockchain
+ 15, // 16: coinbase.chainstorage.NativeBlock.network:type_name -> coinbase.c3.common.Network
+ 22, // 17: coinbase.chainstorage.NativeBlock.timestamp:type_name -> google.protobuf.Timestamp
+ 1, // 18: coinbase.chainstorage.NativeBlock.side_chain:type_name -> coinbase.chainstorage.SideChain
+ 24, // 19: coinbase.chainstorage.NativeBlock.ethereum:type_name -> coinbase.chainstorage.EthereumBlock
+ 25, // 20: coinbase.chainstorage.NativeBlock.bitcoin:type_name -> coinbase.chainstorage.BitcoinBlock
+ 23, // 21: coinbase.chainstorage.NativeBlock.rosetta:type_name -> coinbase.crypto.rosetta.types.Block
+ 26, // 22: coinbase.chainstorage.NativeBlock.solana:type_name -> coinbase.chainstorage.SolanaBlock
+ 27, // 23: coinbase.chainstorage.NativeBlock.aptos:type_name -> coinbase.chainstorage.AptosBlock
+ 28, // 24: coinbase.chainstorage.NativeBlock.solana_v2:type_name -> coinbase.chainstorage.SolanaBlockV2
+ 29, // 25: coinbase.chainstorage.NativeBlock.ethereum_beacon:type_name -> coinbase.chainstorage.EthereumBeaconBlock
+ 14, // 26: coinbase.chainstorage.NativeTransaction.blockchain:type_name -> coinbase.c3.common.Blockchain
+ 15, // 27: coinbase.chainstorage.NativeTransaction.network:type_name -> coinbase.c3.common.Network
+ 22, // 28: coinbase.chainstorage.NativeTransaction.block_timestamp:type_name -> google.protobuf.Timestamp
+ 30, // 29: coinbase.chainstorage.NativeTransaction.ethereum:type_name -> coinbase.chainstorage.EthereumTransaction
+ 31, // 30: coinbase.chainstorage.NativeTransaction.bitcoin:type_name -> coinbase.chainstorage.BitcoinTransaction
+ 32, // 31: coinbase.chainstorage.NativeTransaction.rosetta:type_name -> coinbase.crypto.rosetta.types.Transaction
+ 33, // 32: coinbase.chainstorage.NativeTransaction.solana:type_name -> coinbase.chainstorage.SolanaTransaction
+ 34, // 33: coinbase.chainstorage.NativeTransaction.aptos:type_name -> coinbase.chainstorage.AptosTransaction
+ 35, // 34: coinbase.chainstorage.GetAccountProofResponse.ethereum:type_name -> coinbase.chainstorage.EthereumAccountStateProof
+ 11, // 35: coinbase.chainstorage.ValidateAccountStateRequest.account_req:type_name -> coinbase.chainstorage.InternalGetVerifiedAccountStateRequest
+ 7, // 36: coinbase.chainstorage.ValidateAccountStateRequest.block:type_name -> coinbase.chainstorage.NativeBlock
+ 9, // 37: coinbase.chainstorage.ValidateAccountStateRequest.account_proof:type_name -> coinbase.chainstorage.GetAccountProofResponse
+ 36, // 38: coinbase.chainstorage.InternalGetVerifiedAccountStateRequest.ethereum:type_name -> coinbase.chainstorage.EthereumExtraInput
+ 37, // 39: coinbase.chainstorage.ValidateAccountStateResponse.ethereum:type_name -> coinbase.chainstorage.EthereumAccountStateResponse
+ 7, // 40: coinbase.chainstorage.ValidateRosettaBlockRequest.native_block:type_name -> coinbase.chainstorage.NativeBlock
+ 41, // [41:41] is the sub-list for method output_type
+ 41, // [41:41] is the sub-list for method input_type
+ 41, // [41:41] is the sub-list for extension type_name
+ 41, // [41:41] is the sub-list for extension extendee
+ 0, // [0:41] is the sub-list for field type_name
}
func init() { file_coinbase_chainstorage_blockchain_proto_init() }
@@ -1891,7 +2002,7 @@ func file_coinbase_chainstorage_blockchain_proto_init() {
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_coinbase_chainstorage_blockchain_proto_rawDesc,
- NumEnums: 1,
+ NumEnums: 2,
NumMessages: 12,
NumExtensions: 0,
NumServices: 0,
diff --git a/protos/coinbase/chainstorage/blockchain.proto b/protos/coinbase/chainstorage/blockchain.proto
index b09172f3..724e2bbc 100644
--- a/protos/coinbase/chainstorage/blockchain.proto
+++ b/protos/coinbase/chainstorage/blockchain.proto
@@ -39,6 +39,16 @@ message BlockIdentifier {
google.protobuf.Timestamp timestamp = 5;
}
+enum BlockObjectFormat {
+ option allow_alias = true;
+
+ // Alias order controls the protojson spelling. Keep the pre-rename name first
+ // until mixed-version REST clients and servers no longer coexist.
+ BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK = 0;
+ BLOCK_OBJECT_FORMAT_SINGLE_BLOCK = 0;
+ BLOCK_OBJECT_FORMAT_CSCB_BATCH = 1;
+}
+
message BlockMetadata {
// Tag is a way to version the blockchain dataset. See http://go/cs-design for more details.
uint32 tag = 1;
@@ -56,6 +66,14 @@ message BlockMetadata {
bool skipped = 7;
// Block timestamp. Note that this attribute is only available in recent blocks.
google.protobuf.Timestamp timestamp = 8;
+ // Offset of this block's raw protobuf payload inside the decoded object payload.
+ uint64 byte_offset = 9;
+ // Length of this block's raw protobuf payload. Zero means single-block whole-object reads.
+ uint64 byte_length = 10;
+ // Length of the decoded payload region that contains this block.
+ uint64 uncompressed_length = 11;
+ // Object format for object_key_main. Zero value is the single-block whole-object format.
+ BlockObjectFormat object_format = 12;
}
message TransactionMetadata {
diff --git a/protos/coinbase/chainstorage/blockchain_aptos.pb.go b/protos/coinbase/chainstorage/blockchain_aptos.pb.go
index 51500a52..62a2e1b3 100644
--- a/protos/coinbase/chainstorage/blockchain_aptos.pb.go
+++ b/protos/coinbase/chainstorage/blockchain_aptos.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/chainstorage/blockchain_aptos.proto
package chainstorage
diff --git a/protos/coinbase/chainstorage/blockchain_bitcoin.pb.go b/protos/coinbase/chainstorage/blockchain_bitcoin.pb.go
index 2eb5e089..4c53df9c 100644
--- a/protos/coinbase/chainstorage/blockchain_bitcoin.pb.go
+++ b/protos/coinbase/chainstorage/blockchain_bitcoin.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/chainstorage/blockchain_bitcoin.proto
package chainstorage
@@ -28,6 +28,10 @@ type BitcoinBlobdata struct {
Header []byte `protobuf:"bytes,1,opt,name=header,proto3" json:"header,omitempty"`
InputTransactions []*RepeatedBytes `protobuf:"bytes,2,rep,name=input_transactions,json=inputTransactions,proto3" json:"input_transactions,omitempty"`
+ // Full raw JSON from getrawtransaction, keyed by txid.
+ // Preserves complete transaction data including chain-specific fields
+ // (e.g. Dash special transaction payloads) that are stripped from input_transactions.
+ RawInputTransactions map[string][]byte `protobuf:"bytes,3,rep,name=raw_input_transactions,json=rawInputTransactions,proto3" json:"raw_input_transactions,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
}
func (x *BitcoinBlobdata) Reset() {
@@ -76,6 +80,13 @@ func (x *BitcoinBlobdata) GetInputTransactions() []*RepeatedBytes {
return nil
}
+func (x *BitcoinBlobdata) GetRawInputTransactions() map[string][]byte {
+ if x != nil {
+ return x.RawInputTransactions
+ }
+ return nil
+}
+
type RepeatedBytes struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -148,6 +159,9 @@ type BitcoinHeader struct {
PreviousBlockHash string `protobuf:"bytes,17,opt,name=previous_block_hash,json=previousBlockHash,proto3" json:"previous_block_hash,omitempty"`
NextBlockHash string `protobuf:"bytes,18,opt,name=next_block_hash,json=nextBlockHash,proto3" json:"next_block_hash,omitempty"`
Timestamp *timestamppb.Timestamp `protobuf:"bytes,19,opt,name=timestamp,proto3" json:"timestamp,omitempty"`
+ // raw_nonce stores the nonce as the original string from the RPC response,
+ // for chains where nonce is not a uint64 (e.g. Zcash Equihash 256-bit nonce).
+ RawNonce string `protobuf:"bytes,20,opt,name=raw_nonce,json=rawNonce,proto3" json:"raw_nonce,omitempty"`
}
func (x *BitcoinHeader) Reset() {
@@ -308,6 +322,13 @@ func (x *BitcoinHeader) GetTimestamp() *timestamppb.Timestamp {
return nil
}
+func (x *BitcoinHeader) GetRawNonce() string {
+ if x != nil {
+ return x.RawNonce
+ }
+ return ""
+}
+
// BitcoinTransaction https://developer.bitcoin.org/reference/rpc/getrawtransaction.html
type BitcoinTransaction struct {
state protoimpl.MessageState
@@ -871,159 +892,173 @@ var file_coinbase_chainstorage_blockchain_bitcoin_proto_rawDesc = []byte{
0x12, 0x15, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e,
0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
- 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x7e, 0x0a, 0x0f, 0x42, 0x69, 0x74, 0x63,
- 0x6f, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x62, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06, 0x68,
- 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x68, 0x65, 0x61,
- 0x64, 0x65, 0x72, 0x12, 0x53, 0x0a, 0x12, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x72, 0x61,
- 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32,
- 0x24, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e,
- 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64,
- 0x42, 0x79, 0x74, 0x65, 0x73, 0x52, 0x11, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x72, 0x61, 0x6e,
- 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x23, 0x0a, 0x0d, 0x52, 0x65, 0x70, 0x65,
- 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74,
- 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xce, 0x04,
- 0x0a, 0x0d, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12,
- 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68,
- 0x61, 0x73, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x72, 0x69, 0x70, 0x70, 0x65, 0x64, 0x5f,
- 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x73, 0x74, 0x72, 0x69,
- 0x70, 0x70, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65,
- 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06,
- 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x77, 0x65,
- 0x69, 0x67, 0x68, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x06,
- 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x18, 0x0a, 0x07,
- 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x76,
- 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f,
- 0x6e, 0x5f, 0x68, 0x65, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x76, 0x65, 0x72,
- 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x72, 0x6b, 0x6c,
- 0x65, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6d, 0x65,
- 0x72, 0x6b, 0x6c, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65,
- 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b,
- 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28,
- 0x04, 0x52, 0x0a, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x14, 0x0a,
- 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6e, 0x6f,
- 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x69, 0x74, 0x73, 0x18, 0x0d, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x66, 0x66, 0x69,
- 0x63, 0x75, 0x6c, 0x74, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x69, 0x66,
- 0x66, 0x69, 0x63, 0x75, 0x6c, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61, 0x69, 0x6e,
- 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63, 0x68, 0x61,
- 0x69, 0x6e, 0x57, 0x6f, 0x72, 0x6b, 0x12, 0x34, 0x0a, 0x16, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72,
- 0x5f, 0x6f, 0x66, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73,
- 0x18, 0x10, 0x20, 0x01, 0x28, 0x04, 0x52, 0x14, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x4f, 0x66,
- 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e, 0x0a, 0x13,
- 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68,
- 0x61, 0x73, 0x68, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x72, 0x65, 0x76, 0x69,
- 0x6f, 0x75, 0x73, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x26, 0x0a, 0x0f,
- 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18,
- 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x42, 0x6c, 0x6f, 0x63, 0x6b,
- 0x48, 0x61, 0x73, 0x68, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d,
- 0x70, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
- 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74,
- 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x9d,
- 0x05, 0x0a, 0x12, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61,
- 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x03, 0x68, 0x65, 0x78, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73,
- 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x0d, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x12,
- 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61,
- 0x73, 0x68, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04,
- 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61,
- 0x6c, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x76, 0x69,
- 0x72, 0x74, 0x75, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x77, 0x65, 0x69,
- 0x67, 0x68, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68,
- 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01,
- 0x28, 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x6c,
- 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08,
- 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x06, 0x69, 0x6e, 0x70, 0x75,
- 0x74, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62,
- 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
- 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74,
- 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x06, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x73,
- 0x12, 0x49, 0x0a, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28,
- 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61,
- 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69,
- 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70,
- 0x75, 0x74, 0x52, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x62,
- 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c,
- 0x6f, 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09,
- 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d,
- 0x65, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1f, 0x0a,
- 0x0b, 0x69, 0x73, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x18, 0x10, 0x20, 0x01,
- 0x28, 0x08, 0x52, 0x0a, 0x69, 0x73, 0x43, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x12, 0x14,
- 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x11, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69,
- 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x63, 0x6f,
- 0x75, 0x6e, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x69, 0x6e, 0x70, 0x75, 0x74,
- 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f,
- 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6f, 0x75, 0x74,
- 0x70, 0x75, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x70, 0x75,
- 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x69,
- 0x6e, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6f, 0x75, 0x74,
- 0x70, 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x04, 0x52,
- 0x0b, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03,
- 0x66, 0x65, 0x65, 0x18, 0x16, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x66, 0x65, 0x65, 0x22, 0xa6,
- 0x03, 0x0a, 0x17, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61,
- 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f,
- 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f,
- 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61,
- 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
- 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2a, 0x0a,
- 0x11, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x69, 0x6e, 0x64,
- 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x66, 0x72, 0x6f, 0x6d, 0x4f, 0x75,
- 0x74, 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x58, 0x0a, 0x10, 0x73, 0x63, 0x72,
- 0x69, 0x70, 0x74, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20,
- 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63,
- 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63,
- 0x6f, 0x69, 0x6e, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75,
- 0x72, 0x65, 0x52, 0x0f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74,
- 0x75, 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18,
- 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12,
- 0x3e, 0x0a, 0x1b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69,
- 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x77, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x06,
- 0x20, 0x03, 0x28, 0x09, 0x52, 0x19, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
- 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x57, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12,
- 0x50, 0x0a, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x07,
- 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
- 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74,
- 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4f,
- 0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x0a, 0x66, 0x72, 0x6f, 0x6d, 0x4f, 0x75, 0x74, 0x70, 0x75,
- 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04,
- 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x46, 0x0a, 0x16, 0x42, 0x69, 0x74, 0x63, 0x6f,
+ 0x6d, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xbf, 0x02, 0x0a, 0x0f, 0x42, 0x69, 0x74,
+ 0x63, 0x6f, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x62, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a, 0x06,
+ 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x68, 0x65,
+ 0x61, 0x64, 0x65, 0x72, 0x12, 0x53, 0x0a, 0x12, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x72,
+ 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b,
+ 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69,
+ 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x52, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65,
+ 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x52, 0x11, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x72, 0x61,
+ 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x76, 0x0a, 0x16, 0x72, 0x61, 0x77,
+ 0x5f, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69,
+ 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x40, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+ 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x62, 0x64, 0x61, 0x74,
+ 0x61, 0x2e, 0x52, 0x61, 0x77, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61,
+ 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x14, 0x72, 0x61, 0x77,
+ 0x49, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+ 0x73, 0x1a, 0x47, 0x0a, 0x19, 0x52, 0x61, 0x77, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x54, 0x72, 0x61,
+ 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10,
+ 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79,
+ 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52,
+ 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, 0x23, 0x0a, 0x0d, 0x52, 0x65,
+ 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x42, 0x79, 0x74, 0x65, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64,
+ 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22,
+ 0xeb, 0x04, 0x0a, 0x0d, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65,
+ 0x72, 0x12, 0x12, 0x0a, 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x04, 0x68, 0x61, 0x73, 0x68, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x74, 0x72, 0x69, 0x70, 0x70, 0x65,
+ 0x64, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0c, 0x73, 0x74,
+ 0x72, 0x69, 0x70, 0x70, 0x65, 0x64, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69,
+ 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x16,
+ 0x0a, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06,
+ 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74,
+ 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x68, 0x65, 0x69, 0x67, 0x68, 0x74, 0x12, 0x18,
+ 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52,
+ 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1f, 0x0a, 0x0b, 0x76, 0x65, 0x72, 0x73,
+ 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x65, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x76,
+ 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x48, 0x65, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x6d, 0x65, 0x72,
+ 0x6b, 0x6c, 0x65, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a,
+ 0x6d, 0x65, 0x72, 0x6b, 0x6c, 0x65, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69,
+ 0x6d, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1f,
+ 0x0a, 0x0b, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0b, 0x20,
+ 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x6e, 0x54, 0x69, 0x6d, 0x65, 0x12,
+ 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05,
+ 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x62, 0x69, 0x74, 0x73, 0x18, 0x0d, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x04, 0x62, 0x69, 0x74, 0x73, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x69, 0x66,
+ 0x66, 0x69, 0x63, 0x75, 0x6c, 0x74, 0x79, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64,
+ 0x69, 0x66, 0x66, 0x69, 0x63, 0x75, 0x6c, 0x74, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x68, 0x61,
+ 0x69, 0x6e, 0x5f, 0x77, 0x6f, 0x72, 0x6b, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x63,
+ 0x68, 0x61, 0x69, 0x6e, 0x57, 0x6f, 0x72, 0x6b, 0x12, 0x34, 0x0a, 0x16, 0x6e, 0x75, 0x6d, 0x62,
+ 0x65, 0x72, 0x5f, 0x6f, 0x66, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
+ 0x6e, 0x73, 0x18, 0x10, 0x20, 0x01, 0x28, 0x04, 0x52, 0x14, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72,
+ 0x4f, 0x66, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x2e,
+ 0x0a, 0x13, 0x70, 0x72, 0x65, 0x76, 0x69, 0x6f, 0x75, 0x73, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x70, 0x72, 0x65,
+ 0x76, 0x69, 0x6f, 0x75, 0x73, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x26,
+ 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73,
+ 0x68, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x42, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x38, 0x0a, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74,
+ 0x61, 0x6d, 0x70, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67,
+ 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65,
+ 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70,
+ 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x61, 0x77, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x14, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x08, 0x72, 0x61, 0x77, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x22, 0x9d, 0x05,
+ 0x0a, 0x12, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
+ 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x03, 0x68, 0x65, 0x78, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61,
+ 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d,
+ 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x12, 0x0a,
+ 0x04, 0x68, 0x61, 0x73, 0x68, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x68, 0x61, 0x73,
+ 0x68, 0x12, 0x12, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52,
+ 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x76, 0x69, 0x72, 0x74, 0x75, 0x61, 0x6c,
+ 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x76, 0x69, 0x72,
+ 0x74, 0x75, 0x61, 0x6c, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x77, 0x65, 0x69, 0x67,
+ 0x68, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74,
+ 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28,
+ 0x04, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x46, 0x0a, 0x06, 0x69, 0x6e, 0x70, 0x75, 0x74,
+ 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61,
+ 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e,
+ 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69,
+ 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x52, 0x06, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x73, 0x12,
+ 0x49, 0x0a, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b,
+ 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69,
+ 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e,
+ 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75,
+ 0x74, 0x52, 0x07, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
+ 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x5f, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x62,
+ 0x6c, 0x6f, 0x63, 0x6b, 0x54, 0x69, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x69, 0x6d, 0x65,
+ 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x1f, 0x0a, 0x0b,
+ 0x69, 0x73, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x18, 0x10, 0x20, 0x01, 0x28,
+ 0x08, 0x52, 0x0a, 0x69, 0x73, 0x43, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x12, 0x14, 0x0a,
+ 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x11, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e,
+ 0x64, 0x65, 0x78, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x5f, 0x63, 0x6f, 0x75,
+ 0x6e, 0x74, 0x18, 0x12, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x43,
+ 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x63,
+ 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x13, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x6f, 0x75, 0x74, 0x70,
+ 0x75, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x69, 0x6e, 0x70, 0x75, 0x74,
+ 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x14, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x69, 0x6e,
+ 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x6f, 0x75, 0x74, 0x70,
+ 0x75, 0x74, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x15, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b,
+ 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x66,
+ 0x65, 0x65, 0x18, 0x16, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x66, 0x65, 0x65, 0x22, 0xa6, 0x03,
+ 0x0a, 0x17, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
+ 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x1a, 0x0a, 0x08, 0x63, 0x6f, 0x69,
+ 0x6e, 0x62, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x69,
+ 0x6e, 0x62, 0x61, 0x73, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
+ 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x74,
+ 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x2a, 0x0a, 0x11,
+ 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x5f, 0x69, 0x6e, 0x64, 0x65,
+ 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0f, 0x66, 0x72, 0x6f, 0x6d, 0x4f, 0x75, 0x74,
+ 0x70, 0x75, 0x74, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x58, 0x0a, 0x10, 0x73, 0x63, 0x72, 0x69,
+ 0x70, 0x74, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x04, 0x20, 0x01,
+ 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68,
+ 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f,
0x69, 0x6e, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72,
- 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x18, 0x01, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x08, 0x61, 0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x12, 0x10, 0x0a,
- 0x03, 0x68, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x68, 0x65, 0x78, 0x22,
- 0xa1, 0x01, 0x0a, 0x18, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73,
- 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x14, 0x0a, 0x05,
- 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x64,
- 0x65, 0x78, 0x12, 0x59, 0x0a, 0x11, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, 0x70, 0x75, 0x62,
- 0x6c, 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e,
+ 0x65, 0x52, 0x0f, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75,
+ 0x72, 0x65, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x05,
+ 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x73, 0x65, 0x71, 0x75, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x3e,
+ 0x0a, 0x1b, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e,
+ 0x70, 0x75, 0x74, 0x5f, 0x77, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x65, 0x73, 0x18, 0x06, 0x20,
+ 0x03, 0x28, 0x09, 0x52, 0x19, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+ 0x49, 0x6e, 0x70, 0x75, 0x74, 0x57, 0x69, 0x74, 0x6e, 0x65, 0x73, 0x73, 0x65, 0x73, 0x12, 0x50,
+ 0x0a, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x07, 0x20,
+ 0x01, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63,
+ 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63,
+ 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75,
+ 0x74, 0x70, 0x75, 0x74, 0x52, 0x0a, 0x66, 0x72, 0x6f, 0x6d, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74,
+ 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x08, 0x20, 0x01, 0x28, 0x04, 0x52,
+ 0x05, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x22, 0x46, 0x0a, 0x16, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69,
+ 0x6e, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65,
+ 0x12, 0x1a, 0x0a, 0x08, 0x61, 0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x08, 0x61, 0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x12, 0x10, 0x0a, 0x03,
+ 0x68, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x68, 0x65, 0x78, 0x22, 0xa1,
+ 0x01, 0x0a, 0x18, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61,
+ 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x69,
+ 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x69, 0x6e, 0x64, 0x65,
+ 0x78, 0x12, 0x59, 0x0a, 0x11, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x5f, 0x70, 0x75, 0x62, 0x6c,
+ 0x69, 0x63, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x63,
+ 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
+ 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x53, 0x63, 0x72, 0x69,
+ 0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x0f, 0x73, 0x63, 0x72,
+ 0x69, 0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05,
+ 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61, 0x6c,
+ 0x75, 0x65, 0x22, 0x74, 0x0a, 0x16, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x53, 0x63, 0x72,
+ 0x69, 0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a, 0x08,
+ 0x61, 0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08,
+ 0x61, 0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x65, 0x78, 0x18,
+ 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x68, 0x65, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79,
+ 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x18,
+ 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x9b, 0x01, 0x0a, 0x0c, 0x42, 0x69, 0x74,
+ 0x63, 0x6f, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x3c, 0x0a, 0x06, 0x68, 0x65, 0x61,
+ 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x69, 0x6e,
+ 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67,
+ 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72, 0x52,
+ 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x4d, 0x0a, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73,
+ 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e,
0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74,
- 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x53, 0x63, 0x72,
- 0x69, 0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x52, 0x0f, 0x73, 0x63,
- 0x72, 0x69, 0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x14, 0x0a,
- 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x76, 0x61,
- 0x6c, 0x75, 0x65, 0x22, 0x74, 0x0a, 0x16, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x53, 0x63,
- 0x72, 0x69, 0x70, 0x74, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, 0x79, 0x12, 0x1a, 0x0a,
- 0x08, 0x61, 0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x08, 0x61, 0x73, 0x73, 0x65, 0x6d, 0x62, 0x6c, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x68, 0x65, 0x78,
- 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x68, 0x65, 0x78, 0x12, 0x12, 0x0a, 0x04, 0x74,
- 0x79, 0x70, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12,
- 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x9b, 0x01, 0x0a, 0x0c, 0x42, 0x69,
- 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x42, 0x6c, 0x6f, 0x63, 0x6b, 0x12, 0x3c, 0x0a, 0x06, 0x68, 0x65,
- 0x61, 0x64, 0x65, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x69,
- 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61,
- 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x48, 0x65, 0x61, 0x64, 0x65, 0x72,
- 0x52, 0x06, 0x68, 0x65, 0x61, 0x64, 0x65, 0x72, 0x12, 0x4d, 0x0a, 0x0c, 0x74, 0x72, 0x61, 0x6e,
- 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29,
- 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73,
- 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72,
- 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73,
- 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75,
- 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63,
- 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
- 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69,
- 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+ 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x42, 0x69, 0x74, 0x63, 0x6f, 0x69, 0x6e, 0x54, 0x72, 0x61,
+ 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61,
+ 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
+ 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68,
+ 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+ 0x73, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e,
+ 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -1038,7 +1073,7 @@ func file_coinbase_chainstorage_blockchain_bitcoin_proto_rawDescGZIP() []byte {
return file_coinbase_chainstorage_blockchain_bitcoin_proto_rawDescData
}
-var file_coinbase_chainstorage_blockchain_bitcoin_proto_msgTypes = make([]protoimpl.MessageInfo, 9)
+var file_coinbase_chainstorage_blockchain_bitcoin_proto_msgTypes = make([]protoimpl.MessageInfo, 10)
var file_coinbase_chainstorage_blockchain_bitcoin_proto_goTypes = []interface{}{
(*BitcoinBlobdata)(nil), // 0: coinbase.chainstorage.BitcoinBlobdata
(*RepeatedBytes)(nil), // 1: coinbase.chainstorage.RepeatedBytes
@@ -1049,23 +1084,25 @@ var file_coinbase_chainstorage_blockchain_bitcoin_proto_goTypes = []interface{}{
(*BitcoinTransactionOutput)(nil), // 6: coinbase.chainstorage.BitcoinTransactionOutput
(*BitcoinScriptPublicKey)(nil), // 7: coinbase.chainstorage.BitcoinScriptPublicKey
(*BitcoinBlock)(nil), // 8: coinbase.chainstorage.BitcoinBlock
- (*timestamppb.Timestamp)(nil), // 9: google.protobuf.Timestamp
+ nil, // 9: coinbase.chainstorage.BitcoinBlobdata.RawInputTransactionsEntry
+ (*timestamppb.Timestamp)(nil), // 10: google.protobuf.Timestamp
}
var file_coinbase_chainstorage_blockchain_bitcoin_proto_depIdxs = []int32{
- 1, // 0: coinbase.chainstorage.BitcoinBlobdata.input_transactions:type_name -> coinbase.chainstorage.RepeatedBytes
- 9, // 1: coinbase.chainstorage.BitcoinHeader.timestamp:type_name -> google.protobuf.Timestamp
- 4, // 2: coinbase.chainstorage.BitcoinTransaction.inputs:type_name -> coinbase.chainstorage.BitcoinTransactionInput
- 6, // 3: coinbase.chainstorage.BitcoinTransaction.outputs:type_name -> coinbase.chainstorage.BitcoinTransactionOutput
- 5, // 4: coinbase.chainstorage.BitcoinTransactionInput.script_signature:type_name -> coinbase.chainstorage.BitcoinScriptSignature
- 6, // 5: coinbase.chainstorage.BitcoinTransactionInput.from_output:type_name -> coinbase.chainstorage.BitcoinTransactionOutput
- 7, // 6: coinbase.chainstorage.BitcoinTransactionOutput.script_public_key:type_name -> coinbase.chainstorage.BitcoinScriptPublicKey
- 2, // 7: coinbase.chainstorage.BitcoinBlock.header:type_name -> coinbase.chainstorage.BitcoinHeader
- 3, // 8: coinbase.chainstorage.BitcoinBlock.transactions:type_name -> coinbase.chainstorage.BitcoinTransaction
- 9, // [9:9] is the sub-list for method output_type
- 9, // [9:9] is the sub-list for method input_type
- 9, // [9:9] is the sub-list for extension type_name
- 9, // [9:9] is the sub-list for extension extendee
- 0, // [0:9] is the sub-list for field type_name
+ 1, // 0: coinbase.chainstorage.BitcoinBlobdata.input_transactions:type_name -> coinbase.chainstorage.RepeatedBytes
+ 9, // 1: coinbase.chainstorage.BitcoinBlobdata.raw_input_transactions:type_name -> coinbase.chainstorage.BitcoinBlobdata.RawInputTransactionsEntry
+ 10, // 2: coinbase.chainstorage.BitcoinHeader.timestamp:type_name -> google.protobuf.Timestamp
+ 4, // 3: coinbase.chainstorage.BitcoinTransaction.inputs:type_name -> coinbase.chainstorage.BitcoinTransactionInput
+ 6, // 4: coinbase.chainstorage.BitcoinTransaction.outputs:type_name -> coinbase.chainstorage.BitcoinTransactionOutput
+ 5, // 5: coinbase.chainstorage.BitcoinTransactionInput.script_signature:type_name -> coinbase.chainstorage.BitcoinScriptSignature
+ 6, // 6: coinbase.chainstorage.BitcoinTransactionInput.from_output:type_name -> coinbase.chainstorage.BitcoinTransactionOutput
+ 7, // 7: coinbase.chainstorage.BitcoinTransactionOutput.script_public_key:type_name -> coinbase.chainstorage.BitcoinScriptPublicKey
+ 2, // 8: coinbase.chainstorage.BitcoinBlock.header:type_name -> coinbase.chainstorage.BitcoinHeader
+ 3, // 9: coinbase.chainstorage.BitcoinBlock.transactions:type_name -> coinbase.chainstorage.BitcoinTransaction
+ 10, // [10:10] is the sub-list for method output_type
+ 10, // [10:10] is the sub-list for method input_type
+ 10, // [10:10] is the sub-list for extension type_name
+ 10, // [10:10] is the sub-list for extension extendee
+ 0, // [0:10] is the sub-list for field type_name
}
func init() { file_coinbase_chainstorage_blockchain_bitcoin_proto_init() }
@@ -1189,7 +1226,7 @@ func file_coinbase_chainstorage_blockchain_bitcoin_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_coinbase_chainstorage_blockchain_bitcoin_proto_rawDesc,
NumEnums: 0,
- NumMessages: 9,
+ NumMessages: 10,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/protos/coinbase/chainstorage/blockchain_bitcoin.proto b/protos/coinbase/chainstorage/blockchain_bitcoin.proto
index 6fb9e4b0..9cb1afca 100644
--- a/protos/coinbase/chainstorage/blockchain_bitcoin.proto
+++ b/protos/coinbase/chainstorage/blockchain_bitcoin.proto
@@ -9,6 +9,10 @@ import "google/protobuf/timestamp.proto";
message BitcoinBlobdata {
bytes header = 1;
repeated RepeatedBytes input_transactions = 2;
+ // Full raw JSON from getrawtransaction, keyed by txid.
+ // Preserves complete transaction data including chain-specific fields
+ // (e.g. Dash special transaction payloads) that are stripped from input_transactions.
+ map raw_input_transactions = 3;
}
message RepeatedBytes {
@@ -36,6 +40,9 @@ message BitcoinHeader {
string previous_block_hash = 17;
string next_block_hash = 18;
google.protobuf.Timestamp timestamp = 19;
+ // raw_nonce stores the nonce as the original string from the RPC response,
+ // for chains where nonce is not a uint64 (e.g. Zcash Equihash 256-bit nonce).
+ string raw_nonce = 20;
}
// BitcoinTransaction https://developer.bitcoin.org/reference/rpc/getrawtransaction.html
diff --git a/protos/coinbase/chainstorage/blockchain_bitcoin.stream.go b/protos/coinbase/chainstorage/blockchain_bitcoin.stream.go
new file mode 100644
index 00000000..253b8beb
--- /dev/null
+++ b/protos/coinbase/chainstorage/blockchain_bitcoin.stream.go
@@ -0,0 +1,250 @@
+package chainstorage
+
+// Bitcoin-specific variant of the Block wire walker. Shares the
+// generic walker infrastructure from block_stream.go, but when the
+// BitcoinBlobdata.Header field is encountered, the bytes are skipped
+// and only their (offset, length) within the walked stream is
+// recorded. Callers layer a spool-file reader over that offset to
+// stream the JSON header directly into the parser without
+// materializing it as a []byte in RAM.
+//
+// Everything else (Block outer fields, BitcoinBlobdata fields other
+// than Header, sub-messages, repeated bytes, map entries) delegates
+// to the generic walker's reflection-driven decoder — so peak RAM on
+// InputTransactions-heavy zcash blocks matches the generic walker's
+// direct-read efficiency.
+
+import (
+ "bufio"
+ "io"
+
+ "golang.org/x/xerrors"
+ "google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// Proto wire types (see https://protobuf.dev/programming-guides/encoding/).
+// Defined here and shared with the generic walker in block_stream.go.
+const (
+ wireVarint = 0
+ wireFixed64 = 1
+ wireBytes = 2 // length-delimited
+ wireStartGroup = 3 // deprecated
+ wireEndGroup = 4 // deprecated
+ wireFixed32 = 5
+)
+
+// Block field numbers.
+const (
+ blockFieldBlockchain = 1
+ blockFieldNetwork = 2
+ blockFieldMetadata = 3
+ blockFieldTransactionMetadata = 4
+ blockFieldSideChain = 5
+ // blobdata oneof:
+ blockFieldEthereum = 100
+ blockFieldBitcoin = 101
+ blockFieldRosetta = 102
+ blockFieldSolana = 103
+ blockFieldAptos = 104
+ blockFieldEthereumBeacon = 105
+)
+
+// BitcoinBlobdata field numbers.
+const (
+ bitcoinBlobFieldHeader = 1
+ bitcoinBlobFieldInputTransactions = 2
+ bitcoinBlobFieldRawInputTransactions = 3
+)
+
+// WireWalkerKnownField describes one field the bitcoin wire walker
+// handles. The contract test in blockchain_bitcoin.stream_test.go
+// asserts that KnownBlockFields and KnownBitcoinBlobFields match the
+// proto descriptors exactly, in both directions.
+type WireWalkerKnownField struct {
+ Kind protoreflect.Kind // expected proto kind
+ Repeated bool // repeated (includes map)
+ Map bool // map
+ Oneof string // containing oneof name, empty if none
+ Process bool // walker decodes this field into the result
+}
+
+// KnownBlockFields enumerates every field the walker recognizes on
+// Block. See the contract test + switch-coverage test for the
+// drift-defense story.
+var KnownBlockFields = map[protoreflect.FieldNumber]WireWalkerKnownField{
+ blockFieldBlockchain: {Kind: protoreflect.EnumKind, Process: true},
+ blockFieldNetwork: {Kind: protoreflect.EnumKind, Process: true},
+ blockFieldMetadata: {Kind: protoreflect.MessageKind, Process: true},
+ blockFieldTransactionMetadata: {Kind: protoreflect.MessageKind, Process: true},
+ blockFieldSideChain: {Kind: protoreflect.EnumKind, Process: true},
+ blockFieldEthereum: {Kind: protoreflect.MessageKind, Oneof: "blobdata", Process: true},
+ blockFieldBitcoin: {Kind: protoreflect.MessageKind, Oneof: "blobdata", Process: true},
+ blockFieldRosetta: {Kind: protoreflect.MessageKind, Oneof: "blobdata", Process: true},
+ blockFieldSolana: {Kind: protoreflect.MessageKind, Oneof: "blobdata", Process: true},
+ blockFieldAptos: {Kind: protoreflect.MessageKind, Oneof: "blobdata", Process: true},
+ blockFieldEthereumBeacon: {Kind: protoreflect.MessageKind, Oneof: "blobdata", Process: true},
+}
+
+// KnownBitcoinBlobFields enumerates every field the walker recognizes
+// on BitcoinBlobdata.
+var KnownBitcoinBlobFields = map[protoreflect.FieldNumber]WireWalkerKnownField{
+ bitcoinBlobFieldHeader: {Kind: protoreflect.BytesKind, Process: true},
+ bitcoinBlobFieldInputTransactions: {Kind: protoreflect.MessageKind, Repeated: true, Process: true},
+ bitcoinBlobFieldRawInputTransactions: {Kind: protoreflect.MessageKind, Repeated: true, Map: true, Process: true},
+}
+
+// BitcoinChunkRef points at a byte range within the walked stream.
+// Zero-value means "not present" (Length == 0 distinguishes empty).
+type BitcoinChunkRef struct {
+ Offset int64
+ Length int64
+}
+
+// BitcoinChunks summarizes the byte ranges the walker skipped inside
+// BitcoinBlobdata so the caller can stream each chunk on demand
+// without retaining the whole blob in memory.
+type BitcoinChunks struct {
+ // Header is the range of BitcoinBlobdata.Header bytes.
+ Header BitcoinChunkRef
+ // InputTransactionsGroups is one entry per element of
+ // BitcoinBlobdata.InputTransactions (a RepeatedBytes message).
+ // Each range is the raw message bytes; the consumer can
+ // proto.Unmarshal them into a RepeatedBytes on demand.
+ InputTransactionsGroups []BitcoinChunkRef
+}
+
+// WalkBitcoinEnvelope reads a proto-encoded Block from r and returns
+// a *Block where BitcoinBlobdata.Header AND
+// BitcoinBlobdata.InputTransactions are left nil. The byte ranges of
+// those fields within the walked stream are returned via
+// BitcoinChunks so the caller can seek into a spooled copy of r and
+// load each chunk on demand.
+//
+// All other fields (Block metadata, other blobdata variants, and any
+// non-{Header, InputTransactions} fields on BitcoinBlobdata) are
+// decoded normally.
+//
+// Peak RAM during this call is bounded by the materialized parts of
+// the Block — which for bitcoin-family blocks is just metadata + the
+// tiny RawInputTransactions map if populated. The big InputTransactions
+// and Header fields are only streamed when the parser asks for them.
+//
+// Unknown field numbers on the wire return an error. See
+// Known{Block,BitcoinBlob}Fields and the contract + switch-coverage
+// tests for drift-defense guarantees.
+func WalkBitcoinEnvelope(r io.Reader) (*Block, BitcoinChunks, error) {
+ block := &Block{}
+ chunks := BitcoinChunks{}
+ w := &walker{br: bufio.NewReaderSize(r, 64*1024)}
+ blockMsg := block.ProtoReflect()
+ blockDesc := blockMsg.Descriptor()
+
+ for {
+ if _, err := w.br.Peek(1); err != nil {
+ if err == io.EOF {
+ break
+ }
+ return nil, BitcoinChunks{}, xerrors.Errorf("peek at top-level: %w", err)
+ }
+ tag, err := w.readVarint()
+ if err != nil {
+ return nil, BitcoinChunks{}, xerrors.Errorf("read top-level tag: %w", err)
+ }
+ fieldNum := protoreflect.FieldNumber(tag >> 3)
+ wireType := int(tag & 0x7)
+
+ fd := blockDesc.Fields().ByNumber(fieldNum)
+ if fd == nil {
+ return nil, BitcoinChunks{}, xerrors.Errorf("unknown Block field %d (wt=%d): proto changed; update KnownBlockFields + switch in blockchain_bitcoin.stream.go", fieldNum, wireType)
+ }
+
+ // Specialized: when descending into the bitcoin blob, track
+ // Header + InputTransactions group offsets instead of
+ // materializing them.
+ if fieldNum == blockFieldBitcoin {
+ length, err := w.readVarint()
+ if err != nil {
+ return nil, BitcoinChunks{}, xerrors.Errorf("read bitcoin length: %w", err)
+ }
+ blob := &BitcoinBlobdata{}
+ block.Blobdata = &Block_Bitcoin{Bitcoin: blob}
+ endPos := w.pos + int64(length)
+ blobChunks, err := w.walkBitcoinBlob(blob, endPos)
+ if err != nil {
+ return nil, BitcoinChunks{}, xerrors.Errorf("walk bitcoin blob: %w", err)
+ }
+ chunks = blobChunks
+ continue
+ }
+
+ // All other fields: delegate to the generic walker's
+ // reflection-driven decoder.
+ if err := w.decodeField(blockMsg, fd, wireType); err != nil {
+ return nil, BitcoinChunks{}, xerrors.Errorf("decode Block.%s: %w", fd.Name(), err)
+ }
+ }
+
+ return block, chunks, nil
+}
+
+// walkBitcoinBlob walks a BitcoinBlobdata sub-message. Header and
+// InputTransactions have their byte ranges recorded; every other
+// field is decoded normally via the generic walker.
+func (w *walker) walkBitcoinBlob(blob *BitcoinBlobdata, endPos int64) (BitcoinChunks, error) {
+ _ = blob // blob is populated lazily via chunks; no fields are decoded here
+ chunks := BitcoinChunks{}
+
+ for w.pos < endPos {
+ tag, err := w.readVarint()
+ if err != nil {
+ return BitcoinChunks{}, xerrors.Errorf("read bitcoin tag: %w", err)
+ }
+ fieldNum := protoreflect.FieldNumber(tag >> 3)
+ wireType := int(tag & 0x7)
+
+ switch fieldNum {
+ case bitcoinBlobFieldHeader:
+ length, err := w.readVarint()
+ if err != nil {
+ return BitcoinChunks{}, xerrors.Errorf("read header length: %w", err)
+ }
+ chunks.Header = BitcoinChunkRef{Offset: w.pos, Length: int64(length)}
+ if err := w.discardFast(int64(length)); err != nil {
+ return BitcoinChunks{}, xerrors.Errorf("discard header bytes: %w", err)
+ }
+ case bitcoinBlobFieldInputTransactions:
+ length, err := w.readVarint()
+ if err != nil {
+ return BitcoinChunks{}, xerrors.Errorf("read input_transactions length: %w", err)
+ }
+ chunks.InputTransactionsGroups = append(chunks.InputTransactionsGroups,
+ BitcoinChunkRef{Offset: w.pos, Length: int64(length)})
+ if err := w.discardFast(int64(length)); err != nil {
+ return BitcoinChunks{}, xerrors.Errorf("discard input_transactions bytes: %w", err)
+ }
+ case bitcoinBlobFieldRawInputTransactions:
+ // raw_input_transactions is a map storing
+ // duplicate raw-JSON copies of the prev-tx data. The
+ // bitcoin parser only reads input_transactions (field 2);
+ // materializing raw_input_transactions would double peak
+ // RAM on zcash (the two fields together are ~4 GB). Skip
+ // it unconditionally in the streaming path — if some
+ // future caller needs it, they can add a Chunks
+ // accessor.
+ length, err := w.readVarint()
+ if err != nil {
+ return BitcoinChunks{}, xerrors.Errorf("read raw_input_transactions length: %w", err)
+ }
+ if err := w.discardFast(int64(length)); err != nil {
+ return BitcoinChunks{}, xerrors.Errorf("discard raw_input_transactions bytes: %w", err)
+ }
+ default:
+ return BitcoinChunks{}, xerrors.Errorf("unknown BitcoinBlobdata field %d (wt=%d): proto changed; update KnownBitcoinBlobFields + switch in blockchain_bitcoin.stream.go", fieldNum, wireType)
+ }
+ }
+
+ if w.pos != endPos {
+ return BitcoinChunks{}, xerrors.Errorf("bitcoin blob consumed %d bytes, expected %d", w.pos, endPos)
+ }
+ return chunks, nil
+}
diff --git a/protos/coinbase/chainstorage/blockchain_bitcoin.stream_test.go b/protos/coinbase/chainstorage/blockchain_bitcoin.stream_test.go
new file mode 100644
index 00000000..65dc3520
--- /dev/null
+++ b/protos/coinbase/chainstorage/blockchain_bitcoin.stream_test.go
@@ -0,0 +1,209 @@
+package chainstorage_test
+
+import (
+ "bytes"
+ "io"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/utils/fixtures"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// TestWalkBitcoinEnvelope_Parity walks a hand-built block and asserts
+// that Header and InputTransactions are exposed via chunk offsets
+// (not materialized) while the rest of the block matches proto.Marshal
+// round-trip behavior.
+func TestWalkBitcoinEnvelope_Parity(t *testing.T) {
+ require := require.New(t)
+
+ original := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_BITCOIN,
+ Network: common.Network_NETWORK_BITCOIN_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Height: 696402,
+ Hash: "000000000000000000088a771bf9592a8bd3e9a5dc4c5a18876b65b283f0fb1e",
+ },
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: []byte(`{"hash":"000000","height":696402,"tx":[{"txid":"abc"}]}`),
+ InputTransactions: []*api.RepeatedBytes{
+ {Data: [][]byte{[]byte(`{"txid":"prev1"}`), []byte(`{"txid":"prev2"}`)}},
+ {Data: [][]byte{[]byte(`{"txid":"prev3"}`)}},
+ },
+ RawInputTransactions: map[string][]byte{
+ "tx1": []byte("raw1"),
+ },
+ },
+ },
+ }
+
+ raw, err := proto.Marshal(original)
+ require.NoError(err)
+
+ block, chunks, err := api.WalkBitcoinEnvelope(bytes.NewReader(raw))
+ require.NoError(err)
+
+ // Top-level fields decode normally.
+ require.Equal(original.Blockchain, block.Blockchain)
+ require.Equal(original.Network, block.Network)
+ require.Equal(original.Metadata.Tag, block.Metadata.Tag)
+ require.Equal(original.Metadata.Hash, block.Metadata.Hash)
+
+ // Blob: Header, InputTransactions, AND RawInputTransactions are
+ // all skipped by the walker — each is exposed lazily via a chunk
+ // ref or can be recomputed from InputTransactions. Skipping
+ // RawInputTransactions avoids doubling peak RAM on chains that
+ // duplicate data between the two fields (e.g. zcash).
+ blob := block.GetBitcoin()
+ require.NotNil(blob)
+ require.Nil(blob.Header, "header must be left nil — walker exposes it via chunks.Header")
+ require.Nil(blob.InputTransactions, "input_transactions must be left nil — walker exposes it via chunks.InputTransactionsGroups")
+ require.Nil(blob.RawInputTransactions, "raw_input_transactions must be left nil — walker skips it (currently unused by the streaming parser)")
+
+ // Header chunk: offset+length point at the header bytes.
+ require.Equal(int64(len(original.GetBitcoin().Header)), chunks.Header.Length)
+ require.Equal(original.GetBitcoin().Header, raw[chunks.Header.Offset:chunks.Header.Offset+chunks.Header.Length])
+
+ // Each InputTransactions group is a separate chunk reference. The
+ // bytes at its offset+length proto.Unmarshal to the original
+ // RepeatedBytes.
+ require.Equal(len(original.GetBitcoin().InputTransactions), len(chunks.InputTransactionsGroups))
+ for i, ref := range chunks.InputTransactionsGroups {
+ got := &api.RepeatedBytes{}
+ require.NoError(proto.Unmarshal(raw[ref.Offset:ref.Offset+ref.Length], got))
+ require.Equal(original.GetBitcoin().InputTransactions[i].Data, got.Data, "group %d mismatch", i)
+ }
+}
+
+// TestWalkBitcoinEnvelope_Fixture walks a real bitcoin block fixture.
+func TestWalkBitcoinEnvelope_Fixture(t *testing.T) {
+ require := require.New(t)
+
+ header, err := fixtures.ReadFile("parser/bitcoin/get_block.json")
+ require.NoError(err)
+ tx1, err := fixtures.ReadFile("parser/bitcoin/get_raw_transaction.json")
+ require.NoError(err)
+ tx2, err := fixtures.ReadFile("parser/bitcoin/get_raw_transaction_tx2.json")
+ require.NoError(err)
+
+ original := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_BITCOIN,
+ Network: common.Network_NETWORK_BITCOIN_MAINNET,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 696402},
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: header,
+ InputTransactions: []*api.RepeatedBytes{
+ {Data: [][]byte{}},
+ {Data: [][]byte{tx1, tx2}},
+ },
+ },
+ },
+ }
+ raw, err := proto.Marshal(original)
+ require.NoError(err)
+
+ block, chunks, err := api.WalkBitcoinEnvelope(bytes.NewReader(raw))
+ require.NoError(err)
+ require.Equal(int64(len(header)), chunks.Header.Length)
+ require.Equal(header, raw[chunks.Header.Offset:chunks.Header.Offset+chunks.Header.Length])
+ require.Nil(block.GetBitcoin().Header)
+ require.Nil(block.GetBitcoin().InputTransactions)
+ require.Equal(2, len(chunks.InputTransactionsGroups))
+
+ // Recover the 2nd group via its chunk ref.
+ got := &api.RepeatedBytes{}
+ ref := chunks.InputTransactionsGroups[1]
+ require.NoError(proto.Unmarshal(raw[ref.Offset:ref.Offset+ref.Length], got))
+ require.Equal([][]byte{tx1, tx2}, got.Data)
+}
+
+// TestWalkBitcoinEnvelope_UnknownBlockField ensures the walker errors
+// on a field number not registered in its known set.
+func TestWalkBitcoinEnvelope_UnknownBlockField(t *testing.T) {
+ require := require.New(t)
+
+ // tag = (77 << 3) | 0 = 616 → varint 0xE8, 0x04; value = 0x2A (42)
+ buf := []byte{0xE8, 0x04, 0x2A}
+ _, _, err := api.WalkBitcoinEnvelope(bytes.NewReader(buf))
+ require.Error(err)
+ require.Contains(err.Error(), "unknown Block field 77")
+}
+
+// TestWalkBitcoinEnvelope_UnknownBitcoinBlobField checks the same at
+// the BitcoinBlobdata level.
+func TestWalkBitcoinEnvelope_UnknownBitcoinBlobField(t *testing.T) {
+ require := require.New(t)
+
+ // Outer tag (field 101, wiretype 2) = 0xAA 0x06; length=3;
+ // inner: field 99 varint 1 = 0x98 0x06 0x01
+ buf := []byte{0xAA, 0x06, 0x03, 0x98, 0x06, 0x01}
+ _, _, err := api.WalkBitcoinEnvelope(bytes.NewReader(buf))
+ require.Error(err)
+ require.Contains(err.Error(), "unknown BitcoinBlobdata field 99")
+}
+
+// TestWalkBitcoinEnvelope_TeedToFile verifies the tee pattern: walker
+// reads from a tee, bytes land in a spool file, and we can reopen
+// the spool + seek to a chunk offset to recover the chunk.
+func TestWalkBitcoinEnvelope_TeedToFile(t *testing.T) {
+ require := require.New(t)
+
+ original := &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_BITCOIN,
+ Network: common.Network_NETWORK_BITCOIN_MAINNET,
+ Metadata: &api.BlockMetadata{Tag: 1, Height: 42},
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: []byte(`{"hash":"ab","tx":[{"txid":"xyz"}]}`),
+ InputTransactions: []*api.RepeatedBytes{
+ {Data: [][]byte{[]byte("prev1")}},
+ },
+ },
+ },
+ }
+ raw, err := proto.Marshal(original)
+ require.NoError(err)
+
+ spool, err := os.CreateTemp(t.TempDir(), "chainstorage-spool-*.bin")
+ require.NoError(err)
+ defer spool.Close()
+
+ tee := io.TeeReader(bytes.NewReader(raw), spool)
+ block, chunks, err := api.WalkBitcoinEnvelope(tee)
+ require.NoError(err)
+ require.NoError(spool.Sync())
+
+ // Recover Header by seeking into the spool file.
+ f, err := os.Open(spool.Name())
+ require.NoError(err)
+ defer f.Close()
+ _, err = f.Seek(chunks.Header.Offset, io.SeekStart)
+ require.NoError(err)
+ got, err := io.ReadAll(io.LimitReader(f, chunks.Header.Length))
+ require.NoError(err)
+ require.Equal(original.GetBitcoin().Header, got)
+
+ full, err := os.ReadFile(spool.Name())
+ require.NoError(err)
+ require.Equal(raw, full)
+
+ // Recover input_transactions[0] by seeking.
+ require.Equal(1, len(chunks.InputTransactionsGroups))
+ ref := chunks.InputTransactionsGroups[0]
+ groupBytes, err := os.ReadFile(spool.Name())
+ require.NoError(err)
+ gotGroup := &api.RepeatedBytes{}
+ require.NoError(proto.Unmarshal(groupBytes[ref.Offset:ref.Offset+ref.Length], gotGroup))
+ require.Equal([][]byte{[]byte("prev1")}, gotGroup.Data)
+
+ // Block's Bitcoin has neither Header nor InputTransactions.
+ require.Nil(block.GetBitcoin().Header)
+ require.Nil(block.GetBitcoin().InputTransactions)
+}
diff --git a/protos/coinbase/chainstorage/blockchain_ethereum.pb.go b/protos/coinbase/chainstorage/blockchain_ethereum.pb.go
index a7215ec2..0f3f2f18 100644
--- a/protos/coinbase/chainstorage/blockchain_ethereum.pb.go
+++ b/protos/coinbase/chainstorage/blockchain_ethereum.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/chainstorage/blockchain_ethereum.proto
package chainstorage
@@ -1203,6 +1203,38 @@ type EthereumTransactionReceipt struct {
//
// *EthereumTransactionReceipt_BlobGasUsed
OptionalBlobGasUsed isEthereumTransactionReceipt_OptionalBlobGasUsed `protobuf_oneof:"optional_blob_gas_used"`
+ // Types that are assignable to OptionalFee:
+ //
+ // *EthereumTransactionReceipt_Fee
+ OptionalFee isEthereumTransactionReceipt_OptionalFee `protobuf_oneof:"optional_fee"`
+ // Types that are assignable to OptionalNetFee:
+ //
+ // *EthereumTransactionReceipt_NetFee
+ OptionalNetFee isEthereumTransactionReceipt_OptionalNetFee `protobuf_oneof:"optional_net_fee"`
+ // Types that are assignable to OptionalNetUsage:
+ //
+ // *EthereumTransactionReceipt_NetUsage
+ OptionalNetUsage isEthereumTransactionReceipt_OptionalNetUsage `protobuf_oneof:"optional_net_usage"`
+ // Types that are assignable to OptionalEnergyUsage:
+ //
+ // *EthereumTransactionReceipt_EnergyUsage
+ OptionalEnergyUsage isEthereumTransactionReceipt_OptionalEnergyUsage `protobuf_oneof:"optional_energy_usage"`
+ // Types that are assignable to OptionalEnergyFee:
+ //
+ // *EthereumTransactionReceipt_EnergyFee
+ OptionalEnergyFee isEthereumTransactionReceipt_OptionalEnergyFee `protobuf_oneof:"optional_energy_fee"`
+ // Types that are assignable to OptionalOriginEnergyUsage:
+ //
+ // *EthereumTransactionReceipt_OriginEnergyUsage
+ OptionalOriginEnergyUsage isEthereumTransactionReceipt_OptionalOriginEnergyUsage `protobuf_oneof:"optional_origin_energy_usage"`
+ // Types that are assignable to OptionalEnergyUsageTotal:
+ //
+ // *EthereumTransactionReceipt_EnergyUsageTotal
+ OptionalEnergyUsageTotal isEthereumTransactionReceipt_OptionalEnergyUsageTotal `protobuf_oneof:"optional_energy_usage_total"`
+ // Types that are assignable to OptionalEnergyPenaltyTotal:
+ //
+ // *EthereumTransactionReceipt_EnergyPenaltyTotal
+ OptionalEnergyPenaltyTotal isEthereumTransactionReceipt_OptionalEnergyPenaltyTotal `protobuf_oneof:"optional_energy_penalty_total"`
}
func (x *EthereumTransactionReceipt) Reset() {
@@ -1419,6 +1451,118 @@ func (x *EthereumTransactionReceipt) GetBlobGasUsed() uint64 {
return 0
}
+func (m *EthereumTransactionReceipt) GetOptionalFee() isEthereumTransactionReceipt_OptionalFee {
+ if m != nil {
+ return m.OptionalFee
+ }
+ return nil
+}
+
+func (x *EthereumTransactionReceipt) GetFee() uint64 {
+ if x, ok := x.GetOptionalFee().(*EthereumTransactionReceipt_Fee); ok {
+ return x.Fee
+ }
+ return 0
+}
+
+func (m *EthereumTransactionReceipt) GetOptionalNetFee() isEthereumTransactionReceipt_OptionalNetFee {
+ if m != nil {
+ return m.OptionalNetFee
+ }
+ return nil
+}
+
+func (x *EthereumTransactionReceipt) GetNetFee() uint64 {
+ if x, ok := x.GetOptionalNetFee().(*EthereumTransactionReceipt_NetFee); ok {
+ return x.NetFee
+ }
+ return 0
+}
+
+func (m *EthereumTransactionReceipt) GetOptionalNetUsage() isEthereumTransactionReceipt_OptionalNetUsage {
+ if m != nil {
+ return m.OptionalNetUsage
+ }
+ return nil
+}
+
+func (x *EthereumTransactionReceipt) GetNetUsage() uint64 {
+ if x, ok := x.GetOptionalNetUsage().(*EthereumTransactionReceipt_NetUsage); ok {
+ return x.NetUsage
+ }
+ return 0
+}
+
+func (m *EthereumTransactionReceipt) GetOptionalEnergyUsage() isEthereumTransactionReceipt_OptionalEnergyUsage {
+ if m != nil {
+ return m.OptionalEnergyUsage
+ }
+ return nil
+}
+
+func (x *EthereumTransactionReceipt) GetEnergyUsage() uint64 {
+ if x, ok := x.GetOptionalEnergyUsage().(*EthereumTransactionReceipt_EnergyUsage); ok {
+ return x.EnergyUsage
+ }
+ return 0
+}
+
+func (m *EthereumTransactionReceipt) GetOptionalEnergyFee() isEthereumTransactionReceipt_OptionalEnergyFee {
+ if m != nil {
+ return m.OptionalEnergyFee
+ }
+ return nil
+}
+
+func (x *EthereumTransactionReceipt) GetEnergyFee() uint64 {
+ if x, ok := x.GetOptionalEnergyFee().(*EthereumTransactionReceipt_EnergyFee); ok {
+ return x.EnergyFee
+ }
+ return 0
+}
+
+func (m *EthereumTransactionReceipt) GetOptionalOriginEnergyUsage() isEthereumTransactionReceipt_OptionalOriginEnergyUsage {
+ if m != nil {
+ return m.OptionalOriginEnergyUsage
+ }
+ return nil
+}
+
+func (x *EthereumTransactionReceipt) GetOriginEnergyUsage() uint64 {
+ if x, ok := x.GetOptionalOriginEnergyUsage().(*EthereumTransactionReceipt_OriginEnergyUsage); ok {
+ return x.OriginEnergyUsage
+ }
+ return 0
+}
+
+func (m *EthereumTransactionReceipt) GetOptionalEnergyUsageTotal() isEthereumTransactionReceipt_OptionalEnergyUsageTotal {
+ if m != nil {
+ return m.OptionalEnergyUsageTotal
+ }
+ return nil
+}
+
+func (x *EthereumTransactionReceipt) GetEnergyUsageTotal() uint64 {
+ if x, ok := x.GetOptionalEnergyUsageTotal().(*EthereumTransactionReceipt_EnergyUsageTotal); ok {
+ return x.EnergyUsageTotal
+ }
+ return 0
+}
+
+func (m *EthereumTransactionReceipt) GetOptionalEnergyPenaltyTotal() isEthereumTransactionReceipt_OptionalEnergyPenaltyTotal {
+ if m != nil {
+ return m.OptionalEnergyPenaltyTotal
+ }
+ return nil
+}
+
+func (x *EthereumTransactionReceipt) GetEnergyPenaltyTotal() uint64 {
+ if x, ok := x.GetOptionalEnergyPenaltyTotal().(*EthereumTransactionReceipt_EnergyPenaltyTotal); ok {
+ return x.EnergyPenaltyTotal
+ }
+ return 0
+}
+
type isEthereumTransactionReceipt_OptionalStatus interface {
isEthereumTransactionReceipt_OptionalStatus()
}
@@ -1480,6 +1624,89 @@ type EthereumTransactionReceipt_BlobGasUsed struct {
func (*EthereumTransactionReceipt_BlobGasUsed) isEthereumTransactionReceipt_OptionalBlobGasUsed() {}
+type isEthereumTransactionReceipt_OptionalFee interface {
+ isEthereumTransactionReceipt_OptionalFee()
+}
+
+type EthereumTransactionReceipt_Fee struct {
+ Fee uint64 `protobuf:"varint,22,opt,name=fee,proto3,oneof"`
+}
+
+func (*EthereumTransactionReceipt_Fee) isEthereumTransactionReceipt_OptionalFee() {}
+
+type isEthereumTransactionReceipt_OptionalNetFee interface {
+ isEthereumTransactionReceipt_OptionalNetFee()
+}
+
+type EthereumTransactionReceipt_NetFee struct {
+ NetFee uint64 `protobuf:"varint,23,opt,name=net_fee,json=netFee,proto3,oneof"`
+}
+
+func (*EthereumTransactionReceipt_NetFee) isEthereumTransactionReceipt_OptionalNetFee() {}
+
+type isEthereumTransactionReceipt_OptionalNetUsage interface {
+ isEthereumTransactionReceipt_OptionalNetUsage()
+}
+
+type EthereumTransactionReceipt_NetUsage struct {
+ NetUsage uint64 `protobuf:"varint,24,opt,name=net_usage,json=netUsage,proto3,oneof"`
+}
+
+func (*EthereumTransactionReceipt_NetUsage) isEthereumTransactionReceipt_OptionalNetUsage() {}
+
+type isEthereumTransactionReceipt_OptionalEnergyUsage interface {
+ isEthereumTransactionReceipt_OptionalEnergyUsage()
+}
+
+type EthereumTransactionReceipt_EnergyUsage struct {
+ EnergyUsage uint64 `protobuf:"varint,25,opt,name=energy_usage,json=energyUsage,proto3,oneof"`
+}
+
+func (*EthereumTransactionReceipt_EnergyUsage) isEthereumTransactionReceipt_OptionalEnergyUsage() {}
+
+type isEthereumTransactionReceipt_OptionalEnergyFee interface {
+ isEthereumTransactionReceipt_OptionalEnergyFee()
+}
+
+type EthereumTransactionReceipt_EnergyFee struct {
+ EnergyFee uint64 `protobuf:"varint,26,opt,name=energy_fee,json=energyFee,proto3,oneof"`
+}
+
+func (*EthereumTransactionReceipt_EnergyFee) isEthereumTransactionReceipt_OptionalEnergyFee() {}
+
+type isEthereumTransactionReceipt_OptionalOriginEnergyUsage interface {
+ isEthereumTransactionReceipt_OptionalOriginEnergyUsage()
+}
+
+type EthereumTransactionReceipt_OriginEnergyUsage struct {
+ OriginEnergyUsage uint64 `protobuf:"varint,27,opt,name=origin_energy_usage,json=originEnergyUsage,proto3,oneof"`
+}
+
+func (*EthereumTransactionReceipt_OriginEnergyUsage) isEthereumTransactionReceipt_OptionalOriginEnergyUsage() {
+}
+
+type isEthereumTransactionReceipt_OptionalEnergyUsageTotal interface {
+ isEthereumTransactionReceipt_OptionalEnergyUsageTotal()
+}
+
+type EthereumTransactionReceipt_EnergyUsageTotal struct {
+ EnergyUsageTotal uint64 `protobuf:"varint,28,opt,name=energy_usage_total,json=energyUsageTotal,proto3,oneof"`
+}
+
+func (*EthereumTransactionReceipt_EnergyUsageTotal) isEthereumTransactionReceipt_OptionalEnergyUsageTotal() {
+}
+
+type isEthereumTransactionReceipt_OptionalEnergyPenaltyTotal interface {
+ isEthereumTransactionReceipt_OptionalEnergyPenaltyTotal()
+}
+
+type EthereumTransactionReceipt_EnergyPenaltyTotal struct {
+ EnergyPenaltyTotal uint64 `protobuf:"varint,29,opt,name=energy_penalty_total,json=energyPenaltyTotal,proto3,oneof"`
+}
+
+func (*EthereumTransactionReceipt_EnergyPenaltyTotal) isEthereumTransactionReceipt_OptionalEnergyPenaltyTotal() {
+}
+
type EthereumEventLog struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1715,25 +1942,26 @@ type EthereumTransactionFlattenedTrace struct {
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
- Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
- Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
- From string `protobuf:"bytes,3,opt,name=from,proto3" json:"from,omitempty"`
- To string `protobuf:"bytes,4,opt,name=to,proto3" json:"to,omitempty"`
- Value string `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"`
- Gas uint64 `protobuf:"varint,6,opt,name=gas,proto3" json:"gas,omitempty"`
- GasUsed uint64 `protobuf:"varint,7,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"`
- Input string `protobuf:"bytes,8,opt,name=input,proto3" json:"input,omitempty"`
- Output string `protobuf:"bytes,9,opt,name=output,proto3" json:"output,omitempty"`
- Subtraces uint64 `protobuf:"varint,10,opt,name=subtraces,proto3" json:"subtraces,omitempty"`
- TraceAddress []uint64 `protobuf:"varint,11,rep,packed,name=trace_address,json=traceAddress,proto3" json:"trace_address,omitempty"`
- TraceType string `protobuf:"bytes,12,opt,name=trace_type,json=traceType,proto3" json:"trace_type,omitempty"`
- CallType string `protobuf:"bytes,13,opt,name=call_type,json=callType,proto3" json:"call_type,omitempty"`
- TraceId string `protobuf:"bytes,14,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"`
- Status uint64 `protobuf:"varint,15,opt,name=status,proto3" json:"status,omitempty"`
- BlockHash string `protobuf:"bytes,16,opt,name=block_hash,json=blockHash,proto3" json:"block_hash,omitempty"`
- BlockNumber uint64 `protobuf:"varint,17,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"`
- TransactionHash string `protobuf:"bytes,18,opt,name=transaction_hash,json=transactionHash,proto3" json:"transaction_hash,omitempty"`
- TransactionIndex uint64 `protobuf:"varint,19,opt,name=transaction_index,json=transactionIndex,proto3" json:"transaction_index,omitempty"`
+ Error string `protobuf:"bytes,1,opt,name=error,proto3" json:"error,omitempty"`
+ Type string `protobuf:"bytes,2,opt,name=type,proto3" json:"type,omitempty"`
+ From string `protobuf:"bytes,3,opt,name=from,proto3" json:"from,omitempty"`
+ To string `protobuf:"bytes,4,opt,name=to,proto3" json:"to,omitempty"`
+ Value string `protobuf:"bytes,5,opt,name=value,proto3" json:"value,omitempty"`
+ Gas uint64 `protobuf:"varint,6,opt,name=gas,proto3" json:"gas,omitempty"`
+ GasUsed uint64 `protobuf:"varint,7,opt,name=gas_used,json=gasUsed,proto3" json:"gas_used,omitempty"`
+ Input string `protobuf:"bytes,8,opt,name=input,proto3" json:"input,omitempty"`
+ Output string `protobuf:"bytes,9,opt,name=output,proto3" json:"output,omitempty"`
+ Subtraces uint64 `protobuf:"varint,10,opt,name=subtraces,proto3" json:"subtraces,omitempty"`
+ TraceAddress []uint64 `protobuf:"varint,11,rep,packed,name=trace_address,json=traceAddress,proto3" json:"trace_address,omitempty"`
+ TraceType string `protobuf:"bytes,12,opt,name=trace_type,json=traceType,proto3" json:"trace_type,omitempty"`
+ CallType string `protobuf:"bytes,13,opt,name=call_type,json=callType,proto3" json:"call_type,omitempty"`
+ TraceId string `protobuf:"bytes,14,opt,name=trace_id,json=traceId,proto3" json:"trace_id,omitempty"`
+ Status uint64 `protobuf:"varint,15,opt,name=status,proto3" json:"status,omitempty"`
+ BlockHash string `protobuf:"bytes,16,opt,name=block_hash,json=blockHash,proto3" json:"block_hash,omitempty"`
+ BlockNumber uint64 `protobuf:"varint,17,opt,name=block_number,json=blockNumber,proto3" json:"block_number,omitempty"`
+ TransactionHash string `protobuf:"bytes,18,opt,name=transaction_hash,json=transactionHash,proto3" json:"transaction_hash,omitempty"`
+ TransactionIndex uint64 `protobuf:"varint,19,opt,name=transaction_index,json=transactionIndex,proto3" json:"transaction_index,omitempty"`
+ CallValueInfo []*CallValueInfo `protobuf:"bytes,20,rep,name=call_value_info,json=callValueInfo,proto3" json:"call_value_info,omitempty"`
}
func (x *EthereumTransactionFlattenedTrace) Reset() {
@@ -1901,6 +2129,13 @@ func (x *EthereumTransactionFlattenedTrace) GetTransactionIndex() uint64 {
return 0
}
+func (x *EthereumTransactionFlattenedTrace) GetCallValueInfo() []*CallValueInfo {
+ if x != nil {
+ return x.CallValueInfo
+ }
+ return nil
+}
+
type EthereumTokenTransfer struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -1919,6 +2154,7 @@ type EthereumTokenTransfer struct {
//
// *EthereumTokenTransfer_Erc20
// *EthereumTokenTransfer_Erc721
+ // *EthereumTokenTransfer_Src20
TokenTransfer isEthereumTokenTransfer_TokenTransfer `protobuf_oneof:"token_transfer"`
}
@@ -2038,6 +2274,13 @@ func (x *EthereumTokenTransfer) GetErc721() *ERC721TokenTransfer {
return nil
}
+func (x *EthereumTokenTransfer) GetSrc20() *SRC20TokenTransfer {
+ if x, ok := x.GetTokenTransfer().(*EthereumTokenTransfer_Src20); ok {
+ return x.Src20
+ }
+ return nil
+}
+
type isEthereumTokenTransfer_TokenTransfer interface {
isEthereumTokenTransfer_TokenTransfer()
}
@@ -2050,10 +2293,16 @@ type EthereumTokenTransfer_Erc721 struct {
Erc721 *ERC721TokenTransfer `protobuf:"bytes,101,opt,name=erc721,proto3,oneof"`
}
+type EthereumTokenTransfer_Src20 struct {
+ Src20 *SRC20TokenTransfer `protobuf:"bytes,102,opt,name=src20,proto3,oneof"`
+}
+
func (*EthereumTokenTransfer_Erc20) isEthereumTokenTransfer_TokenTransfer() {}
func (*EthereumTokenTransfer_Erc721) isEthereumTokenTransfer_TokenTransfer() {}
+func (*EthereumTokenTransfer_Src20) isEthereumTokenTransfer_TokenTransfer() {}
+
type ERC20TokenTransfer struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -2180,6 +2429,69 @@ func (x *ERC721TokenTransfer) GetTokenId() string {
return ""
}
+type SRC20TokenTransfer struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ FromAddress string `protobuf:"bytes,1,opt,name=from_address,json=fromAddress,proto3" json:"from_address,omitempty"`
+ ToAddress string `protobuf:"bytes,2,opt,name=to_address,json=toAddress,proto3" json:"to_address,omitempty"`
+ Value string `protobuf:"bytes,3,opt,name=value,proto3" json:"value,omitempty"`
+}
+
+func (x *SRC20TokenTransfer) Reset() {
+ *x = SRC20TokenTransfer{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[15]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *SRC20TokenTransfer) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SRC20TokenTransfer) ProtoMessage() {}
+
+func (x *SRC20TokenTransfer) ProtoReflect() protoreflect.Message {
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[15]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SRC20TokenTransfer.ProtoReflect.Descriptor instead.
+func (*SRC20TokenTransfer) Descriptor() ([]byte, []int) {
+ return file_coinbase_chainstorage_blockchain_ethereum_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *SRC20TokenTransfer) GetFromAddress() string {
+ if x != nil {
+ return x.FromAddress
+ }
+ return ""
+}
+
+func (x *SRC20TokenTransfer) GetToAddress() string {
+ if x != nil {
+ return x.ToAddress
+ }
+ return ""
+}
+
+func (x *SRC20TokenTransfer) GetValue() string {
+ if x != nil {
+ return x.Value
+ }
+ return ""
+}
+
type EthereumAccountStateProof struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -2191,7 +2503,7 @@ type EthereumAccountStateProof struct {
func (x *EthereumAccountStateProof) Reset() {
*x = EthereumAccountStateProof{}
if protoimpl.UnsafeEnabled {
- mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[15]
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[16]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2204,7 +2516,7 @@ func (x *EthereumAccountStateProof) String() string {
func (*EthereumAccountStateProof) ProtoMessage() {}
func (x *EthereumAccountStateProof) ProtoReflect() protoreflect.Message {
- mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[15]
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[16]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2217,7 +2529,7 @@ func (x *EthereumAccountStateProof) ProtoReflect() protoreflect.Message {
// Deprecated: Use EthereumAccountStateProof.ProtoReflect.Descriptor instead.
func (*EthereumAccountStateProof) Descriptor() ([]byte, []int) {
- return file_coinbase_chainstorage_blockchain_ethereum_proto_rawDescGZIP(), []int{15}
+ return file_coinbase_chainstorage_blockchain_ethereum_proto_rawDescGZIP(), []int{16}
}
func (x *EthereumAccountStateProof) GetAccountProof() []byte {
@@ -2238,7 +2550,7 @@ type EthereumExtraInput struct {
func (x *EthereumExtraInput) Reset() {
*x = EthereumExtraInput{}
if protoimpl.UnsafeEnabled {
- mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[16]
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[17]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2251,7 +2563,7 @@ func (x *EthereumExtraInput) String() string {
func (*EthereumExtraInput) ProtoMessage() {}
func (x *EthereumExtraInput) ProtoReflect() protoreflect.Message {
- mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[16]
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[17]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2264,7 +2576,7 @@ func (x *EthereumExtraInput) ProtoReflect() protoreflect.Message {
// Deprecated: Use EthereumExtraInput.ProtoReflect.Descriptor instead.
func (*EthereumExtraInput) Descriptor() ([]byte, []int) {
- return file_coinbase_chainstorage_blockchain_ethereum_proto_rawDescGZIP(), []int{16}
+ return file_coinbase_chainstorage_blockchain_ethereum_proto_rawDescGZIP(), []int{17}
}
func (x *EthereumExtraInput) GetErc20Contract() string {
@@ -2287,7 +2599,7 @@ type EthereumAccountStateResponse struct {
func (x *EthereumAccountStateResponse) Reset() {
*x = EthereumAccountStateResponse{}
if protoimpl.UnsafeEnabled {
- mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[17]
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[18]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2300,7 +2612,7 @@ func (x *EthereumAccountStateResponse) String() string {
func (*EthereumAccountStateResponse) ProtoMessage() {}
func (x *EthereumAccountStateResponse) ProtoReflect() protoreflect.Message {
- mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[17]
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[18]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2313,7 +2625,7 @@ func (x *EthereumAccountStateResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use EthereumAccountStateResponse.ProtoReflect.Descriptor instead.
func (*EthereumAccountStateResponse) Descriptor() ([]byte, []int) {
- return file_coinbase_chainstorage_blockchain_ethereum_proto_rawDescGZIP(), []int{17}
+ return file_coinbase_chainstorage_blockchain_ethereum_proto_rawDescGZIP(), []int{18}
}
func (x *EthereumAccountStateResponse) GetNonce() uint64 {
@@ -2337,6 +2649,61 @@ func (x *EthereumAccountStateResponse) GetCodeHash() string {
return ""
}
+type CallValueInfo struct {
+ state protoimpl.MessageState
+ sizeCache protoimpl.SizeCache
+ unknownFields protoimpl.UnknownFields
+
+ TokenId string `protobuf:"bytes,1,opt,name=token_id,json=tokenId,proto3" json:"token_id,omitempty"`
+ CallValue int64 `protobuf:"varint,2,opt,name=call_value,json=callValue,proto3" json:"call_value,omitempty"`
+}
+
+func (x *CallValueInfo) Reset() {
+ *x = CallValueInfo{}
+ if protoimpl.UnsafeEnabled {
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[19]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+ }
+}
+
+func (x *CallValueInfo) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CallValueInfo) ProtoMessage() {}
+
+func (x *CallValueInfo) ProtoReflect() protoreflect.Message {
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[19]
+ if protoimpl.UnsafeEnabled && x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CallValueInfo.ProtoReflect.Descriptor instead.
+func (*CallValueInfo) Descriptor() ([]byte, []int) {
+ return file_coinbase_chainstorage_blockchain_ethereum_proto_rawDescGZIP(), []int{19}
+}
+
+func (x *CallValueInfo) GetTokenId() string {
+ if x != nil {
+ return x.TokenId
+ }
+ return ""
+}
+
+func (x *CallValueInfo) GetCallValue() int64 {
+ if x != nil {
+ return x.CallValue
+ }
+ return 0
+}
+
type EthereumTransactionReceipt_L1FeeInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -2351,7 +2718,7 @@ type EthereumTransactionReceipt_L1FeeInfo struct {
func (x *EthereumTransactionReceipt_L1FeeInfo) Reset() {
*x = EthereumTransactionReceipt_L1FeeInfo{}
if protoimpl.UnsafeEnabled {
- mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[18]
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[20]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -2364,7 +2731,7 @@ func (x *EthereumTransactionReceipt_L1FeeInfo) String() string {
func (*EthereumTransactionReceipt_L1FeeInfo) ProtoMessage() {}
func (x *EthereumTransactionReceipt_L1FeeInfo) ProtoReflect() protoreflect.Message {
- mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[18]
+ mi := &file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[20]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -2626,7 +2993,7 @@ var file_coinbase_chainstorage_blockchain_ethereum_proto_rawDesc = []byte{
0x5f, 0x6d, 0x69, 0x6e, 0x74, 0x42, 0x13, 0x0a, 0x11, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61,
0x6c, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x69, 0x64, 0x42, 0x1f, 0x0a, 0x1d, 0x6f, 0x70,
0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x6d, 0x61, 0x78, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x70,
- 0x65, 0x72, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x67, 0x61, 0x73, 0x22, 0xdb, 0x08, 0x0a, 0x1a,
+ 0x65, 0x72, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x67, 0x61, 0x73, 0x22, 0xcf, 0x0c, 0x0a, 0x1a,
0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74,
0x69, 0x6f, 0x6e, 0x52, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72,
0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x01,
@@ -2677,46 +3044,96 @@ var file_coinbase_chainstorage_blockchain_ethereum_proto_rawDesc = []byte{
0x20, 0x01, 0x28, 0x04, 0x48, 0x04, 0x52, 0x0c, 0x62, 0x6c, 0x6f, 0x62, 0x47, 0x61, 0x73, 0x50,
0x72, 0x69, 0x63, 0x65, 0x12, 0x24, 0x0a, 0x0d, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x67, 0x61, 0x73,
0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x15, 0x20, 0x01, 0x28, 0x04, 0x48, 0x05, 0x52, 0x0b, 0x62,
- 0x6c, 0x6f, 0x62, 0x47, 0x61, 0x73, 0x55, 0x73, 0x65, 0x64, 0x1a, 0x88, 0x01, 0x0a, 0x09, 0x4c,
- 0x31, 0x46, 0x65, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x1e, 0x0a, 0x0b, 0x6c, 0x31, 0x5f, 0x67,
- 0x61, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6c,
- 0x31, 0x47, 0x61, 0x73, 0x55, 0x73, 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x6c, 0x31, 0x5f, 0x67,
- 0x61, 0x73, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a,
- 0x6c, 0x31, 0x47, 0x61, 0x73, 0x50, 0x72, 0x69, 0x63, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x31,
- 0x5f, 0x66, 0x65, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6c, 0x31, 0x46, 0x65,
- 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x6c, 0x31, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x63, 0x61, 0x6c,
- 0x61, 0x72, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x6c, 0x31, 0x46, 0x65, 0x65, 0x53,
- 0x63, 0x61, 0x6c, 0x61, 0x72, 0x42, 0x11, 0x0a, 0x0f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61,
- 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x42, 0x16, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69,
- 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x6c, 0x31, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f,
- 0x42, 0x18, 0x0a, 0x16, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x70,
- 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x42, 0x22, 0x0a, 0x20, 0x6f, 0x70,
- 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x72,
- 0x65, 0x63, 0x65, 0x69, 0x70, 0x74, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x19,
- 0x0a, 0x17, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x5f,
- 0x67, 0x61, 0x73, 0x5f, 0x70, 0x72, 0x69, 0x63, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x6f, 0x70, 0x74,
- 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x67, 0x61, 0x73, 0x5f, 0x75,
- 0x73, 0x65, 0x64, 0x4a, 0x04, 0x08, 0x0d, 0x10, 0x0e, 0x22, 0xa9, 0x02, 0x0a, 0x10, 0x45, 0x74,
- 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4c, 0x6f, 0x67, 0x12, 0x18,
- 0x0a, 0x07, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52,
- 0x07, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x6f, 0x67, 0x5f,
- 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c, 0x6f, 0x67,
- 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
- 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x61, 0x73, 0x68,
- 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
- 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x74, 0x72, 0x61,
- 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1d, 0x0a,
- 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x05, 0x20, 0x01, 0x28,
- 0x09, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x21, 0x0a, 0x0c,
- 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x06, 0x20, 0x01,
- 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12,
- 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09,
- 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74,
- 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x16, 0x0a,
- 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28, 0x09, 0x52, 0x06, 0x74,
- 0x6f, 0x70, 0x69, 0x63, 0x73, 0x22, 0xa0, 0x02, 0x0a, 0x18, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65,
- 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x61,
+ 0x6c, 0x6f, 0x62, 0x47, 0x61, 0x73, 0x55, 0x73, 0x65, 0x64, 0x12, 0x12, 0x0a, 0x03, 0x66, 0x65,
+ 0x65, 0x18, 0x16, 0x20, 0x01, 0x28, 0x04, 0x48, 0x06, 0x52, 0x03, 0x66, 0x65, 0x65, 0x12, 0x19,
+ 0x0a, 0x07, 0x6e, 0x65, 0x74, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x17, 0x20, 0x01, 0x28, 0x04, 0x48,
+ 0x07, 0x52, 0x06, 0x6e, 0x65, 0x74, 0x46, 0x65, 0x65, 0x12, 0x1d, 0x0a, 0x09, 0x6e, 0x65, 0x74,
+ 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x18, 0x18, 0x20, 0x01, 0x28, 0x04, 0x48, 0x08, 0x52, 0x08,
+ 0x6e, 0x65, 0x74, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x23, 0x0a, 0x0c, 0x65, 0x6e, 0x65, 0x72,
+ 0x67, 0x79, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x18, 0x19, 0x20, 0x01, 0x28, 0x04, 0x48, 0x09,
+ 0x52, 0x0b, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1f, 0x0a,
+ 0x0a, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x1a, 0x20, 0x01, 0x28,
+ 0x04, 0x48, 0x0a, 0x52, 0x09, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x46, 0x65, 0x65, 0x12, 0x30,
+ 0x0a, 0x13, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x5f, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x5f,
+ 0x75, 0x73, 0x61, 0x67, 0x65, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x04, 0x48, 0x0b, 0x52, 0x11, 0x6f,
+ 0x72, 0x69, 0x67, 0x69, 0x6e, 0x45, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65,
+ 0x12, 0x2e, 0x0a, 0x12, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65,
+ 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x04, 0x48, 0x0c, 0x52, 0x10,
+ 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x55, 0x73, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x74, 0x61, 0x6c,
+ 0x12, 0x32, 0x0a, 0x14, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x5f, 0x70, 0x65, 0x6e, 0x61, 0x6c,
+ 0x74, 0x79, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x1d, 0x20, 0x01, 0x28, 0x04, 0x48, 0x0d,
+ 0x52, 0x12, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x50, 0x65, 0x6e, 0x61, 0x6c, 0x74, 0x79, 0x54,
+ 0x6f, 0x74, 0x61, 0x6c, 0x1a, 0x88, 0x01, 0x0a, 0x09, 0x4c, 0x31, 0x46, 0x65, 0x65, 0x49, 0x6e,
+ 0x66, 0x6f, 0x12, 0x1e, 0x0a, 0x0b, 0x6c, 0x31, 0x5f, 0x67, 0x61, 0x73, 0x5f, 0x75, 0x73, 0x65,
+ 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x6c, 0x31, 0x47, 0x61, 0x73, 0x55, 0x73,
+ 0x65, 0x64, 0x12, 0x20, 0x0a, 0x0c, 0x6c, 0x31, 0x5f, 0x67, 0x61, 0x73, 0x5f, 0x70, 0x72, 0x69,
+ 0x63, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a, 0x6c, 0x31, 0x47, 0x61, 0x73, 0x50,
+ 0x72, 0x69, 0x63, 0x65, 0x12, 0x15, 0x0a, 0x06, 0x6c, 0x31, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x03,
+ 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6c, 0x31, 0x46, 0x65, 0x65, 0x12, 0x22, 0x0a, 0x0d, 0x6c,
+ 0x31, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x73, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x18, 0x04, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x0b, 0x6c, 0x31, 0x46, 0x65, 0x65, 0x53, 0x63, 0x61, 0x6c, 0x61, 0x72, 0x42,
+ 0x11, 0x0a, 0x0f, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x74, 0x61, 0x74,
+ 0x75, 0x73, 0x42, 0x16, 0x0a, 0x14, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x6c,
+ 0x31, 0x5f, 0x66, 0x65, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f, 0x42, 0x18, 0x0a, 0x16, 0x6f, 0x70,
+ 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x6e,
+ 0x6f, 0x6e, 0x63, 0x65, 0x42, 0x22, 0x0a, 0x20, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c,
+ 0x5f, 0x64, 0x65, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x5f, 0x72, 0x65, 0x63, 0x65, 0x69, 0x70, 0x74,
+ 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x42, 0x19, 0x0a, 0x17, 0x6f, 0x70, 0x74, 0x69,
+ 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x67, 0x61, 0x73, 0x5f, 0x70, 0x72,
+ 0x69, 0x63, 0x65, 0x42, 0x18, 0x0a, 0x16, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f,
+ 0x62, 0x6c, 0x6f, 0x62, 0x5f, 0x67, 0x61, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x42, 0x0e, 0x0a,
+ 0x0c, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x66, 0x65, 0x65, 0x42, 0x12, 0x0a,
+ 0x10, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x6e, 0x65, 0x74, 0x5f, 0x66, 0x65,
+ 0x65, 0x42, 0x14, 0x0a, 0x12, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x6e, 0x65,
+ 0x74, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x42, 0x17, 0x0a, 0x15, 0x6f, 0x70, 0x74, 0x69, 0x6f,
+ 0x6e, 0x61, 0x6c, 0x5f, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65,
+ 0x42, 0x15, 0x0a, 0x13, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x61, 0x6c, 0x5f, 0x65, 0x6e, 0x65,
+ 0x72, 0x67, 0x79, 0x5f, 0x66, 0x65, 0x65, 0x42, 0x1e, 0x0a, 0x1c, 0x6f, 0x70, 0x74, 0x69, 0x6f,
+ 0x6e, 0x61, 0x6c, 0x5f, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x5f, 0x65, 0x6e, 0x65, 0x72, 0x67,
+ 0x79, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65, 0x42, 0x1d, 0x0a, 0x1b, 0x6f, 0x70, 0x74, 0x69, 0x6f,
+ 0x6e, 0x61, 0x6c, 0x5f, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x5f, 0x75, 0x73, 0x61, 0x67, 0x65,
+ 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x1f, 0x0a, 0x1d, 0x6f, 0x70, 0x74, 0x69, 0x6f, 0x6e,
+ 0x61, 0x6c, 0x5f, 0x65, 0x6e, 0x65, 0x72, 0x67, 0x79, 0x5f, 0x70, 0x65, 0x6e, 0x61, 0x6c, 0x74,
+ 0x79, 0x5f, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x4a, 0x04, 0x08, 0x0d, 0x10, 0x0e, 0x22, 0xa9, 0x02,
+ 0x0a, 0x10, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x45, 0x76, 0x65, 0x6e, 0x74, 0x4c,
+ 0x6f, 0x67, 0x12, 0x18, 0x0a, 0x07, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x08, 0x52, 0x07, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x64, 0x12, 0x1b, 0x0a, 0x09,
+ 0x6c, 0x6f, 0x67, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52,
+ 0x08, 0x6c, 0x6f, 0x67, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72, 0x61,
+ 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+ 0x48, 0x61, 0x73, 0x68, 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74,
+ 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x04, 0x20, 0x01, 0x28, 0x04, 0x52,
+ 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65,
+ 0x78, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18,
+ 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68,
+ 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72,
+ 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d,
+ 0x62, 0x65, 0x72, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x07,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x12, 0x0a,
+ 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x64, 0x61, 0x74,
+ 0x61, 0x12, 0x16, 0x0a, 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x18, 0x09, 0x20, 0x03, 0x28,
+ 0x09, 0x52, 0x06, 0x74, 0x6f, 0x70, 0x69, 0x63, 0x73, 0x22, 0xa0, 0x02, 0x0a, 0x18, 0x45, 0x74,
+ 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
+ 0x6e, 0x54, 0x72, 0x61, 0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04,
+ 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65,
+ 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04,
+ 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x02, 0x74, 0x6f, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x61,
+ 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x67, 0x61, 0x73, 0x12, 0x19, 0x0a, 0x08,
+ 0x67, 0x61, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07,
+ 0x67, 0x61, 0x73, 0x55, 0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74,
+ 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x16, 0x0a,
+ 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f,
+ 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x45, 0x0a, 0x05, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x18, 0x0a,
+ 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e,
+ 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x74, 0x68,
+ 0x65, 0x72, 0x65, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
+ 0x54, 0x72, 0x61, 0x63, 0x65, 0x52, 0x05, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x22, 0xfc, 0x04, 0x0a,
+ 0x21, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
+ 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x6c, 0x61, 0x74, 0x74, 0x65, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61,
0x63, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65,
0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04,
@@ -2729,111 +3146,112 @@ var file_coinbase_chainstorage_blockchain_ethereum_proto_rawDesc = []byte{
0x73, 0x65, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x08, 0x20, 0x01,
0x28, 0x09, 0x52, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74,
0x70, 0x75, 0x74, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75,
- 0x74, 0x12, 0x45, 0x0a, 0x05, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b,
- 0x32, 0x2f, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69,
- 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75,
- 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x72, 0x61, 0x63,
- 0x65, 0x52, 0x05, 0x63, 0x61, 0x6c, 0x6c, 0x73, 0x22, 0xae, 0x04, 0x0a, 0x21, 0x45, 0x74, 0x68,
- 0x65, 0x72, 0x65, 0x75, 0x6d, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
- 0x46, 0x6c, 0x61, 0x74, 0x74, 0x65, 0x6e, 0x65, 0x64, 0x54, 0x72, 0x61, 0x63, 0x65, 0x12, 0x14,
- 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65,
- 0x72, 0x72, 0x6f, 0x72, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x04, 0x74, 0x79, 0x70, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d,
- 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02,
- 0x74, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x12, 0x14, 0x0a, 0x05,
- 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c,
- 0x75, 0x65, 0x12, 0x10, 0x0a, 0x03, 0x67, 0x61, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x04, 0x52,
- 0x03, 0x67, 0x61, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x67, 0x61, 0x73, 0x5f, 0x75, 0x73, 0x65, 0x64,
- 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x67, 0x61, 0x73, 0x55, 0x73, 0x65, 0x64, 0x12,
- 0x14, 0x0a, 0x05, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
- 0x69, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x18,
- 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x6f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x12, 0x1c, 0x0a,
- 0x09, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x65, 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x04,
- 0x52, 0x09, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x65, 0x73, 0x12, 0x23, 0x0a, 0x0d, 0x74,
- 0x72, 0x61, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x0b, 0x20, 0x03,
- 0x28, 0x04, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x63, 0x65, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
- 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0c,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x72, 0x61, 0x63, 0x65, 0x54, 0x79, 0x70, 0x65, 0x12,
- 0x1b, 0x0a, 0x09, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x0d, 0x20, 0x01,
- 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x6c, 0x6c, 0x54, 0x79, 0x70, 0x65, 0x12, 0x19, 0x0a, 0x08,
- 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07,
- 0x74, 0x72, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75,
- 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12,
- 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x10, 0x20,
- 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x21,
- 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x11,
- 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65,
- 0x72, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e,
- 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x72, 0x61,
- 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x61, 0x73, 0x68, 0x12, 0x2b, 0x0a, 0x11,
- 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x6e, 0x64, 0x65,
- 0x78, 0x18, 0x13, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
- 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x22, 0xe6, 0x03, 0x0a, 0x15, 0x45, 0x74,
- 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73,
- 0x66, 0x65, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x61, 0x64, 0x64,
- 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f, 0x6b, 0x65,
- 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d,
- 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
- 0x66, 0x72, 0x6f, 0x6d, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74,
- 0x6f, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x09, 0x74, 0x6f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
- 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
+ 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x65, 0x73, 0x18, 0x0a,
+ 0x20, 0x01, 0x28, 0x04, 0x52, 0x09, 0x73, 0x75, 0x62, 0x74, 0x72, 0x61, 0x63, 0x65, 0x73, 0x12,
+ 0x23, 0x0a, 0x0d, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
+ 0x18, 0x0b, 0x20, 0x03, 0x28, 0x04, 0x52, 0x0c, 0x74, 0x72, 0x61, 0x63, 0x65, 0x41, 0x64, 0x64,
+ 0x72, 0x65, 0x73, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x74, 0x79,
+ 0x70, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x72, 0x61, 0x63, 0x65, 0x54,
+ 0x79, 0x70, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x74, 0x79, 0x70, 0x65,
+ 0x18, 0x0d, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x63, 0x61, 0x6c, 0x6c, 0x54, 0x79, 0x70, 0x65,
+ 0x12, 0x19, 0x0a, 0x08, 0x74, 0x72, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x0e, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x07, 0x74, 0x72, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x73,
+ 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x0f, 0x20, 0x01, 0x28, 0x04, 0x52, 0x06, 0x73, 0x74, 0x61,
+ 0x74, 0x75, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68, 0x61, 0x73,
+ 0x68, 0x18, 0x10, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x48, 0x61,
+ 0x73, 0x68, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x75, 0x6d, 0x62,
+ 0x65, 0x72, 0x18, 0x11, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x4e,
+ 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x29, 0x0a, 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
+ 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x12, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x61, 0x73, 0x68,
0x12, 0x2b, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
- 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x74, 0x72, 0x61,
- 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x29, 0x0a,
- 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68, 0x61, 0x73,
- 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63,
- 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x6f, 0x67, 0x5f,
- 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c, 0x6f, 0x67,
- 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x68,
- 0x61, 0x73, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
- 0x48, 0x61, 0x73, 0x68, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f, 0x6e, 0x75,
- 0x6d, 0x62, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c, 0x6f, 0x63,
- 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x05, 0x65, 0x72, 0x63, 0x32, 0x30,
- 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
- 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45,
- 0x52, 0x43, 0x32, 0x30, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65,
- 0x72, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x63, 0x32, 0x30, 0x12, 0x44, 0x0a, 0x06, 0x65, 0x72,
- 0x63, 0x37, 0x32, 0x31, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63, 0x6f, 0x69,
- 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61,
- 0x67, 0x65, 0x2e, 0x45, 0x52, 0x43, 0x37, 0x32, 0x31, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x72,
- 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x48, 0x00, 0x52, 0x06, 0x65, 0x72, 0x63, 0x37, 0x32, 0x31,
- 0x42, 0x10, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x66,
- 0x65, 0x72, 0x22, 0x6c, 0x0a, 0x12, 0x45, 0x52, 0x43, 0x32, 0x30, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
- 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d,
- 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b,
- 0x66, 0x72, 0x6f, 0x6d, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74,
- 0x6f, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x09, 0x74, 0x6f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61,
- 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65,
- 0x22, 0x72, 0x0a, 0x13, 0x45, 0x52, 0x43, 0x37, 0x32, 0x31, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x54,
- 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f,
- 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66,
- 0x72, 0x6f, 0x6d, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f,
- 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09,
- 0x74, 0x6f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x19, 0x0a, 0x08, 0x74, 0x6f, 0x6b,
- 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x74, 0x6f, 0x6b,
- 0x65, 0x6e, 0x49, 0x64, 0x22, 0x40, 0x0a, 0x19, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d,
- 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x50, 0x72, 0x6f, 0x6f,
- 0x66, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f, 0x70, 0x72, 0x6f,
- 0x6f, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e,
- 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x22, 0x3b, 0x0a, 0x12, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65,
- 0x75, 0x6d, 0x45, 0x78, 0x74, 0x72, 0x61, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12, 0x25, 0x0a, 0x0e,
- 0x65, 0x72, 0x63, 0x32, 0x30, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x18, 0x01,
- 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x72, 0x63, 0x32, 0x30, 0x43, 0x6f, 0x6e, 0x74, 0x72,
- 0x61, 0x63, 0x74, 0x22, 0x74, 0x0a, 0x1c, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x41,
- 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f,
- 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01,
- 0x28, 0x04, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x73, 0x74, 0x6f,
- 0x72, 0x61, 0x67, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x0b, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09,
- 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52,
- 0x08, 0x63, 0x6f, 0x64, 0x65, 0x48, 0x61, 0x73, 0x68, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74,
- 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65,
- 0x2f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x72,
- 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68,
- 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
- 0x6f, 0x33,
+ 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x13, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x74, 0x72, 0x61,
+ 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x4c, 0x0a,
+ 0x0f, 0x63, 0x61, 0x6c, 0x6c, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x5f, 0x69, 0x6e, 0x66, 0x6f,
+ 0x18, 0x14, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73,
+ 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x43,
+ 0x61, 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x52, 0x0d, 0x63, 0x61,
+ 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0xa9, 0x04, 0x0a, 0x15,
+ 0x45, 0x74, 0x68, 0x65, 0x72, 0x65, 0x75, 0x6d, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x72, 0x61,
+ 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x61,
+ 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x74, 0x6f,
+ 0x6b, 0x65, 0x6e, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72,
+ 0x6f, 0x6d, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x1d, 0x0a,
+ 0x0a, 0x74, 0x6f, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28,
+ 0x09, 0x52, 0x09, 0x74, 0x6f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05,
+ 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c,
+ 0x75, 0x65, 0x12, 0x2b, 0x0a, 0x11, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f,
+ 0x6e, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x05, 0x20, 0x01, 0x28, 0x04, 0x52, 0x10, 0x74,
+ 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12,
+ 0x29, 0x0a, 0x10, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x68,
+ 0x61, 0x73, 0x68, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x74, 0x72, 0x61, 0x6e, 0x73,
+ 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x48, 0x61, 0x73, 0x68, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x6f,
+ 0x67, 0x5f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x07, 0x20, 0x01, 0x28, 0x04, 0x52, 0x08, 0x6c,
+ 0x6f, 0x67, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x1d, 0x0a, 0x0a, 0x62, 0x6c, 0x6f, 0x63, 0x6b,
+ 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x62, 0x6c, 0x6f,
+ 0x63, 0x6b, 0x48, 0x61, 0x73, 0x68, 0x12, 0x21, 0x0a, 0x0c, 0x62, 0x6c, 0x6f, 0x63, 0x6b, 0x5f,
+ 0x6e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x18, 0x09, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x62, 0x6c,
+ 0x6f, 0x63, 0x6b, 0x4e, 0x75, 0x6d, 0x62, 0x65, 0x72, 0x12, 0x41, 0x0a, 0x05, 0x65, 0x72, 0x63,
+ 0x32, 0x30, 0x18, 0x64, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62,
+ 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65,
+ 0x2e, 0x45, 0x52, 0x43, 0x32, 0x30, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73,
+ 0x66, 0x65, 0x72, 0x48, 0x00, 0x52, 0x05, 0x65, 0x72, 0x63, 0x32, 0x30, 0x12, 0x44, 0x0a, 0x06,
+ 0x65, 0x72, 0x63, 0x37, 0x32, 0x31, 0x18, 0x65, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2a, 0x2e, 0x63,
+ 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f,
+ 0x72, 0x61, 0x67, 0x65, 0x2e, 0x45, 0x52, 0x43, 0x37, 0x32, 0x31, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
+ 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x48, 0x00, 0x52, 0x06, 0x65, 0x72, 0x63, 0x37,
+ 0x32, 0x31, 0x12, 0x41, 0x0a, 0x05, 0x73, 0x72, 0x63, 0x32, 0x30, 0x18, 0x66, 0x20, 0x01, 0x28,
+ 0x0b, 0x32, 0x29, 0x2e, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2e, 0x63, 0x68, 0x61,
+ 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2e, 0x53, 0x52, 0x43, 0x32, 0x30, 0x54,
+ 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x48, 0x00, 0x52, 0x05,
+ 0x73, 0x72, 0x63, 0x32, 0x30, 0x42, 0x10, 0x0a, 0x0e, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x74,
+ 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x22, 0x6c, 0x0a, 0x12, 0x45, 0x52, 0x43, 0x32, 0x30,
+ 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12, 0x21, 0x0a,
+ 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
+ 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02,
+ 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12,
+ 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
+ 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x72, 0x0a, 0x13, 0x45, 0x52, 0x43, 0x37, 0x32, 0x31, 0x54,
+ 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12, 0x21, 0x0a, 0x0c,
+ 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12,
+ 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x02, 0x20,
+ 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x19,
+ 0x0a, 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x07, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x64, 0x22, 0x6c, 0x0a, 0x12, 0x53, 0x52, 0x43,
+ 0x32, 0x30, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x12,
+ 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x41, 0x64, 0x64, 0x72, 0x65,
+ 0x73, 0x73, 0x12, 0x1d, 0x0a, 0x0a, 0x74, 0x6f, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73,
+ 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x74, 0x6f, 0x41, 0x64, 0x64, 0x72, 0x65, 0x73,
+ 0x73, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09,
+ 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x40, 0x0a, 0x19, 0x45, 0x74, 0x68, 0x65, 0x72,
+ 0x65, 0x75, 0x6d, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x50,
+ 0x72, 0x6f, 0x6f, 0x66, 0x12, 0x23, 0x0a, 0x0d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x5f,
+ 0x70, 0x72, 0x6f, 0x6f, 0x66, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x61, 0x63, 0x63,
+ 0x6f, 0x75, 0x6e, 0x74, 0x50, 0x72, 0x6f, 0x6f, 0x66, 0x22, 0x3b, 0x0a, 0x12, 0x45, 0x74, 0x68,
+ 0x65, 0x72, 0x65, 0x75, 0x6d, 0x45, 0x78, 0x74, 0x72, 0x61, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x12,
+ 0x25, 0x0a, 0x0e, 0x65, 0x72, 0x63, 0x32, 0x30, 0x5f, 0x63, 0x6f, 0x6e, 0x74, 0x72, 0x61, 0x63,
+ 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x65, 0x72, 0x63, 0x32, 0x30, 0x43, 0x6f,
+ 0x6e, 0x74, 0x72, 0x61, 0x63, 0x74, 0x22, 0x74, 0x0a, 0x1c, 0x45, 0x74, 0x68, 0x65, 0x72, 0x65,
+ 0x75, 0x6d, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x53, 0x74, 0x61, 0x74, 0x65, 0x52, 0x65,
+ 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x18,
+ 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x05, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x12, 0x21, 0x0a, 0x0c,
+ 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x02, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x0b, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12,
+ 0x1b, 0x0a, 0x09, 0x63, 0x6f, 0x64, 0x65, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x03, 0x20, 0x01,
+ 0x28, 0x09, 0x52, 0x08, 0x63, 0x6f, 0x64, 0x65, 0x48, 0x61, 0x73, 0x68, 0x22, 0x49, 0x0a, 0x0d,
+ 0x43, 0x61, 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x49, 0x6e, 0x66, 0x6f, 0x12, 0x19, 0x0a,
+ 0x08, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52,
+ 0x07, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x63, 0x61, 0x6c, 0x6c,
+ 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x63, 0x61,
+ 0x6c, 0x6c, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x42, 0x3f, 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75,
+ 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63,
+ 0x68, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+ 0x6f, 0x73, 0x2f, 0x63, 0x6f, 0x69, 0x6e, 0x62, 0x61, 0x73, 0x65, 0x2f, 0x63, 0x68, 0x61, 0x69,
+ 0x6e, 0x73, 0x74, 0x6f, 0x72, 0x61, 0x67, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -2848,7 +3266,7 @@ func file_coinbase_chainstorage_blockchain_ethereum_proto_rawDescGZIP() []byte {
return file_coinbase_chainstorage_blockchain_ethereum_proto_rawDescData
}
-var file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes = make([]protoimpl.MessageInfo, 19)
+var file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes = make([]protoimpl.MessageInfo, 21)
var file_coinbase_chainstorage_blockchain_ethereum_proto_goTypes = []interface{}{
(*EthereumBlobdata)(nil), // 0: coinbase.chainstorage.EthereumBlobdata
(*PolygonExtraData)(nil), // 1: coinbase.chainstorage.PolygonExtraData
@@ -2865,35 +3283,39 @@ var file_coinbase_chainstorage_blockchain_ethereum_proto_goTypes = []interface{}
(*EthereumTokenTransfer)(nil), // 12: coinbase.chainstorage.EthereumTokenTransfer
(*ERC20TokenTransfer)(nil), // 13: coinbase.chainstorage.ERC20TokenTransfer
(*ERC721TokenTransfer)(nil), // 14: coinbase.chainstorage.ERC721TokenTransfer
- (*EthereumAccountStateProof)(nil), // 15: coinbase.chainstorage.EthereumAccountStateProof
- (*EthereumExtraInput)(nil), // 16: coinbase.chainstorage.EthereumExtraInput
- (*EthereumAccountStateResponse)(nil), // 17: coinbase.chainstorage.EthereumAccountStateResponse
- (*EthereumTransactionReceipt_L1FeeInfo)(nil), // 18: coinbase.chainstorage.EthereumTransactionReceipt.L1FeeInfo
- (*timestamppb.Timestamp)(nil), // 19: google.protobuf.Timestamp
+ (*SRC20TokenTransfer)(nil), // 15: coinbase.chainstorage.SRC20TokenTransfer
+ (*EthereumAccountStateProof)(nil), // 16: coinbase.chainstorage.EthereumAccountStateProof
+ (*EthereumExtraInput)(nil), // 17: coinbase.chainstorage.EthereumExtraInput
+ (*EthereumAccountStateResponse)(nil), // 18: coinbase.chainstorage.EthereumAccountStateResponse
+ (*CallValueInfo)(nil), // 19: coinbase.chainstorage.CallValueInfo
+ (*EthereumTransactionReceipt_L1FeeInfo)(nil), // 20: coinbase.chainstorage.EthereumTransactionReceipt.L1FeeInfo
+ (*timestamppb.Timestamp)(nil), // 21: google.protobuf.Timestamp
}
var file_coinbase_chainstorage_blockchain_ethereum_proto_depIdxs = []int32{
1, // 0: coinbase.chainstorage.EthereumBlobdata.polygon:type_name -> coinbase.chainstorage.PolygonExtraData
4, // 1: coinbase.chainstorage.EthereumBlock.header:type_name -> coinbase.chainstorage.EthereumHeader
7, // 2: coinbase.chainstorage.EthereumBlock.transactions:type_name -> coinbase.chainstorage.EthereumTransaction
4, // 3: coinbase.chainstorage.EthereumBlock.uncles:type_name -> coinbase.chainstorage.EthereumHeader
- 19, // 4: coinbase.chainstorage.EthereumHeader.timestamp:type_name -> google.protobuf.Timestamp
+ 21, // 4: coinbase.chainstorage.EthereumHeader.timestamp:type_name -> google.protobuf.Timestamp
3, // 5: coinbase.chainstorage.EthereumHeader.withdrawals:type_name -> coinbase.chainstorage.EthereumWithdrawal
5, // 6: coinbase.chainstorage.EthereumTransactionAccessList.access_list:type_name -> coinbase.chainstorage.EthereumTransactionAccess
8, // 7: coinbase.chainstorage.EthereumTransaction.receipt:type_name -> coinbase.chainstorage.EthereumTransactionReceipt
12, // 8: coinbase.chainstorage.EthereumTransaction.token_transfers:type_name -> coinbase.chainstorage.EthereumTokenTransfer
6, // 9: coinbase.chainstorage.EthereumTransaction.transaction_access_list:type_name -> coinbase.chainstorage.EthereumTransactionAccessList
11, // 10: coinbase.chainstorage.EthereumTransaction.flattened_traces:type_name -> coinbase.chainstorage.EthereumTransactionFlattenedTrace
- 19, // 11: coinbase.chainstorage.EthereumTransaction.block_timestamp:type_name -> google.protobuf.Timestamp
+ 21, // 11: coinbase.chainstorage.EthereumTransaction.block_timestamp:type_name -> google.protobuf.Timestamp
9, // 12: coinbase.chainstorage.EthereumTransactionReceipt.logs:type_name -> coinbase.chainstorage.EthereumEventLog
- 18, // 13: coinbase.chainstorage.EthereumTransactionReceipt.l1_fee_info:type_name -> coinbase.chainstorage.EthereumTransactionReceipt.L1FeeInfo
+ 20, // 13: coinbase.chainstorage.EthereumTransactionReceipt.l1_fee_info:type_name -> coinbase.chainstorage.EthereumTransactionReceipt.L1FeeInfo
10, // 14: coinbase.chainstorage.EthereumTransactionTrace.calls:type_name -> coinbase.chainstorage.EthereumTransactionTrace
- 13, // 15: coinbase.chainstorage.EthereumTokenTransfer.erc20:type_name -> coinbase.chainstorage.ERC20TokenTransfer
- 14, // 16: coinbase.chainstorage.EthereumTokenTransfer.erc721:type_name -> coinbase.chainstorage.ERC721TokenTransfer
- 17, // [17:17] is the sub-list for method output_type
- 17, // [17:17] is the sub-list for method input_type
- 17, // [17:17] is the sub-list for extension type_name
- 17, // [17:17] is the sub-list for extension extendee
- 0, // [0:17] is the sub-list for field type_name
+ 19, // 15: coinbase.chainstorage.EthereumTransactionFlattenedTrace.call_value_info:type_name -> coinbase.chainstorage.CallValueInfo
+ 13, // 16: coinbase.chainstorage.EthereumTokenTransfer.erc20:type_name -> coinbase.chainstorage.ERC20TokenTransfer
+ 14, // 17: coinbase.chainstorage.EthereumTokenTransfer.erc721:type_name -> coinbase.chainstorage.ERC721TokenTransfer
+ 15, // 18: coinbase.chainstorage.EthereumTokenTransfer.src20:type_name -> coinbase.chainstorage.SRC20TokenTransfer
+ 19, // [19:19] is the sub-list for method output_type
+ 19, // [19:19] is the sub-list for method input_type
+ 19, // [19:19] is the sub-list for extension type_name
+ 19, // [19:19] is the sub-list for extension extendee
+ 0, // [0:19] is the sub-list for field type_name
}
func init() { file_coinbase_chainstorage_blockchain_ethereum_proto_init() }
@@ -3083,7 +3505,7 @@ func file_coinbase_chainstorage_blockchain_ethereum_proto_init() {
}
}
file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*EthereumAccountStateProof); i {
+ switch v := v.(*SRC20TokenTransfer); i {
case 0:
return &v.state
case 1:
@@ -3095,7 +3517,7 @@ func file_coinbase_chainstorage_blockchain_ethereum_proto_init() {
}
}
file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*EthereumExtraInput); i {
+ switch v := v.(*EthereumAccountStateProof); i {
case 0:
return &v.state
case 1:
@@ -3107,7 +3529,7 @@ func file_coinbase_chainstorage_blockchain_ethereum_proto_init() {
}
}
file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
- switch v := v.(*EthereumAccountStateResponse); i {
+ switch v := v.(*EthereumExtraInput); i {
case 0:
return &v.state
case 1:
@@ -3119,6 +3541,30 @@ func file_coinbase_chainstorage_blockchain_ethereum_proto_init() {
}
}
file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*EthereumAccountStateResponse); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
+ switch v := v.(*CallValueInfo); i {
+ case 0:
+ return &v.state
+ case 1:
+ return &v.sizeCache
+ case 2:
+ return &v.unknownFields
+ default:
+ return nil
+ }
+ }
+ file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*EthereumTransactionReceipt_L1FeeInfo); i {
case 0:
return &v.state
@@ -3156,10 +3602,19 @@ func file_coinbase_chainstorage_blockchain_ethereum_proto_init() {
(*EthereumTransactionReceipt_DepositReceiptVersion)(nil),
(*EthereumTransactionReceipt_BlobGasPrice)(nil),
(*EthereumTransactionReceipt_BlobGasUsed)(nil),
+ (*EthereumTransactionReceipt_Fee)(nil),
+ (*EthereumTransactionReceipt_NetFee)(nil),
+ (*EthereumTransactionReceipt_NetUsage)(nil),
+ (*EthereumTransactionReceipt_EnergyUsage)(nil),
+ (*EthereumTransactionReceipt_EnergyFee)(nil),
+ (*EthereumTransactionReceipt_OriginEnergyUsage)(nil),
+ (*EthereumTransactionReceipt_EnergyUsageTotal)(nil),
+ (*EthereumTransactionReceipt_EnergyPenaltyTotal)(nil),
}
file_coinbase_chainstorage_blockchain_ethereum_proto_msgTypes[12].OneofWrappers = []interface{}{
(*EthereumTokenTransfer_Erc20)(nil),
(*EthereumTokenTransfer_Erc721)(nil),
+ (*EthereumTokenTransfer_Src20)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
@@ -3167,7 +3622,7 @@ func file_coinbase_chainstorage_blockchain_ethereum_proto_init() {
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_coinbase_chainstorage_blockchain_ethereum_proto_rawDesc,
NumEnums: 0,
- NumMessages: 19,
+ NumMessages: 21,
NumExtensions: 0,
NumServices: 0,
},
diff --git a/protos/coinbase/chainstorage/blockchain_ethereum.proto b/protos/coinbase/chainstorage/blockchain_ethereum.proto
index b5b9d02d..d2851db6 100644
--- a/protos/coinbase/chainstorage/blockchain_ethereum.proto
+++ b/protos/coinbase/chainstorage/blockchain_ethereum.proto
@@ -168,8 +168,33 @@ message EthereumTransactionReceipt {
oneof optional_blob_gas_used {
uint64 blob_gas_used = 21;
}
+ oneof optional_fee {
+ uint64 fee = 22;
+ }
+ oneof optional_net_fee {
+ uint64 net_fee = 23;
+ }
+ oneof optional_net_usage {
+ uint64 net_usage = 24;
+ }
+ oneof optional_energy_usage {
+ uint64 energy_usage = 25;
+ }
+ oneof optional_energy_fee {
+ uint64 energy_fee = 26;
+ }
+ oneof optional_origin_energy_usage {
+ uint64 origin_energy_usage = 27;
+ }
+ oneof optional_energy_usage_total {
+ uint64 energy_usage_total = 28;
+ }
+ oneof optional_energy_penalty_total {
+ uint64 energy_penalty_total = 29;
+ }
}
+
message EthereumEventLog {
bool removed = 1;
uint64 log_index = 2;
@@ -215,6 +240,7 @@ message EthereumTransactionFlattenedTrace {
uint64 block_number = 17;
string transaction_hash = 18;
uint64 transaction_index = 19;
+ repeated CallValueInfo call_value_info = 20;
}
message EthereumTokenTransfer {
@@ -230,6 +256,7 @@ message EthereumTokenTransfer {
oneof token_transfer {
ERC20TokenTransfer erc20 = 100;
ERC721TokenTransfer erc721 = 101;
+ SRC20TokenTransfer src20 = 102;
}
}
@@ -245,6 +272,12 @@ message ERC721TokenTransfer {
string token_id = 3;
}
+message SRC20TokenTransfer {
+ string from_address = 1;
+ string to_address = 2;
+ string value = 3;
+}
+
message EthereumAccountStateProof {
bytes account_proof = 1;
}
@@ -258,3 +291,8 @@ message EthereumAccountStateResponse {
string storage_hash = 2;
string code_hash = 3;
}
+
+message CallValueInfo {
+ string token_id = 1;
+ int64 call_value = 2;
+}
diff --git a/protos/coinbase/chainstorage/blockchain_ethereum_beacon.pb.go b/protos/coinbase/chainstorage/blockchain_ethereum_beacon.pb.go
index 0ad51ed7..eacee151 100644
--- a/protos/coinbase/chainstorage/blockchain_ethereum_beacon.pb.go
+++ b/protos/coinbase/chainstorage/blockchain_ethereum_beacon.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/chainstorage/blockchain_ethereum_beacon.proto
package chainstorage
diff --git a/protos/coinbase/chainstorage/blockchain_rosetta.pb.go b/protos/coinbase/chainstorage/blockchain_rosetta.pb.go
index 3143ac90..5255ef15 100644
--- a/protos/coinbase/chainstorage/blockchain_rosetta.pb.go
+++ b/protos/coinbase/chainstorage/blockchain_rosetta.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/chainstorage/blockchain_rosetta.proto
package chainstorage
diff --git a/protos/coinbase/chainstorage/blockchain_solana.pb.go b/protos/coinbase/chainstorage/blockchain_solana.pb.go
index 76637156..d4b9cfdc 100644
--- a/protos/coinbase/chainstorage/blockchain_solana.pb.go
+++ b/protos/coinbase/chainstorage/blockchain_solana.pb.go
@@ -1,7 +1,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/chainstorage/blockchain_solana.proto
package chainstorage
diff --git a/protos/coinbase/chainstorage/enum_test.go b/protos/coinbase/chainstorage/enum_test.go
index 8a754557..b310f445 100644
--- a/protos/coinbase/chainstorage/enum_test.go
+++ b/protos/coinbase/chainstorage/enum_test.go
@@ -1,11 +1,47 @@
package chainstorage
import (
+ "fmt"
"testing"
"github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/encoding/protojson"
)
+func TestSingleBlockEnumAliasesPreserveRollingRESTCompatibility(t *testing.T) {
+ assert.Equal(
+ t,
+ BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ BlockReadSource_BLOCK_READ_SOURCE_LEGACY,
+ )
+ assert.Equal(
+ t,
+ BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK,
+ BlockObjectFormat_BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK,
+ )
+ assert.Equal(t, "BLOCK_READ_SOURCE_LEGACY", BlockReadSource(1).String())
+ assert.Equal(t, "BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK", BlockObjectFormat(0).String())
+
+ requestJSON, err := protojson.Marshal(&GetRawBlockRequest{
+ ReadSource: BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ })
+ require.NoError(t, err)
+ assert.JSONEq(t, `{"readSource":"BLOCK_READ_SOURCE_LEGACY"}`, string(requestJSON))
+
+ for _, name := range []string{"BLOCK_READ_SOURCE_LEGACY", "BLOCK_READ_SOURCE_SINGLE_BLOCK"} {
+ var request GetRawBlockRequest
+ require.NoError(t, protojson.Unmarshal([]byte(fmt.Sprintf(`{"readSource":%q}`, name)), &request))
+ assert.Equal(t, BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK, request.GetReadSource())
+ }
+
+ for _, name := range []string{"BLOCK_OBJECT_FORMAT_LEGACY_SINGLE_BLOCK", "BLOCK_OBJECT_FORMAT_SINGLE_BLOCK"} {
+ var file BlockFile
+ require.NoError(t, protojson.Unmarshal([]byte(fmt.Sprintf(`{"objectFormat":%q}`, name)), &file))
+ assert.Equal(t, BlockObjectFormat_BLOCK_OBJECT_FORMAT_SINGLE_BLOCK, file.GetObjectFormat())
+ }
+}
+
func TestSolanaProgramName(t *testing.T) {
expectedNames := map[SolanaProgram]string{
SolanaProgram_RAW: "raw",
diff --git a/protos/coinbase/chainstorage/mocks/mocks.go b/protos/coinbase/chainstorage/mocks/mocks.go
index 142bdab3..1a71c96a 100644
--- a/protos/coinbase/chainstorage/mocks/mocks.go
+++ b/protos/coinbase/chainstorage/mocks/mocks.go
@@ -42,6 +42,26 @@ func (m *MockChainStorageClient) EXPECT() *MockChainStorageClientMockRecorder {
return m.recorder
}
+// GetBlockByTimestamp mocks base method.
+func (m *MockChainStorageClient) GetBlockByTimestamp(arg0 context.Context, arg1 *chainstorage.GetBlockByTimestampRequest, arg2 ...grpc.CallOption) (*chainstorage.GetBlockByTimestampResponse, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "GetBlockByTimestamp", varargs...)
+ ret0, _ := ret[0].(*chainstorage.GetBlockByTimestampResponse)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlockByTimestamp indicates an expected call of GetBlockByTimestamp.
+func (mr *MockChainStorageClientMockRecorder) GetBlockByTimestamp(arg0, arg1 any, arg2 ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByTimestamp", reflect.TypeOf((*MockChainStorageClient)(nil).GetBlockByTimestamp), varargs...)
+}
+
// GetBlockByTransaction mocks base method.
func (m *MockChainStorageClient) GetBlockByTransaction(arg0 context.Context, arg1 *chainstorage.GetBlockByTransactionRequest, arg2 ...grpc.CallOption) (*chainstorage.GetBlockByTransactionResponse, error) {
m.ctrl.T.Helper()
diff --git a/protos/coinbase/chainstorage/wire_walker_contract_test.go b/protos/coinbase/chainstorage/wire_walker_contract_test.go
new file mode 100644
index 00000000..6e37d84d
--- /dev/null
+++ b/protos/coinbase/chainstorage/wire_walker_contract_test.go
@@ -0,0 +1,250 @@
+package chainstorage_test
+
+// Contract + switch-coverage tests for the bitcoin wire walker.
+//
+// There are three distinct drift hazards the walker must defend against.
+// Each has a dedicated test:
+//
+// 1. A new proto field that the walker doesn't know about.
+// Caught by TestWireWalkerContract_*. The contract test iterates
+// the proto descriptor and asserts every field has an entry in
+// the walker's Known* map, and vice versa.
+//
+// 2. A walker Known* map entry that doesn't match the proto shape
+// (kind / cardinality / oneof).
+// Also caught by TestWireWalkerContract_*.
+//
+// 3. A field registered in Known* but missing from the walker's
+// switch statement.
+// Caught by TestWireWalkerSwitchCoverage_*. These tests build
+// a proto containing only one registered field (per field), walk
+// it, and assert the walker did not return an "unknown field"
+// error. If the switch case is missing, the walk fails.
+//
+// Together these defenses make it impossible to merge a proto edit
+// that the walker doesn't fully handle — the drift surfaces at CI
+// with an actionable message pointing at blockchain_bitcoin.stream.go.
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+ "google.golang.org/protobuf/proto"
+ "google.golang.org/protobuf/reflect/protoreflect"
+
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+// TestWireWalkerContract_Block cross-references api.KnownBlockFields
+// against the Block proto descriptor.
+func TestWireWalkerContract_Block(t *testing.T) {
+ assertContractMatches(t,
+ (&api.Block{}).ProtoReflect().Descriptor(),
+ api.KnownBlockFields,
+ )
+}
+
+// TestWireWalkerContract_BitcoinBlobdata cross-references
+// api.KnownBitcoinBlobFields against the BitcoinBlobdata descriptor.
+func TestWireWalkerContract_BitcoinBlobdata(t *testing.T) {
+ assertContractMatches(t,
+ (&api.BitcoinBlobdata{}).ProtoReflect().Descriptor(),
+ api.KnownBitcoinBlobFields,
+ )
+}
+
+func assertContractMatches(
+ t *testing.T,
+ d protoreflect.MessageDescriptor,
+ known map[protoreflect.FieldNumber]api.WireWalkerKnownField,
+) {
+ t.Helper()
+ fields := d.Fields()
+
+ // --- Forward: every proto field must be registered in the walker ---
+ for i := 0; i < fields.Len(); i++ {
+ f := fields.Get(i)
+ entry, ok := known[f.Number()]
+ require.Truef(t, ok,
+ "%s.%s (number %d, kind %s): proto has a field the wire walker does not know about.\n"+
+ "Add it to the Known* map in blockchain_bitcoin.stream.go AND either handle it in "+
+ "WalkBitcoinEnvelope (Process: true) or add its number to the known-but-unused "+
+ "skip group (Process: false). Without an update the walker will return "+
+ "\"unknown field\" errors on any block that contains this field.",
+ d.FullName(), f.Name(), f.Number(), f.Kind())
+
+ require.Equalf(t, entry.Kind, f.Kind(),
+ "%s.%s: kind drift (walker expects %s, proto has %s); verify the walker's decode branch still applies",
+ d.FullName(), f.Name(), entry.Kind, f.Kind())
+
+ isRepeated := f.IsList() || f.IsMap()
+ require.Equalf(t, entry.Repeated, isRepeated,
+ "%s.%s: cardinality drift (walker expects repeated=%v, proto has list=%v map=%v)",
+ d.FullName(), f.Name(), entry.Repeated, f.IsList(), f.IsMap())
+
+ require.Equalf(t, entry.Map, f.IsMap(),
+ "%s.%s: map drift (walker expects map=%v, proto has map=%v)",
+ d.FullName(), f.Name(), entry.Map, f.IsMap())
+
+ if entry.Oneof != "" {
+ oneof := f.ContainingOneof()
+ require.NotNilf(t, oneof,
+ "%s.%s: walker expects oneof %q, but field is not in any oneof",
+ d.FullName(), f.Name(), entry.Oneof)
+ require.Equalf(t, entry.Oneof, string(oneof.Name()),
+ "%s.%s: oneof drift (walker expects %q, proto has %q)",
+ d.FullName(), f.Name(), entry.Oneof, oneof.Name())
+ } else if oneof := f.ContainingOneof(); oneof != nil {
+ t.Fatalf("%s.%s: walker does not register an oneof, but proto places this field in oneof %q",
+ d.FullName(), f.Name(), oneof.Name())
+ }
+ }
+
+ // --- Reverse: every walker entry must map to a real proto field ---
+ for num := range known {
+ f := fields.ByNumber(num)
+ require.NotNilf(t, f,
+ "%s: walker has a stale Known* entry for field number %d that no longer exists in the proto — remove it from KnownBlockFields / KnownBitcoinBlobFields and the corresponding walker switch case",
+ d.FullName(), num)
+ }
+}
+
+// TestWireWalkerSwitchCoverage_Block exercises every field registered
+// in api.KnownBlockFields by constructing a Block with ONLY that field
+// set, serializing it, and walking it. If the walker's switch is
+// missing a case for the field, the walk returns an "unknown Block
+// field" error and the test fails with an actionable message.
+func TestWireWalkerSwitchCoverage_Block(t *testing.T) {
+ desc := (&api.Block{}).ProtoReflect().Descriptor()
+
+ for num := range api.KnownBlockFields {
+ fd := desc.Fields().ByNumber(num)
+ require.NotNilf(t, fd, "Known* map references field %d that is missing from the proto descriptor (contract test should have caught this already)", num)
+
+ t.Run(string(fd.Name()), func(t *testing.T) {
+ m := (&api.Block{}).ProtoReflect()
+ populateFieldNonZero(m, fd)
+
+ raw, err := proto.Marshal(m.Interface())
+ require.NoError(t, err)
+
+ _, _, err = api.WalkBitcoinEnvelope(bytes.NewReader(raw))
+ require.NoErrorf(t, err,
+ "walker returned an error on a Block containing only field %q (number %d).\n"+
+ "If the error mentions \"unknown Block field\", the switch in WalkBitcoinEnvelope is missing a case for this field — either handle it (Process) or add its number to the known-but-unused skip group in blockchain_bitcoin.stream.go.",
+ fd.Name(), num)
+ })
+ }
+}
+
+// TestWireWalkerSwitchCoverage_BitcoinBlobdata does the same for
+// BitcoinBlobdata. Because BitcoinBlobdata is nested in Block, each
+// probe is wrapped in a Block with the Bitcoin variant of the oneof
+// set.
+func TestWireWalkerSwitchCoverage_BitcoinBlobdata(t *testing.T) {
+ desc := (&api.BitcoinBlobdata{}).ProtoReflect().Descriptor()
+
+ for num := range api.KnownBitcoinBlobFields {
+ fd := desc.Fields().ByNumber(num)
+ require.NotNilf(t, fd, "Known* map references BitcoinBlobdata field %d that is missing from the proto descriptor", num)
+
+ t.Run(string(fd.Name()), func(t *testing.T) {
+ blob := (&api.BitcoinBlobdata{}).ProtoReflect()
+ populateFieldNonZero(blob, fd)
+
+ block := &api.Block{
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: blob.Interface().(*api.BitcoinBlobdata),
+ },
+ }
+ raw, err := proto.Marshal(block)
+ require.NoError(t, err)
+
+ _, _, err = api.WalkBitcoinEnvelope(bytes.NewReader(raw))
+ require.NoErrorf(t, err,
+ "walker returned an error on a Block containing only BitcoinBlobdata.%s (number %d).\n"+
+ "If the error mentions \"unknown BitcoinBlobdata field\", the switch in walkBitcoinBlob is missing a case for this field — either handle it (Process) or add its number to the known-but-unused skip group in blockchain_bitcoin.stream.go.",
+ fd.Name(), num)
+ })
+ }
+}
+
+// populateFieldNonZero sets a single field on m to a non-zero value
+// so that proto3 serializes it. For oneof fields, setting one variant
+// auto-unsets siblings — which is what we want for per-field probing.
+func populateFieldNonZero(m protoreflect.Message, fd protoreflect.FieldDescriptor) {
+ switch {
+ case fd.IsMap():
+ mp := m.Mutable(fd).Map()
+ key := mapKeyNonZero(fd.MapKey())
+ var val protoreflect.Value
+ if fd.MapValue().Kind() == protoreflect.MessageKind {
+ val = mp.NewValue()
+ } else {
+ val = scalarNonZero(fd.MapValue().Kind())
+ }
+ mp.Set(key, val)
+ case fd.IsList():
+ list := m.Mutable(fd).List()
+ if fd.Kind() == protoreflect.MessageKind {
+ list.Append(list.NewElement())
+ } else {
+ list.Append(scalarNonZero(fd.Kind()))
+ }
+ case fd.Kind() == protoreflect.MessageKind:
+ // Mutable creates an empty submessage and marks the field as
+ // set. An empty sub-message serializes in proto3 as a zero-
+ // length length-delimited entry, which is enough for the
+ // walker to see the field on the wire.
+ m.Mutable(fd)
+ default:
+ m.Set(fd, scalarNonZero(fd.Kind()))
+ }
+}
+
+// scalarNonZero returns a Value that survives proto3 serialization
+// for the given scalar kind (i.e. not the default zero value).
+func scalarNonZero(k protoreflect.Kind) protoreflect.Value {
+ switch k {
+ case protoreflect.BoolKind:
+ return protoreflect.ValueOfBool(true)
+ case protoreflect.EnumKind:
+ return protoreflect.ValueOfEnum(1)
+ case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
+ return protoreflect.ValueOfInt32(1)
+ case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
+ return protoreflect.ValueOfUint32(1)
+ case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
+ return protoreflect.ValueOfInt64(1)
+ case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
+ return protoreflect.ValueOfUint64(1)
+ case protoreflect.FloatKind:
+ return protoreflect.ValueOfFloat32(1)
+ case protoreflect.DoubleKind:
+ return protoreflect.ValueOfFloat64(1)
+ case protoreflect.StringKind:
+ return protoreflect.ValueOfString("x")
+ case protoreflect.BytesKind:
+ return protoreflect.ValueOfBytes([]byte{0x01})
+ }
+ panic("unsupported scalar kind: " + k.String())
+}
+
+func mapKeyNonZero(fd protoreflect.FieldDescriptor) protoreflect.MapKey {
+ switch fd.Kind() {
+ case protoreflect.StringKind:
+ return protoreflect.ValueOfString("k").MapKey()
+ case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind:
+ return protoreflect.ValueOfInt32(1).MapKey()
+ case protoreflect.Uint32Kind, protoreflect.Fixed32Kind:
+ return protoreflect.ValueOfUint32(1).MapKey()
+ case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
+ return protoreflect.ValueOfInt64(1).MapKey()
+ case protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
+ return protoreflect.ValueOfUint64(1).MapKey()
+ case protoreflect.BoolKind:
+ return protoreflect.ValueOfBool(true).MapKey()
+ }
+ panic("unsupported map key kind: " + fd.Kind().String())
+}
diff --git a/protos/coinbase/crypto/rosetta/types/account_identifer.pb.go b/protos/coinbase/crypto/rosetta/types/account_identifer.pb.go
index e3b5971e..96b629db 100644
--- a/protos/coinbase/crypto/rosetta/types/account_identifer.pb.go
+++ b/protos/coinbase/crypto/rosetta/types/account_identifer.pb.go
@@ -4,7 +4,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/crypto/rosetta/types/account_identifer.proto
// The stable release for rosetta types
diff --git a/protos/coinbase/crypto/rosetta/types/amount.pb.go b/protos/coinbase/crypto/rosetta/types/amount.pb.go
index c51382e0..7b0a0e37 100644
--- a/protos/coinbase/crypto/rosetta/types/amount.pb.go
+++ b/protos/coinbase/crypto/rosetta/types/amount.pb.go
@@ -6,7 +6,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/crypto/rosetta/types/amount.proto
// The stable release for rosetta types
diff --git a/protos/coinbase/crypto/rosetta/types/block.pb.go b/protos/coinbase/crypto/rosetta/types/block.pb.go
index 9a2f9e6d..d85a17bd 100644
--- a/protos/coinbase/crypto/rosetta/types/block.pb.go
+++ b/protos/coinbase/crypto/rosetta/types/block.pb.go
@@ -4,7 +4,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/crypto/rosetta/types/block.proto
// The stable release for rosetta types
diff --git a/protos/coinbase/crypto/rosetta/types/coin_change.pb.go b/protos/coinbase/crypto/rosetta/types/coin_change.pb.go
index 2de5d861..575682b3 100644
--- a/protos/coinbase/crypto/rosetta/types/coin_change.pb.go
+++ b/protos/coinbase/crypto/rosetta/types/coin_change.pb.go
@@ -4,7 +4,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/crypto/rosetta/types/coin_change.proto
// The stable release for rosetta types
diff --git a/protos/coinbase/crypto/rosetta/types/network_identifier.pb.go b/protos/coinbase/crypto/rosetta/types/network_identifier.pb.go
index ba8dfb35..3584eec4 100644
--- a/protos/coinbase/crypto/rosetta/types/network_identifier.pb.go
+++ b/protos/coinbase/crypto/rosetta/types/network_identifier.pb.go
@@ -4,7 +4,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/crypto/rosetta/types/network_identifier.proto
// The stable release for rosetta types
diff --git a/protos/coinbase/crypto/rosetta/types/operation.pb.go b/protos/coinbase/crypto/rosetta/types/operation.pb.go
index f34553a5..664f5f37 100644
--- a/protos/coinbase/crypto/rosetta/types/operation.pb.go
+++ b/protos/coinbase/crypto/rosetta/types/operation.pb.go
@@ -4,7 +4,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/crypto/rosetta/types/operation.proto
// The stable release for rosetta types
diff --git a/protos/coinbase/crypto/rosetta/types/transaction.pb.go b/protos/coinbase/crypto/rosetta/types/transaction.pb.go
index 1f902d02..330e6e18 100644
--- a/protos/coinbase/crypto/rosetta/types/transaction.pb.go
+++ b/protos/coinbase/crypto/rosetta/types/transaction.pb.go
@@ -4,7 +4,7 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.32.0
-// protoc v4.25.2
+// protoc v5.29.4
// source: coinbase/crypto/rosetta/types/transaction.proto
// The stable release for rosetta types
diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh
index 7072e542..8bd95dcc 100755
--- a/scripts/bootstrap.sh
+++ b/scripts/bootstrap.sh
@@ -8,7 +8,7 @@ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.32.0
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.3.0
go install go.uber.org/mock/mockgen@v0.4.0
go install github.com/gordonklaus/ineffassign@v0.0.0-20230610083614-0e73809eb601
-go install github.com/kisielk/errcheck@v1.8.0
+go install github.com/kisielk/errcheck@latest
go install golang.org/x/tools/cmd/goimports@v0.17.0
go mod download
go mod tidy
diff --git a/scripts/init-local-postgres.sh b/scripts/init-local-postgres.sh
new file mode 100755
index 00000000..ec0dcda4
--- /dev/null
+++ b/scripts/init-local-postgres.sh
@@ -0,0 +1,113 @@
+#!/bin/sh
+set -e
+
+echo "🚀 Initializing PostgreSQL for local Chainstorage development..."
+
+# --- Configuration ---
+# Master credentials (from Docker environment)
+MASTER_USER="${POSTGRES_USER:-postgres}"
+MASTER_PASSWORD="${POSTGRES_PASSWORD:-postgres}"
+
+# Shared passwords for all network-specific roles (from Docker environment)
+WORKER_PASSWORD="${CHAINSTORAGE_WORKER_PASSWORD:-worker_password}"
+SERVER_PASSWORD="${CHAINSTORAGE_SERVER_PASSWORD:-server_password}"
+
+# List of all networks to create. Format: _
+NETWORKS="
+ethereum_mainnet
+ethereum_goerli
+ethereum_holesky
+bitcoin_mainnet
+base_mainnet
+base_goerli
+arbitrum_mainnet
+polygon_mainnet
+polygon_testnet
+solana_mainnet
+aptos_mainnet
+avacchain_mainnet
+bsc_mainnet
+fantom_mainnet
+optimism_mainnet
+tron_mainnet
+story_mainnet
+dogecoin_mainnet
+litecoin_mainnet
+bitcoincash_mainnet
+dash_mainnet
+"
+
+# --- Helper Functions ---
+# Function to execute a SQL command against the master 'postgres' database.
+# It uses the default Unix socket connection which is most reliable for init scripts.
+psql_master() {
+ PGPASSWORD="$MASTER_PASSWORD" psql -v ON_ERROR_STOP=1 -U "$MASTER_USER" -d "postgres" --no-password -c "$1"
+}
+
+# Function to execute a SQL command against a specific network database.
+psql_network() {
+ local db_name=$1
+ local sql_command=$2
+ PGPASSWORD="$MASTER_PASSWORD" psql -v ON_ERROR_STOP=1 -U "$MASTER_USER" -d "$db_name" --no-password -c "$sql_command"
+}
+
+# --- Main Logic ---
+# The official postgres entrypoint script executes files in /docker-entrypoint-initdb.d/
+# after the server is initialized but before it's opened for general connections.
+# This means we don't need a separate wait loop; the server is ready for us.
+echo "✅ PostgreSQL server is ready for initialization."
+echo ""
+
+# Loop through all networks to create databases and roles
+for network in $NETWORKS; do
+ if [ -n "$network" ]; then
+ db_name="chainstorage_$network"
+ worker_user="cs_${network}_worker"
+ server_user="cs_${network}_server"
+
+ echo "📦 Setting up: $db_name"
+
+ # --- Create Roles (if they don't exist) ---
+ echo " - Creating role: $worker_user"
+ psql_master "DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '$worker_user') THEN CREATE ROLE \"$worker_user\" WITH LOGIN PASSWORD '$WORKER_PASSWORD'; ELSE ALTER ROLE \"$worker_user\" WITH PASSWORD '$WORKER_PASSWORD'; END IF; END \$\$;"
+
+ echo " - Creating role: $server_user"
+ psql_master "DO \$\$ BEGIN IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = '$server_user') THEN CREATE ROLE \"$server_user\" WITH LOGIN PASSWORD '$SERVER_PASSWORD'; ELSE ALTER ROLE \"$server_user\" WITH PASSWORD '$SERVER_PASSWORD'; END IF; END \$\$;"
+
+ # --- Create Database (if it doesn't exist) ---
+ echo " - Creating database: $db_name"
+ # Use a trick to create database if not exists, since "CREATE DATABASE IF NOT EXISTS" is not available
+ if ! psql_master "SELECT 1 FROM pg_database WHERE datname = '$db_name'" | grep -q 1; then
+ psql_master "CREATE DATABASE \"$db_name\" OWNER \"$worker_user\";"
+ else
+ echo " Database $db_name already exists, skipping creation."
+ fi
+
+ # --- Grant Permissions ---
+ echo " - Granting permissions..."
+ # Grant connect to the server user on the new database
+ psql_master "GRANT CONNECT ON DATABASE \"$db_name\" TO \"$server_user\";"
+
+ # Connect to the new database to set schema permissions
+ # Grant server read-only access
+ psql_network "$db_name" "GRANT USAGE ON SCHEMA public TO \"$server_user\";"
+ psql_network "$db_name" "GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"$server_user\";"
+ psql_network "$db_name" "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO \"$server_user\";"
+
+ # Grant worker full access
+ psql_network "$db_name" "GRANT ALL PRIVILEGES ON SCHEMA public TO \"$worker_user\";"
+ psql_network "$db_name" "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"$worker_user\";"
+ psql_network "$db_name" "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL PRIVILEGES ON TABLES TO \"$worker_user\";"
+
+ echo " - ✅ Setup complete for $db_name"
+ echo ""
+ fi
+done
+
+echo "🎉 All network databases initialized successfully for local development!"
+echo ""
+echo "📋 Summary of Shared Credentials:"
+echo " - All worker roles (e.g., cs_ethereum_mainnet_worker) use password: '$WORKER_PASSWORD'"
+echo " - All server roles (e.g., cs_ethereum_mainnet_server) use password: '$SERVER_PASSWORD'"
+echo ""
+echo "🚀 Ready to start Chainstorage!"
\ No newline at end of file
diff --git a/scripts/init-temporal-postgres.sh b/scripts/init-temporal-postgres.sh
new file mode 100755
index 00000000..be5779a5
--- /dev/null
+++ b/scripts/init-temporal-postgres.sh
@@ -0,0 +1,21 @@
+#!/bin/sh
+set -e
+
+echo "Setting up PostgreSQL for Temporal..."
+
+# Since temporal is already the main PostgreSQL user, we just need to create the databases
+psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-'EOSQL'
+ -- Grant CREATEDB privilege to temporal so it can create additional databases
+ ALTER ROLE temporal CREATEDB;
+
+ -- Create temporal_visibility database (temporal database already exists as default)
+ CREATE DATABASE temporal_visibility OWNER temporal;
+
+ -- Grant all privileges to temporal role on both databases
+ GRANT ALL PRIVILEGES ON DATABASE temporal TO temporal;
+ GRANT ALL PRIVILEGES ON DATABASE temporal_visibility TO temporal;
+EOSQL
+
+echo "✅ Temporal PostgreSQL setup complete!"
+echo "Databases: temporal, temporal_visibility"
+echo "Roles: temporal (CREATEDB + full access)"
\ No newline at end of file
diff --git a/scripts/protogen-py.sh b/scripts/protogen-py.sh
new file mode 100755
index 00000000..b4e7984b
--- /dev/null
+++ b/scripts/protogen-py.sh
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+
+set -eo pipefail
+
+python -m grpc_tools.protoc \
+ --python_out=gen/src/python \
+ --grpc_python_out=gen/src/python \
+ --proto_path=protos \
+ protos/coinbase/chainstorage/*.proto \
+ protos/coinbase/c3/common/*.proto \
+ protos/coinbase/crypto/rosetta/types/*.proto
diff --git a/sdk/client.go b/sdk/client.go
index 082d591f..94bcf928 100644
--- a/sdk/client.go
+++ b/sdk/client.go
@@ -18,7 +18,6 @@ import (
"github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
"github.com/coinbase/chainstorage/internal/utils/log"
"github.com/coinbase/chainstorage/internal/utils/retry"
- "github.com/coinbase/chainstorage/internal/utils/syncgroup"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
)
@@ -74,12 +73,33 @@ type (
// and thus you may get back FailedPrecondition errors if it goes beyond current tip due to reorg, especially for streaming case.
GetBlocksByRangeWithTag(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) ([]*api.Block, error)
+ // OpenRawBlockPayload opens the validated, decompressed protobuf payload bytes for one raw block
+ // without unmarshaling api.Block. height is required and hash is optional.
+ OpenRawBlockPayload(ctx context.Context, height uint64, hash string) (*RawBlockPayload, error)
+
+ // OpenRawBlockPayloadWithTag opens the validated, decompressed protobuf payload bytes for one raw block
+ // without unmarshaling api.Block. tag and hash are optional.
+ OpenRawBlockPayloadWithTag(ctx context.Context, tag uint32, height uint64, hash string) (*RawBlockPayload, error)
+
+ // OpenRawBlockPayloadsByRange opens an iterator over raw block payloads between [startHeight, endHeight)
+ // without unmarshaling api.Block. endHeight is optional and defaults to startHeight + 1.
+ OpenRawBlockPayloadsByRange(ctx context.Context, startHeight uint64, endHeight uint64) (RawBlockPayloadIterator, error)
+
+ // OpenRawBlockPayloadsByRangeWithTag opens an iterator over raw block payloads between [startHeight, endHeight)
+ // without unmarshaling api.Block. tag is optional and defaults to stable tag.
+ OpenRawBlockPayloadsByRangeWithTag(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) (RawBlockPayloadIterator, error)
+
// GetBlockByTransaction returns the raw block(s) where the transaction resides,
// or an empty list if the transaction is not found.
// In most networks a transaction belongs to a single block, but there are exceptions.
// Note that this API is still experimental and may change at any time.
GetBlockByTransaction(ctx context.Context, tag uint32, transactionHash string) ([]*api.Block, error)
+ // GetBlockByTimestamp returns the latest block before or at the given timestamp.
+ // The timestamp should be a Unix timestamp (seconds since January 1, 1970 UTC).
+ // If no block is found at or before the timestamp, it returns an error.
+ GetBlockByTimestamp(ctx context.Context, tag uint32, timestamp uint64) (*api.Block, error)
+
// StreamChainEvents streams raw blocks from ChainStorage.
// The caller is responsible for keeping track of the sequence or sequence_num in BlockchainEvent.
StreamChainEvents(ctx context.Context, cfg StreamingConfiguration) (<-chan *ChainEventResult, error)
@@ -95,6 +115,35 @@ type (
// GetStaticChainMetadata returns the static chain metadata, getting from the Config, instead of querying the ChainStorage server.
// This is useful if the caller needs a consistent snapshot of chain metadata during its current lifecycle.
GetStaticChainMetadata(ctx context.Context, req *api.GetChainMetadataRequest) (*api.GetChainMetadataResponse, error)
+
+ // StreamNativeBlock downloads a block to a local disk spool
+ // and returns a chain-agnostic NativeStreamedBlock. Callers
+ // pick the chain-matching accessor (GetBitcoin, GetEthereum,
+ // ...) to iterate. Accessors that don't match the configured
+ // chain — or that correspond to chains whose streaming walker
+ // hasn't landed yet — return nil; callers should fall back to
+ // GetBlock + ParseNativeBlock in that case.
+ //
+ // The returned stream owns the disk spool. Callers MUST call
+ // Close() when done. A runtime cleanup is wired as a safety
+ // net but should not be relied on.
+ StreamNativeBlock(ctx context.Context, tag uint32, height uint64, hash string, opts ...ParseOption) (NativeStreamedBlock, error)
+ }
+
+ ReadSourceClient interface {
+ // GetBlockWithTagAndReadSource returns the raw block using the requested blob read source.
+ GetBlockWithTagAndReadSource(ctx context.Context, tag uint32, height uint64, hash string, readSource api.BlockReadSource) (*api.Block, error)
+
+ // GetBlocksByRangeWithTagAndReadSource returns raw blocks using the requested blob read source.
+ GetBlocksByRangeWithTagAndReadSource(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, readSource api.BlockReadSource) ([]*api.Block, error)
+ }
+
+ RawPayloadReadSourceClient interface {
+ // OpenRawBlockPayloadWithTagAndReadSource opens raw block payload bytes using the requested blob read source.
+ OpenRawBlockPayloadWithTagAndReadSource(ctx context.Context, tag uint32, height uint64, hash string, readSource api.BlockReadSource) (*RawBlockPayload, error)
+
+ // OpenRawBlockPayloadsByRangeWithTagAndReadSource opens raw block payload bytes using the requested blob read source.
+ OpenRawBlockPayloadsByRangeWithTagAndReadSource(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, readSource api.BlockReadSource) (RawBlockPayloadIterator, error)
}
ChainEventResult struct {
@@ -186,18 +235,65 @@ func (c *clientImpl) GetBlock(ctx context.Context, height uint64, hash string) (
}
func (c *clientImpl) GetBlockWithTag(ctx context.Context, tag uint32, height uint64, hash string) (*api.Block, error) {
- return c.downloadBlock(ctx, tag, height, hash)
+ return c.GetBlockWithTagAndReadSource(ctx, tag, height, hash, api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT)
+}
+
+func (c *clientImpl) GetBlockWithTagAndReadSource(ctx context.Context, tag uint32, height uint64, hash string, readSource api.BlockReadSource) (*api.Block, error) {
+ block, err := c.downloadBlock(ctx, tag, height, hash, readSource)
+ if err == nil || !shouldRetrySingleBlockRead(readSource) {
+ return block, err
+ }
+
+ c.logger.Warn(
+ "preferred block download failed; retrying single-block",
+ zap.Uint32("tag", tag),
+ zap.Uint64("height", height),
+ zap.String("hash", hash),
+ zap.String("read_source", readSource.String()),
+ zap.Error(err),
+ )
+ block, fallbackErr := c.downloadBlock(ctx, tag, height, hash, api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK)
+ if fallbackErr != nil {
+ return nil, xerrors.Errorf("failed to download consolidated block and single-block fallback failed (originalErr=%v): %w", err, fallbackErr)
+ }
+ return block, nil
}
func (c *clientImpl) GetBlocksByRangeWithTag(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) ([]*api.Block, error) {
+ return c.GetBlocksByRangeWithTagAndReadSource(ctx, tag, startHeight, endHeight, api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT)
+}
+
+func (c *clientImpl) GetBlocksByRangeWithTagAndReadSource(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, readSource api.BlockReadSource) ([]*api.Block, error) {
if endHeight == 0 {
endHeight = startHeight + 1
}
+ blocks, err := c.downloadBlocksByRange(ctx, tag, startHeight, endHeight, readSource)
+ if err == nil || !shouldRetrySingleBlockRead(readSource) {
+ return blocks, err
+ }
+
+ c.logger.Warn(
+ "preferred block range download failed; retrying single-block",
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", startHeight),
+ zap.Uint64("end_height", endHeight),
+ zap.String("read_source", readSource.String()),
+ zap.Error(err),
+ )
+ blocks, fallbackErr := c.downloadBlocksByRange(ctx, tag, startHeight, endHeight, api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK)
+ if fallbackErr != nil {
+ return nil, xerrors.Errorf("failed to download consolidated block range and single-block fallback failed (originalErr=%v): %w", err, fallbackErr)
+ }
+ return blocks, nil
+}
+
+func (c *clientImpl) downloadBlocksByRange(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, readSource api.BlockReadSource) ([]*api.Block, error) {
resp, err := c.client.GetBlockFilesByRange(ctx, &api.GetBlockFilesByRangeRequest{
Tag: tag,
StartHeight: startHeight,
EndHeight: endHeight,
+ ReadSource: readSource,
})
if err != nil {
@@ -208,44 +304,150 @@ func (c *clientImpl) GetBlocksByRangeWithTag(ctx context.Context, tag uint32, st
}
blockFiles := resp.GetFiles()
- blocks := make([]*api.Block, endHeight-startHeight)
- group, ctx := syncgroup.New(ctx, syncgroup.WithThrottling(int(c.config.SDK.NumWorkers)))
+ for _, blockFile := range blockFiles {
+ c.logger.Debug(
+ "downloading block",
+ zap.Uint32("tag", blockFile.Tag),
+ zap.Uint64("height", blockFile.Height),
+ zap.String("hash", blockFile.Hash),
+ )
+ }
+
+ blocks, err := c.blockDownloader.DownloadMany(ctx, blockFiles)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to download block files: %w", err)
+ }
+ if len(blocks) != len(blockFiles) {
+ return nil, xerrors.Errorf("downloaded block count mismatch: got %d want %d", len(blocks), len(blockFiles))
+ }
+
for i := range blockFiles {
- i := i
- group.Go(func() error {
+ if c.blockValidation {
blockFile := blockFiles[i]
- c.logger.Debug(
- "downloading block",
- zap.Uint32("tag", blockFile.Tag),
- zap.Uint64("height", blockFile.Height),
- zap.String("hash", blockFile.Hash),
- )
- rawBlock, err := c.blockDownloader.Download(ctx, blockFile)
+ err := c.validateBlock(ctx, blocks[i])
if err != nil {
- return xerrors.Errorf("failed download blockFile from %s: %w", blockFile.GetFileUrl(), err)
+ return nil, xerrors.Errorf("failed to validate block (blockHeight=%v, blockHash=%v): %w", blockFile.Height, blockFile.Hash, err)
}
+ }
+ }
- if c.blockValidation {
- err := c.validateBlock(ctx, rawBlock)
- if err != nil {
- return xerrors.Errorf("failed to validate block (blockHeight=%v, blockHash=%v): %w", blockFile.Height, blockFile.Hash, err)
- }
- }
+ return blocks, nil
+}
- blocks[i] = rawBlock
- return nil
- })
+func (c *clientImpl) GetBlocksByRange(ctx context.Context, startHeight uint64, endHeight uint64) ([]*api.Block, error) {
+ return c.GetBlocksByRangeWithTag(ctx, c.tag, startHeight, endHeight)
+}
+
+func (c *clientImpl) OpenRawBlockPayload(ctx context.Context, height uint64, hash string) (*RawBlockPayload, error) {
+ return c.OpenRawBlockPayloadWithTag(ctx, c.tag, height, hash)
+}
+
+func (c *clientImpl) OpenRawBlockPayloadWithTag(ctx context.Context, tag uint32, height uint64, hash string) (*RawBlockPayload, error) {
+ return c.OpenRawBlockPayloadWithTagAndReadSource(ctx, tag, height, hash, api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT)
+}
+
+func (c *clientImpl) OpenRawBlockPayloadWithTagAndReadSource(ctx context.Context, tag uint32, height uint64, hash string, readSource api.BlockReadSource) (*RawBlockPayload, error) {
+ payload, err := c.openRawBlockPayload(ctx, tag, height, hash, readSource)
+ if err == nil || !shouldRetrySingleBlockRead(readSource) {
+ return payload, err
}
- if err := group.Wait(); err != nil {
- return nil, xerrors.Errorf("failed to download block files: %w", err)
+ c.logger.Warn(
+ "preferred raw block payload open failed; retrying single-block",
+ zap.Uint32("tag", tag),
+ zap.Uint64("height", height),
+ zap.String("hash", hash),
+ zap.String("read_source", readSource.String()),
+ zap.Error(err),
+ )
+ payload, fallbackErr := c.openRawBlockPayload(ctx, tag, height, hash, api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK)
+ if fallbackErr != nil {
+ return nil, xerrors.Errorf("failed to open consolidated raw block payload and single-block fallback failed (originalErr=%v): %w", err, fallbackErr)
}
+ return payload, nil
+}
- return blocks, nil
+func (c *clientImpl) openRawBlockPayload(ctx context.Context, tag uint32, height uint64, hash string, readSource api.BlockReadSource) (*RawBlockPayload, error) {
+ blockFile, err := c.client.GetBlockFile(ctx, &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: readSource,
+ })
+ if err != nil {
+ return nil, xerrors.Errorf("failed to query block file (tag=%v, height=%v, hash=%v): %w", tag, height, hash, err)
+ }
+
+ payload, err := c.blockDownloader.OpenRawBlockPayload(ctx, blockFile.GetFile())
+ if err != nil {
+ return nil, xerrors.Errorf("failed to open raw block payload (blockFile={%+v}): %w", blockFile.GetFile(), err)
+ }
+ return wrapRawBlockPayload(payload), nil
}
-func (c *clientImpl) GetBlocksByRange(ctx context.Context, startHeight uint64, endHeight uint64) ([]*api.Block, error) {
- return c.GetBlocksByRangeWithTag(ctx, c.tag, startHeight, endHeight)
+func (c *clientImpl) OpenRawBlockPayloadsByRange(ctx context.Context, startHeight uint64, endHeight uint64) (RawBlockPayloadIterator, error) {
+ return c.OpenRawBlockPayloadsByRangeWithTag(ctx, c.tag, startHeight, endHeight)
+}
+
+func (c *clientImpl) OpenRawBlockPayloadsByRangeWithTag(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) (RawBlockPayloadIterator, error) {
+ return c.OpenRawBlockPayloadsByRangeWithTagAndReadSource(ctx, tag, startHeight, endHeight, api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT)
+}
+
+func (c *clientImpl) OpenRawBlockPayloadsByRangeWithTagAndReadSource(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, readSource api.BlockReadSource) (RawBlockPayloadIterator, error) {
+ if endHeight == 0 {
+ endHeight = startHeight + 1
+ }
+
+ iter, err := c.openRawBlockPayloadsByRange(ctx, tag, startHeight, endHeight, readSource)
+ if err == nil || !shouldRetrySingleBlockRead(readSource) {
+ return iter, err
+ }
+
+ c.logger.Warn(
+ "preferred raw block payload range open failed; retrying single-block",
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", startHeight),
+ zap.Uint64("end_height", endHeight),
+ zap.String("read_source", readSource.String()),
+ zap.Error(err),
+ )
+ iter, fallbackErr := c.openRawBlockPayloadsByRange(ctx, tag, startHeight, endHeight, api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK)
+ if fallbackErr != nil {
+ return nil, xerrors.Errorf("failed to open consolidated raw block payload range and single-block fallback failed (originalErr=%v): %w", err, fallbackErr)
+ }
+ return iter, nil
+}
+
+func (c *clientImpl) openRawBlockPayloadsByRange(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, readSource api.BlockReadSource) (RawBlockPayloadIterator, error) {
+ resp, err := c.client.GetBlockFilesByRange(ctx, &api.GetBlockFilesByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: readSource,
+ })
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get block file metadata (tag=%d, startHeight=%d, endHeight=%d): %w", tag, startHeight, endHeight, err)
+ }
+ blockFiles := resp.GetFiles()
+ if len(blockFiles) == 0 {
+ return nil, xerrors.Errorf("no block file metadata found")
+ }
+
+ iter, err := c.blockDownloader.OpenRawBlockPayloads(ctx, blockFiles)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to open raw block payload iterator: %w", err)
+ }
+ return &rawBlockPayloadIterator{inner: iter}, nil
+}
+
+func shouldRetrySingleBlockRead(readSource api.BlockReadSource) bool {
+ switch readSource {
+ case api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT,
+ api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED:
+ return true
+ default:
+ return false
+ }
}
func (c *clientImpl) StreamChainEvents(ctx context.Context, cfg StreamingConfiguration) (<-chan *ChainEventResult, error) {
@@ -344,7 +546,7 @@ func (c *clientImpl) streamBlocks(
if !cfg.EventOnly {
var err error
blockID := event.GetBlock()
- block, err = c.downloadBlock(ctx, blockID.GetTag(), blockID.GetHeight(), blockID.GetHash())
+ block, err = c.GetBlockWithTagAndReadSource(ctx, blockID.GetTag(), blockID.GetHeight(), blockID.GetHash(), api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT)
if err != nil {
c.sendBlockResult(ctx, ch, &ChainEventResult{
Error: xerrors.Errorf("failed to download block (cfg={%+v}, request={%+v}, event={%+v}): %w", cfg, request, event, err),
@@ -380,7 +582,7 @@ func (c *clientImpl) sendBlockResult(
}
}
-func (c *clientImpl) downloadBlock(ctx context.Context, tag uint32, height uint64, hash string) (*api.Block, error) {
+func (c *clientImpl) downloadBlock(ctx context.Context, tag uint32, height uint64, hash string, readSource api.BlockReadSource) (*api.Block, error) {
c.logger.Debug(
"downloading block",
zap.Uint32("tag", tag),
@@ -388,9 +590,10 @@ func (c *clientImpl) downloadBlock(ctx context.Context, tag uint32, height uint6
zap.String("hash", hash),
)
blockFile, err := c.client.GetBlockFile(ctx, &api.GetBlockFileRequest{
- Tag: tag,
- Height: height,
- Hash: hash,
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: readSource,
})
if err != nil {
return nil, xerrors.Errorf("failed to query block file (tag=%v, height=%v, hash=%v): %w", tag, height, hash, err)
@@ -472,7 +675,7 @@ func (c *clientImpl) GetBlockByTransaction(ctx context.Context, tag uint32, tran
// because a transaction belongs to a single block in most cases.
blocks := make([]*api.Block, len(blockIds))
for i, blockId := range blockIds {
- block, err := c.downloadBlock(ctx, blockId.Tag, blockId.Height, blockId.Hash)
+ block, err := c.GetBlockWithTagAndReadSource(ctx, blockId.Tag, blockId.Height, blockId.Hash, api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT)
if err != nil {
return nil, xerrors.Errorf("failed to download block data: %w", err)
}
@@ -483,6 +686,85 @@ func (c *clientImpl) GetBlockByTransaction(ctx context.Context, tag uint32, tran
return blocks, nil
}
+func (c *clientImpl) GetBlockByTimestamp(ctx context.Context, tag uint32, timestamp uint64) (*api.Block, error) {
+ resp, err := c.client.GetBlockByTimestamp(ctx, &api.GetBlockByTimestampRequest{
+ Tag: tag,
+ Timestamp: timestamp,
+ })
+ if err != nil {
+ return nil, xerrors.Errorf("failed to get block by timestamp (tag=%v, timestamp=%v): %w", tag, timestamp, err)
+ }
+
+ // Download the block data using the metadata from the response
+ block, err := c.GetBlockWithTagAndReadSource(ctx, resp.Tag, resp.Height, resp.Hash, api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT)
+ if err != nil {
+ return nil, xerrors.Errorf("failed to download block data: %w", err)
+ }
+
+ return block, nil
+}
+
+// streamingParser is an unexported mirror of the internal parser's
+// streaming method. The injected c.parser always implements this (it
+// comes from the internal parser impl), but sdk.Parser is deliberately
+// trimmed to exclude it — consumers stream via Client.StreamNativeBlock
+// rather than holding a Parser + constructing a SpooledBlock themselves.
+type streamingParser interface {
+ ParseStreamNative(ctx context.Context, spooled *downloader.SpooledBlock, opts ...ParseOption) (NativeStreamedBlock, error)
+}
+
+func (c *clientImpl) StreamNativeBlock(
+ ctx context.Context,
+ tag uint32, height uint64, hash string,
+ opts ...ParseOption,
+) (NativeStreamedBlock, error) {
+ spooled, err := c.downloadBlockStream(ctx, tag, height, hash, api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT)
+ if err != nil {
+ c.logger.Warn(
+ "preferred native block stream download failed; retrying single-block",
+ zap.Uint32("tag", tag),
+ zap.Uint64("height", height),
+ zap.String("hash", hash),
+ zap.Error(err),
+ )
+ var fallbackErr error
+ spooled, fallbackErr = c.downloadBlockStream(ctx, tag, height, hash, api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK)
+ if fallbackErr != nil {
+ return nil, xerrors.Errorf("failed to download preferred native block stream and single-block fallback failed (originalErr=%v): %w", err, fallbackErr)
+ }
+ }
+
+ sp, ok := c.parser.(streamingParser)
+ if !ok {
+ _ = spooled.Close()
+ return nil, xerrors.Errorf("parser %T does not support streaming", c.parser)
+ }
+ stream, err := sp.ParseStreamNative(ctx, spooled, opts...)
+ if err != nil {
+ _ = spooled.Close()
+ return nil, xerrors.Errorf("failed to create native stream: %w", err)
+ }
+ return stream, nil
+}
+
+func (c *clientImpl) downloadBlockStream(ctx context.Context, tag uint32, height uint64, hash string, readSource api.BlockReadSource) (*downloader.SpooledBlock, error) {
+ blockFileResp, err := c.client.GetBlockFile(ctx, &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: readSource,
+ })
+ if err != nil {
+ return nil, xerrors.Errorf("failed to query block file (tag=%v, height=%v, hash=%v): %w", tag, height, hash, err)
+ }
+
+ spooled, err := c.blockDownloader.DownloadStream(ctx, blockFileResp.GetFile())
+ if err != nil {
+ return nil, err
+ }
+ return spooled, nil
+}
+
func (c *clientImpl) validateBlock(ctx context.Context, rawBlock *api.Block) error {
hash := rawBlock.GetMetadata().GetHash()
height := rawBlock.GetMetadata().GetHeight()
diff --git a/sdk/client_interceptor.go b/sdk/client_interceptor.go
index 0f6c0eaa..53db8272 100644
--- a/sdk/client_interceptor.go
+++ b/sdk/client_interceptor.go
@@ -106,6 +106,37 @@ func (c *timeoutableClient) GetBlockWithTag(ctx context.Context, tag uint32, hei
})
}
+func (c *timeoutableClient) GetBlockWithTagAndReadSource(ctx context.Context, tag uint32, height uint64, hash string, readSource api.BlockReadSource) (*api.Block, error) {
+ readSourceClient, ok := c.client.(ReadSourceClient)
+ if !ok {
+ return nil, xerrors.Errorf("client %T does not support read source override", c.client)
+ }
+ if readSource != api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED {
+ return c.getBlockWithTagAndReadSource(ctx, readSourceClient, tag, height, hash, readSource)
+ }
+
+ block, err := c.getBlockWithTagAndReadSource(ctx, readSourceClient, tag, height, hash, readSource)
+ if err == nil {
+ return block, nil
+ }
+ if ctx.Err() != nil {
+ return nil, err
+ }
+
+ c.logger.Warn(
+ "consolidated block read timed out or failed; retrying single-block",
+ zap.Uint32("tag", tag),
+ zap.Uint64("height", height),
+ zap.String("hash", hash),
+ zap.Error(err),
+ )
+ block, fallbackErr := c.getBlockWithTagAndReadSource(ctx, readSourceClient, tag, height, hash, api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK)
+ if fallbackErr != nil {
+ return nil, xerrors.Errorf("failed to read consolidated block and single-block fallback failed (originalErr=%v): %w", err, fallbackErr)
+ }
+ return block, nil
+}
+
func (c *timeoutableClient) GetBlocksByRange(ctx context.Context, startHeight uint64, endHeight uint64) ([]*api.Block, error) {
// No retry needed because this is a wrapper on GetBlocksByRangeWithTag
return c.client.GetBlocksByRange(ctx, startHeight, endHeight)
@@ -113,10 +144,7 @@ func (c *timeoutableClient) GetBlocksByRange(ctx context.Context, startHeight ui
func (c *timeoutableClient) GetBlocksByRangeWithTag(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) ([]*api.Block, error) {
return intercept(ctx, c.logger, func(ctx context.Context) ([]*api.Block, error) {
- timeout := c.mediumTimeout
- if endHeight-startHeight > 2 {
- timeout = c.longTimeout
- }
+ timeout := c.rangeReadTimeout(startHeight, endHeight)
ctx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
@@ -125,6 +153,121 @@ func (c *timeoutableClient) GetBlocksByRangeWithTag(ctx context.Context, tag uin
})
}
+func (c *timeoutableClient) GetBlocksByRangeWithTagAndReadSource(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, readSource api.BlockReadSource) ([]*api.Block, error) {
+ readSourceClient, ok := c.client.(ReadSourceClient)
+ if !ok {
+ return nil, xerrors.Errorf("client %T does not support read source override", c.client)
+ }
+ if readSource != api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED {
+ return c.getBlocksByRangeWithTagAndReadSource(ctx, readSourceClient, tag, startHeight, endHeight, readSource)
+ }
+
+ blocks, err := c.getBlocksByRangeWithTagAndReadSource(ctx, readSourceClient, tag, startHeight, endHeight, readSource)
+ if err == nil {
+ return blocks, nil
+ }
+ if ctx.Err() != nil {
+ return nil, err
+ }
+
+ c.logger.Warn(
+ "consolidated block range read timed out or failed; retrying single-block",
+ zap.Uint32("tag", tag),
+ zap.Uint64("start_height", startHeight),
+ zap.Uint64("end_height", endHeight),
+ zap.Error(err),
+ )
+ blocks, fallbackErr := c.getBlocksByRangeWithTagAndReadSource(ctx, readSourceClient, tag, startHeight, endHeight, api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK)
+ if fallbackErr != nil {
+ return nil, xerrors.Errorf("failed to read consolidated block range and single-block fallback failed (originalErr=%v): %w", err, fallbackErr)
+ }
+ return blocks, nil
+}
+
+func (c *timeoutableClient) rangeReadTimeout(startHeight uint64, endHeight uint64) time.Duration {
+ effectiveEndHeight := endHeight
+ if effectiveEndHeight == 0 {
+ effectiveEndHeight = startHeight + 1
+ }
+
+ if effectiveEndHeight > startHeight && effectiveEndHeight-startHeight > 2 {
+ return c.longTimeout
+ }
+ return c.mediumTimeout
+}
+
+func (c *timeoutableClient) getBlockWithTagAndReadSource(
+ ctx context.Context,
+ readSourceClient ReadSourceClient,
+ tag uint32,
+ height uint64,
+ hash string,
+ readSource api.BlockReadSource,
+) (*api.Block, error) {
+ return intercept(ctx, c.logger, func(ctx context.Context) (*api.Block, error) {
+ ctx, cancel := context.WithTimeout(ctx, c.mediumTimeout)
+ defer cancel()
+
+ return readSourceClient.GetBlockWithTagAndReadSource(ctx, tag, height, hash, readSource)
+ })
+}
+
+func (c *timeoutableClient) getBlocksByRangeWithTagAndReadSource(
+ ctx context.Context,
+ readSourceClient ReadSourceClient,
+ tag uint32,
+ startHeight uint64,
+ endHeight uint64,
+ readSource api.BlockReadSource,
+) ([]*api.Block, error) {
+ return intercept(ctx, c.logger, func(ctx context.Context) ([]*api.Block, error) {
+ ctx, cancel := context.WithTimeout(ctx, c.rangeReadTimeout(startHeight, endHeight))
+ defer cancel()
+
+ return readSourceClient.GetBlocksByRangeWithTagAndReadSource(ctx, tag, startHeight, endHeight, readSource)
+ })
+}
+
+// OpenRawBlockPayload passes through to the wrapped client. No timeout
+// wrapping: the returned reader lifetime is user-controlled.
+func (c *timeoutableClient) OpenRawBlockPayload(ctx context.Context, height uint64, hash string) (*RawBlockPayload, error) {
+ return c.client.OpenRawBlockPayload(ctx, height, hash)
+}
+
+// OpenRawBlockPayloadWithTag passes through to the wrapped client. No timeout
+// wrapping: the returned reader lifetime is user-controlled.
+func (c *timeoutableClient) OpenRawBlockPayloadWithTag(ctx context.Context, tag uint32, height uint64, hash string) (*RawBlockPayload, error) {
+ return c.client.OpenRawBlockPayloadWithTag(ctx, tag, height, hash)
+}
+
+func (c *timeoutableClient) OpenRawBlockPayloadWithTagAndReadSource(ctx context.Context, tag uint32, height uint64, hash string, readSource api.BlockReadSource) (*RawBlockPayload, error) {
+ rawPayloadClient, ok := c.client.(RawPayloadReadSourceClient)
+ if !ok {
+ return nil, xerrors.Errorf("client %T does not support raw payload read source override", c.client)
+ }
+ return rawPayloadClient.OpenRawBlockPayloadWithTagAndReadSource(ctx, tag, height, hash, readSource)
+}
+
+// OpenRawBlockPayloadsByRange passes through to the wrapped client. No timeout
+// wrapping: iterator reads are user-controlled.
+func (c *timeoutableClient) OpenRawBlockPayloadsByRange(ctx context.Context, startHeight uint64, endHeight uint64) (RawBlockPayloadIterator, error) {
+ return c.client.OpenRawBlockPayloadsByRange(ctx, startHeight, endHeight)
+}
+
+// OpenRawBlockPayloadsByRangeWithTag passes through to the wrapped client. No
+// timeout wrapping: iterator reads are user-controlled.
+func (c *timeoutableClient) OpenRawBlockPayloadsByRangeWithTag(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64) (RawBlockPayloadIterator, error) {
+ return c.client.OpenRawBlockPayloadsByRangeWithTag(ctx, tag, startHeight, endHeight)
+}
+
+func (c *timeoutableClient) OpenRawBlockPayloadsByRangeWithTagAndReadSource(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, readSource api.BlockReadSource) (RawBlockPayloadIterator, error) {
+ rawPayloadClient, ok := c.client.(RawPayloadReadSourceClient)
+ if !ok {
+ return nil, xerrors.Errorf("client %T does not support raw payload read source override", c.client)
+ }
+ return rawPayloadClient.OpenRawBlockPayloadsByRangeWithTagAndReadSource(ctx, tag, startHeight, endHeight, readSource)
+}
+
func (c *timeoutableClient) GetBlockByTransaction(ctx context.Context, tag uint32, transactionHash string) ([]*api.Block, error) {
return intercept(ctx, c.logger, func(ctx context.Context) ([]*api.Block, error) {
ctx, cancel := context.WithTimeout(ctx, c.mediumTimeout)
@@ -134,6 +277,15 @@ func (c *timeoutableClient) GetBlockByTransaction(ctx context.Context, tag uint3
})
}
+func (c *timeoutableClient) GetBlockByTimestamp(ctx context.Context, tag uint32, timestamp uint64) (*api.Block, error) {
+ return intercept(ctx, c.logger, func(ctx context.Context) (*api.Block, error) {
+ ctx, cancel := context.WithTimeout(ctx, c.mediumTimeout)
+ defer cancel()
+
+ return c.client.GetBlockByTimestamp(ctx, tag, timestamp)
+ })
+}
+
func (c *timeoutableClient) StreamChainEvents(ctx context.Context, cfg StreamingConfiguration) (<-chan *ChainEventResult, error) {
// No timeout is implemented.
return c.client.StreamChainEvents(ctx, cfg)
@@ -167,6 +319,13 @@ func (c *timeoutableClient) GetStaticChainMetadata(ctx context.Context, req *api
return c.client.GetStaticChainMetadata(ctx, req)
}
+// StreamNativeBlock passes through to the wrapped client. No timeout
+// wrapping: the download + iterator lifetime is user-controlled and may
+// intentionally exceed short/medium/long defaults.
+func (c *timeoutableClient) StreamNativeBlock(ctx context.Context, tag uint32, height uint64, hash string, opts ...ParseOption) (NativeStreamedBlock, error) {
+ return c.client.StreamNativeBlock(ctx, tag, height, hash, opts...)
+}
+
func intercept[T any](ctx context.Context, logger *zap.Logger, operation retry.OperationWithResultFn[T]) (T, error) {
return retry.WrapWithResult(
ctx,
diff --git a/sdk/client_interceptor_test.go b/sdk/client_interceptor_test.go
index adc274b4..65511bf7 100644
--- a/sdk/client_interceptor_test.go
+++ b/sdk/client_interceptor_test.go
@@ -3,6 +3,7 @@ package sdk_test
import (
"context"
"testing"
+ "time"
"github.com/stretchr/testify/suite"
"go.uber.org/mock/gomock"
@@ -24,8 +25,22 @@ type (
client *sdkmocks.MockClient
interceptor sdk.Client
}
+
+ readSourceClientForTest struct {
+ sdk.Client
+ getBlockWithTagAndReadSource func(context.Context, uint32, uint64, string, api.BlockReadSource) (*api.Block, error)
+ getBlocksByRangeWithTagAndReadSource func(context.Context, uint32, uint64, uint64, api.BlockReadSource) ([]*api.Block, error)
+ }
)
+func (c *readSourceClientForTest) GetBlockWithTagAndReadSource(ctx context.Context, tag uint32, height uint64, hash string, readSource api.BlockReadSource) (*api.Block, error) {
+ return c.getBlockWithTagAndReadSource(ctx, tag, height, hash, readSource)
+}
+
+func (c *readSourceClientForTest) GetBlocksByRangeWithTagAndReadSource(ctx context.Context, tag uint32, startHeight uint64, endHeight uint64, readSource api.BlockReadSource) ([]*api.Block, error) {
+ return c.getBlocksByRangeWithTagAndReadSource(ctx, tag, startHeight, endHeight, readSource)
+}
+
func TestClientInterceptor(t *testing.T) {
suite.Run(t, new(clientInterceptorTestSuite))
}
@@ -126,6 +141,138 @@ func (s *clientInterceptorTestSuite) TestGetBlockWithTag() {
require.Equal(expected, actual)
}
+func (s *clientInterceptorTestSuite) TestGetBlocksByRangeWithTagAndReadSource_DefaultEndHeightUsesMediumTimeout() {
+ var (
+ tag uint32 = 2
+ startHeight uint64 = 123
+ endHeight uint64
+ readSource = api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED
+ expected = []*api.Block{testutil.MakeBlock(startHeight, tag)}
+ )
+
+ require := testutil.Require(s.T())
+ delegate := &readSourceClientForTest{
+ Client: s.client,
+ getBlockWithTagAndReadSource: func(context.Context, uint32, uint64, string, api.BlockReadSource) (*api.Block, error) {
+ return nil, nil
+ },
+ getBlocksByRangeWithTagAndReadSource: func(ctx context.Context, tag_ uint32, startHeight_ uint64, endHeight_ uint64, readSource_ api.BlockReadSource) ([]*api.Block, error) {
+ require.Equal(tag, tag_)
+ require.Equal(startHeight, startHeight_)
+ require.Equal(endHeight, endHeight_)
+ require.Equal(readSource, readSource_)
+
+ deadline, ok := ctx.Deadline()
+ require.True(ok)
+ require.Less(time.Until(deadline), 8*time.Second)
+ return expected, nil
+ },
+ }
+ interceptor := sdk.WithTimeoutableClientInterceptor(delegate, zap.NewNop())
+ readSourceInterceptor, ok := interceptor.(sdk.ReadSourceClient)
+ require.True(ok)
+
+ actual, err := readSourceInterceptor.GetBlocksByRangeWithTagAndReadSource(context.Background(), tag, startHeight, endHeight, readSource)
+ require.NoError(err)
+ require.Equal(expected, actual)
+}
+
+func (s *clientInterceptorTestSuite) TestGetBlockWithTagAndReadSource_FallsBackAfterConsolidatedTimeout() {
+ var (
+ tag uint32 = 2
+ height uint64 = 123
+ hash = "0xabc"
+ expected = testutil.MakeBlock(height, tag)
+ )
+
+ require := testutil.Require(s.T())
+ var consolidatedAttempts int
+ var singleBlockAttempts int
+ delegate := &readSourceClientForTest{
+ Client: s.client,
+ getBlockWithTagAndReadSource: func(ctx context.Context, tag_ uint32, height_ uint64, hash_ string, readSource_ api.BlockReadSource) (*api.Block, error) {
+ require.Equal(tag, tag_)
+ require.Equal(height, height_)
+ require.Equal(hash, hash_)
+ switch readSource_ {
+ case api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED:
+ consolidatedAttempts++
+ <-ctx.Done()
+ return nil, ctx.Err()
+ case api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK:
+ singleBlockAttempts++
+ require.NoError(ctx.Err())
+ return expected, nil
+ default:
+ require.FailNow("unexpected read source", readSource_.String())
+ }
+ return nil, nil
+ },
+ getBlocksByRangeWithTagAndReadSource: func(context.Context, uint32, uint64, uint64, api.BlockReadSource) ([]*api.Block, error) {
+ return nil, nil
+ },
+ }
+ interceptor := sdk.WithTimeoutableClientInterceptor(delegate, zap.NewNop())
+ timeoutClient := interceptor.(interface{ SetClientTimeout(time.Duration) })
+ timeoutClient.SetClientTimeout(10 * time.Millisecond)
+ readSourceInterceptor, ok := interceptor.(sdk.ReadSourceClient)
+ require.True(ok)
+
+ actual, err := readSourceInterceptor.GetBlockWithTagAndReadSource(context.Background(), tag, height, hash, api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED)
+ require.NoError(err)
+ require.Equal(expected, actual)
+ require.Equal(retry.DefaultMaxAttempts, consolidatedAttempts)
+ require.Equal(1, singleBlockAttempts)
+}
+
+func (s *clientInterceptorTestSuite) TestGetBlocksByRangeWithTagAndReadSource_FallsBackAfterConsolidatedTimeout() {
+ var (
+ tag uint32 = 2
+ startHeight uint64 = 123
+ endHeight uint64 = 125
+ expected = []*api.Block{testutil.MakeBlock(startHeight, tag), testutil.MakeBlock(startHeight+1, tag)}
+ )
+
+ require := testutil.Require(s.T())
+ var consolidatedAttempts int
+ var singleBlockAttempts int
+ delegate := &readSourceClientForTest{
+ Client: s.client,
+ getBlockWithTagAndReadSource: func(context.Context, uint32, uint64, string, api.BlockReadSource) (*api.Block, error) {
+ return nil, nil
+ },
+ getBlocksByRangeWithTagAndReadSource: func(ctx context.Context, tag_ uint32, startHeight_ uint64, endHeight_ uint64, readSource_ api.BlockReadSource) ([]*api.Block, error) {
+ require.Equal(tag, tag_)
+ require.Equal(startHeight, startHeight_)
+ require.Equal(endHeight, endHeight_)
+ switch readSource_ {
+ case api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED:
+ consolidatedAttempts++
+ <-ctx.Done()
+ return nil, ctx.Err()
+ case api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK:
+ singleBlockAttempts++
+ require.NoError(ctx.Err())
+ return expected, nil
+ default:
+ require.FailNow("unexpected read source", readSource_.String())
+ }
+ return nil, nil
+ },
+ }
+ interceptor := sdk.WithTimeoutableClientInterceptor(delegate, zap.NewNop())
+ timeoutClient := interceptor.(interface{ SetClientTimeout(time.Duration) })
+ timeoutClient.SetClientTimeout(10 * time.Millisecond)
+ readSourceInterceptor, ok := interceptor.(sdk.ReadSourceClient)
+ require.True(ok)
+
+ actual, err := readSourceInterceptor.GetBlocksByRangeWithTagAndReadSource(context.Background(), tag, startHeight, endHeight, api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED)
+ require.NoError(err)
+ require.Equal(expected, actual)
+ require.Equal(retry.DefaultMaxAttempts, consolidatedAttempts)
+ require.Equal(1, singleBlockAttempts)
+}
+
func (s *clientInterceptorTestSuite) TestGetChainEvents() {
var (
req = &api.GetChainEventsRequest{}
diff --git a/sdk/client_stream_test.go b/sdk/client_stream_test.go
new file mode 100644
index 00000000..ec110a8a
--- /dev/null
+++ b/sdk/client_stream_test.go
@@ -0,0 +1,167 @@
+package sdk
+
+import (
+ "context"
+ "io"
+ "os"
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "go.uber.org/fx"
+ "go.uber.org/mock/gomock"
+ "google.golang.org/protobuf/proto"
+
+ "github.com/coinbase/chainstorage/internal/blockchain/parser"
+ "github.com/coinbase/chainstorage/internal/gateway"
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
+ downloadermocks "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader/mocks"
+ "github.com/coinbase/chainstorage/internal/utils/fixtures"
+ "github.com/coinbase/chainstorage/internal/utils/testapp"
+ "github.com/coinbase/chainstorage/internal/utils/testutil"
+ "github.com/coinbase/chainstorage/protos/coinbase/c3/common"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+ apimocks "github.com/coinbase/chainstorage/protos/coinbase/chainstorage/mocks"
+)
+
+// streamBitcoinClientSuite brings up an SDK Client wired for
+// bitcoin-mainnet so StreamBlock can actually stream through the
+// bitcoin parser.
+type streamBitcoinClientSuite struct {
+ suite.Suite
+ ctrl *gomock.Controller
+ app testapp.TestApp
+ gatewayClient *apimocks.MockChainStorageClient
+ downloaderClient *downloadermocks.MockBlockDownloader
+ client Client
+ require *testutil.Assertions
+}
+
+func TestStreamBitcoinClientSuite(t *testing.T) {
+ suite.Run(t, new(streamBitcoinClientSuite))
+}
+
+func (s *streamBitcoinClientSuite) SetupTest() {
+ s.ctrl = gomock.NewController(s.T())
+ s.require = testutil.Require(s.T())
+
+ s.gatewayClient = apimocks.NewMockChainStorageClient(s.ctrl)
+ s.downloaderClient = downloadermocks.NewMockBlockDownloader(s.ctrl)
+
+ s.app = testapp.New(
+ s.T(),
+ Module,
+ parser.Module,
+ testapp.WithBlockchainNetwork(common.Blockchain_BLOCKCHAIN_BITCOIN, common.Network_NETWORK_BITCOIN_MAINNET),
+ fx.Provide(func() downloader.BlockDownloader { return s.downloaderClient }),
+ fx.Provide(func() gateway.Client { return s.gatewayClient }),
+ fx.Populate(&s.client),
+ )
+ s.require.NotNil(s.client)
+}
+
+func (s *streamBitcoinClientSuite) TearDownTest() {
+ s.app.Close()
+ s.ctrl.Finish()
+}
+
+// TestStreamBlock_BitcoinEndToEnd verifies that a bitcoin-configured
+// SDK client plumbs a streamed block through the downloader into the
+// bitcoin parser's iterator and delivers transactions to the caller.
+func (s *streamBitcoinClientSuite) TestStreamBlock_BitcoinEndToEnd() {
+ const (
+ tag = uint32(1)
+ height = uint64(696402)
+ hash = "000000000000000000088a771bf9592a8bd3e9a5dc4c5a18876b65b283f0fb1e"
+ )
+
+ rawBlock := buildBitcoinFixtureBlock(s.T())
+ rawBlock.Metadata = &api.BlockMetadata{
+ Tag: tag, Height: height, Hash: hash,
+ ParentHash: "0000000000000000000bbc2c027a9f9a9144f5368d1e02091bddd0307b058ec3",
+ }
+
+ bf := &api.BlockFile{Tag: tag, Height: height, Hash: hash}
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), gomock.Any()).Return(&api.GetBlockFileResponse{File: bf}, nil)
+
+ // Produce the proto-marshaled decompressed bytes the SpooledBlock
+ // Open() would return. The parser walks these bytes to collect
+ // chunk offsets, so it's critical we give it the full
+ // BitcoinBlobdata including Header + InputTransactions.
+ blockBytes, err := proto.Marshal(rawBlock)
+ s.require.NoError(err)
+
+ // Write to a temp file so the parser's seek-based loaders can use
+ // io.Seeker semantics on what Open() returns.
+ spoolFile, err := os.CreateTemp(s.T().TempDir(), "chainstorage-spool-*.bin")
+ s.require.NoError(err)
+ _, err = spoolFile.Write(blockBytes)
+ s.require.NoError(err)
+ s.require.NoError(spoolFile.Close())
+
+ spooled := &downloader.SpooledBlock{
+ BlockFile: bf,
+ Open: func() (io.ReadCloser, error) {
+ return os.Open(spoolFile.Name())
+ },
+ }
+ s.downloaderClient.EXPECT().DownloadStream(gomock.Any(), bf).Return(spooled, nil)
+
+ native, err := s.client.StreamNativeBlock(context.Background(), tag, height, hash)
+ s.require.NoError(err)
+ defer native.Close()
+ s.require.NotNil(native.GetMetadata())
+
+ bs := native.GetBitcoin()
+ s.require.NotNil(bs, "bitcoin-configured client must populate GetBitcoin()")
+ s.require.Nil(native.GetEthereum(), "non-ethereum config must leave GetEthereum() nil")
+
+ var txCount int
+ for tx, iterErr := range bs.Transactions() {
+ s.require.NoError(iterErr)
+ _ = tx
+ txCount++
+ }
+ header, err := bs.Header()
+ s.require.NoError(err)
+ s.require.Greater(txCount, 0)
+ s.require.NotNil(header)
+ s.require.Equal(hash, header.Hash)
+}
+
+// buildBitcoinFixtureBlock assembles an api.Block with bitcoin blobdata
+// from the standard get_block fixtures already used by parser tests.
+func buildBitcoinFixtureBlock(t *testing.T) *api.Block {
+ t.Helper()
+ header, err := fixtures.ReadFile("parser/bitcoin/get_block.json")
+ if err != nil {
+ t.Fatalf("load get_block.json: %v", err)
+ }
+ tx1, err := fixtures.ReadFile("parser/bitcoin/get_raw_transaction.json")
+ if err != nil {
+ t.Fatalf("load get_raw_transaction.json: %v", err)
+ }
+ tx2, err := fixtures.ReadFile("parser/bitcoin/get_raw_transaction_tx2.json")
+ if err != nil {
+ t.Fatalf("load get_raw_transaction_tx2.json: %v", err)
+ }
+
+ return &api.Block{
+ Blockchain: common.Blockchain_BLOCKCHAIN_BITCOIN,
+ Network: common.Network_NETWORK_BITCOIN_MAINNET,
+ Metadata: &api.BlockMetadata{
+ Tag: 1,
+ Height: 696402,
+ Hash: "000000000000000000088a771bf9592a8bd3e9a5dc4c5a18876b65b283f0fb1e",
+ ParentHash: "0000000000000000000bbc2c027a9f9a9144f5368d1e02091bddd0307b058ec3",
+ },
+ Blobdata: &api.Block_Bitcoin{
+ Bitcoin: &api.BitcoinBlobdata{
+ Header: header,
+ InputTransactions: []*api.RepeatedBytes{
+ {Data: [][]byte{}},
+ {Data: [][]byte{tx1, tx2}},
+ },
+ },
+ },
+ }
+}
diff --git a/sdk/client_test.go b/sdk/client_test.go
index 41b645da..3a10e8d5 100644
--- a/sdk/client_test.go
+++ b/sdk/client_test.go
@@ -1,7 +1,11 @@
package sdk
import (
+ "bytes"
"context"
+ "io"
+ "net/http"
+ "net/http/httptest"
"testing"
"time"
@@ -11,6 +15,7 @@ import (
"golang.org/x/xerrors"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
+ "google.golang.org/protobuf/proto"
"github.com/coinbase/chainstorage/internal/blockchain/parser"
"github.com/coinbase/chainstorage/internal/config"
@@ -112,6 +117,130 @@ func (s *clientTestSuite) TestGetBlockWithTag() {
s.require.Equal(block, fetchedBlock)
}
+func (s *clientTestSuite) TestGetBlockWithTagAndReadSource() {
+ const (
+ tag = uint32(2)
+ height = uint64(12345)
+ hash = "0xabcde"
+ )
+
+ block := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+ blockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ }
+ s.downloaderClient.EXPECT().Download(gomock.Any(), blockFile).Return(block, nil)
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ }).Return(&api.GetBlockFileResponse{
+ File: blockFile,
+ }, nil)
+
+ readSourceClient, ok := s.client.(ReadSourceClient)
+ s.require.True(ok)
+ fetchedBlock, err := readSourceClient.GetBlockWithTagAndReadSource(context.Background(), tag, height, hash, api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED)
+ s.require.NoError(err)
+ s.require.Equal(block, fetchedBlock)
+}
+
+func (s *clientTestSuite) TestGetBlockWithTagAndReadSource_FallsBackToSingleBlockOnDownloadError() {
+ const (
+ tag = uint32(2)
+ height = uint64(12345)
+ hash = "0xabcde"
+ )
+
+ block := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+ consolidatedBlockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ }
+ singleBlockBlockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ }
+
+ gomock.InOrder(
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ }).Return(&api.GetBlockFileResponse{
+ File: consolidatedBlockFile,
+ }, nil),
+ s.downloaderClient.EXPECT().Download(gomock.Any(), consolidatedBlockFile).Return(nil, xerrors.New("consolidated download failed")),
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ }).Return(&api.GetBlockFileResponse{
+ File: singleBlockBlockFile,
+ }, nil),
+ s.downloaderClient.EXPECT().Download(gomock.Any(), singleBlockBlockFile).Return(block, nil),
+ )
+
+ readSourceClient, ok := s.client.(ReadSourceClient)
+ s.require.True(ok)
+ fetchedBlock, err := readSourceClient.GetBlockWithTagAndReadSource(context.Background(), tag, height, hash, api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED)
+ s.require.NoError(err)
+ s.require.Equal(block, fetchedBlock)
+}
+
+func (s *clientTestSuite) TestGetBlockWithTag_DefaultFallsBackToSingleBlockOnDownloadError() {
+ const (
+ tag = uint32(2)
+ height = uint64(12345)
+ hash = "0xabcde"
+ )
+
+ block := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+ consolidatedBlockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ }
+ singleBlockBlockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ }
+
+ gomock.InOrder(
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT,
+ }).Return(&api.GetBlockFileResponse{
+ File: consolidatedBlockFile,
+ }, nil),
+ s.downloaderClient.EXPECT().Download(gomock.Any(), consolidatedBlockFile).Return(nil, xerrors.New("consolidated download failed")),
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ }).Return(&api.GetBlockFileResponse{
+ File: singleBlockBlockFile,
+ }, nil),
+ s.downloaderClient.EXPECT().Download(gomock.Any(), singleBlockBlockFile).Return(block, nil),
+ )
+
+ fetchedBlock, err := s.client.GetBlockWithTag(context.Background(), tag, height, hash)
+ s.require.NoError(err)
+ s.require.Equal(block, fetchedBlock)
+}
+
func (s *clientTestSuite) TestGetBlocksByRange() {
const (
tag = uint32(0)
@@ -129,8 +258,8 @@ func (s *clientTestSuite) TestGetBlocksByRange() {
Height: metadata.Height,
}
blockFiles[i] = blockFile
- s.downloaderClient.EXPECT().Download(gomock.Any(), blockFile).Return(blocks[i], nil)
}
+ s.downloaderClient.EXPECT().DownloadMany(gomock.Any(), blockFiles).Return(blocks, nil)
s.gatewayClient.EXPECT().GetBlockFilesByRange(gomock.Any(), &api.GetBlockFilesByRangeRequest{
StartHeight: startHeight,
EndHeight: endHeight,
@@ -161,8 +290,8 @@ func (s *clientTestSuite) TestGetBlocksByRangeWithTag() {
Height: metadata.Height,
}
blockFiles[i] = blockFile
- s.downloaderClient.EXPECT().Download(gomock.Any(), blockFile).Return(blocks[i], nil)
}
+ s.downloaderClient.EXPECT().DownloadMany(gomock.Any(), blockFiles).Return(blocks, nil)
s.gatewayClient.EXPECT().GetBlockFilesByRange(gomock.Any(), &api.GetBlockFilesByRangeRequest{
Tag: tag,
StartHeight: startHeight,
@@ -177,6 +306,207 @@ func (s *clientTestSuite) TestGetBlocksByRangeWithTag() {
s.require.Equal(blocks, fetchedBlocks)
}
+func (s *clientTestSuite) TestGetBlocksByRangeWithTagAndReadSource() {
+ const (
+ tag = uint32(2)
+ startHeight = uint64(12345)
+ endHeight = uint64(12350)
+ numBlocks = int(endHeight - startHeight)
+ )
+
+ blocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ blockFiles := make([]*api.BlockFile, numBlocks)
+ for i := range blockFiles {
+ metadata := blocks[i].Metadata
+ blockFile := &api.BlockFile{
+ Tag: metadata.Tag,
+ Height: metadata.Height,
+ }
+ blockFiles[i] = blockFile
+ }
+ s.downloaderClient.EXPECT().DownloadMany(gomock.Any(), blockFiles).Return(blocks, nil)
+ s.gatewayClient.EXPECT().GetBlockFilesByRange(gomock.Any(), &api.GetBlockFilesByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ }).Return(&api.GetBlockFilesByRangeResponse{
+ Files: blockFiles,
+ }, nil)
+
+ readSourceClient, ok := s.client.(ReadSourceClient)
+ s.require.True(ok)
+ fetchedBlocks, err := readSourceClient.GetBlocksByRangeWithTagAndReadSource(context.Background(), tag, startHeight, endHeight, api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED)
+ s.require.NoError(err)
+ s.require.Equal(numBlocks, len(fetchedBlocks))
+ s.require.Equal(blocks, fetchedBlocks)
+}
+
+func (s *clientTestSuite) TestGetBlocksByRangeWithTagAndReadSource_FallsBackToSingleBlockOnDownloadError() {
+ const (
+ tag = uint32(2)
+ startHeight = uint64(12345)
+ endHeight = uint64(12350)
+ numBlocks = int(endHeight - startHeight)
+ )
+
+ blocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ consolidatedBlockFiles := make([]*api.BlockFile, numBlocks)
+ singleBlockBlockFiles := make([]*api.BlockFile, numBlocks)
+ for i := range blocks {
+ metadata := blocks[i].Metadata
+ consolidatedBlockFiles[i] = &api.BlockFile{
+ Tag: metadata.Tag,
+ Height: metadata.Height,
+ Hash: metadata.Hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ }
+ singleBlockBlockFiles[i] = &api.BlockFile{
+ Tag: metadata.Tag,
+ Height: metadata.Height,
+ Hash: metadata.Hash,
+ }
+ }
+
+ gomock.InOrder(
+ s.gatewayClient.EXPECT().GetBlockFilesByRange(gomock.Any(), &api.GetBlockFilesByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED,
+ }).Return(&api.GetBlockFilesByRangeResponse{
+ Files: consolidatedBlockFiles,
+ }, nil),
+ s.downloaderClient.EXPECT().DownloadMany(gomock.Any(), consolidatedBlockFiles).Return(nil, xerrors.New("consolidated download failed")),
+ s.gatewayClient.EXPECT().GetBlockFilesByRange(gomock.Any(), &api.GetBlockFilesByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ }).Return(&api.GetBlockFilesByRangeResponse{
+ Files: singleBlockBlockFiles,
+ }, nil),
+ s.downloaderClient.EXPECT().DownloadMany(gomock.Any(), singleBlockBlockFiles).Return(blocks, nil),
+ )
+
+ readSourceClient, ok := s.client.(ReadSourceClient)
+ s.require.True(ok)
+ fetchedBlocks, err := readSourceClient.GetBlocksByRangeWithTagAndReadSource(context.Background(), tag, startHeight, endHeight, api.BlockReadSource_BLOCK_READ_SOURCE_CONSOLIDATED)
+ s.require.NoError(err)
+ s.require.Equal(numBlocks, len(fetchedBlocks))
+ s.require.Equal(blocks, fetchedBlocks)
+}
+
+func (s *clientTestSuite) TestGetBlocksByRangeWithTag_DefaultFallsBackToSingleBlockOnDownloadError() {
+ const (
+ tag = uint32(2)
+ startHeight = uint64(12345)
+ endHeight = uint64(12350)
+ numBlocks = int(endHeight - startHeight)
+ )
+
+ blocks := testutil.MakeBlocksFromStartHeight(startHeight, numBlocks, tag)
+ consolidatedBlockFiles := make([]*api.BlockFile, numBlocks)
+ singleBlockBlockFiles := make([]*api.BlockFile, numBlocks)
+ for i := range blocks {
+ metadata := blocks[i].Metadata
+ consolidatedBlockFiles[i] = &api.BlockFile{
+ Tag: metadata.Tag,
+ Height: metadata.Height,
+ Hash: metadata.Hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ }
+ singleBlockBlockFiles[i] = &api.BlockFile{
+ Tag: metadata.Tag,
+ Height: metadata.Height,
+ Hash: metadata.Hash,
+ }
+ }
+
+ gomock.InOrder(
+ s.gatewayClient.EXPECT().GetBlockFilesByRange(gomock.Any(), &api.GetBlockFilesByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT,
+ }).Return(&api.GetBlockFilesByRangeResponse{
+ Files: consolidatedBlockFiles,
+ }, nil),
+ s.downloaderClient.EXPECT().DownloadMany(gomock.Any(), consolidatedBlockFiles).Return(nil, xerrors.New("consolidated download failed")),
+ s.gatewayClient.EXPECT().GetBlockFilesByRange(gomock.Any(), &api.GetBlockFilesByRangeRequest{
+ Tag: tag,
+ StartHeight: startHeight,
+ EndHeight: endHeight,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ }).Return(&api.GetBlockFilesByRangeResponse{
+ Files: singleBlockBlockFiles,
+ }, nil),
+ s.downloaderClient.EXPECT().DownloadMany(gomock.Any(), singleBlockBlockFiles).Return(blocks, nil),
+ )
+
+ fetchedBlocks, err := s.client.GetBlocksByRangeWithTag(context.Background(), tag, startHeight, endHeight)
+ s.require.NoError(err)
+ s.require.Equal(numBlocks, len(fetchedBlocks))
+ s.require.Equal(blocks, fetchedBlocks)
+}
+
+func TestClientOpenRawBlockPayloadWithTag(t *testing.T) {
+ require := testutil.Require(t)
+ ctrl := gomock.NewController(t)
+ defer ctrl.Finish()
+
+ const (
+ tag = uint32(2)
+ height = uint64(12345)
+ hash = "0xabcde"
+ )
+ block := testutil.MakeBlock(height, tag)
+ blockBytes, err := proto.Marshal(block)
+ require.NoError(err)
+
+ server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
+ require.Equal(http.MethodGet, request.Method)
+ _, _ = writer.Write(blockBytes)
+ }))
+ defer server.Close()
+
+ gatewayClient := apimocks.NewMockChainStorageClient(ctrl)
+ blockFile := &api.BlockFile{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ FileUrl: server.URL,
+ }
+ gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ }).Return(&api.GetBlockFileResponse{
+ File: blockFile,
+ }, nil)
+
+ var client Client
+ app := testapp.New(
+ t,
+ Module,
+ parser.Module,
+ fx.Provide(func() downloader.HTTPClient { return server.Client() }),
+ fx.Provide(downloader.NewBlockDownloader),
+ fx.Provide(func() gateway.Client { return gatewayClient }),
+ fx.Populate(&client),
+ )
+ defer app.Close()
+
+ payload, err := client.OpenRawBlockPayloadWithTag(context.Background(), tag, height, hash)
+ require.NoError(err)
+ defer payload.Close()
+ require.Equal(blockFile, payload.BlockFile)
+
+ got, err := io.ReadAll(payload)
+ require.NoError(err)
+ require.Equal(blockBytes, got)
+}
+
func (s *clientTestSuite) TestStreamBlocks() {
s.gatewayClient.EXPECT().StreamChainEvents(gomock.Any(), gomock.Any()).Return(s.streamClient, nil)
@@ -223,6 +553,69 @@ func (s *clientTestSuite) TestStreamBlocks() {
s.require.Equal(numberOfEvents, count)
}
+func (s *clientTestSuite) TestStreamBlocks_DefaultFallsBackToSingleBlockOnDownloadError() {
+ const (
+ tag = uint32(2)
+ height = uint64(12345)
+ hash = "0xabcde"
+ )
+
+ block := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+ event := &api.BlockchainEvent{
+ Block: &api.BlockIdentifier{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ },
+ }
+ consolidatedBlockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ }
+ singleBlockBlockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ }
+
+ s.gatewayClient.EXPECT().StreamChainEvents(gomock.Any(), gomock.Any()).Return(s.streamClient, nil)
+ gomock.InOrder(
+ s.streamClient.EXPECT().Recv().Return(&api.ChainEventsResponse{Event: event}, nil),
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT,
+ }).Return(&api.GetBlockFileResponse{
+ File: consolidatedBlockFile,
+ }, nil),
+ s.downloaderClient.EXPECT().Download(gomock.Any(), consolidatedBlockFile).Return(nil, xerrors.New("consolidated download failed")),
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ }).Return(&api.GetBlockFileResponse{
+ File: singleBlockBlockFile,
+ }, nil),
+ s.downloaderClient.EXPECT().Download(gomock.Any(), singleBlockBlockFile).Return(block, nil),
+ )
+
+ ch, err := s.client.StreamChainEvents(context.Background(), StreamingConfiguration{
+ ChainEventsRequest: &api.ChainEventsRequest{},
+ NumberOfEvents: 1,
+ })
+ s.require.NoError(err)
+
+ result := <-ch
+ s.require.NotNil(result)
+ s.require.NoError(result.Error)
+ s.require.Equal(block, result.Block)
+ s.require.Equal(event, result.BlockchainEvent)
+}
+
func (s *clientTestSuite) TestStreamBlocks_EventOnly() {
s.gatewayClient.EXPECT().StreamChainEvents(gomock.Any(), gomock.Any()).Return(s.streamClient, nil)
@@ -333,6 +726,117 @@ func (s *clientTestSuite) TestStreamBlocks_ErrStreamChainEvents() {
s.require.Nil(ch)
}
+func (s *clientTestSuite) TestStreamNativeBlock_EthereumReturnsNilAccessors() {
+ const (
+ tag = uint32(2)
+ height = uint64(12345)
+ hash = "0xabcde"
+ )
+ bf := &api.BlockFile{Tag: tag, Height: height, Hash: hash, Skipped: true}
+
+ spooled := &downloader.SpooledBlock{
+ BlockFile: bf,
+ Open: func() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(nil)), nil
+ },
+ }
+
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), gomock.Any()).Return(&api.GetBlockFileResponse{File: bf}, nil)
+ s.downloaderClient.EXPECT().DownloadStream(gomock.Any(), bf).Return(spooled, nil)
+
+ // Default config is ethereum. StreamNativeBlock returns a stream
+ // but GetBitcoin() is nil (native parser does not implement
+ // BitcoinStreamer). GetEthereum() is also nil today (no walker).
+ native, err := s.client.StreamNativeBlock(context.Background(), tag, height, hash)
+ s.require.NoError(err)
+ defer native.Close()
+ s.require.Nil(native.GetBitcoin(), "ethereum config must not populate GetBitcoin()")
+ s.require.Nil(native.GetEthereum(), "no ethereum streaming walker exists yet")
+}
+
+func (s *clientTestSuite) TestStreamNativeBlock_DefaultFallsBackToSingleBlockOnDownloadError() {
+ const (
+ tag = uint32(2)
+ height = uint64(12345)
+ hash = "0xabcde"
+ )
+ consolidatedBlockFile := &api.BlockFile{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ }
+ singleBlockBlockFile := &api.BlockFile{Tag: tag, Height: height, Hash: hash}
+ spooled := &downloader.SpooledBlock{
+ BlockFile: singleBlockBlockFile,
+ Open: func() (io.ReadCloser, error) {
+ return io.NopCloser(bytes.NewReader(nil)), nil
+ },
+ }
+
+ gomock.InOrder(
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT,
+ }).Return(&api.GetBlockFileResponse{File: consolidatedBlockFile}, nil),
+ s.downloaderClient.EXPECT().DownloadStream(gomock.Any(), consolidatedBlockFile).Return(nil, xerrors.New("consolidated stream download failed")),
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ }).Return(&api.GetBlockFileResponse{File: singleBlockBlockFile}, nil),
+ s.downloaderClient.EXPECT().DownloadStream(gomock.Any(), singleBlockBlockFile).Return(spooled, nil),
+ )
+
+ native, err := s.client.StreamNativeBlock(context.Background(), tag, height, hash)
+ s.require.NoError(err)
+ defer native.Close()
+ s.require.Nil(native.GetBitcoin(), "ethereum config must not populate GetBitcoin()")
+ s.require.Nil(native.GetEthereum(), "no ethereum streaming walker exists yet")
+}
+
+func (s *clientTestSuite) TestStreamNativeBlock_DownloadError() {
+ const (
+ tag = uint32(2)
+ height = uint64(12345)
+ hash = "0xabcde"
+ )
+ consolidatedBlockFile := &api.BlockFile{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ }
+ singleBlockBlockFile := &api.BlockFile{Tag: tag, Height: height, Hash: hash}
+
+ sentinel := xerrors.New("download exploded")
+ fallbackSentinel := xerrors.New("single-block download exploded")
+ gomock.InOrder(
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT,
+ }).Return(&api.GetBlockFileResponse{File: consolidatedBlockFile}, nil),
+ s.downloaderClient.EXPECT().DownloadStream(gomock.Any(), consolidatedBlockFile).Return(nil, sentinel),
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ }).Return(&api.GetBlockFileResponse{File: singleBlockBlockFile}, nil),
+ s.downloaderClient.EXPECT().DownloadStream(gomock.Any(), singleBlockBlockFile).Return(nil, fallbackSentinel),
+ )
+
+ view, err := s.client.StreamNativeBlock(context.Background(), tag, height, hash)
+ s.require.ErrorIs(err, fallbackSentinel)
+ s.require.Contains(err.Error(), sentinel.Error())
+ s.require.Nil(view)
+}
+
func (s *clientTestSuite) TestStreamBlocks_ErrRecv() {
mockError := xerrors.New("some mock error")
s.gatewayClient.EXPECT().StreamChainEvents(gomock.Any(), gomock.Any()).Return(s.streamClient, nil)
@@ -598,3 +1102,111 @@ func (s *clientTestSuite) TestGetStaticChainMetadata() {
s.require.Equal(uint64(10), resp.IrreversibleDistance)
s.require.Equal("3s", resp.BlockTime)
}
+
+func (s *clientTestSuite) TestGetBlockByTransaction_DefaultFallsBackToSingleBlockOnDownloadError() {
+ const (
+ tag = uint32(2)
+ height = uint64(12345)
+ hash = "0xabcde"
+ transactionHash = "0xtx"
+ )
+
+ block := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+ consolidatedBlockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ }
+ singleBlockBlockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ }
+
+ gomock.InOrder(
+ s.gatewayClient.EXPECT().GetBlockByTransaction(gomock.Any(), &api.GetBlockByTransactionRequest{
+ Tag: tag,
+ TransactionHash: transactionHash,
+ }).Return(&api.GetBlockByTransactionResponse{
+ Blocks: []*api.BlockIdentifier{{Tag: tag, Height: height, Hash: hash}},
+ }, nil),
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT,
+ }).Return(&api.GetBlockFileResponse{
+ File: consolidatedBlockFile,
+ }, nil),
+ s.downloaderClient.EXPECT().Download(gomock.Any(), consolidatedBlockFile).Return(nil, xerrors.New("consolidated download failed")),
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ }).Return(&api.GetBlockFileResponse{
+ File: singleBlockBlockFile,
+ }, nil),
+ s.downloaderClient.EXPECT().Download(gomock.Any(), singleBlockBlockFile).Return(block, nil),
+ )
+
+ blocks, err := s.client.GetBlockByTransaction(context.Background(), tag, transactionHash)
+ s.require.NoError(err)
+ s.require.Equal([]*api.Block{block}, blocks)
+}
+
+func (s *clientTestSuite) TestGetBlockByTimestamp_DefaultFallsBackToSingleBlockOnDownloadError() {
+ const (
+ tag = uint32(2)
+ height = uint64(12345)
+ hash = "0xabcde"
+ timestamp = uint64(123456789)
+ )
+
+ block := testutil.MakeBlocksFromStartHeight(height, 1, tag)[0]
+ consolidatedBlockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ ObjectFormat: api.BlockObjectFormat_BLOCK_OBJECT_FORMAT_CSCB_BATCH,
+ }
+ singleBlockBlockFile := &api.BlockFile{
+ Tag: block.Metadata.Tag,
+ Height: block.Metadata.Height,
+ Hash: block.Metadata.Hash,
+ }
+
+ gomock.InOrder(
+ s.gatewayClient.EXPECT().GetBlockByTimestamp(gomock.Any(), &api.GetBlockByTimestampRequest{
+ Tag: tag,
+ Timestamp: timestamp,
+ }).Return(&api.GetBlockByTimestampResponse{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ }, nil),
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_DEFAULT,
+ }).Return(&api.GetBlockFileResponse{
+ File: consolidatedBlockFile,
+ }, nil),
+ s.downloaderClient.EXPECT().Download(gomock.Any(), consolidatedBlockFile).Return(nil, xerrors.New("consolidated download failed")),
+ s.gatewayClient.EXPECT().GetBlockFile(gomock.Any(), &api.GetBlockFileRequest{
+ Tag: tag,
+ Height: height,
+ Hash: hash,
+ ReadSource: api.BlockReadSource_BLOCK_READ_SOURCE_SINGLE_BLOCK,
+ }).Return(&api.GetBlockFileResponse{
+ File: singleBlockBlockFile,
+ }, nil),
+ s.downloaderClient.EXPECT().Download(gomock.Any(), singleBlockBlockFile).Return(block, nil),
+ )
+
+ got, err := s.client.GetBlockByTimestamp(context.Background(), tag, timestamp)
+ s.require.NoError(err)
+ s.require.Equal(block, got)
+}
diff --git a/sdk/mocks/mocks.go b/sdk/mocks/mocks.go
index a9da86be..4f9cd0d4 100644
--- a/sdk/mocks/mocks.go
+++ b/sdk/mocks/mocks.go
@@ -56,6 +56,21 @@ func (mr *MockClientMockRecorder) GetBlock(arg0, arg1, arg2 any) *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlock", reflect.TypeOf((*MockClient)(nil).GetBlock), arg0, arg1, arg2)
}
+// GetBlockByTimestamp mocks base method.
+func (m *MockClient) GetBlockByTimestamp(arg0 context.Context, arg1 uint32, arg2 uint64) (*chainstorage.Block, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "GetBlockByTimestamp", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*chainstorage.Block)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// GetBlockByTimestamp indicates an expected call of GetBlockByTimestamp.
+func (mr *MockClientMockRecorder) GetBlockByTimestamp(arg0, arg1, arg2 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByTimestamp", reflect.TypeOf((*MockClient)(nil).GetBlockByTimestamp), arg0, arg1, arg2)
+}
+
// GetBlockByTransaction mocks base method.
func (m *MockClient) GetBlockByTransaction(arg0 context.Context, arg1 uint32, arg2 string) ([]*chainstorage.Block, error) {
m.ctrl.T.Helper()
@@ -233,6 +248,66 @@ func (mr *MockClientMockRecorder) GetTag() *gomock.Call {
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTag", reflect.TypeOf((*MockClient)(nil).GetTag))
}
+// OpenRawBlockPayload mocks base method.
+func (m *MockClient) OpenRawBlockPayload(arg0 context.Context, arg1 uint64, arg2 string) (*sdk.RawBlockPayload, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "OpenRawBlockPayload", arg0, arg1, arg2)
+ ret0, _ := ret[0].(*sdk.RawBlockPayload)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// OpenRawBlockPayload indicates an expected call of OpenRawBlockPayload.
+func (mr *MockClientMockRecorder) OpenRawBlockPayload(arg0, arg1, arg2 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenRawBlockPayload", reflect.TypeOf((*MockClient)(nil).OpenRawBlockPayload), arg0, arg1, arg2)
+}
+
+// OpenRawBlockPayloadWithTag mocks base method.
+func (m *MockClient) OpenRawBlockPayloadWithTag(arg0 context.Context, arg1 uint32, arg2 uint64, arg3 string) (*sdk.RawBlockPayload, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "OpenRawBlockPayloadWithTag", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(*sdk.RawBlockPayload)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// OpenRawBlockPayloadWithTag indicates an expected call of OpenRawBlockPayloadWithTag.
+func (mr *MockClientMockRecorder) OpenRawBlockPayloadWithTag(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenRawBlockPayloadWithTag", reflect.TypeOf((*MockClient)(nil).OpenRawBlockPayloadWithTag), arg0, arg1, arg2, arg3)
+}
+
+// OpenRawBlockPayloadsByRange mocks base method.
+func (m *MockClient) OpenRawBlockPayloadsByRange(arg0 context.Context, arg1, arg2 uint64) (sdk.RawBlockPayloadIterator, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "OpenRawBlockPayloadsByRange", arg0, arg1, arg2)
+ ret0, _ := ret[0].(sdk.RawBlockPayloadIterator)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// OpenRawBlockPayloadsByRange indicates an expected call of OpenRawBlockPayloadsByRange.
+func (mr *MockClientMockRecorder) OpenRawBlockPayloadsByRange(arg0, arg1, arg2 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenRawBlockPayloadsByRange", reflect.TypeOf((*MockClient)(nil).OpenRawBlockPayloadsByRange), arg0, arg1, arg2)
+}
+
+// OpenRawBlockPayloadsByRangeWithTag mocks base method.
+func (m *MockClient) OpenRawBlockPayloadsByRangeWithTag(arg0 context.Context, arg1 uint32, arg2, arg3 uint64) (sdk.RawBlockPayloadIterator, error) {
+ m.ctrl.T.Helper()
+ ret := m.ctrl.Call(m, "OpenRawBlockPayloadsByRangeWithTag", arg0, arg1, arg2, arg3)
+ ret0, _ := ret[0].(sdk.RawBlockPayloadIterator)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// OpenRawBlockPayloadsByRangeWithTag indicates an expected call of OpenRawBlockPayloadsByRangeWithTag.
+func (mr *MockClientMockRecorder) OpenRawBlockPayloadsByRangeWithTag(arg0, arg1, arg2, arg3 any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "OpenRawBlockPayloadsByRangeWithTag", reflect.TypeOf((*MockClient)(nil).OpenRawBlockPayloadsByRangeWithTag), arg0, arg1, arg2, arg3)
+}
+
// SetBlockValidation mocks base method.
func (m *MockClient) SetBlockValidation(arg0 bool) {
m.ctrl.T.Helper()
@@ -284,6 +359,26 @@ func (mr *MockClientMockRecorder) StreamChainEvents(arg0, arg1 any) *gomock.Call
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamChainEvents", reflect.TypeOf((*MockClient)(nil).StreamChainEvents), arg0, arg1)
}
+// StreamNativeBlock mocks base method.
+func (m *MockClient) StreamNativeBlock(arg0 context.Context, arg1 uint32, arg2 uint64, arg3 string, arg4 ...sdk.ParseOption) (sdk.NativeStreamedBlock, error) {
+ m.ctrl.T.Helper()
+ varargs := []any{arg0, arg1, arg2, arg3}
+ for _, a := range arg4 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "StreamNativeBlock", varargs...)
+ ret0, _ := ret[0].(sdk.NativeStreamedBlock)
+ ret1, _ := ret[1].(error)
+ return ret0, ret1
+}
+
+// StreamNativeBlock indicates an expected call of StreamNativeBlock.
+func (mr *MockClientMockRecorder) StreamNativeBlock(arg0, arg1, arg2, arg3 any, arg4 ...any) *gomock.Call {
+ mr.mock.ctrl.T.Helper()
+ varargs := append([]any{arg0, arg1, arg2, arg3}, arg4...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StreamNativeBlock", reflect.TypeOf((*MockClient)(nil).StreamNativeBlock), varargs...)
+}
+
// MockParser is a mock of Parser interface.
type MockParser struct {
ctrl *gomock.Controller
@@ -323,18 +418,23 @@ func (mr *MockParserMockRecorder) GetNativeTransaction(arg0, arg1, arg2 any) *go
}
// ParseNativeBlock mocks base method.
-func (m *MockParser) ParseNativeBlock(arg0 context.Context, arg1 *chainstorage.Block) (*chainstorage.NativeBlock, error) {
+func (m *MockParser) ParseNativeBlock(arg0 context.Context, arg1 *chainstorage.Block, arg2 ...sdk.ParseOption) (*chainstorage.NativeBlock, error) {
m.ctrl.T.Helper()
- ret := m.ctrl.Call(m, "ParseNativeBlock", arg0, arg1)
+ varargs := []any{arg0, arg1}
+ for _, a := range arg2 {
+ varargs = append(varargs, a)
+ }
+ ret := m.ctrl.Call(m, "ParseNativeBlock", varargs...)
ret0, _ := ret[0].(*chainstorage.NativeBlock)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// ParseNativeBlock indicates an expected call of ParseNativeBlock.
-func (mr *MockParserMockRecorder) ParseNativeBlock(arg0, arg1 any) *gomock.Call {
+func (mr *MockParserMockRecorder) ParseNativeBlock(arg0, arg1 any, arg2 ...any) *gomock.Call {
mr.mock.ctrl.T.Helper()
- return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseNativeBlock", reflect.TypeOf((*MockParser)(nil).ParseNativeBlock), arg0, arg1)
+ varargs := append([]any{arg0, arg1}, arg2...)
+ return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ParseNativeBlock", reflect.TypeOf((*MockParser)(nil).ParseNativeBlock), varargs...)
}
// ParseRosettaBlock mocks base method.
diff --git a/sdk/parser.go b/sdk/parser.go
index 3ab012d4..3d7da4ec 100644
--- a/sdk/parser.go
+++ b/sdk/parser.go
@@ -3,11 +3,55 @@ package sdk
import (
"context"
+ "github.com/coinbase/chainstorage/internal/blockchain/parser"
api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
)
+// ParseOption is re-exported from the parser package so SDK consumers
+// can skip fields they will not consume (e.g. bitcoin scripts,
+// witnesses, zcash shielded data). See WithSkip* helpers.
+type ParseOption = parser.ParseOption
+
+// NativeStreamedBlock is the chain-agnostic view returned by
+// Client.StreamNativeBlock. It exposes block metadata plus a Close
+// method that releases backing resources (disk spool). Chain-specific
+// iteration is available via nil-checkable accessors that mirror
+// *api.NativeBlock's oneof shape.
+//
+// Consumer pattern parallels GetBlock + ParseNativeBlock:
+//
+// native, err := client.StreamNativeBlock(ctx, tag, height, hash)
+// if err != nil { return err }
+// defer native.Close()
+//
+// if bs := native.GetBitcoin(); bs != nil {
+// for tx, err := range bs.Transactions() { ... }
+// header, _ := bs.Header()
+// }
+type NativeStreamedBlock = parser.NativeStreamedBlock
+
+// BitcoinNativeStream is the bitcoin-family iterator view. Obtained
+// from NativeStreamedBlock.GetBitcoin() when the configured chain is
+// bitcoin-family (bitcoin, bitcoincash, litecoin, dash, zcash).
+type BitcoinNativeStream = parser.BitcoinNativeStream
+
+// EthereumNativeStream is the ethereum iterator view. Stub interface
+// until the ethereum streaming walker lands; GetEthereum() always
+// returns nil today.
+type EthereumNativeStream = parser.EthereumNativeStream
+
+var (
+ WithSkipScripts = parser.WithSkipScripts
+ WithSkipWitnesses = parser.WithSkipWitnesses
+ WithSkipShielded = parser.WithSkipShielded
+)
+
+// Parser is the chain-agnostic parsing surface for SDK consumers.
+// Streaming entry points live on Client (which owns the download
+// lifecycle); this interface handles already-materialized *api.Block
+// values.
type Parser interface {
- ParseNativeBlock(ctx context.Context, rawBlock *api.Block) (*api.NativeBlock, error)
+ ParseNativeBlock(ctx context.Context, rawBlock *api.Block, opts ...ParseOption) (*api.NativeBlock, error)
GetNativeTransaction(ctx context.Context, nativeBlock *api.NativeBlock, transactionHash string) (*api.NativeTransaction, error)
ParseRosettaBlock(ctx context.Context, rawBlock *api.Block) (*api.RosettaBlock, error)
ValidateBlock(ctx context.Context, nativeBlock *api.NativeBlock) error
diff --git a/sdk/raw_payload.go b/sdk/raw_payload.go
new file mode 100644
index 00000000..74cf897b
--- /dev/null
+++ b/sdk/raw_payload.go
@@ -0,0 +1,85 @@
+package sdk
+
+import (
+ "context"
+ "io"
+ "sync"
+
+ "golang.org/x/xerrors"
+
+ "github.com/coinbase/chainstorage/internal/storage/blobstorage/downloader"
+ api "github.com/coinbase/chainstorage/protos/coinbase/chainstorage"
+)
+
+type (
+ // RawBlockPayload is an open reader for one validated, decompressed
+ // protobuf-serialized api.Block payload. It deliberately does not unmarshal
+ // the payload. Callers must read it to EOF or close it to complete
+ // downloader-side validation and release resources.
+ RawBlockPayload struct {
+ BlockFile *api.BlockFile
+ Length uint64
+
+ reader io.ReadCloser
+ closeOnce sync.Once
+ }
+
+ // RawBlockPayloadIterator yields raw block payloads in height/range order
+ // and returns io.EOF when exhausted. Callers must close every returned
+ // payload and then close the iterator.
+ RawBlockPayloadIterator interface {
+ Next(ctx context.Context) (*RawBlockPayload, error)
+ Close() error
+ }
+
+ rawBlockPayloadIterator struct {
+ inner downloader.RawBlockPayloadIterator
+ }
+)
+
+func (p *RawBlockPayload) Read(buf []byte) (int, error) {
+ if p == nil || p.reader == nil {
+ return 0, xerrors.New("raw block payload is closed")
+ }
+ return p.reader.Read(buf)
+}
+
+func (p *RawBlockPayload) Close() error {
+ if p == nil {
+ return nil
+ }
+ var err error
+ p.closeOnce.Do(func() {
+ if p.reader != nil {
+ err = p.reader.Close()
+ }
+ p.reader = nil
+ })
+ return err
+}
+
+func (i *rawBlockPayloadIterator) Next(ctx context.Context) (*RawBlockPayload, error) {
+ payload, err := i.inner.Next(ctx)
+ if err != nil {
+ return nil, err
+ }
+ return wrapRawBlockPayload(payload), nil
+}
+
+func (i *rawBlockPayloadIterator) Close() error {
+ if i == nil || i.inner == nil {
+ return nil
+ }
+ return i.inner.Close()
+}
+
+func wrapRawBlockPayload(payload *downloader.RawBlockPayload) *RawBlockPayload {
+ if payload == nil {
+ return nil
+ }
+ return &RawBlockPayload{
+ BlockFile: payload.BlockFile,
+ Length: payload.Length,
+ reader: payload,
+ }
+}